comparison doodle/core/undo.d @ 47:14f1c051c35b

Moved undo into core.
author daveb
date Tue, 03 Aug 2010 16:57:06 +0930
parents doodle/undo/undo.d@ad3ba55ae57b
children 576b9fba4677
comparison
equal deleted inserted replaced
46:ad3ba55ae57b 47:14f1c051c35b
1 module doodle.core.undo;
2
3 import std.array;
4 import std.date;
5
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
30
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 }
42
43 string description() const { return _description; }
44 d_time timeStamp() const { return _timeStamp; }
45
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
58 void undo();
59 void redo();
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);
67 }
68
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();
95 }
96
97 void undo() {
98 assert(canUndo);
99 auto edit = _undoEdits.back;
100 edit.undo;
101 _undoEdits.popBack;
102 _redoEdits ~= edit;
103
104 notifyObservers();
105 }
106
107 void redo() {
108 assert(canRedo);
109 auto edit = _redoEdits.back;
110 edit.redo;
111 _redoEdits.popBack;
112 _undoEdits ~= edit;
113
114 notifyObservers();
115 }
116
117 bool canUndo() const { return !_undoEdits.empty; }
118 bool canRedo() const { return !_redoEdits.empty; }
119
120 void reset() {
121 _undoEdits.length = _redoEdits.length = 0;
122 notifyObservers();
123 }
124
125 void addObserver(IUndoManagerObserver observer) {
126 _observers ~= observer;
127 }
128
129 void removeObserver(IUndoManagerObserver observer) {
130 // NYI
131 }
132
133 // IUndoManager overrides:
134
135 private {
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 }
147 }
148 }