From 81cbd84a255e15e0c0a9cc5118546912afaf515f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miro=20Sp=C3=B6nemann?= Date: Fri, 30 Aug 2019 10:03:11 +0200 Subject: [PATCH 1/2] #8, #85: Implemented request/response actions, code cleanup --- examples/app.ts | 4 +- examples/circlegraph/circlegraph.html | 7 +- examples/circlegraph/src/di.config.ts | 13 +- examples/circlegraph/src/standalone.ts | 98 ++++++--- examples/classdiagram/class-diagram.html | 4 +- examples/classdiagram/src/popup.ts | 11 +- examples/index.html | 8 +- examples/mindmap/mindmap.html | 4 +- examples/multicore/multicore.html | 4 +- examples/svg/svg-prerendered.html | 4 +- src/base/actions/action-dispatcher.ts | 92 ++++++--- src/base/actions/action.ts | 36 ++++ src/base/commands/command-registration.ts | 32 +-- src/base/commands/command-stack.spec.ts | 24 ++- src/base/commands/command-stack.ts | 188 ++++++++++-------- src/base/commands/command.ts | 28 ++- src/base/commands/request-command.ts | 47 +++++ src/base/di.config.ts | 41 ++-- src/base/features/initialize-canvas.ts | 15 +- src/base/features/set-model.ts | 24 ++- src/base/types.ts | 6 +- .../ui-extensions/ui-extension-registry.ts | 21 +- src/base/views/viewer-cache.ts | 65 ++---- src/base/views/viewer.tsx | 169 ++++++++++------ src/base/views/vnode-decorators.ts | 4 +- src/features/bounds/bounds-manipulation.ts | 48 +++-- src/features/bounds/hidden-bounds-updater.ts | 12 +- src/features/edit/create.ts | 7 +- src/features/edit/delete.ts | 7 +- src/features/edit/edit-label.ts | 9 +- src/features/edit/edit-routing.ts | 10 +- src/features/edit/model.ts | 2 +- src/features/edit/reconnect.ts | 7 +- src/features/export/export.spec.ts | 11 +- src/features/export/export.ts | 47 +++-- src/features/export/model.ts | 9 +- src/features/export/svg-exporter.ts | 11 +- src/features/hover/hover.ts | 47 +++-- src/features/hover/initializer.ts | 4 +- src/features/move/move.ts | 14 +- src/features/select/di.config.ts | 3 +- src/features/select/select.ts | 52 ++++- src/features/update/update-model.ts | 7 +- src/features/viewport/center-fit.ts | 20 +- src/features/viewport/di.config.ts | 5 +- src/features/viewport/scroll.ts | 4 +- src/features/viewport/viewport-root.ts | 5 +- src/features/viewport/viewport.spec.ts | 6 +- src/features/viewport/viewport.ts | 67 +++++-- src/features/viewport/zoom.ts | 4 +- src/model-source/commit-model.ts | 2 +- src/model-source/local-model-source.spec.ts | 51 +++-- src/model-source/local-model-source.ts | 73 ++++--- 53 files changed, 958 insertions(+), 535 deletions(-) create mode 100644 src/base/commands/request-command.ts diff --git a/examples/app.ts b/examples/app.ts index 66eb6f1c..d26798f9 100644 --- a/examples/app.ts +++ b/examples/app.ts @@ -16,7 +16,7 @@ import "reflect-metadata"; -import runStandalone from "./circlegraph/src/standalone"; +import runCircleGraph from "./circlegraph/src/standalone"; import runClassDiagram from "./classdiagram/src/standalone"; import runMindmap from "./mindmap/src/standalone"; import runSvgPreRendered from "./svg/src/standalone"; @@ -26,7 +26,7 @@ const appDiv = document.getElementById('sprotty-app') if(appDiv) { const appMode = appDiv.getAttribute('data-app'); if (appMode === 'circlegraph') - runStandalone(); + runCircleGraph(); else if (appMode === 'class-diagram') runClassDiagram(); else if (appMode === 'mindmap') diff --git a/examples/circlegraph/circlegraph.html b/examples/circlegraph/circlegraph.html index edd96586..c9a3aead 100644 --- a/examples/circlegraph/circlegraph.html +++ b/examples/circlegraph/circlegraph.html @@ -3,7 +3,7 @@ - sprotty Circles Example + Sprotty Circles Example @@ -13,10 +13,11 @@
-

sprotty Circles Example

+

Sprotty Circles Example

- + +

