18.2 The UndoManager Class


UndoManager is an extension of CompoundEdit that can track a history of edits, allowing them to be undone or redone one at time. Additionally, it implements UndoableEditListener by calling addEdit( ) each time an UndoableEditEvent is fired. This allows a single UndoManager to be added as a listener to many components that support undo, providing a single place to track all edits and populate an undo menu for the entire application.

It may seem a bit strange that UndoManager extends CompoundEdit. We'll explain why shortly, but first it's important to understand the primary ways in which UndoManager acts differently than CompoundEdit. For starters, when you add an edit to an UndoManager, it is placed in a list of edits available for undo. When you call undo( ), only the first (significant) edit is undone. This is different from the behavior of CompoundEdit, in which a call to undo( ) results in a call to undo( ) on all of the added edits.

Another major difference between UndoManager and its superclass is the semantics of the inProgress property. In CompoundEdit, we could add new edits only when we were inProgress, and only after calling end( ) could undo( ) or redo( ) be called. In contrast, UndoManager allows undo( ) and redo( ) to be called while it is inProgress. Furthermore, when end( ) is called, it stops supporting sequential undo/redo behavior and starts acting like CompoundEdit (undo( ) and redo( ) call their superclass implementations when the UndoManager is not inProgress).

For the strong-hearted,[3] Figure 18-6 shows a state chart for the UndoManager class.[4] For several reasons, this chart is considerably more complicated than the ones in the previous sections. First, as mentioned earlier, UndoManager has the curious behavior that once end( ) is called, it begins to act (for the most part) like a CompoundEdit. This is why we have the transition from the inProgress state to a new superstate (notInProgress, for lack of a better name), the contents of which look just like the CompoundEdit state chart (see Figure 18-4).

[3] All others might want to skip ahead to the description of Figure 18-7.

[4] This chart assumes that all edits are significant. For details on why this is important, see the descriptions of the editToBeUndone( ) and editToBeRedone( ) methods later in this section.

Figure 18-6. UndoManager state chart
figs/swng2.1806.gif

This state chart is also complicated because, within the inProgress state, whether we are undoable, redoable, or both (undoableOrRedoable) depends on whether all of the edits have been undone or redone. For example, if there are two edits in the UndoManager and we've undone one, we are undoableOrRedoable. We can undo the remaining edit, or redo the one we've undone. If we choose to undo the remaining edit, we go from undoableOrRedoable to redoable since there are no more edits to undo. However, if there were still more undoable edits, we'd have stayed in the undoableOrRedoable state. Or, if we'd chosen to redo the undone edit, there would be no more redoable edits, so we'd go from undoableOrRedoable to undoable.

Another factor that contributes to the complexity is that any time we add a new edit, we are no longer able to redo past undos because the new edit takes the place of the last undone edit, and all pending redoable edits are dropped. Therefore, any time we add an edit, we go to the undoable state.

18.2.1 A Codeless Example

Figure 18-7 attempts to simplify the explanation of UndoManager state transitions by showing how the UndoManager handles additions, undos, and redos for a sample scenario. This example shows that the most typical use of UndoManager is straightforward, despite all its complexity. We add three edits to the UndoManager and then undo each of them. We then redo the first edit. At this point, we could redo the second edit or undo the first edit again. In the example, we instead add a new edit. Adding an edit causes any edits that appear later in the list (those edits that originated latest) to be lost. In this example, that causes our initial second and third edits to be dropped from the manager before the new edit is added. Finally, we undo this new edit.

Figure 18-7. UndoManager example
figs/swng2.1807.gif

18.2.2 Transformer?

Probably the most nonintuitive things about the design of UndoManager are its extension from CompoundEdit and the fact that after its end( ) method is called, an UndoManager is essentially transformed into a CompoundEdit. The idea is to use an UndoManager in a temporary capacity during a specific editing task and then later be able to treat all of the edits given to the UndoManager as a single CompoundEdit.

As an example, consider a spreadsheet application. The process of editing the formula for a single cell can be managed by an UndoManager, allowing small changes to be undone and redone. Once the formula has been committed, the UndoManager's end( ) method can be called, and the manager begins to act like a CompoundEdit. This edit can then be handed off to a primary undo manager, allowing the entire formula change to be undone as a single edit.

18.2.3 Properties

UndoManager defines the properties shown in Table 18-6.

Table 18-6. UndoManager properties

Property

Data type

get

(get)

is

set

(set)

Default value

limit

int

·

 

·

100

redoPresentationNameo

String

·

   

"Redo"

undoOrRedoPresentationName

String

·

   

"Undo"

undoPresentationNameo

String

·

   

"Undo"

ooverridden

See also properties from the CompoundEdit class (Table 18-4).

The limit property represents the maximum number of edits the UndoManager can hold. Setting this value so that limit is less than the current number of edits in the manager causes the list of edits to be reduced to fit inside the new limit. The strategy for decreasing the list size is described later in this chapter.

If the manager is inProgress, the redoPresentationName and undoPresentationName properties are set to the values returned by the next edit to be redone or undone, respectively. If redo or undo is not possible, AbstractUndoEdit.Redo or AbstractUndoableEdit.Undo is returned. If the manager is not inProgress, these values revert to the values defined by CompoundEdit.

A new property, undoOrRedoPresentationName, is defined in this class. This property is used only when limit is set to 1. It returns the value of undoPresentationName if the single edit was not undone, or the value of redoPresentationName if it was.

18.2.4 Constructor

public UndoManager( )

Create a new manager containing no edits, with a limit of 100.

18.2.5 UndoableEditListener Method

