|
1 /* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ |
|
2 /* vim: set ft=javascript ts=2 et sw=2 tw=80: */ |
|
3 /* This Source Code Form is subject to the terms of the Mozilla Public |
|
4 * License, v. 2.0. If a copy of the MPL was not distributed with this |
|
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ |
|
6 |
|
7 /** |
|
8 * A simple undo stack manager. |
|
9 * |
|
10 * Actions are added along with the necessary code to |
|
11 * reverse the action. |
|
12 * |
|
13 * @param function aChange Called whenever the size or position |
|
14 * of the undo stack changes, to use for updating undo-related |
|
15 * UI. |
|
16 * @param integer aMaxUndo Maximum number of undo steps. |
|
17 * defaults to 50. |
|
18 */ |
|
19 function UndoStack(aMaxUndo) |
|
20 { |
|
21 this.maxUndo = aMaxUndo || 50; |
|
22 this._stack = []; |
|
23 } |
|
24 |
|
25 exports.UndoStack = UndoStack; |
|
26 |
|
27 UndoStack.prototype = { |
|
28 // Current index into the undo stack. Is positioned after the last |
|
29 // currently-applied change. |
|
30 _index: 0, |
|
31 |
|
32 // The current batch depth (see startBatch() for details) |
|
33 _batchDepth: 0, |
|
34 |
|
35 destroy: function Undo_destroy() |
|
36 { |
|
37 this.uninstallController(); |
|
38 delete this._stack; |
|
39 }, |
|
40 |
|
41 /** |
|
42 * Start a collection of related changes. Changes will be batched |
|
43 * together into one undo/redo item until endBatch() is called. |
|
44 * |
|
45 * Batches can be nested, in which case the outer batch will contain |
|
46 * all items from the inner batches. This allows larger user |
|
47 * actions made up of a collection of smaller actions to be |
|
48 * undone as a single action. |
|
49 */ |
|
50 startBatch: function Undo_startBatch() |
|
51 { |
|
52 if (this._batchDepth++ === 0) { |
|
53 this._batch = []; |
|
54 } |
|
55 }, |
|
56 |
|
57 /** |
|
58 * End a batch of related changes, performing its action and adding |
|
59 * it to the undo stack. |
|
60 */ |
|
61 endBatch: function Undo_endBatch() |
|
62 { |
|
63 if (--this._batchDepth > 0) { |
|
64 return; |
|
65 } |
|
66 |
|
67 // Cut off the end of the undo stack at the current index, |
|
68 // and the beginning to prevent a stack larger than maxUndo. |
|
69 let start = Math.max((this._index + 1) - this.maxUndo, 0); |
|
70 this._stack = this._stack.slice(start, this._index); |
|
71 |
|
72 let batch = this._batch; |
|
73 delete this._batch; |
|
74 let entry = { |
|
75 do: function() { |
|
76 for (let item of batch) { |
|
77 item.do(); |
|
78 } |
|
79 }, |
|
80 undo: function() { |
|
81 for (let i = batch.length - 1; i >= 0; i--) { |
|
82 batch[i].undo(); |
|
83 } |
|
84 } |
|
85 }; |
|
86 this._stack.push(entry); |
|
87 this._index = this._stack.length; |
|
88 entry.do(); |
|
89 this._change(); |
|
90 }, |
|
91 |
|
92 /** |
|
93 * Perform an action, adding it to the undo stack. |
|
94 * |
|
95 * @param function aDo Called to perform the action. |
|
96 * @param function aUndo Called to reverse the action. |
|
97 */ |
|
98 do: function Undo_do(aDo, aUndo) { |
|
99 this.startBatch(); |
|
100 this._batch.push({ do: aDo, undo: aUndo }); |
|
101 this.endBatch(); |
|
102 }, |
|
103 |
|
104 /* |
|
105 * Returns true if undo() will do anything. |
|
106 */ |
|
107 canUndo: function Undo_canUndo() |
|
108 { |
|
109 return this._index > 0; |
|
110 }, |
|
111 |
|
112 /** |
|
113 * Undo the top of the undo stack. |
|
114 * |
|
115 * @return true if an action was undone. |
|
116 */ |
|
117 undo: function Undo_canUndo() |
|
118 { |
|
119 if (!this.canUndo()) { |
|
120 return false; |
|
121 } |
|
122 this._stack[--this._index].undo(); |
|
123 this._change(); |
|
124 return true; |
|
125 }, |
|
126 |
|
127 /** |
|
128 * Returns true if redo() will do anything. |
|
129 */ |
|
130 canRedo: function Undo_canRedo() |
|
131 { |
|
132 return this._stack.length > this._index; |
|
133 }, |
|
134 |
|
135 /** |
|
136 * Redo the most recently undone action. |
|
137 * |
|
138 * @return true if an action was redone. |
|
139 */ |
|
140 redo: function Undo_canRedo() |
|
141 { |
|
142 if (!this.canRedo()) { |
|
143 return false; |
|
144 } |
|
145 this._stack[this._index++].do(); |
|
146 this._change(); |
|
147 return true; |
|
148 }, |
|
149 |
|
150 _change: function Undo__change() |
|
151 { |
|
152 if (this._controllerWindow) { |
|
153 this._controllerWindow.goUpdateCommand("cmd_undo"); |
|
154 this._controllerWindow.goUpdateCommand("cmd_redo"); |
|
155 } |
|
156 }, |
|
157 |
|
158 /** |
|
159 * ViewController implementation for undo/redo. |
|
160 */ |
|
161 |
|
162 /** |
|
163 * Install this object as a command controller. |
|
164 */ |
|
165 installController: function Undo_installController(aControllerWindow) |
|
166 { |
|
167 this._controllerWindow = aControllerWindow; |
|
168 aControllerWindow.controllers.appendController(this); |
|
169 }, |
|
170 |
|
171 /** |
|
172 * Uninstall this object from the command controller. |
|
173 */ |
|
174 uninstallController: function Undo_uninstallController() |
|
175 { |
|
176 if (!this._controllerWindow) { |
|
177 return; |
|
178 } |
|
179 this._controllerWindow.controllers.removeController(this); |
|
180 }, |
|
181 |
|
182 supportsCommand: function Undo_supportsCommand(aCommand) |
|
183 { |
|
184 return (aCommand == "cmd_undo" || |
|
185 aCommand == "cmd_redo"); |
|
186 }, |
|
187 |
|
188 isCommandEnabled: function Undo_isCommandEnabled(aCommand) |
|
189 { |
|
190 switch(aCommand) { |
|
191 case "cmd_undo": return this.canUndo(); |
|
192 case "cmd_redo": return this.canRedo(); |
|
193 }; |
|
194 return false; |
|
195 }, |
|
196 |
|
197 doCommand: function Undo_doCommand(aCommand) |
|
198 { |
|
199 switch(aCommand) { |
|
200 case "cmd_undo": return this.undo(); |
|
201 case "cmd_redo": return this.redo(); |
|
202 } |
|
203 }, |
|
204 |
|
205 onEvent: function Undo_onEvent(aEvent) {}, |
|
206 } |