diff --git a/examples/circlegraph/src/di.config.ts b/examples/circlegraph/src/di.config.ts index 0fac292d..08f6476f 100644 --- a/examples/circlegraph/src/di.config.ts +++ b/examples/circlegraph/src/di.config.ts @@ -19,10 +19,19 @@ import { defaultModule, TYPES, configureViewerOptions, SGraphView, PolylineEdgeView, ConsoleLogger, LogLevel, WebSocketDiagramServer, boundsModule, moveModule, selectModule, undoRedoModule, viewportModule, LocalModelSource, exportModule, CircularNode, configureModelElement, SGraph, SEdge, updateModule, - graphModule, routingModule, modelSourceModule + graphModule, routingModule, modelSourceModule, selectFeature } from "../../../src"; import { CircleNodeView } from "./views"; +class CustomEdge extends SEdge { + hasFeature(feature: symbol): boolean { + if (feature === selectFeature) + return false; + else + return super.hasFeature(feature); + } +} + export default (useWebsocket: boolean) => { require("../../../css/sprotty.css"); require("../css/diagram.css"); @@ -36,7 +45,7 @@ export default (useWebsocket: boolean) => { const context = { bind, unbind, isBound, rebind }; configureModelElement(context, 'graph', SGraph, SGraphView); configureModelElement(context, 'node:circle', CircularNode, CircleNodeView); - configureModelElement(context, 'edge:straight', SEdge, PolylineEdgeView); + configureModelElement(context, 'edge:straight', CustomEdge, PolylineEdgeView); configureViewerOptions(context, { needsClientLayout: false }); diff --git a/examples/circlegraph/src/standalone.ts b/examples/circlegraph/src/standalone.ts index c86c38e1..810cb258 100644 --- a/examples/circlegraph/src/standalone.ts +++ b/examples/circlegraph/src/standalone.ts @@ -16,79 +16,115 @@ import { TYPES, IActionDispatcher, SModelElementSchema, SEdgeSchema, SNodeSchema, SGraphSchema, SGraphFactory, - ElementMove, MoveAction, LocalModelSource + ElementMove, MoveAction, LocalModelSource, Bounds, SelectAction, Point } from "../../../src"; import createContainer from "./di.config"; -export default function runStandalone() { - const container = createContainer(false); - - // Initialize gmodel - const node0 = { id: 'node0', type: 'node:circle', position: { x: 100, y: 100 }, size: { width: 80, height: 80 } }; - const graph: SGraphSchema = { id: 'graph', type: 'graph', children: [node0] }; +const NODE_SIZE = 60; +export default async function runCircleGraph() { let count = 2; - function addNode(): SModelElementSchema[] { + function addNode(bounds: Bounds): SModelElementSchema[] { const newNode: SNodeSchema = { id: 'node' + count, type: 'node:circle', position: { - x: Math.random() * 1024, - y: Math.random() * 768 + x: bounds.x + Math.random() * (bounds.width - NODE_SIZE), + y: bounds.y + Math.random() * (bounds.height - NODE_SIZE) }, size: { - width: 80, - height: 80 + width: NODE_SIZE, + height: NODE_SIZE } }; const newEdge: SEdgeSchema = { id: 'edge' + count, type: 'edge:straight', sourceId: 'node0', - targetId: 'node' + count++ + targetId: 'node' + count }; + count++; return [newNode, newEdge]; } - for (let i = 0; i < 200; ++i) { - const newElements = addNode(); - for (const e of newElements) { - graph.children.splice(0, 0, e); + function focusGraph(): void { + const graphElement = document.getElementById('graph'); + if (graphElement !== null && typeof graphElement.focus === 'function') + graphElement.focus(); + } + + function getVisibleBounds({ canvasBounds, scroll, zoom }: { canvasBounds: Bounds; scroll: Point; zoom: number; }): Bounds { + return { + ...scroll, + width: canvasBounds.width / zoom, + height: canvasBounds.height / zoom } } - // Run + const container = createContainer(false); + const dispatcher = container.get(TYPES.IActionDispatcher); + const factory = container.get(TYPES.IModelFactory); const modelSource = container.get(TYPES.ModelSource); + + // Initialize model + const node0 = { id: 'node0', type: 'node:circle', position: { x: 100, y: 100 }, size: { width: NODE_SIZE, height: NODE_SIZE } }; + const graph: SGraphSchema = { id: 'graph', type: 'graph', children: [node0] }; + + const initialViewport = await modelSource.getViewport() + for (let i = 0; i < 200; ++i) { + const newElements = addNode(getVisibleBounds(initialViewport)); + graph.children.push(...newElements); + } + + // Run modelSource.setModel(graph); // Button features - document.getElementById('addNode')!.addEventListener('click', () => { - const newElements = addNode(); + document.getElementById('addNode')!.addEventListener('click', async () => { + const viewport = await modelSource.getViewport() + const newElements = addNode(getVisibleBounds(viewport)); modelSource.addElements(newElements); - const graphElement = document.getElementById('graph'); - if (graphElement !== null && typeof graphElement.focus === 'function') - graphElement.focus(); + dispatcher.dispatch(new SelectAction(newElements.map(e => e.id))); + focusGraph(); }); - const dispatcher = container.get(TYPES.IActionDispatcher); - const factory = container.get(TYPES.IModelFactory); - document.getElementById('scrambleNodes')!.addEventListener('click', function (e) { + document.getElementById('scrambleAll')!.addEventListener('click', async () => { + const viewport = await modelSource.getViewport() + const bounds = getVisibleBounds(viewport); const nodeMoves: ElementMove[] = []; graph.children.forEach(shape => { if (factory.isNodeSchema(shape)) { nodeMoves.push({ elementId: shape.id, toPosition: { - x: Math.random() * 1024, - y: Math.random() * 768 + x: bounds.x + Math.random() * (bounds.width - NODE_SIZE), + y: bounds.y + Math.random() * (bounds.height - NODE_SIZE) } }); } }); dispatcher.dispatch(new MoveAction(nodeMoves, true)); - const graphElement = document.getElementById('graph'); - if (graphElement !== null && typeof graphElement.focus === 'function') - graphElement.focus(); + focusGraph(); + }); + + document.getElementById('scrambleSelection')!.addEventListener('click', async () => { + const selection = await modelSource.getSelection(); + const viewport = await modelSource.getViewport() + const bounds = getVisibleBounds(viewport); + const nodeMoves: ElementMove[] = []; + selection.forEach(shape => { + if (factory.isNodeSchema(shape)) { + nodeMoves.push({ + elementId: shape.id, + toPosition: { + x: bounds.x + Math.random() * (bounds.width - NODE_SIZE), + y: bounds.y + Math.random() * (bounds.height - NODE_SIZE) + } + }); + } + }); + dispatcher.dispatch(new MoveAction(nodeMoves, true)); + focusGraph(); }); } diff --git a/examples/classdiagram/class-diagram.html b/examples/classdiagram/class-diagram.html index 505fd69c..9953fb71 100644 --- a/examples/classdiagram/class-diagram.html +++ b/examples/classdiagram/class-diagram.html @@ -3,7 +3,7 @@ - sprotty Class Diagram Example + Sprotty Class Diagram Example @@ -15,7 +15,7 @@
-

sprotty Class Diagram Example

+

Sprotty Class Diagram Example

Help diff --git a/examples/classdiagram/src/popup.ts b/examples/classdiagram/src/popup.ts index edc029a0..7b17a03e 100644 --- a/examples/classdiagram/src/popup.ts +++ b/examples/classdiagram/src/popup.ts @@ -14,16 +14,21 @@ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ -import { injectable } from "inversify"; +import { injectable, inject } from "inversify"; import { - SModelElementSchema, SModelRootSchema, RequestPopupModelAction, PreRenderedElementSchema, IPopupModelProvider + TYPES, IModelFactory, SModelElementSchema, SModelRootSchema, RequestPopupModelAction, + PreRenderedElementSchema, IPopupModelProvider } from "../../../src"; +import { ClassNode } from "./model"; @injectable() export class PopupModelProvider implements IPopupModelProvider { + @inject(TYPES.IModelFactory) modelFactory: IModelFactory; + getPopupModel(request: RequestPopupModelAction, element?: SModelElementSchema): SModelRootSchema | undefined { if (element !== undefined && element.type === 'node:class') { + const node = this.modelFactory.createElement(element) as ClassNode; return { type: 'html', id: 'popup', @@ -31,7 +36,7 @@ export class PopupModelProvider implements IPopupModelProvider { { type: 'pre-rendered', id: 'popup-title', - code: `
Class ${element.id === 'node0' ? 'Foo' : 'Bar'}
` + code: `
Class ${node.name}
` }, { type: 'pre-rendered', diff --git a/examples/index.html b/examples/index.html index 01e1744c..e7b83743 100644 --- a/examples/index.html +++ b/examples/index.html @@ -3,16 +3,16 @@ - sprotty Examples + Sprotty Examples
-

sprotty Examples

- sprotty is a web-based diagramming framework. - For information how to use these examples see Using sprotty. +

Sprotty Examples

+ Sprotty is a web-based diagramming framework. + For information how to use these examples see Using Sprotty.

Without server

    diff --git a/examples/mindmap/mindmap.html b/examples/mindmap/mindmap.html index 29565053..aafb9081 100644 --- a/examples/mindmap/mindmap.html +++ b/examples/mindmap/mindmap.html @@ -3,7 +3,7 @@ - sprotty Mindmap Example + Sprotty Mindmap Example @@ -13,7 +13,7 @@
    -

    sprotty Mindmap Example

    +

    Sprotty Mindmap Example

    Use the mouse hover popup buttons to add or remove nodes. diff --git a/examples/multicore/multicore.html b/examples/multicore/multicore.html index 4900146f..ae4b2cb2 100644 --- a/examples/multicore/multicore.html +++ b/examples/multicore/multicore.html @@ -3,7 +3,7 @@ - sprotty Multicore Example + Sprotty Multicore Example @@ -13,7 +13,7 @@
    -

    sprotty Multicore Example

    +

    Sprotty Multicore Example

    Help diff --git a/examples/svg/svg-prerendered.html b/examples/svg/svg-prerendered.html index b235360e..d8685b04 100644 --- a/examples/svg/svg-prerendered.html +++ b/examples/svg/svg-prerendered.html @@ -3,7 +3,7 @@ - sprotty SVG Example + Sprotty SVG Example @@ -13,7 +13,7 @@
    -

    sprotty SVG Example

    +

    Sprotty SVG Example

    Help diff --git a/src/base/actions/action-dispatcher.ts b/src/base/actions/action-dispatcher.ts index 4dce8661..741736c4 100644 --- a/src/base/actions/action-dispatcher.ts +++ b/src/base/actions/action-dispatcher.ts @@ -17,18 +17,20 @@ import { inject, injectable } from "inversify"; import { TYPES } from "../types"; import { ILogger } from "../../utils/logging"; +import { Deferred } from "../../utils/async"; import { EMPTY_ROOT } from '../model/smodel-factory'; import { ICommandStack } from "../commands/command-stack"; import { AnimationFrameSyncer } from "../animations/animation-frame-syncer"; import { SetModelAction } from '../features/set-model'; import { RedoAction, UndoAction } from "../../features/undo-redo/undo-redo"; -import { Action, isAction } from "./action"; +import { Action, isAction, RequestAction, ResponseAction, isResponseAction } from './action'; import { ActionHandlerRegistry } from "./action-handler"; import { IDiagramLocker } from "./diagram-locker"; export interface IActionDispatcher { dispatch(action: Action): Promise dispatchAll(actions: Action[]): Promise + request(action: RequestAction): Promise } /** @@ -46,9 +48,10 @@ export class ActionDispatcher implements IActionDispatcher { protected actionHandlerRegistry: ActionHandlerRegistry; + protected initialized: Promise | undefined; protected blockUntil?: (action: Action) => boolean; protected postponedActions: PostponedAction[] = []; - protected initialized: Promise | undefined; + protected readonly requests: Map> = new Map(); initialize(): Promise { if (!this.initialized) { @@ -60,46 +63,81 @@ export class ActionDispatcher implements IActionDispatcher { return this.initialized; } - dispatchAll(actions: Action[]): Promise { - return Promise.all(actions.map(action => this.dispatch(action))) as Promise; - } - + /** + * Dispatch an action by querying all handlers that are registered for its kind. + * The returned promise is resolved when all handler results (commands or actions) + * have been processed. + */ dispatch(action: Action): Promise { return this.initialize().then(() => { if (this.blockUntil !== undefined) { return this.handleBlocked(action, this.blockUntil); } else if (this.diagramLocker.isAllowed(action)) { - if (action.kind === UndoAction.KIND) { - return this.commandStack.undo().then(() => {}); - } else if (action.kind === RedoAction.KIND) { - return this.commandStack.redo().then(() => {}); - } else { - return this.handleAction(action); - } + return this.handleAction(action); } return undefined; }); } + /** + * Calls `dispatch` on every action in the given array. The returned promise + * is resolved when the promises of all `dispatch` calls have been resolved. + */ + dispatchAll(actions: Action[]): Promise { + return Promise.all(actions.map(action => this.dispatch(action))) as Promise; + } + + /** + * Dispatch a request. The returned promise is resolved when a response with matching + * identifier is dispatched. That response is _not_ passed to the registered action + * handlers. Instead, it is the responsibility of the caller of this method to handle + * the response properly. For example, it can be sent to the registered handlers by + * passing it again to the `dispatch` method. + */ + request(action: RequestAction): Promise { + if (!action.requestId) { + return Promise.reject(new Error('Request without requestId')); + } + const deferred = new Deferred(); + this.requests.set(action.requestId, deferred); + this.dispatch(action); + return deferred.promise; + } + protected handleAction(action: Action): Promise { - this.logger.log(this, 'handle', action); - const handlers = this.actionHandlerRegistry.get(action.kind); - if (handlers.length > 0) { - const promises: Promise[] = []; - for (const handler of handlers) { - const result = handler.handle(action); - if (isAction(result)) { - promises.push(this.dispatch(result)); - } else if (result !== undefined) { - promises.push(this.commandStack.execute(result)); - this.blockUntil = result.blockUntil; - } + if (action.kind === UndoAction.KIND) { + return this.commandStack.undo().then(() => {}); + } + if (action.kind === RedoAction.KIND) { + return this.commandStack.redo().then(() => {}); + } + if (isResponseAction(action)) { + const deferred = this.requests.get(action.responseId); + if (deferred !== undefined) { + this.requests.delete(action.responseId); + deferred.resolve(action); + return Promise.resolve(); } - return Promise.all(promises) as Promise; - } else { + this.logger.log(this, 'No matching request for response', action); + } + + const handlers = this.actionHandlerRegistry.get(action.kind); + if (handlers.length === 0) { this.logger.warn(this, 'Missing handler for action', action); return Promise.reject(`Missing handler for action '${action.kind}'`); } + this.logger.log(this, 'Handle', action); + const promises: Promise[] = []; + for (const handler of handlers) { + const result = handler.handle(action); + if (isAction(result)) { + promises.push(this.dispatch(result)); + } else if (result !== undefined) { + promises.push(this.commandStack.execute(result)); + this.blockUntil = result.blockUntil; + } + } + return Promise.all(promises) as Promise; } protected handleBlocked(action: Action, predicate: (action: Action) => boolean): Promise { diff --git a/src/base/actions/action.ts b/src/base/actions/action.ts index 00496605..5efbfb28 100644 --- a/src/base/actions/action.ts +++ b/src/base/actions/action.ts @@ -26,3 +26,39 @@ export interface Action { export function isAction(object?: any): object is Action { return object !== undefined && object.hasOwnProperty('kind') && typeof(object['kind']) === 'string'; } + +/** + * A request action is tied to the expectation of receiving a corresponding response action. + * The `requestId` property is used to match the received response with the original request. + */ +export interface RequestAction extends Action { + readonly requestId: string +} + +export function isRequestAction(object?: any): object is RequestAction { + return isAction(object) && object.hasOwnProperty('requestId') + && typeof((object as any)['requestId']) === 'string'; +} + +let nextRequestId = 1; +/** + * Generate a unique `requestId` for a request action. + */ +export function generateRequestId(): string { + return (nextRequestId++).toString(); +} + +/** + * A response action is sent to respond to a request action. The `responseId` must match + * the `requestId` of the preceding request. In case the `responseId` is empty or undefined, + * the action is handled as standalone, i.e. it was fired without a preceding request. + */ +export interface ResponseAction extends Action { + readonly responseId: string +} + +export function isResponseAction(object?: any): object is ResponseAction { + return isAction(object) && object.hasOwnProperty('responseId') + && typeof((object as any)['responseId']) === 'string' + && (object as any)['responseId'] !== ''; +} diff --git a/src/base/commands/command-registration.ts b/src/base/commands/command-registration.ts index b5d1f9e9..9310a4ab 100644 --- a/src/base/commands/command-registration.ts +++ b/src/base/commands/command-registration.ts @@ -57,21 +57,21 @@ export interface ICommandConstructor { * Use this method in your DI configuration to register a new command to the diagram. */ export function configureCommand(context: { bind: interfaces.Bind, isBound: interfaces.IsBound }, constr: ICommandConstructor) { - if (isInjectable(constr)) { - if (!context.isBound(constr)) - context.bind(constr).toSelf(); - context.bind(TYPES.CommandRegistration).toDynamicValue((ctx) => { - return { - factory: (action: Action) => { - const childContainer = new Container(); - childContainer.parent = ctx.container; - childContainer.bind(TYPES.Action).toConstantValue(action); - return childContainer.get(constr); - }, - kind: constr.KIND - }; - }); - } else { - throw Error(`Commands should be @injectable ${constr.name}`); + if (!isInjectable(constr)) { + throw new Error(`Commands should be @injectable ${constr.name}`); } + if (!context.isBound(constr)) { + context.bind(constr).toSelf(); + } + context.bind(TYPES.CommandRegistration).toDynamicValue((ctx) => { + return { + factory: (action: Action) => { + const childContainer = new Container(); + childContainer.parent = ctx.container; + childContainer.bind(TYPES.Action).toConstantValue(action); + return childContainer.get(constr); + }, + kind: constr.KIND + }; + }); } diff --git a/src/base/commands/command-stack.spec.ts b/src/base/commands/command-stack.spec.ts index 56b257fc..380b3828 100644 --- a/src/base/commands/command-stack.spec.ts +++ b/src/base/commands/command-stack.spec.ts @@ -20,7 +20,7 @@ import { expect } from "chai"; import { Container, injectable } from "inversify"; import { TYPES } from "../types"; import defaultModule from "../di.config"; -import { IViewer } from "../views/viewer"; +import { IViewerProvider } from "../views/viewer"; import { Command, HiddenCommand, SystemCommand, CommandExecutionContext, CommandResult, MergeableCommand, PopupCommand } from './command'; @@ -139,21 +139,27 @@ describe('CommandStack', () => { let hiddenViewerUpdates: number = 0; let popupUpdates: number = 0; - const mockViewer: IViewer = { - update() { - ++viewerUpdates; + const mockViewerProvider: IViewerProvider = { + modelViewer: { + update() { + ++viewerUpdates; + } }, - updateHidden() { - ++hiddenViewerUpdates; + hiddenModelViewer: { + update() { + ++hiddenViewerUpdates; + } }, - updatePopup() { - ++popupUpdates; + popupModelViewer: { + update() { + ++popupUpdates; + } } }; const container = new Container(); container.load(defaultModule); - container.rebind(TYPES.IViewer).toConstantValue(mockViewer); + container.rebind(TYPES.IViewerProvider).toConstantValue(mockViewerProvider); const commandStack = container.get(TYPES.ICommandStack); diff --git a/src/base/commands/command-stack.ts b/src/base/commands/command-stack.ts index a6a13f2d..ea8d0358 100644 --- a/src/base/commands/command-stack.ts +++ b/src/base/commands/command-stack.ts @@ -19,11 +19,13 @@ import { TYPES } from "../types"; import { ILogger } from "../../utils/logging"; import { EMPTY_ROOT, IModelFactory } from "../model/smodel-factory"; import { SModelRoot } from "../model/smodel"; +import { Action } from "../actions/action"; import { AnimationFrameSyncer } from "../animations/animation-frame-syncer"; import { IViewer, IViewerProvider } from "../views/viewer"; import { CommandStackOptions } from './command-stack-options'; import { - HiddenCommand, ICommand, CommandExecutionContext, CommandResult, SystemCommand, MergeableCommand, PopupCommand, ResetCommand + HiddenCommand, ICommand, CommandExecutionContext, CommandResult, SystemCommand, + MergeableCommand, PopupCommand, ResetCommand, DetailedCommandResult } from './command'; /** @@ -109,7 +111,9 @@ export class CommandStack implements ICommandStack { protected currentPromise: Promise; - protected viewer?: IViewer; + protected modelViewer?: IViewer; + protected hiddenModelViewer?: IViewer; + protected popupModelViewer?: IViewer; protected undoStack: ICommand[] = []; protected redoStack: ICommand[] = []; @@ -131,18 +135,24 @@ export class CommandStack implements ICommandStack { @postConstruct() protected initialize() { this.currentPromise = Promise.resolve({ - root: this.modelFactory.createRoot(EMPTY_ROOT), - hiddenRoot: undefined, - popupRoot: undefined, - rootChanged: false, - hiddenRootChanged: false, - popupChanged: false + main: { + model: this.modelFactory.createRoot(EMPTY_ROOT), + isChanged: false, + }, + hidden: { + model: this.modelFactory.createRoot(EMPTY_ROOT), + isChanged: false, + }, + popup: { + model: this.modelFactory.createRoot(EMPTY_ROOT), + isChanged: false, + } }); } protected get currentModel(): Promise { return this.currentPromise.then( - state => state.root + state => state.main.model ); } @@ -194,58 +204,53 @@ export class CommandStack implements ICommandStack { * 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. + * resolving the Promise to return the new model. Usually puts the + * command on the appropriate stack. */ protected handleCommand(command: ICommand, operation: (context: CommandExecutionContext) => CommandResult, beforeResolve: (command: ICommand, context: CommandExecutionContext) => void) { this.currentPromise = this.currentPromise.then(state => - new Promise((resolve: (result: CommandStackState) => void, reject: (reason?: any) => void) => { - const context = this.createContext(state.root); - - let newResult: CommandResult; + new Promise(resolve => { + let target: 'main' | 'hidden' | 'popup'; + if (command instanceof HiddenCommand) + target = 'hidden'; + else if (command instanceof PopupCommand) + target = 'popup'; + else + target = 'main'; + const context = this.createContext(state[target].model); + + let commandResult: CommandResult; try { - newResult = operation.call(command, context); + commandResult = operation.call(command, context); } catch (error) { this.logger.error(this, "Failed to execute command:", error); - newResult = state.root; + commandResult = state[target].model; } - if (command instanceof HiddenCommand) { - resolve({ - ...state, ...{ - hiddenRoot: newResult as SModelRoot, - hiddenRootChanged: true - } - }); - } else if (command instanceof PopupCommand) { - resolve({ - ...state, ...{ - popupRoot: newResult as SModelRoot, - popupChanged: true - } - }); - } else if (newResult instanceof Promise) { - newResult.then( - (newModel: SModelRoot) => { + const newState = copyState(state); + if (commandResult instanceof Promise) { + commandResult.then(newModel => { + if (target === 'main') beforeResolve.call(this, command, context); - resolve({ - ...state, ...{ - root: newModel, - rootChanged: true - } - }); - } - ); - } else { - beforeResolve.call(this, command, context); - resolve({ - ...state, ...{ - root: newResult, - rootChanged: true - } + newState[target] = { model: newModel, isChanged: true }; + resolve(newState); }); + } else if (commandResult instanceof SModelRoot) { + if (target === 'main') + beforeResolve.call(this, command, context); + newState[target] = { model: commandResult, isChanged: true }; + resolve(newState); + } else { + if (target === 'main') + beforeResolve.call(this, command, context); + newState[target] = { + model: commandResult.model, + isChanged: state[target].isChanged || commandResult.isChanged, + cause: commandResult.cause + }; + resolve(newState); } }) ); @@ -262,50 +267,56 @@ export class CommandStack implements ICommandStack { * and returns a Promise for the new model. */ protected thenUpdate(): Promise { - this.currentPromise = this.currentPromise.then(async state => { - if (state.hiddenRootChanged && state.hiddenRoot !== undefined) - await this.updateHidden(state.hiddenRoot); - if (state.rootChanged) - await this.update(state.root); - if (state.popupChanged && state.popupRoot !== undefined) - await this.updatePopup(state.popupRoot); - return { - root: state.root, - hiddenRoot: undefined, - popupRoot: undefined, - rootChanged: false, - hiddenRootChanged: false, - popupChanged: false - }; + this.currentPromise = this.currentPromise.then(state => { + const newState = copyState(state); + if (state.hidden.isChanged) { + this.updateHidden(state.hidden.model, state.hidden.cause); + newState.hidden.isChanged = false; + newState.hidden.cause = undefined; + } + if (state.main.isChanged) { + this.update(state.main.model, state.main.cause); + newState.main.isChanged = false; + newState.main.cause = undefined; + } + if (state.popup.isChanged) { + this.updatePopup(state.popup.model, state.popup.cause); + newState.popup.isChanged = false; + newState.popup.cause = undefined; + } + return newState; }); return this.currentModel; } /** - * Notify the Viewer that the model has changed. + * Notify the `ModelViewer` that the model has changed. */ - async update(model: SModelRoot): Promise { - if (this.viewer === undefined) - this.viewer = await this.viewerProvider(); - this.viewer.update(model); + update(model: SModelRoot, cause?: Action): void { + if (this.modelViewer === undefined) { + this.modelViewer = this.viewerProvider.modelViewer; + } + this.modelViewer.update(model, cause); } /** - * Notify the Viewer that the hidden model has changed. + * Notify the `HiddenModelViewer` that the hidden model has changed. */ - async updateHidden(model: SModelRoot): Promise { - if (this.viewer === undefined) - this.viewer = await this.viewerProvider(); - this.viewer.updateHidden(model); + updateHidden(model: SModelRoot, cause?: Action): void { + if (this.hiddenModelViewer === undefined) { + this.hiddenModelViewer = this.viewerProvider.hiddenModelViewer; + } + this.hiddenModelViewer.update(model, cause); } /** - * Notify the Viewer that the model has changed. + * Notify the `PopupModelViewer` that the popup model has changed. */ - async updatePopup(model: SModelRoot): Promise { - if (this.viewer === undefined) - this.viewer = await this.viewerProvider(); - this.viewer.updatePopup(model); + updatePopup(model: SModelRoot, cause?: Action): void { + if (this.popupModelViewer === undefined) { + this.popupModelViewer = this.viewerProvider.popupModelViewer; + } + this.popupModelViewer.update(model, cause); } /** @@ -423,15 +434,18 @@ export class CommandStack implements ICommandStack { } /** - * Internal type to pass the results between the Promises in the - * ICommandStack. + * Internal type to pass the results between the promises in the `CommandStack`. */ export interface CommandStackState { - root: SModelRoot - hiddenRoot: SModelRoot | undefined - popupRoot: SModelRoot | undefined - rootChanged: boolean - hiddenRootChanged: boolean - popupChanged: boolean + main: DetailedCommandResult, + hidden: DetailedCommandResult, + popup: DetailedCommandResult } +function copyState(state: CommandStackState): CommandStackState { + return { + main: {...state.main}, + hidden: {...state.hidden}, + popup: {...state.popup} + }; +} diff --git a/src/base/commands/command.ts b/src/base/commands/command.ts index f511a297..072d6215 100644 --- a/src/base/commands/command.ts +++ b/src/base/commands/command.ts @@ -52,14 +52,22 @@ export interface ICommand { } /** - * Commands return the changed model or a Promise for it. The latter - * serves animating commands to render some intermediate states before + * Commands return the changed model or a Promise for it. Promises + * serve animating commands to render some intermediate states before * finishing. The CommandStack is in charge of chaining these promises, * such that they run sequentially only one at a time. Due to that * chaining, it is essential that a command does not make any assumption * on the state of the model before execute() is called. */ -export type CommandResult = SModelRoot | Promise; +export type CommandResult = SModelRoot | Promise | DetailedCommandResult; + +/** + * The `DetailedCommandResult` allows to specify whether the model has changed + * and the original action that caused the command to be executed. In case such + * an action is given, it is passed to the viewer in order to link any + * subsequent response action to the original request. + */ +export type DetailedCommandResult = { model: SModelRoot, isChanged: boolean, cause?: Action }; /** * Base class for all commands. @@ -125,7 +133,7 @@ export abstract class MergeableCommand extends Command { */ @injectable() export abstract class HiddenCommand extends Command { - abstract execute(context: CommandExecutionContext): SModelRoot; + abstract execute(context: CommandExecutionContext): SModelRoot | DetailedCommandResult; undo(context: CommandExecutionContext): CommandResult { context.logger.error(this, 'Cannot undo a hidden command'); @@ -170,23 +178,21 @@ export abstract class ResetCommand extends Command { * access to the context. */ export interface CommandExecutionContext { - /** the current sprotty model */ + /** The current Sprotty model */ root: SModelRoot - /** - * used ot turn sprotty schema elements (e.g. from the action) - * into model elements*/ + /** Used to turn sprotty schema elements (e.g. from the action) into model elements */ modelFactory: IModelFactory - /** allows to give some feedback to the console */ + /** Allows to give some feedback to the console */ logger: ILogger /** Used for anmiations to trigger the rendering of a new frame */ modelChanged: IViewer - /** duration of an anmiation */ + /** Duration of an anmiation */ duration: number - /** provides the ticks for animations */ + /** Provides the ticks for animations */ syncer: AnimationFrameSyncer } diff --git a/src/base/commands/request-command.ts b/src/base/commands/request-command.ts new file mode 100644 index 00000000..4e2d7688 --- /dev/null +++ b/src/base/commands/request-command.ts @@ -0,0 +1,47 @@ +/******************************************************************************** + * Copyright (c) 2019 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 + ********************************************************************************/ + +import { injectable, inject } from "inversify"; +import { TYPES } from "../types"; +import { SystemCommand, CommandExecutionContext, CommandResult } from "./command"; +import { ResponseAction } from "../actions/action"; +import { IActionDispatcher } from "../actions/action-dispatcher"; + +/** + * A command that does not modify the internal model, but retrieves information + * from it by dispatching a response action. + */ +@injectable() +export abstract class ModelRequestCommand extends SystemCommand { + + @inject(TYPES.IActionDispatcher) protected actionDispatcher: IActionDispatcher; + + execute(context: CommandExecutionContext): CommandResult { + const result = this.retrieveResult(context); + this.actionDispatcher.dispatch(result); + return { model: context.root, isChanged: false }; + } + + protected abstract retrieveResult(context: CommandExecutionContext): ResponseAction; + + undo(context: CommandExecutionContext): CommandResult { + return { model: context.root, isChanged: false }; + } + + redo(context: CommandExecutionContext): CommandResult { + return { model: context.root, isChanged: false }; + } +} diff --git a/src/base/di.config.ts b/src/base/di.config.ts index c0c92207..0b072a8c 100644 --- a/src/base/di.config.ts +++ b/src/base/di.config.ts @@ -24,7 +24,7 @@ import { CommandStack, ICommandStack } from "./commands/command-stack"; import { CommandStackOptions } from "./commands/command-stack-options"; import { SModelFactory, SModelRegistry } from './model/smodel-factory'; import { AnimationFrameSyncer } from "./animations/animation-frame-syncer"; -import { IViewer, Viewer, ModelRenderer } from "./views/viewer"; +import { IViewer, ModelViewer, HiddenModelViewer, PopupModelViewer, ModelRenderer, PatcherProvider } from "./views/viewer"; import { ViewerOptions, defaultViewerOptions } from "./views/viewer-options"; import { MouseTool, PopupMouseTool, MousePositionTracker } from "./views/mouse-tool"; import { KeyTool } from "./views/key-tool"; @@ -89,20 +89,37 @@ const defaultContainerModule = new ContainerModule((bind, _unbind, isBound) => { }); // Viewer --------------------------------------------- - bind(Viewer).toSelf().inSingletonScope(); - bind(TYPES.IViewer).toDynamicValue(context => - context.container.get(Viewer)).inSingletonScope().whenTargetNamed('delegate'); - bind(ViewerCache).toSelf().inSingletonScope(); - bind(TYPES.IViewer).toDynamicValue(context => - context.container.get(ViewerCache)).inSingletonScope().whenTargetIsDefault(); - bind(TYPES.IViewerProvider).toProvider((context) => { - return () => { - return new Promise((resolve) => { - resolve(context.container.get(TYPES.IViewer)); - }); + bind(ModelViewer).toSelf().inSingletonScope(); + bind(HiddenModelViewer).toSelf().inSingletonScope(); + bind(PopupModelViewer).toSelf().inSingletonScope(); + bind(TYPES.ModelViewer).toDynamicValue(context => { + const container = context.container.createChild(); + container.bind(TYPES.IViewer).toService(ModelViewer); + container.bind(ViewerCache).toSelf(); + return container.get(ViewerCache); + }).inSingletonScope(); + bind(TYPES.PopupModelViewer).toDynamicValue(context => { + const container = context.container.createChild(); + container.bind(TYPES.IViewer).toService(PopupModelViewer); + container.bind(ViewerCache).toSelf(); + return container.get(ViewerCache); + }).inSingletonScope(); + bind(TYPES.HiddenModelViewer).toService(HiddenModelViewer); + bind(TYPES.IViewerProvider).toDynamicValue(context => { + return { + get modelViewer() { + return context.container.get(TYPES.ModelViewer); + }, + get hiddenModelViewer() { + return context.container.get(TYPES.HiddenModelViewer); + }, + get popupModelViewer() { + return context.container.get(TYPES.PopupModelViewer); + } }; }); bind(TYPES.ViewerOptions).toConstantValue(defaultViewerOptions()); + bind(TYPES.PatcherProvider).to(PatcherProvider).inSingletonScope(); bind(TYPES.DOMHelper).to(DOMHelper).inSingletonScope(); bind(TYPES.ModelRendererFactory).toFactory((context: interfaces.Context) => { return (decorators: IVNodeDecorator[]) => { diff --git a/src/base/features/initialize-canvas.ts b/src/base/features/initialize-canvas.ts index 4acf757a..30ff188d 100644 --- a/src/base/features/initialize-canvas.ts +++ b/src/base/features/initialize-canvas.ts @@ -22,7 +22,7 @@ import { Action } from '../actions/action'; import { IActionDispatcher } from '../actions/action-dispatcher'; import { IVNodeDecorator } from "../views/vnode-decorators"; import { SModelElement, SModelRoot } from "../model/smodel"; -import { SystemCommand, CommandExecutionContext } from '../commands/command'; +import { SystemCommand, CommandExecutionContext, CommandResult } from '../commands/command'; /** * Grabs the bounds from the root element in page coordinates and fires a @@ -73,7 +73,8 @@ export class CanvasBoundsInitializer implements IVNodeDecorator { } export class InitializeCanvasBoundsAction implements Action { - readonly kind = InitializeCanvasBoundsCommand.KIND; + static readonly KIND: string = 'initializeCanvasBounds'; + readonly kind = InitializeCanvasBoundsAction.KIND; constructor(public readonly newCanvasBounds: Bounds) { } @@ -81,25 +82,25 @@ export class InitializeCanvasBoundsAction implements Action { @injectable() export class InitializeCanvasBoundsCommand extends SystemCommand { - static readonly KIND: string = 'initializeCanvasBounds'; + static readonly KIND: string = InitializeCanvasBoundsAction.KIND; private newCanvasBounds: Bounds; - constructor(@inject(TYPES.Action) protected action: InitializeCanvasBoundsAction) { + constructor(@inject(TYPES.Action) protected readonly action: InitializeCanvasBoundsAction) { super(); } - execute(context: CommandExecutionContext) { + execute(context: CommandExecutionContext): CommandResult { this.newCanvasBounds = this.action.newCanvasBounds; context.root.canvasBounds = this.newCanvasBounds; return context.root; } - undo(context: CommandExecutionContext) { + undo(context: CommandExecutionContext): CommandResult { return context.root; } - redo(context: CommandExecutionContext) { + redo(context: CommandExecutionContext): CommandResult { return context.root; } } diff --git a/src/base/features/set-model.ts b/src/base/features/set-model.ts index 21d7ee84..c76e579c 100644 --- a/src/base/features/set-model.ts +++ b/src/base/features/set-model.ts @@ -15,7 +15,7 @@ ********************************************************************************/ import { inject, injectable } from "inversify"; -import { Action } from "../actions/action"; +import { Action, RequestAction, ResponseAction, generateRequestId } from "../actions/action"; import { CommandExecutionContext, ResetCommand } from "../commands/command"; import { SModelRoot, SModelRootSchema } from "../model/smodel"; import { TYPES } from "../types"; @@ -26,32 +26,38 @@ import { InitializeCanvasBoundsCommand } from './initialize-canvas'; * is the first message that is sent to the source, so it is also used to initiate the communication. * The response is a SetModelAction or an UpdateModelAction. */ -export class RequestModelAction implements Action { +export class RequestModelAction implements RequestAction { static readonly KIND = 'requestModel'; readonly kind = RequestModelAction.KIND; - constructor(public readonly options?: { [key: string]: string }) { + constructor(public readonly options?: { [key: string]: string }, + public readonly requestId = '') {} + + /** Factory function to dispatch a request with the `IActionDispatcher` */ + static create(options?: { [key: string]: string }): RequestAction { + return new RequestModelAction(options, generateRequestId()); } } /** * Sent from the model source to the client in order to set the model. If a model is already present, it is replaced. */ -export class SetModelAction implements Action { - readonly kind = SetModelCommand.KIND; +export class SetModelAction implements ResponseAction { + static readonly KIND = 'setModel'; + readonly kind = SetModelAction.KIND; - constructor(public readonly newRoot: SModelRootSchema) { - } + constructor(public readonly newRoot: SModelRootSchema, + public readonly responseId = '') {} } @injectable() export class SetModelCommand extends ResetCommand { - static readonly KIND = 'setModel'; + static readonly KIND = SetModelAction.KIND; oldRoot: SModelRoot; newRoot: SModelRoot; - constructor(@inject(TYPES.Action) public action: SetModelAction) { + constructor(@inject(TYPES.Action) protected readonly action: SetModelAction) { super(); } diff --git a/src/base/types.ts b/src/base/types.ts index 32c3e7a3..92794af6 100644 --- a/src/base/types.ts +++ b/src/base/types.ts @@ -30,6 +30,7 @@ export const TYPES = { IDiagramLocker: Symbol('IDiagramLocker'), DOMHelper: Symbol('DOMHelper'), IEdgeRouter: Symbol('IEdgeRouter'), + HiddenModelViewer: Symbol('HiddenModelViewer'), HiddenVNodeDecorator: Symbol('HiddenVNodeDecorator'), HoverState: Symbol('HoverState'), KeyListener: Symbol('KeyListener'), @@ -42,16 +43,19 @@ export const TYPES = { ModelRendererFactory: Symbol('ModelRendererFactory'), ModelSource: Symbol('ModelSource'), ModelSourceProvider: Symbol('ModelSourceProvider'), + ModelViewer: Symbol('ModelViewer'), MouseListener: Symbol('MouseListener'), + PatcherProvider: Symbol('PatcherProvider'), IPopupModelProvider: Symbol('IPopupModelProvider'), + PopupModelViewer: Symbol('PopupModelViewer'), PopupMouseListener: Symbol('PopupMouseListener'), PopupVNodeDecorator: Symbol('PopupVNodeDecorator'), SModelElementRegistration: Symbol('SModelElementRegistration'), SModelRegistry: Symbol('SModelRegistry'), ISnapper: Symbol('ISnapper'), SvgExporter: Symbol('SvgExporter'), - IViewer: Symbol('IViewer'), ViewerOptions: Symbol('ViewerOptions'), + IViewer: Symbol('IViewer'), IViewerProvider: Symbol('IViewerProvider'), ViewRegistration: Symbol('ViewRegistration'), ViewRegistry: Symbol('ViewRegistry'), diff --git a/src/base/ui-extensions/ui-extension-registry.ts b/src/base/ui-extensions/ui-extension-registry.ts index 7ef4ad68..323880d4 100644 --- a/src/base/ui-extensions/ui-extension-registry.ts +++ b/src/base/ui-extensions/ui-extension-registry.ts @@ -16,7 +16,7 @@ import { inject, injectable, multiInject, optional } from "inversify"; import { InstanceRegistry } from "../../utils/registry"; import { Action } from "../actions/action"; -import { CommandExecutionContext, CommandResult, SystemCommand } from "../commands/command"; +import { CommandExecutionContext, SystemCommand, CommandResult } from "../commands/command"; import { TYPES } from "../types"; import { IUIExtension } from "./ui-extension"; @@ -35,16 +35,21 @@ export class UIExtensionRegistry extends InstanceRegistry { * Action to set the visibility state of the UI extension with the specified `id`. */ export class SetUIExtensionVisibilityAction implements Action { - readonly kind = SetUIExtensionVisibilityCommand.KIND; - constructor(public readonly extensionId: string, public readonly visible: boolean, public readonly contextElementsId: string[] = []) { } + static readonly KIND = "setUIExtensionVisibility"; + readonly kind = SetUIExtensionVisibilityAction.KIND; + + constructor(public readonly extensionId: string, + public readonly visible: boolean, + public readonly contextElementsId: string[] = []) {} } @injectable() export class SetUIExtensionVisibilityCommand extends SystemCommand { - static KIND = "setUIExtensionVisibility"; + static readonly KIND = SetUIExtensionVisibilityAction.KIND; + @inject(TYPES.UIExtensionRegistry) protected readonly registry: UIExtensionRegistry; - constructor(@inject(TYPES.Action) public action: SetUIExtensionVisibilityAction) { + constructor(@inject(TYPES.Action) protected readonly action: SetUIExtensionVisibilityAction) { super(); } @@ -53,13 +58,13 @@ export class SetUIExtensionVisibilityCommand extends SystemCommand { if (extension) { this.action.visible ? extension.show(context.root, ...this.action.contextElementsId) : extension.hide(); } - return context.root; + return { model: context.root, isChanged: false }; } undo(context: CommandExecutionContext): CommandResult { - return context.root; + return { model: context.root, isChanged: false }; } redo(context: CommandExecutionContext): CommandResult { - return context.root; + return { model: context.root, isChanged: false }; } } diff --git a/src/base/views/viewer-cache.ts b/src/base/views/viewer-cache.ts index 20577132..41079807 100644 --- a/src/base/views/viewer-cache.ts +++ b/src/base/views/viewer-cache.ts @@ -14,9 +14,10 @@ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ -import { inject, injectable, named } from "inversify"; +import { inject, injectable } from "inversify"; import { SModelRoot } from "../model/smodel"; import { TYPES } from "../types"; +import { Action } from "../actions/action"; import { AnimationFrameSyncer } from "../animations/animation-frame-syncer"; import { IViewer } from "./viewer"; @@ -28,55 +29,31 @@ import { IViewer } from "./viewer"; */ @injectable() export class ViewerCache implements IViewer { - @inject(TYPES.IViewer) @named('delegate') protected delegate: IViewer; - @inject(TYPES.AnimationFrameSyncer) protected syncer: AnimationFrameSyncer; - - cachedModelRoot: SModelRoot | undefined; - cachedHiddenModelRoot: SModelRoot | undefined; - cachedPopup: SModelRoot | undefined; - - protected isCacheEmpty(): boolean { - return this.cachedModelRoot === undefined && this.cachedHiddenModelRoot === undefined && - this.cachedPopup === undefined; - } - - updatePopup(model: SModelRoot): void { - const isCacheEmpty = this.isCacheEmpty(); - this.cachedPopup = model; - if (isCacheEmpty) - this.scheduleUpdate(); - } - update(model: SModelRoot): void { - const isCacheEmpty = this.isCacheEmpty(); - this.cachedModelRoot = model; - if (isCacheEmpty) - this.scheduleUpdate(); - } + @inject(TYPES.IViewer) protected delegate: IViewer; + @inject(TYPES.AnimationFrameSyncer) protected syncer: AnimationFrameSyncer; - updateHidden(hiddenModel: SModelRoot): void { - const isCacheEmpty = this.isCacheEmpty(); - this.cachedHiddenModelRoot = hiddenModel; - if (isCacheEmpty) - this.scheduleUpdate(); + protected cachedModel?: SModelRoot; + + update(model: SModelRoot, cause?: Action): void { + if (cause !== undefined) { + // Forward the update immediately in order to pass the cause action + this.delegate.update(model, cause); + this.cachedModel = undefined; + } else { + const isCacheEmpty = this.cachedModel === undefined; + this.cachedModel = model; + if (isCacheEmpty) { + this.scheduleUpdate(); + } + } } protected scheduleUpdate() { this.syncer.onEndOfNextFrame(() => { - if (this.cachedHiddenModelRoot) { - const nextHiddenModelRoot = this.cachedHiddenModelRoot; - this.delegate.updateHidden(nextHiddenModelRoot); - this.cachedHiddenModelRoot = undefined; - } - if (this.cachedModelRoot) { - const nextModelRoot = this.cachedModelRoot; - this.delegate.update(nextModelRoot); - this.cachedModelRoot = undefined; - } - if (this.cachedPopup) { - const nextModelRoot = this.cachedPopup; - this.delegate.updatePopup(nextModelRoot); - this.cachedPopup = undefined; + if (this.cachedModel) { + this.delegate.update(this.cachedModel); + this.cachedModel = undefined; } }); } diff --git a/src/base/views/viewer.tsx b/src/base/views/viewer.tsx index ea349a02..56d39476 100644 --- a/src/base/views/viewer.tsx +++ b/src/base/views/viewer.tsx @@ -31,6 +31,7 @@ import { ILogger } from "../../utils/logging"; import { ORIGIN_POINT } from "../../utils/geometry"; import { SModelElement, SModelRoot, SParentElement } from "../model/smodel"; import { IActionDispatcher } from "../actions/action-dispatcher"; +import { Action } from '../actions/action'; import { InitializeCanvasBoundsAction } from "../features/initialize-canvas"; import { IVNodeDecorator } from "./vnode-decorators"; import { RenderingContext, ViewRegistry } from "./view"; @@ -40,9 +41,13 @@ import { isThunk } from "./thunk-view"; import { EMPTY_ROOT } from "../model/smodel-factory"; export interface IViewer { - update(model: SModelRoot): void - updateHidden(hiddenModel: SModelRoot): void - updatePopup(popupModel: SModelRoot): void + update(model: SModelRoot, cause?: Action): void +} + +export interface IViewerProvider { + readonly modelViewer: IViewer + readonly hiddenModelViewer: IViewer + readonly popupModelViewer: IViewer } export class ModelRenderer implements RenderingContext { @@ -68,45 +73,24 @@ export class ModelRenderer implements RenderingContext { return element.children.map((child) => this.renderElement(child, args)); } - postUpdate() { - this.decorators.forEach(decorator => decorator.postUpdate()); + postUpdate(cause?: Action) { + this.decorators.forEach(decorator => decorator.postUpdate(cause)); } } export type ModelRendererFactory = (decorators: IVNodeDecorator[]) => ModelRenderer; -/** - * The component that turns the model into an SVG DOM. - * Uses a VDOM based on snabbdom.js for performance. - */ +export type Patcher = (oldRoot: VNode | Element, newRoot: VNode) => VNode; + @injectable() -export class Viewer implements IViewer { +export class PatcherProvider { - @inject(TYPES.ViewerOptions) protected options: ViewerOptions; - @inject(TYPES.ILogger) protected logger: ILogger; - @inject(TYPES.IActionDispatcher) protected actiondispatcher: IActionDispatcher; + readonly patcher: Patcher; - constructor( - @inject(TYPES.ModelRendererFactory) protected readonly modelRendererFactory: ModelRendererFactory, - @multiInject(TYPES.IVNodeDecorator) @optional() decorators: IVNodeDecorator[], - @multiInject(TYPES.HiddenVNodeDecorator) @optional() hiddenDecorators: IVNodeDecorator[], - @multiInject(TYPES.PopupVNodeDecorator) @optional() popupDecorators: IVNodeDecorator[]) { - this.patcher = this.createPatcher(); - this.renderer = this.modelRendererFactory(decorators); - this.hiddenRenderer = this.modelRendererFactory(hiddenDecorators); - this.popupRenderer = this.modelRendererFactory(popupDecorators); + constructor() { + this.patcher = init(this.createModules()); } - protected renderer: ModelRenderer; - protected hiddenRenderer: ModelRenderer; - protected popupRenderer: ModelRenderer; - - protected readonly patcher: Patcher; - - protected lastVDOM: VNode; - protected lastHiddenVDOM: VNode; - protected lastPopupVDOM: VNode; - protected createModules(): Module[] { return [ propsModule, @@ -117,30 +101,32 @@ export class Viewer implements IViewer { ]; } - protected createPatcher() { - return init(this.createModules()); - } +} - protected onWindowResize = (vdom: VNode): void => { - const baseDiv = document.getElementById(this.options.baseDiv); - if (baseDiv !== null) { - const newBounds = this.getBoundsInPage(baseDiv as Element); - this.actiondispatcher.dispatch(new InitializeCanvasBoundsAction(newBounds)); - } - } +/** + * The component that turns the model into an SVG DOM. + * Uses a VDOM based on snabbdom.js for performance. + */ +@injectable() +export class ModelViewer implements IViewer { - protected getBoundsInPage(element: Element) { - const bounds = element.getBoundingClientRect(); - const scroll = typeof window !== 'undefined' ? {x: window.scrollX, y: window.scrollY} : ORIGIN_POINT; - return { - x: bounds.left + scroll.x, - y: bounds.top + scroll.y, - width: bounds.width, - height: bounds.height - }; + @inject(TYPES.ViewerOptions) protected options: ViewerOptions; + @inject(TYPES.ILogger) protected logger: ILogger; + @inject(TYPES.IActionDispatcher) protected actiondispatcher: IActionDispatcher; + + constructor(@inject(TYPES.ModelRendererFactory) modelRendererFactory: ModelRendererFactory, + @inject(TYPES.PatcherProvider) patcherProvider: PatcherProvider, + @multiInject(TYPES.IVNodeDecorator) @optional() decorators: IVNodeDecorator[]) { + this.renderer = modelRendererFactory(decorators); + this.patcher = patcherProvider.patcher; } - update(model: Readonly): void { + protected readonly renderer: ModelRenderer; + protected readonly patcher: Patcher; + + protected lastVDOM: VNode; + + update(model: Readonly, cause?: Action): void { this.logger.log(this, 'rendering', model); const newVDOM =
    {this.renderer.renderElement(model)} @@ -165,7 +151,7 @@ export class Viewer implements IViewer { this.logger.error(this, 'element not in DOM:', this.options.baseDiv); } } - this.renderer.postUpdate(); + this.renderer.postUpdate(cause); } protected hasFocus(): boolean { @@ -190,7 +176,51 @@ export class Viewer implements IViewer { } } - updateHidden(hiddenModel: Readonly): void { + protected onWindowResize = (vdom: VNode): void => { + const baseDiv = document.getElementById(this.options.baseDiv); + if (baseDiv !== null) { + const newBounds = this.getBoundsInPage(baseDiv as Element); + this.actiondispatcher.dispatch(new InitializeCanvasBoundsAction(newBounds)); + } + } + + protected getBoundsInPage(element: Element) { + const bounds = element.getBoundingClientRect(); + const scroll = typeof window !== 'undefined' ? {x: window.scrollX, y: window.scrollY} : ORIGIN_POINT; + return { + x: bounds.left + scroll.x, + y: bounds.top + scroll.y, + width: bounds.width, + height: bounds.height + }; + } + +} + +/** + * Viewer for the _hidden_ model. This serves as an intermediate step to compute bounds + * of elements. The model is rendered in a section that is not visible to the user, + * and then the bounds are extracted from the DOM. + */ +@injectable() +export class HiddenModelViewer implements IViewer { + + @inject(TYPES.ViewerOptions) protected options: ViewerOptions; + @inject(TYPES.ILogger) protected logger: ILogger; + + constructor(@inject(TYPES.ModelRendererFactory) modelRendererFactory: ModelRendererFactory, + @inject(TYPES.PatcherProvider) patcherProvider: PatcherProvider, + @multiInject(TYPES.HiddenVNodeDecorator) @optional() hiddenDecorators: IVNodeDecorator[]) { + this.hiddenRenderer = modelRendererFactory(hiddenDecorators); + this.patcher = patcherProvider.patcher; + } + + protected readonly hiddenRenderer: ModelRenderer; + protected readonly patcher: Patcher; + + protected lastHiddenVDOM: VNode; + + update(hiddenModel: Readonly, cause?: Action): void { this.logger.log(this, 'rendering hidden'); let newVDOM: VNode; @@ -219,10 +249,30 @@ export class Viewer implements IViewer { setClass(newVDOM, this.options.hiddenClass, true); this.lastHiddenVDOM = this.patcher.call(this, placeholder, newVDOM); } - this.hiddenRenderer.postUpdate(); + this.hiddenRenderer.postUpdate(cause); } - updatePopup(model: Readonly): void { +} + +@injectable() +export class PopupModelViewer implements IViewer { + + @inject(TYPES.ViewerOptions) protected options: ViewerOptions; + @inject(TYPES.ILogger) protected logger: ILogger; + + constructor(@inject(TYPES.ModelRendererFactory) protected readonly modelRendererFactory: ModelRendererFactory, + @inject(TYPES.PatcherProvider) patcherProvider: PatcherProvider, + @multiInject(TYPES.PopupVNodeDecorator) @optional() popupDecorators: IVNodeDecorator[]) { + this.popupRenderer = this.modelRendererFactory(popupDecorators); + this.patcher = patcherProvider.patcher; + } + + protected readonly popupRenderer: ModelRenderer; + protected readonly patcher: Patcher; + + protected lastPopupVDOM: VNode; + + update(model: Readonly, cause?: Action): void { this.logger.log(this, 'rendering popup', model); const popupClosed = model.type === EMPTY_ROOT.type; @@ -256,10 +306,7 @@ export class Viewer implements IViewer { setClass(newVDOM, this.options.popupClosedClass, popupClosed); this.lastPopupVDOM = this.patcher.call(this, placeholder, newVDOM); } - this.popupRenderer.postUpdate(); + this.popupRenderer.postUpdate(cause); } -} -export type Patcher = (oldRoot: VNode | Element, newRoot: VNode) => VNode; - -export type IViewerProvider = () => Promise; +} diff --git a/src/base/views/vnode-decorators.ts b/src/base/views/vnode-decorators.ts index 40baf3c8..37dcf949 100644 --- a/src/base/views/vnode-decorators.ts +++ b/src/base/views/vnode-decorators.ts @@ -17,6 +17,7 @@ import { injectable } from "inversify"; import { VNode } from "snabbdom/vnode"; import { SModelElement } from "../model/smodel"; +import { Action } from "../actions/action"; import { setAttr } from "./vnode-utils"; /** @@ -25,8 +26,7 @@ import { setAttr } from "./vnode-utils"; */ export interface IVNodeDecorator { decorate(vnode: VNode, element: SModelElement): VNode - - postUpdate(): void + postUpdate(cause?: Action): void } @injectable() diff --git a/src/features/bounds/bounds-manipulation.ts b/src/features/bounds/bounds-manipulation.ts index d6f789e9..99d90ce1 100644 --- a/src/features/bounds/bounds-manipulation.ts +++ b/src/features/bounds/bounds-manipulation.ts @@ -15,9 +15,9 @@ ********************************************************************************/ import { Bounds, Point } from "../../utils/geometry"; -import { SModelElement, SModelRoot, SModelRootSchema } from "../../base/model/smodel"; -import { Action } from "../../base/actions/action"; -import { CommandExecutionContext, HiddenCommand, SystemCommand } from "../../base/commands/command"; +import { SModelElement, SModelRootSchema } from "../../base/model/smodel"; +import { Action, RequestAction, ResponseAction, generateRequestId } from "../../base/actions/action"; +import { CommandExecutionContext, HiddenCommand, SystemCommand, DetailedCommandResult, CommandResult } from "../../base/commands/command"; import { BoundsAware, isBoundsAware, Alignable } from './model'; import { injectable, inject } from "inversify"; import { TYPES } from "../../base/types"; @@ -27,7 +27,8 @@ import { TYPES } from "../../base/types"; * (or all) model elements. */ export class SetBoundsAction implements Action { - readonly kind = SetBoundsCommand.KIND; + static readonly KIND: string = 'setBounds'; + readonly kind = SetBoundsAction.KIND; constructor(public readonly bounds: ElementAndBounds[]) { } @@ -39,10 +40,16 @@ export class SetBoundsAction implements Action { * This hidden rendering round-trip is necessary if the client is responsible for parts of the layout * (see `needsClientLayout` viewer option). */ -export class RequestBoundsAction implements Action { - readonly kind = RequestBoundsCommand.KIND; +export class RequestBoundsAction implements RequestAction { + static readonly KIND: string = 'requestBounds'; + readonly kind = RequestBoundsAction.KIND; - constructor(public readonly newRoot: SModelRootSchema) { + constructor(public readonly newRoot: SModelRootSchema, + public readonly requestId: string = '') {} + + /** Factory function to dispatch a request with the `IActionDispatcher` */ + static create(newRoot: SModelRootSchema): RequestAction { + return new RequestBoundsAction(newRoot, generateRequestId()); } } @@ -53,15 +60,14 @@ export class RequestBoundsAction implements Action { * received with this action. Otherwise there is no need to send the computed bounds to the server, * so they can be processed locally by the client. */ -export class ComputedBoundsAction implements Action { +export class ComputedBoundsAction implements ResponseAction { static readonly KIND = 'computedBounds'; - readonly kind = ComputedBoundsAction.KIND; constructor(public readonly bounds: ElementAndBounds[], public readonly revision?: number, - public readonly alignments?: ElementAndAlignment[]) { - } + public readonly alignments?: ElementAndAlignment[], + public readonly responseId = '') {} } /** @@ -104,15 +110,15 @@ export interface ResolvedElementAndAlignment { @injectable() export class SetBoundsCommand extends SystemCommand { - static readonly KIND: string = 'setBounds'; + static readonly KIND: string = SetBoundsAction.KIND; protected bounds: ResolvedElementAndBounds[] = []; - constructor(@inject(TYPES.Action) protected action: SetBoundsAction) { + constructor(@inject(TYPES.Action) protected readonly action: SetBoundsAction) { super(); } - execute(context: CommandExecutionContext) { + execute(context: CommandExecutionContext): CommandResult { this.action.bounds.forEach( b => { const element = context.root.index.getById(b.elementId); @@ -128,14 +134,14 @@ export class SetBoundsCommand extends SystemCommand { return this.redo(context); } - undo(context: CommandExecutionContext) { + undo(context: CommandExecutionContext): CommandResult { this.bounds.forEach( b => b.element.bounds = b.oldBounds ); return context.root; } - redo(context: CommandExecutionContext) { + redo(context: CommandExecutionContext): CommandResult { this.bounds.forEach( b => b.element.bounds = b.newBounds ); @@ -145,14 +151,18 @@ export class SetBoundsCommand extends SystemCommand { @injectable() export class RequestBoundsCommand extends HiddenCommand { - static readonly KIND: string = 'requestBounds'; + static readonly KIND: string = RequestBoundsAction.KIND; constructor(@inject(TYPES.Action) protected action: RequestBoundsAction) { super(); } - execute(context: CommandExecutionContext): SModelRoot { - return context.modelFactory.createRoot(this.action.newRoot); + execute(context: CommandExecutionContext): DetailedCommandResult { + return { + model: context.modelFactory.createRoot(this.action.newRoot), + isChanged: true, + cause: this.action + }; } get blockUntil(): (action: Action) => boolean { diff --git a/src/features/bounds/hidden-bounds-updater.ts b/src/features/bounds/hidden-bounds-updater.ts index 11ca32ff..b07bd6d2 100644 --- a/src/features/bounds/hidden-bounds-updater.ts +++ b/src/features/bounds/hidden-bounds-updater.ts @@ -19,12 +19,12 @@ import { VNode } from "snabbdom/vnode"; import { TYPES } from "../../base/types"; import { almostEquals, Bounds, Point } from '../../utils/geometry'; import { SModelElement, SModelRoot } from "../../base/model/smodel"; +import { Action } from "../../base/actions/action"; import { IVNodeDecorator } from "../../base/views/vnode-decorators"; import { IActionDispatcher } from "../../base/actions/action-dispatcher"; -import { ComputedBoundsAction, ElementAndBounds, ElementAndAlignment } from './bounds-manipulation'; +import { ComputedBoundsAction, ElementAndBounds, ElementAndAlignment, RequestBoundsAction } from './bounds-manipulation'; import { BoundsAware, isSizeable, isLayoutContainer, isAlignable } from "./model"; import { Layouter } from "./layout"; -import { isExportable } from "../export/model"; export class BoundsData { vnode?: VNode; @@ -69,9 +69,11 @@ export class HiddenBoundsUpdater implements IVNodeDecorator { return vnode; } - postUpdate() { - if (this.root !== undefined && isExportable(this.root) && this.root.export) + postUpdate(cause?: Action) { + if (cause === undefined || cause.kind !== RequestBoundsAction.KIND) { return; + } + const request = cause as RequestBoundsAction; this.getBoundsFromDOM(); this.layouter.layout(this.element2boundsData); const resizes: ElementAndBounds[] = []; @@ -90,7 +92,7 @@ export class HiddenBoundsUpdater implements IVNodeDecorator { }); }); const revision = (this.root !== undefined) ? this.root.revision : undefined; - this.actionDispatcher.dispatch(new ComputedBoundsAction(resizes, revision, realignments)); + this.actionDispatcher.dispatch(new ComputedBoundsAction(resizes, revision, realignments, request.requestId)); this.element2boundsData.clear(); } diff --git a/src/features/edit/create.ts b/src/features/edit/create.ts index b938410b..0beb0cec 100644 --- a/src/features/edit/create.ts +++ b/src/features/edit/create.ts @@ -21,19 +21,20 @@ import { inject, injectable } from "inversify"; import { TYPES } from "../../base/types"; export class CreateElementAction implements Action { - readonly kind = CreateElementCommand.KIND; + static readonly KIND = "createElement"; + readonly kind = CreateElementAction.KIND; constructor(readonly containerId: string, readonly elementSchema: SModelElementSchema) {} } @injectable() export class CreateElementCommand extends Command { - static readonly KIND = "createElement"; + static readonly KIND = CreateElementAction.KIND; container: SParentElement; newElement: SChildElement; - constructor(@inject(TYPES.Action) readonly action: CreateElementAction) { + constructor(@inject(TYPES.Action) protected readonly action: CreateElementAction) { super(); } diff --git a/src/features/edit/delete.ts b/src/features/edit/delete.ts index aa70878c..dcfcd45e 100644 --- a/src/features/edit/delete.ts +++ b/src/features/edit/delete.ts @@ -31,7 +31,8 @@ export function isDeletable(element: T): element is T & } export class DeleteElementAction implements Action { - kind = DeleteElementCommand.KIND; + static readonly KIND = 'delete'; + kind = DeleteElementAction.KIND; constructor(readonly elementIds: string[]) {} } @@ -43,11 +44,11 @@ export class ResolvedDelete { @injectable() export class DeleteElementCommand extends Command { - static readonly KIND = 'delete'; + static readonly KIND = DeleteElementAction.KIND; resolvedDeletes: ResolvedDelete[] = []; - constructor(@inject(TYPES.Action) readonly action: DeleteElementAction) { + constructor(@inject(TYPES.Action) protected readonly action: DeleteElementAction) { super(); } diff --git a/src/features/edit/edit-label.ts b/src/features/edit/edit-label.ts index 774447da..735a66f1 100644 --- a/src/features/edit/edit-label.ts +++ b/src/features/edit/edit-label.ts @@ -37,8 +37,9 @@ export function isEditLabelAction(element?: any): element is EditLabelAction { } export class ApplyLabelEditAction implements Action { - static KIND = 'ApplyLabelEdit'; + static readonly KIND = 'applyLabelEdit'; kind = ApplyLabelEditAction.KIND; + constructor(readonly labelId: string, readonly text: string) { } } @@ -49,11 +50,11 @@ export class ResolvedLabelEdit { } export class ApplyLabelEditCommand extends Command { - static KIND = ApplyLabelEditAction.KIND; + static readonly KIND = ApplyLabelEditAction.KIND; - resolvedLabelEdit: ResolvedLabelEdit; + protected resolvedLabelEdit: ResolvedLabelEdit; - constructor(@inject(TYPES.Action) public action: ApplyLabelEditAction) { + constructor(@inject(TYPES.Action) protected readonly action: ApplyLabelEditAction) { super(); } diff --git a/src/features/edit/edit-routing.ts b/src/features/edit/edit-routing.ts index 18cf6086..6cccdbe3 100644 --- a/src/features/edit/edit-routing.ts +++ b/src/features/edit/edit-routing.ts @@ -25,7 +25,8 @@ import { EdgeRouterRegistry } from "../routing/routing"; import { canEditRouting } from './model'; export class SwitchEditModeAction implements Action { - kind = SwitchEditModeCommand.KIND; + static readonly KIND: string = "switchEditMode"; + kind = SwitchEditModeAction.KIND; constructor(public readonly elementsToActivate: string[] = [], public readonly elementsToDeactivate: string[] = []) { @@ -34,20 +35,19 @@ export class SwitchEditModeAction implements Action { @injectable() export class SwitchEditModeCommand extends Command { + static readonly KIND: string = SwitchEditModeAction.KIND; @inject(EdgeRouterRegistry) edgeRouterRegistry: EdgeRouterRegistry; - static KIND: string = "switchEditMode"; - protected elementsToActivate: SModelElement[] = []; protected elementsToDeactivate: SModelElement[] = []; protected handlesToRemove: { handle: SRoutingHandle, parent: SRoutableElement, point?: Point }[] = []; - constructor(@inject(TYPES.Action) public action: SwitchEditModeAction) { + constructor(@inject(TYPES.Action) protected readonly action: SwitchEditModeAction) { super(); } - execute(context: CommandExecutionContext): SModelRoot { + execute(context: CommandExecutionContext): CommandResult { const index = context.root.index; this.action.elementsToActivate.forEach(id => { const element = index.getById(id); diff --git a/src/features/edit/model.ts b/src/features/edit/model.ts index d1ea69b0..4d0ffff2 100644 --- a/src/features/edit/model.ts +++ b/src/features/edit/model.ts @@ -42,4 +42,4 @@ export interface WithEditableLabel extends SModelExtension { export function isWithEditableLabel(element: T): element is T & WithEditableLabel { return 'editableLabel' in element && element.hasFeature(withEditLabelFeature); -} \ No newline at end of file +} diff --git a/src/features/edit/reconnect.ts b/src/features/edit/reconnect.ts index 0276698d..e8e432a8 100644 --- a/src/features/edit/reconnect.ts +++ b/src/features/edit/reconnect.ts @@ -22,7 +22,8 @@ import { SRoutableElement } from "../routing/model"; import { EdgeMemento, EdgeRouterRegistry } from "../routing/routing"; export class ReconnectAction implements Action { - readonly kind = ReconnectCommand.KIND; + static readonly KIND = 'reconnect'; + readonly kind = ReconnectAction.KIND; constructor(readonly routableId: string, readonly newSourceId?: string, @@ -31,13 +32,13 @@ export class ReconnectAction implements Action { @injectable() export class ReconnectCommand extends Command { - static KIND = 'reconnect'; + static readonly KIND = ReconnectAction.KIND; @inject(EdgeRouterRegistry) edgeRouterRegistry: EdgeRouterRegistry; memento: EdgeMemento | undefined; - constructor(@inject(TYPES.Action)readonly action: ReconnectAction) { + constructor(@inject(TYPES.Action) protected readonly action: ReconnectAction) { super(); } diff --git a/src/features/export/export.spec.ts b/src/features/export/export.spec.ts index 3d3c100c..185571cf 100644 --- a/src/features/export/export.spec.ts +++ b/src/features/export/export.spec.ts @@ -21,10 +21,9 @@ import { Container } from 'inversify'; import { TYPES } from '../../base/types'; import { ConsoleLogger } from "../../utils/logging"; import { CommandExecutionContext } from "../../base/commands/command"; -import { SModelRoot } from "../../base/model/smodel"; import { SGraphFactory } from "../../graph/sgraph-factory"; import { SNode, SNodeSchema, SGraph } from "../../graph/sgraph"; -import { ExportSvgCommand } from './export'; +import { ExportSvgCommand, RequestExportSvgAction } from './export'; import defaultModule from "../../base/di.config"; describe('ExportSvgCommand', () => { @@ -48,7 +47,7 @@ describe('ExportSvgCommand', () => { const myNode = model.children[0] as SNode; - const cmd = new ExportSvgCommand(); + const cmd = new ExportSvgCommand(new RequestExportSvgAction()); const context: CommandExecutionContext = { root: model, @@ -61,14 +60,14 @@ describe('ExportSvgCommand', () => { it('execute() clears selection', () => { myNode.selected = true; - const newModel = cmd.execute(context) as SModelRoot; + const newModel = cmd.execute(context).model; expect(newModel.children[0]).instanceof(SNode); expect((newModel.children[0] as SNode).selected).to.equal(false); }); it('execute() removes hover feedback', () => { myNode.hoverFeedback = true; - const newModel = cmd.execute(context) as SModelRoot; + const newModel = cmd.execute(context).model; expect(newModel.children[0]).instanceof(SNode); expect((newModel.children[0] as SNode).hoverFeedback).to.equal(false); }); @@ -76,7 +75,7 @@ describe('ExportSvgCommand', () => { it('execute() resets viewport', () => { model.zoom = 17; model.scroll = { x: 12, y: 12}; - const newModel = cmd.execute(context) as SModelRoot; + const newModel = cmd.execute(context).model; expect(newModel).instanceof(SGraph); expect((newModel as SGraph).zoom).to.equal(1); expect((newModel as SGraph).scroll.x).to.equal(0); diff --git a/src/features/export/export.ts b/src/features/export/export.ts index b886501c..4b85dc8d 100644 --- a/src/features/export/export.ts +++ b/src/features/export/export.ts @@ -16,16 +16,15 @@ import { injectable, inject } from "inversify"; import { VNode } from 'snabbdom/vnode'; -import { CommandExecutionContext, HiddenCommand } from '../../base/commands/command'; +import { CommandExecutionContext, HiddenCommand, DetailedCommandResult } from '../../base/commands/command'; import { IVNodeDecorator } from '../../base/views/vnode-decorators'; import { isSelectable } from '../select/model'; -import { Action } from '../../base/actions/action'; +import { Action, RequestAction, generateRequestId } from '../../base/actions/action'; import { SModelElement, SModelRoot } from '../../base/model/smodel'; import { KeyListener } from '../../base/views/key-tool'; import { matchesKeystroke } from '../../utils/keyboard'; import { isExportable } from './model'; -import { SvgExporter } from './svg-exporter'; -import { EMPTY_ROOT } from '../../base/model/smodel-factory'; +import { SvgExporter, ExportSvgAction } from './svg-exporter'; import { isViewport } from '../viewport/model'; import { isHoverable } from '../hover/model'; import { TYPES } from '../../base/types'; @@ -40,18 +39,29 @@ export class ExportSvgKeyListener extends KeyListener { } } -export class RequestExportSvgAction implements Action { - kind = ExportSvgCommand.KIND; +export class RequestExportSvgAction implements RequestAction { + static readonly KIND = 'requestExportSvg'; + readonly kind = RequestExportSvgAction.KIND; + + constructor(public readonly requestId: string = '') {} + + /** Factory function to dispatch a request with the `IActionDispatcher` */ + static create(): RequestAction { + return new RequestExportSvgAction(generateRequestId()); + } } export class ExportSvgCommand extends HiddenCommand { - static KIND = 'requestExportSvg'; + static readonly KIND = RequestExportSvgAction.KIND; - execute(context: CommandExecutionContext): SModelRoot { + constructor(@inject(TYPES.Action) protected action: RequestExportSvgAction) { + super(); + } + + execute(context: CommandExecutionContext): DetailedCommandResult { if (isExportable(context.root)) { const root = context.modelFactory.createRoot(context.modelFactory.createSchema(context.root)); if (isExportable(root)) { - root.export = true; if (isViewport(root)) { root.zoom = 1; root.scroll = { @@ -65,10 +75,17 @@ export class ExportSvgCommand extends HiddenCommand { if (isHoverable(element) && element.hoverFeedback) element.hoverFeedback = false; }); - return root; + return { + model: root, + isChanged: true, + cause: this.action + }; } } - return context.modelFactory.createRoot(EMPTY_ROOT); + return { + model: context.root, + isChanged: false + }; } } @@ -85,9 +102,9 @@ export class ExportSvgDecorator implements IVNodeDecorator { return vnode; } - postUpdate(): void { - if (this.root && isExportable(this.root) && this.root.export) - this.svgExporter.export(this.root); + postUpdate(cause?: Action): void { + if (this.root && cause !== undefined && cause.kind === RequestExportSvgAction.KIND) { + this.svgExporter.export(this.root, cause as RequestExportSvgAction); + } } } - diff --git a/src/features/export/model.ts b/src/features/export/model.ts index 2e1b4152..52a222e9 100644 --- a/src/features/export/model.ts +++ b/src/features/export/model.ts @@ -15,14 +15,9 @@ ********************************************************************************/ import { SModelElement } from '../../base/model/smodel'; -import { SModelExtension } from '../../base/model/smodel-extension'; export const exportFeature = Symbol('exportFeature'); -export interface Exportable extends SModelExtension { - export: boolean -} - -export function isExportable(element: SModelElement): element is SModelElement & Exportable { - return element.hasFeature(exportFeature) && (element as any)['export'] !== undefined; +export function isExportable(element: SModelElement): boolean { + return element.hasFeature(exportFeature); } diff --git a/src/features/export/svg-exporter.ts b/src/features/export/svg-exporter.ts index b1aa986f..db6727ad 100644 --- a/src/features/export/svg-exporter.ts +++ b/src/features/export/svg-exporter.ts @@ -16,7 +16,7 @@ import { ViewerOptions } from '../../base/views/viewer-options'; import { isBoundsAware } from '../bounds/model'; -import { Action } from '../../base/actions/action'; +import { ResponseAction, RequestAction } from '../../base/actions/action'; import { ActionDispatcher } from '../../base/actions/action-dispatcher'; import { TYPES } from '../../base/types'; import { SModelRoot } from '../../base/model/smodel'; @@ -24,11 +24,12 @@ import { Bounds, combine, EMPTY_BOUNDS } from '../../utils/geometry'; import { ILogger } from '../../utils/logging'; import { injectable, inject } from "inversify"; -export class ExportSvgAction implements Action { +export class ExportSvgAction implements ResponseAction { static KIND = 'exportSvg'; kind = ExportSvgAction.KIND; - constructor(public readonly svg: string) {} + constructor(public readonly svg: string, + public readonly responseId: string = '') {} } @injectable() @@ -38,13 +39,13 @@ export class SvgExporter { @inject(TYPES.IActionDispatcher) protected actionDispatcher: ActionDispatcher; @inject(TYPES.ILogger) protected log: ILogger; - export(root: SModelRoot): void { + export(root: SModelRoot, request?: RequestAction): void { if (typeof document !== 'undefined') { const div = document.getElementById(this.options.hiddenDiv); if (div !== null && div.firstElementChild && div.firstElementChild.tagName === 'svg') { const svgElement = div.firstElementChild as SVGSVGElement; const svg = this.createSvg(svgElement, root); - this.actionDispatcher.dispatch(new ExportSvgAction(svg)); + this.actionDispatcher.dispatch(new ExportSvgAction(svg, request ? request.requestId : '')); } } } diff --git a/src/features/hover/hover.ts b/src/features/hover/hover.ts index 82edab65..0756ba45 100644 --- a/src/features/hover/hover.ts +++ b/src/features/hover/hover.ts @@ -20,8 +20,8 @@ import { Bounds, Point, translate } from "../../utils/geometry"; import { TYPES } from "../../base/types"; import { SModelElement, SModelRoot, SModelRootSchema } from "../../base/model/smodel"; import { MouseListener } from "../../base/views/mouse-tool"; -import { Action } from "../../base/actions/action"; -import { CommandExecutionContext, PopupCommand, SystemCommand } from "../../base/commands/command"; +import { Action, RequestAction, ResponseAction, generateRequestId } from "../../base/actions/action"; +import { CommandExecutionContext, PopupCommand, SystemCommand, CommandResult } from "../../base/commands/command"; import { EMPTY_ROOT } from "../../base/model/smodel-factory"; import { KeyListener } from "../../base/views/key-tool"; import { findParentByFeature, findParent } from "../../base/model/smodel-utils"; @@ -33,7 +33,8 @@ import { hasPopupFeature, isHoverable } from "./model"; * Triggered when the user puts the mouse pointer over an element. */ export class HoverFeedbackAction implements Action { - kind = HoverFeedbackCommand.KIND; + static readonly KIND = 'hoverFeedback'; + kind = HoverFeedbackAction.KIND; constructor(public readonly mouseoverElement: string, public readonly mouseIsOver: boolean) { } @@ -41,14 +42,13 @@ export class HoverFeedbackAction implements Action { @injectable() export class HoverFeedbackCommand extends SystemCommand { - static readonly KIND = 'hoverFeedback'; + static readonly KIND = HoverFeedbackAction.KIND; - constructor(@inject(TYPES.Action) public action: HoverFeedbackAction) { + constructor(@inject(TYPES.Action) protected readonly action: HoverFeedbackAction) { super(); } - execute(context: CommandExecutionContext): SModelRoot { - + execute(context: CommandExecutionContext): CommandResult { const model: SModelRoot = context.root; const modelElement: SModelElement | undefined = model.index.getById(this.action.mouseoverElement); @@ -61,11 +61,11 @@ export class HoverFeedbackCommand extends SystemCommand { return this.redo(context); } - undo(context: CommandExecutionContext): SModelRoot { + undo(context: CommandExecutionContext): CommandResult { return context.root; } - redo(context: CommandExecutionContext): SModelRoot { + redo(context: CommandExecutionContext): CommandResult { return context.root; } } @@ -75,11 +75,17 @@ export class HoverFeedbackCommand extends SystemCommand { * that element. This action is sent from the client to the model source, e.g. a DiagramServer. * The response is a SetPopupModelAction. */ -export class RequestPopupModelAction implements Action { +export class RequestPopupModelAction implements RequestAction { static readonly KIND = 'requestPopupModel'; readonly kind = RequestPopupModelAction.KIND; - constructor(public readonly elementId: string, public readonly bounds: Bounds) { + constructor(public readonly elementId: string, + public readonly bounds: Bounds, + public readonly requestId = '') {} + + /** Factory function to dispatch a request with the `IActionDispatcher` */ + static create(elementId: string, bounds: Bounds): RequestAction { + return new RequestPopupModelAction(elementId, bounds, generateRequestId()); } } @@ -87,36 +93,37 @@ export class RequestPopupModelAction implements Action { * Sent from the model source to the client to display a popup in response to a RequestPopupModelAction. * This action can also be used to remove any existing popup by choosing EMPTY_ROOT as root element. */ -export class SetPopupModelAction implements Action { - readonly kind = SetPopupModelCommand.KIND; +export class SetPopupModelAction implements ResponseAction { + static readonly KIND = 'setPopupModel'; + readonly kind = SetPopupModelAction.KIND; - constructor(public readonly newRoot: SModelRootSchema) { - } + constructor(public readonly newRoot: SModelRootSchema, + public readonly responseId = '') {} } @injectable() export class SetPopupModelCommand extends PopupCommand { - static readonly KIND = 'setPopupModel'; + static readonly KIND = SetPopupModelAction.KIND; oldRoot: SModelRoot; newRoot: SModelRoot; - constructor(@inject(TYPES.Action) public action: SetPopupModelAction) { + constructor(@inject(TYPES.Action) protected readonly action: SetPopupModelAction) { super(); } - execute(context: CommandExecutionContext): SModelRoot { + execute(context: CommandExecutionContext): CommandResult { this.oldRoot = context.root; this.newRoot = context.modelFactory.createRoot(this.action.newRoot); return this.newRoot; } - undo(context: CommandExecutionContext): SModelRoot { + undo(context: CommandExecutionContext): CommandResult { return this.oldRoot; } - redo(context: CommandExecutionContext): SModelRoot { + redo(context: CommandExecutionContext): CommandResult { return this.newRoot; } } diff --git a/src/features/hover/initializer.ts b/src/features/hover/initializer.ts index 05337524..133bc4a1 100644 --- a/src/features/hover/initializer.ts +++ b/src/features/hover/initializer.ts @@ -21,7 +21,7 @@ import { ICommand } from "../../base/commands/command"; import { SetPopupModelAction, SetPopupModelCommand } from "./hover"; import { EMPTY_ROOT } from "../../base/model/smodel-factory"; import { CenterCommand, FitToScreenCommand } from "../viewport/center-fit"; -import { ViewportCommand } from "../viewport/viewport"; +import { SetViewportCommand } from "../viewport/viewport"; import { MoveCommand } from "../move/move"; class ClosePopupActionHandler implements IActionHandler { @@ -42,7 +42,7 @@ export class PopupActionHandlerInitializer implements IActionHandlerInitializer const closePopupActionHandler = new ClosePopupActionHandler(); registry.register(FitToScreenCommand.KIND, closePopupActionHandler); registry.register(CenterCommand.KIND, closePopupActionHandler); - registry.register(ViewportCommand.KIND, closePopupActionHandler); + registry.register(SetViewportCommand.KIND, closePopupActionHandler); registry.register(SetPopupModelCommand.KIND, closePopupActionHandler); registry.register(MoveCommand.KIND, closePopupActionHandler); } diff --git a/src/features/move/move.ts b/src/features/move/move.ts index 2394ad8c..e612fbcd 100644 --- a/src/features/move/move.ts +++ b/src/features/move/move.ts @@ -18,7 +18,7 @@ import { inject, injectable, optional } from "inversify"; import { VNode } from "snabbdom/vnode"; import { Action } from "../../base/actions/action"; import { Animation, CompoundAnimation } from "../../base/animations/animation"; -import { CommandExecutionContext, ICommand, MergeableCommand } from "../../base/commands/command"; +import { CommandExecutionContext, ICommand, MergeableCommand, CommandResult } from "../../base/commands/command"; import { SChildElement, SModelElement, SModelRoot } from '../../base/model/smodel'; import { findParentByFeature, translatePoint } from "../../base/model/smodel-utils"; import { TYPES } from "../../base/types"; @@ -73,14 +73,14 @@ export class MoveCommand extends MergeableCommand { @inject(EdgeRouterRegistry)@optional() edgeRouterRegistry?: EdgeRouterRegistry; - resolvedMoves: Map = new Map; - edgeMementi: EdgeMemento[] = []; + protected resolvedMoves: Map = new Map; + protected edgeMementi: EdgeMemento[] = []; - constructor(@inject(TYPES.Action) protected action: MoveAction) { + constructor(@inject(TYPES.Action) protected readonly action: MoveAction) { super(); } - execute(context: CommandExecutionContext) { + execute(context: CommandExecutionContext): CommandResult { const index = context.root.index; const edge2handleMoves = new Map(); const attachedEdgeShifts = new Map(); @@ -197,14 +197,14 @@ export class MoveCommand extends MergeableCommand { }); } - undo(context: CommandExecutionContext) { + undo(context: CommandExecutionContext): Promise { return new CompoundAnimation(context.root, context, [ new MoveAnimation(context.root, this.resolvedMoves, context, true), new MorphEdgesAnimation(context.root, this.edgeMementi, context, true) ]).start(); } - redo(context: CommandExecutionContext) { + redo(context: CommandExecutionContext): Promise { return new CompoundAnimation(context.root, context, [ new MoveAnimation(context.root, this.resolvedMoves, context, false), new MorphEdgesAnimation(context.root, this.edgeMementi, context, false) diff --git a/src/features/select/di.config.ts b/src/features/select/di.config.ts index a68b8e0f..3c7df9a5 100644 --- a/src/features/select/di.config.ts +++ b/src/features/select/di.config.ts @@ -16,12 +16,13 @@ import { ContainerModule } from "inversify"; import { TYPES } from "../../base/types"; -import { SelectCommand, SelectAllCommand, SelectKeyboardListener, SelectMouseListener } from "./select"; +import { SelectCommand, SelectAllCommand, SelectKeyboardListener, SelectMouseListener, GetSelectionCommand } from "./select"; import { configureCommand } from "../../base/commands/command-registration"; const selectModule = new ContainerModule((bind, _unbind, isBound) => { configureCommand({ bind, isBound }, SelectCommand); configureCommand({ bind, isBound }, SelectAllCommand); + configureCommand({ bind, isBound }, GetSelectionCommand); bind(TYPES.KeyListener).to(SelectKeyboardListener); bind(TYPES.MouseListener).to(SelectMouseListener); }); diff --git a/src/features/select/select.ts b/src/features/select/select.ts index 0c634cf7..3b5666af 100644 --- a/src/features/select/select.ts +++ b/src/features/select/select.ts @@ -16,8 +16,9 @@ import { inject, injectable, optional } from 'inversify'; import { VNode } from "snabbdom/vnode"; -import { Action } from "../../base/actions/action"; +import { Action, RequestAction, ResponseAction, generateRequestId } from "../../base/actions/action"; import { Command, CommandExecutionContext } from "../../base/commands/command"; +import { ModelRequestCommand } from '../../base/commands/request-command'; import { SChildElement, SModelElement, SModelRoot, SParentElement } from '../../base/model/smodel'; import { findParentByFeature } from "../../base/model/smodel-utils"; import { TYPES } from '../../base/types'; @@ -41,7 +42,8 @@ import { isSelectable } from "./model"; * Furthermore, the server can send such an action to the client in order to change the selection programmatically. */ export class SelectAction implements Action { - kind = SelectCommand.KIND; + static readonly KIND = 'elementSelected'; + kind = SelectAction.KIND; constructor(public readonly selectedElementsIDs: string[] = [], public readonly deselectedElementsIDs: string[] = []) { @@ -52,7 +54,8 @@ export class SelectAction implements Action { * Programmatic action for selecting or deselecting all elements. */ export class SelectAllAction implements Action { - kind = SelectAllCommand.KIND; + static readonly KIND = 'allSelected'; + kind = SelectAllAction.KIND; /** * If `select` is true, all elements are selected, othewise they are deselected. @@ -61,6 +64,26 @@ export class SelectAllAction implements Action { } } +export class GetSelectionAction implements RequestAction { + static readonly KIND = 'getSelection'; + kind = GetSelectionAction.KIND; + + constructor(public readonly requestId: string = '') {} + + /** Factory function to dispatch a request with the `IActionDispatcher` */ + static create(): RequestAction { + return new GetSelectionAction(generateRequestId()); + } +} + +export class SelectionResult implements ResponseAction { + static readonly KIND = 'selectionResult'; + kind = SelectionResult.KIND; + + constructor(public readonly selectedElementsIDs: string[] = [], + public readonly responseId: string) {} +} + export type ElementSelection = { element: SChildElement parent: SParentElement @@ -139,11 +162,11 @@ export class SelectCommand extends Command { @injectable() export class SelectAllCommand extends Command { - static readonly KIND = 'allSelected'; + static readonly KIND = SelectAllAction.KIND; protected previousSelection: Record = {}; - constructor(@inject(TYPES.Action) public action: SelectAllAction) { + constructor(@inject(TYPES.Action) protected readonly action: SelectAllAction) { super(); } @@ -259,6 +282,25 @@ export class SelectMouseListener extends MouseListener { } } +@injectable() +export class GetSelectionCommand extends ModelRequestCommand { + static readonly KIND = GetSelectionAction.KIND; + + protected previousSelection: Record = {}; + + constructor(@inject(TYPES.Action) protected readonly action: GetSelectionAction) { + super(); + } + + protected retrieveResult(context: CommandExecutionContext): ResponseAction { + const selection = context.root.index.all() + .filter(e => isSelectable(e) && e.selected) + .map(e => e.id); + return new SelectionResult(toArray(selection), this.action.requestId); + } + +} + export class SelectKeyboardListener extends KeyListener { keyDown(element: SModelElement, event: KeyboardEvent): Action[] { if (matchesKeystroke(event, 'KeyA', 'ctrlCmd')) { diff --git a/src/features/update/update-model.ts b/src/features/update/update-model.ts index 1f218d9f..2846a1d8 100644 --- a/src/features/update/update-model.ts +++ b/src/features/update/update-model.ts @@ -36,7 +36,8 @@ import { TYPES } from "../../base/types"; * this behaves the same as a SetModelAction. The transition from the old model to the new one can be animated. */ export class UpdateModelAction implements Action { - readonly kind = UpdateModelCommand.KIND; + static readonly KIND = 'updateModel'; + readonly kind = UpdateModelAction.KIND; public readonly newRoot?: SModelRootSchema; public readonly matches?: Match[]; @@ -58,12 +59,12 @@ export interface UpdateAnimationData { @injectable() export class UpdateModelCommand extends Command { - static readonly KIND = 'updateModel'; + static readonly KIND = UpdateModelAction.KIND; oldRoot: SModelRoot; newRoot: SModelRoot; - constructor(@inject(TYPES.Action) public action: UpdateModelAction) { + constructor(@inject(TYPES.Action) protected readonly action: UpdateModelAction) { super(); } diff --git a/src/features/viewport/center-fit.ts b/src/features/viewport/center-fit.ts index 77b784cd..39c8582c 100644 --- a/src/features/viewport/center-fit.ts +++ b/src/features/viewport/center-fit.ts @@ -18,7 +18,7 @@ import { Bounds, center, combine, isValidDimension } from "../../utils/geometry" import { matchesKeystroke } from "../../utils/keyboard"; import { SChildElement } from '../../base/model/smodel'; import { Action } from "../../base/actions/action"; -import { Command, CommandExecutionContext } from "../../base/commands/command"; +import { Command, CommandExecutionContext, CommandResult } from "../../base/commands/command"; import { SModelElement, SModelRoot } from "../../base/model/smodel"; import { KeyListener } from "../../base/views/key-tool"; import { isBoundsAware } from "../bounds/model"; @@ -35,7 +35,8 @@ import { TYPES } from "../../base/types"; * viewport change programmatically. */ export class CenterAction implements Action { - readonly kind = CenterCommand.KIND; + static readonly KIND = 'center'; + readonly kind = CenterAction.KIND; constructor(public readonly elementIds: string[], public readonly animate: boolean = true) { @@ -49,7 +50,8 @@ export class CenterAction implements Action { * to perform such a viewport change programmatically. */ export class FitToScreenAction implements Action { - readonly kind = FitToScreenCommand.KIND; + static readonly KIND = 'fit'; + readonly kind = FitToScreenAction.KIND; constructor(public readonly elementIds: string[], public readonly padding?: number, @@ -117,12 +119,12 @@ export abstract class BoundsAwareViewportCommand extends Command { protected abstract getElementIds(): string[]; - execute(context: CommandExecutionContext) { + execute(context: CommandExecutionContext): CommandResult { this.initialize(context.root); return this.redo(context); } - undo(context: CommandExecutionContext) { + undo(context: CommandExecutionContext): CommandResult { const model = context.root; if (isViewport(model) && this.newViewport !== undefined && !this.equal(this.newViewport, this.oldViewport)) { if (this.animate) @@ -135,7 +137,7 @@ export abstract class BoundsAwareViewportCommand extends Command { return model; } - redo(context: CommandExecutionContext) { + redo(context: CommandExecutionContext): CommandResult { const model = context.root; if (isViewport(model) && this.newViewport !== undefined && !this.equal(this.newViewport, this.oldViewport)) { if (this.animate) { @@ -154,7 +156,7 @@ export abstract class BoundsAwareViewportCommand extends Command { } export class CenterCommand extends BoundsAwareViewportCommand { - static readonly KIND = 'center'; + static readonly KIND = CenterAction.KIND; constructor(@inject(TYPES.Action) protected action: CenterAction) { super(action.animate); @@ -180,9 +182,9 @@ export class CenterCommand extends BoundsAwareViewportCommand { } export class FitToScreenCommand extends BoundsAwareViewportCommand { - static readonly KIND = 'fit'; + static readonly KIND = FitToScreenAction.KIND; - constructor(@inject(TYPES.Action) protected action: FitToScreenAction) { + constructor(@inject(TYPES.Action) protected readonly action: FitToScreenAction) { super(action.animate); } diff --git a/src/features/viewport/di.config.ts b/src/features/viewport/di.config.ts index f2200c38..614d39c2 100644 --- a/src/features/viewport/di.config.ts +++ b/src/features/viewport/di.config.ts @@ -17,7 +17,7 @@ import { ContainerModule } from "inversify"; import { TYPES } from "../../base/types"; import { CenterCommand, CenterKeyboardListener, FitToScreenCommand } from "./center-fit"; -import { ViewportCommand } from "./viewport"; +import { SetViewportCommand, GetViewportCommand } from "./viewport"; import { ScrollMouseListener } from "./scroll"; import { ZoomMouseListener } from "./zoom"; import { configureCommand } from "../../base/commands/command-registration"; @@ -25,7 +25,8 @@ import { configureCommand } from "../../base/commands/command-registration"; const viewportModule = new ContainerModule((bind , _unbind, isBound) => { configureCommand({ bind, isBound }, CenterCommand); configureCommand({ bind, isBound }, FitToScreenCommand); - configureCommand({ bind, isBound }, ViewportCommand); + configureCommand({ bind, isBound }, SetViewportCommand); + configureCommand({ bind, isBound }, GetViewportCommand); bind(TYPES.KeyListener).to(CenterKeyboardListener); bind(TYPES.MouseListener).to(ScrollMouseListener); bind(TYPES.MouseListener).to(ZoomMouseListener); diff --git a/src/features/viewport/scroll.ts b/src/features/viewport/scroll.ts index 912ddd1c..43c84cb2 100644 --- a/src/features/viewport/scroll.ts +++ b/src/features/viewport/scroll.ts @@ -20,7 +20,7 @@ import { MouseListener } from "../../base/views/mouse-tool"; import { Action } from "../../base/actions/action"; import { SModelExtension } from "../../base/model/smodel-extension"; import { findParentByFeature } from "../../base/model/smodel-utils"; -import { ViewportAction } from "./viewport"; +import { SetViewportAction } from "./viewport"; import { isViewport, Viewport } from "./model"; import { isMoveable } from "../move/model"; import { SRoutingHandle } from "../routing/model"; @@ -65,7 +65,7 @@ export class ScrollMouseListener extends MouseListener { zoom: viewport.zoom }; this.lastScrollPosition = {x: event.pageX, y: event.pageY}; - return [new ViewportAction(viewport.id, newViewport, false)]; + return [new SetViewportAction(viewport.id, newViewport, false)]; } } return []; diff --git a/src/features/viewport/viewport-root.ts b/src/features/viewport/viewport-root.ts index 02b34b8a..7d288e03 100644 --- a/src/features/viewport/viewport-root.ts +++ b/src/features/viewport/viewport-root.ts @@ -17,16 +17,15 @@ import { Bounds, Point, isBounds, isValidDimension } from "../../utils/geometry"; import { SModelRoot, SModelIndex, SModelElement } from '../../base/model/smodel'; import { Viewport, viewportFeature } from "./model"; -import { Exportable, exportFeature } from "../export/model"; +import { exportFeature } from "../export/model"; /** * Model root element that defines a viewport, so it transforms the coordinate system with * a `scroll` translation and a `zoom` scaling. */ -export class ViewportRootElement extends SModelRoot implements Viewport, Exportable { +export class ViewportRootElement extends SModelRoot implements Viewport { scroll: Point = { x: 0, y: 0 }; zoom: number = 1; - export: boolean = false; constructor(index?: SModelIndex) { super(index); diff --git a/src/features/viewport/viewport.spec.ts b/src/features/viewport/viewport.spec.ts index 6f6a3e96..c7bc605d 100644 --- a/src/features/viewport/viewport.spec.ts +++ b/src/features/viewport/viewport.spec.ts @@ -24,7 +24,7 @@ import { ConsoleLogger } from '../../utils/logging'; import { AnimationFrameSyncer } from '../../base/animations/animation-frame-syncer'; import { CommandExecutionContext } from '../../base/commands/command'; import { SGraphFactory } from '../../graph/sgraph-factory'; -import { ViewportAction, ViewportCommand } from './viewport'; +import { SetViewportAction, SetViewportCommand } from './viewport'; import { Viewport } from './model'; import { ViewportRootElement } from './viewport-root'; import defaultModule from "../../base/di.config"; @@ -43,8 +43,8 @@ describe('BoundsAwareViewportCommand', () => { const newViewportData: Viewport = { scroll: { x: 100, y: 100 }, zoom: 10 }; - const viewportAction = new ViewportAction(viewport.id, newViewportData, false); - const cmd = new ViewportCommand(viewportAction); + const viewportAction = new SetViewportAction(viewport.id, newViewportData, false); + const cmd = new SetViewportCommand(viewportAction); const context: CommandExecutionContext = { root: viewport, diff --git a/src/features/viewport/viewport.ts b/src/features/viewport/viewport.ts index a77d5a0d..1de40eab 100644 --- a/src/features/viewport/viewport.ts +++ b/src/features/viewport/viewport.ts @@ -14,16 +14,19 @@ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ +import { ORIGIN_POINT, Bounds } from "../../utils/geometry"; import { SModelElement, SModelRoot } from "../../base/model/smodel"; -import { Action } from "../../base/actions/action"; -import { MergeableCommand, ICommand, CommandExecutionContext } from "../../base/commands/command"; +import { Action, RequestAction, ResponseAction, generateRequestId } from "../../base/actions/action"; +import { MergeableCommand, ICommand, CommandExecutionContext, CommandResult } from "../../base/commands/command"; import { Animation } from "../../base/animations/animation"; import { isViewport, Viewport } from "./model"; import { injectable, inject } from "inversify"; import { TYPES } from "../../base/types"; +import { ModelRequestCommand } from "../../base/commands/request-command"; -export class ViewportAction implements Action { - kind = ViewportCommand.KIND; +export class SetViewportAction implements Action { + static readonly KIND = 'viewport'; + kind = SetViewportAction.KIND; constructor(public readonly elementId: string, public readonly newViewport: Viewport, @@ -31,20 +34,41 @@ export class ViewportAction implements Action { } } +export class GetViewportAction implements RequestAction { + static readonly KIND = 'getViewport'; + kind = GetViewportAction.KIND; + + constructor(public readonly requestId: string = '') {} + + /** Factory function to dispatch a request with the `IActionDispatcher` */ + static create(): RequestAction { + return new GetViewportAction(generateRequestId()); + } +} + +export class ViewportResult implements ResponseAction { + static readonly KIND = 'viewportResult'; + kind = ViewportResult.KIND; + + constructor(public readonly viewport: Viewport, + public readonly canvasBounds: Bounds, + public readonly responseId: string) {} +} + @injectable() -export class ViewportCommand extends MergeableCommand { - static readonly KIND = 'viewport'; +export class SetViewportCommand extends MergeableCommand { + static readonly KIND = SetViewportAction.KIND; protected element: SModelElement & Viewport; protected oldViewport: Viewport; protected newViewport: Viewport; - constructor(@inject(TYPES.Action) protected action: ViewportAction) { + constructor(@inject(TYPES.Action) protected readonly action: SetViewportAction) { super(); this.newViewport = action.newViewport; } - execute( context: CommandExecutionContext) { + execute(context: CommandExecutionContext): CommandResult { const model = context.root; const element = model.index.getById(this.action.elementId); if (element && isViewport(element)) { @@ -63,16 +87,16 @@ export class ViewportCommand extends MergeableCommand { return model; } - undo(context: CommandExecutionContext) { + undo(context: CommandExecutionContext): CommandResult { return new ViewportAnimation(this.element, this.newViewport, this.oldViewport, context).start(); } - redo(context: CommandExecutionContext) { + redo(context: CommandExecutionContext): CommandResult { return new ViewportAnimation(this.element, this.oldViewport, this.newViewport, context).start(); } - merge(command: ICommand, context: CommandExecutionContext) { - if (!this.action.animate && command instanceof ViewportCommand && this.element === command.element) { + merge(command: ICommand, context: CommandExecutionContext): boolean { + if (!this.action.animate && command instanceof SetViewportCommand && this.element === command.element) { this.newViewport = command.newViewport; return true; } @@ -80,6 +104,25 @@ export class ViewportCommand extends MergeableCommand { } } +export class GetViewportCommand extends ModelRequestCommand { + static readonly KIND = GetViewportAction.KIND; + + constructor(@inject(TYPES.Action) protected readonly action: GetViewportAction) { + super(); + } + + protected retrieveResult(context: CommandExecutionContext): ResponseAction { + const elem = context.root; + let viewport: Viewport; + if (isViewport(elem)) { + viewport = { scroll: elem.scroll, zoom: elem.zoom }; + } else { + viewport = { scroll: ORIGIN_POINT, zoom: 1 }; + } + return new ViewportResult(viewport, elem.canvasBounds, this.action.requestId); + } +} + export class ViewportAnimation extends Animation { protected zoomFactor: number; diff --git a/src/features/viewport/zoom.ts b/src/features/viewport/zoom.ts index 6f440f3d..10ad9567 100644 --- a/src/features/viewport/zoom.ts +++ b/src/features/viewport/zoom.ts @@ -19,7 +19,7 @@ import { MouseListener } from "../../base/views/mouse-tool"; import { Action } from "../../base/actions/action"; import { SModelExtension } from "../../base/model/smodel-extension"; import { findParentByFeature } from "../../base/model/smodel-utils"; -import { ViewportAction } from "./viewport"; +import { SetViewportAction } from "./viewport"; import { isViewport, Viewport } from "./model"; export interface Zoomable extends SModelExtension { @@ -53,7 +53,7 @@ export class ZoomMouseListener extends MouseListener { }, zoom: viewport.zoom * newZoom }; - return [new ViewportAction(viewport.id, newViewport, false)]; + return [new SetViewportAction(viewport.id, newViewport, false)]; } return []; } diff --git a/src/model-source/commit-model.ts b/src/model-source/commit-model.ts index 3cf06f48..6053d988 100644 --- a/src/model-source/commit-model.ts +++ b/src/model-source/commit-model.ts @@ -42,7 +42,7 @@ export class CommitModelCommand extends SystemCommand { originalModel: SModelRootSchema; newModel: SModelRootSchema; - constructor(@inject(TYPES.Action) action: CommitModelAction) { + constructor(@inject(TYPES.Action) protected readonly action: CommitModelAction) { super(); } diff --git a/src/model-source/local-model-source.spec.ts b/src/model-source/local-model-source.spec.ts index 4f0dcff3..a926739c 100644 --- a/src/model-source/local-model-source.spec.ts +++ b/src/model-source/local-model-source.spec.ts @@ -18,9 +18,10 @@ import "reflect-metadata"; import "mocha"; import { expect } from "chai"; import { Container, injectable } from "inversify"; +import { Deferred } from "../utils/async"; import { TYPES } from "../base/types"; import { SModelRootSchema } from "../base/model/smodel"; -import { Action } from "../base/actions/action"; +import { Action, RequestAction, ResponseAction, isResponseAction } from "../base/actions/action"; import { SetModelAction } from "../base/features/set-model"; import { ViewerOptions, overrideViewerOptions } from "../base/views/viewer-options"; import { ComputedBoundsAction, RequestBoundsAction } from "../features/bounds/bounds-manipulation"; @@ -34,6 +35,7 @@ describe('LocalModelSource', () => { @injectable() class MockActionDispatcher implements IActionDispatcher { readonly actions: Action[] = []; + readonly requests: { requestId: string, deferred: Deferred }[] = []; dispatchAll(actions: Action[]): Promise { for (const action of actions) { @@ -43,9 +45,27 @@ describe('LocalModelSource', () => { } dispatch(action: Action): Promise { + if (isResponseAction(action)) { + const request = this.requests.find(r => r.requestId === action.responseId); + if (request !== undefined) { + request.deferred.resolve(action); + return Promise.resolve(); + } + return Promise.reject(`No matching request for response ${action}`); + } this.actions.push(action); return Promise.resolve(); } + + request, Res extends ResponseAction>(action: Req): Promise { + if (!action.requestId) { + return Promise.reject(new Error('Request without requestId')); + } + const deferred = new Deferred(); + this.requests.push({ requestId: action.requestId, deferred }); + this.dispatch(action); + return deferred.promise; + } } function setup(options: Partial) { @@ -88,7 +108,7 @@ describe('LocalModelSource', () => { expect(action1.newRoot).to.equal(root2); }); - it('requests bounds in dynamic mode', () => { + it('requests bounds in dynamic mode', async () => { const container = setup({ needsClientLayout: true }); const modelSource = container.get(TYPES.ModelSource); const dispatcher = container.get(TYPES.IActionDispatcher); @@ -103,13 +123,14 @@ describe('LocalModelSource', () => { } ] }; - modelSource.setModel(root1); - modelSource.handle(new ComputedBoundsAction([ + const promise1 = modelSource.setModel(root1); + expect(dispatcher.requests).to.have.lengthOf(1); + dispatcher.dispatch(new ComputedBoundsAction([ { elementId: 'child1', newBounds: { x: 10, y: 10, width: 20, height: 20 } } - ])); + ], undefined, undefined, dispatcher.requests[0].requestId)); const root2: SModelRootSchema = { type: 'root', id: 'root', @@ -120,13 +141,17 @@ describe('LocalModelSource', () => { } ] }; - modelSource.updateModel(root2); - modelSource.handle(new ComputedBoundsAction([ + await promise1; + + const promise2 = modelSource.updateModel(root2); + expect(dispatcher.requests).to.have.lengthOf(2); + dispatcher.dispatch(new ComputedBoundsAction([ { elementId: 'bar', newBounds: { x: 10, y: 10, width: 20, height: 20 } } - ])); + ], undefined, undefined, dispatcher.requests[1].requestId)); + await promise2; expect(dispatcher.actions).to.have.lengthOf(4); const action0 = dispatcher.actions[0] as RequestBoundsAction; @@ -257,9 +282,10 @@ describe('LocalModelSource', () => { children: [{ type: 'node', id: 'child1' }] }; const promise1 = modelSource.setModel(root1); - modelSource.handle(new ComputedBoundsAction([ + expect(dispatcher.requests).to.have.lengthOf(1); + dispatcher.dispatch(new ComputedBoundsAction([ { elementId: 'child1', newBounds: { x: 10, y: 10, width: 20, height: 20 } } - ])); + ], undefined, undefined, dispatcher.requests[0].requestId)); await promise1; const root2: SModelRootSchema = { @@ -268,9 +294,10 @@ describe('LocalModelSource', () => { children: [{ type: 'node', id: 'bar' }] }; const promise2 = modelSource.updateModel(root2); - modelSource.handle(new ComputedBoundsAction([ + expect(dispatcher.requests).to.have.lengthOf(2); + dispatcher.dispatch(new ComputedBoundsAction([ { elementId: 'bar', newBounds: { x: 10, y: 10, width: 20, height: 20 } } - ])); + ], undefined, undefined, dispatcher.requests[1].requestId)); await promise2; expect(dispatcher.actions).to.have.lengthOf(4); diff --git a/src/model-source/local-model-source.ts b/src/model-source/local-model-source.ts index 491412db..b1cdb0a9 100644 --- a/src/model-source/local-model-source.ts +++ b/src/model-source/local-model-source.ts @@ -16,22 +16,25 @@ import { saveAs } from 'file-saver'; import { inject, injectable, optional } from "inversify"; +import { Bounds, Point } from "../utils/geometry"; +import { ILogger } from "../utils/logging"; +import { FluentIterable } from '../utils/iterable'; +import { TYPES } from "../base/types"; import { Action } from "../base/actions/action"; import { ActionHandlerRegistry } from "../base/actions/action-handler"; import { RequestModelAction, SetModelAction } from "../base/features/set-model"; import { SModelElementSchema, SModelIndex, SModelRootSchema } from "../base/model/smodel"; import { findElement } from "../base/model/smodel-utils"; -import { TYPES } from "../base/types"; +import { EMPTY_ROOT } from '../base/model/smodel-factory'; import { ComputedBoundsAction, RequestBoundsAction } from '../features/bounds/bounds-manipulation'; +import { GetViewportAction } from '../features/viewport/viewport'; +import { Viewport } from '../features/viewport/model'; import { ExportSvgAction } from '../features/export/svg-exporter'; import { RequestPopupModelAction, SetPopupModelAction } from "../features/hover/hover"; import { applyMatches, Match } from "../features/update/model-matching"; import { UpdateModelAction } from "../features/update/update-model"; -import { Deferred } from "../utils/async"; -import { Bounds, Point } from "../utils/geometry"; -import { ILogger } from "../utils/logging"; +import { GetSelectionAction } from '../features/select/select'; import { ModelSource } from "./model-source"; -import { EMPTY_ROOT } from '../base/model/smodel-factory'; /** * A model source that allows to set and modify the model through function calls. @@ -53,14 +56,6 @@ export class LocalModelSource extends ModelSource { */ protected lastSubmittedModelType: string; - /** - * When client layout is active, model updates are not applied immediately. Instead the - * model is rendered on a hidden canvas first to derive actual bounds. The promises listed - * here are resolved after the new bounds have been applied and the new model state has - * been actually applied to the visible canvas. - */ - protected pendingUpdates: Deferred[] = []; - get model(): SModelRootSchema { return this.currentRoot; } @@ -105,18 +100,39 @@ export class LocalModelSource extends ModelSource { } } + /** + * Get the current selection from the model. + */ + async getSelection(): Promise> { + const res = await this.actionDispatcher.request(GetSelectionAction.create()); + const index = new SModelIndex(); + index.add(this.currentRoot); + return index.all().filter(e => res.selectedElementsIDs.indexOf(e.id) >= 0); + } + + /** + * Get the current viewport from the model. + */ + async getViewport(): Promise { + const res = await this.actionDispatcher.request(GetViewportAction.create()); + return { + scroll: res.viewport.scroll, + zoom: res.viewport.zoom, + canvasBounds: res.canvasBounds + }; + } + /** * If client layout is active, run a `RequestBoundsAction` and wait for the resulting * `ComputedBoundsAction`, otherwise call `doSubmitModel(…)` directly. */ - protected submitModel(newRoot: SModelRootSchema, update: boolean | Match[]): Promise { + protected async submitModel(newRoot: SModelRootSchema, update: boolean | Match[], cause?: Action): Promise { if (this.viewerOptions.needsClientLayout) { - const deferred = new Deferred(); - this.pendingUpdates.push(deferred); - this.actionDispatcher.dispatch(new RequestBoundsAction(newRoot)); - return deferred.promise; + const computedBounds = await this.actionDispatcher.request(RequestBoundsAction.create(newRoot)); + const index = this.handleComputedBounds(computedBounds); + await this.doSubmitModel(newRoot, true, cause, index); } else { - return this.doSubmitModel(newRoot, update); + await this.doSubmitModel(newRoot, update, cause); } } @@ -124,7 +140,8 @@ export class LocalModelSource extends ModelSource { * Submit the given model with an `UpdateModelAction` or a `SetModelAction` depending on the * `update` argument. If available, the model layout engine is invoked first. */ - protected async doSubmitModel(newRoot: SModelRootSchema, update: boolean | Match[], index?: SModelIndex): Promise { + protected async doSubmitModel(newRoot: SModelRootSchema, update: boolean | Match[], + cause?: Action, index?: SModelIndex): Promise { if (this.layoutEngine !== undefined) { try { const layoutResult = this.layoutEngine.layout(newRoot, index); @@ -139,15 +156,15 @@ export class LocalModelSource extends ModelSource { const lastSubmittedModelType = this.lastSubmittedModelType; this.lastSubmittedModelType = newRoot.type; - const updates = this.pendingUpdates; - this.pendingUpdates = []; - if (update && newRoot.type === lastSubmittedModelType) { + if (cause && cause.kind === RequestModelAction.KIND && (cause as RequestModelAction).requestId) { + const request = cause as RequestModelAction; + await this.actionDispatcher.dispatch(new SetModelAction(newRoot, request.requestId)); + } else if (update && newRoot.type === lastSubmittedModelType) { const input = Array.isArray(update) ? update : newRoot; await this.actionDispatcher.dispatch(new UpdateModelAction(input)); } else { await this.actionDispatcher.dispatch(new SetModelAction(newRoot)); } - updates.forEach(d => d.resolve()); } /** @@ -232,10 +249,10 @@ export class LocalModelSource extends ModelSource { } protected handleRequestModel(action: RequestModelAction): void { - this.submitModel(this.currentRoot, false); + this.submitModel(this.currentRoot, false, action); } - protected handleComputedBounds(action: ComputedBoundsAction): void { + protected handleComputedBounds(action: ComputedBoundsAction): SModelIndex { const root = this.currentRoot; const index = new SModelIndex(); index.add(root); @@ -251,7 +268,7 @@ export class LocalModelSource extends ModelSource { this.applyAlignment(element, a.newAlignment); } } - this.doSubmitModel(root, true, index); + return index; } protected applyBounds(element: SModelElementSchema, newBounds: Bounds) { @@ -271,7 +288,7 @@ export class LocalModelSource extends ModelSource { const popupRoot = this.popupModelProvider.getPopupModel(action, element); if (popupRoot !== undefined) { popupRoot.canvasBounds = action.bounds; - this.actionDispatcher.dispatch(new SetPopupModelAction(popupRoot)); + this.actionDispatcher.dispatch(new SetPopupModelAction(popupRoot, action.requestId)); } } } From 57f781c7ff79c4bd268283c38cf710436319fcbe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miro=20Sp=C3=B6nemann?= Date: Tue, 3 Sep 2019 12:37:27 +0200 Subject: [PATCH 2/2] Renamed CommandResult to CommandReturn --- examples/circlegraph/src/views.tsx | 5 ++- examples/mindmap/src/popup.ts | 8 ++-- src/base/actions/action-dispatcher.spec.ts | 8 ++-- src/base/commands/command-stack.spec.ts | 26 ++++++------- src/base/commands/command-stack.ts | 38 +++++++++---------- src/base/commands/command.ts | 24 ++++++------ src/base/commands/request-command.ts | 14 +++---- src/base/features/initialize-canvas.ts | 8 ++-- .../ui-extensions/ui-extension-registry.ts | 14 +++---- src/features/bounds/bounds-manipulation.ts | 12 +++--- src/features/edit/create.ts | 8 ++-- src/features/edit/delete.ts | 8 ++-- src/features/edit/edit-label.ts | 8 ++-- src/features/edit/edit-routing.ts | 8 ++-- src/features/edit/reconnect.ts | 8 ++-- src/features/export/export.ts | 8 ++-- src/features/hover/hover.ts | 14 +++---- src/features/move/move.ts | 4 +- src/features/select/select.ts | 3 ++ src/features/update/update-model.ts | 10 ++--- src/features/viewport/center-fit.ts | 8 ++-- src/features/viewport/viewport.ts | 11 ++++-- src/model-source/commit-model.ts | 10 ++--- src/model-source/diagram-server.ts | 2 +- 24 files changed, 137 insertions(+), 130 deletions(-) diff --git a/examples/circlegraph/src/views.tsx b/examples/circlegraph/src/views.tsx index 23669a54..c2fb2292 100644 --- a/examples/circlegraph/src/views.tsx +++ b/examples/circlegraph/src/views.tsx @@ -34,7 +34,8 @@ export class CircleNodeView implements IView { ; } - protected getRadius(node: SNode) { - return 40; + protected getRadius(node: SNode): number { + const d = Math.min(node.size.width, node.size.height); + return d > 0 ? d / 2 : 0; } } diff --git a/examples/mindmap/src/popup.ts b/examples/mindmap/src/popup.ts index 9eb08cf7..0e143018 100644 --- a/examples/mindmap/src/popup.ts +++ b/examples/mindmap/src/popup.ts @@ -18,7 +18,7 @@ import { inject, injectable } from "inversify"; import { TYPES, SModelElementSchema, SModelRootSchema, RequestPopupModelAction, MouseListener, SModelElement, Action, LocalModelSource, SNodeSchema, SetPopupModelAction, EMPTY_ROOT, - Point, Command, CommandExecutionContext, CommandResult, SChildElement, FadeAnimation, + Point, Command, CommandExecutionContext, CommandReturn, SChildElement, FadeAnimation, isFadeable, isLocateable, isBoundsAware, subtract, IPopupModelProvider } from "../../../src"; import { PopupButtonSchema, PopupButton } from "./model"; @@ -108,7 +108,7 @@ export class AddElementCommand extends Command { super(); } - execute(context: CommandExecutionContext): CommandResult { + execute(context: CommandExecutionContext): CommandReturn { const newElement = context.modelFactory.createElement(this.action.newElement); context.root.add(newElement); this.initialize(newElement); @@ -130,7 +130,7 @@ export class AddElementCommand extends Command { } } - undo(context: CommandExecutionContext): CommandResult { + undo(context: CommandExecutionContext): CommandReturn { const element = context.root.index.getById(this.action.newElement.id); if (element instanceof SChildElement) { element.parent.remove(element); @@ -138,7 +138,7 @@ export class AddElementCommand extends Command { return context.root; } - redo(context: CommandExecutionContext): CommandResult { + redo(context: CommandExecutionContext): CommandReturn { return this.execute(context); } } diff --git a/src/base/actions/action-dispatcher.spec.ts b/src/base/actions/action-dispatcher.spec.ts index 26bbf297..b47b33f9 100644 --- a/src/base/actions/action-dispatcher.spec.ts +++ b/src/base/actions/action-dispatcher.spec.ts @@ -22,7 +22,7 @@ import { TYPES } from "../types"; import { EMPTY_BOUNDS } from '../../utils/geometry'; import { InitializeCanvasBoundsAction } from '../features/initialize-canvas'; import { RedoAction, UndoAction } from "../../features/undo-redo/undo-redo"; -import { Command, CommandExecutionContext, CommandResult, ICommand } from '../commands/command'; +import { Command, CommandExecutionContext, CommandReturn, ICommand } from '../commands/command'; import { ICommandStack } from "../commands/command-stack"; import { ActionDispatcher } from "./action-dispatcher"; import { Action } from "./action"; @@ -35,15 +35,15 @@ describe('ActionDispatcher', () => { class MockCommand extends Command { static KIND = 'mock'; - execute(context: CommandExecutionContext): CommandResult { + execute(context: CommandExecutionContext): CommandReturn { return context.root; } - undo(context: CommandExecutionContext): CommandResult { + undo(context: CommandExecutionContext): CommandReturn { return context.root; } - redo(context: CommandExecutionContext): CommandResult { + redo(context: CommandExecutionContext): CommandReturn { return context.root; } } diff --git a/src/base/commands/command-stack.spec.ts b/src/base/commands/command-stack.spec.ts index 380b3828..dc56fe9d 100644 --- a/src/base/commands/command-stack.spec.ts +++ b/src/base/commands/command-stack.spec.ts @@ -22,7 +22,7 @@ import { TYPES } from "../types"; import defaultModule from "../di.config"; import { IViewerProvider } from "../views/viewer"; import { - Command, HiddenCommand, SystemCommand, CommandExecutionContext, CommandResult, MergeableCommand, PopupCommand + Command, HiddenCommand, SystemCommand, CommandExecutionContext, CommandReturn, MergeableCommand, PopupCommand } from './command'; import { ICommandStack } from "./command-stack"; @@ -34,17 +34,17 @@ class TestCommand extends Command { super(); } - execute(context: CommandExecutionContext): CommandResult { + execute(context: CommandExecutionContext): CommandReturn { operations.push('exec ' + this.name); return context.root; } - undo(context: CommandExecutionContext): CommandResult { + undo(context: CommandExecutionContext): CommandReturn { operations.push('undo ' + this.name); return context.root; } - redo(context: CommandExecutionContext): CommandResult { + redo(context: CommandExecutionContext): CommandReturn { operations.push('redo ' + this.name); return context.root; } @@ -56,17 +56,17 @@ class TestSystemCommand extends SystemCommand { super(); } - execute(context: CommandExecutionContext): CommandResult { + execute(context: CommandExecutionContext): CommandReturn { operations.push('exec ' + this.name); return context.root; } - undo(context: CommandExecutionContext): CommandResult { + undo(context: CommandExecutionContext): CommandReturn { operations.push('undo ' + this.name); return context.root; } - redo(context: CommandExecutionContext): CommandResult { + redo(context: CommandExecutionContext): CommandReturn { operations.push('redo ' + this.name); return context.root; } @@ -77,17 +77,17 @@ class TestMergeableCommand extends MergeableCommand { super(); } - execute(context: CommandExecutionContext): CommandResult { + execute(context: CommandExecutionContext): CommandReturn { operations.push('exec ' + this.name); return context.root; } - undo(context: CommandExecutionContext): CommandResult { + undo(context: CommandExecutionContext): CommandReturn { operations.push('undo ' + this.name); return context.root; } - redo(context: CommandExecutionContext): CommandResult { + redo(context: CommandExecutionContext): CommandReturn { operations.push('redo ' + this.name); return context.root; } @@ -117,17 +117,17 @@ class TestPopupCommand extends PopupCommand { super(); } - execute(context: CommandExecutionContext): CommandResult { + execute(context: CommandExecutionContext): CommandReturn { operations.push('exec ' + this.name); return context.root; } - undo(context: CommandExecutionContext): CommandResult { + undo(context: CommandExecutionContext): CommandReturn { operations.push('undo ' + this.name); return context.root; } - redo(context: CommandExecutionContext): CommandResult { + redo(context: CommandExecutionContext): CommandReturn { operations.push('redo ' + this.name); return context.root; } diff --git a/src/base/commands/command-stack.ts b/src/base/commands/command-stack.ts index ea8d0358..c6a5dffd 100644 --- a/src/base/commands/command-stack.ts +++ b/src/base/commands/command-stack.ts @@ -24,8 +24,8 @@ import { AnimationFrameSyncer } from "../animations/animation-frame-syncer"; import { IViewer, IViewerProvider } from "../views/viewer"; import { CommandStackOptions } from './command-stack-options'; import { - HiddenCommand, ICommand, CommandExecutionContext, CommandResult, SystemCommand, - MergeableCommand, PopupCommand, ResetCommand, DetailedCommandResult + HiddenCommand, ICommand, CommandExecutionContext, CommandReturn, SystemCommand, + MergeableCommand, PopupCommand, ResetCommand, CommandResult } from './command'; /** @@ -137,15 +137,15 @@ export class CommandStack implements ICommandStack { this.currentPromise = Promise.resolve({ main: { model: this.modelFactory.createRoot(EMPTY_ROOT), - isChanged: false, + modelChanged: false, }, hidden: { model: this.modelFactory.createRoot(EMPTY_ROOT), - isChanged: false, + modelChanged: false, }, popup: { model: this.modelFactory.createRoot(EMPTY_ROOT), - isChanged: false, + modelChanged: false, } }); } @@ -208,7 +208,7 @@ export class CommandStack implements ICommandStack { * command on the appropriate stack. */ protected handleCommand(command: ICommand, - operation: (context: CommandExecutionContext) => CommandResult, + operation: (context: CommandExecutionContext) => CommandReturn, beforeResolve: (command: ICommand, context: CommandExecutionContext) => void) { this.currentPromise = this.currentPromise.then(state => new Promise(resolve => { @@ -221,7 +221,7 @@ export class CommandStack implements ICommandStack { target = 'main'; const context = this.createContext(state[target].model); - let commandResult: CommandResult; + let commandResult: CommandReturn; try { commandResult = operation.call(command, context); } catch (error) { @@ -234,20 +234,20 @@ export class CommandStack implements ICommandStack { commandResult.then(newModel => { if (target === 'main') beforeResolve.call(this, command, context); - newState[target] = { model: newModel, isChanged: true }; + newState[target] = { model: newModel, modelChanged: true }; resolve(newState); }); } else if (commandResult instanceof SModelRoot) { if (target === 'main') beforeResolve.call(this, command, context); - newState[target] = { model: commandResult, isChanged: true }; + newState[target] = { model: commandResult, modelChanged: true }; resolve(newState); } else { if (target === 'main') beforeResolve.call(this, command, context); newState[target] = { model: commandResult.model, - isChanged: state[target].isChanged || commandResult.isChanged, + modelChanged: state[target].modelChanged || commandResult.modelChanged, cause: commandResult.cause }; resolve(newState); @@ -269,19 +269,19 @@ export class CommandStack implements ICommandStack { protected thenUpdate(): Promise { this.currentPromise = this.currentPromise.then(state => { const newState = copyState(state); - if (state.hidden.isChanged) { + if (state.hidden.modelChanged) { this.updateHidden(state.hidden.model, state.hidden.cause); - newState.hidden.isChanged = false; + newState.hidden.modelChanged = false; newState.hidden.cause = undefined; } - if (state.main.isChanged) { + if (state.main.modelChanged) { this.update(state.main.model, state.main.cause); - newState.main.isChanged = false; + newState.main.modelChanged = false; newState.main.cause = undefined; } - if (state.popup.isChanged) { + if (state.popup.modelChanged) { this.updatePopup(state.popup.model, state.popup.cause); - newState.popup.isChanged = false; + newState.popup.modelChanged = false; newState.popup.cause = undefined; } return newState; @@ -437,9 +437,9 @@ export class CommandStack implements ICommandStack { * Internal type to pass the results between the promises in the `CommandStack`. */ export interface CommandStackState { - main: DetailedCommandResult, - hidden: DetailedCommandResult, - popup: DetailedCommandResult + main: CommandResult, + hidden: CommandResult, + popup: CommandResult } function copyState(state: CommandStackState): CommandStackState { diff --git a/src/base/commands/command.ts b/src/base/commands/command.ts index 072d6215..930fd3f3 100644 --- a/src/base/commands/command.ts +++ b/src/base/commands/command.ts @@ -44,11 +44,11 @@ export interface ICommand { */ readonly blockUntil?: (action: Action) => boolean; - execute(context: CommandExecutionContext): CommandResult + execute(context: CommandExecutionContext): CommandReturn - undo(context: CommandExecutionContext): CommandResult + undo(context: CommandExecutionContext): CommandReturn - redo(context: CommandExecutionContext): CommandResult + redo(context: CommandExecutionContext): CommandReturn } /** @@ -59,15 +59,15 @@ export interface ICommand { * chaining, it is essential that a command does not make any assumption * on the state of the model before execute() is called. */ -export type CommandResult = SModelRoot | Promise | DetailedCommandResult; +export type CommandReturn = SModelRoot | Promise | CommandResult; /** - * The `DetailedCommandResult` allows to specify whether the model has changed + * The `CommandResult` allows to specify whether the model has changed * and the original action that caused the command to be executed. In case such * an action is given, it is passed to the viewer in order to link any * subsequent response action to the original request. */ -export type DetailedCommandResult = { model: SModelRoot, isChanged: boolean, cause?: Action }; +export type CommandResult = { model: SModelRoot, modelChanged: boolean, cause?: Action }; /** * Base class for all commands. @@ -89,11 +89,11 @@ export type DetailedCommandResult = { model: SModelRoot, isChanged: boolean, cau @injectable() export abstract class Command implements ICommand { - abstract execute(context: CommandExecutionContext): CommandResult; + abstract execute(context: CommandExecutionContext): CommandReturn; - abstract undo(context: CommandExecutionContext): CommandResult; + abstract undo(context: CommandExecutionContext): CommandReturn; - abstract redo(context: CommandExecutionContext): CommandResult; + abstract redo(context: CommandExecutionContext): CommandReturn; } /** @@ -133,14 +133,14 @@ export abstract class MergeableCommand extends Command { */ @injectable() export abstract class HiddenCommand extends Command { - abstract execute(context: CommandExecutionContext): SModelRoot | DetailedCommandResult; + abstract execute(context: CommandExecutionContext): SModelRoot | CommandResult; - undo(context: CommandExecutionContext): CommandResult { + undo(context: CommandExecutionContext): CommandReturn { context.logger.error(this, 'Cannot undo a hidden command'); return context.root; } - redo(context: CommandExecutionContext): CommandResult { + redo(context: CommandExecutionContext): CommandReturn { context.logger.error(this, 'Cannot redo a hidden command'); return context.root; } diff --git a/src/base/commands/request-command.ts b/src/base/commands/request-command.ts index 4e2d7688..8116629f 100644 --- a/src/base/commands/request-command.ts +++ b/src/base/commands/request-command.ts @@ -16,7 +16,7 @@ import { injectable, inject } from "inversify"; import { TYPES } from "../types"; -import { SystemCommand, CommandExecutionContext, CommandResult } from "./command"; +import { SystemCommand, CommandExecutionContext, CommandReturn } from "./command"; import { ResponseAction } from "../actions/action"; import { IActionDispatcher } from "../actions/action-dispatcher"; @@ -29,19 +29,19 @@ export abstract class ModelRequestCommand extends SystemCommand { @inject(TYPES.IActionDispatcher) protected actionDispatcher: IActionDispatcher; - execute(context: CommandExecutionContext): CommandResult { + execute(context: CommandExecutionContext): CommandReturn { const result = this.retrieveResult(context); this.actionDispatcher.dispatch(result); - return { model: context.root, isChanged: false }; + return { model: context.root, modelChanged: false }; } protected abstract retrieveResult(context: CommandExecutionContext): ResponseAction; - undo(context: CommandExecutionContext): CommandResult { - return { model: context.root, isChanged: false }; + undo(context: CommandExecutionContext): CommandReturn { + return { model: context.root, modelChanged: false }; } - redo(context: CommandExecutionContext): CommandResult { - return { model: context.root, isChanged: false }; + redo(context: CommandExecutionContext): CommandReturn { + return { model: context.root, modelChanged: false }; } } diff --git a/src/base/features/initialize-canvas.ts b/src/base/features/initialize-canvas.ts index 30ff188d..64e5c34c 100644 --- a/src/base/features/initialize-canvas.ts +++ b/src/base/features/initialize-canvas.ts @@ -22,7 +22,7 @@ import { Action } from '../actions/action'; import { IActionDispatcher } from '../actions/action-dispatcher'; import { IVNodeDecorator } from "../views/vnode-decorators"; import { SModelElement, SModelRoot } from "../model/smodel"; -import { SystemCommand, CommandExecutionContext, CommandResult } from '../commands/command'; +import { SystemCommand, CommandExecutionContext, CommandReturn } from '../commands/command'; /** * Grabs the bounds from the root element in page coordinates and fires a @@ -90,17 +90,17 @@ export class InitializeCanvasBoundsCommand extends SystemCommand { super(); } - execute(context: CommandExecutionContext): CommandResult { + execute(context: CommandExecutionContext): CommandReturn { this.newCanvasBounds = this.action.newCanvasBounds; context.root.canvasBounds = this.newCanvasBounds; return context.root; } - undo(context: CommandExecutionContext): CommandResult { + undo(context: CommandExecutionContext): CommandReturn { return context.root; } - redo(context: CommandExecutionContext): CommandResult { + redo(context: CommandExecutionContext): CommandReturn { return context.root; } } diff --git a/src/base/ui-extensions/ui-extension-registry.ts b/src/base/ui-extensions/ui-extension-registry.ts index 323880d4..cb90d8e0 100644 --- a/src/base/ui-extensions/ui-extension-registry.ts +++ b/src/base/ui-extensions/ui-extension-registry.ts @@ -16,7 +16,7 @@ import { inject, injectable, multiInject, optional } from "inversify"; import { InstanceRegistry } from "../../utils/registry"; import { Action } from "../actions/action"; -import { CommandExecutionContext, SystemCommand, CommandResult } from "../commands/command"; +import { CommandExecutionContext, SystemCommand, CommandReturn } from "../commands/command"; import { TYPES } from "../types"; import { IUIExtension } from "./ui-extension"; @@ -53,18 +53,18 @@ export class SetUIExtensionVisibilityCommand extends SystemCommand { super(); } - execute(context: CommandExecutionContext): CommandResult { + execute(context: CommandExecutionContext): CommandReturn { const extension = this.registry.get(this.action.extensionId); if (extension) { this.action.visible ? extension.show(context.root, ...this.action.contextElementsId) : extension.hide(); } - return { model: context.root, isChanged: false }; + return { model: context.root, modelChanged: false }; } - undo(context: CommandExecutionContext): CommandResult { - return { model: context.root, isChanged: false }; + undo(context: CommandExecutionContext): CommandReturn { + return { model: context.root, modelChanged: false }; } - redo(context: CommandExecutionContext): CommandResult { - return { model: context.root, isChanged: false }; + redo(context: CommandExecutionContext): CommandReturn { + return { model: context.root, modelChanged: false }; } } diff --git a/src/features/bounds/bounds-manipulation.ts b/src/features/bounds/bounds-manipulation.ts index 99d90ce1..169179fd 100644 --- a/src/features/bounds/bounds-manipulation.ts +++ b/src/features/bounds/bounds-manipulation.ts @@ -17,7 +17,7 @@ import { Bounds, Point } from "../../utils/geometry"; import { SModelElement, SModelRootSchema } from "../../base/model/smodel"; import { Action, RequestAction, ResponseAction, generateRequestId } from "../../base/actions/action"; -import { CommandExecutionContext, HiddenCommand, SystemCommand, DetailedCommandResult, CommandResult } from "../../base/commands/command"; +import { CommandExecutionContext, HiddenCommand, SystemCommand, CommandResult, CommandReturn } from "../../base/commands/command"; import { BoundsAware, isBoundsAware, Alignable } from './model'; import { injectable, inject } from "inversify"; import { TYPES } from "../../base/types"; @@ -118,7 +118,7 @@ export class SetBoundsCommand extends SystemCommand { super(); } - execute(context: CommandExecutionContext): CommandResult { + execute(context: CommandExecutionContext): CommandReturn { this.action.bounds.forEach( b => { const element = context.root.index.getById(b.elementId); @@ -134,14 +134,14 @@ export class SetBoundsCommand extends SystemCommand { return this.redo(context); } - undo(context: CommandExecutionContext): CommandResult { + undo(context: CommandExecutionContext): CommandReturn { this.bounds.forEach( b => b.element.bounds = b.oldBounds ); return context.root; } - redo(context: CommandExecutionContext): CommandResult { + redo(context: CommandExecutionContext): CommandReturn { this.bounds.forEach( b => b.element.bounds = b.newBounds ); @@ -157,10 +157,10 @@ export class RequestBoundsCommand extends HiddenCommand { super(); } - execute(context: CommandExecutionContext): DetailedCommandResult { + execute(context: CommandExecutionContext): CommandResult { return { model: context.modelFactory.createRoot(this.action.newRoot), - isChanged: true, + modelChanged: true, cause: this.action }; } diff --git a/src/features/edit/create.ts b/src/features/edit/create.ts index 0beb0cec..b85405bd 100644 --- a/src/features/edit/create.ts +++ b/src/features/edit/create.ts @@ -15,7 +15,7 @@ ********************************************************************************/ import { Action } from "../../base/actions/action"; -import { Command, CommandExecutionContext, CommandResult } from "../../base/commands/command"; +import { Command, CommandExecutionContext, CommandReturn } from "../../base/commands/command"; import { SParentElement, SChildElement, SModelElementSchema } from "../../base/model/smodel"; import { inject, injectable } from "inversify"; import { TYPES } from "../../base/types"; @@ -38,7 +38,7 @@ export class CreateElementCommand extends Command { super(); } - execute(context: CommandExecutionContext): CommandResult { + execute(context: CommandExecutionContext): CommandReturn { const container = context.root.index.getById(this.action.containerId); if (container instanceof SParentElement) { this.container = container; @@ -48,12 +48,12 @@ export class CreateElementCommand extends Command { return context.root; } - undo(context: CommandExecutionContext): CommandResult { + undo(context: CommandExecutionContext): CommandReturn { this.container.remove(this.newElement); return context.root; } - redo(context: CommandExecutionContext): CommandResult { + redo(context: CommandExecutionContext): CommandReturn { this.container.add(this.newElement); return context.root; } diff --git a/src/features/edit/delete.ts b/src/features/edit/delete.ts index dcfcd45e..b6f7042d 100644 --- a/src/features/edit/delete.ts +++ b/src/features/edit/delete.ts @@ -14,7 +14,7 @@ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ -import { Command, CommandExecutionContext, CommandResult } from "../../base/commands/command"; +import { Command, CommandExecutionContext, CommandReturn } from "../../base/commands/command"; import { Action } from "../../base/actions/action"; import { SModelElement, SParentElement, SChildElement } from "../../base/model/smodel"; import { SModelExtension } from "../../base/model/smodel-extension"; @@ -52,7 +52,7 @@ export class DeleteElementCommand extends Command { super(); } - execute(context: CommandExecutionContext): CommandResult { + execute(context: CommandExecutionContext): CommandReturn { const index = context.root.index; for (const id of this.action.elementIds) { const element = index.getById(id); @@ -64,13 +64,13 @@ export class DeleteElementCommand extends Command { return context.root; } - undo(context: CommandExecutionContext): CommandResult { + undo(context: CommandExecutionContext): CommandReturn { for (const resolvedDelete of this.resolvedDeletes) resolvedDelete.parent.add(resolvedDelete.child); return context.root; } - redo(context: CommandExecutionContext): CommandResult { + redo(context: CommandExecutionContext): CommandReturn { for (const resolvedDelete of this.resolvedDeletes) resolvedDelete.parent.remove(resolvedDelete.child); return context.root; diff --git a/src/features/edit/edit-label.ts b/src/features/edit/edit-label.ts index 735a66f1..66f85354 100644 --- a/src/features/edit/edit-label.ts +++ b/src/features/edit/edit-label.ts @@ -16,7 +16,7 @@ import { inject } from "inversify"; import { Action, isAction } from "../../base/actions/action"; -import { CommandExecutionContext, CommandResult, Command } from "../../base/commands/command"; +import { CommandExecutionContext, CommandReturn, Command } from "../../base/commands/command"; import { SModelElement } from "../../base/model/smodel"; import { TYPES } from "../../base/types"; import { MouseListener } from "../../base/views/mouse-tool"; @@ -58,7 +58,7 @@ export class ApplyLabelEditCommand extends Command { super(); } - execute(context: CommandExecutionContext): CommandResult { + execute(context: CommandExecutionContext): CommandReturn { const index = context.root.index; const label = index.getById(this.action.labelId); if (label && isEditableLabel(label)) { @@ -68,14 +68,14 @@ export class ApplyLabelEditCommand extends Command { return context.root; } - undo(context: CommandExecutionContext): CommandResult { + undo(context: CommandExecutionContext): CommandReturn { if (this.resolvedLabelEdit) { this.resolvedLabelEdit.label.text = this.resolvedLabelEdit.oldLabel; } return context.root; } - redo(context: CommandExecutionContext): CommandResult { + redo(context: CommandExecutionContext): CommandReturn { if (this.resolvedLabelEdit) { this.resolvedLabelEdit.label.text = this.resolvedLabelEdit.newLabel; } diff --git a/src/features/edit/edit-routing.ts b/src/features/edit/edit-routing.ts index 6cccdbe3..1c417e61 100644 --- a/src/features/edit/edit-routing.ts +++ b/src/features/edit/edit-routing.ts @@ -16,7 +16,7 @@ import { inject, injectable } from "inversify"; import { Action } from "../../base/actions/action"; -import { Command, CommandExecutionContext, CommandResult } from "../../base/commands/command"; +import { Command, CommandExecutionContext, CommandReturn } from "../../base/commands/command"; import { SModelElement, SModelRoot, SParentElement } from '../../base/model/smodel'; import { TYPES } from "../../base/types"; import { Point } from "../../utils/geometry"; @@ -47,7 +47,7 @@ export class SwitchEditModeCommand extends Command { super(); } - execute(context: CommandExecutionContext): CommandResult { + execute(context: CommandExecutionContext): CommandReturn { const index = context.root.index; this.action.elementsToActivate.forEach(id => { const element = index.getById(id); @@ -110,7 +110,7 @@ export class SwitchEditModeCommand extends Command { return false; } - undo(context: CommandExecutionContext): CommandResult { + undo(context: CommandExecutionContext): CommandReturn { this.handlesToRemove.forEach(entry => { if (entry.point !== undefined) entry.parent.routingPoints.splice(entry.handle.pointIndex, 0, entry.point); @@ -131,7 +131,7 @@ export class SwitchEditModeCommand extends Command { return context.root; } - redo(context: CommandExecutionContext): CommandResult { + redo(context: CommandExecutionContext): CommandReturn { return this.doExecute(context); } } diff --git a/src/features/edit/reconnect.ts b/src/features/edit/reconnect.ts index e8e432a8..02dfeb20 100644 --- a/src/features/edit/reconnect.ts +++ b/src/features/edit/reconnect.ts @@ -16,7 +16,7 @@ import { inject, injectable } from "inversify"; import { Action } from "../../base/actions/action"; -import { Command, CommandExecutionContext, CommandResult } from "../../base/commands/command"; +import { Command, CommandExecutionContext, CommandReturn } from "../../base/commands/command"; import { TYPES } from "../../base/types"; import { SRoutableElement } from "../routing/model"; import { EdgeMemento, EdgeRouterRegistry } from "../routing/routing"; @@ -42,7 +42,7 @@ export class ReconnectCommand extends Command { super(); } - execute(context: CommandExecutionContext): CommandResult { + execute(context: CommandExecutionContext): CommandReturn { this.doExecute(context); return context.root; } @@ -63,7 +63,7 @@ export class ReconnectCommand extends Command { } } - undo(context: CommandExecutionContext): CommandResult { + undo(context: CommandExecutionContext): CommandReturn { if (this.memento) { const router = this.edgeRouterRegistry.get(this.memento.edge.routerKind); router.applySnapshot(this.memento.edge, this.memento.before); @@ -71,7 +71,7 @@ export class ReconnectCommand extends Command { return context.root; } - redo(context: CommandExecutionContext): CommandResult { + redo(context: CommandExecutionContext): CommandReturn { if (this.memento) { const router = this.edgeRouterRegistry.get(this.memento.edge.routerKind); router.applySnapshot(this.memento.edge, this.memento.after); diff --git a/src/features/export/export.ts b/src/features/export/export.ts index 4b85dc8d..ad3d063c 100644 --- a/src/features/export/export.ts +++ b/src/features/export/export.ts @@ -16,7 +16,7 @@ import { injectable, inject } from "inversify"; import { VNode } from 'snabbdom/vnode'; -import { CommandExecutionContext, HiddenCommand, DetailedCommandResult } from '../../base/commands/command'; +import { CommandExecutionContext, HiddenCommand, CommandResult } from '../../base/commands/command'; import { IVNodeDecorator } from '../../base/views/vnode-decorators'; import { isSelectable } from '../select/model'; import { Action, RequestAction, generateRequestId } from '../../base/actions/action'; @@ -58,7 +58,7 @@ export class ExportSvgCommand extends HiddenCommand { super(); } - execute(context: CommandExecutionContext): DetailedCommandResult { + execute(context: CommandExecutionContext): CommandResult { if (isExportable(context.root)) { const root = context.modelFactory.createRoot(context.modelFactory.createSchema(context.root)); if (isExportable(root)) { @@ -77,14 +77,14 @@ export class ExportSvgCommand extends HiddenCommand { }); return { model: root, - isChanged: true, + modelChanged: true, cause: this.action }; } } return { model: context.root, - isChanged: false + modelChanged: false }; } } diff --git a/src/features/hover/hover.ts b/src/features/hover/hover.ts index 0756ba45..c5f725e0 100644 --- a/src/features/hover/hover.ts +++ b/src/features/hover/hover.ts @@ -21,7 +21,7 @@ import { TYPES } from "../../base/types"; import { SModelElement, SModelRoot, SModelRootSchema } from "../../base/model/smodel"; import { MouseListener } from "../../base/views/mouse-tool"; import { Action, RequestAction, ResponseAction, generateRequestId } from "../../base/actions/action"; -import { CommandExecutionContext, PopupCommand, SystemCommand, CommandResult } from "../../base/commands/command"; +import { CommandExecutionContext, PopupCommand, SystemCommand, CommandReturn } from "../../base/commands/command"; import { EMPTY_ROOT } from "../../base/model/smodel-factory"; import { KeyListener } from "../../base/views/key-tool"; import { findParentByFeature, findParent } from "../../base/model/smodel-utils"; @@ -48,7 +48,7 @@ export class HoverFeedbackCommand extends SystemCommand { super(); } - execute(context: CommandExecutionContext): CommandResult { + execute(context: CommandExecutionContext): CommandReturn { const model: SModelRoot = context.root; const modelElement: SModelElement | undefined = model.index.getById(this.action.mouseoverElement); @@ -61,11 +61,11 @@ export class HoverFeedbackCommand extends SystemCommand { return this.redo(context); } - undo(context: CommandExecutionContext): CommandResult { + undo(context: CommandExecutionContext): CommandReturn { return context.root; } - redo(context: CommandExecutionContext): CommandResult { + redo(context: CommandExecutionContext): CommandReturn { return context.root; } } @@ -112,18 +112,18 @@ export class SetPopupModelCommand extends PopupCommand { super(); } - execute(context: CommandExecutionContext): CommandResult { + execute(context: CommandExecutionContext): CommandReturn { this.oldRoot = context.root; this.newRoot = context.modelFactory.createRoot(this.action.newRoot); return this.newRoot; } - undo(context: CommandExecutionContext): CommandResult { + undo(context: CommandExecutionContext): CommandReturn { return this.oldRoot; } - redo(context: CommandExecutionContext): CommandResult { + redo(context: CommandExecutionContext): CommandReturn { return this.newRoot; } } diff --git a/src/features/move/move.ts b/src/features/move/move.ts index e612fbcd..8958d422 100644 --- a/src/features/move/move.ts +++ b/src/features/move/move.ts @@ -18,7 +18,7 @@ import { inject, injectable, optional } from "inversify"; import { VNode } from "snabbdom/vnode"; import { Action } from "../../base/actions/action"; import { Animation, CompoundAnimation } from "../../base/animations/animation"; -import { CommandExecutionContext, ICommand, MergeableCommand, CommandResult } from "../../base/commands/command"; +import { CommandExecutionContext, ICommand, MergeableCommand, CommandReturn } from "../../base/commands/command"; import { SChildElement, SModelElement, SModelRoot } from '../../base/model/smodel'; import { findParentByFeature, translatePoint } from "../../base/model/smodel-utils"; import { TYPES } from "../../base/types"; @@ -80,7 +80,7 @@ export class MoveCommand extends MergeableCommand { super(); } - execute(context: CommandExecutionContext): CommandResult { + execute(context: CommandExecutionContext): CommandReturn { const index = context.root.index; const edge2handleMoves = new Map(); const attachedEdgeShifts = new Map(); diff --git a/src/features/select/select.ts b/src/features/select/select.ts index 3b5666af..1f874b8f 100644 --- a/src/features/select/select.ts +++ b/src/features/select/select.ts @@ -64,6 +64,9 @@ export class SelectAllAction implements Action { } } +/** + * Request action for retrieving the current selection. + */ export class GetSelectionAction implements RequestAction { static readonly KIND = 'getSelection'; kind = GetSelectionAction.KIND; diff --git a/src/features/update/update-model.ts b/src/features/update/update-model.ts index 2846a1d8..48f68cd1 100644 --- a/src/features/update/update-model.ts +++ b/src/features/update/update-model.ts @@ -17,7 +17,7 @@ import { injectable, inject } from "inversify"; import { isValidDimension, almostEquals } from "../../utils/geometry"; import { Animation, CompoundAnimation } from '../../base/animations/animation'; -import { CommandExecutionContext, CommandResult, Command } from '../../base/commands/command'; +import { CommandExecutionContext, CommandReturn, Command } from '../../base/commands/command'; import { FadeAnimation, ResolvedElementFade } from '../fade/fade'; import { Action } from '../../base/actions/action'; import { SModelRootSchema, SModelRoot, SChildElement, SModelElement, SParentElement } from "../../base/model/smodel"; @@ -68,7 +68,7 @@ export class UpdateModelCommand extends Command { super(); } - execute(context: CommandExecutionContext): CommandResult { + execute(context: CommandExecutionContext): CommandReturn { let newRoot: SModelRoot; if (this.action.newRoot !== undefined) { newRoot = context.modelFactory.createRoot(this.action.newRoot); @@ -82,7 +82,7 @@ export class UpdateModelCommand extends Command { return this.performUpdate(this.oldRoot, this.newRoot, context); } - protected performUpdate(oldRoot: SModelRoot, newRoot: SModelRoot, context: CommandExecutionContext): CommandResult { + protected performUpdate(oldRoot: SModelRoot, newRoot: SModelRoot, context: CommandExecutionContext): CommandReturn { if ((this.action.animate === undefined || this.action.animate) && oldRoot.id === newRoot.id) { let matchResult: MatchResult; if (this.action.matches === undefined) { @@ -266,11 +266,11 @@ export class UpdateModelCommand extends Command { return animations; } - undo(context: CommandExecutionContext): CommandResult { + undo(context: CommandExecutionContext): CommandReturn { return this.performUpdate(this.newRoot, this.oldRoot, context); } - redo(context: CommandExecutionContext): CommandResult { + redo(context: CommandExecutionContext): CommandReturn { return this.performUpdate(this.oldRoot, this.newRoot, context); } } diff --git a/src/features/viewport/center-fit.ts b/src/features/viewport/center-fit.ts index 39c8582c..f8d25f75 100644 --- a/src/features/viewport/center-fit.ts +++ b/src/features/viewport/center-fit.ts @@ -18,7 +18,7 @@ import { Bounds, center, combine, isValidDimension } from "../../utils/geometry" import { matchesKeystroke } from "../../utils/keyboard"; import { SChildElement } from '../../base/model/smodel'; import { Action } from "../../base/actions/action"; -import { Command, CommandExecutionContext, CommandResult } from "../../base/commands/command"; +import { Command, CommandExecutionContext, CommandReturn } from "../../base/commands/command"; import { SModelElement, SModelRoot } from "../../base/model/smodel"; import { KeyListener } from "../../base/views/key-tool"; import { isBoundsAware } from "../bounds/model"; @@ -119,12 +119,12 @@ export abstract class BoundsAwareViewportCommand extends Command { protected abstract getElementIds(): string[]; - execute(context: CommandExecutionContext): CommandResult { + execute(context: CommandExecutionContext): CommandReturn { this.initialize(context.root); return this.redo(context); } - undo(context: CommandExecutionContext): CommandResult { + undo(context: CommandExecutionContext): CommandReturn { const model = context.root; if (isViewport(model) && this.newViewport !== undefined && !this.equal(this.newViewport, this.oldViewport)) { if (this.animate) @@ -137,7 +137,7 @@ export abstract class BoundsAwareViewportCommand extends Command { return model; } - redo(context: CommandExecutionContext): CommandResult { + redo(context: CommandExecutionContext): CommandReturn { const model = context.root; if (isViewport(model) && this.newViewport !== undefined && !this.equal(this.newViewport, this.oldViewport)) { if (this.animate) { diff --git a/src/features/viewport/viewport.ts b/src/features/viewport/viewport.ts index 1de40eab..d9e74d13 100644 --- a/src/features/viewport/viewport.ts +++ b/src/features/viewport/viewport.ts @@ -17,7 +17,7 @@ import { ORIGIN_POINT, Bounds } from "../../utils/geometry"; import { SModelElement, SModelRoot } from "../../base/model/smodel"; import { Action, RequestAction, ResponseAction, generateRequestId } from "../../base/actions/action"; -import { MergeableCommand, ICommand, CommandExecutionContext, CommandResult } from "../../base/commands/command"; +import { MergeableCommand, ICommand, CommandExecutionContext, CommandReturn } from "../../base/commands/command"; import { Animation } from "../../base/animations/animation"; import { isViewport, Viewport } from "./model"; import { injectable, inject } from "inversify"; @@ -34,6 +34,9 @@ export class SetViewportAction implements Action { } } +/** + * Request action for retrieving the current viewport and canvas bounds. + */ export class GetViewportAction implements RequestAction { static readonly KIND = 'getViewport'; kind = GetViewportAction.KIND; @@ -68,7 +71,7 @@ export class SetViewportCommand extends MergeableCommand { this.newViewport = action.newViewport; } - execute(context: CommandExecutionContext): CommandResult { + execute(context: CommandExecutionContext): CommandReturn { const model = context.root; const element = model.index.getById(this.action.elementId); if (element && isViewport(element)) { @@ -87,11 +90,11 @@ export class SetViewportCommand extends MergeableCommand { return model; } - undo(context: CommandExecutionContext): CommandResult { + undo(context: CommandExecutionContext): CommandReturn { return new ViewportAnimation(this.element, this.newViewport, this.oldViewport, context).start(); } - redo(context: CommandExecutionContext): CommandResult { + redo(context: CommandExecutionContext): CommandReturn { return new ViewportAnimation(this.element, this.oldViewport, this.newViewport, context).start(); } diff --git a/src/model-source/commit-model.ts b/src/model-source/commit-model.ts index 6053d988..1490b98f 100644 --- a/src/model-source/commit-model.ts +++ b/src/model-source/commit-model.ts @@ -16,7 +16,7 @@ import { inject, injectable } from "inversify"; import { Action } from "../base/actions/action"; -import { CommandExecutionContext, CommandResult, SystemCommand } from "../base/commands/command"; +import { CommandExecutionContext, CommandReturn, SystemCommand } from "../base/commands/command"; import { SModelRoot, SModelRootSchema } from "../base/model/smodel"; import { TYPES } from "../base/types"; import { ModelSource } from "./model-source"; @@ -46,12 +46,12 @@ export class CommitModelCommand extends SystemCommand { super(); } - execute(context: CommandExecutionContext): CommandResult { + execute(context: CommandExecutionContext): CommandReturn { this.newModel = context.modelFactory.createSchema(context.root); return this.doCommit(this.newModel, context.root, true); } - protected doCommit(model: SModelRootSchema, result: SModelRoot, doSetOriginal: boolean): CommandResult { + protected doCommit(model: SModelRootSchema, result: SModelRoot, doSetOriginal: boolean): CommandReturn { const commitResult = this.modelSource.commitModel(model); if (commitResult instanceof Promise) { return commitResult.then(originalModel => { @@ -66,11 +66,11 @@ export class CommitModelCommand extends SystemCommand { } } - undo(context: CommandExecutionContext): CommandResult { + undo(context: CommandExecutionContext): CommandReturn { return this.doCommit(this.originalModel, context.root, false); } - redo(context: CommandExecutionContext): CommandResult { + redo(context: CommandExecutionContext): CommandReturn { return this.doCommit(this.newModel, context.root, false); } } diff --git a/src/model-source/diagram-server.ts b/src/model-source/diagram-server.ts index b5e6ef60..3dedfc14 100644 --- a/src/model-source/diagram-server.ts +++ b/src/model-source/diagram-server.ts @@ -41,7 +41,7 @@ export interface ActionMessage { } export function isActionMessage(object: any): object is ActionMessage { - return object !== undefined && object.hasOwnProperty('clientId') && object.hasOwnProperty('action'); + return object !== undefined && object.hasOwnProperty('action'); } /**