Improving The Design¶
The simple layout hierarchy described in Creating the basic sheet hierarchy has a number of problems associated with it, all of which revolve around the fact that the task list manager does not yet look very much like a standard Windows application. Although it is a simple design that does not warrant a complicated user interface, the design you have already seen looks more like a dialog box than an application window.
This section shows you how to improve on the basic design, adding a menu bar and replacing the buttons with a proper tool bar. It also shows you how to move the text field into a separate dialog that pops up when you click the Add task button in the tool bar.
From this point on, the interface is defined more formally, using
frames. Up to now, the layout hierarchy has been presented informally,
and you have used contain
to display the layout interactively. This is
fine for code that you want to evaluate once only, perhaps using the
interactor, but for permanent code, a more rigorous framework is
preferable.
Defining a project¶
From this point on, you should put the code for the task list manager
into a project, rather than evaluating pieces of code using the
interactor. Please refer to the Getting Started with Open Dylan
for complete details on how to manage projects using the environment;
this section provides brief details that tell you how to create a
project specifically for the task list manager application. Use the New
Project wizard to create a GUI application, and call the project
task-list
when prompted. The New Project wizard offers the option of
generating template source code to help you get started. For
this example, you must ensure that this option is switched off (this
is the default setting).
Two versions of the task list manager are included with Open Dylan, so that you can load the code straight into the environment if you wish. These are available in the Open Example Project dialog, in the Documentation category. You can display this dialog by choosing Tools > Open Example Project from the environment. The two versions included represent the state of the task list manager at the end of Adding Callbacks to the Application, and at the end of Using Command Tables.
Note
Please note that both projects have the same name within the
source code— task-list
—and you should not load them both into
the environment at the same time.
The number of source code files in a given project, and the names you
give them, is entirely up to you. For the purposes of this example, you
will use the files suggested by the New Project wizard. When you use the
New Project wizard, Open Dylan will create a number of files for a
project named task-list
.
module.dylan
andlibrary.dylan
These files define the library and modules for the project. For the purposes of this application, you can ignore these files.
task-list.dylan
Add non-GUI-specific code to this file.
Finally, you need to create the following new file using File > New, and add it to the project using the Project > Insert File command. Make sure that this file is the last one listed in the project window.
frame.dylan
Add the GUI-specific code to this file.
Starting the application¶
As you add source code to the files in your project, there will be times
when you want to build the project to test it. This section defines some
methods that let you run the application in a clean way. Add these
methods to frame.dylan
.
The frame class that is used to implement the task list manager is
called <task-frame>
. This class will be introduced in Defining a
new frame class. You can define a method to create an instance of
<task-frame>
as follows:
define method start-task () => ()
let frame = make(<task-frame>);
start-frame(frame);
end method start-task;
This method is provided as a convenient way to create the frame and then start its event loop. It returns when the event loop shuts down.
Note
Obviously, you should not call this method until you have
defined a frame class called <task-frame>
.
Finally, you can start the application with the following method, and its subsequent call:
define method main (arguments :: <sequence>) => ()
// handle the arguments
start-task();
end method main;
begin
main(application-arguments()) // Start the application!
end;
Make sure that this is the very last definition in the file
frame.dylan
, and remember that frame.dylan
should itself be the
last file listed in the project window.
Once you have added these methods to your code, you can compile and link the code, and run the application to test it, using the appropriate commands in the Dylan environment.
Note that, unlike languages such as C, Dylan does not insist on a single
entrance point to an application such as the one given here. All the
same, it is still good practice to define one if you can. The main
difference between the use of the method main
here, and the use of the
main
function in C, is in the arguments that need to be passed. In C,
you need to pass two generic arguments: argc
, which specifies the
number of arguments you are passing, and argv
, an array of strings
that define the arguments themselves. In Dylan, however, you only need
to pass the second of these arguments; since any Dylan collection
already knows its own size, you do not need to pass the number of
arguments as an additional parameter.
Adding a default callback¶
Nothing is more frustrating than designing a user interface that does not respond to user input. Although, in the early stages at least, the user interface does nothing particularly useful, you can at least define a “not yet implemented” message that can be used until you define real behavior for the application.
The definition of the function that gives you this default behavior is as follows:
define function not-yet-implemented (gadget :: <gadget>) => ()
notify-user("Not yet implemented!", owner: sheet-frame(gadget))
end function not-yet-implemented;
Add this function to frame.dylan
.
You can call this function from any gadget in the task list manager by
defining it as the activate callback for each gadget. There are several
types of callback, and this is the type that is used most in the task
list manager. You can define the activate callback for any gadget using
the activate-callback:
init-keyword. More information about callbacks
is given in Adding Callbacks to the Application, in which some real callbacks are
defined, to make the task list manager do something more substantial.
Defining a new frame class¶
To begin with, define a frame class using the layout hierarchy you have already created. Although it might seem redundant to implement an inelegant layout again, it is easier to illustrate the basic techniques using a design you are already familiar with. In addition, there are several elements in the design that can be reused successfully.
Add the code described in this section to frame.dylan
.
Defining a new class of frame is just like defining any Dylan class,
except that there are several extra options available beyond the slot
options normally available to define class
. Each of these extra
options lets you describe a particular aspect of the user interface. To
define the new frame class, use the following structure:
define frame <task-frame> (<simple-frame>)
// definitions of frame slots and options go here
end frame <task-frame>;
In this case, <task-frame>
is the name of the new class of frame,
and``<simple-frame>`` is its superclass. Like ordinary Dylan classes,
frame classes can have any number of superclasses, with multiple
superclasses separated by commas. The superclass of any “standard” frame
is usually <simple-frame>
. If you were designing a dialog box, its
superclass would be <dialog-frame>
. If you were designing a wizard,
its superclass would be <wizard-frame>
.
Adding slots to a frame class is exactly the same as adding slots to a standard Dylan class. You can define slot names, init-keywords, init-functions, default values, and so on. For this example, you are not defining any slots.
Each user interface element in the new class of frame is specified as a pane with a name and a definition. A pane is a sheet within a layout, and you can think of panes as sheets that represent concrete classes in an interface (as opposed to abstract classes). In effect, specifying a pane allows you to group together existing gadgets into some meaningful relationship that effectively creates a new gadget, without actually defining a gadget class.
The name is used to refer to the pane, both from within the frame definition itself, and from other code. The pane definition includes code to create the interface element. A pane specification also includes a place to declare a local variable that can be used within the pane’s definition to refer to the surrounding frame.
The following code fragment defines the two buttons, the text field, the radio box, and the list box from the initial design:
pane add-button (frame)
make(<push-button>, label: "Add task",
activate-callback: not-yet-implemented);
pane remove-button (frame)
make(<push-button>, label: "Remove task",
activate-callback: not-yet-implemented);
pane task-text (frame)
make(<text-field>, label: "Task text:",
activate-callback: not-yet-implemented);
pane priority-box (frame)
make (<radio-box>, label: "Priority:",
items: #("High", "Medium", "Low"),
orientation: #"vertical",
activate-callback: not-yet-implemented);
pane task-list (frame)
make(<list-box>, items: #(), lines: 15,
activate-callback: not-yet-implemented);
Note that the definition of each element is identical to the
definitions included in the original layout described in
Creating the basic sheet hierarchy
(except that
activate callbacks have been added to the code). Adding (frame)
immediately after the name of each pane lets you refer to the frame
itself within the frame definition using a local variable. This means
that you can refer to any pane within the frame using normal slot
syntax; that is, a pane called my-pane
can be referred to as
frame.my-pane
throughout all of the definition of the frame
class. This ability is essential when you come to layout each pane in
the frame itself.
In addition, you need to define the layout in which to place these panes. This is itself just another pane, and its definition is again identical to the original layout described in Creating the basic sheet hierarchy, with one exception; rather than defining each element explicitly, you just include a reference to the relevant pane that you have already defined using normal slot syntax, thus:
pane task-layout (frame)
horizontally ()
frame.task-list;
vertically ()
horizontally ()
frame.task-text;
frame.add-button;
end;
frame.remove-button;
frame.priority-box;
end;
end;
To describe the top-level layout for the frame, you need to refer to
this pane using the layout
option, as follows:
layout (frame) frame.task-layout;
You actually have a certain amount of freedom when choosing what to
define as a pane in the definition of a frame class. For example, the
layout in the task-layout
pane actually contains a number of
sub-layouts. If you wanted, each one of these sub-layouts could be
defined as a separate pane within the frame definition. Note, however,
that you only have to “activate” the top-most layout; there should only
be one use of the layout
option.
Similarly, you are free to use whatever programming constructs you like
when defining elements in your code. Just as in the earlier examples,
you could define the layouts with explicit calls to make
, rather than
by using the horizontally
and vertically
macros. Thus, the following
definition of task-layout
is just as valid as the one above:
pane task-layout (frame)
make(<row-layout>,
children: vector(frame.task-list,
make(<column-layout>,
children: vector(make(<row-layout>,
children: vector(frame.task-text, frame.add-button))))));
Notice that this construct is rather more complicated than the one using macros!
Throughout this section, you may have noticed that you can identify a sequence of steps that need to occur inside the definition of a frame. It is good practice to keep this sequence in mind when writing your own frame-based code:
Define the content panes
Define the layout panes
Use the
layout
option
If you glue all the code defined in this section together, you end up with the following complete definition of a frame class.
define frame <task-frame> (<simple-frame>)
pane add-button (frame)
make(<push-button>, label: "Add task",
activate-callback: not-yet-implemented);
pane remove-button (frame)
make(<push-button>, label: "Remove task",
activate-callback: not-yet-implemented);
pane task-text (frame)
make(<text-field>, label: "Task text:",
activate-callback: not-yet-implemented);
pane priority-box (frame)
make(<radio-box>, label: "Priority:",
items: #("High", "Medium", "Low"),
orientation: #"vertical",
activate-callback: not-yet-implemented);
pane task-list (frame)
make (<list-box>, items: #(), lines: 15,
activate-callback: not-yet-implemented);
pane task-layout (frame)
horizontally ()
frame.task-list;
vertically ()
horizontally ()
frame.task-text;
frame.add-button;
end;
frame.remove-button;
frame.priority-box;
end;
end;
layout (frame) frame.task-layout;
keyword title: = "Task List Manager";
end frame <task-frame>;
Note the addition of a title:
keyword at the end of the definition.
This can be used to give any instance of the frame class a title that is
displayed in the title bar of the frame’s window when it is mapped to
the screen.
At this stage, the application still has no real functionality, and there is no improvement in the interface compared to the initial design, but by defining a frame class, the implementation is inherently more robust, making it easier to modify and, eventually, maintain.
If you want to try running your code, remember that you need to define some additional methods to create a frame instance and exit it cleanly. Methods for doing this were provided in Starting the application. If you define these methods now, you can create running versions of each successive generation of the application as it is developed.
Adding a tool bar¶
So far, you have seen how to experiment interactively to create an initial interface design. You have also seen how you can take that initial design and turn it into a more rigorous definition, for use within project source code, using a frame class. However, the design of the interface still leaves a lot to be desired, and the application still does not do anything. In this section, you start to look at improving the overall design of the interface.
To begin with, add a tool bar to the interface of the application. Most modern applications have a tool bar that runs along the top edge of the main application window, beneath the application menu bar. Tool bars contain a number of buttons that give you quick access to some of the most common commands in the application. Each button has a label that designates its use, or, more often, a small icon. Although you have already added buttons to the interface that perform important tasks, they have the appearance of buttons in a dialog box, rather than buttons in the main window of an application. The solution is to use a tool bar.
Adding a tool bar to the definition of a frame class is very similar to
defining the overall layout of the panes in a frame class. You need to
create the tool bar as a pane in the frame definition, and then
incorporate it using the tool-bar
clause, as shown below:
pane task-tool-bar (frame)
make(<tool-bar>, child: ...);
// ...more definitions here...
tool-bar (frame) frame.task-tool-bar;
A tool bar has a layout as its child, and each button in the tool bar is defined as a child of that layout. You can either define each button within the definition of the tool bar itself, or, more appropriately, define each button as a pane in the frame, and then refer to the names of these panes in the tool bar definition.
In fact, the buttons you defined in the earlier interface design can be
used just as easily in a tool bar as they can within the main layout of
the application itself. However, first you must remove the buttons from
the task-layout pane of the definition of <task-frame>
. (If you fail
to do this, DUIM attempts to use the same buttons in two different parts
of the interface, with undefined results.) A complete definition of a
simple tool bar containing two buttons is as follows:
pane task-tool-bar (frame)
make(<tool-bar>,
child: horizontally ()
frame.add-button;
frame.remove-button
end);
// ...more definitions here...
tool-bar (frame) frame.task-tool-bar;
A tool bar that only contains two buttons is on the lean side, however, so let’s add two more buttons to open a file and save a file to disk.
pane open-button (frame)
make(<push-button>,
label: "Open file",
activate-callback: not-yet-implemented);
pane save-button (frame)
make(<push-button>,
label: "Save file",
activate-callback: not-yet-implemented);
// ...more definitions here...
pane task-tool-bar (frame)
make(<tool-bar>,
child: horizontally ()
frame.open-button;
frame.save-button;
frame.add-button;
frame.remove-button
end);
// ...more definitions here...
tool-bar (frame) frame.task-tool-bar;
More commonly, an icon is used to label buttons in a tool bar, rather
than a text label. You can do this by supplying an instance of <image>
to the label:
init-keyword when you define the button, rather than an
instance of <string>
.
So now the application has a tool bar. Somewhat oddly, it does not yet have a menu bar or a system of menus — most tool bars represent a subset of the commands already available from the application’s menu system. A menu system is added to the task list manager in Adding Menus To The Application.
Adding a status bar¶
As well as a tool bar, most applications have a status bar. This is a bar that runs along the bottom edge of the main application window, and displays information about the current status of the application. At its most basic, a status bar provides a label that displays text of some sort. In many applications, status bars contain a number of different fields, providing a wide range of functionality. At their most complex, a status bar may have several different labels that display information about the current state of the application, and labels that display help for the currently selected menu command.
It is worth adding a very simple status bar to the task list
application. This contains a label that could eventually be used to
display the name of the file currently loaded into the application.
Adding a status bar to the definition of a frame class is very similar
to adding a tool bar: you need to define a pane that describes the
status bar, and then you need to incorporate it using the status-bar
clause.
pane task-status-bar (frame)
make(<status-bar>, label: "Task Manager");
// ...more definitions here...
status-bar (frame) frame.task-status-bar;
Now you have added a status bar to the application. The next step is to glue all the pieces together once again to create your modified frame design.
Gluing the new design together¶
In improving the initial design of the application, you have added a tool bar and a status bar. Adding a tool bar, in particular, has obviated the need for some of the elements that you added to the earlier version of the frame design. In this section, you throw away those elements that are no longer needed, and add in the new elements, to create a new, improved design for the frame class.
One part of the initial design you have not yet improved on is the radio box that shows the priority of any task in the list. Ideally, rather than using a radio box, you would display the priority of each task alongside the task itself, within the list box. For now, however, keep the radio box.
pane priority-box (frame)
make(<radio-box>,
items: $priority-items,
orientation: #"horizontal",
label-key: first,
value-key: second,
value: #"medium",
activate-callback: not-yet-implemented);
Notice that the orientation is no longer constrained to be vertical. In
the new design, a horizontal radio box looks better. By default, the
orientation of a radio box is horizontal, so you could just completely
remove the initialization of the orientation:
init-keyword from the
definition of the pane. In general, though, if you care about the
orientation of a gadget, you should specify it explicitly, so leave the
init-keyword in the pane definition, and change its value, as shown
above.
Next, notice that the items are now specified using a named constant, rather than by embedding literals in the pane definition. The definition of this constant is as follows:
define constant $priority-items
= #(#("Low", #"low"),
#("Medium", #"medium"),
#("High", #"high"));
Add the definition for this constant to frame.dylan
.
Using lists of string and symbol values in this constant lets you assign
values to the individual components of the radio box elegantly, in
conjunction with the other improvements to the definition of
priority-box
.
The label key is a function which is passed an entry from the sequence and returns a string to use as the label.
Assigning
first
to the label key ofpriority-box
ensures that the first element from each sub-list of$priority-items
(the string) is used as the label for the appropriate item. Thus, the first button in priority box is labeled “Low”.The value key is a function which is passed an entry and returns the logical value of the entry.
Assigning
second
to the value key ofpriority-box
ensures that the second element from each sub-list of$priority-items
(the symbol) is used as the value for the appropriate item. Thus, the first button in priority box has the value#"low"
.
Lastly, priority-box
is given a default value: #"medium"
. This
ensures that the button labeled “Medium” is selected by default whenever
priority-box
is first created.
The definitions for add-button
, remove-button
, and task-list
remain unchanged from the initial design. In addition, you need to add
the definitions for open-button
and save-button
described in
Adding a tool bar.
You also need to add in the definitions for the tool bar and status bar themselves, as described in Adding a tool bar and Adding a status bar.
The definition for task-layout
has become much simpler. Because you
have added buttons to the tool bar, the main layout for the application
has reduced to a single column layout whose children are task-list
and
priority-box
.
The definition for the new design of the frame class now looks as follows (button definitions vary slightly for the Task List 2 project, see A task list manager using command tables):
define frame <task-frame> (<simple-frame>)
// definition of buttons
pane add-button (frame)
make(<push-button>, label: "Add task",
activate-callback: not-yet-implemented);
pane remove-button (frame)
make(<push-button>, label: "Remove task",
activate-callback: not-yet-implemented);
pane open-button (frame)
make(<push-button>, label: "Open file",
activate-callback: not-yet-implemented);
pane save-button (frame)
make(<push-button>, label: "Save file",
activate-callback: not-yet-implemented);
// definition of radio box
pane priority-box (frame)
make (<radio-box>,
items: $priority-items,
orientation: #"horizontal",
label-key: first,
value-key: second,
value: #"medium",
activate-callback: not-yet-implemented);
// definition of tool bar
pane task-tool-bar (frame)
make(<tool-bar>,
child: horizontally ()
frame.open-button;
frame.save-button;
frame.add-button;
frame.remove-button
end);
// definition of status bar
pane task-status-bar (frame)
make(<status-bar>, label: "Task Manager");
// definition of list
pane task-list (frame)
make (<list-box>, items: #(), lines: 15,
activate-callback: not-yet-implemented);
// main layout
pane task-layout (frame)
vertically ()
frame.task-list;
frame.priority-box;
end;
// activation of frame elements
layout (frame) frame.task-layout;
tool-bar (frame) frame.task-tool-bar;
status-bar (frame) frame.task-status-bar;
// frame title
keyword title: = "Task List Manager";
end frame <task-frame>;
Note that this definition does not incorporate the original task-text
pane defined in Defining a new frame class. In fact, this part of the
original interface is handled differently in the final design, and is
re-implemented in Creating a dialog for adding new items.
Creating a dialog for adding new items¶
You may be wondering what has happened to task-text
, the text field
in which you typed the text of each new task. In the new design, this is
moved to a new dialog box that is popped up whenever you choose a
command to add a new task to the list. This section shows you how to
define this dialog.
The method prompt-for-task
below creates and displays a dialog that
asks the user to type the text for a new task. The definition of
task-text
is very similar to the definition you provided in the
initial design, with the exception that the activate callback exits the
dialog, rather than calling the not-yet-implemented
function.
The method takes two keyword arguments: a title, which is assigned a
value by default, and an owner, which is used as the owner for the
dialog that is displayed by prompt-for-task
. Note that the title is
never explicitly set by any calls to prompt-for-task
in the task list
manager; it is provided here as an illustration of how you can provide a
default value for a keyword argument, rather than requiring that it
either always be passed in the call to the method, or that it be
hard-wired into the code.
The method returns two values: the name of the new task, that is, the text that the user types into the text field, and the priority of the new task.
Add this method to frame.dylan
.
Note
The definition of the prompt-for-task
method uses the
<priority>
type. Note that this type is defined in
Defining the underlying data structures for tasks. Until the relevant
code in Defining the underlying data structures for tasks is added
to your project, any attempt to build it will generate a serious warning.
define method prompt-for-task
(#key title = "Type text of new task", owner)
=> (name :: false-or(<string>),
priority :: false-or(<priority>))
let task-text = make(<text-field>,
label: "Task text:",
activate-callback: exit-dialog);
let priority-field = make(<radio-box>,
items: $priority-items,
label-key: first,
value-key: second,
value: #"medium");
let frame-add-task-dialog = make(<dialog-frame>,
title: title,
owner: owner,
layout: vertically ()
task-text;
priority-field
end,
input-focus: task-text);
if (start-dialog(frame-add-task-dialog))
values(gadget-value(task-text), gadget-value(priority-field))
end
end method prompt-for-task;
Notice that the dialog used in the prompt-for-task
method is created
inline within the method definition. In this particular case, the dialog
is only ever needed within the context of prompt-for-task
and so it is
not necessary to use define frame
to create a distinct class of frame
specifically for this dialog.
Note also that OK and Cancel buttons are generated automatically for a dialog box; you do not need to define them explicitly.
Later on, the activate callback you define for the add-button
pane
calls this method, then inserts the return value into the list
task-list
.