Welcome back to our series on a designing a drawing editor in Java. Before reading the article, you may want to have a look at the real thing. Try it out by downloading this JAR file
. All the source code will be released in the next article. As promised, in this part, we will have a look at the class organization, focusing on the actions and the central Draw class.
The Overall Architecture
We can think of a drawing editor as a concrete instance of a class of software systems specialized in creating and manipulating a population of objects that obey certain abstract rules. The design techniques in this series can be used in the design of just such a large class of systems, and are even reusable in completely different contexts.
To build a simple, yet professional, drawing editor, we need:
- A set of symbols that obey given rules, such as the ability to be cut and pasted, or the ability to fire events on their own. See part 1 of this series:
.) Here’s a typical scenario: the user triggers an action, for example, clicks on the button for cutting an image from the drawing; the action is executed; and some of its processing involves the canvas, that centralizes all the operations on symbols, the access to the clipboard, currently selected symbols, etc. The canvas performs the requested operations, eventually involving other classes, like the stack used for undoing actions and the mediator (part of the auxiliary elements introduced at the beginning) for coordinating the state of other widgets. We will see in detail the mediator in the next (and last) part of this series.
The Draw class has several attributes. The most important ones include:
- An array of symbols that records all the items added to the drawing. The order is meaningful, because it describes the painting order, so it affects the layering of one symbol on the other.
- The clipboard, is implemented as an array of
. Note that in the current implementation only a single selection is supported, so the clipboard is made up of only one symbol at a time.AbstractSymbols
- The currently selected symbols, holding references to some objects in the symbols array. Again, only a single selection is currently implemented.
- Some additional helper members discussed below.
To recap: the
asks theCut Action
class to cut the currently selected symbol. TheDraw
itself performs some internal processing, and then calls the mediator that recalculates the state of all the affected widgets (for example, the paste icon will get enabled). As you can see, the overall class interaction is clear and well-defined.Draw
We have the symbols and we have their container, but we still need a user. Many choices are possible; here we’ll use an action-centric design to represent user input. Let’s take a look.
Actions and Undo Support
We saw that
are, in a way the user’s extensions into our software. The user manipulates symbols by means ofActions
. Clearly, ourActions
class extends the standardAction
class. Because we designed ourAbstractAction
class as the symbol manipulator class, every command dealing with symbols should invoke theDraw
instance’s methods accordingly.Draw
Each
provides two basic methods:Action
to execute that commandActionPerformed()
to neutralize itself, once executedUndo()
Let’s see the structure of a typical
suited for our class framework:Action
public final class Cut extends Action {
This action has one operand, the symbol to be cut:
private AbstractSymbol symbol;
The constructor specifies the referring Draw instance:
public Cut(Draw d) { super(IconRepository.CUT_ICON,d); mnemonic = ‘x’; toolTipText = “cuts the selected symbol”; }
The command itself:
public void actionPerformed(ActionEvent e) { super.actionPerformed(e);//to register for undo draw.cutSelectedSymbol(); symbol = draw.getClipboard(); }
Note the call to the
of its superclassactionPerformed()
; without that line of code the action wouldn’t get registered for undo.
Invoked when undoing this action:
public void undo() { symbol.setSelected(false); draw.add(symbol); } }
Undo Support
We adopted a very simple form of undo support for two reasons: First, standard undo classes are implemented only for text-related components; second, we wanted to create a very simple and effective mechanism, not to reinvent a general purpose one.
Actions that support undo (not all the actions provide undo, for example the
action) must save information about the current instances affected by their operation, i.e., their operands. In this way theSave
method can access the operands and restore the situation that existed before the command was executed. Using the stack, we can pop past actions as needed, calling theirundo()
method to restore the situation from just before theirundo()
was executed.actionPerformed()
The
itself is a command just like any other. Note that with this design, the redo command is straightforward to implement, just expanding the class that stores the executed actions.Undo
For the sake of convenience, all the actions are gathered together in a repository, from which they are taken as needed. This kind of organization works quite well for large numbers of similar classes that have short code implementations.
Finally, a side note on the function of our
class. We stretched the standard implementation ofAction
, as provided by those nice guys at Sun, in two ways: First, we specialized it for fitting in our class framework; see, for instance, theAbstractAction
reference every Action has; second, we modified the meaning of an action slightly; keeping the operands self-contained makes it a recorded command, rather than an executable action. Leveraging existing class frameworks in this way eased the design process enormously. Dealing with class frameworks is always a two-faced coin: In order to make the most of them, developers need to understand their intended approach to problems, rather than just cutting and pasting code from somewhere else. That is more difficult at first, of course. But it pays off in the long run.Draw
Unfortunately, the trend in modern programming languages is toward more complex, abstract, and powerful APIs, where classes are knitted together to create powerful class frameworks.
In the next article, we’ll explore the chosen mediator implementation, the remaining classes, and some implementation issues.
About the Author
Mauro Marinilli has been a Java developer since the language’s early days. He’s also active in academic research, mainly in information filtering, case-based reasoning, and adaptive hypermedia. He is currently working as chief engineer for the GUI team developing the new Italian Air Force Meteorological and Forecasting System.