User Operations
Some of Lexi's functionality is available through the document'sWYSIWYG
representation. You enter and delete text, move the insertionpoint, and select
ranges of text by pointing, clicking, and typingdirectly in the document. Other
functionality is accessed indirectlythrough user operations in Lexi's pull-down
menus, buttons, andkeyboard accelerators. The functionality includes operations
for
• creating a new document,
• opening, saving, and printing an existing document,
• cutting selected text out of the document and pasting it back in,
• changing the font and style of selected text,
• changing the formatting of text, such as its alignment andjustification,
• quitting the application,
• and on and on.
Lexi provides different user interfaces for these [Link] we don't want
to associate a particular user operation with aparticular user interface, because
we may want multiple userinterfaces to the same operation (you can turn the page
using either apage button or a menu operation, for example). We may also want
tochange the interface in the future.
Furthermore, these operations are implemented in many differentclasses. We as
implementors want to access their functionalitywithout creating a lot of
dependencies between implementation and userinterface classes. Otherwise we'll
end up with a tightly coupledimplementation, which will be harder to understand,
extend, andmaintain.
To further complicate matters, we want Lexi to support undo andredo8ofmost but
not all its functionality. Specifically, we want to beable to undo
document-modifying operations like delete, with which auser can destroy lots of
data inadvertently. But we shouldn't try toundo an operation like saving a drawing
or quitting the [Link] operations should have no effect on the undo
process. We alsodon't want an arbitrary limit on the number of levels of undo
andredo.
It's clear that support for user operations permeates the [Link]
challenge is to come up with a simple and extensible mechanismthat satisfies all
of these needs.
Encapsulating a Request
From our perspective as designers, a pull-down menu is just anotherkind of glyph
that contains other glyphs. What distinguishespull-down menus from other glyphs
that have children is that mostglyphs in menus do some work in response to an
up-click.
Let's assume that these work-performing glyphs are instances of aGlyph subclass
called MenuItem and that they do their work inresponse to a request from a
client.9Carrying out therequest might involve an operation on one object, or many
operationson many objects, or something in between.
We could define a subclass of MenuItem for every user operation andthen hard-code
each subclass to carry out the request. But that's notreally right; we don't need
a subclass of MenuItem for each requestany more than we need a subclass for each
text string in a pull-downmenu. Moreover, this approach couples the request to
a particularuser interface, making it hard to fulfill the request through
adifferent user interface.
To illustrate, suppose you could advance to the last page in thedocument both
through a MenuItem in a pull-down menu and bypressing a page icon at the bottom
of Lexi's interface (which mightbe more convenient for short documents). If we
associate the requestwith a MenuItem through inheritance, then we must do the
same for thepage icon and any other kind of widget that might issue such arequest.
That can give rise to a number of classes approaching theproduct of the number
of widget types and the number of requests.
What's missing is a mechanism that lets us parameterize menu items bythe request
they should fulfill. That way we avoid a proliferation ofsubclasses and allow
for greater flexibility at run-time. We couldparameterize MenuItem with a function
to call, but that's not a completesolution for at least three reasons:
1. It doesn't address the undo/redo problem.
2. It's hard to associate state with a function. For example, afunction that
changes the font needs to know which font.
3. Functions are hard to extend, and it's hard to reuse parts of them.
These reasons suggest that we should parameterize MenuItems with anobject, not
a function. Then we can use inheritance to extendand reuse the request's
implementation. We also have a place to storestate and implement undo/redo
functionality. Here we have anotherexample of encapsulating the concept that
varies, in this case arequest. We'll encapsulate each request in a commandobject.
Command Class and Subclasses
First we define a Command abstract class toprovide an interface for issuing a
request. The basic interfaceconsists of a single abstract operation called
"Execute." Subclassesof Command implement Execute in different ways to fulfill
differentrequests. Some subclasses may delegate part or all of the work toother
objects. Other subclasses may be in a position to fulfillthe request entirely
on their own (see Figure 2.11).To the requester, however, a Command object is
a Command object—theyare treated uniformly.
Figure 2.11: Partial Command class hierarchy
Now MenuItem can store a Command object that encapsulates arequest (Figure 2.12).
We give each menu item objectan instance of the Command subclass that's suitable
for that menuitem, just as we specify the text to appear in the menu item. Whena
user chooses a particular menu item, the MenuItem simply callsExecute on its
Command object to carry out the request. Note thatbuttons and other widgets can
use commands in the same way menuitems do.
Figure 2.12: MenuItem-Command relationship
Undoability
Undo/redo is an important capability in interactive applications. Toundo and redo
commands, we add an Unexecute operation to Command'sinterface. Unexecute reverses
the effects of a preceding Executeoperation using whatever undo information
Execute stored. In the caseof a FontCommand, for example, the Execute operation
would store therange of text affected by the font change along with the
originalfont(s). FontCommand's Unexecute operation would restore the range oftext
to its original font(s).
Sometimes undoability must be determined at run-time. A request tochange the font
of a selection does nothing if the text alreadyappears in that font. Suppose the
user selects some text and thenrequests a spurious font change. What should be
the result of asubsequent undo request? Should a meaningless change cause the
undorequest to do something equally meaningless? Probably not. If theuser repeats
the spurious font change several times, he shouldn't haveto perform exactly the
same number of undo operations to get back tothe last meaningful operation. If
the net effect of executing acommand was nothing, then there's no need for a
corresponding undorequest.
So to determine if a command is undoable, we add an abstractReversible operation
to the Command interface. Reversible returns aBoolean value. Subclasses can
redefine this operation to return trueor false based on run-time criteria.
Command History
The final step in supporting arbitrary-level undo and redo is todefine a command
history, or list of commands that havebeen executed (or unexecuted, if some
commands have been undone).Conceptually, the command history looks like this:
Each circle represents a Command object. In this case the user hasissued four
commands. The leftmost command was issued first, followedby the second-leftmost,
and so on until the most recently issuedcommand, which is rightmost. The line
marked "present" keeps trackof the most recently executed (and unexecuted)
command.
To undo the last command, we simply call Unexecute on the most recentcommand:
After unexecuting the command, we move the "present" line onecommand to the left.
If the user chooses undo again, the next-mostrecently issued command will be undone
in the same way, and we're leftin the state depicted here:
You can see that by simply repeating this procedure we get multiplelevels of undo.
The number of levels is limited only by the length ofthe command history.
To redo a command that's just been undone, we do the same thing inreverse. Commands
to the right of the present line are commands thatmay be redone in the future.
To redo the last undone command, we callExecute on the command to the right of
the present line:
Then we advance the present line so that a subsequent redo will callredo on the
following command in the future.
Of course, if the subsequent operation is not another redo but an undo,then the
command to the left of the present line will be undone. Thusthe user can effectively
go back and forth in time as needed torecover from errors.
Command Pattern
Lexi's commands are an application of the Command (263) pattern, which describes
how toencapsulate a request. The Command pattern prescribes a uniforminterface
for issuing requests that lets you configure clients tohandle different requests.
The interface shields clients from therequest's implementation. A command may
delegate all, part, or noneof the request's implementation to other objects. This
is perfect forapplications like Lexi that must provide centralized access
tofunctionality scattered throughout the application. The pattern alsodiscusses
undo and redo mechanisms built on the basic Commandinterface.