This method is defined in the UndoableEditListener interface implemented by UndoManager:

public void undoableEditHappened(UndoableEditEvent e)

Call addEdit( ), passing in the UndoableEdit stored in the input event.

18.2.6 UndoableEdit Methods

UndoManager overrides the following UndoableEdit methods:

public synchronized boolean addEdit(UndoableEdit anEdit)

If the manager is inProgress, this method adds anEdit at the current insertion point. Any undone edits are removed from the manager, and die( ) is called on each of them in reverse order (the last edit added is killed first). If the manager is not inProgress, this method returns false.

public synchronized boolean canRedo( )

If the UndoManager is inProgress, this method uses editToBeRedone( ) (described later) to find the next significant redoable edit. If an edit is found, and it returns true when canRedo( ) is called on it, this method returns true. If the manager is not inProgress, super.canRedo( ) is called, making the manager act like a CompoundEdit.

public synchronized boolean canUndo( )

If the UndoManager is inProgress, this method uses editToBeUndone( ) to find the next significant undoable edit. If an edit is found and returns true when canUndo( ) is called on it, this method returns true. If the manager is not inProgress, super.canUndo( ) is called, making the manager act like a CompoundEdit.

public synchronized void redo( ) throws CannotRedoException

If the UndoManager is inProgress, this method uses editToBeRedone( ) to find the next significant redoable edit (the most recently undone edit). If an edit is found, redoTo( ) is called to redo all edits up to the next significant one. If no edit is found, an exception is thrown. If the manager is not inProgress, super.redo( ) is called, making the manager act like a CompoundEdit.

public synchronized void undo( ) throws CannotUndoException

If the UndoManager is inProgress, this method uses editToBeUndone( ) to find the next significant undoable edit. If an edit is found, undoTo( ) is called to undo all edits up to the next significant one. If no edit is found, an exception is thrown. If the manager is not inProgress, super.undo( ) is called, making the manager act like a CompoundEdit.

Calling undo( ) and then calling redo( ) does not necessarily put you right back where you started. Any insignificant edits undone by the undo( ) call are not redone by a subsequent redo( ) call.

18.2.7 Public Methods

The following methods are introduced in the UndoManager class:

public synchronized void discardAllEdits( )

Remove all edits from the UndoManager, calling die( ) on them in the order they were added.

public synchronized boolean canUndoOrRedo( )

This method is intended to be used when the manager is limited to holding a single edit (limit == 1). If the edit has been undone, it returns the result of a call to canRedo( ); otherwise, it returns the result of a call to canUndo( ).

public synchronized void undoOrRedo( ) throws CannotRedoException, CannotUndoException

This method is intended to be used when the manager is limited to holding a single edit (limit == 1). If the edit has been undone, it calls redo( ); otherwise, it calls undo( ).

18.2.8 Protected Methods

These methods are used internally by the UndoManager to manage its list of edits. They provide support for ignoring insignificant edits and removing edits that are no longer accessible.

protected UndoableEdit editToBeRedone( )

Return the next edit to be redone. This is simply the last significant edit that was undone. Any insignificant edits are skipped. If there are no significant redoable edits available, this method returns null.

protected UndoableEdit editToBeUndone( )

Return the next edit to be undone. This is the last significant edit that was either redone or added. Any insignificant edits are skipped. If there are no significant undoable edits available, this method returns null.

protected void trimEdits(int from, int to)

Remove the specified range of edits from the manager (if from is greater than to, it does nothing). The die( ) method is called on each removed edit in reverse order (to down to from). If the insertion point was within the trimmed range, it is reset to the value of from.

protected void trimForLimit( )

Reduce the number of edits to fit within the set limit for this manager. If the number of edits is not greater than limit, it does nothing. Otherwise, it removes edits from either end of the list (or both), trying to end up with equal numbers of undoable and redoable edits (or as close as possible). For example, if there are 10 edits, half of which had been undone, and limit is reduced to 6, the first 2 undone edits (those that were undone first) and the first 2 edits added (those that would be undone last) are removed. This leaves six edits (the new limit), three of which have been undone.

protected void redoTo(UndoableEdit edit) throws CannotRedoException

Start with the last undone edit and call redo( ) on each edit in the list, stopping after calling redo( ) on the input edit. An ArrayIndexOutOfBoundsException will be thrown if the input edit is not found before reaching the end of the edit list.

protected void undoTo(UndoableEdit edit) throws CannotUndoException

Start with the last redone or added edit and call undo( ) on each edit in the list, stopping after calling undo( ) on the input edit. An ArrayIndexOutOfBoundsException will be thrown if the input edit is not found before reaching the beginning of the edit list.

18.2.9 Using an Undo Manager

In the previous examples, we created UndoableEdits in our main program each time we were notified of an action that we wanted to allow the user to undo. A more desirable strategy is to make the component that generated the action responsible for creating the UndoableEdit and firing an UndoableEditEvent, passing us the edit. Using an UndoManager, we can then easily provide the user with the ability to undo and redo as many changes as necessary.

For this example to work, we need to provide a component that generates UndoableEdits and allows UndoableEditListeners to be added and removed. In keeping with the examples provided so far in this chapter, we'll do this by creating an extension of JToggleButton that fires an UndoableEditEvent each time its state is toggled. This event will contain an UndoableToggleEdit (the class introduced in Section 18.1.2.5) that can be used to undo the toggle. To keep the example as simple as possible, we'll allow only a single listener to be added to the button. In a real application, you should maintain a list of interested listeners instead.[5]

