michael@0: /* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ michael@0: /* vim: set ft=javascript ts=2 et sw=2 tw=80: */ michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: /** michael@0: * A simple undo stack manager. michael@0: * michael@0: * Actions are added along with the necessary code to michael@0: * reverse the action. michael@0: * michael@0: * @param function aChange Called whenever the size or position michael@0: * of the undo stack changes, to use for updating undo-related michael@0: * UI. michael@0: * @param integer aMaxUndo Maximum number of undo steps. michael@0: * defaults to 50. michael@0: */ michael@0: function UndoStack(aMaxUndo) michael@0: { michael@0: this.maxUndo = aMaxUndo || 50; michael@0: this._stack = []; michael@0: } michael@0: michael@0: exports.UndoStack = UndoStack; michael@0: michael@0: UndoStack.prototype = { michael@0: // Current index into the undo stack. Is positioned after the last michael@0: // currently-applied change. michael@0: _index: 0, michael@0: michael@0: // The current batch depth (see startBatch() for details) michael@0: _batchDepth: 0, michael@0: michael@0: destroy: function Undo_destroy() michael@0: { michael@0: this.uninstallController(); michael@0: delete this._stack; michael@0: }, michael@0: michael@0: /** michael@0: * Start a collection of related changes. Changes will be batched michael@0: * together into one undo/redo item until endBatch() is called. michael@0: * michael@0: * Batches can be nested, in which case the outer batch will contain michael@0: * all items from the inner batches. This allows larger user michael@0: * actions made up of a collection of smaller actions to be michael@0: * undone as a single action. michael@0: */ michael@0: startBatch: function Undo_startBatch() michael@0: { michael@0: if (this._batchDepth++ === 0) { michael@0: this._batch = []; michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * End a batch of related changes, performing its action and adding michael@0: * it to the undo stack. michael@0: */ michael@0: endBatch: function Undo_endBatch() michael@0: { michael@0: if (--this._batchDepth > 0) { michael@0: return; michael@0: } michael@0: michael@0: // Cut off the end of the undo stack at the current index, michael@0: // and the beginning to prevent a stack larger than maxUndo. michael@0: let start = Math.max((this._index + 1) - this.maxUndo, 0); michael@0: this._stack = this._stack.slice(start, this._index); michael@0: michael@0: let batch = this._batch; michael@0: delete this._batch; michael@0: let entry = { michael@0: do: function() { michael@0: for (let item of batch) { michael@0: item.do(); michael@0: } michael@0: }, michael@0: undo: function() { michael@0: for (let i = batch.length - 1; i >= 0; i--) { michael@0: batch[i].undo(); michael@0: } michael@0: } michael@0: }; michael@0: this._stack.push(entry); michael@0: this._index = this._stack.length; michael@0: entry.do(); michael@0: this._change(); michael@0: }, michael@0: michael@0: /** michael@0: * Perform an action, adding it to the undo stack. michael@0: * michael@0: * @param function aDo Called to perform the action. michael@0: * @param function aUndo Called to reverse the action. michael@0: */ michael@0: do: function Undo_do(aDo, aUndo) { michael@0: this.startBatch(); michael@0: this._batch.push({ do: aDo, undo: aUndo }); michael@0: this.endBatch(); michael@0: }, michael@0: michael@0: /* michael@0: * Returns true if undo() will do anything. michael@0: */ michael@0: canUndo: function Undo_canUndo() michael@0: { michael@0: return this._index > 0; michael@0: }, michael@0: michael@0: /** michael@0: * Undo the top of the undo stack. michael@0: * michael@0: * @return true if an action was undone. michael@0: */ michael@0: undo: function Undo_canUndo() michael@0: { michael@0: if (!this.canUndo()) { michael@0: return false; michael@0: } michael@0: this._stack[--this._index].undo(); michael@0: this._change(); michael@0: return true; michael@0: }, michael@0: michael@0: /** michael@0: * Returns true if redo() will do anything. michael@0: */ michael@0: canRedo: function Undo_canRedo() michael@0: { michael@0: return this._stack.length > this._index; michael@0: }, michael@0: michael@0: /** michael@0: * Redo the most recently undone action. michael@0: * michael@0: * @return true if an action was redone. michael@0: */ michael@0: redo: function Undo_canRedo() michael@0: { michael@0: if (!this.canRedo()) { michael@0: return false; michael@0: } michael@0: this._stack[this._index++].do(); michael@0: this._change(); michael@0: return true; michael@0: }, michael@0: michael@0: _change: function Undo__change() michael@0: { michael@0: if (this._controllerWindow) { michael@0: this._controllerWindow.goUpdateCommand("cmd_undo"); michael@0: this._controllerWindow.goUpdateCommand("cmd_redo"); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * ViewController implementation for undo/redo. michael@0: */ michael@0: michael@0: /** michael@0: * Install this object as a command controller. michael@0: */ michael@0: installController: function Undo_installController(aControllerWindow) michael@0: { michael@0: this._controllerWindow = aControllerWindow; michael@0: aControllerWindow.controllers.appendController(this); michael@0: }, michael@0: michael@0: /** michael@0: * Uninstall this object from the command controller. michael@0: */ michael@0: uninstallController: function Undo_uninstallController() michael@0: { michael@0: if (!this._controllerWindow) { michael@0: return; michael@0: } michael@0: this._controllerWindow.controllers.removeController(this); michael@0: }, michael@0: michael@0: supportsCommand: function Undo_supportsCommand(aCommand) michael@0: { michael@0: return (aCommand == "cmd_undo" || michael@0: aCommand == "cmd_redo"); michael@0: }, michael@0: michael@0: isCommandEnabled: function Undo_isCommandEnabled(aCommand) michael@0: { michael@0: switch(aCommand) { michael@0: case "cmd_undo": return this.canUndo(); michael@0: case "cmd_redo": return this.canRedo(); michael@0: }; michael@0: return false; michael@0: }, michael@0: michael@0: doCommand: function Undo_doCommand(aCommand) michael@0: { michael@0: switch(aCommand) { michael@0: case "cmd_undo": return this.undo(); michael@0: case "cmd_redo": return this.redo(); michael@0: } michael@0: }, michael@0: michael@0: onEvent: function Undo_onEvent(aEvent) {}, michael@0: }