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:

  • LunchApplet -- Starting point for the undo applet
  • UndoPanel -- The main program class
  • AddEdit -- Captures the effect of adding elements to the list
  • RemoveEdit -- Captures the effect of removing elements from the list
  • AddAction -- Inner class of UndoPanel; the add command
  • RemoveAction -- Inner class of UndoPanel; the remove command

We'll begin with AddEdit.

001 class AddEdit extends AbstractUndoableEdit {
002
003     private Object element_;
004
005     private int index_;
006
007     private DefaultListModel model_;
008
009     public AddEdit(DefaultListModel model, Object element, int index) {
010          model_=model;
011
012          element_ = element;
013
014          index_=index;
015     }
016
017     public void undo() throws CannotUndoException {
018
019         model_.removeElementAt(index_);
020
021     }
022
023     public void redo() throws CannotRedoException {
024          model_.insertElementAt(element_,index_);
025     }
026
027     public boolean canUndo() { return true; }
028
029     public boolean canRedo() { return true; }
030
031     public String getPresentationName() { return "Add"; }
032
034 }

In the constructor (009-015) we store all the information needed to unexecute/re-execute an add action, including:

  • The element that was added to the list
  • The element index
  • The list model itself (the receiver): The DefaultListModel class is a simple Vector-like interface to accessing the contents of a Swing JList component

The undo() method (017-021) removes the element from the list, while redo() (023-025) inserts it back in. The getPresentationName() method, returns the name to be used for the undo and redo menu items. Note that if you inherit from AbstractUndoableEdit, Swing will handle the getUndoPresentationName() and getRedoPresentationName() return values by adding either "undo" or "redo" to the value returned from the getPresentationName() method.

Now let's examine the add operation itself. The following segment defines the action object attached to the Add button. Action is a new Swing interface that makes the UI the central point of control. That is, an action can be added directly to a toolbar (resulting in a new button), or to a menu (resulting in a new menu item). When the action changes one of its properties (for example, becomes enabled or disabled), the UI elements are notified and change their state accordingly. For example, when CutAction becomes disabled, both the Cut toolbar button and the Cut menu item will be disabled as well. In any case, the Action interface encapsulates the ActionListener interface for handling ActionEvent events, and a description of the action itself.

When added to a container that supports Action, like JToolBar or JMenu, the Action item is queried to determine details of the component to be produced and is then automatically registered for UI events. The container registers the new component as a PropertyListener of the action.

AbstractAction is a concrete implementation of this interface that provides default implementations of all the new methods. In this case, however, we use only the listener aspect of Action.

001 private class AddAction extends AbstractAction {
002
003     public void actionPerformed(ActionEvent evt) {
005         // always add to the end of the JList
006         int NumOfElements = elementModel_.getSize();
007         // however, give the element its ID number 
008         Object element = new String("Foo " + _lastElementID);
009         
010         // record the effect
011         UndoableEdit edit = new AddEdit(elementModel_,
012                                    element, NumOfElements);
013         // perform the operation
014         elementModel_.addElement(element);
015         
016         // notify the listeners
017         undoSupport_.postEdit(edit);
017          
018         // increment the ID
019          _lastElementID ++ ;
020
021     }
022
023 }

The AddAction class:

  • Creates a new element (008)
  • Creates a new AddEdit object and passes it the ListModel (the receiver of the action), the index of the new element, and the element itself (011)
  • Performs the actual add operation (014)
  • Notifies the undo listeners by calling postEdit on the undoSupport objects (017)

