Undo Design

From ParaQ Wiki
Jump to navigationJump to search

To encourage experimentation and improve the overall user experience, ParaQ will include a complete undo/redo system, subject to the following requirements:

Use cases

  • A user makes changes to individual properties in a property inspector, in "immediate-update mode". Each property-modification shows-up immediately as a separate step in the undo/redo UI.
  • A user makes changes to individual properties in a property inspector, in "explicit-update mode". When the user hits the "update" button, a single step appears in the undo/redo UI, encapsulating all changes.
  • A user interacts with a continuously-varying UI component such as a spinbox or a slider, in "immediate-update mode". Although the underlying property goes through many state changes, only a single step appears in the undo/redo UI.
  • User A wants changes in camera position/orientation/frustum to be excluded from undo/redo recording.
  • User B wants changes in camera position/orientation/frustum to be included in undo/redo recording.
  • User C wants changes in camera position/orientation/frustum to be included in undo/redo recording, but in a different "stack" with separate UI.
  • A user is running a UI session and has made changes. They run a script in the script client against the same session. The script may have made many state changes to the current session, but the user should be able to undo the effects of the script in one step.
  • (Optional) A user may expect certain client-specific operations (such as resizing a splitter window) to be undo-able.

Requirements

  • Undo/redo recording should be implemented at the server. Rationale: different clients should not have to each implement undo/redo. Even the scripting-client needs undo/redo, since it can be used in conjunction with the UI client.
  • There should not be any arbitrary limitation on the number of state changes that can be undone/redone.
  • Individual state changes can be "grouped" into a change-set. The change-set is what is visible in the UI as an undoable/redoable "step". The UI/client layer is responsible for designating the "start" and "end" of a change-set.
  • Undo/redo recording can be arbitrarily turned on-or-off. The UI/client layer is responsible for turning recording on/off before making changes to the server state.
  • Once recording is turned off, the resultant change-set can be stored in one-to-many undo/redo stacks.
  • The UI/client layer controls the creation/deletion of undo/redo stacks, based on user preference. There should not be any arbitrary limitation on the number of stacks available.

Interaction with Updates

See also Screen Updates

Ui update undo redo interaction.png

API

The undo/redo API is exposed via a set of classes and functions that are part of the server manager API (class declarations are abbreviated for clarity).

vtkStateContainer is an abstract interface to an object that stores a state change, and can restore it on-demand:

class vtkStateContainer
{
public:
  virtual void RestoreState() = 0;
};

The server implementation defines as many concrete implementations of vtkStateContainer as-required to handle recording all undoable state changes (node creation and deletion, property changes, domain changes, etc).

vtkStateChangeSet is a concrete class that stores a collection of vtkStateContainer instances, separated into "undo" and "redo" collections:

class vtkStateChangeSet
{
public:
  ~vtkStateChangeSet();

  void StoreUndoState(vtkStateContainer* State);
  void StoreRedoState(vtkStateContainer* State);

  void Undo();
  void Redo();
};

vtkStateChangeSet acts as a "grouping" mechanism, allowing one-to-many state changes to be undone or redone by the UI layer in a single step. Calling vtkStateChangeSet::Redo() calls the RestoreState() method on all of its redo objects, in the order that they were added to the change set. Calling vtkStateChangeSet::Undo() calls the RestoreState() method on all of its undo objects, in reverse order. vtkStateChangeSet takes responsibility for the lifetimes of the objects assigned to it. Note that a typical server state change will generate two vtkStateContainer instances, one to store the "old" state, and one to store the "new" state.

vtkUndoStack provides storage and human-readable labels for a collection of vtkStateChangeSet objects:

class vtkUndoStack
{
public:
  ~vtkUndoStack();

  void StoreChangeSet(char* Label, vtkStateChangeSet* ChangeSet);

  char* GetNextUndo();
  char* GetNextRedo();

  void Undo();
  void Redo();
};

Internally, vtkUndoStack maintains a "position" that is updated as the user performs undo or redo operations. Based on this position, GetNextUndo() and GetNextRedo() return the human-readable labels of the change sets that will be undone or redone by the next calls to Undo() or Redo() respectively. GetNextUndo() and GetNextRedo() return NULL when no change set is available to perform an undo or redo. The Undo() and Redo() methods will call the corresponding methods on the appropriate change set, and update the internal stack position. Change sets are added to the stack using StoreChangeSet(), and the stack takes responsibility for the lifetime of the added change set. If the stack position is anywhere other than at the "end" of the stack, adding a change set deletes all of the change sets that have been undone. vtkUndoStack should provide a callback mechanism to notify clients whenever the contents of the stack change.

Optional: instead of vtkUndoStack, hierarchical storage makes it possible to store a "tree" of state changes, allowing users to move back-and-forth among multiple branches of modifications.

The server maintains a collection of one-to-many undo stacks, with appropriate functions to allow clients to add, delete, and name stacks, and callbacks to notify clients whenever the set of available stacks changes. The first, or "primary" stack is created automatically and cannot be deleted.

Undo/redo recording is explicitly disabled, until enabled by the client. The server provides methods to start and stop undo/redo recording:

void vtkStartUndoRecording();
void vtkStopUndoRecording(char* Label);
void vtkStopUndoRecording(char* Label, vtkUndoStack* Stack);

vtkStartUndoRecording() begins the undo recording process. Internally, the server creates an instance of vtkStateChangeSet and makes it "current". All subsequent server state changes are encapsulated in vtkStateContainer derivates and stored in the change set.

vtkStopUndoRecording() ends the undo recording process. The first form of the method assigns the current change set to the primary undo stack. The second form is used to assign the change set to an arbitrary undo stack. In either case, the server resets the "current" change set, so that subsequent server state changes are not recorded, until vtkStartUndoRecording() is called again.

Undo-enabled clients bracket each server state change with calls to vtkStartUndoRecording() and vtkStopUndoRecording(). In the case of continuously-changing, interactive modifications (e.g. properties controlled by a slider, camera zoom & pan, etc), the user interface should be aware of the start and end of the set of changes, and only call vtkStartUndoRecording()/vtkStopUndoRecording() once, bracketing the entire set of changes.

Client-side Undo

As described above, undo stacks, changesets, and state containers are all server-side objects that are not directly manipulated by clients. It may be necessary to provide a mechanism for clients to insert their own state changes into a changeset while undo recording is in progress. When played-back, these state changes are ignored by the server, but made available to the client(s) to restore the appropriate state. Because there may be multiple connected clients of differing types (e.g. one Qt client and one Python client), state changes particular to one type of client will be ignored by others. Accordingly, the server will provide methods to record client-side undo/redo data:

void vtkStoreClientUndoState(char* Client, char* Data);
void vtkStoreClientRedoState(char* Client, char* Data);

When undoable changes occur, the client serializes its old and new states as text, and calls the corresponding methods. The "Client" argument to each method will be an identified (details TBD) which uniquely identifies the type of client and process. If recording is enabled (i.e. if there is a "current" changeset), the data is stored in vtkStateContainer-derived classes as normal. If recording is diabled, these functions are a no-op.

To restore client-side state, the server calls a callback function which includes both the "Client" and "Data" strings that were recorded earlier. Clients which are connected to the callback examine the "Client" string to filter-out data intended for other clients, then deserialize the data and restore their state as-appropriate.