Add an undo/redo function to your Java apps with Swing

Find out how the Swing GUI component set utilizes the Command pattern for easy support of undo/redo

Not too long ago, Sun's Java division introduced the JFC, a comprehensive set of UI components and foundation services that give Java developers more flexibility to determine the look and feel of their applications. The UI component library, called Swing, is a whole lot more than a stripped-down set of lightweight UI components. Swing takes Java applications a step forward by allowing easy implementation of application services like undo -- the topic of our discussion today.

Historically, application frameworks (for example, MacApp framework) have based the design of the undo/redo mechanism around the Command pattern. And that's what we're going to do as well. We'll discuss the Command pattern and describe how it supports the design of undo/redo systems. We'll then examine Java's support for the pattern and see how Swing's undo package adds the missing functionality, providing you with a complete undo/redo mechanism.

Requirements from an undo/redo mechanism

Undo allows users to correct their mistakes and also to try out different aspects of the application without risk of repercussions.

At minimum, an undo/redo mechanism should provide users with the ability to:

  • Unexecute (undo) the last action they just performed
  • Re-execute (redo) the last undone action
  • Undo and redo several recent actions (preferable, but optional)

In order to design such a mechanism, we must treat the user's operations as individual atomic actions (self-contained actions that know how to undo/redo their effect on the application state) that should be stored for undo or redo later on. We can fulfill these requirements by using design patterns, specifically the Command pattern. If you are already familiar with the design patterns (specifically the Command pattern and how Java supports it), you can skip the next three sections and move right to "The undo/redo mechanism in Swing."

Design patterns

In a nutshell, design patterns encourage design reuse by providing established, successful design solutions for particular situations (also known as contexts).

You may be wondering why I'm discussing design patterns in an article devoted to undo/redo mechanisms. It's really quite simple. In addition to presenting you with the technical implementation issues, I think it's important that you understand the design essentials of an undo/redo systems.

According to Design Patterns: Elements of Reusable Object-Oriented Software by the now infamous Gang of Four (see Resources for more information), the essential parts of any pattern are:

  • Intent -- The design goal that this pattern addresses
  • Applicability -- In what situation the pattern can be applied
  • Structure -- The design solution to the design problem
  • Consequences -- The trade-off of the solution

A number of different patterns exist, but we're concerned only with the one that addresses undo/redo capabilities: the Command pattern.

The Command pattern

Once again, according to Design Patterns, the purpose of the Command pattern is to:

Encapsulate a request as an object, thereby letting you parameterize clients with different requests, queue or log requests, and support undoable operations.

Let's see how this works.

At the heart of the pattern is the Command interface, which defines the execute() method.

001 public interface Command {
002
003    public void execute();
004
005 }

All user actions (such as insert text, cut, paste; any operation that the user can perform) are encapsulated as command objects. A command object is a class that implements the function of performing the user's requested operation. For example, in a text editor when the user types in some text, we do not add this text directly to the document within the UI code; instead we create an InsertTextCommand object that we then apply to the text document.

All command objects must implement the Command interface. This interface declares an execute() method that should invoke the actual command operation on the target object. The following snippet shows a cut command for a text editor application.

001 public class CutCommand implements Command{
002
003     private TextArea target_;
004
005     public CutCommand(TextArea area) {
006          target_ = area;
007     }
008
009     public execute() {
010
011 //       target_.cut();
012          int startPos = target_.getSelectionStart();
013          int endPos = target_.getSelectionEnd ();
014          String text = target_.getText ();
015          target_.setText (text.substring (0, startPos) + text.substring (endPos));
016
017     }
018
019 }

When a CutCommand object is created, it's initialized with its target -- in this case, a text area. To build a text editor with cut capabilities, we attach this CutCommand to the editor's Cut menu item with the text area as its target. When the Cut menu item is selected, the execute() method of this command object is automatically invoked and the cut operation is performed as desired.

From a design perspective, the important thing about the Command pattern is that it treats each user operation as a first-class object. If we add unexecute() and reexecute() methods to these objects (that is, to the Command interface), we can then support unexecution (undo) or re-execution (redo) of operations, giving us a basic undo/redo mechanism.

Of course, adding undo and redo capabilities to this mechanism requires "smarter" commands. Not only should a command be able to invoke an operation on its target, but it should also be able to undo or redo that operation when prompted. For example, the cut command would store the selected string and location before the cut, allowing it to return the selection to the text area if its undo() method is invoked.

Encapsulating each operation as a separate first-level object means that we can also easily support multilevel undo/redo operations: We store each command operation that the user performs in a history list. When the user selects undo, we perform the undo operation of the current item in the list and then step backwards to the previous item. To redo an operation, we execute the redo operation of the next item in the list and step forward. If the user performs a new command after some undo operations, we clear the front of the list to disable further redos.

However, because the Command pattern is attached to the menu item only at initialization, we must provide the means to duplicate commands and store them inside the history list. We can accomplish this using two different approaches:

  • Use the Prototype pattern -- Define the clone () method for each command. After the command is executed, it is cloned and stored inside the history list.

  • Separate the effect of the command from the actual execution -- When the command is executed, it creates a new "effect" object, which stores the effect of the command. This object is then stored in the history list.

