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

1 2 Page 2
Page 2 of 2
  • 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
1 2 Page 2
Page 2 of 2