47
|
1 module doodle.core.undo;
|
40
|
2
|
46
|
3 import std.array;
|
|
4 import std.date;
|
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;
|
|
34 d_time _timeStamp;
|
|
35 }
|
|
36
|
49
|
37 this(in string description) {
|
|
38 _description = description;
|
|
39 _timeStamp = getUTCtime;
|
|
40 }
|
|
41
|
46
|
42 this(in string description, d_time timeStamp) {
|
|
43 assert(description);
|
|
44 _description = description;
|
|
45 _timeStamp = timeStamp;
|
|
46 }
|
40
|
47
|
46
|
48 string description() const { return _description; }
|
|
49 d_time 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
|
|
90 void addEdit(Edit edit) {
|
|
91 _redoEdits.length = 0;
|
|
92
|
|
93 if (_undoEdits.empty || !_undoEdits.back.merge(edit)) {
|
|
94 _undoEdits ~= edit;
|
|
95 if (_maxUndoLevel >= 0 && _undoEdits.length > _maxUndoLevel) {
|
|
96 _undoEdits.length = _undoEdits.length - 1;
|
|
97 }
|
|
98 }
|
|
99
|
|
100 notifyObservers();
|
40
|
101 }
|
|
102
|
|
103 void undo() {
|
46
|
104 assert(canUndo);
|
|
105 auto edit = _undoEdits.back;
|
|
106 edit.undo;
|
|
107 _undoEdits.popBack;
|
|
108 _redoEdits ~= edit;
|
|
109
|
|
110 notifyObservers();
|
40
|
111 }
|
|
112
|
|
113 void redo() {
|
46
|
114 assert(canRedo);
|
|
115 auto edit = _redoEdits.back;
|
|
116 edit.redo;
|
|
117 _redoEdits.popBack;
|
|
118 _undoEdits ~= edit;
|
|
119
|
|
120 notifyObservers();
|
40
|
121 }
|
|
122
|
46
|
123 bool canUndo() const { return !_undoEdits.empty; }
|
|
124 bool canRedo() const { return !_redoEdits.empty; }
|
40
|
125
|
46
|
126 void reset() {
|
|
127 _undoEdits.length = _redoEdits.length = 0;
|
|
128 notifyObservers();
|
40
|
129 }
|
|
130
|
46
|
131 void addObserver(IUndoManagerObserver observer) {
|
|
132 _observers ~= observer;
|
|
133 }
|
|
134
|
|
135 void removeObserver(IUndoManagerObserver observer) {
|
|
136 // NYI
|
|
137 }
|
40
|
138
|
|
139 // IUndoManager overrides:
|
|
140
|
|
141 private {
|
46
|
142 int _maxUndoLevel;
|
|
143 Edit[] _undoEdits;
|
|
144 Edit[] _redoEdits;
|
|
145 IUndoManagerObserver[] _observers; // FIXME, use a different container
|
|
146
|
|
147 void notifyObservers() {
|
|
148 foreach (o; _observers) {
|
|
149 o.undoRedoUpdate(canUndo, canUndo ? _undoEdits.back.description : null,
|
|
150 canRedo, canRedo ? _redoEdits.back.description : null);
|
|
151 }
|
|
152 }
|
40
|
153 }
|
|
154 }
|