[5] Later in the chapter, we'll introduce UndoableEditSupport, a class that simplifies this process.

Here's the code for this event-generating button class:

// UndoableJToggleButton.java // import java.awt.event.*; import javax.swing.*; import javax.swing.event.*; import javax.swing.undo.*; // Sample undoable toggle button class. Supports only a single listener to // simplify the code. public class UndoableJToggleButton extends JToggleButton {   private UndoableEditListener listener;   // For this example, we'll just provide one constructor.   public UndoableJToggleButton(String txt) {     super(txt);   }   // Set the UndoableEditListener.   public void addUndoableEditListener(UndoableEditListener l) {     listener = l; // Should ideally throw an exception if listener != null   }   // Remove the UndoableEditListener.   public void removeUndoableEditListener(UndoableEditListener l) {     listener = null;   }   // We override this method to call the super implementation first (to fire the   // action event) and then fire a new UndoableEditEvent to our listener.   protected void fireActionPerformed(ActionEvent ev) {     // Fire the ActionEvent as usual.     super.fireActionPerformed(ev);     if (listener != null) {       listener.undoableEditHappened(new UndoableEditEvent(this,         new UndoableToggleEdit(this)));     }   } }

As you can see, all we've done here is override fireActionPerformed( ) so that each time an ActionEvent is fired (indicating that the button was toggled), we also create and fire a new UndoableEditEvent. Of course, the strategy for generating edits varies considerably based on the type of class you're making undoable.

Now let's look at a program that uses an UndoManager to allow the undo of multiple toggle button edits. In this example, we'll create three UndoableJToggleButtons and provide undo and redo buttons that allow the user to undo and redo up to 100 (the default limit) button toggles.

This example doesn't take advantage of the fact that UndoManager implements UndoableEditListener by adding the manager as a listener to our undoable buttons. We want to do more than track the edit when it is generated; we also want to update the user interface so that the user knows that the undo and redo options are available. To support this, we instead add our own UndoableEditListener inner class, calling addEdit( ) on the UndoManager each time an event is fired and then updating our undo and redo buttons appropriately.

Lack of listener support has been identified by the Swing team as an important hole in the current UndoManager. Look for more support in this area in a future release. At the end of the chapter, we show how you can extend the current UndoManager to give it better listener support.

Here's the source code, again similar in structure to the previous examples:

// UndoableToggleApp3.java // import javax.swing.*; import javax.swing.event.*; import javax.swing.undo.*; import java.awt.*; import java.awt.event.*; // A sample app showing the use of UndoManager public class UndoableToggleApp3 extends JFrame {   private UndoManager manager = new UndoManager( );   private JButton undoButton;   private JButton redoButton;   // Create the main frame and everything in it.   public UndoableToggleApp3( ) {     // Create some toggle buttons.     UndoableJToggleButton tog1 = new UndoableJToggleButton("One");     UndoableJToggleButton tog2 = new UndoableJToggleButton("Two");     UndoableJToggleButton tog3 = new UndoableJToggleButton("Three");          // Add our listener to each toggle button.     SimpleUEListener sl = new SimpleUEListener( );     tog1.addUndoableEditListener(sl);     tog2.addUndoableEditListener(sl);     tog3.addUndoableEditListener(sl);     // Lay out the buttons.     Box buttonBox = new Box(BoxLayout.Y_AXIS);     buttonBox.add(tog1);     buttonBox.add(tog2);     buttonBox.add(tog3);     // Create undo and redo buttons (initially disabled).     undoButton = new JButton("Undo");     redoButton = new JButton("Redo");     undoButton.setEnabled(false);     redoButton.setEnabled(false);     // Add a listener to the undo button. It attempts to call undo( ) on the     // UndoManager, then enables/disables the undo/redo buttons as appropriate.     undoButton.addActionListener(new ActionListener( ) {       public void actionPerformed(ActionEvent ev) {         try {           manager.undo( );         } catch (CannotUndoException ex) { ex.printStackTrace( ); }         finally {           updateButtons( );         }       }     });     // Add a redo listener, which is just like the undo listener.     redoButton.addActionListener(new ActionListener( ) {       public void actionPerformed(ActionEvent ev) {         try {           manager.redo( );         } catch (CannotRedoException ex) { ex.printStackTrace( ); }         finally {           updateButtons( );         }       }     });     // Lay out the undo/redo buttons.     Box undoRedoBox = new Box(BoxLayout.X_AXIS);     undoRedoBox.add(Box.createGlue( ));     undoRedoBox.add(undoButton);     undoRedoBox.add(Box.createHorizontalStrut(2));     undoRedoBox.add(redoButton);     undoRedoBox.add(Box.createGlue( ));     // Lay out the main frame.     getContentPane( ).setLayout(new BorderLayout( ));     getContentPane( ).add(buttonBox, BorderLayout.CENTER);     getContentPane( ).add(undoRedoBox, BorderLayout.SOUTH);     setSize(400, 150);   }   public class SimpleUEListener implements UndoableEditListener {     // When an UndoableEditEvent is generated (each time one of the buttons is     // pressed), we add it to the UndoManager and then get the manager's undo/redo     // names and set the undo/redo button labels. Finally, we enable/disable these     // buttons by asking the manager what we are allowed to do.     public void undoableEditHappened(UndoableEditEvent ev) {       manager.addEdit(ev.getEdit( ));       updateButtons( );     }   }   // Method to set the text and state of the undo/redo buttons   protected void updateButtons( ) {     undoButton.setText(manager.getUndoPresentationName( ));     redoButton.setText(manager.getRedoPresentationName( ));     undoButton.getParent( ).validate( );     undoButton.setEnabled(manager.canUndo( ));     redoButton.setEnabled(manager.canRedo( ));   }   // Main program just creates the frame and displays it   public static void main(String[] args) {     JFrame f = new UndoableToggleApp3( );     f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);     f.setVisible(true);   } } 

