Home | pfodApps/pfodDevices | WebStringTemplates | Java/J2EE | Unix | Torches | Superannuation | | About Us
 

Forward Logo (image)      

UndoRedo
The complete Undo/Redo Manager

Java GUI Programming Tips and Guidelines
What's Wrong with Undo/Redo and How to fix it.

There are two main problems with Java's Undo/Redo.

  1. Programmers do not make it available to the user most of the time.

  2. After you undo you loose all your previous redos history once you do any other undoable operation.

  3. The UndoableEdit's getPresentationName() method does not provide sufficient detail on what is going to be undone.

There is not a lot that I can do personally to fix the first problem, except to encourage you to implement undo for all non-trivial actions. At present undo is usually only seen in text editors. Although Java provides a javax.swing.undo.StateEdit class to capture and undo general changes in state, it seems to be under utilized.

As for the other two problems, the UndoRedo package presented here contains a UndoRedoManager that keeps a complete history of all the user's actions, undos and redos that are notified to the UndoRedoManager. Unlike the standard UndoManager, the UndoRedoManager does not truncate the undo history, nor does it throw away the redo history once the user performs another action. The UndoRedoManager also provides an optional IHaveDescription interface which if present in the UndoableEdit is used to show a fuller description of the what effect undoing or redoing this operation will have.

The package, source code, docs and this test example can be downloaded in .zip and .gz and used freely for commercial use subject to this Licence.




In the UndoRedoManager, Undo and Redo work in just the same way as the previous Java UndoManager. The Undo actions are available to be undone by calling UndoRedoManager's undo() method, which undoes one action at a time until you reach the initial point of the application. Calling the UndoRedoManager's redo() method redoes the last series of undos in the same way the previous Java UndoManager did.

As well as these traditional functions, the UndoRedoManager maintains a tree structure and a JTree which can be displayed to allow the user to select any previous state and undo/redo to that point by selecting that node in the tree and calling the UndoRedoManager's processUndoRedoCommand(). By default the pressing the Enter key on the selected node performs this action.

The UndoRedoManager tree keeps a main trunk of all the actions the user has done or redone which have not been undone, in the order they were performed, most recent at the top. Actions the user has done are shown as available to Undo. Actions the user undid are shown in a branch node as available to Redo.

As actions are undone they are transferred to a Redo branch from which they can be redone. As actions are redone they add a new undo leaf to the top of the tree. A consequence of this is that each time a user performs a series of Undo actions, a new Redo branch is created, even if the user has previously retraced this same route. This faithfully records the time order of all the user's actions.

To see the UndoRedoManager in action, change directory to the location of the UndoRedo.jar file and type

java -jar UndoRedo.jar

Then type 12345 in the text field.

Next Press Ctrl+Z twice to undo to 123

Then Press Ctrl+Y to redo to 1234

Finally type 0 to get the display 12340

The UndoRedo Manager Test shows.




So far this is no different from the standard Java UndoManager. Note howevere that now that you have type 0 there is no way of using the standard Java undo/redo to go back to the text 12345

Press Ctrl+Shift+Z to open the Undo/Redo tree dialog (Escape will close it). Note that while the node names provide some detail on the operation performed, it is not easy to see what the effect will be of undoing any particular action.




In this test example I have wrapped the UndoableEdit objects returned from the text field's Document to implement the IHaveDescription interface. This interface is optional but if present the getDescription() method will be called to display a more detailed description for the node. Select any node to display its description. You will see a “Loading description...” message and after a short delay the description will be displayed.




The delay is artificial to illustrate that the description is being prepared on a separate background thread. Using a separate background thread is not mandatory but is recommended if you are loading the description from disk or building it from the incremental operations in the undo tree. When you select a new node, the cancelGetDescription() method of IHaveDescription is called to cancel the background thread. See the UndoRedoTest.java code for a trivial implementation of this background thread. By default a JTextArea is used to display the description, but you can provide you own component to the UndoRedoManager constructor.

You can now select any node and Press Enter to Undo or Redo to that previous point in the edit.

In this test UndoRedo Tree display dialog, the Ctrl+Z and Ctrl+Y keys have been mapped to undo() and redo(). Try pressing Ctrl+Y. Nothing happens since there is no Redo branch at the top of the main trunk of the tree. Press Ctrl+Z and a redo branch is inserted containing “Redo INSERT '0' at offset:4”




Each Branch to Redo node is labled with the deepest Redo contained in that branch. If you Press Enter on a Branch to Redo node then you will redo to that deepest node indicated.

Some Branch to Redo nodes are prefixed with > or >>.
> indicates you have backtracked some way down that branch.
>> indicates you have backtracked to the deepest Redo node in that branch and have continued on from there.

Open the “>Branch to Redo INSERT '5' at offset:4” node.




The > next to the “Redo INSERT '4' at offset:3” indicates that this node has been redone. That is you will find a corresponding Undo node in the main trunk.

