changeset 46:ad3ba55ae57b

Undo framework mostly complete
author daveb
date Tue, 03 Aug 2010 16:55:45 +0930
parents 01bbf3f6f966
children 14f1c051c35b
files doodle/undo/undo.d
diffstat 1 files changed, 121 insertions(+), 39 deletions(-) [+]
line wrap: on
line diff
--- a/doodle/undo/undo.d	Mon Aug 02 14:44:12 2010 +0930
+++ b/doodle/undo/undo.d	Tue Aug 03 16:55:45 2010 +0930
@@ -1,66 +1,148 @@
 module doodle.undo.undo;
 
-// http://doc.trolltech.com/4.2/qundo.html
-// http://www.codeproject.com/KB/cs/undo_support.aspx
+import std.array;
+import std.date;
 
-// rename Action to Edit?
+// An abstract framework for undo/redo.
+// Assume the application works on one document at a time,
+// therefore a single undo/redo history.
+// Each change to the document is modelled as an Edit.
+// Previous edits are represented by an undo stack
+// and can be undone in order.
+// As edits are undone the are placed on a redo stack
+// and can be redone in order.
+// As edits are redone the are placed on the undo stack, etc.
+//
+// When a new edit is made an attempt to merge it with
+// the previous Edit is made. For example, typing characters
+// in short succession generates many Edits that may be
+// merged into one Edit.
+// When a new edit is made the redo stack is cleared.
+//
+// Application code must generate Edits from user interaction.
+// Typically application code will receive some input event and:
+// * Attempt to perform an action,
+// * If the action succeeds then encapsulate the action
+//   in an Edit and add it to the undo manager.
+// Note, not all interaction results in Edits, for example,
+// changing the view, zooming/scrolling, etc are not edits
+// and do not affect undo/redo
 
-// Related design patterns: Command, Memento
+abstract class Edit {
+    private {
+        string _description;
+        d_time _timeStamp;
+    }
+
+    this(in string description, d_time timeStamp) {
+        assert(description);
+        _description = description;
+        _timeStamp = timeStamp;
+    }
 
-// Action represents and encapsulates the information needed 
-// Consider command merging and time-stamping of commands
+    string description() const { return _description; }
+    d_time timeStamp() const { return _timeStamp; }
 
-abstract class Action {
+    final bool merge(Edit subsequent) {
+        if (mergeImpl(subsequent)) {
+            // Adopt the new timestamp and description
+            _timeStamp = subsequent._timeStamp;
+            _description = subsequent._description;
+            return true;
+        }
+        else {
+            return false;
+        }
+    }
+
     void undo();
     void redo();
-    //string description() const;
-    // bool merge(Action other);
-    // time-stamp
+    protected bool mergeImpl(Edit other) { return false; }
+}
+
+interface IUndoManagerObserver {
+    // Each description is null if the associated bool is false
+    void undoRedoUpdate(in bool canUndo, in string undoDescription,
+                        in bool canRedo, in string redoDescription);
 }
 
-final class CompoundAction {
-    this(in Action[] sub_actions) {
-        mSubActions = sub_actions.dup;
+interface IUndoManager {
+    void addEdit(Edit edit);
+    void undo();
+    void redo();
+    void reset();
+
+    void addObserver(IUndoManagerObserver observer);
+    void removeObserver(IUndoManagerObserver observer);
+}
+
+class UndoManager : IUndoManager {
+    this(int maxUndoLevel = -1) {
+        _maxUndoLevel = maxUndoLevel;
+    }
+
+    void addEdit(Edit edit) {
+        _redoEdits.length = 0;
+
+        if (_undoEdits.empty || !_undoEdits.back.merge(edit)) {
+            _undoEdits ~= edit;
+            if (_maxUndoLevel >= 0 && _undoEdits.length > _maxUndoLevel) {
+                _undoEdits.length = _undoEdits.length - 1;
+            }
+        }
+
+        notifyObservers();
     }
 
     void undo() {
-        foreach_reverse(a; mSubActions) { a.undo(); }
+        assert(canUndo);
+        auto edit = _undoEdits.back;
+        edit.undo;
+        _undoEdits.popBack;
+        _redoEdits ~= edit;
+
+        notifyObservers();
     }
 
     void redo() {
-        foreach(a; mSubActions) { a.redo(); }
+        assert(canRedo);
+        auto edit = _redoEdits.back;
+        edit.redo;
+        _redoEdits.popBack;
+        _undoEdits ~= edit;
+
+        notifyObservers();
     }
 
-    private {
-        Action[] mSubActions;
-    }
-}
-
-interface IUndoManagerObserver {
-    void canUndo(in bool value, in string description);
-    void canRedo(in bool value, in string description);
-}
+    bool canUndo() const { return !_undoEdits.empty; }
+    bool canRedo() const { return !_redoEdits.empty; }
 
-interface IUndoManager {
-    void addAction(Action action);
-    void undo();
-    void redo();
-    // bool can_undo() const;
-    // bool can_redo() const;
-}
-
-class UndoManager : IUndoManager {
-    this(int max_undo_level = -1) {
+    void reset() {
+        _undoEdits.length = _redoEdits.length = 0;
+        notifyObservers();
     }
 
-    void addAction(Action action);
-    void undo();
-    void redo();
+    void addObserver(IUndoManagerObserver observer) {
+        _observers ~= observer;
+    }
+
+    void removeObserver(IUndoManagerObserver observer) {
+        // NYI
+    }
 
     // IUndoManager overrides:
 
     private {
-        Action[] mUndoActions;
-        Action[] mRedoActions;
+        int _maxUndoLevel;
+        Edit[] _undoEdits;
+        Edit[] _redoEdits;
+        IUndoManagerObserver[] _observers;          // FIXME, use a different container
+
+        void notifyObservers() {
+            foreach (o; _observers) {
+                o.undoRedoUpdate(canUndo, canUndo ? _undoEdits.back.description : null,
+                                 canRedo, canRedo ? _redoEdits.back.description : null);
+            }
+        }
     }
 }