Figure 18-8 shows the application running. Before taking this screenshot, we toggled each of the buttons in order and then undid the third toggle. Notice that we can now retoggle button Three or undo the previous toggle (button Two).

Figure 18-8. Undoing and redoing toggle buttons
figs/swng2.1808.gif

18.2.10 Understanding the UndoManager

There are a lot of subtle details about UndoManager that may be hard to understand without seeing them in action. In this section, we'll try to provide a concrete example of how all these little things work. To do so, let's create a very simple UndoableEdit implementation that is not associated with any component. It will help us see what the UndoManager is doing in certain situations. All this class does is output various bits of useful information when its methods are called:

// SampleUndoableEdit.java // import javax.swing.undo.*; import java.util.*; public class SampleUndoableEdit extends AbstractUndoableEdit {   private boolean isSignificant;   private boolean isReplacer;   private int number;   private boolean allowAdds;   private Vector addedEdits;   private UndoableEdit replaced;   // Create a new edit with an identifying number. The boolean arguments define   // the edit's behavior.   public SampleUndoableEdit(int number, boolean allowAdds,                             boolean isSignificant,                             boolean isReplacer) {     this.number = number;     this.allowAdds = allowAdds;     if (allowAdds)       addedEdits = new Vector( );     this.isSignificant = isSignificant;     this.isReplacer = isReplacer;   }   // "Undo" the edit by printing a message to the screen.   public void undo( ) throws CannotUndoException {     super.undo( );     System.out.print("Undo " + number);     dumpState( );   }   // "Redo" the edit by printing a message to the screen.   public void redo( ) throws CannotRedoException {     super.redo( );     System.out.print("Redo " + number);     dumpState( );   }   // If allowAdds is true, we store the input edit. If not, just return false.   public boolean addEdit(UndoableEdit anEdit) {     if (allowAdds) {       addedEdits.addElement(anEdit);       return true;     }     else       return false;   }   // If isReplacer is true, we store the edit we are replacing.   public boolean replaceEdit(UndoableEdit anEdit) {     if (isReplacer) {       replaced = anEdit;       return true;     }     else       return false;   }   // Significance is based on constructor parameter.   public boolean isSignificant( ) {     return isSignificant;   }   // Just return our identifier.   public String toString( ) {     return "<" + number + ">";   }   // Debug output.   public void dumpState( ) {     if (allowAdds && addedEdits.size( ) > 0) {       Enumeration e = addedEdits.elements( );       System.out.print(" (absorbed: ");       while (e.hasMoreElements( )) {         System.out.print(e.nextElement( ));       }       System.out.print(")");     }     if (isReplacer && replaced != null) {       System.out.print(" (replaced: " + replaced + ")");     }     System.out.println( );   } }

In our main program, we'll add instances of this new edit class to an UndoManager to show how different features work. We won't step through this program line by line. The comments in the code and in the output serve as an explanation of the different UndoManager features (and quirks) being shown:

// UndoManagerDetails.java // import javax.swing.undo.*; // An example that shows lots of little UndoManager details public class UndoManagerDetails {   public static void main(String[] args) {     UndoManager mgr = new UndoManager( );     // Show how insignificant edits are skipped over.     //     //                                 #  adds?  sig?  replace?     mgr.addEdit(new SampleUndoableEdit(1, false, true, false));     mgr.addEdit(new SampleUndoableEdit(2, false, true, false));     mgr.addEdit(new SampleUndoableEdit(3, false, false, false));     mgr.addEdit(new SampleUndoableEdit(4, false, false, false));     System.out.println("--------------------------");     System.out.println("Insignificant edit example");     System.out.println("--------------------------");     mgr.undo( );     mgr.redo( );     System.out.println(mgr.canRedo( )); // No more sig. edits     // Show how edits that call add/replace are used.     //     //                                 #  adds?  sig?  replace?     mgr.addEdit(new SampleUndoableEdit(5, true,  true, false));     mgr.addEdit(new SampleUndoableEdit(6, false, true, false));     System.out.println("----------------------------------");     System.out.println("Absorbed (by addEdit) edit example");     System.out.println("----------------------------------");     mgr.undo( );     mgr.discardAllEdits( );     //                                 #  adds?  sig?  replace?     mgr.addEdit(new SampleUndoableEdit(1, false, true, false));     mgr.addEdit(new SampleUndoableEdit(2, false, true, true));     System.out.println("--------------------------------------");     System.out.println("Absorbed (by replaceEdit) edit example");     System.out.println("--------------------------------------");     mgr.undo( );     System.out.println(mgr.canUndo( ));     // Show how changing limit works.     mgr.discardAllEdits( );     //                                 #  adds?  sig?  replace?     mgr.addEdit(new SampleUndoableEdit(1, false, true, false));     mgr.addEdit(new SampleUndoableEdit(2, false, true, false));     mgr.addEdit(new SampleUndoableEdit(3, false, true, false));     mgr.addEdit(new SampleUndoableEdit(4, false, true, false));     mgr.addEdit(new SampleUndoableEdit(5, false, true, false));     mgr.addEdit(new SampleUndoableEdit(6, false, true, false));     System.out.println("----------------------");     System.out.println("Changing limit example");     System.out.println("----------------------");     mgr.undo( );     mgr.undo( );     mgr.undo( );      // Now 3 undoable, 3 redoable     mgr.setLimit(4); // Now 2 undoable, 2 redoable!     while (mgr.canUndo( ))       mgr.undo( );     while (mgr.canRedo( ))       mgr.redo( );     // undoOrRedo example     mgr.discardAllEdits( );     mgr.setLimit(1);     //                                 #  adds?  sig?  replace?     mgr.addEdit(new SampleUndoableEdit(1, false, true, false));     System.out.println("------------------");     System.out.println("undoOrRedo example");     System.out.println("------------------");     System.out.println(mgr.getUndoOrRedoPresentationName( ));     mgr.undoOrRedo( );     System.out.println(mgr.getUndoOrRedoPresentationName( ));     mgr.undoOrRedo( );     // Show how UndoManager becomes a CompositeEdit.     mgr.discardAllEdits( );     mgr.setLimit(100);     //                                 #  adds?  sig?  replace?     mgr.addEdit(new SampleUndoableEdit(1, false, true, false));     mgr.addEdit(new SampleUndoableEdit(2, false, true, false));     mgr.addEdit(new SampleUndoableEdit(3, false, true, false));     System.out.println("------------------------------");     System.out.println("Transform to composite example");     System.out.println("------------------------------");     mgr.end( );     mgr.undo( );     mgr.redo( );     // Show that adds are no longer allowed. Note that addEdit( ) returns true in     // pre-JDK 1.2 Swing releases. This is fixed in JDK 1.2.     System.out.println(mgr.addEdit(      new SampleUndoableEdit(4, false, true, false)));     mgr.undo( ); // Note that edit 4 is not there.   } }