Select the “Redo INSERT '5' at offset:4” node below and Press Enter




The branch now shows “>> Branch to Redo INSERT '5' at offset:4” indicating you have completely retracted the steps in this branch. The text field now shows 12345.




Type 6 in the text field. The UndoRedo Tree now shows




Notice that the “Branch to Redo INSERT '5' at offset:4” still shows >> indicating you have completely retraced this branch and have continued on from there.

If an UndoableEdit is dead then “!!DEAD!!” is prefixed to the node. UndoRedoManager never calls die(). When undoing or redoing the UndoRedoManager will stop at the first dead node it meets.

Check the javadocs for the UndoRedoManager and the UndoRedoTest.java code for details on adding this manager to your own applications. Since UndoRedoManager implements UndoableEditListener at the simplest you can just add an instance of UndoRedoManager as a listener for your UndoableEditEvents.

Cancelling Undos/Redos

Note: This section of the code is under development and its functionality and implementation may change. It is disabled by the default constructor.

In a responsive GUI any time consuming task should be handed off to a background thread for execution (see FutureTalker for a simple way of keeping track of what this background thread is doing). Once the task is given to the background thread the user is free to commence other operations. The situation to be considered here is what to do if the user cancels some of these background tasks or what happens if they throw an error and don't complete successfully.

One option is that you could just not register the UndoableEdit with the UndoRedoManager until the task completes but this prevents the user from seamlessly continuing with their work. The user should be able to request a task be done and assume it will be done (possibly at some future time). As they make other requests the tasks stack up in order to be done. (Note: this assumes sequential task processing which fits with the tree display of the users actions. Concurrent task process should probably be handled by two or more UndoRedoManagers each one handling those tasks that must be sequential to each other.)

The preferable option is to create the UndoableEdit when the user makes the request and registered the UndoableEdit with the UndoRedoManager before the task is executed. This task is now available for undo by the user, even if the original task has not yet completed. The undo task will just be queued up after the original task seamlessly to the user. Note: the GUI needed to be able to reflect the successful completion of the task before the task executes so the user can continue to work as though the task completed successfully.

Now when a task fails or the user presses Cancel to cancel pending tasks, the UndoRedoManager must clean up to represent the current state. The GUI also need to be restored to this point as well. You could use the Undo information to do this, but it is safer to reload the GUI with the current state if possible.

When a task is cancelled or fails to complete it needs to be removed from the tree. If the UndoableEdit passed to the UndoRedoManager implements the interface ICanBeRolledBack and if the UndoRedoManager has been constructed with rollBackSupport enabled, then a shallow clone of the entire undo-redo tree is stored and a frame marker passed back to the UndoableEdit. This frame marker can then be used to restore the undo-redo tree to either the state before a cancelled or failed action (cancel()) or to the state after the last successful action (syncToLastSuccessfull()). When the roll back is no longer needed for an operation you can just null all references you hold to that frame and it will be garbage collected as the UndoRedoManager frame map only holds a week reference to the frames.

Note cloning the tree is fairly cheep as the underlying UndoableEdits do not need to be cloned only the wrapping UndoRedo and the Mutable Tree Node are cloned. Note when the task completes need to drop the cloned tree, otherwise need to restore it and reset the underlying UndoableEdit's undo state.

Things to do:

Things to do: The following items still need to be addressed.

  1. The UndoRedoManager should handle dead nodes. If there are any dead nodes, they and any unreachable nodes should be disabled.

    The latest version does this by preventing undos past dead nodes and updating the tree as the use tries to traverse it.

  2. Implement the “Selective Undo Framework”. Even for a single user, the interchange method described in this article would allow you to undo/redo some previous action without having to undo/redo all the intermediate actions.

    This “Selective Undo Framework” Takes a slightly different approach then the one used here. The approach here is that if you decide you want to undo some operation then regardless of what else that entails that operation will be undone. That may mean in many cases undoing other later operations. The Selective Undo Framework takes a slightly different view. It tries to undo the selected operation without affecting any later operations. If it cannot achieve this it raises a conflict and does not undo any operations. In general I think this is too restrictive and so this UndoRedoManager assumes the user knows what they are doing and so the UndoRedoManager will do what ever is necessary to achieve the requested result. Note: that because all actions are recorded if the user changes their mind later they can always reverse the undo they previously requested.

  3. The UndoRedoManager ignores the isSignificant() hint when displaying tree nodes. However Insignificant nodes are shown in strike through and are skipped on undo redo (unless you specifically request that node to be undone)

  4. The UndoRedo class does not implement addEdit() and replaceEdit(), so compound edits need to be collected before calling the UndoRedoManager's undoableEditHappened()

  5. The operation of rolling back operations that failed or were cancelled needs more real life testing to refine the functionality.


Forward home page link (image)

Contact Forward Computing and Control by
©Copyright 1996-2015 Forward Computing and Control Pty. Ltd. ACN 003 669 994