Conventions Used in This Book

Java > Core SWING advanced programming > 9. THE SWING UNDO PACKAGE > Compound Edits

 

Compound Edits

UndoableEdits created from the AbstractUndoableEdit class represent only a single change to an object. In many cases, it is convenient to group together several actual changes in whatever it is that is being edited and represent them as a single edit that can be undone or redone as a unit. The undo package provides a subclass of AbstractUndoableEdit called Compound-Edit that can be used to create edits made up of several smaller pieces, each of which is an UndoableEdit of some kind. The CompoundEdit class provides the same methods as AbstractUndoableEdit and adds two more of its own isInProgress and end. As well as implementing these new methods, most of the methods inherited from AbstractUndoableEdit are overridden to provide behavior appropriate for an edit with multiple subedits.

To create a CompoundEdit, you do the following:

  1. Create a CompoundEdit object. CompoundEdit acts as a container for other edits, which is initially empty and "in progress."

  2. Add one or more UndoableEdits to it using the addEdit method. At this stage, the isInProgress method would return true.

  3. Complete the CompoundEdit by calling its end method.

Once the end method has been called, the edit is no longer "in progress" and the isInProgress method would now return false. This has two consequences:

  • You can't add any more edits. If you try to do so, the addEdit method simply returns false.

  • You can now undo the edit. If you call undo before invoking the end method, you get a CannotUndoException. Similarly, invoking redo before the edit is ended results in a CannotRedoException.

Calling end is an irreversible step you cannot subsequently "reopen" the edit and add something new.

Although a CompoundEdit is made up of one or more smaller units, calling undo results in all the edits that it contains being undone, in reverse order. In other words, a CompoundEdit is treated as a single atomic action. Likewise, calling redo reapplies all the edits in the order in which they were added. Because of this, users can't tell that they are undoing or redoing a Compound-Edit rather than a simple edit the compound nature of the edit is, in fact, only for the convenience of the software as we'll see when we look at a couple of examples later in this section. Table 9-2 summarizes what the most important CompoundEdit methods do by comparison with those of AbstractUndoableEdit.

Table 9-2. Implementation of CompoundEdit
Method Description
undo If the end method has not been called or undo has already been called since the last redo call (in other words the edit has already been undone), a Cannot-UndoException is thrown. Otherwise, the last edit added is undone, followed by the one added before that and so on, until they have all been undone.
canUndo If the end method has not been called or undo has already been called since the last redo call (in other words the edit has already been undone), this method returns false. Otherwise, it returns true.
redo If the end method has not been called or redo has already been called since the last undo call (in other words the edit has already been redone), a CannotRedoException is thrown. Otherwise, the edits are redone in order, starting with the first one added to the CompoundEdit.
canRedo If the end method has not been called or redo has already been called since the last undo call (in other words the edit has already been redone), this method returns false. Otherwise, it returns true.
end Marks the edit as being no longer in progress.
die Calls die on all the contained edits.
addEdit If end has been called, this method returns false. Otherwise, it adds the give UndoableEdit at the end of the current set of edits and returns true. See later in this chapter for further discussion of this method.
isInProgress Returns true if the end method has not been called. After end has been invoked, this method always returns false.
isSignificant Returns true if the isSignificant method of any of the edits contained by this CompoundEdit returns true. Otherwise returns false.
getPresentationName Returns the presentation name of the last edit added to the CompoundEdit.
getUndoPresentationName Returns the result of calling getUndoPresentationName on the last edit added to the CompoundEdit.
getRedoPresentationName Returns the result of calling getRedoPresentationName on the last edit added to the CompoundEdit.

In the simplest case, the edits in a CompoundEdit are simply held in a list in the order in which they were added in other words, if you create a CompoundEdit to which you add five UndoableEdits and call end, you end up with a list of five UndoableEdits. However, the addEdit method does not simply add each edit to the internal list. Instead, it does the following:

  1. If the internal list is empty, the edit is placed in the list.

  2. Otherwise, the addEdit method of the last edit in the list is called with the new edit as its argument. This gives the last edit a chance to "absorb" the new one, allowing the latest one to be discarded. If this call returns true, the new edit is discarded (having added its effect to the last one) and no further action is taken.

  3. Otherwise, the replaceEdit method of the new edit is called with the last edit added as its argument, giving the new edit the chance to absorb the old edit and take its place, thus reversing the roles of the previous step. If this call returns true, the last edit is removed from the list (having become a part of the new edit) and the new one added in its place.

  4. Finally, if none of the above applies, the new edit is added to the list.