Here's the output generated by this program. We've added some comments to the output in constant width italic.

-------------------------- Insignificant edit example -------------------------- Undo 4 // Three undos from a single mgr.undo( ) call Undo 3 Undo 2 Redo 2 // But mgr.redo( ) only redoes the significant one! false  //  . . . and there are no more redos ---------------------------------- Absorbed (by addEdit) edit example ---------------------------------- Undo 5 (absorbed: <6>) // Edit 6 was absorbed by edit 5 and undone. -------------------------------------- Absorbed (by replaceEdit) edit example -------------------------------------- Undo 2 (replaced: <1>) // Edit 1 was replaced by edit 2 and undone. false // No more edits to undo ---------------------- Changing limit example ---------------------- Undo 6 // We perform three undos . . .  Undo 5 Undo 4 //  . . . and then set the limit to 4, which trims from both ends. Undo 3 // Only two undos left . . .  Undo 2 Redo 2 // and then four redos are available. Redo 3 Redo 4 Redo 5 ------------------ undoOrRedo example ------------------ Undo   // undoOrRedoPresentationName is "Undo" here... Undo 1 // ...then we do an undoOrRedo( )... Redo   // ...and it's now "Redo". Redo 1 ------------------------------ Transform to composite example ------------------------------ Undo 3 // Because we called end( ), undo( ) undoes all the edits . . .  Undo 2 Undo 1 Redo 1 //  . . . and redo( ) redoes them all. Redo 2 Redo 3 true   // addEdit( ) claims the edit was added (returns false in JDK 1.2), Undo 3 // but edit 4 never got added because end( ) had been called. Undo 2 Undo 1

All the details in this example can be a little overwhelming, but don't let this keep you from using UndoManager. For most applications, the basic features of UndoManager (shown in the first example in this section) give you everything you need to provide your users with powerful undo capabilities.

At the end of this chapter, we'll show how you might extend UndoManager to add functionality. We'll create an undo manager that gives us access to the edits it contains and notifies us any time an edit is added.

18.2.11 The StateEditable Interface

So far in this chapter, we've seen that the responsibility for undoing or redoing an edit lies in the UndoableEdit object itself. The Swing undo package provides another mechanism for handling undo and redo, which is based on the idea of letting an "outside object" define its state before and after a series of changes are made to it. Once these pre and post states of the object are defined, a StateEdit toggles back and forth between these states, undoing and redoing the changes. The outside object is responsible for defining the object's significant state and must implement the StateEditable interface, which defines the methods listed below.

18.2.11.1 Methods

StateEditable defines two simple methods:

public void storeState(Hashtable state)

Called to ask the object to store its current state by inserting attributes and values as key/value pairs into the given Hashtable.

public void restoreState(Hashtable state)

Called to tell the object to restore its state, based on the key/value pairs found in the given Hashtable.

18.2.12 The StateEdit Class

StateEdit is an extension of AbstractUndoableEdit that is used to toggle between two arbitrary states. These states are defined by a StateEditable associated with the StateEdit. When the edit is created, it gets the current (pre) state from the StateEditable. Later, when end( ) is called on the edit (presumably after some changes have been made to the state of the StateEditable), it again gets the current (post) state from the StateEditable. After this point, undo( ) and redo( ) calls result in the state of the StateEditable being toggled back and forth between the pre and post states. Figure 18-9 shows a typical sequence of method calls between an application object (some object in the system that is managing edits), a StateEdit, its StateEditable, and the two Hashtables used to store the state of the StateEditable.

Figure 18-9. StateEdit sequence diagram
figs/swng2.1809.gif
18.2.12.1 State optimization

It's important to understand that StateEdit optimizes its representation of the states returned by the StateEditable. This is done by removing all duplicate key/value pairs from the pre and post Hashtables, keeping only those values that have changed. This is important to understand because it means the StateEditable cannot assume that all keys and values stored by its storeState( ) method are in the table that is passed into restoreState( ).

