.. index:: single: adding; callbacks to a DUIM application single: callbacks; adding to a DUIM application single: DUIM applications; adding callbacks *********************************** 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 :ref:`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. .. index:: single: task list manager; underlying data structures for tasks .. _defining-the-underlying-data-structures-for-tasks: 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 ```` class and a ```` class. The definition of ````, 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 ````. 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 ````. .. code-block:: dylan define class () constant slot task-list-tasks = make(), init-keyword: tasks:; slot task-list-filename :: false-or() = #f, init-keyword: filename:; slot task-list-modified? :: = #f; end class ; 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: .. code-block:: dylan define constant = 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 ```` 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. .. code-block:: dylan define class () slot task-name :: , required-init-keyword: name:; slot task-priority :: , required-init-keyword: priority:; end class ; 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 ````. 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 :ref:`menus--gluing-it-together`, the definition of ``task-list`` was given as: .. code-block:: dylan // definition of list pane task-list (frame) make (, 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 ```` associated with the frame. Recall that a ``frame-task-list`` slot was specified in the definition of ```` ; this slot is used to hold the instance of ```` that is associated with the ````. 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: .. code-block:: dylan 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: .. code-block:: dylan label-key: task-name, This gives the following new definition for the ``task-list`` pane: .. code-block:: dylan // definition of list pane task-list (frame) make (, 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`_. .. index:: single: callbacks; assigning to gadgets single: gadgets; specifying callbacks single: specifying; callbacks in gadget definitions 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 ````. 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 ```` class: .. code-block:: dylan pane add-button (frame) make(, label: "Add task", activate-callback: frame-add-task); You can use exactly the same callback in the new definition of ``add-menu-button`` : .. code-block:: dylan pane add-menu-button (frame) make(, 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. .. index:: single: callbacks; defining single: defining; callbacks 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. .. index:: single: applications; DUIM file handling in single: file handling in DUIM applications single: handling files in the task list manager single: task list manager; handling files .. _callbacks--handling-files-in-the-task-list-manager: 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. .. index:: single: DUIM applications; file handling in 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 ```` 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 function ``load-task-list``. It is used as the activate callback for both ``open-button`` (on the application tool bar) and ``open-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 both ``save-button`` (on the application tool bar) and ``save-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 for ``save-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 ```` to a named file. It is called by ``save-as-file``. - ``load-task-list`` This function takes the contents of a file on disk and converts it into an instance of ````. It is called by ``open-as-file``. The following sections present and explain the code for each of these methods and functions in turn. .. index:: single: task list manager; open-file method The open-file method -------------------- The code for open-file is shown below. Add this code to ``frame.dylan``. .. code-block:: dylan define method open-file (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 the ``frame-task-list`` slot for frame. This identifies the instance of ```` 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: .. code-block:: dylan 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 ```` 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. .. code-block:: dylan 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 .. code-block:: dylan 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 .. code-block:: dylan let task-list = load-task-list(filename); The function ``load-task-list`` is described in `The load-task-list function`_. The clause .. code-block:: dylan 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 .. code-block:: dylan frame.frame-task-list := task-list; assigns the ``frame-task-list`` slot of frame to the value of ``task-list``. The line .. code-block:: dylan 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`_. .. index:: single: task list manager; save-file method The save-file method -------------------- The code for ``save-file`` is as follows: .. code-block:: dylan define method save-file (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 the ``frame-task-list`` slot for ``frame``. This identifies the instance of ```` 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 ```` 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. .. index:: single: task list manager; save-as-file method The save-as-file method ----------------------- The code for ``save-as-file`` is as follows: .. code-block:: dylan define method save-as-file (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 to ``save-as-file``. Instead, the user is prompted to supply it. In addition, the ``gadget`` is ``save-as-menu-button``. - When invoked by ``save-file``, a ``filename`` may be passed, if the associated task list has been saved before. In addition, the gadget is either ``save-button`` or ``save-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 the ``frame-task-list`` slot for ``frame``, and identifies the instance of ```` to be saved. - ``filename`` The filename to which the instance of ```` 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. .. code-block:: dylan 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 .. code-block:: dylan 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`` : .. code-block:: dylan 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: .. code-block:: dylan 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: .. code-block:: dylan 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`_. .. index:: single: task list manager; load-task-list function 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. .. code-block:: dylan define function load-task-list (filename :: ) => (task-list :: false-or()) let tasks = make(); 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(, name: name, priority: as(, priority)); add!(tasks, task) end end end; make(, 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 ````, which itself contains any number of instances of ````. It takes one argument, the filename, and returns one value, the instance of ````. 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 ```` whose ``task-list-tasks`` slot was a sequence of two instances of ````. - The first ```` would have a ``task-name`` of *"Wash the dog"* and a ``task-priority`` of ``#"medium"``. - The second ```` would have a ``task-name`` of *"Video Men Behaving Badly"* and a ``task-priority`` of ``#"high"``. The ``task-list-filename`` slot of the ```` is the filename itself. Note that the ``task-list-modified?`` slot of the ```` 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 :ref:`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 ```` 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 ```` 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. .. index:: single: task list manager; save-task-list function 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. .. code-block:: dylan define function save-task-list (task-list :: , #key filename) => (saved? :: ) 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(, 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 ```` as an argument, and optionally a ``filename``. It then attempts to save the instance of ```` 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 ```` 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 ```` is reset to ``#f``, and the ``task-list-filename`` slot of the ```` 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. .. index:: single: adding; items to a list single: deleting items from a list single: items; adding to a list single: items; removing from a list single: list; adding items to single: list; adding items from single: removing items from a list 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 ```` to an instance of ````. ``remove-task`` This removes an instance of ```` from an instance of ````. 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``. .. index:: single: adding; items to a list single: items; adding to a list single: items; removing from a list single: list; adding items to single: list; adding items from single: removing items from a list 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. .. index:: single: deleting items from a list Add the code described in this section to ``frame.dylan``. The code for ``frame-add-task`` is as follows: .. code-block:: dylan define method frame-add-task (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(, 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 the ``frame-task-list`` slot for ``frame``. This identifies the instance of ```` 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 :ref:`improve--creating-a-dialog-for-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. .. code-block:: dylan if (name & priority) let new-task = make(, 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`` and ``priority`` 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: .. code-block:: dylan define method frame-remove-task (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 method ``frame-selected-task`` is called to determine which task this is. - ``task-list`` The value of the ``frame-task-list`` slot for ``frame``. This identifies the instance of ```` 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. .. code-block:: dylan 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 from ``task-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`_. .. index:: single: task list manager; adding tasks single: task list manager; removing tasks Non-DUIM support for adding and removing tasks ---------------------------------------------- This section describes the functions necessary for adding an instance of ```` to a ````, and removing a ```` from a ````. 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: .. code-block:: dylan define function add-task (task-list :: , task :: ) => () add!(task-list.task-list-tasks, task); task-list.task-list-modified? := #t end function add-task; This function takes two arguments, a ```` and the ```` that is to be added to it, and returns no values. The ``add-task`` function first adds the ```` to the end of the sequence bound to the ``task-list-tasks`` slot of the ````, and then sets the ``task-list-modified?`` slot of the ```` to ``#t``, to indicate that a change in the ```` has occurred. The code for ``remove-task`` is as follows: .. code-block:: dylan define function remove-task (task-list :: , 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 ```` from the ``task-list-tasks`` slot of the ````, and then sets the ``task-list-modified?`` slot of the ```` to ``#t``, to indicate that a change in the ```` has occurred. .. index:: single: GUI; updating interface single: updating a GUI 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`` An ``initialize`` method is provided for ```` that ensures information is displayed correctly when the task list manager is first displayed. This method is described in `Initializing a new instance of \`_. ``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 :ref:`enable-disable-buttons`. ``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 ------------------------------------------- The code below provides an ``initialize`` method for the class ````. This simply ensures that the display in a ```` 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. .. code-block:: dylan define method initialize (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. .. code-block:: dylan define method frame-selected-task (frame :: ) => (task :: false-or()) 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 ```` class is ``task-list``. A setter method is also defined for ``frame-selected-task``, as shown below: .. code-block:: dylan define method frame-selected-task-setter (task :: false-or(), frame :: ) => (task :: false-or()) 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, .. code-block:: dylan 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``. .. _enable-disable-buttons: Enabling and disabling buttons in the interface ----------------------------------------------- The two methods for ``note-task-selection-change`` make a number of changes to the GUI of the task list manager, to ensure that the correct information is displayed to the user. In particular, they perform any changes necessary after an item in the task list has been selected or deselected. They ensure that the correct priority is displayed in the radio box, depending on whether there is a task currently selected, and they also enable or disable the *Remove task* button and its equivalent command in the *Task* menu, depending on whether there is a task selected or not (if there is no task selected, then the button and menu command should both be disabled). There are two methods defined, one on an instance of ````, and one on an instance of ````. The Task List 1 project requires both of these methods. For the Task List 2 project, however, the first method requires a slightly different definition, and the second method is not required at all. The ``note-task-selection-change`` method defined on ```` is called by ``refresh-task-frame``, described in `Refreshing the list of tasks <#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 ```` as an argument and returns no values. For the Task List 1 project, the ``note-task-selection-change`` method is defined: .. code-block:: dylan define method note-task-selection-change (frame :: ) => () let task = frame-selected-task(frame); if (task) frame.priority-box.gadget-value := task.task-priority; end; let selection? = (task ~= #f); frame.remove-button.gadget-enabled? := selection?; frame.remove-menu-button.gadget-enabled? := selection?; end method note-task-selection-change; For the Task List 2 project the ``note-task-selection-change`` method is defined: .. code-block:: dylan define method note-task-selection-change (frame :: ) => () let task = frame-selected-task(frame); if (task) frame.priority-box.gadget-value := task.task-priority; end; command-enabled?(frame-remove-task, frame) := task ~= #f; end method note-task-selection-change; The method takes an instance of ```` as an argument, and returns no values. It works by calling ``frame-selected-task`` to determine which, if any, task is currently selected, and sets that to a local variable, ``task``. The expression .. code-block:: dylan if (task) frame.priority-box.gadget-value := task.task-priority; end; sets the gadget value of the ``priority-box`` pane in the task list manager to the value of the ``task-priority`` slot of the selected task, if a task is selected. This ensures that if a task is selected, its priority is displayed correctly beneath the list of tasks. Note that ``priority-box`` may take the same set of values as the ``task-priority`` slot, namely ``#"low"``, ``#"medium"``, and ``#"high"``, so it is straightforward to make this kind of assignment. The rest of the method deals with enabling or disabling gadgets that let the user remove a task from the task list. If there is no task selected, then ``remove-button`` and ``remove-menu-button`` need to be disabled. If there is a task selected, then they need to be enabled. This behavior is achieved by converting the value of the variable ``task``, which can take a value of ``false-or()``, into a boolean value, called ``selection?``. This is done in the expression .. code-block:: dylan let selection? = (task ~= #f); This sets ``selection?`` to the result of performing an inequality comparison on ``task`` and ``#f``. Thus, if ``task`` is ``#f`` (there is no task selected), then ``selection?`` is ``#f``, but if ``task`` is an instance of ```` (there is a task selected), then ``selection?`` is ``#t``. The two calls to ``gadget-enabled?`` then set the ``gadget-enabled`` slot of the appropriate gadgets to the value of ``selection?``, enabling or disabling each gadget as appropriate. The second method for ``note-task-selection-change`` is defined for an instance of ````, as follows: .. code-block:: dylan define method note-task-selection-change (gadget :: ) => () let frame = gadget.sheet-frame; note-task-selection-change(frame) end method note-task-selection-change; This takes a gadget as an argument. It simply finds the frame that the gadget belongs to, and calls the other method for ``note-task-selection-change`` on that frame. The second method for ``note-task-selection-change`` needs to be used as the value-changed callback of the ``task-list`` pane in the definition of ```` ; a value-changed callback is invoked whenever the ``gadget-value`` of a gadget changes. Because the ``gadget-value`` of a list box is the currently selected item, whenever a different item is selected in the list box, ``note-task-selection-change`` is called. In order to achieve this, a small change is needed to the definition of the task-list pane in ``frame.dylan``. In this definition for the Task List 1 project, change the line that reads: .. code-block:: dylan activate-callback: not-yet-implemented); to .. code-block:: dylan value-changed-callback: note-task-selection-change); and for the Task List 2 project change the line to .. code-block:: dylan value-changed-callback: method (gadget) note-task-selection-change(frame) end); to give a final definition for this pane as follows: .. code-block:: dylan // definition of list pane task-list (frame) make (, items: frame.frame-task-list.task-list-tasks, label-key: task-name, lines: 15, value-changed-callback: note-task-selection-change); 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 ```` as an argument and returns no values. For the Task List 1 project the definition is: .. code-block:: dylan define method refresh-task-frame (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: .. code-block:: dylan define method refresh-task-frame (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 the ``task-list-modified?`` slot of ``task-list``. - ``tasks`` The sequence of tasks stored in ``task-list``. Next, the following code is executed: .. code-block:: dylan 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 .. code-block:: dylan 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``. .. index:: single: About; adding to an application single: creating; information dialog single: dialogs; about dialog, creating single: information dialog; creating 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. .. code-block:: dylan define function about-task (gadget :: ) => () notify-user("Task List Manager", owner: sheet-frame(gadget)) end function about-task; .. index:: single: exiting the task list manager single: task list manager; exiting 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. .. code-block:: dylan define method exit-task (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 with ``frame``. 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. .. index:: single: enhancing the task list manager single: task list manager; enhancing 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. :doc:`commands` introduces command tables as a way of defining hierarchies of menu commands. To do this, it re-implements the menu hierarchy you defined in :doc:`menus`, 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 :doc:`commands`, read through :doc:`tour` to learn more about the features that DUIM provides. Then, using the *DUIM Reference Manual* as your reference source, get coding!