Core Architecture: Major System: Undo & Redo - UA-ScriptEase/scriptease GitHub Wiki

Let's do the Time Warp again!

-- Transylvanians

Overview

The Undo / Redo system is based on the usage of aspects and pointcuts to record undoable actions and their modifications to the model. These modifications are then stored on a stack associated to the appropriate StoryModel xor the Library depending on the focus on the program when the changes are made. The system was designed with the potential for limitless undo-ability, but we've capped it to avoid runaway memory use.

UndoManager

The functionality of the system is handled by the UndoManager and calls to startUndoableAction(String name) and then endUndoableAction(). This signifies that the system should record any changes to the model (via appendModification(Modification newChange)) so that they may be undone. The system then notifies it's observers that the stack has changed, and updated the UndoAction/RedoAction to be enabled or disabled and with the correct action name.

The trick then becomes wrapping undoable actions properly to encompass the full extent of the changes made to the model. It is key that the Undo.aj catch all changes to the model if you wish each action to be fully undoable. If Undo.aj is not up to date with the model, undefined behavior may occur as a result of undoing and redoing.

A very tricky example of undoing/redoing is changes made to the model through Drag and Drop / TransferHandlers. These actions call a couple methods (exportDone, import, ...) in different orders depending on the action being preformed (copy, cut, drag, ...). This made it excruciatingly difficult to define when to start an undoable action and when to end it to properly record all changes.

How to Add Undoability:

  1. First of all, note that undoability is model side.
  2. Now, we go to Undo.aj and look for a relevant model modification. If it exists, we can continue to #5.
  3. We didn't find a relevant one, so we will have to make our own. First create a new pointcut at the top in the same format as the other ones but relevant to the function you wish to undo. This will be in the following form:
public pointcut undoFunctionName(): 
      within(TheModelClass+) && execution(* theMethodInTheClass(TheParameterTypes));
  1. Now we need some sort of functionality. Add an Advice in the same format as the others to the bottom of the Undo.aj Aspect. Fill in the appropriate undo and redo methods.
  2. At this point, if we did everything correctly, we should be able to undo changes to the model. But we need to tell the UndoManager to record the changes. We need to go the place where a model changing function is called and add the following code around it:
if (!UndoManager.getInstance().hasOpenUndoableAction()) 
 UndoManager.getInstance().startUndoableAction("Text to display in the status bar");
<<Undoable Model Changing Methods Go Here>>
UndoManager.getInstance().endUndoableAction();

That's it! Debug it like mad and then party like it's 1999.

Sidenote on FieldModifications

If you are undoing and redoing a field, you can easily create a new Advice using the FieldModification class. The content of the Advice will be as follows:

Modification mod = new FieldModification<TypeOfTheValue>(newValue, oldValue) {
 @Override
 public void setOp(TypeOfTheValue value) {
 owner.setValue(value);
 }