Adding Callbacks to the Application¶
At this point, the task list manager still does very little. If you try running the code (as described in Starting the application), and interacting with any of the elements in the GUI (clicking on a button, choosing a menu command, and so on), then only the “not yet implemented” message is displayed. This section shows you how to remedy this situation, by adding callback functions to the task list manager.
Getting the application to respond to mouse events such as clicking on a button or choosing a menu command consists of two things:
For each gadget in the GUI, you need to specify which callbacks to use. There are several different types of callback, depending on the type of event for which you want to define behavior.
You need to define the callback functions themselves. These are the functions that are invoked when a particular callback type is detected, and are the functions you use to define the correct behavior for your application.
In addition, you need to set up the basic data structures that allow you to work with tasks in your application.
At this point, you may be wondering exactly what a callback is, and why they are used to respond to application events, rather than event handlers. If you have developed GUI applications using other development environments, you may be more used to writing event handlers that work for a whole class of objects, and discriminating on which instance of a class to work on at any one time by means of case statements.
Writing event handlers in this way can be cumbersome. It turns out to be much simpler to define a function that works only for a particular instance of a class, and then refer to this function when defining the class instance. This function is what is referred to as a callback. This makes the source code for your application much clearer and easier to write, and the only price you pay is that you have to specify a callback for each gadget when you define the gadget itself.
In fact, DUIM provides a complete protocol for defining and handling events of all descriptions. However, you only need to use this protocol if you are creating new classes of gadgets, for which you need to define the event behavior, or new classes of events (for example, support for different input devices or notification of low resources). If you are just using gadgets, then you only ever need to use callbacks.
Defining the underlying data structures for tasks¶
Before defining any real callbacks, it is time to consider how you can represent task lists, and the information contained in them. This is essential, not just for handling tasks within the application, but for saving task lists to disk, and loading them back into the application.
Add the code described in this section to task-list.dylan.
There are two basic kinds of object that you need to model: task lists
and tasks. A task list is a collection of one or more tasks. The best
way to represent these is by defining a <task-list> class and a
<task> class.
The definition of <task-list>, below, contains three slots:
task-list-tasks
This slot specifies a sequence of tasks that are contained in the task list. Each object in the sequence will be an instance of
<task>. The default for new task lists is an empty stretchy vector. An init-keyword has been specified so that this slot can be set when an instance of the class is initialized.
task-list-filename
This slot specifies the file on disk to which the task list has been saved, if it has been saved at all. The default for new task lists is
#f, since the task list has not yet been saved to disk. An init-keyword has been specified so that this slot can be set when an instance of the class is initialized.
task-list-modified?
The purpose for this slot is less obvious. It is useful to flag whether or not a task list has been modified so that, for instance, the Save command in the application can be disabled if the task list is unmodified. There is no init-keyword defined for this class, because you only ever want to use the supplied default value for new instances of
<task-list>.
define class <task-list> (<object>)
constant slot task-list-tasks = make(<stretchy-vector>),
init-keyword: tasks:;
slot task-list-filename :: false-or(<string>) = #f,
init-keyword: filename:;
slot task-list-modified? :: <boolean> = #f;
end class <task-list>;
Next, consider the information that needs to be encoded in each individual task. There are two pieces of information that need to be recorded:
The text of the task, which should be a string.
The priority, which should be one of high, medium, or low.
Priorities can be recorded using a constant, as shown below:
define constant <priority> = one-of(#"low", #"medium", #"high");
Notice that it is most straightforward to encode each priority as a
symbol. Later on, you will see how you can use as to convert each
symbol to a format that can be saved to disk and read back into the
application as a symbol.
The <task> class can then be defined as having two slots: one for the
task text itself, and another for the priority. Both have init-keywords
so that they can be specified when a new instance is created, and both
init-keywords are required; they must be specified whenever a task is
created.
define class <task> (<object>)
slot task-name :: <string>,
required-init-keyword: name:;
slot task-priority :: <priority>,
required-init-keyword: priority:;
end class <task>;
These three definitions are all that is needed to be able to represent tasks and task lists within the task list application.
In order to handle tasks effectively in the GUI of the task list
manager, some changes are necessary to the definition of the task-list
pane in the definition of <task-frame>. These changes are needed to
ensure that information about tasks is passed to the task-list pane
correctly. Make these changes to the existing definition in the file
frame.dylan.
In Gluing the final design together, the definition of task-list was
given as:
// definition of list
pane task-list (frame)
make (<list-box>, items: #(), lines: 15,
activate-callback: not-yet-implemented);
First, you need to ensure that the items passed to task-list are the
tasks in the <task-list> associated with the frame. Recall that a
frame-task-list slot was specified in the definition of <task-frame>
; this slot is used to hold the instance of <task-list> that is
associated with the <task-frame>. The sequence of tasks contained in
the associated frame-task-list can then be found using the
frame-task-list.task-list-tasks accessor. To display these tasks in
the task-list pane, the items: init-keyword needs to be set to the
value of this accessor:
items: frame.frame-task-list.task-list-tasks,
Next, you need to ensure that the label for each task in the task-list
pane is the text of the task itself. As described above, the text of any
task is stored in its task-name slot. In order to display this text as
the label for every item in the list box, you need to specify the
task-name slot as the gadget-label-key of the list box. A label key
is a function that is used to calculate the label of each item in a
gadget, and it can be specified using the label-key: init-keyword:
label-key: task-name,
This gives the following new definition for the task-list pane:
// definition of list
pane task-list (frame)
make (<list-box>,
items: frame.frame-task-list.task-list-tasks,
label-key: task-name,
lines: 15,
activate-callback: not-yet-implemented);
There is one final change that still needs to be made to this pane definition. This is described in Updating the user interface.
Specifying a callback in the definition of each gadget¶
As you have already seen when using the not-yet-implemented callback,
providing a callback for a gadget is just a matter of specifying another
keyword-value pair in the definition of the gadget. There are two ways
that you can specify the callback function to use.
If you wish, you can define the callback function inline, making the definition itself the value part of the keyword-value pair.
This can be useful for a simple callback function that you only need to invoke from a single callback type in a single pane. However, if several panes, or several types of callback, need to invoke the same callback function, you need to define the function explicitly in each gadget that uses it.
Alternatively, you can define a callback function explicitly in your application code, and then refer to it by name in the keyword-value pair.
This method is best for portability and reusability of your code, since the same callback function can be referred to by name in as many gadgets as you need to use it in, without having to redefine the callback function in each gadget. It can also lead to more readable source code. This technique is the one used throughout this example application.
As already mentioned, there are a number of different kinds of callback available, depending on the behavior that you want to specify, and the gadget for which you are defining a callback. When defining different callbacks for a gadget, you need to use a different init-keyword for each callback.
As you have already seen, by far the most common callback is the
activate callback. This type of callback is invoked when you activate
any instance of <action-gadget>. For buttons, the activate callback
is invoked when you click on the button. For menu commands, the activate
callback is invoked when you choose the command from the menu. The
activate callback is the callback that is used most in the task list
manager. You can specify an activate callback for any gadget using the
activate-callback: init-keyword. In addition, you have seen the
value-changed callback, which is invoked when the gadget-value has been
changed. You can specify this callback using the
value-changed-callback: init-keyword.
You have already defined a callback for all the gadgets in the GUI. All
you need to do now is replace the reference to not-yet-implemented
with the real function name that should get called when each gadget is
activated. Thus, to specify an activate callback for the Add task
button in the tool bar, redefine the button as follows in the definition
of the <task-frame> class:
pane add-button (frame)
make(<push-button>, label: "Add task",
activate-callback: frame-add-task);
You can use exactly the same callback in the new definition of
add-menu-button :
pane add-menu-button (frame)
make(<menu-button>, label: "Add...",
activate-callback: frame-add-task,
accelerator: make-keyboard-gesture
(#"a", #"control", #"shift"),
documentation: "Add a new task.");
Notice how both of these gadgets specify the same activate callback. This is because the Add command in the menu should perform exactly the same action as the Add task button in the tool bar.
At this point, redefine the callback for each gadget listed in the table below, making sure that you supply the same callback to those gadgets that perform the same functions.
The callback functions used in the Task List Manager
Gadget
Callback
open-menu-button
open-file
save-menu-button
save-file
save-as-menu-button
save-as-file
exit-menu-button
exit-task
add-menu-button
frame-add-task
remove-menu-button
frame-remove-task
about-menu-button
about-task
add-button
frame-add-task
remove-button
frame-remove-task
open-button
open-file
save-button
save-file
The following sections show you how to define the callbacks themselves. You will need to define other functions and methods, as well as the callback functions listed above. These other functions and methods are called by some of the callbacks themselves.
Defining the callbacks¶
This section shows you how to define the callbacks that are necessary in the task list manager, as well as any other associated functions and methods.
First, you will look at methods and functions that enable file handling in the task list manager; that is, functions and methods that let you save and load files into the application.
Next, you will look at methods and functions for adding and removing tasks from the task list.
Last, you will define a few additional methods that are necessary to update the GUI elegantly, when other operations are performed.
All the code discussed in this chapter is structured so that callbacks which affect the GUI do not also perform other tasks that are not related to the GUI. This helps to keep the design of the application clean, so that you can follow the code more easily, and is recommended for all GUI design. Separating GUI code and non-GUI code also lets you produce code that is more easily reusable, either in other parts of a developing application, or in completely different applications.
Handling files in the task list manager¶
To begin with, you will define the functions and methods that let you save files to disk and load them back into the task list manager. Once you have added these to your code, you will be able to save and reload your task lists into the application; this type of functionality is essential in even the most trivial application.
There are three methods and two functions necessary for handling files.
The methods handle GUI-specific operations involved in loading and
saving files. The functions deal with the basic task of saving data
structures to disk, and loading them from disk. Add the definitions of
the methods to frame.dylan, and the definitions of the functions to
task-list.dylan.
Each method is invoked as a callback in the definition of the
<task-frame> class:
open-fileThis method prompts the user to choose a filename, and then loads that file into the task list manager by calling the functionload-task-list. It is used as the activate callback for bothopen-button(on the application tool bar) andopen-menu-button(in the File menu of the application).save-fileThis method saves the task list currently loaded into the application to disk. It is used as the activate callback for bothsave-button(on the application tool bar) andsave-menu-button(in the File menu of the application).save-as-fileThis method saves the task list currently loaded into the application to disk, and prompts the user to supply a name. It is used as the activate callback forsave-as-menu-button(in the File menu of the application).
The following functions are called by the methods described above:
save-task-listThis function saves an instance of<task-list>to a named file. It is called bysave-as-file.load-task-listThis function takes the contents of a file on disk and converts it into an instance of<task-list>. It is called byopen-as-file.
The following sections present and explain the code for each of these methods and functions in turn.
The open-file method¶
The code for open-file is shown below. Add this code to frame.dylan.
define method open-file
(gadget :: <gadget>) => ()
let frame = sheet-frame(gadget);
let task-list = frame-task-list(frame);
let filename
= choose-file(frame: frame,
default: task-list.task-list-filename,
direction: #"input");
if (filename)
let task-list = load-task-list(filename);
if (task-list)
frame.frame-task-list := task-list;
refresh-task-frame(frame)
else
notify-user(format-to-string("Failed to open file %s", filename),
owner: frame)
end
end
end method open-file;
The method takes a gadget as an argument and returns no values. The
argument is the gadget that is used to invoke it, which in the case of
the task list manager means either open-menu-button (in the File
menu of the application) or open-button (on the tool bar). The
open-file method then sets three local variables:
frameThis contains the frame of which the gadget argument is a part. This is a simple way of identifying the main application frame.task-listThis contains the value of theframe-task-listslot for frame. This identifies the instance of<task-list>that is being used to hold the task list information currently loaded into the task list manager.filenameThis is the name of the file that is to be loaded into the task list manager, and the user is always prompted to supply it.
The method choose-file (a method provided by DUIM) is used to prompt
for a file to load. The portion of code that performs this task is
repeated here:
choose-file(frame: frame,
default: task-list.task-list-filename,
direction: #"input");
This method displays a standard file dialog box so that the user can
select a file on any disk connected to the host computer. For
open-file, you need to supply three arguments to choose-file : the
frame that owns the dialog, a default value to supply to the user, and
the direction of the interaction.
You need to supply a frame so that the system knows how to treat the frame correctly, with respect to the dialog box. Thus, while the dialog is displayed, the frame that owns it cannot be minimized, resized, or interacted with in any way; this is standard behavior for modal dialog boxes.
In this case, supplying a default value is useful in that it lets us
supply the filename for the currently loaded task list as a default
value. It determines this by examining the task-list-filename slot of
task-list (which, remember, is defined as a local variable and
represents the instance of <task-list> in use). If this slot has a
value, then it is offered as a default. (Note that if the currently
loaded task list has never been saved to disk, then this slot is #f,
and so no default is offered.)
The direction of interaction should also be specified when calling
choose-file, since the same generic function can be used to prompt
for a filename using a standard Open File dialog or a standard Save File
dialog. In this case, the direction is #"input", which indicates that
data is being read in (that is, Open File is used).
The rest of the open-file method deals with loading in the task list
information safely. It consists of two nested if statements as shown
below.
if (filename)
let task-list = load-task-list(filename);
if (task-list)
frame.frame-task-list := task-list;
refresh-task-frame(frame)
else
notify-user(format-to-string("Failed to open file %s", filename),
owner: frame)
end
end
The clause
if (filename)
...
end
is necessary to handle the case where the user cancels the Open file
dialog: on cancelling the dialog, the open-file method should return
silently with no side effects.
If a filename is supplied, then it is read from disk and converted into a format that is readable by the application, in the line that reads
let task-list = load-task-list(filename);
The function load-task-list is described in The load-task-list
function.
The clause
if (task-list)
...
else
...
end
is necessary to handle the case where the filename specified does not
contain data that can be interpreted by load-task-list. If
task-list cannot be assigned, then the else code is run. This calls
the function notify-user, which is a simple way to display a short
message to the user in a message box.
If task-list can be assigned (that is, the contents of the specified
file have been successfully read by load-task-list ), then two lines
of code are run. The line
frame.frame-task-list := task-list;
assigns the frame-task-list slot of frame to the value of task-list.
The line
refresh-task-frame(frame)
calls a method that refreshes the list of tasks displayed in the task
list manager, so that the contents of the newly loaded file are
correctly displayed on the screen. The method refresh-task-frame is
described in Updating the user interface.
The save-file method¶
The code for save-file is as follows:
define method save-file
(gadget :: <gadget>) => ()
let frame = sheet-frame(gadget);
let task-list = frame-task-list(frame);
save-as-file(gadget, filename: task-list.task-list-filename)
end method save-file;
Add this code to frame.dylan.
This method is very simple, in that it just calls the method
save-as-file, passing it a filename as an argument. The
save-as-file method then does the real work of updating the GUI and
calling the relevant code to save information to disk.
Just like the open-file method, save-file takes the gadget used to
invoke it as an argument and returns no values. In the case of the task
list manager the gadget is either open-menu-button (in the File menu
of the application) or open-button (on the tool bar). The save-file
method sets the following two local variables:
frameThe frame of which the gadget argument is a part, so that the main application frame can be identified.task-listThis contains the value of theframe-task-listslot forframe. This identifies the instance of<task-list>that needs to be saved to disk.
Note that similar local variables are used in the definition of
open-file.
The save-file method then calls save-as-file, passing it the
following two arguments:
The gadget that invoked
save-file.The filename associated with the instance of
<task-list>that needs to be saved to disk.
Notice that the second of these arguments may be #f, if the task list
has not previously been saved to disk.
The save-as-file method¶
The code for save-as-file is as follows:
define method save-as-file
(gadget :: <gadget>, #key filename) => ()
let frame = sheet-frame(gadget);
let task-list = frame-task-list(frame);
let filename
= filename
| choose-file(frame: frame,
default: task-list.task-list-filename,
direction: #"output");
if (filename)
if (save-task-list(task-list, filename: filename))
frame.frame-task-list := task-list;
refresh-task-frame(frame)
else
notify-user(format-to-string
("Failed to save file %s", filename),
owner: frame)
end
end
end method save-as-file;
Add this code to frame.dylan.
Like open-file and save-file, this method takes a gadget as an
argument and returns no values. This argument is the gadget which is
used to invoke it. In addition, an optional keyword argument, a
filename, can be passed.
This method is a little unusual; as well as being the activate callback
for the save-as-menu-button (the command File > Save As ), it is
also called by the save-file method.
When directly invoked as an activate callback, the
filenameargument is not passed tosave-as-file. Instead, the user is prompted to supply it. In addition, thegadgetissave-as-menu-button.When invoked by
save-file, afilenamemay be passed, if the associated task list has been saved before. In addition, the gadget is eithersave-buttonorsave-menu-button.
As with open-file, save-as-file sets three local variables:
frameThis is the frame containing the gadget passed as an argument.task-listThis contains the value of theframe-task-listslot forframe, and identifies the instance of<task-list>to be saved.filenameThe filename to which the instance of<task-list>is saved.
Unless filename is passed as an optional argument, the user is
prompted to supply a filename in which the task list information is to
be saved. As with open-file, the choose-file method is used to do
this. In fact, the call to choose-file here is identical to the call
to choose-file in open-file, with the exception of the direction
argument, which is set to #"output".
The rest of the save-as-file method deals with saving the task list
information safely. It is similar to the equivalent code in open-file,
and consists of two nested if statements as shown below.
if (filename)
if (save-task-list(task-list, filename: filename))
frame.frame-task-list := task-list;
refresh-task-frame(frame)
else
notify-user(format-to-string("Failed to save file %s", filename),
owner: frame)
end
end
As with open-file, the clause
if (filename)
...
end
is necessary in case the user cancels the Save file dialog: on
cancelling the dialog, save-as-file should fail silently with no side
effects.
The second if statement is more interesting. The body of the if
statement is like the body of the equivalent if statement in
open-file :
frame.frame-task-list := task-list;
refresh-task-frame(frame)
This sets the frame-task-list slot of frame and then calls
refresh-task-frame to ensure that the correct information is shown on
the screen.
Similarly, the body of the else clause warns that the task list could
not be saved, when the if condition does not return true:
notify-user(format-to-string("Failed to save file %s", filename),
owner: frame)
The interesting part of this if statement is the if condition
itself:
save-task-list(task-list, filename: filename)
As well as providing a test for whether the task list frame should be
updated, it actually performs the save operation, by calling the
function save-task-list with the required arguments.
The function save-task-list is described in The save-task-list
function and the method refresh-task-frame is described in
Updating the user interface.
The load-task-list function¶
The code for load-task-list is shown below. Because this function does
not use any DUIM code, it is described only briefly.
define function load-task-list
(filename :: <string>) => (task-list :: false-or(<task-list>))
let tasks = make(<stretchy-vector>);
block (return)
with-open-file (stream = filename, direction: #"input")
while (#t)
let name = read-line(stream, on-end-of-stream: #f);
unless (name) return() end;
let priority = read-line(stream, on-end-of-stream: #f);
unless (priority)
error("Unexpectedly missing priority!")
end;
let task = make(<task>, name: name,
priority: as(<symbol>, priority));
add!(tasks, task)
end
end
end;
make(<task-list>, tasks: tasks, filename: filename)
end function load-task-list;
Add this code to task-list.dylan.
The function load-task-list reads a file from disk and attempts to
convert its contents into an instance of <task-list>, which itself
contains any number of instances of <task>. It takes one argument,
the filename, and returns one value, the instance of <task-list>.
This function uses a generic function and a macro from the Streams library to read information from the file. For full information about this library, please refer to the I/O and Networks Library Reference.
The file format used by the task list manager is very simple, with each
element of a task occupying a single line in the file. Suppose
load-task-list is called on a file containing the following
information:
Wash the dog
medium
Video Men Behaving Badly
high
This would create an instance of <task-list> whose task-list-tasks
slot was a sequence of two instances of <task>.
The first
<task>would have atask-nameof “Wash the dog” and atask-priorityof#"medium".The second
<task>would have atask-nameof “Video Men Behaving Badly” and atask-priorityof#"high".
The task-list-filename slot of the <task-list> is the filename
itself. Note that the task-list-modified? slot of the <task-list> is
set to #f, reflecting the fact that the task list is loaded, but
unchanged. This does not have to be done explicitly by load-task-list,
since #f is the default value of this slot, as you can see from its
definition in Defining the underlying data structures for tasks.
The file is opened for reading using the with-open-file macro. It is
then read a line at a time, setting the local variables name and
priority with each alternate line. After successfully setting both
name and priority, an instance of <task> is created, and added to
the stretchy vector tasks using add!. When the end of the file is
reached, #f is returned and an instance of <task-list> is created
from tasks and returned by the function.
Note how the as method is used to convert a string value such as
"medium" into a symbol such as #"medium". This is a useful
technique to use when you wish to save and load symbol information in an
application.
The save-task-list function¶
The code for save-task-list is shown below. Because this function does
not use any DUIM code, it is described only briefly.
define function save-task-list
(task-list :: <task-list>, #key filename)
=> (saved? :: <boolean>)
let filename = filename | task-list-filename(task-list);
with-open-file (stream = filename, direction: #"output")
for (task in task-list.task-list-tasks)
format(stream, "%s\\n%s\\n",
task.task-name, as(<string>, task.task-priority))
end
end;
task-list.task-list-modified? := #f;
task-list.task-list-filename := filename;
#t
end function save-task-list;
Add this code to task-list.dylan.
The function save-task-list takes an instance of <task-list> as an
argument, and optionally a filename. It then attempts to save the
instance of <task-list> to the file specified by filename. It
returns a boolean value that indicates whether the file was successfully
saved or not. If filename is not passed as an argument to
save-task-list (in the case where the user has chosen File > Save or
clicked the Save button when working with a task list file that has
previously been saved), then the task-list-filename slot of the
<task-list> is used instead.
Like load-task-list, this function uses the Streams library to save
information to a file. For full information about this library, please
refer to the I/O and Networks Library Reference. It also uses the
format function from the Format library, which is described in the
same reference.
The file is opened for saving using the with-open-file macro (just
like load-task-list, but in the opposite direction), A for loop is
used to save each element in each task to the file. The format
function then writes each element to the file, separated by a newline
character. Note how the as method is used to convert the
task-priority symbol to a string when saving each priority value: this
is the reverse situation to load-task-list, where a method for as
was used to convert the string to a symbol.
Once every element in the file has been saved, the task-list-modified
slot of the <task-list> is reset to #f, and the
task-list-filename slot of the <task-list> is set to the filename
used by save-task-list. This last step is necessary to allow for the
case where the user has chosen the File > Save As command to save the
file under a different name.
Finally, save-task-list returns #t to indicate that the file has
been successfully saved.
Adding and removing tasks from the task list¶
This section describes the functions and methods necessary for adding to the task list and removing tasks from the task list. A total of two methods and two functions are necessary.
frame-add-task
This prompts the user for the details of a new task and adds it to the list.
frame-remove-task
This removes the currently selected task from the list, prompting the user before removing it completely.
add-task
This adds an instance of
<task>to an instance of<task-list>.
remove-task
This removes an instance of
<task>from an instance of<task-list>.
As with the file handling code, DUIM code and non-DUIM code has been
separated. The methods beginning with frame- deal with the GUI-related
issues of adding and removing tasks, and the functions deal with the
underlying data structures.
Add the definitions of the methods to frame.dylan, and the
definitions of the functions to task-list.dylan.
DUIM support for adding and removing tasks¶
This section describes the methods necessary to provide support in the task list manager GUI for adding and removing tasks.
Add the code described in this section to frame.dylan.
The code for frame-add-task is as follows:
define method frame-add-task (gadget :: <gadget>) => ()
let frame = sheet-frame(gadget);
let task-list = frame-task-list(frame);
let (name, priority) = prompt-for-task(owner: frame);
if (name & priority)
let new-task = make(<task>, name: name, priority: priority);
add-task(task-list, new-task);
refresh-task-frame(frame);
frame-selected-task(frame) := new-task
end
end method frame-add-task;
The method takes a gadget as an argument and returns no values. The
argument is the gadget which is used to invoke it, which in the case of
the task list manager means either add-menu-button (in the Task menu
of the application) or add-button (on the tool bar). The
frame-add-task method then sets a number of local variables:
frameThe frame containing the gadget passed as an argument.task-listThe value of theframe-task-listslot forframe. This identifies the instance of<task-list>to which a task is to be added.nameThe text of the task to be added.priorityThe priority of the task to be added.
As with other DUIM methods you have seen, frame and task-list are
specified using known slot values about the gadget supplied to
frame-add-task, and the frame that contains the gadget. The name
and priority values are specified by calling the prompt-for-task
method defined in Creating a dialog for adding new items. This method displays a dialog into which
the user types the text for the new task and chooses the priority, both
of which values are returned from prompt-for-task.
Once all the local variables have been specified, the main body of code for the method, repeated below, is executed.
if (name & priority)
let new-task = make(<task>, name: name, priority: priority);
add-task(task-list, new-task);
refresh-task-frame(frame);
frame-selected-task(frame) := new-task
end
This consists of four expressions around which is wrapped an if
statement.
The first expression creates a new task from the values of the
nameandprioritylocal variables.The second expression adds the new task to task list, by calling the
add-taskfunction.The third expression refreshes the display of the task list in the task list manager, so that the new task is displayed on the screen once it has been added.
The fourth expression ensures that the new task is selected in the task list manager. The frame-selected–task method is described in Updating the user interface.
The if statement ensures that all the information needed to construct
the new task is specified before the new task is created.
The add-task function is described in Non-DUIM support for adding
and removing tasks.
The code for frame-remove-task is as follows:
define method frame-remove-task (gadget :: <gadget>) => ()
let frame = sheet-frame(gadget);
let task = frame-selected-task(frame);
let task-list = frame-task-list(frame);
if (notify-user(format-to-string
("Really remove task %s", task.task-name),
owner: frame, style: #"question"))
frame-selected-task(frame) := #f;
remove-task(task-list, task);
refresh-task-frame(frame)
end
end method frame-remove-task;
As with frame-add-task, this method takes the gadget that is used to
invoke it as an argument and returns no values. In the case of the task
list manager, the gadget is either remove-menu-button (in the Task
menu of the application) or remove-button (on the tool bar). The
frame-remove-task method then sets a number of local variables:
frameThe frame containing the gadget passed as an argument.taskThe task that is to be removed. The task to be removed is the one selected in the list of tasks on screen. The methodframe-selected-taskis called to determine which task this is.task-listThe value of theframe-task-listslot forframe. This identifies the instance of<task-list>from which a task is to be removed.
The method frame-selected-task is described in Updating the user
interface.
Once these local variables have been set, the rest of the code goes
about removing the task. The code consists of three expressions around
which is wrapped an if statement, as shown below.
if (notify-user(format-to-string
("Really remove task %s", task.task-name),
owner: frame, style: #"question"))
frame-selected-task(frame) := #f;
remove-task(task-list, task);
refresh-task-frame(frame)
end
Notice here that the method notify-user is used as the condition in
the if statement: if the call to notify-user returns #t, then the
subsequent expressions are executed. This use of notify-user
illustrates how you can use the method to generate a yes-no question for
the user to answer, by using the style: init-keyword. You might like
to compare the user of notify-user in this method with its use in
open-file or save-as-file ; essentially, the only difference is in
the use of the style: init-keyword.
If the call to notify-user returns #t, then three expressions are
executed:
The first calls the setter for
frame-selected-task, to ensure that no items in the task list are selected.The second calls the function
remove-task, which removes task fromtask-list.Then,
refresh-task-frameis called to ensure that the task that has been removed is no longer displayed in the list of tasks on the screen.
The methods defined for frame-selected-task are described in
Updating the user interface. The function remove-task is
described in Non-DUIM support for adding and removing tasks. The
refresh-task-frame method is described in Updating the user
interface.
Non-DUIM support for adding and removing tasks¶
This section describes the functions necessary for adding an instance of
<task> to a <task-list>, and removing a <task> from a
<task-list>. These functions are called by the callback functions
frame-add-task and frame-remove-task, respectively. Because these
functions do not use any DUIM code, they are described only briefly.
Add the code described in this section to task-list.dylan.
The code for add-task is as follows:
define function add-task
(task-list :: <task-list>, task :: <task>) => ()
add!(task-list.task-list-tasks, task);
task-list.task-list-modified? := #t
end function add-task;
This function takes two arguments, a <task-list> and the <task> that
is to be added to it, and returns no values. The add-task function
first adds the <task> to the end of the sequence bound to the
task-list-tasks slot of the <task-list>, and then sets the
task-list-modified? slot of the <task-list> to #t, to indicate
that a change in the <task-list> has occurred.
The code for remove-task is as follows:
define function remove-task
(task-list :: <task-list>, task :: <task>) => ()
remove!(task-list.task-list-tasks, task);
task-list.task-list-modified? := #t
end function remove-task;
This function is analogous to add-task. It takes the same arguments,
and returns no values. The function first removes the <task> from the
task-list-tasks slot of the <task-list>, and then sets the
task-list-modified? slot of the <task-list> to #t, to indicate
that a change in the <task-list> has occurred.
Updating the user interface¶
This section describes a number of miscellaneous methods that are required for smooth operation of the task list manager. Each of the methods defined here ensures that the task list manager displays the correct information and gives the user access to appropriate commands in any given situation. Here is a list of the methods defined in this section, together with a brief description of each one:
initializeAninitializemethod is provided for<task-frame>that ensures information is displayed correctly when the task list manager is first displayed. This method is described in Initializing a new instance of <task-frame>.
frame-selected-task
This method returns the task that is currently selected in the task list manager. This method is described in Determining and setting the selected task.
frame-selected-task-setter
This is a setter method for frame-selected-task, and is used to select or deselect item in the task list manager. This method is described in Determining and setting the selected task.
note-task-selection-change
Two methods are defined that deal with updating the GUI whenever a change is made to the task selection state. This method is described in Enabling and disabling buttons in the interface.
refresh-task-frame
This method can be called to refresh the task frame at any time. This method is described in Refreshing the list of tasks.
Each of these methods should be added to the file frame.dylan.
Initializing a new instance of <task-frame>¶
The code below provides an initialize method for the class
<task-frame>. This simply ensures that the display in a
<task-frame> is refreshed as soon as it is created, and calls any
subsequent methods that may be defined for it (although, in the case of
the task list manager, there are none). While not strictly necessary,
this initialize method illustrates general good practice when defining
your own classes of frame. If the application was associated with files
of a particular type on disk, then the initialize method would be
necessary to ensure that tasks were displayed correctly after starting
the task list manager by double-clicking on a file of tasks.
define method initialize
(frame :: <task-frame>, #key) => ()
next-method();
refresh-task-frame(frame);
end method initialize;
Add the code for this method to frame.dylan.
Determining and setting the selected task¶
Two methods are used to determine which task is selected in the task
list manager, and to set a specific task in the task list manager:
frame-selected-task and frame-selected-task-setter.
The frame-selected-task method returns the task that is currently
selected in the task list manager, or #f if no task is selected. This
method is used by frame-remove-task to determine which task should be
deleted from the task list. It is also used by
note-task-selection-change to determine whether or not a task is
selected.
define method frame-selected-task
(frame :: <task-frame>) => (task :: false-or(<task>))
let list-box = task-list(frame);
gadget-value(list-box)
end method frame-selected-task;
The frame-selected-task method works by determining the gadget-value
of the list box that displays the tasks in the task list manager. The
gadget-value of a collection such as a list box is the selected item.
Notice how you can access the value of a pane in a frame instance in
exactly the same way that you can access the value of a slot in a class
instance; the definition of the pane creates an accessor that is just
like a slot accessor. Recall that the name of the list box in the
definition of the <task-frame> class is task-list.
A setter method is also defined for frame-selected-task, as shown
below:
define method frame-selected-task-setter
(task :: false-or(<task>), frame :: <task-frame>)
=> (task :: false-or(<task>))
let list-box = task-list(frame);
gadget-value(list-box) := task;
note-task-selection-change(frame);
task
end method frame-selected-task-setter;
This method takes two arguments: the task to select in the task list
manager, and the frame to which the task belongs. It returns the task.
The method determines the list box used to display tasks in frame,
and then sets the gadget-value of that list box to task. Finally,
it calls note-task-selection-change, described below, to update other
parts of the user interface appropriately, such as buttons on the tool
bar.
As with most setter methods, frame-selected-task-setter is not called
directly. Instead, it is called implicitly by setting a value using
frame-selected-task. For example,
frame-selected-task(frame) := #f;
ensures that no tasks are selected in frame.
The frame-selected-task-setter method is called by two other methods:
frame-add-task (to ensure that the task added is subsequently
selected) and frame-remove-task (to ensure that no tasks are selected
once a task has been removed from the list). These methods are described
in DUIM support for adding and removing tasks.
Add the code for these methods to frame.dylan.
Refreshing the list of tasks¶
The refresh-task-frame method is called whenever the list of tasks
needs to be refreshed for whatever reason. This happens most commonly
after adding or removing a task from the list, or loading in a new task
list from a file on disk. The method refresh-task-frame takes an
instance of <task-frame> as an argument and returns no values. For the
Task List 1 project the definition is:
define method refresh-task-frame
(frame :: <task-frame>) => ()
let list-box = frame.task-list;
let task-list = frame.frame-task-list;
let modified? = task-list.task-list-modified?;
let tasks = task-list.task-list-tasks;
if (gadget-items(list-box) == tasks)
update-gadget(list-box)
else
gadget-items(list-box) := tasks
end;
gadget-enabled?(frame.save-button) := modified?;
gadget-enabled?(frame.save-menu-button) := modified?;
note-task-selection-change(frame);
end method refresh-task-frame;
However, the Task List 2 project requires a call to command-enabled?,
so the definition is:
define method refresh-task-frame
(frame :: <task-frame>) => ()
let list-box = frame.task-list;
let task-list = frame.frame-task-list;
let modified? = task-list.task-list-modified?;
let tasks = task-list.task-list-tasks;
if (gadget-items(list-box) == tasks)
update-gadget(list-box)
else
gadget-items(list-box) := tasks
end;
command-enabled?(save-file, frame) := modified?;
note-task-selection-change(frame);
end method refresh-task-frame;
To begin, refresh-task-frame sets a number of local variables:
list-boxThe list box used to display the list of tasks in task list manager.task-listThe task list currently loaded in the task list manager.modified?The value of thetask-list-modified?slot oftask-list.tasksThe sequence of tasks stored intask-list.
Next, the following code is executed:
if (gadget-items(list-box) == tasks)
update-gadget(list-box)
else
gadget-items(list-box) := tasks
end;
This code ensures that if the items in the list box are the same as the sequence of tasks in the task list, then the display in the list box is updated to ensure all the items are displayed correctly. If the items in the list box are not the same as the sequence of tasks, then the items in the list box are updated to reflect the current task list. The items in the list box could be different if a task had been added or removed from the list, or if a completely new set of tasks had been loaded into the task list manager.
Lastly, the following three lines
gadget-enabled?(frame.save-button) := modified?;
gadget-enabled?(frame.save-menu-button) := modified?;
note-task-selection-change(frame);
ensure that the Save button and File > Save menu command are enabled
if the task list has been modified, and then any changes that need to be
made to the GUI as a result of changing the selected item are performed,
by calling note-task-selection-change.
Add the code for this method to frame.dylan.
Creating an information dialog¶
The following function displays a simple dialog box that provides information about the application. This dialog is displayed when you choose the Help > About menu command.
define function about-task (gadget :: <gadget>) => ()
notify-user("Task List Manager", owner: sheet-frame(gadget))
end function about-task;
Exiting the task list manager¶
The exit-task method allows you to exit the task list manager. It is
invoked by choosing File > Exit. The definition of this method is
quite simple.
define method exit-task (gadget :: <gadget>) => ()
let frame = sheet-frame(gadget);
let task-list = frame-task-list(frame);
save-file (gadget);
exit-frame(frame)
end method exit-task;
Add this method to the file frame.dylan.
The method takes the gadget used to invoke it and returns no values. In
this case, exit-task is only ever invoked by the exit-menu-button
gadget.
As with many other callbacks in this example, exit-task sets a number
of local variables:
frameThe frame that the gadget argument belongs to.task-listThe task list associated withframe.
The method begins by calling the save-file method (defined in The
save-file method) to save the current task list to disk. This ensures
that the user does not lose any work. Next, the exit-frame generic
function is invoked to exit the task list manager window.
Enhancing the task list manager¶
This concludes the tutorial on building application with DUIM. At this point, you can build and run a functional task list manager, but it is a very basic application. Using Command Tables introduces command tables as a way of defining hierarchies of menu commands. To do this, it re-implements the menu hierarchy you defined in Adding Menus To The Application, but does not add any new functionality to the application.
There are many ways that the task list manager could be extended, and you might like to try experimenting with the code. To begin with, very little error checking has been written into the application, and you might like to add some in order to make the task list manager more robust. For example, it is currently possible to exit the task list manager and lose any changes in an unsaved list of tasks.
In addition to error checking, there is a wide range of new functionality you might like to add. A few ideas are listed below:
Re-implement the list box and radio box in the main window of the task list manager as a table control, so that the priority of each task is displayed next to the text for the task.
Implement the facility to define categories, so that tasks could be assigned categories such as “Home” and “Business”. Categories could be listed in the table control alongside priorities.
Allow sorting the list of tasks according to a key. Tasks could then be sorted by priority or category.
Implement the ability to mark tasks as complete.
Allow users to add text memos to any task.
This is only a very limited list of ideas. After learning about command tables in Using Command Tables, read through A Tour of the DUIM Libraries to learn more about the features that DUIM provides. Then, using the DUIM Reference Manual as your reference source, get coding!