Summary

Java > Core SWING advanced programming > 9. THE SWING UNDO PACKAGE > Inside the Undo Package

 

Inside the Undo Package

The examples that we have used so far to illustrate the undo mechanism have both used the text package, because undo support is integrated directly into the text components. However, the undo mechanism is completely generic and can be used to allow the user to reverse actions on anything from a single GUI component to the whole user interface. It can also be used to store internal application state even when the state changes did not originate directly from the user interface, thereby facilitating a simple transactional system in which a series of changes can be made separately and then committed or rolled back as a unit. In this section, we'll look at the small number of classes and interfaces that form the basis of the undo package.

UndoableEdit and AbstractUndoableEdit

At the root of the undo mechanism is the UndoableEdit interface, which specifies the methods that any class, which describes a state change that can be reversed, must provide. UndoableEdit has 11 methods:

 public void undo() throws CannotUndoException; public boolean canUndo(); public void redo() throws CannotRedoException; public boolean canRedo(); public String getPresentationName(); public String getUndoPresentationName(); public String getRedoPresentationName(); public boolean isSignificant(); public boolean addEdit(UndoableEdit anEdit); public boolean replaceEdit(UndoableEdit anEdit); public void die(); 

The motivation behind the first seven of these methods should be fairly obvious. The undo and redo methods cause the UndoableEdit to reverse or reapply whatever state changes were made that resulted in it being created. An UndoableEdit has two possible states either it has been done, or it has been undone. The undo method can only be invoked when the edit has been done and results in the edit moving to the undone state. Similarly, the redo method can only be used when the edit has been undone. When an UndoableEdit is created, it will usually be in the done state, because it is typically generated at the same time as the action that it describes is performed. If you try to undo an edit that has already been undone, the undo method throws a CannotUndoException and, similarly, a CannotRedoException results from an attempt to redo a redone edit. The canundo and canRedo methods can be used to deduce the current state of the edit if necessary. Note that this description applies only to simple edits that encapsulate only one logical state change we'll see later, in the discussion of UndoManager, that it is possible to create edits that describe changes that have more than one state change which can be reversed or redone separately. We saw this with undoExample2 in which we were able to type several characters or change text attributes and then undo each action separately, in the reverse order to that in which they were performed.

The getPresentationName method is one of three that is intended to be used when displaying information about an UndoableEdit to the user, the other two being getUndoPresentationName and getRedoPresentationName, which supply text that should be used to describe respectively the action of undoing or redoing this particular edit. The expectation is that the string returned by getPresentationName will be used to create the more specific text returned from the other two methods and, in the implementation of these methods in the AbstractUndoableEdit class, the undo presentation name is formed by prefixing the string returned by getPresentationName with the word Undo, and the redo presentation name is similarly derived. You've already seen examples of what getPresentationName returns when applied to edits created by the Swing text package because both UndoExample1 and UndoExample2 display the string that it provides in their lower right-hand frames. Typical examples are addition, deletion, and style change.

The isSignificant method returns a boolean that indicates, not surprisingly, whether the edit is "significant." The best way to explain this is to consider an example. Suppose you create a user interface that consists of several text fields and you use a single UndoManager to catch all the changes in all the text fields, so that the user can progressively undo changes that were made field by field. Labeling these text fields A, B, and C, the user might type a name into field A, house number and street name into field B, and city name into field C. Suppose the user wanted to reverse the last two edits and type a completely different address, leaving the name unchanged. The events generated by the text fields and held in the UndoManager make this possible, but there are other things that happen in addition to text being typed in particular, the user moved the focus between the text fields to type each part of the address. These events are not recorded as UndoableEdits by the text fields, but you could arrange to catch focus-loss events and generate an UndoableEdit to record them. If you did this, the UndoManager would store something like this:

  1. Several text insertion edits in text field A

  2. A focus-lost edit from text field A

  3. Several text insertion edits in text field B

  4. A focus-lost event from text field B

  5. Text insertion edits in text field C

