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-file
This 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-file
This 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-file
This 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-list
This function saves an instance of<task-list>
to a named file. It is called bysave-as-file
.load-task-list
This 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:
frame
This contains the frame of which the gadget argument is a part. This is a simple way of identifying the main application frame.task-list
This contains the value of theframe-task-list
slot 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.filename
This 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:
frame
The frame of which the gadget argument is a part, so that the main application frame can be identified.task-list
This contains the value of theframe-task-list
slot 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
filename
argument is not passed tosave-as-file
. Instead, the user is prompted to supply it. In addition, thegadget
issave-as-menu-button
.When invoked by
save-file
, afilename
may be passed, if the associated task list has been saved before. In addition, the gadget is eithersave-button
orsave-menu-button
.
As with open-file
, save-as-file
sets three local variables:
frame
This is the frame containing the gadget passed as an argument.task-list
This contains the value of theframe-task-list
slot forframe
, and identifies the instance of<task-list>
to be saved.filename
The 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-name
of “Wash the dog” and atask-priority
of#"medium"
.The second
<task>
would have atask-name
of “Video Men Behaving Badly” and atask-priority
of#"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:
frame
The frame containing the gadget passed as an argument.task-list
The value of theframe-task-list
slot forframe
. This identifies the instance of<task-list>
to which a task is to be added.name
The text of the task to be added.priority
The 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
name
andpriority
local variables.The second expression adds the new task to task list, by calling the
add-task
function.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:
frame
The frame containing the gadget passed as an argument.task
The 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-task
is called to determine which task this is.task-list
The value of theframe-task-list
slot 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-frame
is 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:
initialize
Aninitialize
method 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-box
The list box used to display the list of tasks in task list manager.task-list
The task list currently loaded in the task list manager.modified?
The value of thetask-list-modified?
slot oftask-list
.tasks
The 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:
frame
The frame that the gadget argument belongs to.task-list
The 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!