In principle, this operation is meant to allow related edits to be merged together. As an example, suppose the user types several characters into a document being monitored by an UndoableEditListener, which is adding the edits from the UndoableEditEvents that get generated into a CompoundEdit that it will subsequently close by calling its end method. As you already know, if you insert 10 characters, you get 10 UndoableEditEvents and 10 UndoableEdits will be added to the CompoundEdit, each able to undo or redo the insertion of a single character. However, if these UndoableEdits were implemented appropriately, their addEdit methods could absorb another UndoableEdit if certain conditions were met (that is, both edits must add characters to the document in adjacent locations, or both must delete characters in adjacent locations). In this case, the 10 single-character UndoableEdits would be replaced by a single UndoableEdit containing all 10 characters. Unfortunately, the edits generated by the Swing text package do not support merging in this way because their addEdit and replaceEdit methods always return false and, in fact, there is no practical use of edit merging anywhere in Swing.

In any case, you might be wondering what the point of edit merging is if CompoundEdits can only be undone or redone as a single unit. Because of this, there is very little difference between a CompoundEdit containing 10 single-character edits and one with one merged 10-character edit after all, calling undo on either of these results in all 10 characters being removed from the document, so the user would not be able to tell the difference between these two cases. In practice, other than the fact that merging would probably save a small amount of memory (by allowing nine UndoableEdits to be discarded) and the undo process would be slightly faster, there appears to be no real reason for CompoundEdit to have the concept of edit merging. However, this is not quite true. In fact, the edit merging code is present for the benefit of UndoManager, which is a subclass of CompoundEdit that has different rules for undoing and redoing the edits that it contains. We'll say more about this in "The UndoManager Class".

A Compound Edit Example

In the last example, you saw how to create a pair of UndoableEdits that can handle the expansion and collapse of a tree node and a subclass of JTree that emits UndoableEditEvents containing these edits when necessary. However, there is a small problem with this example. To see what it is, run the example again and click on the expansion handle of the Apollo 8 node to expand it and reveal its three child nodes, and then select one of them as shown in Figure 9-3.

Figure 9-3. Selecting a node in a tree with undo support.
graphics/09fig03.gif

Now close the Apollo 8 node by clicking on its expansion handle again. As you know, this will cause the tree to generate an UndoableEdit that will be stored by the UndoManager so that you can undo it using the Undo button. If you now press Undo, the node reopens but, as you can see from Figure 9-4, the selection is not where it originally was in Figure 9-3. When you collapse a tree node, if any of its children were selected, they are deselected (because they are going out of view) and the collapsing node itself is selected instead. Unfortunately, although our UndoableEdits capture the collapse of the branch node, they do not record the fact that the selection might also change as a direct consequence of this.

Figure 9-4. Incorrect selection behavior when undoing a node collapse operation.
graphics/09fig04.gif

One way around this is to modify the CollapseEdit and ExpandEdit classes so that they incorporate the selection information and implement the undo and redo methods so that they restore the selection as appropriate. There is, however, a more flexible solution available. Because expanding or collapsing nodes and moving the selection are two separate operations that exist independently of each other, instead of adding code to the existing classes, we implement a new UndoableEdit that deals only with the selection change. When the user expands or collapses a node, instead of creating one UndoableEdit, the tree would create two one for the node state change and one for the selection change. The advantage of this over adding the functionality to the existing classes is that the new UndoableEdit class can be used in other contexts in which the user changes the tree selection, without expanding or collapsing a node. If we call this new class SelectionEdit, we could change the tree implementation to fire two undoable-EditEvents when a node is collapsed, like this:

  1. Fire an UndoableEditEvent containing a SelectionEdit.

  2. Fire an UndoableEditEvent containing a CollapseEdit.

The undoManager would then store these events so that they could be undone on demand. The order in which the events are generated is essential if they were stored the other way around, the UndoManager would first reverse the SelectionEdit and then the CollapseEdit. This would not achieve the desired effect, because undoing the SelectionEdit first would attempt to select a child node that is still invisible. The tree would ignore this, so the branch node would remain selected and would then expand, leaving the selection in the wrong place. On the other hand, storing the edits in the order shown earlier causes the operations to be reversed properly the CollapseEdit is undone first, expanding the node and revealing the three child nodes, followed by the SelectionEdit, which will pass the selection to the original child node, which is now visible.