Figure 18-10 shows an example of how this works. The top two tables show the complete state as returned by the StateEditable. The bottom two tables show the tables as they appear after StateEdit.end( ) has compressed them to remove duplicate data. This is how the tables would look when passed to StateEditable.restoreState( ).

Figure 18-10. Example of state compression done by StateEdit
figs/swng2.1810.gif
18.2.12.2 Property

Table 18-7 shows the default property value defined by StateEdit. The presentationName property defaults to null if not specified in the StateEdit constructor. All other properties are defined by the superclass.

Table 18-7. StateEdit property

Property

Data type

get

is

set

Default value

presentationNameo

String

·

   

null

ooverridden

See also properties from the AbstractUndoableEdit class (Table 18-2).

18.2.12.3 Protected fields

The following fields are available to subclasses of StateEdit:

protected StateEditable object

The StateEditable associated with this edit.

protected Hashtable preState
protected Hashtable postState

The Hashtables used to store the state information.

protected String undoRedoName

The presentation name used for this edit.

18.2.12.4 Constructors
public StateEdit(StateEditable anObject)

Create a new edit that saves and restores its state using the given StateEditable. StateEdit calls init( ) to set up the initial state.

public StateEdit(StateEditable anObject, String name)

Create a new edit that saves and restores its state using the given StateEditable. It uses the given name as its presentationName and calls init( ) to set up the initial state.

18.2.12.5 UndoableEdit methods

The following methods override the AbstractUndoableEdit implementations of methods in the UndoableEdit interface. These methods should be called only after end( ), but they don't enforce this restriction and thus may produce confusing results.[6] In practice, you should ensure that end( ) is called before allowing undo( ) or redo( ) to be called on the edit.

[6] Calling undo( ) basically works. The problem is that a subsequent call to canRedo( ) would then return true. However, if you then called redo( ), the Hashtable passed to restoreState( ) would be null since it is not created until end( ) is called. Future Swing releases will probably throw exceptions in these abnormal cases.

public void redo( )

Call super.redo( ) to ensure that the edit can be redone and then call restoreState( ) on the edit's StateEditable, passing in the Hashtable that was populated when end( ) was called on the edit[7].

[7] CannotRedoException is not listed in the throws clause of this method as it was in the superclass version. This is valid only because this exception extends RuntimeException. Since super.redo( ) is called, this method will throw a CannotRedoException if the edit has already been undone.

public void undo( )

Call super.undo( ) to ensure that the edit can be undone and then call restoreState( ) on the edit's StateEditable, passing in the Hashtable that was populated when the edit was constructed[8].

[8] CannotUndoException is not listed in the throws clause of this method as it was in the superclass version. This is valid only because this exception extends RuntimeException. Since super.undo() is called, this method will throw a CannotUndoException if the edit has already been undone.

18.2.12.6 New public method

The following new method is defined in this class.

public void end( )

Called to indicate that the state of the StateEditable has changed. The storeState( ) method is called on the StateEditable to determine its post state. This method then uses removeRedundantState( ) to compress the pre and post state Hashtables. Note that the StateEdit is not fully ready to handle undo( ) and redo( ) requests until this method has been called.

18.2.12.7 Protected methods
protected void init (StateEditable anObject, String name)

Called by the constructors to set the initial state of the edit. It stores the input StateEditable and edit name (used for the presentationName property) and creates a new Hashtable, which it passes to the input StateEditable's storeState( ) method. This new Hashtable now holds the edit's pre state.

protected void removeRedundantState( )

This method is called by end( ) to remove any duplicate key/value pairs from the pre and post Hashtables. Only entries that have the same key and value are removed from the tables. Comparisons are done using the equals( ) method. See Figure 18-10 for an illustration of how this method is used.

18.2.12.8 StateEdit example

Here's one last version of our toggle button application. In this example, we'll use a StateEdit to store the state of all three toggle buttons. The user can determine when we start and stop the creation of the edit (buttons are provided for these functions). The main application frame serves as the StateEditable, and its storeState( ) method stores the selected property of each of its buttons in the input Hashtable. The boldface code shows the differences between this example and UndoableToggleApp2:

// UndoableToggleApp4.java // import javax.swing.*; import javax.swing.event.*; import javax.swing.undo.*; import java.awt.*; import java.awt.event.*; import java.util.Hashtable; // A sample app showing the use of StateEdit(able) public class UndoableToggleApp4 extends JFrame implements StateEditable {   private JToggleButton tog;   private JCheckBox cb;   private JRadioButton radio;   private JButton undoButton;   private JButton redoButton;   private JButton startButton;   private JButton endButton;   private StateEdit edit;   // Create the main frame and everything in it.   public UndoableToggleApp4( ) {     // Create some toggle buttons (and subclasses).     tog = new JToggleButton("ToggleButton");     cb = new JCheckBox("CheckBox");     radio = new JRadioButton("RadioButton");     // Add our listener to the buttons.     SimpleListener sl = new SimpleListener( );     tog.addActionListener(sl);     cb.addActionListener(sl);     radio.addActionListener(sl);     // Lay out the buttons.     Box buttonBox = new Box(BoxLayout.Y_AXIS);     buttonBox.add(tog);     buttonBox.add(cb);     buttonBox.add(radio);     // Create undo, redo, start, and end buttons.     startButton = new JButton("Start");     endButton = new JButton("End");     undoButton = new JButton("Undo");     redoButton = new JButton("Redo");     startButton.setEnabled(true);     endButton.setEnabled(false);     undoButton.setEnabled(false);     redoButton.setEnabled(false);     // Add a listener to the start button. It creates a new StateEdit,     // passing in this frame as the StateEditable.     startButton.addActionListener(new ActionListener( ) {       public void actionPerformed(ActionEvent ev) {         edit = new StateEdit(UndoableToggleApp4.this);         startButton.setEnabled(false);         endButton.setEnabled(true);         // undoButton.setEnabled(edit.canUndo( ));         //         // NOTE: We really don't want to be able to undo until end( ) is pressed,          // but StateEdit does not enforce this for us!         undoButton.setEnabled(false);         redoButton.setEnabled(edit.canRedo( ));       }     });     // Add a listener to the end button. It will call end( ) on the StateEdit.     endButton.addActionListener(new ActionListener( ) {       public void actionPerformed(ActionEvent ev) {         edit.end( );         startButton.setEnabled(true);         endButton.setEnabled(false);         undoButton.setEnabled(edit.canUndo( ));         redoButton.setEnabled(edit.canRedo( ));       }     });     // Add a listener to the undo button. It attempts to call undo( ) on the     // current edit, then enables/disables the undo/redo buttons as appropriate.     undoButton.addActionListener(new ActionListener( ) {       public void actionPerformed(ActionEvent ev) {         try {           edit.undo( );         } catch (CannotUndoException ex) { ex.printStackTrace( ); }         finally {           undoButton.setEnabled(edit.canUndo( ));           redoButton.setEnabled(edit.canRedo( ));         }       }     });     // Add a redo listener, which is just like the undo listener.     redoButton.addActionListener(new ActionListener( ) {       public void actionPerformed(ActionEvent ev) {         try {           edit.redo( );         } catch (CannotRedoException ex) { ex.printStackTrace( ); }         finally {           undoButton.setEnabled(edit.canUndo( ));           redoButton.setEnabled(edit.canRedo( ));         }       }     });     // Lay out the state/end and undo/redo buttons.     Box undoRedoBox = new Box(BoxLayout.X_AXIS);     undoRedoBox.add(Box.createGlue( ));     undoRedoBox.add(startButton);     undoRedoBox.add(Box.createHorizontalStrut(2));     undoRedoBox.add(endButton);     undoRedoBox.add(Box.createHorizontalStrut(2));     undoRedoBox.add(undoButton);     undoRedoBox.add(Box.createHorizontalStrut(2));     undoRedoBox.add(redoButton);     undoRedoBox.add(Box.createGlue( ));     // Lay out the main frame.     Container content = getContentPane( );     content.setLayout(new BorderLayout( ));     content.add(buttonBox, BorderLayout.CENTER);     content.add(undoRedoBox, BorderLayout.SOUTH);     setSize(400, 150);   }   public class SimpleListener implements ActionListener {     // When any toggle button is clicked, we turn off the undo and redo buttons,     // reflecting the fact that we can only undo/redo the last set of state changes     // as long as no additional changes have been made.     public void actionPerformed(ActionEvent ev) {       undoButton.setEnabled(false);       redoButton.setEnabled(false);     }   }   // Save the state of the app by storing the current state of the three buttons.   // We'll use the buttons themselves as keys and their selected state as values.   public void storeState(Hashtable ht) {     ht.put(tog, new Boolean(tog.isSelected( )));     ht.put(cb, new Boolean(cb.isSelected( )));     ht.put(radio, new Boolean(radio.isSelected( )));   }   // Restore state based on the values we saved when storeState( ) was called. Note   // that StateEdit discards any state info that did not change from between the   // start state and the end state, so we can't assume that the state for all three   // buttons is in the Hashtable.   public void restoreState(Hashtable ht) {     Boolean b1 = (Boolean)ht.get(tog);     if (b1 != null)       tog.setSelected(b1.booleanValue( ));     Boolean b2 = (Boolean)ht.get(cb);     if (b2 != null)       cb.setSelected(b2.booleanValue( ));     Boolean b3 = (Boolean)ht.get(radio);     if (b3 != null)       radio.setSelected(b3.booleanValue( ));   }   // Main program just creates the frame and displays it   public static void main(String[] args) {     JFrame f = new UndoableToggleApp4( );     f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);     f.setVisible(true);   } } 

Note that we could have used whatever keys and values we needed to store the current state in the storeState( ) method. We simplified this example by using the button itself as the key and a Boolean to hold the value. There are no restrictions on the keys and values you choose, as long as they are Objects, and the storeState( ) and restoreState( ) methods are implemented to use the same keys.

18.2.13 The UndoableEditSupport Class

UndoableEditSupport is a simple utility class for classes that need to support undo.[9] It provides methods for adding and removing UndoableEditListeners, as well as a postEdit( ) method used to send an UndoableEditEvent to the added listeners. Additionally, it allows multiple edits to be added to it and fired as a single CompoundEdit.

[9] Presently, none of the Swing classes that support undo actually use this class. Instead, they manage their edits and listeners themselves.

18.2.13.1 Properties

UndoableEditSupport defines the properties shown in Table 18-8. updateLevel reflects the current level of nesting of beginUpdate( ) calls. (See Section 18.2.13.5 for more information on this property.) As with other event-generating classes in Swing, a convenience property to retrieve currently registered listeners undoableEditListeners in this case was added in SDK 1.4.

Table 18-8. UndoableEditSupport properties

Property

Data type

get

(get)

is

set

(set)

Default value

undoableEditListeners1.4

UndoableEditListener[]

·

   

Empty array

updateLevel

int

·

   

0

1.4since 1.4

18.2.13.2 Protected fields

