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
|
|
37 this(in string description, d_time timeStamp) {
|
|
38 assert(description);
|
|
39 _description = description;
|
|
40 _timeStamp = timeStamp;
|
|
41 }
|
40
|
42
|
46
|
43 string description() const { return _description; }
|
|
44 d_time timeStamp() const { return _timeStamp; }
|
40
|
45
|
46
|
46 final bool merge(Edit subsequent) {
|
|
47 if (mergeImpl(subsequent)) {
|
|
48 // Adopt the new timestamp and description
|
|
49 _timeStamp = subsequent._timeStamp;
|
|
50 _description = subsequent._description;
|
|
51 return true;
|
|
52 }
|
|
53 else {
|
|
54 return false;
|
|
55 }
|
|
56 }
|
|
57
|
40
|
58 void undo();
|
|
59 void redo();
|
46
|
60 protected bool mergeImpl(Edit other) { return false; }
|
|
61 }
|
|
62
|
|
63 interface IUndoManagerObserver {
|
|
64 // Each description is null if the associated bool is false
|
|
65 void undoRedoUpdate(in bool canUndo, in string undoDescription,
|
|
66 in bool canRedo, in string redoDescription);
|
40
|
67 }
|
|
68
|
46
|
69 interface IUndoManager {
|
|
70 void addEdit(Edit edit);
|
|
71 void undo();
|
|
72 void redo();
|
|
73 void reset();
|
|
74
|
|
75 void addObserver(IUndoManagerObserver observer);
|
|
76 void removeObserver(IUndoManagerObserver observer);
|
|
77 }
|
|
78
|
|
79 class UndoManager : IUndoManager {
|
|
80 this(int maxUndoLevel = -1) {
|
|
81 _maxUndoLevel = maxUndoLevel;
|
|
82 }
|
|
83
|
|
84 void addEdit(Edit edit) {
|
|
85 _redoEdits.length = 0;
|
|
86
|
|
87 if (_undoEdits.empty || !_undoEdits.back.merge(edit)) {
|
|
88 _undoEdits ~= edit;
|
|
89 if (_maxUndoLevel >= 0 && _undoEdits.length > _maxUndoLevel) {
|
|
90 _undoEdits.length = _undoEdits.length - 1;
|
|
91 }
|
|
92 }
|
|
93
|
|
94 notifyObservers();
|
40
|
95 }
|
|
96
|
|
97 void undo() {
|
46
|
98 assert(canUndo);
|
|
99 auto edit = _undoEdits.back;
|
|
100 edit.undo;
|
|
101 _undoEdits.popBack;
|
|
102 _redoEdits ~= edit;
|
|
103
|
|
104 notifyObservers();
|
40
|
105 }
|
|
106
|
|
107 void redo() {
|
46
|
108 assert(canRedo);
|
|
109 auto edit = _redoEdits.back;
|
|
110 edit.redo;
|
|
111 _redoEdits.popBack;
|
|
112 _undoEdits ~= edit;
|
|
113
|
|
114 notifyObservers();
|
40
|
115 }
|
|
116
|
46
|
117 bool canUndo() const { return !_undoEdits.empty; }
|
|
118 bool canRedo() const { return !_redoEdits.empty; }
|
40
|
119
|
46
|
120 void reset() {
|
|
121 _undoEdits.length = _redoEdits.length = 0;
|
|
122 notifyObservers();
|
40
|
123 }
|
|
124
|
46
|
125 void addObserver(IUndoManagerObserver observer) {
|
|
126 _observers ~= observer;
|
|
127 }
|
|
128
|
|
129 void removeObserver(IUndoManagerObserver observer) {
|
|
130 // NYI
|
|
131 }
|
40
|
132
|
|
133 // IUndoManager overrides:
|
|
134
|
|
135 private {
|
46
|
136 int _maxUndoLevel;
|
|
137 Edit[] _undoEdits;
|
|
138 Edit[] _redoEdits;
|
|
139 IUndoManagerObserver[] _observers; // FIXME, use a different container
|
|
140
|
|
141 void notifyObservers() {
|
|
142 foreach (o; _observers) {
|
|
143 o.undoRedoUpdate(canUndo, canUndo ? _undoEdits.back.description : null,
|
|
144 canRedo, canRedo ? _redoEdits.back.description : null);
|
|
145 }
|
|
146 }
|
40
|
147 }
|
|
148 }
|