Note that the AddAction class is a private inner class of our undo applet. This approach guarantees direct access to private members of the applet (for example, undoSupport_). Generally, I prefer to define actions as inner classes of the object that handles them, both to prevent bloated interfaces and to avoid breaking encapsulation (by exposing the object's internal structure -- for instance, where an external add action might want access to the ListModel).

UI consistency

Of course, for a simple undo system, you will likely want to register the UndoManager as a sole listener to UndoableEvents, providing an UndoAction that shows only a generic Undo label and invoking undo() on the manager.

However, for a more sophisticated UI, you will want to provide the user with the last undoable operation. For instance, instead of just showing Undo you'll want to show Undo Cut.

The following code snippet shows how to provide an UndoAdaptor class that updates the state of the undo components (in this case, Undo and Redo buttons) according to the new state of the undo history list.

001 private class UndoAdaptor implements UndoableEditListener {
002
003     public void undoableEditHappened (UndoableEditEvent evt) {
004
005         UndoableEdit edit = evt.getEdit();
006
007         undoManager_.addEdit(edit);
008
009         refreshUndoRedo();
010     }
011 }

The UndoAdaptor is registered in the UndoableEditSupport during the application setup. Each time an undo event occurs, the adaptor:

  • Extracts the edit from the event (005)
  • Adds it to the UndoManager (007)
  • Refreshes the undo-related GUI state (009)

An alternative approach would be to implement a subclass of UndoManager that overrides the addEdit() method to automatically refresh our user interface.

Here's the refreshUndoRedo() method.

001 public void refreshUndoRedo() {
002
003     //refresh undo
004
005     undoBtn_.setText(undoManager_.getUndoPresentationName());
006     undoBtn_.setEnabled(undoManager_.canUndo());
007
008     // refresh redo 
009
010     redoBtn_.setText(undoManager_.getRedoPresentationName());
011     redoBtn_.setEnabled(undoManager_.canRedo()); 
012
013 } 

This method refreshes both the undo and the redo UI. The method retrieves the current edit information from the undoManager.

The Undo action

When the user presses the Undo button, the undo action is invoked, as shown next.

001 private class UndoAction extends AbstractAction {
002
003     public void actionPerformed(ActionEvent evt ) {
004
005         undoManager_.undo();
006
007         refreshUndoRedo();
008     }
009 }

All the complexity of managing the undo history list is handled by the UndoManager class. The undo operation simply invokes undo() on the manager and refreshes the applet GUI. We use the same code for the redo action, so I won't detail it here.

Wiring the parts together

In the application constructor, we set up the system.

001 public UndoPanel () {
002    // construct the actions
003    ActionListener undoAction = new undoAction();
004    ActionListener redoAction = new redoAction();
005   
006    // register the listener
007    undoBtn_ = new JButton("undo");
008    undoBtn_.addActionListener(undoAction);
009    redoBtn_ = new JButton("redo");
010    redoBtn_.addActionListener(redoAction);
011    
012
013    // initialize the undo.redo system
014    undoManager_= new UndoManager();
015    undoSupport_ = new UndoableEditSupport();
016    undoSupport_.addUndoableEditListener(new UndoAdapter());
017
018 }

Conclusion

The Command pattern of encapsulating user actions in an application as individual first-class objects is extremely useful. It allows us to localize the implementation of undo/redo facilities to individual classes that themselves perform and undo the changes. This approach greatly eases maintenance; when we change a command operation, the undo/redo code is nearby and is completely independent of the undo/redo user interface code.

Supporting undo/redo in your apps will provide your users with a sense of confidence as they learn how to manipulate the program. This article has shown you how easy it is to use Swing to implement such a feature. While it may be a small step for you as a developer, it is a huge step toward developing more complete and friendly applications.

Tomer Meshorer is a framework architect at Comverse Network Systems in Tel Aviv, Israel. He develops Java-based object-oriented frameworks for visual programming IDEs. Tomer is a certified JDK 1.1 programmer and is devoted to design patterns, frameworks and, of course, Java.

Learn more about this topic

  • Download the complete source as a gzipped TAR file http://www.javaworld.com/jw-06-1998/undoredo/jw-06-undoredo.tar.gz
  • Download the complete source as a ZIP file http://www.javaworld.com/jw-06-1998/undoredo/jw-06-undoredo.zip
  • Read Sun's Swing applet page to find out how to run Swing applets on Netscape and Internet Explorer http://java.sun.com/products/jfc/swingdoc-current/applets.html
  • Add 1.1 support to Netscape Communicator 4.0x with these step-by-step instructions for applying the 1.1 support patch http://developer.netscape.com/software/jdk/download.html
  • Download the JDK 1.1 preview release for Communicator 4.05 http://developer.netscape.com/software/jdk/download.html
  • Find out more about design patterns at the Pattern Web site http://hillside.net/patterns/patterns.html
  • If you don't already have a copy, pick up Design Patterns Elements of Reusable Object-Oriented Software (Addison-Wesley, ISBN 0-201-63361-2) to improve your understanding of design patterns http://hillside.net/patterns/DPBook/DPBook.html
  • Find out more about Unified Modeling Language at Rational Software's UML Resource Center http://www.rational.com/uml