The ILcdUndoable interface provides support to reverse the effect of an action. A class that wants it effects to be reversible should provide methods to register an ILcdUndoableListener. Whenever an ILcdUndoable object is created by the class, the listener gets notified of this object and can react to it in an appropriate way. One implementation of ILcdUndoableListener is TLcdUndoManager. This class collects all ILcdUndoable objects of which it is notified and provides methods to undo and redo these undoable objects in the correct order. The following sections describe the steps for adding undo support to your application in more detail. For more information on listening to changes, refer to Notifying objects of changes with listeners.

Adding undo/redo capabilities to your application

To add undo/redo support to your application, you should first create a TLcdUndoManager. You can then add this TLcdUndoManager as a listener to the appropriate class(es). The TLcdUndoAction and TLcdRedoAction can then be used to interact with the TLcdUndoManager. Program: Creating a link:../../../reference/LuciadLightspeed/com/luciad/gui/TLcdUndoManager.html[TLcdUndoManager] and its corresponding actions demonstrates how to create a TLcdUndoManager with the undo and redo actions.

Program: Creating a TLcdUndoManager and its corresponding actions (from samples/gxy/undo/MainPanel)
// create the undo manager
TLcdUndoManager undoManager = new TLcdUndoManager(10);

// Set up the actions that interact with the undo manager
TLcdUndoAction undoAction = new TLcdUndoAction(undoManager);
TLcdRedoAction redoAction = new TLcdRedoAction(undoManager);

// insert these actions in the toolbar
getToolBars()[0].addAction(undoAction);
getToolBars()[0].addAction(redoAction);

Program: Making the changes made by the edit controller undoable shows how the TLcdUndoManager is added as an ILcdUndoableListener to the TLcdGXYEditController2. The edit controller creates ILcdUndoable objects when it edits the domain objects, and notifies the TLcdUndoManager of these objects. The TLcdUndoManager then places this ILcdUndoable in its queue so that it can be undone and redone when the TLcdUndoAction and TLcdRedoAction are performed.

Program: Making the changes made by the edit controller undoable (from samples/gxy/undo/MainPanel)
// add the TLcdUndoManager as an ILcdUndoableListener to the edit controller.
aEditController.addUndoableListener(aUndoManager);

Making graphical edits undoable

As demonstrated above, the TLcdGXYEditControllerModel2 provides undo/redo support for editing the domain objects. However, because the TLcdGXYEditController2 delegates the actual modification of the domain objects to ILcdGXYEditor instances, the creation of the ILcdUndoable objects must be delegated to them as well. In order to let the TLcdGXYEditController2 capture these ILcdUndoable objects, it needs to attach itself as a listener to the ILcdGXYEditor. That is why the ILcdGXYEditor implementation needs to implement ILcdUndoableSource as well if the changes made by the ILcdGXYEditor need to be reversible. This interface allows ILcdUndoableListener objects to be attached.

The undo sample in the LuciadLightspeed distribution has an implementation of an ILcdGXYEditor that demonstrates these concepts. Describing the undo sample analyzes this sample in more detail.

Describing the undo sample

The LuciadLightspeed distribution contains the undo sample which demonstrates the undo/redo support. It initializes the undo/redo support as seen in Program: Creating a link:../../../reference/LuciadLightspeed/com/luciad/gui/TLcdUndoManager.html[TLcdUndoManager] and its corresponding actions. The major part of the sample is however dedicated to the ILcdGXYEditor and ILcdUndoable implementation and the domain objects which it edits.

Program: Saving the state of the objects before and after the change shows the relevant portion of the editor. The editor saves the state of the object it is about to change right before the actual change and right after that. These states are saved in the undoable and the listeners are notified of this undoable.

Program: Saving the state of the objects before and after the change (from samples/gxy/undo/UndoableEditor)
@Override
public boolean edit(Graphics aGraphics, int aMode, ILcdGXYContext aContext) {
  Object editedObject = getObject();
  ModelElementEditedUndoable undoable = createUndoable(aContext, editedObject);
  boolean objectChanged = fDelegateEditor.edit(aGraphics, aMode, aContext);
  if (undoable != null)
    if (objectChanged) {
      undoable.finishedEditingAndFire(fUndoSupport::fireUndoableHappened);
    } else {
      undoable.die();
  }
  return objectChanged;
}

private ModelElementEditedUndoable createUndoable(ILcdGXYContext aContext, Object editedObject) {
  if (editedObject instanceof StateAware) {
    StateAware stateAware = (StateAware) editedObject;
    try {
      return new ModelElementEditedUndoable(
              generateDisplayName(stateAware),
              stateAware,
              aContext.getGXYLayer(),
              false // no events need to be fired, the edit controller takes care to properly wrap the undoables.
          );
    } catch (StateException ignored) {
    }
  }
  return null;
}

The ILcdUndoable implementation is based on a variation of the memento pattern. The object that is about to be edited is asked to store its state right before and right after the change. Actually reverting the change and redoing it then only consist of telling the domain object to restore itself to the appropriate state, as demonstrated in Program: Undoing and redoing actions consist of simply restoring the correct state.

Program: Undoing and redoing actions consist of simply restoring the correct state (from samples/common/undo/ModelElementEditedUndoable)
@Override
protected final void undoImpl() throws TLcdCannotUndoRedoException {
  restoreState(fBeforeMap);
}

@Override
protected final void redoImpl() throws TLcdCannotUndoRedoException {
  restoreState(fAfterMap);
}

private void restoreState(Map aState) {
  StateAware editedObject = getEditedObject();
  Object domainObject = getModelObject();
  ILcdModel model = getModel();
  try {
    if (!fFireEvents) {
      editedObject.restoreState(aState, model);
    } else {
      ILcdLayer layer = getLayer();
      try (Lock autoUnlock = writeLock(model)) {
        editedObject.restoreState(aState, model);
        model.elementChanged(domainObject, ILcdFireEventMode.FIRE_LATER);
      } finally {
        model.fireCollectedModelChanges();
      }
      if (layer != null) {
        if (!layer.isVisible()) {
          layer.setVisible(true);
        }
        layer.selectObject(domainObject, true, ILcdFireEventMode.FIRE_NOW);
      }
    }
  } catch (StateException e) {
    LOGGER.error(e.getMessage(), e);
    throw new TLcdCannotUndoRedoException(e.getMessage(), e);
  }
}

How the state of the domain object is stored and restored depends on the object itself. In the case of this sample, the object is a Polyline which stores its state by storing the location of each of its points. It restores its state by moving each of its points to the stored location as shown in Program: Storing and restoring the state of a polyline comes down to storing and restoring the location of its points.

Program: Storing and restoring the state of a polyline comes down to storing and restoring the location of its points (from samples/gxy/undo/Polyline)
@Override
public void storeState(Map aMap, ILcdModel aSourceModel) {
  //store the location of the points.
  int count = getPointCount();
  ILcdPoint[] points = new ILcdPoint[count];
  for (int i = 0; i < count; i++) {
    // create separate clones of the point, to make this stored state
    // independent of the actual state of the polyline.
    points[i] = (ILcdPoint) getPoint(i).clone();
  }
  aMap.put(POINTS_KEY, points);
}

@Override
public void restoreState(Map aMap, ILcdModel aTargetModel) {
  ILcdPoint[] points = (ILcdPoint[]) aMap.get(POINTS_KEY);
  if (points != null) {
    equalizeNumberOfPoints(points);
    for (int i = 0; i < points.length; i++) {
      move2DPoint(i, points[i].getX(), points[i].getY());
    }
  }
}