Summary

Java > Core SWING advanced programming > 9. THE SWING UNDO PACKAGE > The UndoManager Class

 

The UndoManager Class

Throughout this chapter we've made use of the UndoManager class to store UndoableEdits generated by the GUI components in our examples and to provide the ability for the user to undo and redo those edits. As we said earlier, UndoManager is a direct subclass of CompoundEdit, but there are a few differences between these two classes that well look at in this section. We'll also show you how to extend the functionality of UndoManager to make it more useful in real applications; the change that we'll make is trivial, but you'll see that the user interface will be greatly improved as a result.

Differences Between UndoManager and CompoundEdit

The principal aim of the CompoundEdit class is to allow the source of UndoableEdit events to group them together is such a way that, while being logically separate from the point of view of the originating software, they are presented as a single unit to the user. By contrast, UndoManager has an almost entirely opposite aim: It is intended to act as a last-in, first-out stack of edits that the user can undo and redo separately, so it is important that the individual edits within an UndoManager can be acted upon individually. While CompoundEdit is a convenience for the developer, UndoManager was created for the benefit of the user.

The undo and redo Methods

In the case of CompoundEdit, you cannot invoke undo or redo until all the edits have been added and the end method has been called. Once this has happened, the undo method undoes all the edits in reverse order and the redo method redoes them in their original order. With UndoManager, the situation is quite different. In most cases, end will never be invoked for UndoManager in fact, if you call its end method, the UndoManager reverts to being a simple CompoundEdit and will only undo or redo all its edits as a unit. In the more usual case in which end has not been called, UndoManager keeps track of the location of the last edit that it undid which, by default, is the last one added to it. If you call undo, it undoes that edit and moves its internal pointer to the edit before (as we'll see below that, because of the concept of insignificant edits, this is not quite true, but we'll avoid that complication for now). Thus, for example, in Figure 9-5 edit C was originally the next edit to be reversed. When undo was called, that edit was undone and the next one to undo became edit B, as shown in Figure 9-6. Calling redo moves the pointer the other way, of course.

The die and addEdit Methods

The implementations of the die and addEdit methods of UndoManager differ from those of CompoundEdit. If you invoke die on a CompoundEdit, the die methods of all the edits that it contains are invoked, thus rendering the entire CompoundEdit useless. With UndoManager, however, calling die affects only those edits that have been undone and not redone. Furthermore, the UndoManager addEdit method, unlike that of CompoundEdit, does not necessarily add a new edit after all the existing ones. To see what actually happens and why the die method behaves as it does, let's look at an example. Suppose the user types the text Some yext into a text field monitored by an UndoManager, causing an UndoableEdit to be generated and stored for each character typed. Assuming the user really intended to type Some text, the user might now decide to back up and change the incorrect y to a t by pressing ctrl+z four times to undo the incorrect insertions and then typing t. After last the four edits have been undone and before the t has been inserted, the state of the UndoManager will be as shown in Figure 9-10.

Figure 9-10. Undoing edits with UndoManager.
graphics/09fig10.gif

The four edits on the right side of Figure 9-10, edits 6 to 9, have been undone, while the five to the left have not. At this point, the user will type the letter t and the text component will send out another UndoableEditEvent containing an insertundo edit for the string t. Let's call this new edit number 10. What should UndoManager do with this edit? Obviously, it can't add it to the end of the list after edit 9, because that doesn't reflect the user's actions. In fact, UndoManager inserts the new edit immediately after the next edit to be undone (edit 5) and then discards all the edits that are in the undone state (edits 6 to 9), calling their die methods in the course of doing so. This leaves the UndoManager in the state shown in Figure 9-11.

Figure 9-11. UndoManager state after undoing edits and inserting a new edit.
graphics/09fig11.gif

There are two important points to note about this:

  • Calling die on an UndoManager results in the die methods of any edit that has been undone being invoked. Edits that have not been undone are not affected.

  • Adding a new edit to an UndoManager by calling its addEdit method discards any edits that are in the undone state which means, of course, that these edits can no longer be redone. This is correct behavior because undoing a set of insertions using ctrl+z and then typing something new implies that the user did not want to retain the characters whose insertions were undone. Note that this is not the same as removing the text by using the DELETE key instead of ctrl+z, because character delete operations are themselves undoableEdits that are held by the UndoManager. Deleting four characters and inserting a new one results in four new RemoveUndo edits and one new Insertundo edit being stored in the UndoManager; the entire sequence can be reversed by calling undo five times.

