Task Manager App - kendmaclean/pharo12 GitHub Wiki

Okay, let's create a different Spec2 application: a Simple Task Manager.

This app will allow users to:

  1. Add tasks with descriptions.
  2. View a list of tasks.
  3. Mark tasks as complete/incomplete using a checkbox directly in the list.
  4. Filter tasks (All, Pending, Completed).
  5. Remove tasks.

Step 1: Define the Data Model (Task)

Object << #Task
	slots: { #description . #isComplete };
	package: 'MyTaskManager'

Class-side convenience creator

Task class >> description: aString
    ^ self new
        description: aString;
        yourself

Add accessor methods, initialization, and a helper method:

"In class Task"

Task >> description
    ^ description

Task >> description: aString
    description := aString

Task >> isComplete
    ^ isComplete ifNil: [ false ] "Default to false if not set"

Task >> isComplete: aBoolean
    isComplete := aBoolean

Task >> toggleComplete
    self isComplete: self isComplete not.

Task >> initialize
    super initialize.
    description := ''.
    isComplete := false.

Task >> printOn: aStream
    super printOn: aStream.
    aStream
        nextPut: $(;
        nextPutAll: self description;
        nextPutAll: (self isComplete ifTrue: [' - Done'] ifFalse: [' - Pending']);
        nextPut: $)

Step 2: The Repository (TaskRepository)

Instance side:

Object << #TaskRepository
	slots: { #tasks };
	package: 'MyTaskManager'

Class side:

Object class << TaskRepository class
	slots: { #uniqueInstance }

TaskRepository class >> default
    "Return the singleton instance"
    ^ self uniqueInstance

TaskRepository class >> reset
    "Reset the singleton instance - useful for testing"
    uniqueInstance := nil

TaskRepository class >> uniqueInstance
    "Lazy initialization of the singleton instance"
    ^ uniqueInstance ifNil: [ uniqueInstance := self new ]

Instance side:

TaskRepository >> addTask: aTask
    tasks add: aTask.
    ^ aTask

TaskRepository >> initialize
    super initialize.
    tasks := OrderedCollection new.
    self loadSampleData.

TaskRepository >> loadSampleData
    "Only load sample data if repository is empty"
    tasks ifNotEmpty: [ ^ self ].
    
    self addTask: (Task description: 'Learn Pharo').
    self addTask: (Task description: 'Build Spec2 App').
    (tasks at: 2) isComplete: true.
    self addTask: (Task description: 'Drink Coffee').

TaskRepository >> removeTask: aTask
    tasks remove: aTask ifAbsent: [ ].
    ^ aTask

TaskRepository >> tasks
	^ tasks

Step 3: Create the Spec Application (TaskManagerApp)

SpPresenter << #TaskManagerApp
	slots: {
			 #displayedTasks .
			 #selectedTask .
			 #taskList .
			 #taskInputField .
			 #addButton .
			 #removeButton .
			 #filterAllRadio .
			 #filterPendingRadio .
			 #filterCompletedRadio .
			 #currentFilter };
	package: 'MyTaskManager'

Class side:

 TaskManagerApp class >> open
    <example>
    | inst |
    inst := self new.
    inst open title: 'Simple Task Manager'. "Set a window title"
    ^ inst

Instance side:

"In class TaskManagerApp"
TaskManagerApp >> addTask
    | description newTask |
    description := taskInputField text trimBoth.
    description ifEmpty: [ ^ self ]. "Do nothing if input is empty"
    
    newTask := Task description: description.
    self taskRepository addTask: newTask.
    
    taskInputField text: ''. "Clear input"
    taskInputField takeKeyboardFocus.
    
    self applyFilter. "Re-apply filter to show the new task"

TaskManagerApp >> applyFilter
    | previouslySelected allTasks |
    previouslySelected := selectedTask. "Remember selection"
    selectedTask := nil.
    removeButton disable.

    "Get tasks from repository"
    allTasks := self taskRepository tasks.
   
    displayedTasks removeAll.
    allTasks do: [ :task |
        	| shouldInclude all pending completed |
			all := (currentFilter == #all).
			pending := ( currentFilter == #pending and:  task isComplete not ).
			completed := ( currentFilter == #completed and:  task isComplete ).
		
       	shouldInclude := all
            or: [ pending 
            or: completed ].
            
       	shouldInclude ifTrue: [ 
            displayedTasks add: task ].
    ].
    
    taskList items: displayedTasks.
    
    "Try to restore selection if the item is still visible"
    (displayedTasks includes: previouslySelected) ifTrue: [
        taskList selectItem: previouslySelected.
    ].

TaskManagerApp >> connectPresenters
    "Connect List Selection"
    taskList whenSelectionChangedDo: [ :selection | 
        self onTaskSelection: selection ].
    
    "Connect Filter Changes"
    filterAllRadio  whenActivatedDo: [ 
		currentFilter := #all.
		self applyFilter ].
    filterPendingRadio  whenActivatedDo: [ 
		currentFilter := #pending.
		self applyFilter ].
    filterCompletedRadio whenActivatedDo: [ 
		currentFilter := #completed.
		self applyFilter ].
    
    "Connect Input Field Submission (Enter key)"
    taskInputField whenSubmitDo: [ :text | 
        self addTask ].

TaskManagerApp >> defaultLayout
    | filterLayout inputLayout buttonLayout |
    
    filterLayout := SpBoxLayout newHorizontal
        add: filterAllRadio;
        add: filterPendingRadio;
        add: filterCompletedRadio;
        spacing: 10;
        yourself.
    
    inputLayout := SpBoxLayout newHorizontal
        add: taskInputField expand: true fill: true padding: 0;
        add: addButton expand: false fill: false padding: 0;
        spacing: 5;
        yourself.
    
    buttonLayout := SpBoxLayout newHorizontal
        addLast: removeButton expand: false;
        yourself.
    
    ^ SpBoxLayout newVertical
        add: filterLayout height: self class toolbarHeight;
        add: taskList expand: true fill: true padding: 0;
        add: inputLayout height: self class inputTextHeight;
        add: buttonLayout height: self class buttonHeight;
        spacing: 5;
        yourself

TaskManagerApp >> initialize
    super initialize.
    
    "Initialize filtered task collection"
    displayedTasks := OrderedCollection new.
    currentFilter := #all. "Initial filter state"

    self applyFilter.


TaskManagerApp >> initializePresenters
    "Task List (Table)"
    taskList := self newTable.
    taskList
        addColumn: ((SpCheckBoxTableColumn 
            title: 'Done' 
            evaluated: [ :task | task isComplete ]) 
                onActivation: [ :task | self toggleTaskStatus: task ];
                onDeactivation: [ :task | self toggleTaskStatus: task ];
                yourself);
        addColumn: (SpStringTableColumn 
            title: 'Description' 
            evaluated: [ :task | task description ]).
    
    "Input Field"
    taskInputField := self newTextInput
        placeholder: 'Enter new task description';
        yourself.
    
    "Buttons"
    addButton := self newButton
        label: 'Add Task';
        icon: (self iconNamed: #smallAdd);
        action: [ self addTask ];
        yourself.
    
    removeButton := self newButton
        label: 'Remove Task';
        icon: (self iconNamed: #smallDelete);
        action: [ self removeSelectedTask ];
        disable;
        yourself.
    
    "Filter Radio Buttons"
    filterAllRadio := self newRadioButton label: 'All'.
    filterPendingRadio := self newRadioButton label: 'Pending'.
    filterCompletedRadio := self newRadioButton label: 'Completed'.

	 filterAllRadio associatedRadioButtons: { filterPendingRadio. filterCompletedRadio }.
	 
	 filterAllRadio state: true. "Set the default selection "

TaskManagerApp >> onTaskSelection: aSelectionMode
	"enable remove button when a task is selected"
    selectedTask := aSelectionMode selectedItem.
    selectedTask 
        ifNil: [ removeButton disable ]
        ifNotNil: [ removeButton enable ].

TaskManagerApp >> removeSelectedTask
    | confirmMessage |
    selectedTask ifNil: [ ^ self ].
    
    confirmMessage := 'Remove task "', selectedTask description, '"?'.
    (self confirm: confirmMessage) ifFalse: [ ^ self ].
    
    self taskRepository removeTask: selectedTask.
    selectedTask := nil.
    removeButton disable.
    
    self applyFilter. "Update the list"

TaskManagerApp >> taskRepository
    "Access the task repository singleton"
    ^ TaskRepository default

TaskManagerApp >> initializeWindow: aWindowPresenter
	aWindowPresenter title: 'Simple Task Manager'

TaskManagerApp >> toggleTaskStatus: aTask
    aTask ifNil: [ ^ self ].
    aTask toggleComplete.
    
    "Re-apply filter as task might need to be hidden/shown"
    self applyFilter.

Step 6: How to Run

Run: Open a Playground. Type TaskManagerApp open and "Do it" (Ctrl+D).

You should now have a working Task Manager application!

⚠️ **GitHub.com Fallback** ⚠️