sprotty
Version:
A next-gen framework for graphical views
401 lines • 16.5 kB
JavaScript
"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