Significant and Insignificant Edits

When we looked at the methods of undoableEdit, we saw that it is possible to implement the isSignificant method in such a way that edits that should not require explicit action by the user to be undone can be automatically reversed as part of an undo operation. An edit of this type, typically something like a shift of the input focus, should return false from its isSignificant method. CompoundEdit ignores the isSignificant method when undoing the edits that it contains, but UndoManager does not it uses the value returned from this method to decide which edits to undo. Previously, we said that UndoManager undoes only the edit added before the one most recently undone that is, in Figure 9-5, when edit C has been undone, the next undo candidate would be edit B. This simple statement is true as long as all the edits have an isSignificant method that returns true. Consider, however, the situation shown in Figure 9-12.

Figure 9-12. UndoManager and insignificant edits.
graphics/09fig12.gif

Here, there are four UndoableEdits, of which three are significant and one is not. The first time undo is called in the UndoManager, edit D is undone, leaving C as the next one to undo, as shown in Figure 9-13.

Figure 9-13. UndoManager after undoing a significant edit.
graphics/09fig13.gif

If undo is called again, edit C will, of course, be undone, but the process would not stop there. Because edit C is not significant, UndoManager will also undo edit B, changing its state to that shown in Figure 9-14.

Figure 9-14. The effect of undoing an insignificant edit.
graphics/09fig14.gif

