/* 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() { 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() { if (this._batchDepth++ === 0) { this._batch = []; } }, /** * End a batch of related changes, performing its action and adding * it to the undo stack. */ endBatch() { 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. const start = Math.max(this._index + 1 - this.maxUndo, 0); this._stack = this._stack.slice(start, this._index); const batch = this._batch; delete this._batch; const entry = { do() { for (const item of batch) { item.do(); } }, undo() { for (let i = batch.length - 1; i >= 0; i--) { batch[i].undo(); } }, }; this._stack.push(entry); this._index = this._stack.length; entry.do(); }, /** * 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(toDo, undo) { this.startBatch(); this._batch.push({ do: toDo, undo }); this.endBatch(); }, /* * Returns true if undo() will do anything. */ canUndo() { return this._index > 0; }, /** * Undo the top of the undo stack. * * @return true if an action was undone. */ undo() { if (!this.canUndo()) { return false; } this._stack[--this._index].undo(); return true; }, /** * Returns true if redo() will do anything. */ canRedo() { return this._stack.length > this._index; }, /** * Redo the most recently undone action. * * @return true if an action was redone. */ redo() { if (!this.canRedo()) { return false; } this._stack[this._index++].do(); return true; }, /** * ViewController implementation for undo/redo. */ /** * Install this object as a command controller. */ installController(controllerWindow) { const controllers = controllerWindow.controllers; // Only available when running in a Firefox panel. if (!controllers || !controllers.appendController) { return; } this._controllerWindow = controllerWindow; controllers.appendController(this); }, /** * Uninstall this object from the command controller. */ uninstallController() { if (!this._controllerWindow) { return; } this._controllerWindow.controllers.removeController(this); }, supportsCommand(command) { return command == "cmd_undo" || command == "cmd_redo"; }, isCommandEnabled(command) { switch (command) { case "cmd_undo": return this.canUndo(); case "cmd_redo": return this.canRedo(); } return false; }, doCommand(command) { switch (command) { case "cmd_undo": return this.undo(); case "cmd_redo": return this.redo(); default: return null; } }, onEvent(event) {}, };