Contents

Swinging Duke
Feedback Button
Left ArrowRight Arrow

 

The JFC Undo API

This document describes the design of the Undo API in Swing.


The Need for an Undo Utility

Many developers have asked for an Undo/Redo application service. Undo operations are so common and so general that Undo and Redo capabilities have been added to the Swing toolkit.


Goals

Our goal in creating an Undo/Redo service was to design an architecture which:

  1. Would be general and flexible enough to extend to all types of applications.
  2. Would allow the component developer to determine the appropriate granularity of operations.
  3. Would be simple to use and extend.
  4. Would support JavaBeans.


Undo: The JFC Approach

Swing uses the JFC approach to Undo and Redo operations. When a component performs an undoable edit, it informs its UndoableEditListeners with an UndoableEditEvent. The listeners can ask the event for its UndoableEdit, which can be told to undo() and redo().

To provide a simple, one-shot Undo/Redo command, the only UndoableEdit that the listener is expected to hold onto is the last one that it has received. If an application needs to provide an Undo/Redo service that is less limited, the listener can hold onto all (or the last few) edits it has received, in the order that it received them. Then the listener can work back and forth along the queue.

JFC provides standard implementations of the UndoableEditListener and UndoableEditEvent interfaces, but specialized versions can be written for any application


The Undo API

This section provides details about the Swing Undo API.

The UndoableEditListener Interface and the UndoableEditEvent Class

In the Swing Undo API, the listener and the event are as straightforward, as usual:

public interface UndoableEditListener extends java.util.EventListener {
    void undoableEditHappened(UndoableEditEvent e);
}

public class UndoableEditEvent extends java.util.EventObject {
    UndoableEdit getEdit();
}

The UndoableEdit Interface

An UndoableEdit knows how to undo and redo some operations. It is possible for a long-lived UndoableEdit to become invalid, so an UndoableEdit must be able to report that it can no longer undo or redo. Edits should provide useful human-readable descriptions of themselves, suitable for display in a history log or as the Undo or Redo items in an Edit menu.

An application can ask Edits whether they can coalesce (see the addEdit() and replaceEdit() documentation for details). This capability allows the collapsing of sets of similar edits. It also makes it easier to implement complicated chains of undoable operations as chains of simpler ones.

Finally, Swing's Undo/Redo services make it possible to specify that an edit is insignificant -- that is, to specify that a particular edit should be done or undone as a side effect of a more important edit, rather than being presented to users as an undoable action in its own right. (For more on this topic, see Using Insignificant Edits, below.)

public interface UndoableEdit {
    /**
     * Undo the edit that was made.
     */
    public void undo() throws CannotUndoException;

    /**
     * True if it is still possible to undo this operation.
     */
    public boolean canUndo();

    /**
     * Re-apply this edit, assuming that it has been undone.
     */
    public void redo() throws CannotRedoException;

    /**
     * True if it is still possible to redo this operation.
     */
    public boolean canRedo();

    /**
     * This UndoableEdit should absorb an Edit if it can. Return true
     * if anEdit has been incorporated, false if it has not.
     *
     * Typically the receiver is already in the queue of a
     * JUndoManager (or other UndoableEditListener), and is being
     * given a chance to incorporate anEdit rather than letting it be
     * added to the queue in turn.
     *
     * If true is returned, from now on anEdit must return false from
     * canUndo() and canRedo(), and must throw the appropriate
     * exception on undo() or redo().
     */
    public boolean addEdit(UndoableEdit anEdit);

    /**
     * Return true if this UndoableEdit should replace anEdit. The
     * receiver should incorporate an Edit's state before returning true.
     *
     * This message is the opposite of addEdit--anEdit has typically
     * already been queued in a JUndoManager (or other
     * UndoableEditListener), and the receiver is being given a chance
     * to take its place.
     *
     * If true is returned, from now on anEdit must return false from
     * canUndo() and canRedo(), and must throw the appropriate
     * exception on undo() or redo().
     */
    public boolean replaceEdit(UndoableEdit anEdit);

    /**
     * Return false if this change is insignificant--for example one
     * that maintains the user's selection, but does not change any
     * model state. This status can be used by an UndoableEditListener
     * (like JUndoManager) when deciding which ChangeEvents to present
     * to the user as Undo/Redo options, and which to perform as side
     * effects of undoing or redoing other events.
     */
    public boolean isSignificant();

    /**
     * Provide a localized, human readable description of the change
     * suitable for use in, say, a change log.
     */
    public String getPresentationName();

    /**
     * Provide a localized, human readable description of the undoable
     * form of this change, e.g. for use as an Undo menu item. Typically
     * derived from getDescription();
     */
    public String getUndoPresentationName();

    /**
     * Provide a localized, human readable description of the redoable
     * form of this change, e.g. for use as a Redo menu item. Typically
     * derived from getPresentationName();
     */
    public String getRedoPresentationName();
}


The Standard Undo Implementation

JFC provides a set of basic classes to implement typical undo behavior. They are:


Example: Using Insignificant Edits

Using significant and insignificant edits is a common technique for maintaining the user's selection, or properly moving focus, as she uses Undo and Redo commands.

For example, read the following scenario of shifting focus. (As you read, please understand that the various edit objects mentioned below are presented for example purposes only; they are not standard JFC classes.)

  1. The user clicks in a 2D drawing bean and the bean takes focus.
    The draw bean emits an insignificant tookFocusEdit.
  2. The user deletes something.
    The draw bean emits a deletedSomethingEdit.
  3. The user clicks in the text editing bean next door, shifting focus
    The text bean emits an insignificant tookFocusEdit.
  4. The user types.
    The text bean emits a replaceSelWithTextEdit.
  5. Now the undo queue looks like this (* means "significant"):

    => replaceSelWithTextEdit*
    tookFocusEdit
    deletedSomethingEdit*
    tookFocusEdit
  6. The user chooses Edit|Undo, and the text typing is undone.

    replaceSelWithTextEdit*
    => tookFocusEdit
    deletedSomethingEdit*
    tookFocusEdit
  7. The user chooses Undo again, and each edit up to and including the next significant edit is sent undo(). So the drawing bean regains focus and then restores the deleted text.

    replaceSelWithTextEdit*
    tookFocusEdit
    deletedSomethingEdit*
    => tookFocusEdit
  8. Now the user chooses Redo twice. Each time the user chooses Redo, redo() is sent to each edit, up to and including the next significant edit. Thus, focus has gone to the right place, and we're back where we started:
    => replaceSelWithTextEdit*
      tookFocusEdit
      deletedSomethingEdit*
      tookFocusEdit

Think of this set of operations in combination with coalescing. If the focus edits know how to add or replace each other, the queue won't contain a record of the 25 other beans the user clicked in between deleting her graphic and typing her text.

Left Arrow Up Arrow Right Arrow


Version 0.5. Last modified 09/04/97.
Copyright © 1995-97 Sun Microsystems, Inc. All Rights Reserved.

Sun's Home Page