47
|
1 module doodle.core.undo;
|
40
|
2
|
46
|
3 import std.array;
|
104
|
4 import std.datetime;
|
40
|
5
|
46
|
6 // An abstract framework for undo/redo.
|
|
7 // Assume the application works on one document at a time,
|
|
8 // therefore a single undo/redo history.
|
|
9 // Each change to the document is modelled as an Edit.
|
|
10 // Previous edits are represented by an undo stack
|
|
11 // and can be undone in order.
|
|
12 // As edits are undone the are placed on a redo stack
|
|
13 // and can be redone in order.
|
|
14 // As edits are redone the are placed on the undo stack, etc.
|
|
15 //
|
|
16 // When a new edit is made an attempt to merge it with
|
|
17 // the previous Edit is made. For example, typing characters
|
|
18 // in short succession generates many Edits that may be
|
|
19 // merged into one Edit.
|
|
20 // When a new edit is made the redo stack is cleared.
|
|
21 //
|
|
22 // Application code must generate Edits from user interaction.
|
|
23 // Typically application code will receive some input event and:
|
|
24 // * Attempt to perform an action,
|
|
25 // * If the action succeeds then encapsulate the action
|
|
26 // in an Edit and add it to the undo manager.
|
|
27 // Note, not all interaction results in Edits, for example,
|
|
28 // changing the view, zooming/scrolling, etc are not edits
|
|
29 // and do not affect undo/redo
|
40
|
30
|
46
|
31 abstract class Edit {
|
|
32 private {
|
|
33 string _description;
|
104
|
34 SysTime _timeStamp;
|
46
|
35 }
|
|
36
|
49
|
37 this(in string description) {
|
|
38 _description = description;
|
132
|
39 _timeStamp = Clock.currTime();
|
49
|
40 }
|
|
41
|
104
|
42 this(in string description, SysTime timeStamp) {
|
46
|
43 assert(description);
|
|
44 _description = description;
|
|
45 _timeStamp = timeStamp;
|
|
46 }
|
40
|
47
|
46
|
48 string description() const { return _description; }
|
104
|
49 const(SysTime) timeStamp() const { return _timeStamp; }
|
40
|
50
|
46
|
51 final bool merge(Edit subsequent) {
|
|
52 if (mergeImpl(subsequent)) {
|
|
53 // Adopt the new timestamp and description
|
|
54 _timeStamp = subsequent._timeStamp;
|
|
55 _description = subsequent._description;
|
|
56 return true;
|
|
57 }
|
|
58 else {
|
|
59 return false;
|
|
60 }
|
|
61 }
|
|
62
|
40
|
63 void undo();
|
|
64 void redo();
|
49
|
65 protected bool mergeImpl(Edit subsequent) { return false; }
|
46
|
66 }
|
|
67
|
|
68 interface IUndoManagerObserver {
|
|
69 // Each description is null if the associated bool is false
|
|
70 void undoRedoUpdate(in bool canUndo, in string undoDescription,
|
|
71 in bool canRedo, in string redoDescription);
|
40
|
72 }
|
|
73
|
49
|
74 // XXX This interface doesn't appear to add any value
|
46
|
75 interface IUndoManager {
|
|
76 void addEdit(Edit edit);
|
|
77 void undo();
|
|
78 void redo();
|
|
79 void reset();
|
|
80
|
|
81 void addObserver(IUndoManagerObserver observer);
|
|
82 void removeObserver(IUndoManagerObserver observer);
|
|
83 }
|
|
84
|
|
85 class UndoManager : IUndoManager {
|
|
86 this(int maxUndoLevel = -1) {
|
|
87 _maxUndoLevel = maxUndoLevel;
|
|
88 }
|
|
89
|
61
|
90 ~this() {
|
|
91 assert(_observers.length == 0);
|
|
92 }
|
|
93
|
46
|
94 void addEdit(Edit edit) {
|
|
95 _redoEdits.length = 0;
|
|
96
|
|
97 if (_undoEdits.empty || !_undoEdits.back.merge(edit)) {
|
|
98 _undoEdits ~= edit;
|
|
99 if (_maxUndoLevel >= 0 && _undoEdits.length > _maxUndoLevel) {
|
|
100 _undoEdits.length = _undoEdits.length - 1;
|
|
101 }
|
|
102 }
|
|
103
|
|
104 notifyObservers();
|
40
|
105 }
|
|
106
|
|
107 void undo() {
|
132
|
108 assert(canUndo());
|
46
|
109 auto edit = _undoEdits.back;
|
132
|
110 edit.undo();
|
|
111 _undoEdits.popBack();
|
46
|
112 _redoEdits ~= edit;
|
|
113
|
|
114 notifyObservers();
|
40
|
115 }
|
|
116
|
|
117 void redo() {
|
132
|
118 assert(canRedo());
|
46
|
119 auto edit = _redoEdits.back;
|
132
|
120 edit.redo();
|
|
121 _redoEdits.popBack();
|
46
|
122 _undoEdits ~= edit;
|
|
123
|
|
124 notifyObservers();
|
40
|
125 }
|
|
126
|
46
|
127 bool canUndo() const { return !_undoEdits.empty; }
|
|
128 bool canRedo() const { return !_redoEdits.empty; }
|
40
|
129
|
46
|
130 void reset() {
|
|
131 _undoEdits.length = _redoEdits.length = 0;
|
|
132 notifyObservers();
|
40
|
133 }
|
|
134
|
46
|
135 void addObserver(IUndoManagerObserver observer) {
|
|
136 _observers ~= observer;
|
|
137 }
|
|
138
|
|
139 void removeObserver(IUndoManagerObserver observer) {
|
|
140 // NYI
|
|
141 }
|
40
|
142
|
|
143 // IUndoManager overrides:
|
|
144
|
|
145 private {
|
46
|
146 int _maxUndoLevel;
|
|
147 Edit[] _undoEdits;
|
|
148 Edit[] _redoEdits;
|
|
149 IUndoManagerObserver[] _observers; // FIXME, use a different container
|
|
150
|
|
151 void notifyObservers() {
|
|
152 foreach (o; _observers) {
|
132
|
153 o.undoRedoUpdate(canUndo(), canUndo() ? _undoEdits.back.description() : null,
|
|
154 canRedo(), canRedo() ? _redoEdits.back.description() : null);
|
46
|
155 }
|
|
156 }
|
40
|
157 }
|
|
158 }
|