The Swing designers chose the latter approach.

Java support for the Command pattern -- event listeners

The closest support that Java provides for the Command pattern is the AWT delegation event model. Within this framework, command objects implement AWT listener interfaces instead of the Command interface. To associate a command object with an AWT component, we simply register it as an event listener. The component knows that the listener interface is there but it doesn't care how you implement it; that is, it doesn't care what the actual command operation is.

For example, to support the cut operation we would provide an ActionListener that implements the actionPerformed() method (in place of execute()) to perform the cut operation, and would then register this with the Cut (MenuItem addActionListener()) method.

Unfortunately, this system does not support the easy undo/redo facilities of the Command pattern. To support undo, each operation must result in a separate command object being instantiated. These objects can then maintain local state information about the effects of the operation they performed. Within the AWT model, however, just a single command object is attached to each operation and is repeatedly invoked for the user's actions.

Enter Swing.

The undo/redo mechanism in Swing

In Swing, the effect of a user action is stored separately from the listener implementation. Each effect is stored in an object that implements the UndoableEdit interface. As before, just a single command object (event listener) is instantiated and registered with an AWT component; however, each time this command object is invoked, a new UndoableEdit object is created to describe the effect of the operation. These effect objects take the place of the local state information in traditional command objects.

The following is a Unified Modeling Language (UML) class diagram of Swing's undo mechanism. UML is an industry-standard language for specifying software systems; in this case, it is a useful standard for describing the classes of the Swing undo mechanism.

Swing's undo mechanism class diagram

Here's how it works.

As I mentioned a moment ago, the effect of a user action is stored in an UndoableEdit object. For convenience, Swing includes an AbstractUndoableEdit class that provides default implementations of the various methods of this interface. So instead of directly implementing UndoableEdit, you can simply subclass AbstractUndoableEdit and only implement those methods that you require.

For every type of edit you want to support (an edit being the effect of a command that the user can invoke, such as inserting or cutting text), you must provide a subclass of AbstractUndoableEdit that encapsulates information about the effect of the command and provides facilities for undoing this effect. In essence, the concept of a command object has been replaced by a listener class that implements the command operation and an edit class that encapsulates information about the effect of each execution of this command.

Sometimes, an edit will actually consist of a sequence of other, simpler edits (for example, a global-replace edit will be a sequence of individual replace edits). This type of edit can be captured by the CompoundEdit class. This class implements UndoableEdit and overrides the undo() method to invoke the undo() on each of its children in reverse order.

The next aspect of the Swing undo mechanism is the undo listener. Undo listeners are objects that implement the UndoableEditListener interface and are notified with UndoableEditEvent objects each time an undoable edit occurs. A special kind of listener is the UndoManager. When this listener is notified about an UndoableEditEvent, it extracts the edit from the event and stores it a queue. More about the UndoManager in a moment.

An application that supports undoable edits must provide the addUndoableEditListener() and removeUndoableEditListener() methods that allow UndoManagers to be registered.

Following the JavaBeans event model, Swing provides you with an UndoableEditSupport class to easily manage your listeners. You register your listeners with addUndoableListener() and deregister with removeUndoableListener(). To notify registered listeners of an edit, simply invoke the postEdit() method. This method automatically creates an UndoableEditEvent and passes it to each listener's undoableEditHappened() method. Additionally, this class provides simple support for performing compounds edits, allowing a sequence of simple edit operations to be automatically combined into a single CompoundEdit.

Finally, Swing provides convenient support for adding a multilevel undo function to your application by supplying the UndoManager class, which implements UndoableEditListener and acts as a history list. The UndoManager class saves you from having to manually store all the edits performed by the user; it automatically stores these edits and provides pointers to the current undo and redo edits. Each time an undoable edit occurs (for example, a new element is added to a list) the UndoManager is notified, and the edit is added to its internal queue. You can set the queue limits by calling setLimit() on this manager.

The following figure illustrates what happens inside the undo queue.

Inside the undo queue: We start from an empty queue, add some actions, perform undo, and then perform another action.

That's pretty much it. Now we can apply this foundation to a real example.

Hands on example

We're going to build a simple program to showcase Swing's undo/redo mechanism. Our applet, shown below, allows the user to add and remove elements to a JList component. When the user adds or removes an element, the applet stores the effect of that operation -- in either an AddEdit or RemoveEdit object -- for undo later.

Of course, to view and manipulate the applet, you must have Swing installed on your system. See Installing Swing for step-by-step instructions on the installation process.

CODEBASE = "." CODE = "undoapplet.LunchApplet.class" NAME = "TestApplet" WIDTH = "96" HEIGHT = "96" HSPACE = 0 VSPACE = 0> You need a Java-enabled browser to view this applet.

Note: You must use a JDK 1.1-compliant browser to access this applet. Options include Netscape Communicator 4.0x with the AWT 1.1 support patch, Netscape Communicator 4.05 pre-beta release for Windows 95/NT, and Appletviewer 1.1. (See Resources for links to upgrade Communicator 4.0x and the latest version of Communicator).

Enough talk, let's code.

The following classes comprise the example applet:

Related:
1 2 Page 1