The following fields are available to subclasses of UndoableEditSupport:

protected CompoundEdit compoundEdit

This is the edit used to group together multiple edits that are added between beginUpdate( ) and endUpdate( ) calls. See Section 18.2.13.5 later in this section.

protected Vector listeners

This is where the list of listeners is stored.

protected Object realSource

Hold the event source used for all events fired by this object. If the source is set when the UndoableEditSupport is created, that object is sent as the source of all events. Otherwise, the UndoableEditSupport itself becomes the source.

protected int updateLevel

This is where the updateLevel property is stored.

18.2.13.3 Constructors
public UndoableEditSupport( )

Create a new support object, which uses itself as the source object for any events it fires.

public UndoableEditSupport(Object r)

Create a new support object, which uses the given object as the source for any events it fires.

18.2.13.4 UndoableEditEvent/listener support methods

The following methods allow an undo-capable object to use an UndoableEditSupport object to manage event listeners:

public synchronized void addUndoableEditListener(UndoableEditListener l)

Add the given listener to a list of listeners to be notified of new UndoableEdits.

public synchronized void removeUndoableEditListener(UndoableEditListener l)

Remove the specified listener.

public synchronized void postEdit(UndoableEdit e)

If updateLevel is 0, this method uses _postEdit( ) to send an UndoableEditEvent to all added listeners. If updateLevel is not 0, this method adds the input edit to a CompoundEdit to be fired later. See the beginUpdate( ) and endUpdate( ) methods for more details on the use of CompoundEdit.

protected void _postEdit(UndoableEdit e)

This protected method is used by postEdit( ) and endUpdate( ). It creates a new UndoableEditEvent containing the input edit and sends it to all registered listeners by calling undoableEditHappened( ) on each.

18.2.13.5 Nested edit support

The following methods allow the UndoableEditSupport class to consolidate multiple edits into a single CompoundEdit, to be fired after a series of edits have been added. To use these methods, the object using the support object first calls beginUpdate( ). Each subsequent postEdit( ) call causes the input edit to be added to a single CompoundEdit. When endUpdate( ) is called, an UndoableEditEvent containing the CompoundEdit is fired.

If multiple beginUpdate( ) calls are made, the support object keeps track of the level of nesting using the updateLevel property. Only when the number of endUpdate( ) calls matches the number of beginUpdate( ) calls is the CompoundEdit finally fired. Regardless of how many times beginUpdate( ) is called, only a single CompoundEdit is created.

public synchronized void beginUpdate( )

This method indicates that subsequent postEdit( ) calls should result in the input edit being added to a CompoundEdit. It increments updateLevel and, if the updateLevel is 0, creates a new CompoundEdit.

public synchronized void endUpdate( )

Decrement updateLevel. If updateLevel is 0, it calls end( ) on the CompoundEdit and then calls _postEdit( ) to deliver the edit to the support object's listeners.

protected CompoundEdit createCompoundEdit( )

Return a new CompoundEdit. A subclass could override this method to return a different CompoundEdit implementation if desired.

Figure 18-11 shows how to use beginUpdate( ) and endUpdate( ). We add a total of four edits to the support object. Notice that the first endUpdate( ) call does nothing but decrement the current level. The next endUpdate( ), which brings the level to 0, causes the composite edit containing the four added edits to be fired.

Figure 18-11. Using beginUpdate( ) and endUpdate( )
figs/swng2.1811.gif

18.2.14 Using Undoable Edit Support

Earlier in this chapter, we created a simple undoable toggle button class. To keep that example simple, we allowed only a single listener to be added to the button. In this example (just a new implementation of the same class), we'll show how easily we can use UndoableEditSupport to allow multiple listeners to be added and notified. Differences from our earlier implementation are bolded.

// UndoableJToggleButton2.java //  import java.awt.event.*; import javax.swing.*; import javax.swing.event.*; import javax.swing.undo.*; // Sample undoable toggle button class using UndoableEditSupport public class UndoableJToggleButton2 extends JToggleButton {   private UndoableEditSupport support;   // For this example, we'll provide just one constructor.   public UndoableJToggleButton2(String txt) {     super(txt);     support = new UndoableEditSupport(this);   }   // Add an UndoableEditListener using our support object.   public void addUndoableEditListener(UndoableEditListener l) {     support.addUndoableEditListener(l);   }   // Remove an UndoableEditListener using our support object.   public void removeUndoableEditListener(UndoableEditListener l) {     support.addUndoableEditListener(l);   }   // Override this method to call the super implementation first (to fire the   // action event) and then fire a new UndoableEditEvent to our listeners using   // our support object.   protected void fireActionPerformed(ActionEvent ev) {     // Fire the ActionEvent as usual.     super.fireActionPerformed(ev);     support.postEdit(new UndoableToggleEdit(this));   } }

18.2.15 The CannotRedoException Class

This class is an extension of RuntimeException thrown when an attempt is made to redo an UndoableEdit that cannot be redone (typically because it has not yet been undone or because it has been " killed").

There are no properties, constructors (other than the implicit default), or methods defined in this class.

18.2.16 The CannotUndoException Class

This class is an extension of RuntimeException thrown when an attempt is made to undo an UndoableEdit that cannot be undone (typically because it has already been undone, or because it has been "killed").

There are no properties, constructors (other than the implicit default), or methods defined in this class.



Java Swing
Graphic Java 2: Mastering the Jfc, By Geary, 3Rd Edition, Volume 2: Swing
ISBN: 0130796670
EAN: 2147483647
Year: 2001
Pages: 289
Authors: David Geary

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net