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/LuciadFusion/com/luciad/gui/TLcdUndoManager.html[TLcdUndoManager
] and its corresponding actions demonstrates how to create a TLcdUndoManager with the undo and redo actions.
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.
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/LuciadFusion/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.
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.
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.
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()); } } }