The disadvantage of generating an UndoableEdit for each state change is that the UndoManager only reverses one of them at a time that is, when you click Undo once, the CollapseEdit is undone so that the branch node reopens, but it retains the selection. You need to click Undo again to reverse the SelectionEdit and have the selection move to the right place. In other words, although the tree created two edits, we want the UndoManager to treat them as one so that pressing Undo seamlessly would reverse both of them together and subsequently pressing Redo would reapply them both. You can achieve exactly this effect by building the two individual edits into a single CompoundEdit because, as we said earlier, once the CompoundEdit has been closed (that is, the end method has been invoked), calling undo on it results in the undo method of all the individual edits being called in reversed order and similarly for the redo method.

There are two ways to create a CompoundEdit from a set of UndoableEdits. The most direct way is to simply instantiate a CompoundEdit object and directly add edits to it using its addEdit method:

 CompoundEdit edit = new CompoundEdit(); edit.addEdit(new SelectionEdit(/* Parameters not shown * /) ; edit.addEdit(new CollapseEdit(path)); edit.end(); support.postEdit(edit);       // Post the event to listeners 

There is also a slightly more compact way to achieve the same thing using the UndoableEditSupport class that we used in the previous example. In that example, and in the previous code extract, we created an UndoableEdit object and broadcast it to our listeners by calling the UndoableEditSupport objects postEdit method. UndoableEditSupport also has a "batching" mode that is triggered by invoking its beginUpdate method, which causes it to create an empty CompoundEdit. Once this has been done, you can pass individual edits to it using the postEdit method. Instead of directly delivering these edits as UndoableEditEvents, UndoableEditSupport adds them to the CompoundEdit, until you call endupdate, at which point it creates and broadcasts an UndoableEditEvent containing the entire CompoundEdit. Calling endupdate also switches off the batching, so a subsequent call to postEdit would deliver the edit immediately unless preceded by another invocation of beginUpdate. Using this mechanism, the code extract shown earlier would be rewritten like this, where support is an instance of UndoableEditSupport:

 support.beginUpdate(); support.postEdit(new SelectionEdit(/* Parameters not shown */); support.postEdit(new CollapseEdit(path)); support.endUpdate(); 

What about the UndoableEdit that manages the change of tree selection? Implementing this is very simple all we need to do is get the state of the tree's selection model before and after the node is expanded or collapsed and store that information in the SelectionEdit object which, like ExpandEdit and CollapseEdit, is derived from AbstractundoableEdit. To undo the edit, we call the trees setSelectionPaths method with the selection saved before the node changed state and to redo it we do the same thing but pass the state of the selection after the expansion or collapse. The details are shown in Listing 9-4, in which the changes made from our first Undoable-Tree implementation (in Listing 9-3) are shown in bold.

Listing 9-4 Creating a CompoundEdit with UndoableEditSupport
 package AdvancedSwing.Chapter9; import javax.swing.*; import javax.swing.event.*; import javax.swing.tree.*; import javax.swing.undo.*; public class UndoableTree2 extends JTree {    public UndoableTree2(TreeNode root) {       super(root);    }    public void addUndoableEditListener(UndoableEditListener 1) {       support.addUndoableEditListener(l);    }    public void removeUndoableEditListener(                                    UndoableEditListener l) {       support.removeUndoableEditListener(l);    }    public void collapsePath(TreePath path) {      boolean wasExpanded = isExpanded(path);      TreePath[] selections = getSelectionPaths();       super.collapsePath(path);       boolean isExpanded = isExpanded(path);       if (isExpanded != wasExpanded) {         TreePath[] newSelections = getSelectionPaths();         support.beginUpdate();         support.postEdit(new SelectionEdit(selections,                                            newSelections));         support.postEdit(new CollapseEdit(path));         support.endUpdate();       }    }    public void expandPath(TreePath path) {      boolean wasExpanded = isExpanded(path);      TreePath[] selections = getSelectionPaths();       super.expandPath(path);       boolean isExpanded = isExpanded(path);       if (isExpanded != wasExpanded) {         TreePath[] newSelections = getSelectionPaths();         support.beginupdate();         support.postEdit(new SelectionEdit(selections,                                            newSelections));         support.postEdit(new ExpandEdit(path));         support.endUpdate();       }    }    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();             UndoableTree2.this.undoCollapse(path);          }          public void redo() throws CannotRedoException {             super.redo();             UndoableTree2.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();       UndoableTree2.this.undoExpansion(path);    }    public void redo() throws CannotRedoException {       super.redo();       UndoableTree2.this.undoCollapse(path);    }    public String getPresentationName() {       return "node expansion";    }    private TreePath path; } private class SelectionEdit extends AbstractUndoableEdit {   public SelectionEdit(TreePath[] oldSelections,                        TreePath[] newSelections) {      this.oldSelections = oldSelections;      this.newSelections = newSelections;   }   public void undo() throws CannotUndoException {      super.undo();      UndoableTree2.this.setSelectionPaths(oldSelections);   }   public void redo() throws CannotRedoException {      super.redo();      UndoableTree2.this.setSelectionPaths(newSelections);   }   public String getPresentationName() {      return "selection change";   }      private TreePath[] oldSelections;      private TreePath[] newSelections;   }    private UndoableEditSupport support =                                new UndoableEditSupport(this); } 

