diff options
Diffstat (limited to 'devtools/client/shared/undo.js')
-rw-r--r-- | devtools/client/shared/undo.js | 192 |
1 files changed, 192 insertions, 0 deletions
diff --git a/devtools/client/shared/undo.js b/devtools/client/shared/undo.js new file mode 100644 index 000000000..65791f50d --- /dev/null +++ b/devtools/client/shared/undo.js @@ -0,0 +1,192 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/** + * A simple undo stack manager. + * + * Actions are added along with the necessary code to + * reverse the action. + * + * @param integer maxUndo Maximum number of undo steps. + * defaults to 50. + */ +function UndoStack(maxUndo) { + this.maxUndo = maxUndo || 50; + this._stack = []; +} + +exports.UndoStack = UndoStack; + +UndoStack.prototype = { + // Current index into the undo stack. Is positioned after the last + // currently-applied change. + _index: 0, + + // The current batch depth (see startBatch() for details) + _batchDepth: 0, + + destroy: function () { + this.uninstallController(); + delete this._stack; + }, + + /** + * Start a collection of related changes. Changes will be batched + * together into one undo/redo item until endBatch() is called. + * + * Batches can be nested, in which case the outer batch will contain + * all items from the inner batches. This allows larger user + * actions made up of a collection of smaller actions to be + * undone as a single action. + */ + startBatch: function () { + if (this._batchDepth++ === 0) { + this._batch = []; + } + }, + + /** + * End a batch of related changes, performing its action and adding + * it to the undo stack. + */ + endBatch: function () { + if (--this._batchDepth > 0) { + return; + } + + // Cut off the end of the undo stack at the current index, + // and the beginning to prevent a stack larger than maxUndo. + let start = Math.max((this._index + 1) - this.maxUndo, 0); + this._stack = this._stack.slice(start, this._index); + + let batch = this._batch; + delete this._batch; + let entry = { + do: function () { + for (let item of batch) { + item.do(); + } + }, + undo: function () { + for (let i = batch.length - 1; i >= 0; i--) { + batch[i].undo(); + } + } + }; + this._stack.push(entry); + this._index = this._stack.length; + entry.do(); + this._change(); + }, + + /** + * Perform an action, adding it to the undo stack. + * + * @param function toDo Called to perform the action. + * @param function undo Called to reverse the action. + */ + do: function (toDo, undo) { + this.startBatch(); + this._batch.push({ do: toDo, undo }); + this.endBatch(); + }, + + /* + * Returns true if undo() will do anything. + */ + canUndo: function () { + return this._index > 0; + }, + + /** + * Undo the top of the undo stack. + * + * @return true if an action was undone. + */ + undo: function () { + if (!this.canUndo()) { + return false; + } + this._stack[--this._index].undo(); + this._change(); + return true; + }, + + /** + * Returns true if redo() will do anything. + */ + canRedo: function () { + return this._stack.length > this._index; + }, + + /** + * Redo the most recently undone action. + * + * @return true if an action was redone. + */ + redo: function () { + if (!this.canRedo()) { + return false; + } + this._stack[this._index++].do(); + this._change(); + return true; + }, + + _change: function () { + if (this._controllerWindow) { + this._controllerWindow.goUpdateCommand("cmd_undo"); + this._controllerWindow.goUpdateCommand("cmd_redo"); + } + }, + + /** + * ViewController implementation for undo/redo. + */ + + /** + * Install this object as a command controller. + */ + installController: function (controllerWindow) { + this._controllerWindow = controllerWindow; + controllerWindow.controllers.appendController(this); + }, + + /** + * Uninstall this object from the command controller. + */ + uninstallController: function () { + if (!this._controllerWindow) { + return; + } + this._controllerWindow.controllers.removeController(this); + }, + + supportsCommand: function (command) { + return (command == "cmd_undo" || + command == "cmd_redo"); + }, + + isCommandEnabled: function (command) { + switch (command) { + case "cmd_undo": return this.canUndo(); + case "cmd_redo": return this.canRedo(); + } + return false; + }, + + doCommand: function (command) { + switch (command) { + case "cmd_undo": return this.undo(); + case "cmd_redo": return this.redo(); + default: return null; + } + }, + + onEvent: function (event) {}, +}; |