I l@ve RuBoard |
![]() ![]() |
18.2 The UndoManager ClassUndoManager 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).
Figure 18-6. UndoManager state chart![]() 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 ExampleFigure 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![]() 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 PropertiesUndoManager defines the properties shown in Table 18-6.
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
18.2.5 UndoableEditListener MethodThis method is defined in the UndoableEditListener interface implemented by UndoManager:
18.2.6 UndoableEdit MethodsUndoManager overrides the following UndoableEdit methods:
18.2.7 Public MethodsThe following methods are introduced in the UndoManager class:
18.2.8 Protected MethodsThese 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.
18.2.9 Using an Undo ManagerIn 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]
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.
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![]() 18.2.10 Understanding the UndoManagerThere 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 InterfaceSo 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 MethodsStateEditable defines two simple methods:
18.2.12 The StateEdit ClassStateEdit 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![]() 18.2.12.1 State optimizationIt'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![]() 18.2.12.2 PropertyTable 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.
18.2.12.3 Protected fieldsThe following fields are available to subclasses of StateEdit:
18.2.12.4 Constructors
18.2.12.5 UndoableEdit methodsThe 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.
18.2.12.6 New public methodThe following new method is defined in this class.
18.2.12.7 Protected methods
18.2.12.8 StateEdit exampleHere'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 ClassUndoableEditSupport 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.
18.2.13.1 PropertiesUndoableEditSupport 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.
18.2.13.2 Protected fieldsThe following fields are available to subclasses of UndoableEditSupport:
18.2.13.3 Constructors
18.2.13.4 UndoableEditEvent/listener support methodsThe following methods allow an undo-capable object to use an UndoableEditSupport object to manage event listeners:
18.2.13.5 Nested edit supportThe 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.
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( )![]() 18.2.14 Using Undoable Edit SupportEarlier 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 ClassThis 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 ClassThis 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. ![]() |
I l@ve RuBoard |
![]() ![]() |