UNPKG

sprotty

Version:

A next-gen framework for graphical views

401 lines 16.5 kB
"use strict"; /******************************************************************************** * Copyright (c) 2017-2018 TypeFox and others. * * This program and the accompanying materials are made available under the * terms of the Eclipse Public License v. 2.0 which is available at * http://www.eclipse.org/legal/epl-2.0. * * This Source Code may also be made available under the following Secondary * Licenses when the conditions for such availability set forth in the Eclipse * Public License v. 2.0 are satisfied: GNU General Public License, version 2 * with the GNU Classpath Exception which is available at * https://www.gnu.org/software/classpath/license.html. * * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; var __metadata = (this && this.__metadata) || function (k, v) { if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v); }; Object.defineProperty(exports, "__esModule", { value: true }); exports.CommandStack = void 0; const inversify_1 = require("inversify"); const types_1 = require("../types"); const smodel_factory_1 = require("../model/smodel-factory"); const smodel_1 = require("../model/smodel"); const animation_frame_syncer_1 = require("../animations/animation-frame-syncer"); const command_1 = require("./command"); /** * The implementation of the ICommandStack. Clients should not use this * class directly. * * The command stack holds the current model as the result of the current * promise. When a new command is executed/undone/redone, its execution is * chained using <code>Promise#then()</code> to the current Promise. This * way we can handle long running commands without blocking the current * thread. * * The command stack also does the special handling for special commands: * * System commands should be transparent to the user and as such be * automatically undone/redone with the next plain command. Additional care * must be taken that system commands that are executed after undo don't * break the correspondence between the topmost commands on the undo and * redo stacks. * * Hidden commands only tell the viewer to render a hidden model such that * its bounds can be extracted from the DOM and forwarded as separate actions. * Hidden commands should not leave any trace on the undo/redo/off stacks. * * Mergeable commands should be merged with their predecessor if possible, * such that e.g. multiple subsequent moves of the smae element can be undone * in one single step. */ let CommandStack = class CommandStack { constructor() { this.undoStack = []; this.redoStack = []; /** * Map which holds the last stoppable command for certain action kinds. */ this.stoppableCommands = new Map(); /** * System commands should be transparent to the user in undo/redo * operations. When a system command is executed when the redo * stack is not empty, it is pushed to offStack instead. * * On redo, all commands form this stack are undone such that the * redo operation gets the exact same model as when it was executed * first. * * On undo, all commands form this stack are undone as well as * system ommands should be transparent to the user. */ this.offStack = []; } initialize() { this.currentPromise = Promise.resolve({ main: { model: this.modelFactory.createRoot(smodel_factory_1.EMPTY_ROOT), modelChanged: false, }, hidden: { model: this.modelFactory.createRoot(smodel_factory_1.EMPTY_ROOT), modelChanged: false, }, popup: { model: this.modelFactory.createRoot(smodel_factory_1.EMPTY_ROOT), modelChanged: false, } }); } get currentModel() { return this.currentPromise.then(state => state.main.model); } executeAll(commands) { commands.forEach(command => { this.logger.log(this, 'Executing', command); this.handleCommand(command, command.execute, this.mergeOrPush); }); return this.thenUpdate(); } execute(command) { this.logger.log(this, 'Executing', command); this.handleCommand(command, command.execute, this.mergeOrPush); return this.thenUpdate(); } undo() { this.undoOffStackSystemCommands(); this.undoPreceedingSystemCommands(); const command = this.undoStack[this.undoStack.length - 1]; if (command !== undefined && !this.isBlockUndo(command)) { this.undoStack.pop(); this.logger.log(this, 'Undoing', command); this.handleCommand(command, command.undo, (c, context) => { this.redoStack.push(c); }); } return this.thenUpdate(); } redo() { this.undoOffStackSystemCommands(); const command = this.redoStack.pop(); if (command !== undefined) { this.logger.log(this, 'Redoing', command); this.handleCommand(command, command.redo, (c, context) => { this.pushToUndoStack(c); }); } this.redoFollowingSystemCommands(); return this.thenUpdate(); } /** * Chains the current promise with another Promise that performs the * given operation on the given command. * * @param beforeResolve a function that is called directly before * resolving the Promise to return the new model. Usually puts the * command on the appropriate stack. */ handleCommand(command, operation, beforeResolve) { // If the command implements the IStoppableCommand interface, we first need to stop the execution of the // previous command with the same action kind and then store the new command as the last stoppable command. if ((0, command_1.isStoppableCommand)(command)) { const stoppableCommand = this.stoppableCommands.get(command.stoppableCommandKey); if (stoppableCommand) { stoppableCommand.stopExecution(); } this.stoppableCommands.set(command.stoppableCommandKey, command); } this.currentPromise = this.currentPromise.then(state => new Promise(resolve => { let target; if (command instanceof command_1.HiddenCommand) target = 'hidden'; else if (command instanceof command_1.PopupCommand) target = 'popup'; else target = 'main'; const context = this.createContext(state.main.model); let commandResult; try { commandResult = operation.call(command, context); } catch (error) { this.logger.error(this, "Failed to execute command:", error); commandResult = state[target].model; } const newState = copyState(state); if (commandResult instanceof Promise) { commandResult.then(newModel => { if (target === 'main') beforeResolve.call(this, command, context); newState[target] = { model: newModel, modelChanged: true }; resolve(newState); }); } else if (commandResult instanceof smodel_1.SModelRootImpl) { if (target === 'main') beforeResolve.call(this, command, context); newState[target] = { model: commandResult, modelChanged: true }; resolve(newState); } else { if (target === 'main') beforeResolve.call(this, command, context); newState[target] = { model: commandResult.model, modelChanged: state[target].modelChanged || commandResult.modelChanged, cause: commandResult.cause }; resolve(newState); } })); } pushToUndoStack(command) { this.undoStack.push(command); if (this.options.undoHistoryLimit >= 0 && this.undoStack.length > this.options.undoHistoryLimit) this.undoStack.splice(0, this.undoStack.length - this.options.undoHistoryLimit); } /** * Notifies the Viewer to render the new model and/or the new hidden model * and returns a Promise for the new model. */ thenUpdate() { this.currentPromise = this.currentPromise.then(state => { const newState = copyState(state); if (state.hidden.modelChanged) { this.updateHidden(state.hidden.model, state.hidden.cause); newState.hidden.modelChanged = false; newState.hidden.cause = undefined; } if (state.main.modelChanged) { this.update(state.main.model, state.main.cause); newState.main.modelChanged = false; newState.main.cause = undefined; } if (state.popup.modelChanged) { this.updatePopup(state.popup.model, state.popup.cause); newState.popup.modelChanged = false; newState.popup.cause = undefined; } return newState; }); return this.currentModel; } /** * Notify the `ModelViewer` that the model has changed. */ update(model, cause) { if (this.modelViewer === undefined) { this.modelViewer = this.viewerProvider.modelViewer; } this.modelViewer.update(model, cause); } /** * Notify the `HiddenModelViewer` that the hidden model has changed. */ updateHidden(model, cause) { if (this.hiddenModelViewer === undefined) { this.hiddenModelViewer = this.viewerProvider.hiddenModelViewer; } this.hiddenModelViewer.update(model, cause); } /** * Notify the `PopupModelViewer` that the popup model has changed. */ updatePopup(model, cause) { if (this.popupModelViewer === undefined) { this.popupModelViewer = this.viewerProvider.popupModelViewer; } this.popupModelViewer.update(model, cause); } /** * Handling of commands after their execution. * * Hidden commands are not pushed to any stack. * * System commands are pushed to the <code>offStack</code> when the redo * stack is not empty, allowing to undo the before a redo to keep the chain * of commands consistent. * * Mergable commands are merged if possible. */ mergeOrPush(command, context) { if (this.isBlockUndo(command)) { this.undoStack = []; this.redoStack = []; this.offStack = []; this.pushToUndoStack(command); return; } if (this.isPushToOffStack(command) && this.redoStack.length > 0) { if (this.offStack.length > 0) { const lastCommand = this.offStack[this.offStack.length - 1]; if (lastCommand instanceof command_1.MergeableCommand && lastCommand.merge(command, context)) return; } this.offStack.push(command); return; } if (this.isPushToUndoStack(command)) { this.offStack.forEach(c => this.undoStack.push(c)); this.offStack = []; this.redoStack = []; if (this.undoStack.length > 0) { const lastCommand = this.undoStack[this.undoStack.length - 1]; if (lastCommand instanceof command_1.MergeableCommand && lastCommand.merge(command, context)) return; } this.pushToUndoStack(command); } } /** * Reverts all system commands on the offStack. */ undoOffStackSystemCommands() { let command = this.offStack.pop(); while (command !== undefined) { this.logger.log(this, 'Undoing off-stack', command); this.handleCommand(command, command.undo, () => { }); command = this.offStack.pop(); } } /** * System commands should be transparent to the user, so this method * is called from <code>undo()</code> to revert all system commands * at the top of the undoStack. */ undoPreceedingSystemCommands() { let command = this.undoStack[this.undoStack.length - 1]; while (command !== undefined && this.isPushToOffStack(command)) { this.undoStack.pop(); this.logger.log(this, 'Undoing', command); this.handleCommand(command, command.undo, (c, context) => { this.redoStack.push(c); }); command = this.undoStack[this.undoStack.length - 1]; } } /** * System commands should be transparent to the user, so this method * is called from <code>redo()</code> to re-execute all system commands * at the top of the redoStack. */ redoFollowingSystemCommands() { let command = this.redoStack[this.redoStack.length - 1]; while (command !== undefined && this.isPushToOffStack(command)) { this.redoStack.pop(); this.logger.log(this, 'Redoing ', command); this.handleCommand(command, command.redo, (c, context) => { this.pushToUndoStack(c); }); command = this.redoStack[this.redoStack.length - 1]; } } /** * Assembles the context object that is passed to the commands execution method. */ createContext(currentModel) { return { root: currentModel, modelChanged: this, modelFactory: this.modelFactory, duration: this.options.defaultDuration, logger: this.logger, syncer: this.syncer }; } isPushToOffStack(command) { return command instanceof command_1.SystemCommand; } isPushToUndoStack(command) { return !(command instanceof command_1.HiddenCommand); } isBlockUndo(command) { return command instanceof command_1.ResetCommand; } }; exports.CommandStack = CommandStack; __decorate([ (0, inversify_1.inject)(types_1.TYPES.IModelFactory), __metadata("design:type", Object) ], CommandStack.prototype, "modelFactory", void 0); __decorate([ (0, inversify_1.inject)(types_1.TYPES.IViewerProvider), __metadata("design:type", Object) ], CommandStack.prototype, "viewerProvider", void 0); __decorate([ (0, inversify_1.inject)(types_1.TYPES.ILogger), __metadata("design:type", Object) ], CommandStack.prototype, "logger", void 0); __decorate([ (0, inversify_1.inject)(types_1.TYPES.AnimationFrameSyncer), __metadata("design:type", animation_frame_syncer_1.AnimationFrameSyncer) ], CommandStack.prototype, "syncer", void 0); __decorate([ (0, inversify_1.inject)(types_1.TYPES.CommandStackOptions), __metadata("design:type", Object) ], CommandStack.prototype, "options", void 0); __decorate([ (0, inversify_1.postConstruct)(), __metadata("design:type", Function), __metadata("design:paramtypes", []), __metadata("design:returntype", void 0) ], CommandStack.prototype, "initialize", null); exports.CommandStack = CommandStack = __decorate([ (0, inversify_1.injectable)() ], CommandStack); function copyState(state) { return { main: Object.assign({}, state.main), hidden: Object.assign({}, state.hidden), popup: Object.assign({}, state.popup) }; } //# sourceMappingURL=command-stack.js.map