You can try this code by typing the following command:

 java AdvancedSwing.Chapter9.UndoExample4 

When the tree appears, do the following:

  1. Click the expansion handle for the Apollo 8 node.

  2. Click the expansion handle for the Apollo 11 node.

  3. Click the node for Armstrong.

  4. Hold down the SHIFT key and click the node labeled Collins. This selects all three child nodes of the Apollo 11 node.

  5. Click the expansion handle for Apollo 11 again. This causes the node to collapse and the selection will move from the three child nodes to the Apollo 11 node.

Each time you open or close a node, an UndoableEditEvent is delivered to the UndoManager, which extracts the associated UndoableEdits and stores them. At this point, three UndoableEdits have been sent to the UndoManager, as shown in Figure 9-5.

Figure 9-5. UndoableEdits generated from the tree.
graphics/09fig05.gif

If you now press the Undo button, the UndoManager will invoke the undo method of the last UndoableEdit that was added to it, which is the CompoundEdit labeled C in Figure 9-5. Because calling undo on a Compound Edit undoes all the edits it contains, this will result in the collapseEdit being undone, followed by the SelectionEdit, thus restoring the selection to the three child nodes of the Apollo 11 node. At this point, the state of the UndoManager will have changed to that shown in Figure 9-6.

Figure 9-6. Undoing a single edit with UndoManager.
graphics/09fig06.gif

Obviously, pressing Undo again will undo CompoundEdit B and collapse the Apollo 11 node again. Pressing Redo at this point will redo edit B to expand that node and restore the selection, while pressing Redo again will collapse the node again. The important points to note about this are the following:

  • The individual edits of a CompoundEdit cannot be separately undone or redone to all intents and purposes, a CompoundEdit appears to be a single edit.

  • The edits stored by UndoManager can be manipulated separately, even though UndoManager is derived from CompoundEdit. This allows the user to reverse individual operations.

Compound Edits and the Text Components

The edits created by the Swing text component are very similar to the ones that you've seen in the two tree examples shown earlier in that they are CompoundEdits made of simpler action-specific edits derived from the base class AbstractUndoableEdit. The basic edits are generated by low-level code within the text package and are merged into a CompoundEdit at the point that the UndoableEditEvent is delivered to the UndoableEditListeners of a text component's Document. There is, in fact, a direct relationship between the edits, the UndoableEditEvent delivered to the UndoableEditListeners, and the corresponding DocumentEvent created for DocumentListeners. The relationship between these classes for a typical change to a text component is shown in Figure 9-7.

Figure 9-7. Text component events and UndoableEdits.
graphics/09fig07.gif

As you can see, the DocumentEvent that is delivered to DocumentListeners is actually an instance of the class DefaultDocumentEvent, an inner class of javax. swing. text .AbstractDocument. Recall from Chapter 1 that, unlike the other Swing events (and AWT events), DocumentEvent is actually an interface, not a class. DefaultDocumentEvent implements the DocumentEvent interface but is also derived from CompoundEdit, so it can contain edits described by the simpler AbstractUndoableEdit subclasses generated by code in the Document implementations. Because of this arrangement, DocumentListeners can get access to the undoableEdit information for any change to the Document simply by casting the DocumentEvent to a DefaultDocumentEvent, but this is not recommended because it makes an assumption about the implementation of the text package. The same DefaultDocumentEvent is also delivered to UndoableEditListeners as the edit associated with the UndoableEdit passed to the listener's methods. Using an UndoableEditListener is the recommended way to get direct access to the UndoableEdit, by calling the UndoableEditEvent's getEdit method, which returns a reference to the DefaultDocumentEvent object.

