Lesson: Customizing the Undo/Redo Support

The default undo/redo support provided by class Graph2DUndoManager is already quite powerful, but if we need support for further actions to be reversible, we have to create our own undo/redo command. Luckily, this is straightforward to do.

As an example, when we allow additional data associated with graph elements to be changed, and want these changes to be reversible, we need an undo/redo command that handles these actions.

The CustomTextChanged class in the UndoRedo2 application is tailored to match the CustomGraph2D class which provides convenience methods for storing and retrieving the additional data associated with nodes. Example 1.3, “Custom undo command implementation” presents the CustomTextChanged class. Note that CustomTextChanged implements the Command interface, which is used by Graph2DUndoManager for its commands.

Example 1.3. Custom undo command implementation

public class CustomTextChanged implements Command {
  CustomGraph2D graph;
  Node node;
  String oldText;

  public CustomTextChanged(CustomGraph2D graph, Node node, String oldText) {
    this.graph = graph;
    this.node = node;
    this.oldText = oldText;
  }

  public void undo() {
    String tmp = graph.getCustomText(node);
    graph.setCustomText(node, oldText);
    this.oldText = tmp;
  }

  public void redo() {
    undo();
  }

  public void execute() {}
}

In an undo/redo command we only need implementations for the undo and redo methods, not the execute method of the interface. In the constructor of our Command implementation we hand over all necessary parameters. For our use case, we need the CustomGraph2D instance that handles the additional data for the nodes, the node for which the data has changed, and its old data. The new data will already be set with the node when our command gets invoked.

Now, how (and when) do we add our undo/redo command to the command stream held by Graph2DUndoManager? Graph2DUndoManager's push method serves the "how," see the line in Example 1.4, “Pushing onto the undo/redo command stream”.

Example 1.4. Pushing onto the undo/redo command stream

getUndoSupport().push(new CustomTextChanged(...));

The answer to the "when" is in the code snippet in Example 1.5, “Adding our undo/redo command to the command stream”.

Example 1.5. Adding our undo/redo command to the command stream

toolbar.addSeparator();
// Add text field.
final JTextField textField = new JTextField(20);
textField.addActionListener(new ActionListener() {
  public void actionPerformed(ActionEvent ae) {
    CustomGraph2D graph = getCustomGraph();
    NodeCursor nc = graph.selectedNodes();
    graph.firePreEvent();
    for (; nc.ok(); nc.next()) {
      Node n = nc.node();
      String oldText = graph.getCustomText(n);
      // Set the entered text as the node's additional data.
      graph.setCustomText(n, textField.getText());
      // Add our tailored undo/redo command to Graph2DUndoManager's command 
      // stream.
      if (oldText == null || !oldText.equals(textField.getText())) {
        getUndoSupport().push(new CustomTextChanged(graph, n, oldText));
      }
    }
    graph.firePostEvent();
    textField.selectAll();
    graph.updateViews();
  }
});
toolbar.add(textField);

We will add our command when a change to the additional data of a node has actually happened, i.e., in the actionPerformed method of the text field. In this callback we can see both old and new text for a node, and can create a CustomTextChanged object which we add to Graph2DUndoManager's command stream using the push method as already mentioned.

With the CustomTextChanged command, the undo/redo support of our application is now able to reverse changes to the additional data of the nodes in the graph. Voilà.

As a side note, our logic in the actionPerformed method additionally inserts bracketing commands into the command stream by means of the firePreEvent and firePostEvent methods provided by the Graph class. The net effect of the bracketing is that changes to the custom text of multiple nodes are treated as a single undo/redo action. In other words, although we add as many undo/redo commands as there are changes to the custom text of nodes, when we press undo, all changes are undone at once. Without the bracketing, we would have to press undo for each text change...