diff --git a/examples/workflow-server/src/common/provider/workflow-selection-palette-item-provider.ts b/examples/workflow-server/src/common/provider/workflow-selection-palette-item-provider.ts new file mode 100644 index 0000000..8dbf0c7 --- /dev/null +++ b/examples/workflow-server/src/common/provider/workflow-selection-palette-item-provider.ts @@ -0,0 +1,58 @@ +/******************************************************************************** + * Copyright (c) 2023 Business Informatics Group (TU Wien) 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 { + DefaultSelectionPaletteItemProvider, SelectionPaletteSettings +} from '@eclipse-glsp/server'; +import { + SelectionPalettePosition, + SelectionPaletteGroupUIType, + DefaultTypes +} from '@eclipse-glsp/protocol'; +import { injectable } from 'inversify'; +import { ModelTypes } from '../util/model-types'; + +@injectable() +export class WorkflowSelectionPaletteItemProvider extends DefaultSelectionPaletteItemProvider { + + protected override selectionPaletteNodeSettings: SelectionPaletteSettings = { + position: SelectionPalettePosition.Top, + showTitle: true, + submenu: false, + showOnlyForChildren: SelectionPaletteGroupUIType.Labels + }; + + protected override selectionPaletteEdgeSettings: SelectionPaletteSettings = { + position: SelectionPalettePosition.Right, + showTitle: true, + submenu: true + }; + + protected override nodeOperationFilter = { + [ModelTypes.AUTOMATED_TASK]: [ModelTypes.WEIGHTED_EDGE, ModelTypes.AUTOMATED_TASK, ModelTypes.MANUAL_TASK, + ModelTypes.ACTIVITY_NODE], + [ModelTypes.MERGE_NODE]: [DefaultTypes.EDGE, ModelTypes.MERGE_NODE, ModelTypes.CATEGORY], + [ModelTypes.FORK_NODE]: [DefaultTypes.EDGE, ModelTypes.FORK_NODE], + [ModelTypes.CATEGORY]: [ModelTypes.WEIGHTED_EDGE, ModelTypes.FORK_NODE], + [ModelTypes.JOIN_NODE]: [ModelTypes.AUTOMATED_TASK, ModelTypes.FORK_NODE, ModelTypes.JOIN_NODE] + }; + + protected override defaultEdge = DefaultTypes.EDGE; + + protected override edgeTypes = { + [ModelTypes.AUTOMATED_TASK]: DefaultTypes.EDGE, + [ModelTypes.MERGE_NODE]: DefaultTypes.EDGE + }; +} diff --git a/examples/workflow-server/src/common/workflow-diagram-module.ts b/examples/workflow-server/src/common/workflow-diagram-module.ts index 8cfdc82..b71f1f5 100644 --- a/examples/workflow-server/src/common/workflow-diagram-module.ts +++ b/examples/workflow-server/src/common/workflow-diagram-module.ts @@ -32,7 +32,8 @@ import { OperationHandlerConstructor, PopupModelFactory, ServerModule, - SourceModelStorage + SourceModelStorage, + SelectionPaletteItemProvider } from '@eclipse-glsp/server'; import { injectable } from 'inversify'; import { CreateAutomatedTaskHandler } from './handler/create-automated-task-handler'; @@ -52,6 +53,7 @@ import { NodeDocumentationNavigationTargetProvider } from './provider/node-docum import { PreviousNodeNavigationTargetProvider } from './provider/previous-node-navigation-target-provider'; import { WorkflowCommandPaletteActionProvider } from './provider/workflow-command-palette-action-provider'; import { WorkflowContextMenuItemProvider } from './provider/workflow-context-menu-item-provider'; +import { WorkflowSelectionPaletteItemProvider } from './provider/workflow-selection-palette-item-provider'; import { EditTaskOperationHandler } from './taskedit/edit-task-operation-handler'; import { TaskEditContextActionProvider } from './taskedit/task-edit-context-provider'; import { TaskEditValidator } from './taskedit/task-edit-validator'; @@ -107,6 +109,10 @@ export class WorkflowDiagramModule extends GModelDiagramModule { return WorkflowCommandPaletteActionProvider; } + protected override bindSelectionPaletteItemProvider(): BindingTarget | undefined { + return WorkflowSelectionPaletteItemProvider; + } + protected override bindLabelEditValidator(): BindingTarget | undefined { return WorkflowLabelEditValidator; } diff --git a/packages/server/src/common/di/diagram-module.ts b/packages/server/src/common/di/diagram-module.ts index 6f2937e..d6f0348 100644 --- a/packages/server/src/common/di/diagram-module.ts +++ b/packages/server/src/common/di/diagram-module.ts @@ -77,6 +77,10 @@ import { NavigationTargetProviders, Operations } from './service-identifiers'; +import { + DefaultSelectionPaletteItemProvider, + SelectionPaletteItemProvider +} from '../features/contextactions/selection-palette-item-provider'; /** * The diagram module is the central configuration artifact for configuring a client session specific injector. For each @@ -154,6 +158,7 @@ export abstract class DiagramModule extends GLSPModule { applyOptionalBindingTarget(context, ToolPaletteItemProvider, this.bindToolPaletteItemProvider()); applyOptionalBindingTarget(context, CommandPaletteActionProvider, this.bindCommandPaletteActionProvider()); applyOptionalBindingTarget(context, ContextMenuItemProvider, this.bindContextMenuItemProvider()); + applyOptionalBindingTarget(context, SelectionPaletteItemProvider, this.bindSelectionPaletteItemProvider()); this.configureMultiBinding(new MultiBinding(ContextActionsProviders), binding => this.configureContextActionProviders(binding) ); @@ -345,6 +350,10 @@ export abstract class DiagramModule extends GLSPModule { return DefaultToolPaletteItemProvider; } + protected bindSelectionPaletteItemProvider(): BindingTarget | undefined { + return DefaultSelectionPaletteItemProvider; + } + protected bindCommandPaletteActionProvider(): BindingTarget | undefined { return undefined; } diff --git a/packages/server/src/common/features/contextactions/context-actions-provider-registry.ts b/packages/server/src/common/features/contextactions/context-actions-provider-registry.ts index 7a2c71c..c0f2147 100644 --- a/packages/server/src/common/features/contextactions/context-actions-provider-registry.ts +++ b/packages/server/src/common/features/contextactions/context-actions-provider-registry.ts @@ -20,6 +20,7 @@ import { CommandPaletteActionProvider } from './command-palette-action-provider' import { ContextActionsProvider } from './context-actions-provider'; import { ContextMenuItemProvider } from './context-menu-item-provider'; import { ToolPaletteItemProvider } from './tool-palette-item-provider'; +import { SelectionPaletteItemProvider } from './selection-palette-item-provider'; /** * A registry that keeps track of all registered {@link ContextActionsProvider}s. @@ -30,7 +31,8 @@ export class ContextActionsProviderRegistry extends Registry this.register(provider.contextId, provider)); @@ -43,5 +45,8 @@ export class ContextActionsProviderRegistry extends Registry { + return this.getItems(editorContext.args); + } + + /** + * Constructs a list of {@link PaletteItem}s for a given map of string arguments. + * + * @param args A map of string arguments. + * @returns A list of {@link PaletteItem}s for a given map of string arguments. + */ + abstract getItems(args?: Args): MaybePromise; +} + +export type SelectionPaletteSettings = + | { + position: SelectionPalettePosition; + showTitle: true; + submenu: boolean; + showOnlyForChildren?: SelectionPaletteGroupUIType; + } + | { + position: SelectionPalettePosition; + showTitle: false; + showOnlyForChildren?: SelectionPaletteGroupUIType; + }; + +@injectable() +export class DefaultSelectionPaletteItemProvider extends SelectionPaletteItemProvider { + @inject(OperationHandlerRegistry) protected operationHandlerRegistry: OperationHandlerRegistry; + @inject(Logger) + protected logger: Logger; + + protected counter: number; + + protected selectionPaletteNodeSettings: SelectionPaletteSettings = { + position: SelectionPalettePosition.Right, + showTitle: true, + submenu: true, + showOnlyForChildren: SelectionPaletteGroupUIType.Icons + }; + + protected selectionPaletteEdgeSettings: SelectionPaletteSettings = { + position: SelectionPalettePosition.Right, + showTitle: true, + submenu: false + }; + /** filter that excludes nodes/edges from options, given a node ID as key */ + protected nodeOperationFilter: Record = {}; + + /** edge that is used between source and destination by default when a new node is created + * (if not given, no edge will be created when creating new node) */ + protected defaultEdge?: string; + + /** list of edges where the key is a node ID and the value is a edge ID + * the edge to a new node when the source node has the ID of the key + * otherwise, the default edge will be used */ + protected edgeTypes: Record; + + getItems(args?: Args): SelectionPaletteGroupItem[] { + const handlers = this.operationHandlerRegistry.getAll().filter(CreateOperationHandler.is) as CreateOperationHandler[]; + this.counter = 0; + const nodes = this.createSelectionPaletteGroupItem( + handlers, + CreateNodeOperation.KIND, + args?.nodeType as string, + this.selectionPaletteNodeSettings.showOnlyForChildren + ); + const edges = this.createSelectionPaletteGroupItem( + handlers, + CreateEdgeOperation.KIND, + args?.nodeType as string, + this.selectionPaletteEdgeSettings.showOnlyForChildren + ); + return [ + { + id: 'selection-palette-node-group', + label: 'Nodes', + actions: [], + children: nodes, + icon: 'symbol-property', + sortString: 'A', + ...this.selectionPaletteNodeSettings + }, + { + id: 'selection-palette-edge-group', + label: 'Edges', + actions: [], + children: edges, + icon: 'symbol-property', + sortString: 'B', + ...this.selectionPaletteEdgeSettings + } + ]; + } + + protected createSelectionPaletteGroupItem( + handlers: CreateOperationHandler[], + kind: string, + selectedNodeType: string, + showOnly?: SelectionPaletteGroupUIType + ): PaletteItem[] { + const includedInNodeFilter = (e: string): boolean => !!this.nodeOperationFilter[selectedNodeType]?.includes(e); + const paletteItems = handlers + .filter( + handler => + handler.operationType === kind && + (selectedNodeType && this.nodeOperationFilter[selectedNodeType] + ? !handler.elementTypeIds.some(includedInNodeFilter) + : true) + ) + .map(handler => + handler.getTriggerActions().map(action => this.createSelectionPaletteItem(action, handler.label, selectedNodeType)) + ) + .reduce((accumulator, value) => accumulator.concat(value), []) + .sort((a, b) => a.sortString.localeCompare(b.sortString)); + if (showOnly === SelectionPaletteGroupUIType.Icons) { + if (paletteItems.every(paletteItem => paletteItem.icon !== '')) { + this.logger.warn('Not all elements have icons. Labels will be shown, check settings for selection palette.'); + return paletteItems; + } + paletteItems.forEach(paletteItem => (paletteItem.label = '')); + } else if (showOnly === SelectionPaletteGroupUIType.Labels) { + paletteItems.forEach(paletteItem => (paletteItem.icon = '')); + } + return paletteItems; + } + + protected createSelectionPaletteItem( + action: PaletteItem.TriggerElementCreationAction, + label: string, + nodeType: string + ): PaletteItem | SelectionPaletteNodeItem { + if (TriggerNodeCreationAction.is(action)) { + let edgeType = this.edgeTypes[nodeType]; + if (!edgeType) { + edgeType = this.defaultEdge; + } + return { + id: `selection-palette-palette-item${this.counter++}`, + sortString: label.charAt(0), + label, + actions: [action], + edgeType: edgeType + }; + } + return { + id: `selection-palette-palette-item${this.counter++}`, + sortString: label.charAt(0), + label, + actions: [action] + }; + } +} diff --git a/packages/server/src/common/gmodel/gmodel-create-node-operation-handler.ts b/packages/server/src/common/gmodel/gmodel-create-node-operation-handler.ts index 7c4f69b..19f78c4 100644 --- a/packages/server/src/common/gmodel/gmodel-create-node-operation-handler.ts +++ b/packages/server/src/common/gmodel/gmodel-create-node-operation-handler.ts @@ -17,7 +17,7 @@ import { GModelElement, GNode } from '@eclipse-glsp/graph'; import { Args, CreateNodeOperation, GhostElement, MaybePromise, Point, SelectAction, - TriggerNodeCreationAction + TriggerNodeCreationAction, CreateEdgeOperation } from '@eclipse-glsp/protocol'; import { inject, injectable } from 'inversify'; import { ActionDispatcher } from '../actions/action-dispatcher'; @@ -54,6 +54,14 @@ export abstract class GModelCreateNodeOperationHandler extends GModelOperationHa container.children.push(element); element.parent = container; this.actionDispatcher.dispatchAfterNextUpdate(SelectAction.create({ selectedElementsIDs: [element.id] })); + // Creates default edge on node creation when a source ID is given in the CreateNodeOperation + if (operation.args?.createEdge && operation.args?.edgeType) { + this.actionDispatcher.dispatchAfterNextUpdate(CreateEdgeOperation.create({ + elementTypeId: operation.args?.edgeType as string, + sourceElementId: operation.args?.source as string, + targetElementId: element.id + })); + } } } diff --git a/packages/server/src/common/index.ts b/packages/server/src/common/index.ts index a2e84b4..6752d91 100644 --- a/packages/server/src/common/index.ts +++ b/packages/server/src/common/index.ts @@ -39,6 +39,7 @@ export * from './features/contextactions/context-actions-provider'; export * from './features/contextactions/context-actions-provider-registry'; export * from './features/contextactions/context-menu-item-provider'; export * from './features/contextactions/request-context-actions-handler'; +export * from './features/contextactions/selection-palette-item-provider'; export * from './features/contextactions/tool-palette-item-provider'; export * from './features/directediting/context-edit-validator'; export * from './features/directediting/context-edit-validator-registry';