The text package defines four UndoableEdit classes, all derived from AbstractUndoableEdit. Table 9-3 describes what these UndoableEdits mean and the information that they store.

Table 9-3. UndoableEdits Defined in the Text Package
Class Name Defining Class Description
AttributeUndoableEdit DefaultStyledDocument This edit is used when changing or replacing the attributes of an Element of a document. It stores a reference to the Element, the existing elements, the new attributes being applied, and a flag indicating whether these attributes replace or merge into the existing ones. To undo the operation, the old attributes are simply used to directly replace the current attribute set of the Element. To apply or redo the operation, the new attributes are merged with the existing set, or overwrite them if total replacement is required.
StyleChangeUndoableEdit DefaultStyledDocument Records a change in the logical style associated with a paragraph. The new Style, the original logical style, and a reference to the paragraph's Element are stored. To perform or redo the operation, the attribute resolving parent of the Element is set to the new Style supplied. To undo the operation, the original resolving parent attribute set is restored.
ElementEdit AbstractDocument ElementEdit records a change to the underlying Element structure of a Document. It stores a reference to a parent Element, an index into that parent where the change starts, an array of Elements added, and an array of Elements removed. To apply the operation, the child Elements in the removed set, which start at the given index within the parent, are removed and then the Elements in the added set are inserted instead of them. The redo action reverses this process.
InsertUndo GapContent and StringContent This edit records the insertion of text into the Content underlying the model. The text being inserted and the offset at which it is inserted are supplied to the constructor. The operations needed to apply or redo and to undo this change are simple and are performed directly by the Content class on the data that it stores.
RemoveUndo GapContent and StringContent This class is the same as InsertUndo except that it represents the removal of text. Because of this, they are almost exact copies of each other with the undo and redo methods swapped over.

In many cases, the DefaultDocumentEvent will only contain one of these edits. Direct insertion of text, for example, will create a DefaultDocumentEvent with a single insertundo edit. However, there are cases to which this does not apply. To see an example of this, type the following command

 java AdvancedSwing.Chapter9.UndoExample2 

which runs an example that we saw earlier in this chapter. Now type text into the text pane, select some (but not all) of it and use the Font and Style menus to change the selected text to bold. When you do this, you'll see the following event appear in the Undo Monitor window:

 style change([javax.swing.text.AbstractDocument$          ElementEdit@696fcbac hasBeenDone: true alive: true, javax.swing.text.DefaultStyledDocument$          AttributeUndoableEdit@698fcbac hasBeenDone: true alive: true]) 

Here, the DefaultDocumentEvent contains an ElementEdit followed by an AttributeUndoableEdit, the latter of which applies the attribute change that makes the text appear bold. Why is there also an ElementEdit here? As we saw earlier in this book, a range of characters within the document that has the same attributes is managed by a single Element. When you type the original text into the text pane, it is all mapped by one leaf Element. However, when you change the font of some of the text to bold, not all the text will have the same attribute set, so it can no longer be covered by a single Element. Hence, the existing element is replaced by two or three new ones, depending on how you selected the text. Suppose you type the text Some text into the text pane. The initial Element structure will include a single leaf Element mapping all these characters, as shown in Figure 9-8.

Figure 9-8. Element structure for a simple document.
graphics/09fig08.gif

If you now select the text me te and change its font to bold, the text will be divided into three regions, each with a different attribute set:

  1. The characters So with the original plain font

  2. The characters me te with a bold font

  3. The characters xt also with the original font

Because there are three different attribute sets involved, there needs to be three Elements to cover the same text mapped by the original leaf Element, as shown in Figure 9-9.

Figure 9-9. Element structure after an attribute change.
graphics/09fig09.gif

To change the font of the middle five characters to bold, the following steps are performed:

Step 1.

The original leaf Element at offset 0 of the branch Element for the paragraph is removed and three new Elements with the same attribute set are inserted instead.

Step 2.

The attribute set of the middle Element is changed to include the bold setting.

The first of these two actions generates the ElementEdit and the second results in the AttributeUndoableEdit. Both of these are encapsulated inside a single CompoundEdit, so they will always be undone or redone as a single unit. If this edit is undone, the attribute change is reversed first and then the Element change is undone by restoring the original leaf Element.

Core Note

Because ElementEdit operates by storing references to Elements that are no longer in use, it is possible to use up memory very quickly if you keep a long history of document changes in your UndoManager. As we'll see in the next section, it is possible to configure how many updates UndoManager will store and to discard unwanted edits at any time.



 

 



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