If the user now used ctrl+z to reverse these edits, the text typed into field C would be removed, one character for each time the user pressed ctrl+z. The user would press ctrl+z `again to move the focus back to text field B and several more times to clear field B. Although this represents a true reversal of exactly what happened, the user will probably find it tedious to have to press ctrl+z to move the focus from field C to field B this seems like something that should be reversed automatically without the user needing to specifically request it. This is why the concept of a significant edit exists think of a significant edit as something that the user would want to have to explicitly reverse, while an insignificant edit is one that should be undone as a by-product of other actions. Here, all the text insertion edits would be significant and the focus changes would be insignificant. UndoManager recognizes insignificant edits and undoes them as it encounters them when told to undo something. In this example, each ctrl+z would undo a character insertion in text field C, until the field is empty. The next ctrl+z will cause the UndoManager to encounter the focus-lost edit; because this is not significant, it will undo that edit and then continue looking for a significant edit to undo, resulting in the last character of text field B being removed as well. Note that none of the edits created by the Swing text components describe themselves as insignificant.

The last two methods, addEdit and replaceEdit, are intended to be used to allow edits to be merged together. This might be appropriate when the user is typing into a text field if the user enters a sequence of characters, these methods can be used to create a single edit that contains everything that the user typed instead of storing the edits individually as they are generated. This scheme has the advantage that a single undo operation would then remove everything that the user typed in one go. Unfortunately, the Swing text components do not generate UndoableEdits that support merging, so this does not happen in practice.

The die method is used to remove from UndoManager edits that should no longer be active. We'll describe this method, as well as addEdit and replaceEdit, in more detail when looking at compound edits later in this chapter.

There is a basic implementation of the UndoableEdit interface in the AbstractUndoableEdit class, which is also part of the javax.swing.undo package. AbstractUndoableEdit is the base class from which all the undoable edits generated by the text package are derived; specific edits override methods of AbstractUndoableEdit as necessary. Table 9-1 summarizes what each of the methods of AbstractUndoableEdit does.

Table 9-1. Implementing AbstractUndoableEdit
Method Description
undo() Checks to see whether the edit can be undone by calling canUndo and throws a CannotUndoException if it has. If it has not been undone yet, the undo methods mark it as not done by setting the instance variable hasBeenDone to false. This variable is initialized to true when the AbstractUndoableEdit is created.
canUndo() If the edit is alive (that is, die has not been called) and hasBeenDone is true, this method returns true. Otherwise, it returns false.
redo() This method is similar to undo it calls canRedo, throwing a CannotRedoException if it returns false. Otherwise, it sets hasBeenDone to true.
canRedo() The canRedo method returns true if the edit is alive and hasBeenDone is false. Otherwise, it returns false (that is, the edit cannot be redone).
getPresentationName Returns an empty string.
getUndoPresentationName Returns the string returned by getPresentationName prefixed with the word Undo. Typically, a subclass will override getPresentationName to return something meaningful and use the AbstractUndoableEdit implementation of this method to get an appropriate string to use when describing this edit in a tooltip.
getRedoPresentationName Returns the string returned by getPresentationName prefixed with the word Redo.
isSignificant Returns false.
addEdit Returns false, indicating that it does not support merging of edits.
replaceEdit Returns false.
die Marks the edit as being not alive, which prevents it being either undone or redone.

Because AbstractUndoableEdit does not encapsulate any real editing behavior, its undo and redo methods do not have anything to reverse this is the responsibility of derived classes. Instead, these methods provide the logic that maintains the internal state of the edit, freeing subclass implementers from having to worry about how to keep track of whether a specific edit has been done or undone. Here's how the undo method of a typical real undoable edit is implemented:

 public void undo() throws CannotUndoException {    super.undo();    // Now undo the edit, whatever that means } 

By first invoking the undo method of AbstractUndoableEdit, the proper state checking is performed without any duplication of code. There is, of course, no return value to check because undo doesn't return anything; instead, if there is an error, it throws a CannotUndoException, which is propagated directly to the caller. As a result, the programmer can assume that the edit is in the correct state to be undone if super.undo() returns successfully.

A Simple UndoableEdit Example

Let's look at a simple example of the use of the undo package with a Swing component. In this example, we are going to add undo support to the JTree component so that the user can use the ctrl+z and ctrl+y keys to undo and redo changes to the expanded state of tree nodes. Whenever the user expands or collapses a node, the tree will create and send an UndoableEdit that can be used to restore the node to its original state. Although this is not a feature you are likely to want to build into a real-world application, it is relatively simple to implement and illustrates how to create UndoableEdits and how to add support for them to a GUI component.

Before we describe the code, let's look at how the example works. If you type the command

 java AdvancedSwing.Chapter9.UndoExample3 

you'll be presented with a tree with three nodes in addition to the root node. Click first on the Apollo 8 node and then on the Apollo 12 node to expand both of them, as shown in Figure 9-2. Now if you press the Undo button or use the shortcut ctrl+z, you'll find that the Apollo 12 node closes. Press Redo or use ctrl+y and it expands again. Pressing Undo twice collapses both the Apollo 12 and the Apollo 8 nodes, restoring the tree to its original state. Pressing Undo again does nothing (other than causing a beep) because there are no more UndoableEdits to reverse.

Figure 9-2. A tree with undo and redo support.
graphics/09fig02.gif

The code for this example is very simple. Listing 9-2 shows the application itself. As you can see, this code is very similar to the text-based example shown in Listing 9-1. It starts by creating an instance of a subclass of JTree called UndoableTree and uses it to build the user interface. The Undoable-Tree class, which we'll be looking at shortly, allows interested parties to register as UndoableEditListeners and receive UndoableEditEvents whenever any node in the tree is expanded or collapsed by user or programmatic action. As with the previous example, we delegate the job of catching and managing these events to UndoManager and we add Undo and Redo buttons that give access to it. We also provide the ctrl+z and ctrl+y shortcuts that were used in Listing 9-1, except that here we register them against the frames content pane so that they operate whenever the focus is in the frame.

Listing 9-2 Using a JTree with Undo Support
 package AdvancedSwing.Chapter9; import java.awt.*; import java.awt.event.*; import javax.swing.*; import javax.swing.event.*; import javax.swing.tree.*; import javax.swing.undo.*; public class UndoExample3 extends JFrame {    public UndoExample3() {       super("Undo/Redo Example 3");       DefaultMutableTreeNode rootNode =                          new DefaultMutableTreeNode("root");       DefaultMutableTreeNode node =                        new DefaultMutableTreeNode("Apollo 8");       rootNode.add(node);       node.add(new DefaultMutableTreeNode("Borman"));       node.add(new DefaultMutableTreeNode("Lovell"));       node.add(new DefaultMutableTreeNode("Anders"));       node = new DefaultMutableTreeNode("Apollo 11");       rootNode.add(node);       node.add(new DefaultMutableTreeNode("Armstrong"));       node.add(new DefaultMutableTreeNode("Aldrin"));       node.add(new DefaultMutableTreeNode("Collins"));       node = new DefaultMutableTreeNode("Apollo 12");       rootNode.add(node);       node.add(new DefaultMutableTreeNode("Conrad"));       node.add(new DefaultMutableTreeNode("Gordon"));       node.add(new DefaultMutableTreeNode("Bean"));       UndoableTree tree = new UndoableTree(rootNode);       getContentPane().add(new JScrollPane(tree),                            BorderLayout.CENTER);       // Create the undo manager and actions       UndoManager manager = new UndoManager();       tree.addUndoableEditListener(manager);       Action undoAction = new UndoAction(manager);       Action redoAction = new RedoAction(manager);       // Add the actions to buttons       JPanel panel = new JPanel();       JButton undoButton = new JButton("Undo");       JButton redoButton = new JButton("Redo");       undoButton.addActionListener(undoAction);       redoButton.addActionListener(redoAction);       panel.add(undoButton);       panel.add(redoButton);       getContentPane().add(panel, BorderLayout.SOUTH);       // Assign the actions to keys       ((JComponent)getContentPane()).                      registerKeyboardAction(undoAction,                      Keystroke.getKeyStroke(KeyEvent.VK_Z,                      InputEvent.CTRL_MASK),                      JComponent.WHEN_IN_FOCUSED_WINDOW);       ((JComponent)getContentPane()).                      registerKeyboardAction(redoAction,                      KeyStroke.getKeyStroke(KeyEvent.VK_Y,                      InputEvent.CTRL_MASK),                      JComponent.WHEN_IN_FOCUSED_WINDOW);    }    // The Undo action    public class UndoAction extends AbstractAction {       public UndoAction(UndoManager manager) {          this.manager = manager;       }       public void actionPerformed(ActionEvent evt) {          try {             manager.undo();          } catch (CannotUndoException e) {             Toolkit.getDefaultToolkit().beep();          }       }       private UndoManager manager;    }       // The Redo action       public class RedoAction extends AbstractAction {          public RedoAction(UndoManager manager) {             this.manager = manager;       }       public void actionPerformed(ActionEvent evt) {          try {             manager.redo();          } catch (CannotRedoException e) {             Toolkit.getDefaultToolkit().beep();          }       }       private UndoManager manager;    }    public static void main(String[] args) {       JFrame f = new UndoExample3();       f.addWindowListener(new WindowAdapter() {          public void windowClosing(WindowEvent evt) {             System.exit(0);          }       });       f.pack();       f.setvisible(true);    } } 

The UndoableTree class sends an event whenever a node is expanded or collapsed. To implement this, we need to do the following:

  1. Allow the registration of UndoableEditListeners with the UndoableTree.

  2. Get control during node expansion or collapse.

  3. Create a suitable object that represents the change of node state and implements the UndoableEdit interface.

  4. Build that object into an UndoableEditEvent and send it to all registered listeners.

  5. When the undo method of the edit is called, revert the node to the state it was in before the event was generated.

  6. When the redo method is called, put the node into the state it was in after the event.

Let's leave the registration of listeners as a task to be dealt with later and look first at the mechanics of creating the UndoableEdit that we'll need for this example. The first task is to get control whenever a node is expanded or contracted. There are four methods in the JTree API that deal with this subject:

 public void collapsePath(TreePath path); public void collapseRow(int row); public void expandPath(TreePath path); public void expandRow(int row); 

Of these, the two that deal with row numbers actually end up calling the other two, so we only need to concern ourselves with the variants of these methods that use TreePath objects. These methods are used by the tree's UI classes to expand or collapse a node in response to user action (such as clicking on a branch node) and they are also the only way to change the expanded state of a node programmatically. Because these methods are effectively a single point of control for this operation, we can override them in our derived class to do whatever we want in addition to actually changing the state of the node. What we need to do, of course, is create a suitable UndoableEdit and then broadcast it to our listeners. Listing 9-3 shows the complete implementation of the UndoableTree class, including the overridden expandPath and collapsePath methods.

Listing 9-3 Adding Undo Support to a JTree
 package AdvancedSwing.Chapter9; import javax.swing.*; import javax.swing.event.*; import javax.swing.tree.*; import javax.swing.undo.*; public class UndoableTree extends JTree {    // Only one constructor for brevity    public UndoableTree(TreeNode root) {       super(root);    }    public void addUndoableEditListener(UndoableEditListener 1) {       support.addUndoableEditListener(1);    }    public void removeUndoableEditListener(                                  UndoableEditListener 1) {       support.removeUndoableEditListener(1);    }    public void collapsePath(TreePath path) {       boolean wasExpanded = isExpanded(path);       super.collapsePath(path);       boolean isExpanded = isExpanded(path);       if (isExpanded != wasExpanded) {          support.postEdit(new CollapseEdit(path));       }    }    public void expandPath(TreePath path) {       boolean wasExpanded = isExpanded(path);       super.expandPath(path);       boolean isExpanded = isExpanded(path);       if (isExpanded != wasExpanded) {          support.postEdit(new ExpandEdit(path));       }    }    private void undoExpansion(TreePath path) {       super.collapsePath(path);     }    private void undoCollapse(TreePath path) {       super.expandPath(path);     }    private class CollapseEdit extends AbstractUndoableEdit {       public CollapseEdit(TreePath path) {          this.path = path;       }       public void undo() throws CannotUndoException {          super.undo();          UndoableTree.this.undoCollapse(path);       }       public void redo() throws CannotRedoException {          super.redo();          UndoableTree.this.undoExpansion(path);       }       public String getPresentationName() {          return "node collapse";       }       private TreePath path;    }    private class ExpandEdit extends AbstractUndoableEdit {       public ExpandEdit(TreePath path) {          this.path = path;       }       public void undo() throws CannotUndoException {          super.undo();          UndoableTree.this.undoExpansion(path);       }       public void redo() throws CannotRedoException {          super.redo();          UndoableTree.this.undoCollapse(path);       }       public String getPresentationName() {          return "node expansion";       }       private TreePath path;    }    private UndoableEditSupport support =                                   new UndoableEditSupport(this) } 

JTree has many constructors but, to avoid cluttering the listing with irrelevant details, we only implement one in our subclass. The logic for handling the expansion or collapse of a node is shown in the expandPath and collapsePath methods, which are very similar to each other, so we'll cover both cases by looking at how collapsePath works. An UndoableEdit should only be created if the node actually collapses as a result of calling this method. Although this should be guaranteed if this method is invoked as a result of user action, there is no certainty that an application will only invoke collapsePath when the node is expanded. To guard against this case, we first get the expanded state of the node using the isExpanded method, and then invoke the superclass collapsePath implementation to actually cause the node to close. When this method returns, we call isExpanded again. If these two calls return different values, the node must have been collapsed and we can generate our edit. If they return the same value, either the node was already collapsed or the TreePath was illegal; in either case, we don't want to do anything more.

The next problem is what our UndoableEdit implementation should do. We could create a class that provides a complete implementation of the UndoableEdit interface, but it is simpler to subclass AbstractUndoableEdit to get default implementations of methods like isSignificant that we don't need to add anything to. In fact, we create two subclasses of AbstractUndoableEdit called CollapseEdit and ExpandEdit that are used by collapsePath and expandPath respectively. Of course, because these events are the reverse of each other, we could have managed with only one class to which we provide a constructor parameter that tells it which way to behave but, given that the code is so simple, it is clearer to create two very simple classes than one slightly more complex one. For convenience, both of these classes are inner classes of UndoableTree. This is, in fact, a very common way to implement UndoableEdits they naturally form a part of the object being "edited," so an inner class is a convenient and natural way to build them. In this case, it also allows easy access to the tree to which they relate and saves us having to store that information as another attribute passed to the constructor.

What should the undo and redo methods actually do? As we know, their first job is to invoke their counterparts in their superclass to verify that the edit is in the correct state. Having done this, the undo method of the CollapseEdit has to expand the TreePath that was originally collapsed when the edit was created. Similarly, the redo method must collapse that TreePath. Because we need access to the TreePath, we need to pass this object to the CollapseEdit constructor. It would seem, then, that we could implement the undo method as simply as this:

 public void undo() throws CannotUndoException {    super.undo();    UndoableTree.this.expandPath(path); // Plausible, but wrong! } 

Unfortunately, this is wrong. The problem is that we have overridden the expandPath method so that we can generate an undoableEditEvent after the node is expanded. If we take this approach, when we undo a Collapse-Edit we'll get an ExpandEdit created and posted to our listeners. This is not what we want! What we actually want to do is invoke the JTree's expandPath method, but we can't do this directly from anything other than UndoableTree itself. To solve this problem, we added to UndoableTree private methods called undoExpansion and undoCollapse that call the JTree expandPath and collapsePath methods for us (see Listing 9-2). Because CollapseEdit is an inner class of UndoableTree, it has access to these private methods. So, the final implementation of the undo method of CollapseEdit looks like this:

 public void undo() throws CannotUndoException {    super.undo();    UndoableTree.this.undoCollapse(path); } 

The redo method similarly calls undoExpand. The same methods are also used by ExpandEdit, in which the undo method uses undoExpand and redo calls undoCollapse.

Of the other UndoableEdit methods, the only one we can usefully override is getPresentationName, which, in both cases, returns a string describing the effect of the edit. We don't make any use of this string in this example, but we will see examples of its use later in this chapter.

Once we've created the correct edit, we need to be able to broadcast it to all the UndoableTree's UndoableEditListeners in the form of an UndoableEdit. This requires three things:

  • The UndoableTree needs to provide addUndoableEditListener and removeUndoableEditListener methods to allow the registration and removal of listeners.

  • When a CollapseEdit or ExpandEdit has been created, we need to be able to turn it into an UndoableEditEvent.

  • The UndoableEditEvent needs to be sent to all listeners.

Fortunately, we don't have to do much work to provide these features because the undo package contains a class called undoableEditSupport that will do all this for us. Here are the public methods of this class:

 public UndoableEditSupport(); public UndoableEditSupport(Object source); public synchronized void addUndoableEditListener (                            UndoableEditListener l); public synchronized void removeUndoableEditListener(                            UndoableEditListener l); public synchronized void postEdit(UndoableEdit e); public int getUpdateLevel(); public synchronized void beginUpdate(); public synchronized void endUpdate(); 

To use UndoableEditSupport, an event source such as UndoableTree creates an instance of it and delegates certain operations to it. The two constructors allow you to determine the source of the UndoableEditEvents that this class will generate; if you use the default constructor, the UndoableEditSupport itself appears as the event source. If you want your component to be the source of the event, use the second constructor, as is shown in Listing 9-2. Similarly, the component implements the addUndoableEditListener and removeUndoableEditListener methods by delegating directly to the ones provided by UndoableEditSupport, which manages the registration and removal process and also handles sending events to all registered listeners.

Having created an UndoableEdit, there are two ways to use UndoableEditSupport to create and deliver an event. The simplest way is to just invoke postEdit, which constructs an UndoableEditEvent containing the edit passed as its argument and passes it to its listeners. This is how UndoableTree delivers its UndoableEditEvents. There is also a slightly more advanced mechanism that makes use of the beginUpdate, postEdit, and endUpdate methods that we'll cover in the next section.

 

 



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