In fact, when undo is called, UndoManager undoes as many insignificant edits as it finds until it locates an edit whose isSignificant method returns true. The significant edit will also be undone and the process will stop. In other words, the logic of the undo method is something like this:

 while there is still an edit to undo and its          isSignificant method returns false {    undo the edit } if there is still an edit to undo {    undo it;       // Must be a significant edit } 

This process allows, for example, focus changes stored as insignificant edits to be reversed automatically without the user having to know about them and press ctrl+z or an Undo button an extra time. The same process applies to redo after redoing an edit, UndoManager looks for any insignificant edits that follow it and will undo those as well, until another significant edit is encountered (which will not be undone) or all the edits have been redone.

Note the subtle difference between this example, which proposes the use of insignificant edits to transparently store and reverse focus changes, and our tree example which used a CompoundEdit to bundle a selection change with a tree expansion or collapse edit to arrange for a selection change to happen together with the change in state of the tree node without the user having to request it. Because the selection change and the focus change are both effectively managed outside the control of the user, why are two different mechanisms used? The reason is a practical one that is connected to who generates the edits. In the case of the tree, the node collapse and the selection change are necessarily related to each other because the latter, if it happens at all, will happen only as a direct result of the former. These edits are, therefore, tightly coupled and can conveniently be placed in a single CompoundEdit. On the other hand, the example shown in Figure 9-12 may result from the following sequence of user actions:

  1. User types A into text field 1, generating significant edit A.

  2. User types B into text field 1, generating significant edit B.

  3. User clicks in text field 2, moving the focus and generating insignificant edit C.

  4. User types C into text field 2, generating significant edit D.

There is no necessary relationship between any of these edits the first two are from the same text field, but they are separate CompoundEdits. The focus-changed edit would probably be generated by some kind of form-level logic that listens for focus changes and has no knowledge of the edits generated by the text fields. It should be obvious that there is no real way for the focus-change edit to be bundled into any of the other CompoundEdits and the same argument holds for the last character insertion. In cases like this, where the edits come from unrelated software components, an edit that needs to be recorded but should be invisible to the user should be implemented as an insignificant edit. When the edit is an immediate and necessary side effect of a state change that also creates an UndoableEdit, it is better to bundle them in the same CompoundEdit and both edits should return true from their isSignificant methods.

Other UndoManager Features

There are a couple of other UndoManager features that we'll mention in passing but otherwise say very little about:

  • UndoManager supplies a set of convenience methods for the simple management of a trivial UndoManager instance that contains only one UndoableEdit. In this simple case, only one of the undo or redo methods can be successfully invoked at any given time; when undo has been called, only redo is then legal and vice versa. To save the application the trouble of having to remember which state the single edit is in, UndoManager provides the undoOrRedo method, which simply calls either undo or redo as appropriate. There is also a canUndoOrRedo method that returns true unless the UndoManager contains no edits, in which case it returns false. Calling undoOrRedo when the UndoManager has no edits results in an exception being thrown.

  • It is possible to set an upper limit on the number of edits that an UndoManager will store. By default, UndoManager will hold up to 100 edits at the same time, but you can set a different limit using the setLimit method which takes the maximum number of contained edits as an integer argument. If invoking setLimit reduces the capacity of the UndoManager to the extent that it contains more edits than it is allowed to, it will discard its oldest edits (and call their die methods) and retains half of its full quota. Thus, for example, if the UndoManager contains 60 edits (numbered 0 through 59) and setLimit is called with argument 50, edits 0 through 24 will be discarded, leaving the UndoManager with 25 edits and capacity for another 25 to be added. The same happens if adding an edit exceeds the allowed capacity half the edits are discarded. It is also possible to remove all the edits by calling the discardAllEdits method.

Extending UndoManager

To close this chapter, we're going to address what seems to be an oversight in the design of the UndoManager class. If you run any of the undo examples that we've seen so far in this chapter, you'll see that the undo and Redo buttons are always enabled. This isn't really correct each button should be enabled only if it can actually perform the function that it is advertising. This means, for example, that the Undo button should start in the disabled state and become enabled only when an UndoableEdit is added to the UndoManager. Furthermore, when the Undo button is pressed, an edit will be undone and, if there are no remaining edits to undo, the Undo button should be disabled again. Of course, you can tell whether there is an edit that can be undone by calling the UndoManager's canUndo method the trouble is that there is no convenient way to know when it might be a good idea to call this method.

To address this problem, we need to arrange for UndoManager to generate an event when its state changes in such a way as to affect objects, like the Undo button, that need to know whether an UndoableEdit is available. To do this, we have to create a subclass of UndoManager and do the following:

  1. Decide on the type of event to broadcast in the event of a suitable state change.

  2. Provide a means for listeners to register to receive these events and to deregister themselves when necessary.

  3. Work out when the event should be generated and add the code to deliver it at the appropriate points.

Event Type

Swing and AWT between them define many event types, as do other packages in the JDK. Some of these events carry state information, while others just convey the fact that a state change has happened. We could choose to reuse an existing event type for our extended UndoManager or create one of our own, but the option of inventing our own event should only be taken if there is no existing event that can properly be used to convey the meaning that we need to pass to our event's listeners. As it turns out, there are at least two existing events that fit this description, so there is no need for us to add a new event for this example. The events that are worth consideration are java.beans.PropertyChangeEvent and javax.swing.event.ChangeEvent. The distinguishing points of these events are as follows:

PropertyChangeEvent This event notifies a change in the value of a bound property of some object. The event contains the source of the event, the name of the property (a string), together with its values before and after the change took place.
ChangeEvent A ChangeEvent signifies a change of some kind in the internal state of its source. Other than carrying a reference to the source, this event has no qualifying information the listener is expected to discover the nature of the change by directly querying the event source or, in simple cases where the meaning of the event is unambiguous, by inference.

We could reasonably use either of these events to satisfy the requirements of this example there is, in fact, very little to choose between them. The implementation you'll see below actually uses ChangeEvent. The reasons for this choice were:

  • A Proper tyChangeEvent should carry the value of the property that changed before and after the change took place. In terms of UndoManager, this means extracting and sending the set of UndoableEdits that have not been undone before and after whatever change occurred. While this could be done, it is unlikely that these values would be of any real use to a listener as you'll see later, all the listener really needs to know is whether the UndoManager can perform an undo or redo operation in its new state, which can be deduced by simply calling its canUndo or canRedo method in other words, by directly querying the source, which follows the prototypical usage of a ChangeEvent.

  • Using a PropertyChangeEvent implies the existence of an underlying property whose value can be obtained using a "getter" method (and perhaps changed with a "setter" method, although that clearly would be inappropriate here). In the case of UndoManager, the property would be the set of UndoableEdits that have not yet been undone. Adding a method to retrieve this set would be possible but, as noted earlier, would be of little use.

By contrast with these points, a ChangeEvent just tells listeners that they need to check back with the event source to get updated state information there is no implication that some tangible "property" exists. Because in the UndoManager case the state that the listener is interested in is just a boolean, there is little overhead involved in requiring the listener to obtain the state from the UndoManager rather than delivering it with the event, especially as the methods to be used to retrieve it (canundo and canRedo) already exist. Using a ChangeEvent results in a simpler implementation both for the event source and for its listeners.

Listener Registration

Having chosen to use ChangeEvents, it follows that listeners of our UndoManager subclass will need to implement the ChangeListener interface and to register and remove them we need to include the following methods:

 public void addChangeListener (ChangeListener listener); public void removeChangeListener (ChangeListener listener); 

As you might expect, the management of listeners is a very common task within the implementation of the Swing component set and there is a core facility that provides the necessary code for this. Listing 9-5 shows the code for our subclass of UndoManager, called MonitorableUndoManager, which contains the listener management methods.

Listing 9-5 An Extended UndoManager Class
 package AdvancedSwing.Chapter9; import javax.swing.event.*; import javax.swing.undo.*; public class MonitorableUndoManager extends UndoManager {    // List of listeners for events from this object    protected EventListenerList listenerList =                                new EventListenerList();    // A ChangeEvent dedicated to a single MonitorableUndoManager    protected ChangeEvent changeEvent;    // Super class overrides    public synchronized void setLimit(int l) {       super.setLimit(l);       fireChangeEvent();    }    public synchronized void discardAllEdits() {       super.discardAllEdits();       fireChangeEvent();    }    public synchronized void undo() throws CannotUndoException {       super.undo();       fireChangeEvent();    }    public synchronized void redo() throws CannotRedoException {       super.redo();       fireChangeEvent();    }    public synchronized boolean addEdit(UndoableEdit anEdit){       boolean retval = super.addEdit(anEdit);       fireChangeEvent();       return retval;    }    // Support for ChangeListeners    public void addChangeListener(ChangeListener l) {       listenerList.add(ChangeListener.class, l);    }    public void removeChangeListener(ChangeListener l) {       listenerList.remove(ChangeListener.class, l);    }    protected void fireChangeEvent() {       Object[] listeners = listenerList.getListenerList();       for (int i = listeners.length - 2; i >= 0; i -= 2) {          if (listeners[i] == ChangeListener.class) {             if (changeEvent == null) {                changeEvent = new ChangeEvent(this);             }             ((ChangeListener)listeners[i+1]).stateChanged(                                             changeEvent);          }       }    } } 

The listener management in this class is typified by the implementation of the addChangeListener method, which looks like this:

 public void addChangeListener (ChangeListener l) {    listenerList.add(ChangeListener.class, l); } 

As you can see, it simply delegates to another object, declared like this:

 protected EventListenerList listenerList =                             new EventListenerList(); 

The EventListenerList is a class that stores information about listeners. To register a new listener, you call its add method, passing the class object for the listener's interface and a reference to a new listener instance to be stored. A single EventListenerList can store all the listeners for a given object, even if that object can generate events of different types. You can, for example, have a single EventListenerList for an object that generates both ActionEvents and ChangeEvents. To register the listeners, you distinguish them by the listener interface class, for example:

 public void addChangeListener(                      ChangeListener changeListenerInstance) {    listenerList.add(ChangeListener.class,                     changeListenerInstance); } public void addChangeListener(                      ActionListener actionListenerInstance) {    listenerList.add(ActionListener.class,                     actionListenerInstance); } 

The removeChangeListener method is implemented in a similar way.

Event Generation and Delivery

Once we detect a state change, we need to deliver a changeEvent. Because these events will be generated in several locations, we create a convenience method called fireChangeEvent that delivers the event to all our listeners:

 protected void fireChangeEvent() {    Object[] listeners = listenerList.getListenerList();    for (int i = listeners.length   2; i >= 0; i  = 2) {       if (listeners[i] == ChangeListener.class) {          if (changeEvent == null) {             changeEvent = new ChangeEvent(this);          }          ((ChangeListener) listeners[i + 1]).stateChanged(                                              changeEvent);       }    } } 

To deliver the event, we need to get hold of the list of our registered listeners, which is maintained by the EventListenerList class and can be obtained, in the form of an object array, from its getListenerList method. To understand this code, you need to know that the array that this method returns always has an even number of entries. Each entry pair consists of the event listener class in the even-numbered slot and the listener instance reference in the odd-numbered slot, as shown in Figure 9-15.

Figure 9-15. How EventListenerList stores listener references.
graphics/09fig15.gif

To deliver the event, this code traverses the array, checking the evennumbered entry of each pair to see if it contains the value ChangeListener.class that signifies a ChangeListener and, if it does, using the odd-numbered entry as a reference to the listener with which to call its stateChanged method. Note that the array is processed from the end rather than from the start, which means that the most recently added listeners will receive the event first. In practice, listeners should not depend on this ordering it just happens to be implemented this way for all JComponents, so we continue the theme here.

Another point to notice is that the event actually delivered is a static member of the class that is created the first time it is needed in other words, we only create one ChangeEvent that is passed on every call to every ChangeListener. This optimization is typical of event delivery in the Swing packages it saves time and reduces the amount of garbage collection that needs to be done. It does, of course, assume that listeners will not modify the content of the event while processing it; because it has no useful information other than the event source, this is a reasonable assumption.

The remaining problem is where to put the calls to the fireChangeEvent method. As you can see from Listing 9-5, there are several such calls. The rationale behind these calls is summarized here, by method.

Methods that do not appear in this list do not need to be overridden and so are inherited directly from UndoManager.

setLimit Usually, changing the number of edits that UndoManager can hold will not affect whether this an undoable or redoable edit available, but there are some cases in which it might. Suppose, for example, that the UndoManager has capacity for 100 edits and actually contains 60, of which numbers 30 through 59 have been undone and numbers 0 to 29 have not. In this case, the user could either undo edit 29 or redo edit 30, so both the Undo and Redo buttons should be enabled. If setLimit is now called to reduce the capacity to 20, only the most recent 10 edits will be retained those numbered 50 through 59. Because all these have been undone, a redo is still possible, but there is nothing to undo, so the Undo button should be disabled. Because of this, a ChangeEvent needs to be generated.
discardAllEdits The reasoning here is the same as for setLimit, because all the edits will be discarded and both the Undo and Redo buttons should be disabled.
undo and redo The rationale behind generating events in these methods should be obvious one of them increases the number of redoable edits and reduces the number of undoable edits, while the other does the reverse. Thus, an event needs to be generated.
addEdit Calling the addEdit method adds another edit that can be undone, which means that the Undo button should be enabled. This requires the generation of an event, to give the application an opportunity to enable the button.

Using the MonitorableUndoManager Class

You can see our MonitorableUndoManager class in action by typing the following command:

 java AdvancedSwing.Chapter9.UndoExample5 

This program is based on one of our earlier JTextPane examples (undoExample2) with a few minor modifications. Listing 9-6 is an extract from the code for this example, showing all the modifications made from UndoExample2 highlighted in bold. The complete source file is, of course, available on the CD-ROM that accompanies this book.

There are two fairly obvious differences between this example and its predecessor that need little comment we create a MonitorableUndoManager instead of an UndoManager and we arrange for both the Undo and Redo buttons to be initially disabled. This is both correct (because there are initially no edits to undo or redo) and safe (because our MonitorableUndoManager will

Listing 9-6 Using the Extended UndoManager Class
 package AdvancedSwing.Chapter9; import java.awt.*; import java.awt.event.*; import java.util.*; import javax.swing.*; import javax.swing.event.*; import javax.swing.undo.*; import AdvancedSwing.Chapter4.MenuSpec; import AdvancedSwing.Chapter4.MenuBuilder; public class UndoExample5 extends JFrame {    public UndoExample5() {       super("Undo/Redo Example 5");       pane = new JTextPane();       pane.setEditable(true);// Editable       getContentPane().add(new JScrollPane(pane),                            BorderLayout.CENTER);       // Add a menu bar       menuBar = new JMenuBar();       setJMenuBar(menuBar);       // Populate the menu bar       createMenuBar();       // Create the undo manager and actions      MonitorableUndoManager manager =                             new MonitorableUndoManager();      pane.getDocument().addUndoableEditListener(manager);       Action undoAction = new UndoAction(manager);       Action redoAction = new RedoAction(manager);       // Add the actions to buttons       JPanel panel = new JPanel();       final JButton undoButton = new JButton("Undo");       final JButton redoButton = new JButton("Redo");       undoButton.addActionListener(undoAction);       redoButton.addActionListener(redoAction);      undoButton.setEnabled(false);      redoButton.setEnabled(false);       panel.add(undoButton);       panel.add(redoButton);       getContentPane().add(panel, BorderLayout.SOUTH);       // Assign the actions to keys       pane.registerKeyboardAction(undoAction,                     Keystroke.getKeyStroke(KeyEvent.VK_Z,                        InputEvent.CTRL_MASK),                        JComponent.WHEN_FOCUSED);       pane.registerKeyboardAction(redoAction,                     Keystroke.getKeyStroke(KeyEvent.VK_Y,                        InputEvent.CTRL_MASK),                        JComponent.WHEN_FOCUSED);      // Handle events from the MonitorableUndoManager      manager.addChangeListener(new ChangeListener() {         public void stateChanged(ChangeEvent evt) {            MonitorableUndoManager m =                       (MonitorableUndoManager)evt.getSource();            boolean canUndo = m.canUndo();            boolean canRedo = m.canRedo();            undoButton.setEnabled(canUndo);            redoButton.setEnabled(canRedo);            undoButton.setToolTipText(canUndo ?                           m.getUndoPresentationName() : null);            redoButton.setToolTipText(canRedo ?                           m.getRedoPresentationName() : null);         }      });    }   // MORE CODE NOT SHOWN } 

inform us when the situation changes). The only other difference is that we create a ChangeListener and register it to receive ChangeEvents from the MonitorableUndoManager.

When such an event is received, we obtain a reference to the MonitorableUndoManager from the getSource method of ChangeEvent, and then invoke its canUndo and canRedo methods to work out which of the Undo and Redo buttons should now be enabled; the values returned by these methods are passed directly to the setEnabled methods of the two buttons. This is the minimal functionality we wanted to achieve by adding event handling to UndoManager. You can see it in action by typing a single character into the JTextPane of the example program as soon as you do this, an UndoableEdit is created and the undo button is enabled. As you type more characters, this button stays enabled and the Redo button is still disabled. Now press the undo button or type ctrl+z. When you do this, the last character you typed disappears and, more importantly, the Redo button is enabled. Now if you press Redo, the character reappears and the Redo button is disabled again. Finally, if you press Undo until all the text has gone, you'll see that the Redo button is enabled but, as the last character is removed, the undo button will be disabled because there are no more edits to undo.

We've also added another enhancement to this example. As you can see from Listing 9-6, when a changeEvent occurs, we set the tooltip of the undo button to the string returned by the UndoManager getUndoPresentationName and that of the Redo button to whatever is returned by getRedoPresentationName. In the case of UndoManager, these methods look at the next edit that would be undone or redone and return its undo or redo presentation name. To see how useful this is, move the mouse pointer over the Redo button while it is enabled and you'll see a tooltip that says Redo addition. This text actually comes from the InsertUndo edit that is generated by the Document's GapContent object and stored in the MonitorableUndoManager. If you press Redo so that the Undo button is enabled and then move the mouse pointer over the Undo button, you'll see that it also has an appropriate tooltip, as shown in Figure 9-16.

Figure 9-16. Using MonitorableUndoManager to create tooltips and disabled buttons.
graphics/09fig16.gif

 

 



Core Swing
Core Swing: Advanced Programming
ISBN: 0130832928
EAN: 2147483647
Year: 1999
Pages: 55
Authors: Kim Topley

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