diff --git a/package.json b/package.json index 4e14b9122..e5a99c508 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,9 @@ "tslint": "^5.5.0", "typescript": "3.6.4" }, + "resolutions": { + "**/sprotty": "0.8.0-next.1d772ad" + }, "scripts": { "prepare": "yarn run clean && yarn run build", "clean": "rimraf lib", diff --git a/src/base/edit-config/edit-config.ts b/src/base/edit-config/edit-config.ts deleted file mode 100644 index b8ff87987..000000000 --- a/src/base/edit-config/edit-config.ts +++ /dev/null @@ -1,72 +0,0 @@ -/******************************************************************************** - * Copyright (c) 2019 EclipseSource 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 { SEdge, SModelElement, SModelElementSchema, SNode, SParentElement } from "sprotty/lib"; - - -export const edgeEditConfig = Symbol.for("edgeEditConfiguration"); -export const nodeEditConfig = Symbol.for("nodeEditConfiguration"); -export interface EditConfig { - deletable: boolean - repositionable: boolean - configType: symbol - elementTypeId?: string - -} - -export interface NodeEditConfig extends EditConfig { - resizable: boolean - isContainer(): boolean - isContainableElement(input: SModelElement | SModelElementSchema | string): boolean -} - -export interface EdgeEditConfig extends EditConfig { - routable: boolean - isAllowedSource(input: SModelElement | SModelElementSchema | string): boolean - isAllowedTarget(input: SModelElement | SModelElementSchema | string): boolean -} - -export interface IEditConfigProvider { - getEditConfig(input: SModelElement | SModelElementSchema | string): EditConfig | undefined -} - -export function isConfigurableElement(element: SModelElement): element is SModelElement & EditConfig { - return (element).configType !== undefined && typeof ((element).configType) === "symbol"; -} - -export function isConfigurableEdge(element: SModelElement): element is SEdge & EdgeEditConfig { - return element instanceof SEdge && isConfigurableElement(element) && element.configType === edgeEditConfig; -} -export function isConfigurableNode(element: SModelElement): element is SNode & NodeEditConfig { - return element instanceof SNode && isConfigurableElement(element) && element.configType === nodeEditConfig; -} - -export function isEdgeEditConfig(editConfig: EditConfig): editConfig is EdgeEditConfig { - return editConfig.configType === edgeEditConfig; -} - -export function isNodeEditConfig(editConfig: EditConfig): editConfig is NodeEditConfig { - return editConfig.configType === nodeEditConfig; -} - -export function movingAllowed(element: SModelElement): element is SNode & NodeEditConfig { - return isConfigurableNode(element) && element.repositionable; -} - -export function containmentAllowed(element: SModelElement, containableElementTypeId: string) - : element is SParentElement & NodeEditConfig { - return isConfigurableNode(element) && element.isContainableElement(containableElementTypeId); -} - diff --git a/src/base/model/update-model-command.ts b/src/base/model/update-model-command.ts index 1907150be..aed725880 100644 --- a/src/base/model/update-model-command.ts +++ b/src/base/model/update-model-command.ts @@ -43,8 +43,6 @@ export class SetModelActionHandler implements IActionHandler { return new UpdateModelAction(action.newRoot, false); } } - - handledActionKinds = [SetModelCommand.KIND]; } export function isSetModelAction(action: Action): action is SetModelAction { diff --git a/src/features/change-bounds/model.ts b/src/features/change-bounds/model.ts index 56794b588..3172008f1 100644 --- a/src/features/change-bounds/model.ts +++ b/src/features/change-bounds/model.ts @@ -24,11 +24,17 @@ import { SChildElement, Selectable, SModelElement, - SNode, SParentElement } from "sprotty/lib"; -import { isConfigurableNode, NodeEditConfig } from "../../base/edit-config/edit-config"; +export const resizeFeature = Symbol("resizeFeature"); + +export interface Resizable extends BoundsAware, Selectable { +} + +export function isResizable(element: SModelElement): element is SParentElement & Resizable { + return isBoundsAware(element) && isSelectable(element) && element instanceof SParentElement && element.hasFeature(resizeFeature); +} export enum ResizeHandleLocation { TopLeft = "top-left", @@ -37,10 +43,6 @@ export enum ResizeHandleLocation { BottomRight = "bottom-right" } -export function isResizeable(element: SModelElement): element is SNode & SParentElement & BoundsAware & Selectable & NodeEditConfig { - return isConfigurableNode(element) && element.resizable && isBoundsAware(element) && isSelectable(element) && element instanceof SParentElement; -} - export function isBoundsAwareMoveable(element: SModelElement): element is SModelElement & Locateable & BoundsAware { return isMoveable(element) && isBoundsAware(element); } diff --git a/src/features/command-palette/action-definitions.ts b/src/features/command-palette/action-definitions.ts deleted file mode 100644 index b22208f1e..000000000 --- a/src/features/command-palette/action-definitions.ts +++ /dev/null @@ -1,36 +0,0 @@ -/******************************************************************************** - * Copyright (c) 2019 EclipseSource 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 { Action, LabeledAction, Point } from "sprotty/lib"; - -export class RequestCommandPaletteActions implements Action { - static readonly KIND = "requestCommandPaletteActions"; - kind = RequestCommandPaletteActions.KIND; - constructor( - public readonly selectedElementIds: string[] = [], - public readonly text: string, - public readonly lastMousePosition?: Point) { } -} - -export class SetCommandPaletteActions implements Action { - static readonly KIND = "setCommandPaletteActions"; - kind = SetCommandPaletteActions.KIND; - constructor(public readonly actions: LabeledAction[]) { } -} - -export function isSetCommandPaletteActionsAction(action: Action): action is SetCommandPaletteActions { - return action !== undefined && (action.kind === SetCommandPaletteActions.KIND) - && (action).actions !== undefined; -} diff --git a/src/features/command-palette/action-provider.ts b/src/features/command-palette/action-provider.ts deleted file mode 100644 index 22edae078..000000000 --- a/src/features/command-palette/action-provider.ts +++ /dev/null @@ -1,69 +0,0 @@ -/******************************************************************************** - * Copyright (c) 2019 EclipseSource 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 { inject, injectable } from "inversify"; -import { - Action, - CenterAction, - ICommandPaletteActionProvider, - ILogger, - isNameable, - LabeledAction, - name, - Point, - SelectAction, - SModelElement, - TYPES -} from "sprotty/lib"; -import { toArray } from "sprotty/lib/utils/iterable"; - -import { GLSP_TYPES } from "../../types"; -import { isSelected } from "../../utils/smodel-util"; -import { RequestResponseSupport } from "../request-response/support"; -import { isSetCommandPaletteActionsAction, RequestCommandPaletteActions } from "./action-definitions"; - -@injectable() -export class NavigationCommandPaletteActionProvider implements ICommandPaletteActionProvider { - - constructor(@inject(TYPES.ILogger) protected logger: ILogger) { } - - getActions(root: Readonly): Promise { - return Promise.resolve(toArray(root.index.all() - .filter(isNameable) - .map(nameable => new LabeledAction(`Select ${name(nameable)}`, - [new SelectAction([nameable.id]), new CenterAction([nameable.id])], 'fa-object-group')))); - } -} - -@injectable() -export class ServerCommandPaletteActionProvider implements ICommandPaletteActionProvider { - - constructor(@inject(GLSP_TYPES.RequestResponseSupport) protected requestResponseSupport: RequestResponseSupport) { } - - getActions(root: Readonly, text: string, lastMousePosition?: Point): Promise { - const selectedElementIds = Array.from(root.index.all().filter(isSelected).map(e => e.id)); - const requestAction = new RequestCommandPaletteActions(selectedElementIds, text, lastMousePosition); - const responseHandler = this.getPaletteActionsFromResponse; - const promise = this.requestResponseSupport.dispatchRequest(requestAction, responseHandler); - return promise; - } - - getPaletteActionsFromResponse(action: Action): LabeledAction[] { - if (isSetCommandPaletteActionsAction(action)) { - return action.actions; - } - return []; - } -} diff --git a/src/features/command-palette/di.config.ts b/src/features/command-palette/di.config.ts index b6eabbc3d..e60a1d541 100644 --- a/src/features/command-palette/di.config.ts +++ b/src/features/command-palette/di.config.ts @@ -18,11 +18,9 @@ import "../../../css/command-palette.css"; import { ContainerModule } from "inversify"; import { TYPES } from "sprotty/lib"; -import { NavigationCommandPaletteActionProvider, ServerCommandPaletteActionProvider } from "./action-provider"; +import { ServerCommandPaletteActionProvider } from "./server-command-palette-provider"; -const glspCommandPaletteModule = new ContainerModule((bind, unbind, isBound, rebind) => { - bind(TYPES.ICommandPaletteActionProvider).to(NavigationCommandPaletteActionProvider); - bind(ServerCommandPaletteActionProvider).toSelf().inSingletonScope(); +const glspCommandPaletteModule = new ContainerModule((bind) => { bind(TYPES.ICommandPaletteActionProvider).to(ServerCommandPaletteActionProvider); }); diff --git a/src/features/command-palette/server-command-palette-provider.ts b/src/features/command-palette/server-command-palette-provider.ts new file mode 100644 index 000000000..a18b33f35 --- /dev/null +++ b/src/features/command-palette/server-command-palette-provider.ts @@ -0,0 +1,50 @@ +/******************************************************************************** + * Copyright (c) 2019 EclipseSource 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 { inject, injectable } from "inversify"; +import { Action, ICommandPaletteActionProvider, LabeledAction, Point, SModelElement, TYPES } from "sprotty/lib"; + +import { isSelected } from "../../utils/smodel-util"; +import { ContextActions, isSetContextActionsAction, RequestContextActions } from "../context-actions/action-definitions"; +import { GLSPActionDispatcher } from "../request-response/glsp-action-dispatcher"; + +export namespace ServerCommandPalette { + export const KEY = "command-palette"; + export const TEXT = "text"; + export const INDEX = "index"; +} + +@injectable() +export class ServerCommandPaletteActionProvider implements ICommandPaletteActionProvider { + + constructor(@inject(TYPES.IActionDispatcher) protected actionDispatcher: GLSPActionDispatcher) { } + + getActions(root: Readonly, text: string, lastMousePosition?: Point, index?: number): Promise { + const selectedElementIds = Array.from(root.index.all().filter(isSelected).map(e => e.id)); + const requestAction = new RequestContextActions(selectedElementIds, lastMousePosition, { + [ContextActions.UI_CONTROL_KEY]: ServerCommandPalette.KEY, + [ServerCommandPalette.TEXT]: text, + [ServerCommandPalette.INDEX]: index ? index : 0 + }); + return this.actionDispatcher.requestUntil(requestAction).then(response => this.getPaletteActionsFromResponse(response)); + } + + getPaletteActionsFromResponse(action: Action): LabeledAction[] { + if (isSetContextActionsAction(action)) { + return action.actions; + } + return []; + } +} diff --git a/src/features/context-actions/action-definitions.ts b/src/features/context-actions/action-definitions.ts new file mode 100644 index 000000000..2aa6f8e16 --- /dev/null +++ b/src/features/context-actions/action-definitions.ts @@ -0,0 +1,42 @@ +/******************************************************************************** + * Copyright (c) 2019 EclipseSource 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 { Action, generateRequestId, LabeledAction, Point, RequestAction, ResponseAction } from "sprotty/lib"; + +export namespace ContextActions { + export const UI_CONTROL_KEY = "ui-control"; +} + +export class RequestContextActions implements RequestAction { + static readonly KIND = "requestContextActions"; + kind = RequestContextActions.KIND; + constructor( + public readonly selectedElementIds: string[] = [], + public readonly lastMousePosition?: Point, + public readonly args?: { [key: string]: string | number | boolean }, + public readonly requestId: string = generateRequestId()) { } +} + +export class SetContextActions implements ResponseAction { + static readonly KIND = "setContextActions"; + kind = SetContextActions.KIND; + constructor(public readonly actions: LabeledAction[], + public readonly responseId: string = '') { } +} + +export function isSetContextActionsAction(action: Action): action is SetContextActions { + return action !== undefined && (action.kind === SetContextActions.KIND) + && (action).actions !== undefined; +} diff --git a/src/features/context-menu/context-menu-service.ts b/src/features/context-menu/context-menu-service.ts new file mode 100644 index 000000000..89c519136 --- /dev/null +++ b/src/features/context-menu/context-menu-service.ts @@ -0,0 +1,51 @@ +/******************************************************************************** + * Copyright (c) 2019 EclipseSource 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 { LabeledAction } from "sprotty"; + +export interface MenuItem extends LabeledAction { + /** Technical id of the menu item. */ + readonly id: string; + /** String indicating the order. */ + readonly sortString?: string; + /** String indicating the grouping (separators). Items with equal group will be in the same group. */ + readonly group?: string; + /** + * The optional parent id can be used to add this element as a child of another element provided by anohter menu provider. + * The `parentId` must be fully qualified in the form of `a.b.c`, whereas `a`, `b` and `c` are referring to the IDs of other elements. + * Note that this attribute will only be considered for root items of a provider and not for children of provided items. + */ + readonly parentId?: string; + /** Function determining whether the element is enabled. */ + readonly isEnabled?: () => boolean; + /** Function determining whether the element is visible. */ + readonly isVisible?: () => boolean; + /** Function determining whether the element is toggled on or off. */ + readonly isToggled?: () => boolean; + /** Children of this item. If this item has children, they will be added into a submenu of this item. */ + children?: MenuItem[]; +} + +export type Anchor = MouseEvent | { x: number, y: number }; + +export function toAnchor(anchor: HTMLElement | { x: number, y: number }): Anchor { + return anchor instanceof HTMLElement ? { x: anchor.offsetLeft, y: anchor.offsetTop } : anchor; +} + +export interface IContextMenuService { + show(items: MenuItem[], anchor: Anchor, onHide?: () => void): void; +} + +export type IContextMenuServiceProvider = () => Promise; diff --git a/src/features/context-menu/di.config.ts b/src/features/context-menu/di.config.ts new file mode 100644 index 000000000..374df2a57 --- /dev/null +++ b/src/features/context-menu/di.config.ts @@ -0,0 +1,42 @@ +/******************************************************************************** + * Copyright (c) 2019 EclipseSource 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 { ContainerModule } from "inversify"; +import { TYPES } from "sprotty/lib"; + +import { GLSP_TYPES } from "../../types"; +import { IContextMenuService } from "./context-menu-service"; +import { ContextMenuProviderRegistry } from "./menu-providers"; +import { ContextMenuMouseListener } from "./mouse-listener"; +import { ServerContextMenuItemProvider } from "./server-context-menu-provider"; + +const glspContextMenuModule = new ContainerModule(bind => { + bind(GLSP_TYPES.IContextMenuServiceProvider).toProvider(ctx => { + return () => { + return new Promise((resolve, reject) => { + if (ctx.container.isBound(GLSP_TYPES.IContextMenuService)) { + resolve(ctx.container.get(GLSP_TYPES.IContextMenuService)); + } else { + reject(); + } + }); + }; + }); + bind(TYPES.MouseListener).to(ContextMenuMouseListener); + bind(GLSP_TYPES.IContextMenuProviderRegistry).to(ContextMenuProviderRegistry); + bind(GLSP_TYPES.IContextMenuProvider).to(ServerContextMenuItemProvider); +}); + +export default glspContextMenuModule; diff --git a/src/features/context-menu/menu-providers.ts b/src/features/context-menu/menu-providers.ts new file mode 100644 index 000000000..28292dac1 --- /dev/null +++ b/src/features/context-menu/menu-providers.ts @@ -0,0 +1,81 @@ +/******************************************************************************** + * Copyright (c) 2019 EclipseSource 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, multiInject, optional } from "inversify"; +import { DeleteElementAction, isDeletable, LabeledAction, Point, SModelRoot } from "sprotty"; + +import { GLSP_TYPES } from "../../types"; +import { isSelected } from "../../utils/smodel-util"; +import { MenuItem } from "./context-menu-service"; + +export interface IContextMenuItemProvider { + getItems(root: Readonly, lastMousePosition?: Point): Promise; +} + +@injectable() +export class ContextMenuProviderRegistry implements IContextMenuItemProvider { + + constructor(@multiInject(GLSP_TYPES.IContextMenuProvider) @optional() protected menuProviders: IContextMenuItemProvider[] = []) { } + + getItems(root: Readonly, lastMousePosition?: Point) { + const menues = this.menuProviders.map(provider => provider.getItems(root, lastMousePosition)); + return Promise.all(menues).then(this.flattenAndRestructure); + } + + private flattenAndRestructure(p: MenuItem[][]): MenuItem[] { + let menuItems = p.reduce((acc, promise) => promise !== undefined ? acc.concat(promise) : acc, []); + const menuItemsWithParentId = menuItems.filter(menuItem => menuItem.parentId); + for (const menuItem of menuItemsWithParentId) { + if (menuItem.parentId) { + const fragments = menuItem.parentId.split("."); + let matchingParent: MenuItem | undefined = undefined; + let nextParents = menuItems; + for (const fragment of fragments) { + matchingParent = nextParents.find(item => fragment === item.id); + if (matchingParent && matchingParent.children) + nextParents = matchingParent.children; + } + if (matchingParent) { + if (matchingParent.children) { + matchingParent.children.push(menuItem); + } else { + matchingParent.children = [menuItem]; + } + menuItems = menuItems.filter(item => item !== menuItem); + } + } + } + return menuItems; + } +} + +@injectable() +export class DeleteContextMenuProviderRegistry implements IContextMenuItemProvider { + getItems(root: Readonly, lastMousePosition?: Point): Promise { + const selectedElements = Array.from(root.index.all().filter(isSelected).filter(isDeletable)); + return Promise.resolve([ + { + id: "delete", + label: "Delete", + sortString: "t", + group: "edit", + actions: [new DeleteElementAction(selectedElements.map(e => e.id))], + isEnabled: () => selectedElements.length > 0, + isVisible: () => true, + isToggled: () => false + } + ]); + } +} diff --git a/src/features/context-menu/mouse-listener.ts b/src/features/context-menu/mouse-listener.ts new file mode 100644 index 000000000..00f0f5662 --- /dev/null +++ b/src/features/context-menu/mouse-listener.ts @@ -0,0 +1,48 @@ +/******************************************************************************** + * Copyright (c) 2019 EclipseSource 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 { inject, optional } from "inversify"; +import { Action, findParentByFeature, isSelectable, MouseListener, SModelElement, TYPES } from "sprotty/lib"; +import { DOMHelper } from "sprotty/lib/base/views/dom-helper"; + +import { GLSP_TYPES } from "../../types"; +import { IContextMenuServiceProvider } from "./context-menu-service"; +import { ContextMenuProviderRegistry } from "./menu-providers"; + +export class ContextMenuMouseListener extends MouseListener { + + constructor( + @inject(GLSP_TYPES.IContextMenuServiceProvider) @optional() protected readonly contextMenuService: IContextMenuServiceProvider, + @inject(GLSP_TYPES.IContextMenuProviderRegistry) @optional() protected readonly menuProvider: ContextMenuProviderRegistry, + @inject(TYPES.DOMHelper) protected domHelper: DOMHelper) { + super(); + } + + mouseDown(target: SModelElement, event: MouseEvent): (Action | Promise)[] { + if (event.button === 2 && this.contextMenuService && this.menuProvider) { + const mousePosition = { x: event.x, y: event.y }; + let isTargetSelected = false; + const selectableTarget = findParentByFeature(target, isSelectable); + if (selectableTarget) { + isTargetSelected = selectableTarget.selected; + selectableTarget.selected = true; + } + const restoreSelection = () => { if (selectableTarget) selectableTarget.selected = isTargetSelected; }; + Promise.all([this.contextMenuService(), this.menuProvider.getItems(target.root, mousePosition)]) + .then(([menuService, menuItems]) => menuService.show(menuItems, mousePosition, restoreSelection)); + } + return []; + } +} diff --git a/src/features/context-menu/server-context-menu-provider.ts b/src/features/context-menu/server-context-menu-provider.ts new file mode 100644 index 000000000..a02a63a44 --- /dev/null +++ b/src/features/context-menu/server-context-menu-provider.ts @@ -0,0 +1,46 @@ +/******************************************************************************** + * Copyright (c) 2019 EclipseSource 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 { inject, injectable } from "inversify"; +import { Action, LabeledAction, Point, SModelElement, subtract, TYPES } from "sprotty/lib"; + +import { isSelected } from "../../utils/smodel-util"; +import { ContextActions, isSetContextActionsAction, RequestContextActions } from "../context-actions/action-definitions"; +import { GLSPActionDispatcher } from "../request-response/glsp-action-dispatcher"; +import { IContextMenuItemProvider } from "./menu-providers"; + +export namespace ServerContextMenu { + export const KEY = "context-menu"; +} + +@injectable() +export class ServerContextMenuItemProvider implements IContextMenuItemProvider { + + constructor(@inject(TYPES.IActionDispatcher) protected actionDispatcher: GLSPActionDispatcher) { } + + getItems(root: Readonly, lastMousePosition?: Point): Promise { + const selectedElementIds = Array.from(root.index.all().filter(isSelected).map(e => e.id)); + const localPosition = lastMousePosition ? root.root.parentToLocal(subtract(lastMousePosition, root.root.canvasBounds)) : undefined; + const requestAction = new RequestContextActions(selectedElementIds, localPosition, { [ContextActions.UI_CONTROL_KEY]: ServerContextMenu.KEY }); + return this.actionDispatcher.requestUntil(requestAction).then(response => this.getContextActionsFromResponse(response)); + } + + getContextActionsFromResponse(action: Action): LabeledAction[] { + if (isSetContextActionsAction(action)) { + return action.actions; + } + return []; + } +} diff --git a/src/features/edit-label-validation/edit-label-validator.ts b/src/features/edit-label-validation/edit-label-validator.ts index 728d47ea8..4ab21c5e4 100644 --- a/src/features/edit-label-validation/edit-label-validator.ts +++ b/src/features/edit-label-validation/edit-label-validator.ts @@ -18,25 +18,33 @@ import { Action, EditableLabel, EditLabelValidationResult, + generateRequestId, IEditLabelValidationDecorator, IEditLabelValidator, + RequestAction, + ResponseAction, Severity, - SModelElement + SModelElement, + TYPES } from "sprotty"; -import { GLSP_TYPES } from "../../types"; -import { RequestResponseSupport } from "../request-response/support"; +import { GLSPActionDispatcher } from "../request-response/glsp-action-dispatcher"; -export class ValidateLabelEditAction implements Action { +export class ValidateLabelEditAction implements RequestAction { static readonly KIND = "validateLabelEdit"; kind = ValidateLabelEditAction.KIND; - constructor(public readonly value: string, public readonly labelId: string) { } + constructor( + public readonly value: string, + public readonly labelId: string, + public readonly requestId: string = generateRequestId()) { } } -export class SetLabelEditValidationResultAction implements Action { +export class SetLabelEditValidationResultAction implements ResponseAction { static readonly KIND = "setLabelEditValidationResult"; kind = SetLabelEditValidationResultAction.KIND; - constructor(public readonly result: EditLabelValidationResult) { } + constructor( + public readonly result: EditLabelValidationResult, + public readonly responseId: string = '') { } } export function isSetLabelEditValidationResultAction(action: Action): action is SetLabelEditValidationResultAction { @@ -47,11 +55,11 @@ export function isSetLabelEditValidationResultAction(action: Action): action is @injectable() export class ServerEditLabelValidator implements IEditLabelValidator { - @inject(GLSP_TYPES.RequestResponseSupport) protected requestResponseSupport: RequestResponseSupport; + @inject(TYPES.IActionDispatcher) protected actionDispatcher: GLSPActionDispatcher; validate(value: string, label: EditableLabel & SModelElement): Promise { const action = new ValidateLabelEditAction(value, label.id); - return this.requestResponseSupport.dispatchRequest(action, this.getValidationResultFromResponse); + return this.actionDispatcher.requestUntil(action).then(response => this.getValidationResultFromResponse(response)); } getValidationResultFromResponse(action: Action): EditLabelValidationResult { diff --git a/src/features/hints/di.config.ts b/src/features/hints/di.config.ts index d5c7a3a75..179c5534c 100644 --- a/src/features/hints/di.config.ts +++ b/src/features/hints/di.config.ts @@ -17,14 +17,14 @@ import { ContainerModule } from "inversify"; import { configureActionHandler, configureCommand } from "sprotty/lib"; import { GLSP_TYPES } from "../../types"; -import { SetTypeHintsAction } from "./action-definition"; -import { ApplyEditConfigCommand, TypeHintsEditConfigProvider } from "./type-hints-action-initializer"; +import { SetTypeHintsAction } from "./request-type-hints-action"; +import { ApplyTypeHintsCommand, TypeHintProvider } from "./type-hints"; const modelHintsModule = new ContainerModule((bind, _unbind, isBound) => { - bind(TypeHintsEditConfigProvider).toSelf().inSingletonScope(); - configureActionHandler({ bind, isBound }, SetTypeHintsAction.KIND, TypeHintsEditConfigProvider); - bind(GLSP_TYPES.IEditConfigProvider).toService(TypeHintsEditConfigProvider); - configureCommand({ bind, isBound }, ApplyEditConfigCommand); + bind(TypeHintProvider).toSelf().inSingletonScope(); + bind(GLSP_TYPES.ITypeHintProvider).toService(TypeHintProvider); + configureActionHandler({ bind, isBound }, SetTypeHintsAction.KIND, TypeHintProvider); + configureCommand({ bind, isBound }, ApplyTypeHintsCommand); }); export default modelHintsModule; diff --git a/src/features/nameable/model.ts b/src/features/hints/model.ts similarity index 55% rename from src/features/nameable/model.ts rename to src/features/hints/model.ts index 3aad5d2b8..0fc677f5f 100644 --- a/src/features/nameable/model.ts +++ b/src/features/hints/model.ts @@ -13,22 +13,20 @@ * * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ -import { SModelElement, SModelExtension } from "sprotty/lib"; +import { SModelElement, SModelElementSchema, SModelExtension } from "sprotty"; -export const nameFeature = Symbol('nameableFeature'); - -export interface Nameable extends SModelExtension { - name: string +export const containerFeature = Symbol("containable"); +export interface Containable extends SModelExtension { + isContainableElement(input: SModelElement | SModelElementSchema | string): boolean } -export function isNameable(element: SModelElement): element is SModelElement & Nameable { - return element.hasFeature(nameFeature); +export function isContainable(element: SModelElement): element is SModelElement & Containable { + return element.hasFeature(containerFeature); } -export function name(element: SModelElement): string { - if (isNameable(element)) { - return element.name; - } else { - return 'unnamed'; - } +export const reparentFeature = Symbol("reparentFeature"); +export interface Reparentable extends SModelExtension { } + +export function isReparentable(element: SModelElement): element is SModelElement & Reparentable { + return element.hasFeature(reparentFeature); } diff --git a/src/features/hints/action-definition.ts b/src/features/hints/request-type-hints-action.ts similarity index 52% rename from src/features/hints/action-definition.ts rename to src/features/hints/request-type-hints-action.ts index bd478cddb..4def9c922 100644 --- a/src/features/hints/action-definition.ts +++ b/src/features/hints/request-type-hints-action.ts @@ -15,6 +15,8 @@ ********************************************************************************/ import { Action } from "sprotty/lib"; +import { EdgeTypeHint, ShapeTypeHint } from "./type-hints"; + export class RequestTypeHintsAction implements Action { static readonly KIND = "requestTypeHints"; kind = RequestTypeHintsAction.KIND; @@ -24,59 +26,10 @@ export class RequestTypeHintsAction implements Action { export class SetTypeHintsAction implements Action { static readonly KIND = "setTypeHints"; kind = SetTypeHintsAction.KIND; - constructor(public readonly nodeHints: NodeTypeHint[], public readonly edgeHints: EdgeTypeHint[]) { } + constructor(public readonly shapeHints: ShapeTypeHint[], public readonly edgeHints: EdgeTypeHint[]) { } } export function isSetTypeHintsAction(action: Action): action is SetTypeHintsAction { return action !== undefined && (action.kind === SetTypeHintsAction.KIND) - && (action).nodeHints !== undefined && (action).edgeHints !== undefined; -} - -export interface TypeHint { - /** - The id of the element. - */ - readonly elementTypeId: string; - - /** - * Specifies whether the element can be relocated. - */ - readonly repositionable: boolean; - - /** - * Specifices wheter the element can be deleted - */ - readonly deletable: boolean; - -} - -export interface NodeTypeHint extends TypeHint { - /** - * Specifies whether the element can be resized. - */ - readonly resizable: boolean; - - /** - * The types of elements that can be contained by this element (if any) - */ - readonly containableElementTypeIds?: string[]; -} - -export interface EdgeTypeHint extends TypeHint { - - /** - * Specifies whether the routing of this element can be changed. - */ - readonly routable: boolean; - - /** - * Allowed source element types for this edge type - */ - readonly sourceElementTypeIds: string[]; - - /** - * Allowed targe element types for this edge type - */ - readonly targetElementTypeIds: string[]; - + && (action).shapeHints !== undefined && (action).edgeHints !== undefined; } diff --git a/src/features/hints/type-hints-action-initializer.ts b/src/features/hints/type-hints-action-initializer.ts deleted file mode 100644 index 2eb3a0f81..000000000 --- a/src/features/hints/type-hints-action-initializer.ts +++ /dev/null @@ -1,137 +0,0 @@ -/******************************************************************************** - * Copyright (c) 2019 EclipseSource 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 { inject, injectable } from "inversify"; -import { - Action, - CommandExecutionContext, - CommandReturn, - IActionHandler, - ICommand, - SModelElement, - SModelElementSchema, - TYPES -} from "sprotty/lib"; - -import { - EdgeEditConfig, - edgeEditConfig, - EditConfig, - IEditConfigProvider, - isEdgeEditConfig, - isNodeEditConfig, - NodeEditConfig, - nodeEditConfig -} from "../../base/edit-config/edit-config"; -import { GLSP_TYPES } from "../../types"; -import { contains } from "../../utils/array-utils"; -import { IFeedbackActionDispatcher } from "../tool-feedback/feedback-action-dispatcher"; -import { FeedbackCommand } from "../tool-feedback/model"; -import { EdgeTypeHint, isSetTypeHintsAction, NodeTypeHint } from "./action-definition"; - - -@injectable() -export class ApplyEditConfigAction implements Action { - readonly kind = ApplyEditConfigCommand.KIND; - constructor(public readonly editConfigs: Map) { } -} - -@injectable() -export class ApplyEditConfigCommand extends FeedbackCommand { - static KIND = "applyEditConfig"; - readonly priority = 10; - constructor(@inject(TYPES.Action) protected action: ApplyEditConfigAction) { - super(); - } - execute(context: CommandExecutionContext): CommandReturn { - context.root.index.all().forEach(element => { - const config = this.action.editConfigs.get(element.type); - if (config) { - Object.assign(element, config); - } - }); - return context.root; - } -} - -@injectable() -export class TypeHintsEditConfigProvider implements IActionHandler, IEditConfigProvider { - @inject(GLSP_TYPES.IFeedbackActionDispatcher) protected feedbackActionDispatcher: IFeedbackActionDispatcher; - - protected editConfigs: Map = new Map; - - handle(action: Action): ICommand | Action | void { - if (isSetTypeHintsAction(action)) { - action.nodeHints.forEach(hint => this.editConfigs.set(hint.elementTypeId, createNodeEditConfig(hint))); - action.edgeHints.forEach(hint => this.editConfigs.set(hint.elementTypeId, createEdgeEditConfig(hint))); - this.feedbackActionDispatcher.registerFeedback(this, [new ApplyEditConfigAction(this.editConfigs)]); - } - } - - getEditConfig(input: SModelElement | SModelElementSchema | string): EditConfig | undefined { - return this.editConfigs.get(getElementTypeId(input)); - } - - getAllEdgeEditConfigs(): EdgeEditConfig[] { - const configs: EdgeEditConfig[] = []; - this.editConfigs.forEach((value, key) => { - if (isEdgeEditConfig(value)) { - configs.push(value); - } - }); - return configs; - } - - getAllNodeEditConfigs(): NodeEditConfig[] { - const configs: NodeEditConfig[] = []; - this.editConfigs.forEach((value, key) => { - if (isNodeEditConfig(value)) { - configs.push(value); - } - }); - return configs; - } -} -export function createNodeEditConfig(hint: NodeTypeHint): NodeEditConfig { - return { - elementTypeId: hint.elementTypeId, - deletable: hint.deletable, - repositionable: hint.repositionable, - resizable: hint.resizable, - configType: nodeEditConfig, - isContainableElement: (element) => { return hint.containableElementTypeIds ? contains(hint.containableElementTypeIds, getElementTypeId(element)) : false; }, - isContainer: () => { return hint.containableElementTypeIds ? hint.containableElementTypeIds.length > 0 : false; } - }; -} - -export function createEdgeEditConfig(hint: EdgeTypeHint): EdgeEditConfig { - return { - elementTypeId: hint.elementTypeId, - deletable: hint.deletable, - repositionable: hint.repositionable, - routable: hint.routable, - configType: edgeEditConfig, - isAllowedSource: (source) => { return contains(hint.sourceElementTypeIds, getElementTypeId(source)); }, - isAllowedTarget: (target) => { return contains(hint.targetElementTypeIds, getElementTypeId(target)); } - }; -} - -function getElementTypeId(input: SModelElement | SModelElementSchema | string) { - if (typeof input === 'string') { - return input; - } else { - return (input)["type"]; - } -} diff --git a/src/features/hints/type-hints.ts b/src/features/hints/type-hints.ts new file mode 100644 index 000000000..ea8ef0715 --- /dev/null +++ b/src/features/hints/type-hints.ts @@ -0,0 +1,250 @@ +/******************************************************************************** + * Copyright (c) 2019 EclipseSource 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 { inject, injectable } from "inversify"; +import { + Action, + CommandExecutionContext, + CommandReturn, + Connectable, + connectableFeature, + deletableFeature, + editFeature, + FeatureSet, + IActionHandler, + ICommand, + moveFeature, + SEdge, + SModelElement, + SModelElementSchema, + SModelRoot, + SShapeElement, + TYPES +} from "sprotty/lib"; + +import { GLSP_TYPES } from "../../types"; +import { resizeFeature } from "../change-bounds/model"; +import { reconnectFeature } from "../reconnect/model"; +import { IFeedbackActionDispatcher } from "../tool-feedback/feedback-action-dispatcher"; +import { FeedbackCommand } from "../tool-feedback/model"; +import { Containable, containerFeature, reparentFeature } from "./model"; +import { isSetTypeHintsAction } from "./request-type-hints-action"; + +export abstract class TypeHint { + /** + The id of the element. + */ + readonly elementTypeId: string; + + /** + * Specifies whether the element can be relocated. + */ + readonly repositionable: boolean; + + /** + * Specifices wheter the element can be deleted + */ + readonly deletable: boolean; +} + +export class ShapeTypeHint extends TypeHint { + /** + * Specifies whether the element can be resized. + */ + readonly resizable: boolean; + + /** + * Specifies whether the element can be moved to another parent + */ + readonly reparentable: boolean; + + /** + * The types of elements that can be contained by this element (if any) + */ + readonly containableElementTypeIds?: string[]; +} + +export class EdgeTypeHint extends TypeHint { + /** + * Specifies whether the routing of this element can be changed. + */ + readonly routable: boolean; + + /** + * Allowed source element types for this edge type + */ + readonly sourceElementTypeIds: string[]; + + /** + * Allowed targe element types for this edge type + */ + readonly targetElementTypeIds: string[]; + + isAllowedSource(input: SModelElement | SModelElementSchema | string): boolean { + const type = getElementTypeId(input); + return this.sourceElementTypeIds.includes(type); + + } + isAllowedTarget(input: SModelElement | SModelElementSchema | string): boolean { + const type = getElementTypeId(input); + return this.targetElementTypeIds.includes(type); + } +} + +@injectable() +export class ApplyTypeHintsAction implements Action { + readonly kind = ApplyTypeHintsCommand.KIND; + constructor() { } +} + +@injectable() +export class ApplyTypeHintsCommand extends FeedbackCommand { + + static KIND = "applyTypeHints"; + readonly priority = 10; + + constructor(@inject(TYPES.Action) protected action: ApplyTypeHintsAction, + @inject(GLSP_TYPES.ITypeHintProvider) protected typeHintProvider: ITypeHintProvider) { + super(); + } + + execute(context: CommandExecutionContext): CommandReturn { + context.root.index.all() + .forEach(element => { + if (element instanceof SShapeElement || element instanceof SModelRoot) { + this.applyShapeTypeHint(element); + + } else if (element instanceof SEdge) { + return this.applyEdgeTypeHint(element); + } + }); + return context.root; + } + + protected applyEdgeTypeHint(element: SModelElement) { + const hint = this.typeHintProvider.getEdgeTypeHint(element); + if (hint && isModifiableFetureSet(element.features)) { + addOrRemove(element.features, deletableFeature, hint.deletable); + addOrRemove(element.features, editFeature, hint.routable); + addOrRemove(element.features, reconnectFeature, hint.repositionable); + } + } + + protected applyShapeTypeHint(element: SModelElement) { + const hint = this.typeHintProvider.getShapeTypeHint(element); + if (hint && isModifiableFetureSet(element.features)) { + addOrRemove(element.features, deletableFeature, hint.deletable); + addOrRemove(element.features, moveFeature, hint.repositionable); + addOrRemove(element.features, resizeFeature, hint.resizable); + addOrRemove(element.features, reparentFeature, hint.reparentable); + + addOrRemove(element.features, containerFeature, true); + const containable = createContainable(hint); + Object.assign(element, containable); + + addOrRemove(element.features, connectableFeature, true); + const validSourceEdges = this.typeHintProvider.getValidEdgeElementTypes(element, "source"); + const validTargetEdges = this.typeHintProvider.getValidEdgeElementTypes(element, "target"); + const connectable = createConnectable(validSourceEdges, validTargetEdges); + Object.assign(element, connectable); + } + } +} + +function createConnectable(validSourceEdges: string[], validTargetEdges: string[]): Connectable { + return { + canConnect: (routable, role) => + role === "source" ? + validSourceEdges.includes(routable.type) : + validTargetEdges.includes(routable.type) + }; +} + +function createContainable(hint: ShapeTypeHint): Containable { + return { + isContainableElement: (element) => + hint.containableElementTypeIds ? + hint.containableElementTypeIds.includes(getElementTypeId(element)) : + false + }; +} + +function addOrRemove(features: Set, feature: symbol, add: boolean) { + if (add && !features.has(feature)) { + features.add(feature); + } else if (!add && features.has(feature)) { + features.delete(feature); + } +} +function isModifiableFetureSet(featureSet?: FeatureSet): featureSet is FeatureSet & Set { + return featureSet !== undefined && featureSet instanceof Set; +} + +export interface ITypeHintProvider { + getShapeTypeHint(input: SModelElement | SModelElement | string): ShapeTypeHint | undefined; + getEdgeTypeHint(input: SModelElement | SModelElement | string): EdgeTypeHint | undefined; + getValidEdgeElementTypes(input: SModelElement | SModelElement | string, role: "source" | "target"): string[]; +} + +@injectable() +export class TypeHintProvider implements IActionHandler, ITypeHintProvider { + + @inject(GLSP_TYPES.IFeedbackActionDispatcher) + protected feedbackActionDispatcher: IFeedbackActionDispatcher; + + protected shapeHints: Map = new Map; + protected edgeHints: Map = new Map; + + handle(action: Action): ICommand | Action | void { + if (isSetTypeHintsAction(action)) { + action.shapeHints.forEach(hint => this.shapeHints.set(hint.elementTypeId, hint)); + action.edgeHints.forEach(hint => this.edgeHints.set(hint.elementTypeId, hint)); + this.feedbackActionDispatcher.registerFeedback(this, [new ApplyTypeHintsAction()]); + } + } + + getValidEdgeElementTypes(input: SModelElement | SModelElement | string, role: "source" | "target"): string[] { + const elementTypeId = getElementTypeId(input); + if (role === "source") { + return Array.from( + Array.from(this.edgeHints.values()) + .filter(hint => hint.sourceElementTypeIds.includes(elementTypeId)) + .map(hint => hint.elementTypeId)); + } else { + return Array.from( + Array.from(this.edgeHints.values()) + .filter(hint => hint.targetElementTypeIds.includes(elementTypeId)) + .map(hint => hint.elementTypeId)); + } + } + + getShapeTypeHint(input: SModelElement | SModelElement | string) { + const type = getElementTypeId(input); + return this.shapeHints.get(type); + } + + getEdgeTypeHint(input: SModelElement | SModelElement | string) { + const type = getElementTypeId(input); + return this.edgeHints.get(type); + } +} +export function getElementTypeId(input: SModelElement | SModelElementSchema | string) { + if (typeof input === 'string') { + return input; + } else { + return (input)["type"]; + } +} + diff --git a/src/features/reconnect/model.ts b/src/features/reconnect/model.ts index a5e80add3..2d977d7ba 100644 --- a/src/features/reconnect/model.ts +++ b/src/features/reconnect/model.ts @@ -19,23 +19,25 @@ import { RoutingHandleKind, selectFeature, SModelElement, + SModelExtension, SRoutableElement, SRoutingHandle } from "sprotty/lib"; -const ROUTING_HANDLE_SOURCE_INDEX: number = -2; +export const reconnectFeature = Symbol("reconnectFeature"); +export interface Reconnectable extends SModelExtension { +} -export function isRoutable(element: T): element is T & SRoutableElement { - return element instanceof SRoutableElement && (element as any).routingPoints !== undefined; +export function isReconnectable(element: SModelElement): element is SRoutableElement & Reconnectable { + return element instanceof SRoutableElement && element.hasFeature(reconnectFeature); } +const ROUTING_HANDLE_SOURCE_INDEX: number = -2; + export function isReconnectHandle(element: SModelElement | undefined): element is SReconnectHandle { return element !== undefined && element instanceof SReconnectHandle; } -export function isRoutingHandle(element: SModelElement | undefined): element is SRoutingHandle { - return element !== undefined && element instanceof SRoutingHandle; -} export function addReconnectHandles(element: SRoutableElement) { removeReconnectHandles(element); diff --git a/src/features/request-response/action-definitions.ts b/src/features/request-response/action-definitions.ts deleted file mode 100644 index 6da86bab2..000000000 --- a/src/features/request-response/action-definitions.ts +++ /dev/null @@ -1,40 +0,0 @@ -/******************************************************************************** - * Copyright (c) 2019 EclipseSource 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 { Action } from "sprotty/lib"; - -export class IdentifiableRequestAction implements Action { - static readonly KIND = "identifiableRequestAction"; - kind = IdentifiableRequestAction.KIND; - constructor(public readonly id: string, public readonly action: Action) { } -} - -export class IdentifiableResponseAction implements Action { - static readonly KIND = "identifiableResponseAction"; - kind = IdentifiableResponseAction.KIND; - constructor(public readonly id: string, public readonly action: Action) { } -} - -export function isIdentifiableRequestAction(action: Action): action is IdentifiableRequestAction { - return action !== undefined && (action.kind === IdentifiableRequestAction.KIND) - && (action).id !== undefined - && (action).action !== undefined; -} - -export function isIdentifiableResponseAction(action: Action): action is IdentifiableResponseAction { - return action !== undefined && (action.kind === IdentifiableResponseAction.KIND) - && (action).id !== undefined - && (action).action !== undefined; -} diff --git a/src/features/request-response/di.config.ts b/src/features/request-response/di.config.ts index 0ed443499..5fded85fe 100644 --- a/src/features/request-response/di.config.ts +++ b/src/features/request-response/di.config.ts @@ -16,14 +16,11 @@ import { ContainerModule } from "inversify"; import { TYPES } from "sprotty"; -import { GLSP_TYPES } from "../../types"; -import { RequestResponseSupport } from "./support"; +import { GLSPActionDispatcher } from "./glsp-action-dispatcher"; const requestResponseModule = new ContainerModule((bind, unbind, isBound, rebind) => { - bind(RequestResponseSupport).toSelf().inSingletonScope(); - bind(GLSP_TYPES.RequestResponseSupport).toService(RequestResponseSupport); - bind(TYPES.IActionHandlerInitializer).toService(RequestResponseSupport); + rebind(TYPES.IActionDispatcher).to(GLSPActionDispatcher).inSingletonScope(); }); export default requestResponseModule; diff --git a/src/features/request-response/glsp-action-dispatcher.ts b/src/features/request-response/glsp-action-dispatcher.ts new file mode 100644 index 000000000..371140c28 --- /dev/null +++ b/src/features/request-response/glsp-action-dispatcher.ts @@ -0,0 +1,77 @@ +/******************************************************************************** + * Copyright (c) 2019 EclipseSource 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 { Action, ActionDispatcher, isResponseAction, RequestAction, ResponseAction } from "sprotty"; + +export class GLSPActionDispatcher extends ActionDispatcher { + + protected readonly timeouts: Map = new Map(); + + protected handleAction(action: Action): Promise { + if (isResponseAction(action)) { + // clear timeout + const timeout = this.timeouts.get(action.responseId); + if (timeout !== undefined) { + clearTimeout(timeout); + this.timeouts.delete(action.responseId); + } + + // we might have reached a timeout, so we simply drop the response + const deferred = this.requests.get(action.responseId); + if (deferred === undefined) { + this.logger.log(this, 'No matching request for response', action); + return Promise.resolve(); + } + } + return super.handleAction(action); + } + + /** + * Dispatch a request and waits for a response until the timeout given in `timeoutMs` has + * been reached. The returned promise is resolved when a response with matching identifier + * is dispatched or when the timeout has been reached. 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. + * If `rejectOnTimeout` is set to false (default) the returned promise will be resolved with + * no value, otherwise it will be rejected. + */ + requestUntil(action: RequestAction, timeoutMs: number = 2000, rejectOnTimeout: boolean = false): Promise { + if (!action.requestId) { + return Promise.reject(new Error('Request without requestId')); + } + + const requestId = action.requestId; + const timeout = setTimeout(() => { + const deferred = this.requests.get(requestId); + if (deferred !== undefined) { + // cleanup + clearTimeout(timeout); + this.requests.delete(requestId); + + const notification = 'Request ' + requestId + ' (' + action + ') time out after ' + timeoutMs + 'ms.'; + if (rejectOnTimeout) { + deferred.reject(notification); + } else { + this.logger.info(this, notification); + deferred.resolve(); + } + } + }, timeoutMs); + this.timeouts.set(requestId, timeout); + + return super.request(action); + } +} diff --git a/src/features/request-response/support.ts b/src/features/request-response/support.ts deleted file mode 100644 index 06b07c73e..000000000 --- a/src/features/request-response/support.ts +++ /dev/null @@ -1,97 +0,0 @@ -/******************************************************************************** - * Copyright (c) 2019 EclipseSource 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 { inject, injectable } from "inversify"; -import { Action, ActionHandlerRegistry, IActionDispatcher, IActionHandlerInitializer, ILogger, TYPES } from "sprotty/lib"; -import { v4 as uuid } from "uuid"; - -import { IdentifiableRequestAction, IdentifiableResponseAction, isIdentifiableResponseAction } from "./action-definitions"; - - -@injectable() -export class RequestResponseSupport implements IActionHandlerInitializer { - private requestedResponses = new Map(); - - @inject(TYPES.IActionDispatcher) protected actionDispatcher: IActionDispatcher; - @inject(TYPES.ActionHandlerRegistryProvider) protected registry: ActionHandlerRegistry; - @inject(TYPES.ILogger) protected logger: ILogger; - - initialize(registry: ActionHandlerRegistry): void { - registry.register(IdentifiableResponseAction.KIND, this); - } - - handle(response: Action) { - if (isIdentifiableResponseAction(response)) { - const responseId = response.id; - if (this.requestedResponses.has(responseId)) { - this.requestedResponses.set(responseId, response.action); - } else { - this.logger.log(this, "[RequestResponse] " + responseId + ": Response without request, ignore."); - } - } - } - - async dispatchRequest(request: Action, responseHandler: (action: Action) => T, // - intervalMs: number = 100, timeoutMs: number = 2000, rejectOnTimeout: boolean = false): Promise { - - const requestId = uuid(); - const requestAction = new IdentifiableRequestAction(requestId, request); - this.requestedResponses.set(requestAction.id, undefined); - - await this.actionDispatcher.dispatch(requestAction); - this.logger.log(this, "[RequestResponse] " + requestId + ": Request for " + JSON.stringify(requestAction.action) + " dispatched."); - - let timeout: NodeJS.Timeout; - let responseInterval: NodeJS.Timeout; - - const requestPromise = new Promise((resolve, reject) => { - responseInterval = setInterval(() => { - const responseAction = this.requestedResponses.get(requestId); - if (responseAction) { - this.logger.log(this, "[RequestResponse] " + requestId + ": Response for request received."); - - // cleanup - clearTimeout(timeout); - clearInterval(responseInterval); - this.requestedResponses.delete(requestId); - - // handle result - const result = responseHandler(responseAction); - return resolve(result); - } - }, intervalMs); - return []; - }); - - const timeoutPromise = new Promise((resolve, reject) => { - timeout = setTimeout(() => { - this.logger.warn(this, "[RequestResponse] " + requestId + ": No response received after " + timeoutMs + "ms."); - - // cleanup - clearTimeout(timeout); - clearInterval(responseInterval); - this.requestedResponses.delete(requestId); - - // handle timeout: reject or resolve with undefined - if (rejectOnTimeout) { - return reject("No response for " + requestId + " (" + requestAction.action + ") timed out after " + timeoutMs + "!"); - } - return resolve(); - }, timeoutMs); - return []; - }); - return Promise.race([requestPromise, timeoutPromise]); - } -} diff --git a/src/features/tool-feedback/change-bounds-tool-feedback.ts b/src/features/tool-feedback/change-bounds-tool-feedback.ts index ac89b646c..9d6625310 100644 --- a/src/features/tool-feedback/change-bounds-tool-feedback.ts +++ b/src/features/tool-feedback/change-bounds-tool-feedback.ts @@ -34,7 +34,7 @@ import { import { isNotUndefined } from "../../utils/smodel-util"; import { getAbsolutePosition } from "../../utils/viewpoint-util"; -import { addResizeHandles, isBoundsAwareMoveable, isResizeable, removeResizeHandles } from "../change-bounds/model"; +import { addResizeHandles, isBoundsAwareMoveable, isResizable, removeResizeHandles } from "../change-bounds/model"; import { IMovementRestrictor } from "../change-bounds/movement-restrictor"; import { FeedbackCommand } from "./model"; @@ -58,11 +58,11 @@ export class ShowChangeBoundsToolResizeFeedbackCommand extends FeedbackCommand { execute(context: CommandExecutionContext): CommandReturn { const index = context.root.index; - index.all().filter(isResizeable).forEach(removeResizeHandles); + index.all().filter(isResizable).forEach(removeResizeHandles); if (isNotUndefined(this.action.elementId)) { const resizeElement = index.getById(this.action.elementId); - if (isNotUndefined(resizeElement) && isResizeable(resizeElement)) { + if (isNotUndefined(resizeElement) && isResizable(resizeElement)) { addResizeHandles(resizeElement); } } @@ -80,7 +80,7 @@ export class HideChangeBoundsToolResizeFeedbackCommand extends FeedbackCommand { execute(context: CommandExecutionContext): CommandReturn { const index = context.root.index; - index.all().filter(isResizeable).forEach(removeResizeHandles); + index.all().filter(isResizable).forEach(removeResizeHandles); return context.root; } } diff --git a/src/features/tool-feedback/creation-tool-feedback.ts b/src/features/tool-feedback/creation-tool-feedback.ts index 7fbcb8e56..b1454b33c 100644 --- a/src/features/tool-feedback/creation-tool-feedback.ts +++ b/src/features/tool-feedback/creation-tool-feedback.ts @@ -31,23 +31,22 @@ import { SChildElement, SConnectableElement, SDanglingAnchor, + SEdgeSchema, SModelElement, SModelRoot, SRoutableElement, TYPES } from "sprotty/lib"; -import { getAbsolutePosition } from "../../utils/viewpoint-util"; -import { isRoutable } from "../reconnect/model"; +import { isRoutable } from "../../utils/smodel-util"; +import { getAbsolutePosition, toAbsolutePosition } from "../../utils/viewpoint-util"; import { FeedbackCommand } from "./model"; - export class DrawFeedbackEdgeAction implements Action { kind = DrawFeedbackEdgeCommand.KIND; - constructor(readonly elementTypeId: string, readonly sourceId: string) { } + constructor(readonly elementTypeId: string, readonly sourceId: string, readonly routerKind?: string) { } } - @injectable() export class DrawFeedbackEdgeCommand extends FeedbackCommand { static readonly KIND = 'drawFeedbackEdge'; @@ -57,7 +56,7 @@ export class DrawFeedbackEdgeCommand extends FeedbackCommand { } execute(context: CommandExecutionContext): CommandReturn { - drawFeedbackEdge(context, this.action.sourceId, this.action.elementTypeId); + drawFeedbackEdge(context, this.action.sourceId, this.action.elementTypeId, this.action.routerKind); return context.root; } } @@ -126,7 +125,7 @@ export function feedbackEdgeEndId(root: SModelRoot): string { return root.id + '_feedback_anchor'; } -function drawFeedbackEdge(context: CommandExecutionContext, sourceId: string, elementTypeId: string) { +function drawFeedbackEdge(context: CommandExecutionContext, sourceId: string, elementTypeId: string, routerKind?: string) { const root = context.root; const sourceChild = root.index.getById(sourceId); if (!sourceChild) { @@ -140,13 +139,15 @@ function drawFeedbackEdge(context: CommandExecutionContext, sourceId: string, el const edgeEnd = new FeedbackEdgeEnd(source.id, elementTypeId); edgeEnd.id = feedbackEdgeEndId(root); - edgeEnd.position = { x: source.bounds.x, y: source.bounds.y }; + edgeEnd.position = toAbsolutePosition(source); - const feedbackEdgeSchema = { + const feedbackEdgeSchema = { type: 'edge', id: feedbackEdgeId(root), sourceId: source.id, targetId: edgeEnd.id, + cssClasses: ["feedback-edge"], + routerKind, opacity: 0.3 }; diff --git a/src/features/tool-feedback/edge-edit-tool-feedback.ts b/src/features/tool-feedback/edge-edit-tool-feedback.ts index b770b95d1..b149a5fd5 100644 --- a/src/features/tool-feedback/edge-edit-tool-feedback.ts +++ b/src/features/tool-feedback/edge-edit-tool-feedback.ts @@ -42,9 +42,9 @@ import { TYPES } from "sprotty/lib"; -import { isNotUndefined, isSelected } from "../../utils/smodel-util"; +import { isNotUndefined, isRoutable, isRoutingHandle, isSelected } from "../../utils/smodel-util"; import { getAbsolutePosition } from "../../utils/viewpoint-util"; -import { addReconnectHandles, isRoutable, isRoutingHandle, removeReconnectHandles } from "../reconnect/model"; +import { addReconnectHandles, removeReconnectHandles } from "../reconnect/model"; import { FeedbackEdgeEnd, feedbackEdgeEndId, @@ -181,7 +181,6 @@ export class FeedbackEdgeSourceMovingMouseListener extends MouseListener { } } - export class FeedbackEdgeRouteMovingMouseListener extends MouseListener { hasDragged = false; lastDragPosition: Point | undefined; diff --git a/src/features/tool-feedback/view.tsx b/src/features/tool-feedback/view.tsx index 5f2b43c4e..b3c5aca09 100644 --- a/src/features/tool-feedback/view.tsx +++ b/src/features/tool-feedback/view.tsx @@ -13,13 +13,14 @@ * * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ - -/** @jsx svg */ -import { injectable } from 'inversify'; -import { svg } from 'snabbdom-jsx'; +import { injectable } from "inversify"; +import * as snabbdom from "snabbdom-jsx"; import { VNode } from "snabbdom/vnode"; import { IView, ORIGIN_POINT, Point, RenderingContext, setAttr, SModelElement } from "sprotty/lib"; -import { isResizeable, ResizeHandleLocation, SResizeHandle } from '../change-bounds/model'; + +import { isResizable, ResizeHandleLocation, SResizeHandle } from "../change-bounds/model"; + +const JSX = { createElement: snabbdom.svg }; /** * This view is used for the invisible end of the feedback edge. @@ -49,7 +50,7 @@ export class SResizeHandleView implements IView { protected getPosition(handle: SResizeHandle): Point | undefined { const parent = handle.parent; - if (isResizeable(parent)) { + if (isResizable(parent)) { if (handle.location === ResizeHandleLocation.TopLeft) { return { x: 0, y: 0 }; } else if (handle.location === ResizeHandleLocation.TopRight) { diff --git a/src/features/tools/change-bounds-tool.ts b/src/features/tools/change-bounds-tool.ts index 48a120140..8bac88e70 100644 --- a/src/features/tools/change-bounds-tool.ts +++ b/src/features/tools/change-bounds-tool.ts @@ -33,7 +33,7 @@ import { import { GLSP_TYPES } from "../../types"; import { forEachElement, isNonRoutableSelectedBoundsAware, isSelected, toElementAndBounds } from "../../utils/smodel-util"; -import { isBoundsAwareMoveable, isResizeable, ResizeHandleLocation, SResizeHandle } from "../change-bounds/model"; +import { isBoundsAwareMoveable, isResizable, ResizeHandleLocation, SResizeHandle } from "../change-bounds/model"; import { IMovementRestrictor } from "../change-bounds/movement-restrictor"; import { IMouseTool } from "../mouse-tool/mouse-tool"; import { ChangeBoundsOperationAction } from "../operation/operation-actions"; @@ -148,7 +148,7 @@ class ChangeBoundsListener extends MouseListener implements SelectionListener { const actions: Action[] = []; if (this.activeResizeHandle) { // An action. Resize, not move. - const resizeElement = findParentByFeature(this.activeResizeHandle, isResizeable); + const resizeElement = findParentByFeature(this.activeResizeHandle, isResizable); if (this.isActiveResizeElement(resizeElement)) { createChangeBoundsAction(resizeElement).forEach(action => actions.push(action)); } @@ -238,7 +238,7 @@ class ChangeBoundsListener extends MouseListener implements SelectionListener { } const actions: Action[] = []; - const resizeElement = findParentByFeature(this.activeResizeHandle, isResizeable); + const resizeElement = findParentByFeature(this.activeResizeHandle, isResizable); if (this.isActiveResizeElement(resizeElement)) { switch (this.activeResizeHandle.location) { case ResizeHandleLocation.TopLeft: @@ -318,3 +318,4 @@ function minHeight(element: SModelElement & BoundsAware): number { return 1; } + diff --git a/src/features/tools/creation-tool.ts b/src/features/tools/creation-tool.ts index 4ef4a3247..378d1dacd 100644 --- a/src/features/tools/creation-tool.ts +++ b/src/features/tools/creation-tool.ts @@ -18,19 +18,19 @@ import { Action, AnchorComputerRegistry, EnableDefaultToolsAction, - findParent, findParentByFeature, isConnectable, isCtrlOrCmd, + SEdge, SModelElement, - SModelRoot, Tool } from "sprotty/lib"; -import { containmentAllowed, EdgeEditConfig, edgeEditConfig, IEditConfigProvider } from "../../base/edit-config/edit-config"; import { TypeAware } from "../../base/tool-manager/tool-manager-action-handler"; import { GLSP_TYPES } from "../../types"; import { getAbsolutePosition } from "../../utils/viewpoint-util"; +import { Containable, isContainable } from "../hints/model"; +import { ITypeHintProvider } from "../hints/type-hints"; import { IMouseTool } from "../mouse-tool/mouse-tool"; import { CreateConnectionOperationAction, CreateNodeOperationAction } from "../operation/operation-actions"; import { deriveOperationId, OperationKind } from "../operation/set-operations"; @@ -43,7 +43,6 @@ import { ApplyCursorCSSFeedbackAction, CursorCSS } from "../tool-feedback/cursor import { IFeedbackActionDispatcher } from "../tool-feedback/feedback-action-dispatcher"; import { DragAwareMouseListener } from "./drag-aware-mouse-listener"; - export const TOOL_ID_PREFIX = "tool"; export function deriveToolId(operationKind: string, elementTypeId?: string) { @@ -80,18 +79,18 @@ export class NodeCreationTool implements Tool, TypeAware { @injectable() export class NodeCreationToolMouseListener extends DragAwareMouseListener { - protected container?: SModelElement; + protected container?: SModelElement & Containable; constructor(protected elementTypeId: string, protected tool: NodeCreationTool) { super(); } - protected creationAllowed(target: SModelElement) { - return this.container || target instanceof SModelRoot; + protected creationAllowed(elementTypeId: string) { + return this.container && this.container.isContainableElement(elementTypeId); } nonDraggingMouseUp(target: SModelElement, event: MouseEvent): Action[] { const result: Action[] = []; - if (this.creationAllowed(target)) { + if (this.creationAllowed(this.elementTypeId)) { const containerId = this.container ? this.container.id : undefined; const location = getAbsolutePosition(target, event); result.push(new CreateNodeOperationAction(this.elementTypeId, location, containerId)); @@ -103,10 +102,10 @@ export class NodeCreationToolMouseListener extends DragAwareMouseListener { } mouseOver(target: SModelElement, event: MouseEvent): Action[] { - const currentContainer = findParent(target, e => containmentAllowed(e, this.elementTypeId)); + const currentContainer = findParentByFeature(target, isContainable); if (!this.container || currentContainer !== this.container) { this.container = currentContainer; - const feedback = this.creationAllowed(target) + const feedback = this.creationAllowed(this.elementTypeId) ? new ApplyCursorCSSFeedbackAction(CursorCSS.NODE_CREATION) : new ApplyCursorCSSFeedbackAction(CursorCSS.OPERATION_NOT_ALLOWED); this.tool.dispatchFeedback([feedback]); @@ -128,7 +127,7 @@ export class EdgeCreationTool implements Tool, TypeAware { constructor(@inject(GLSP_TYPES.MouseTool) protected mouseTool: IMouseTool, @inject(GLSP_TYPES.IFeedbackActionDispatcher) protected feedbackDispatcher: IFeedbackActionDispatcher, @inject(AnchorComputerRegistry) protected anchorRegistry: AnchorComputerRegistry, - @inject(GLSP_TYPES.IEditConfigProvider) public readonly editConfigProvider: IEditConfigProvider) { } + @inject(GLSP_TYPES.ITypeHintProvider) public readonly typeHintProvider: ITypeHintProvider) { } get id() { return deriveToolId(OperationKind.CREATE_CONNECTION, this.elementTypeId); @@ -160,14 +159,11 @@ export class EdgeCreationToolMouseListener extends DragAwareMouseListener { protected target?: string; protected currentTarget?: SModelElement; protected allowedTarget: boolean = false; - protected edgeEditConfig?: EdgeEditConfig; - + protected proxyEdge: SEdge; constructor(protected elementTypeId: string, protected tool: EdgeCreationTool) { super(); - const config = tool.editConfigProvider.getEditConfig(this.elementTypeId); - if (config && config.configType === edgeEditConfig) { - this.edgeEditConfig = config as EdgeEditConfig; - } + this.proxyEdge = new SEdge(); + this.proxyEdge.type = elementTypeId; } protected reinitialize() { @@ -236,10 +232,11 @@ export class EdgeCreationToolMouseListener extends DragAwareMouseListener { } protected isAllowedSource(element: SModelElement | undefined): boolean { - return element !== undefined && this.edgeEditConfig ? this.edgeEditConfig.isAllowedSource(element) : false; + return element !== undefined && isConnectable(element) && element.canConnect(this.proxyEdge, "source"); } protected isAllowedTarget(element: SModelElement | undefined): boolean { - return element !== undefined && this.edgeEditConfig ? this.edgeEditConfig.isAllowedTarget(element) : false; + return element !== undefined && isConnectable(element) && element.canConnect(this.proxyEdge, "target"); + } } diff --git a/src/features/tools/delete-tool.ts b/src/features/tools/delete-tool.ts index 2900c5751..9b6664cb8 100644 --- a/src/features/tools/delete-tool.ts +++ b/src/features/tools/delete-tool.ts @@ -18,12 +18,12 @@ import { Action, EnableDefaultToolsAction, isCtrlOrCmd, + isDeletable, isSelectable, KeyListener, KeyTool, MouseListener, SModelElement, - SModelRoot, Tool } from "sprotty/lib"; import { matchesKeystroke } from "sprotty/lib/utils/keyboard"; @@ -59,7 +59,7 @@ export class DelKeyDeleteTool implements Tool { export class DeleteKeyListener extends KeyListener { keyDown(element: SModelElement, event: KeyboardEvent): Action[] { if (matchesKeystroke(event, 'Delete')) { - const deleteElementIds = Array.from(element.root.index.all().filter(e => isSelectable(e) && e.selected) + const deleteElementIds = Array.from(element.root.index.all().filter(e => isDeletable(e) && isSelectable(e) && e.selected) .filter(e => e.id !== e.root.id).map(e => e.id)); return [new DeleteElementOperationAction(deleteElementIds)]; } @@ -90,14 +90,12 @@ export class MouseDeleteTool implements Tool { this.mouseTool.deregister(this.deleteToolMouseListener); this.feedbackDispatcher.registerFeedback(this, [new ApplyCursorCSSFeedbackAction()]); } - - } @injectable() export class DeleteToolMouseListener extends MouseListener { mouseUp(target: SModelElement, event: MouseEvent): Action[] { - if (target instanceof SModelRoot) { + if (!isDeletable(target)) { return []; } diff --git a/src/features/tools/edge-edit-tool.ts b/src/features/tools/edge-edit-tool.ts index 3dd298f09..f92c4bf4b 100644 --- a/src/features/tools/edge-edit-tool.ts +++ b/src/features/tools/edge-edit-tool.ts @@ -17,6 +17,7 @@ import { inject, injectable, optional } from "inversify"; import { Action, AnchorComputerRegistry, + canEditRouting, Connectable, EdgeRouterRegistry, findParentByFeature, @@ -29,15 +30,13 @@ import { Tool } from "sprotty/lib"; -import { isConfigurableEdge } from "../../base/edit-config/edit-config"; import { GLSP_TYPES } from "../../types"; -import { isSelected } from "../../utils/smodel-util"; +import { isRoutable, isRoutingHandle, isSelected } from "../../utils/smodel-util"; import { IMouseTool } from "../mouse-tool/mouse-tool"; import { ReconnectConnectionOperationAction, RerouteConnectionOperationAction } from "../reconnect/action-definitions"; import { + isReconnectable, isReconnectHandle, - isRoutable, - isRoutingHandle, isSourceRoutingHandle, isTargetRoutingHandle, SReconnectHandle @@ -130,7 +129,14 @@ class ReconnectEdgeListener extends MouseListener implements SelectionListener { this.edge = edge; // note: order is important here as we want the reconnect handles to cover the routing handles - this.tool.dispatchFeedback([new SwitchRoutingModeAction([this.edge.id], []), new ShowEdgeReconnectHandlesFeedbackAction(this.edge.id)]); + const feedbackActions = []; + if (canEditRouting(edge)) { + feedbackActions.push(new SwitchRoutingModeAction([this.edge.id], [])); + } + if (isReconnectable(edge)) { + feedbackActions.push(new ShowEdgeReconnectHandlesFeedbackAction(this.edge.id)); + } + this.tool.dispatchFeedback(feedbackActions); } protected isEdgeSelected(): boolean { @@ -244,9 +250,9 @@ class ReconnectEdgeListener extends MouseListener implements SelectionListener { const currentTarget = findParentByFeature(target, isConnectable); if (!this.newConnectable || currentTarget !== this.newConnectable) { this.setNewConnectable(currentTarget); - if (currentTarget && isConfigurableEdge(this.edge)) { - if ((this.reconnectMode === 'NEW_SOURCE' && this.edge.isAllowedSource(currentTarget.type)) || - (this.reconnectMode === 'NEW_TARGET' && this.edge.isAllowedTarget(currentTarget.type))) { + if (currentTarget) { + if ((this.reconnectMode === 'NEW_SOURCE' && currentTarget.canConnect(this.edge, "source")) || + (this.reconnectMode === 'NEW_TARGET' && currentTarget.canConnect(this.edge, "target"))) { this.tool.dispatchFeedback([new ApplyCursorCSSFeedbackAction(CursorCSS.EDGE_RECONNECT)]); return []; diff --git a/src/index.ts b/src/index.ts index c3f419598..c16a48837 100644 --- a/src/index.ts +++ b/src/index.ts @@ -15,6 +15,7 @@ ********************************************************************************/ import defaultGLSPModule from "./base/di.config"; import glspCommandPaletteModule from "./features/command-palette/di.config"; +import glspContextMenuModule from "./features/context-menu/di.config"; import glspEditLabelValidationModule from "./features/edit-label-validation/di.config"; import executeModule from "./features/execute/di.config"; import modelHintsModule from "./features/hints/di.config"; @@ -28,47 +29,61 @@ import paletteModule from "./features/tool-palette/di.config"; import validationModule from "./features/validation/di.config"; export * from 'sprotty/lib'; -export * from './base/edit-config/edit-config'; -export * from './model-source/websocket-diagram-server'; + export * from './base/model/update-model-command'; export * from './base/tool-manager/tool-manager-action-handler'; +export * from './base/command-stack'; export * from './features/change-bounds/model'; export * from './features/change-bounds/movement-restrictor'; -export * from './features/command-palette/action-definitions'; -export * from './features/command-palette/action-provider'; +export * from './features/context-actions/action-definitions'; +export * from './features/command-palette/server-command-palette-provider'; +export * from './features/context-menu/mouse-listener'; +export * from './features/context-menu/context-menu-service'; +export * from './features/context-menu/menu-providers'; +export * from './features/edit-label-validation/edit-label-validator'; export * from './features/execute/execute-command'; export * from './features/execute/model'; -export * from './features/hints/action-definition'; -export * from './features/hints/type-hints-action-initializer'; -export * from './features/mouse-tool/di.config'; +export * from './features/hints/request-type-hints-action'; +export * from './features/hints/type-hints'; +export * from './features/hints/model'; +export * from './features/layout/layout-commands'; +export * from './features/mouse-tool/mouse-tool'; export * from './features/operation/operation-actions'; export * from './features/operation/set-operations'; -export * from './features/request-response/action-definitions'; -export * from './features/request-response/support'; +export * from './features/rank/model'; +export * from './features/reconnect/action-definitions'; +export * from './features/reconnect/model'; +export * from './features/request-response/glsp-action-dispatcher'; export * from './features/save/model'; export * from './features/save/save'; -export * from './features/select/di.config'; +export * from './features/tool-feedback/change-bounds-tool-feedback'; export * from './features/tool-feedback/creation-tool-feedback'; +export * from './features/tool-feedback/cursor-feedback'; +export * from './features/tool-feedback/edge-edit-tool-feedback'; +export * from './features/tool-feedback/feedback-action-dispatcher'; +export * from './features/tool-feedback/model'; export * from './features/tool-feedback/model'; export * from './features/tool-palette/tool-palette'; export * from './features/tools/change-bounds-tool'; export * from './features/tools/creation-tool'; export * from './features/tools/default-tools'; export * from './features/tools/delete-tool'; +export * from './features/tools/drag-aware-mouse-listener'; +export * from './features/tools/edge-edit-tool'; + export * from './features/undo-redo/model'; export * from './features/validation/validate'; -export * from './features/layout/layout-commands'; export * from './lib/model'; export * from './types'; export * from './utils/array-utils'; export * from './utils/marker'; export * from './utils/smodel-util'; export * from './utils/viewpoint-util'; - +export * from './model-source/websocket-diagram-server'; export * from "./model-source/glsp-server-status"; export { validationModule, saveModule, executeModule, paletteModule, toolFeedbackModule, defaultGLSPModule, modelHintsModule, glspCommandPaletteModule, requestResponseModule, // - glspSelectModule, glspMouseToolModule, layoutCommandsModule, glspEditLabelValidationModule + glspContextMenuModule, glspSelectModule, glspMouseToolModule, layoutCommandsModule, glspEditLabelValidationModule }; diff --git a/src/lib/model.ts b/src/lib/model.ts index 76a2c3d14..5f3e7544f 100644 --- a/src/lib/model.ts +++ b/src/lib/model.ts @@ -13,14 +13,15 @@ * * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ -import { SGraph } from "sprotty/lib"; +import { exportFeature, SGraph, SModelElement, SModelElementSchema, viewportFeature } from "sprotty/lib"; +import { Containable, containerFeature } from "../features/hints/model"; import { Saveable, saveFeature } from "../features/save/model"; -export class GLSPGraph extends SGraph implements Saveable { - dirty: boolean; - - hasFeature(feature: symbol) { - return feature === saveFeature || super.hasFeature(feature); +export class GLSPGraph extends SGraph implements Saveable, Containable { + static readonly DEFAULT_FEATURES = [viewportFeature, exportFeature, saveFeature, containerFeature]; + dirty: boolean = false; + isContainableElement(input: string | SModelElement | SModelElementSchema): boolean { + return true; } } diff --git a/src/model-source/websocket-diagram-server.ts b/src/model-source/websocket-diagram-server.ts index c5089011c..3e4cab07f 100644 --- a/src/model-source/websocket-diagram-server.ts +++ b/src/model-source/websocket-diagram-server.ts @@ -23,14 +23,14 @@ import { } from "sprotty"; import * as rpc from "vscode-ws-jsonrpc"; import { NotificationType } from "vscode-ws-jsonrpc"; -import { RequestCommandPaletteActions } from "../features/command-palette/action-definitions"; +import { RequestContextActions } from "../features/context-actions/action-definitions"; import { ExecuteServerCommandAction } from "../features/execute/execute-command"; -import { RequestTypeHintsAction } from "../features/hints/action-definition"; +import { RequestTypeHintsAction } from "../features/hints/request-type-hints-action"; import { OperationKind, RequestOperationsAction } from "../features/operation/set-operations"; -import { IdentifiableRequestAction } from "../features/request-response/action-definitions"; import { SaveModelAction } from "../features/save/save"; import { GlspRedoAction, GlspUndoAction } from "../features/undo-redo/model"; import { RequestMarkersAction } from "../features/validation/validate"; +import { ValidateLabelEditAction } from "../features/edit-label-validation/edit-label-validator"; @injectable() export class GLSPWebsocketDiagramServer extends DiagramServer { @@ -46,7 +46,6 @@ export class GLSPWebsocketDiagramServer extends DiagramServer { this.connection = connection; } }); - } protected sendMessage(message: ActionMessage): void { @@ -99,8 +98,8 @@ export function registerDefaultGLSPServerActions(registry: ActionHandlerRegistry registry.register(ServerStatusAction.KIND, diagramServer); registry.register(RequestModelAction.KIND, diagramServer); registry.register(ExportSvgAction.KIND, diagramServer); - registry.register(RequestCommandPaletteActions.KIND, diagramServer); - registry.register(IdentifiableRequestAction.KIND, diagramServer); + registry.register(RequestContextActions.KIND, diagramServer); + registry.register(ValidateLabelEditAction.KIND, diagramServer); registry.register(RequestMarkersAction.KIND, diagramServer); registry.register(LayoutAction.KIND, diagramServer); registry.register(ApplyLabelEditAction.KIND, diagramServer); diff --git a/src/types.ts b/src/types.ts index 3e9b0f1db..047973d50 100644 --- a/src/types.ts +++ b/src/types.ts @@ -17,11 +17,14 @@ export const GLSP_TYPES = { ICommandPaletteActionProviderRegistry: Symbol.for("ICommandPaletteActionProviderRegistry"), IFeedbackActionDispatcher: Symbol.for("IFeedbackActionDispatcher"), IToolFactory: Symbol.for("Factory"), - IEditConfigProvider: Symbol.for("IEditConfigProvider"), + ITypeHintProvider: Symbol.for("ITypeHintProvider"), IMovementRestrictor: Symbol.for("IMovmementRestrictor"), - RequestResponseSupport: Symbol.for("RequestResponseSupport"), SelectionService: Symbol.for("SelectionService"), SelectionListener: Symbol.for("SelectionListener"), SModelRootListener: Symbol.for("SModelRootListener"), - MouseTool: Symbol.for("MouseTool") + MouseTool: Symbol.for("MouseTool"), + IContextMenuService: Symbol.for("IContextMenuService"), + IContextMenuServiceProvider: Symbol.for("IContextMenuServiceProvider"), + IContextMenuProviderRegistry: Symbol.for("IContextMenuProviderRegistry"), + IContextMenuProvider: Symbol.for("IContextMenuProvider") }; diff --git a/src/utils/array-utils.ts b/src/utils/array-utils.ts index a6f63babd..b96fb49ad 100644 --- a/src/utils/array-utils.ts +++ b/src/utils/array-utils.ts @@ -14,11 +14,6 @@ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ -export function contains(array: T[], value: T): boolean { - if (value === undefined) return false; - return array.indexOf(value) >= 0; -} - export function remove(array: T[], value: T): boolean { const index = array.indexOf(value); if (index >= 0) { @@ -29,7 +24,7 @@ export function remove(array: T[], value: T): boolean { } export function distinctAdd(array: T[], value: T): boolean { - if (!contains(array, value)) { + if (!array.includes(value)) { array.push(value); return true; } diff --git a/src/utils/smodel-util.ts b/src/utils/smodel-util.ts index ab15cb427..ba55544cb 100644 --- a/src/utils/smodel-util.ts +++ b/src/utils/smodel-util.ts @@ -13,10 +13,16 @@ * * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ -import { BoundsAware, isBoundsAware, isSelectable, Selectable, SModelElement, SParentElement } from "sprotty/lib"; +import { + BoundsAware, + isBoundsAware, + isSelectable, + Selectable, + SModelElement, + SRoutableElement, + SRoutingHandle +} from "sprotty/lib"; -import { isConfigurableNode, NodeEditConfig } from "../base/edit-config/edit-config"; -import { isRoutable } from "../features/reconnect/model"; export function getIndex(element: SModelElement) { return element.root.index; @@ -77,15 +83,18 @@ export function removeCssClasses(root: SModelElement, cssClasses: string[]) { } } -export function isContainmentAllowed(element: SModelElement, containableElementTypeId: string) - : element is SParentElement & NodeEditConfig { - return isConfigurableNode(element) && element.isContainableElement(containableElementTypeId); -} - export function isNonRoutableSelectedBoundsAware(element: SModelElement): element is SelectableBoundsAware { return isBoundsAware(element) && isSelected(element) && !isRoutable(element); } +export function isRoutable(element: T): element is T & SRoutableElement { + return element instanceof SRoutableElement && (element as any).routingPoints !== undefined; +} + +export function isRoutingHandle(element: SModelElement | undefined): element is SRoutingHandle { + return element !== undefined && element instanceof SRoutingHandle; +} + export type SelectableBoundsAware = SModelElement & BoundsAware & Selectable; export function toElementAndBounds(element: SModelElement & BoundsAware) { diff --git a/src/utils/viewpoint-util.ts b/src/utils/viewpoint-util.ts index e6bf3d3b6..18941815d 100644 --- a/src/utils/viewpoint-util.ts +++ b/src/utils/viewpoint-util.ts @@ -13,12 +13,19 @@ * * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ -import { Point } from "sprotty/lib"; -import { SModelElement } from "sprotty/lib"; -import { Viewport } from "sprotty/lib"; - -import { findParentByFeature } from "sprotty/lib"; -import { isViewport } from "sprotty/lib"; +import { + Bounds, + BoundsAware, + Dimension, + findParentByFeature, + isAlignable, + isViewport, + ORIGIN_POINT, + Point, + SModelElement, + translateBounds, + Viewport +} from "sprotty/lib"; /** * Return the position corresponding to this mouse event (Browser coordinates) @@ -56,3 +63,38 @@ export function getAbsolutePosition(target: SModelElement, mouseEvent: MouseEven y: yPos }; } + +/** + * Translates the bounds of the diagram element (local coordinates) into the diagram coordinates system + * (i.e. relative to the Diagram's 0;0 point) + * + * @param target A bounds-aware element from the diagram + */ +export function toAbsoluteBounds(element: SModelElement & BoundsAware): Bounds { + const location = isAlignable(element) ? element.alignment : ORIGIN_POINT; + const x = location.x; + const y = location.y; + const width = element.bounds.width; + const height = element.bounds.height; + return translateBounds({ x, y, width, height }, element, element.root); +} + +/** + * Translates the position of the diagram element (local coordinates) into the diagram coordinates system + * (i.e. relative to the Diagram's 0;0 point) + * + * @param target A bounds-aware element from the diagram + */ +export function toAbsolutePosition(target: SModelElement & BoundsAware): Point { + return toAbsoluteBounds(target); +} + +/** + * Translates the size of the diagram element (local coordinates) into the diagram coordinates system + * (i.e. relative to the Diagram's 0;0 point) + * + * @param target A bounds-aware element from the diagram + */ +export function toAbsoluteSize(target: SModelElement & BoundsAware): Dimension { + return toAbsoluteBounds(target); +} diff --git a/tsconfig.json b/tsconfig.json index af01b66ce..8df07f750 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,6 +3,7 @@ "compilerOptions": { "rootDir": "src", "outDir": "lib", + "reactNamespace": "JSX", "baseUrl": "." }, "include": [ diff --git a/yarn.lock b/yarn.lock index 188721f85..103181c1b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -272,10 +272,10 @@ sprintf-js@~1.0.2: resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw= -sprotty@0.7.0: - version "0.7.0" - resolved "https://registry.yarnpkg.com/sprotty/-/sprotty-0.7.0.tgz#8346c4ea99529a7394fd69f0692ad8293f350fd2" - integrity sha512-eyRBuotqQejSamcD1U8ahohUfzz4MSofFdim3i2+iZbe2dsiPC3w7W27mqoGE/1/61L1hNQMhMneD0SmIjAj/g== +sprotty@0.8.0-next.1d772ad, sprotty@next: + version "0.8.0-next.1d772ad" + resolved "https://registry.yarnpkg.com/sprotty/-/sprotty-0.8.0-next.1d772ad.tgz#fa5e83d74882bf9e07830990b31335b9b44e1797" + integrity sha512-kUa86D0tQHc9nRONtdhxoi04EBd6KX1VN7DNVaRBuudD25zOIorhz6T7VTdFoSS3gAsTfy7HcWQBH5UttBz+Ig== dependencies: autocompleter "5.1.0" file-saver "2.0.2"