From 09243d59201e42f984307652494e28395cfbfca7 Mon Sep 17 00:00:00 2001 From: Martin Fleck Date: Tue, 7 May 2024 17:16:14 +0200 Subject: [PATCH] Introduce an optional grid module to deal with a grid layout - Optional grid module defines a grid based on x/y coordinates -- Adds the grid as a background that properly zooms/resizes -- By default uses the GridSnapper for positioning elements -- Use grid for helper lines and movement tools - Optional debug module that shows the bounds of elements -- Useful for debugging but not meant for production - Render optional debug and grid toggles in tool palette if present - Add both optional modules to the workflow example Fixes https://github.com/eclipse-glsp/glsp/issues/1336 --- examples/workflow-glsp/src/index.ts | 1 + .../src/workflow-diagram-module.ts | 24 ++- .../workflow-glsp/src/workflow-startup.ts | 30 +++ examples/workflow-standalone/css/diagram.css | 3 +- examples/workflow-standalone/src/di.config.ts | 13 +- packages/client/css/debug.css | 25 +++ packages/client/css/grid.css | 24 +++ packages/client/css/keyboard-tool-palette.css | 1 - packages/client/css/tool-palette.css | 3 +- .../keyboard-tool-palette.ts | 23 ++- .../resize-key-tool/resize-key-handler.ts | 15 +- .../view-key-tools/movement-key-tool.ts | 13 +- ...spec.ts => point-position-updater.spec.ts} | 13 +- .../client/src/features/change-bounds/snap.ts | 25 +-- .../features/debug/debug-bounds-decorator.tsx | 173 ++++++++++++++++++ .../src/features/debug/debug-manager.ts | 56 ++++++ .../client/src/features/debug/debug-model.ts | 63 +++++++ .../client/src/features/debug/debug-module.ts | 32 ++++ packages/client/src/features/debug/index.ts | 19 ++ .../src/features/grid/grid-background.ts | 72 ++++++++ .../client/src/features/grid/grid-manager.ts | 58 ++++++ .../client/src/features/grid/grid-model.ts | 62 +++++++ .../client/src/features/grid/grid-module.ts | 40 ++++ .../src/features/grid/grid-snapper.spec.ts | 30 +++ .../client/src/features/grid/grid-snapper.ts | 59 ++++++ packages/client/src/features/grid/grid.ts | 19 ++ packages/client/src/features/grid/index.ts | 21 +++ .../helper-line-manager-default.ts | 12 +- packages/client/src/features/index.ts | 2 + .../src/features/tool-palette/tool-palette.ts | 63 ++++++- packages/glsp-sprotty/src/re-exports.ts | 3 +- packages/glsp-sprotty/src/types.ts | 4 +- 32 files changed, 913 insertions(+), 88 deletions(-) create mode 100644 examples/workflow-glsp/src/workflow-startup.ts create mode 100644 packages/client/css/debug.css create mode 100644 packages/client/css/grid.css rename packages/client/src/features/change-bounds/{snap.spec.ts => point-position-updater.spec.ts} (87%) create mode 100644 packages/client/src/features/debug/debug-bounds-decorator.tsx create mode 100644 packages/client/src/features/debug/debug-manager.ts create mode 100644 packages/client/src/features/debug/debug-model.ts create mode 100644 packages/client/src/features/debug/debug-module.ts create mode 100644 packages/client/src/features/debug/index.ts create mode 100644 packages/client/src/features/grid/grid-background.ts create mode 100644 packages/client/src/features/grid/grid-manager.ts create mode 100644 packages/client/src/features/grid/grid-model.ts create mode 100644 packages/client/src/features/grid/grid-module.ts create mode 100644 packages/client/src/features/grid/grid-snapper.spec.ts create mode 100644 packages/client/src/features/grid/grid-snapper.ts create mode 100644 packages/client/src/features/grid/grid.ts create mode 100644 packages/client/src/features/grid/index.ts diff --git a/examples/workflow-glsp/src/index.ts b/examples/workflow-glsp/src/index.ts index d67a397f5..c7abf6480 100644 --- a/examples/workflow-glsp/src/index.ts +++ b/examples/workflow-glsp/src/index.ts @@ -16,4 +16,5 @@ export * from './direct-task-editing'; export * from './model'; export * from './workflow-diagram-module'; +export * from './workflow-startup'; export * from './workflow-views'; diff --git a/examples/workflow-glsp/src/workflow-diagram-module.ts b/examples/workflow-glsp/src/workflow-diagram-module.ts index 70bab3817..e643bf3ed 100644 --- a/examples/workflow-glsp/src/workflow-diagram-module.ts +++ b/examples/workflow-glsp/src/workflow-diagram-module.ts @@ -27,11 +27,8 @@ import { GLSPProjectionView, GLabel, GLabelView, - GridSnapper, IHelperLineOptions, - ISnapper, LogLevel, - Point, RectangularNodeView, RevealNamedElementActionProvider, RoundedCornerNodeView, @@ -41,7 +38,9 @@ import { bindOrRebind, configureDefaultModelElements, configureModelElement, + debugModule, editLabelFeature, + gridModule, helperLineModule, initializeDiagramContainer } from '@eclipse-glsp/client'; @@ -51,6 +50,7 @@ import 'sprotty/css/edit-label.css'; import '../css/diagram.css'; import { directTaskEditor } from './direct-task-editing/di.config'; import { ActivityNode, CategoryNode, Icon, TaskNode, WeightedEdge } from './model'; +import { WorkflowStartup } from './workflow-startup'; import { IconView, WorkflowEdgeView } from './workflow-views'; export const workflowDiagramModule = new ContainerModule((bind, unbind, isBound, rebind) => { @@ -58,7 +58,6 @@ export const workflowDiagramModule = new ContainerModule((bind, unbind, isBound, bindOrRebind(context, TYPES.ILogger).to(ConsoleLogger).inSingletonScope(); bindOrRebind(context, TYPES.LogLevel).toConstantValue(LogLevel.warn); - bind(TYPES.ISnapper).to(GridSnapper); bindAsService(context, TYPES.ICommandPaletteActionProvider, RevealNamedElementActionProvider); bindAsService(context, TYPES.IContextMenuItemProvider, DeleteElementContextMenuItemProvider); @@ -82,16 +81,13 @@ export const workflowDiagramModule = new ContainerModule((bind, unbind, isBound, bind(TYPES.IHelperLineOptions).toDynamicValue(ctx => { const options: IHelperLineOptions = {}; - // the user needs to use twice the force (double the distance) to break through a helper line compared to moving on the grid - const snapper = ctx.container.get(TYPES.ISnapper); - if (snapper instanceof GridSnapper) { - options.minimumMoveDelta = Point.multiplyScalar(snapper.grid, 2); - } // skip icons for alignment as well as compartments which are only used for structure options.alignmentElementFilter = element => DEFAULT_ALIGNABLE_ELEMENT_FILTER(element) && !(element instanceof Icon) && !(element instanceof GCompartment); return options; }); + + bindAsService(context, TYPES.IDiagramStartup, WorkflowStartup); }); export function createWorkflowDiagramContainer(...containerConfiguration: ContainerConfiguration): Container { @@ -99,5 +95,13 @@ export function createWorkflowDiagramContainer(...containerConfiguration: Contai } export function initializeWorkflowDiagramContainer(container: Container, ...containerConfiguration: ContainerConfiguration): Container { - return initializeDiagramContainer(container, workflowDiagramModule, directTaskEditor, helperLineModule, ...containerConfiguration); + return initializeDiagramContainer( + container, + workflowDiagramModule, + directTaskEditor, + helperLineModule, + gridModule, + debugModule, + ...containerConfiguration + ); } diff --git a/examples/workflow-glsp/src/workflow-startup.ts b/examples/workflow-glsp/src/workflow-startup.ts new file mode 100644 index 000000000..57bd60e35 --- /dev/null +++ b/examples/workflow-glsp/src/workflow-startup.ts @@ -0,0 +1,30 @@ +/******************************************************************************** + * Copyright (c) 2024 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 { GridManager, IDiagramStartup } from '@eclipse-glsp/client'; +import { MaybePromise, TYPES } from '@eclipse-glsp/sprotty'; +import { inject, injectable } from 'inversify'; + +@injectable() +export class WorkflowStartup implements IDiagramStartup { + rank = -1; + + @inject(TYPES.IGridManager) protected gridManager: GridManager; + + preRequestModel(): MaybePromise { + this.gridManager.setGridVisible(true); + } +} diff --git a/examples/workflow-standalone/css/diagram.css b/examples/workflow-standalone/css/diagram.css index bc379b976..782f1d524 100644 --- a/examples/workflow-standalone/css/diagram.css +++ b/examples/workflow-standalone/css/diagram.css @@ -17,7 +17,8 @@ --glsp-info-foreground: blue; } -.sprotty-graph { +.sprotty-graph, +.grid-background { background: rgb(179, 196, 202); } diff --git a/examples/workflow-standalone/src/di.config.ts b/examples/workflow-standalone/src/di.config.ts index 1c0652d94..57c8d7697 100644 --- a/examples/workflow-standalone/src/di.config.ts +++ b/examples/workflow-standalone/src/di.config.ts @@ -21,10 +21,8 @@ import { LogLevel, STANDALONE_MODULE_CONFIG, TYPES, - accessibilityModule, bindOrRebind, - createDiagramOptionsModule, - toolPaletteModule + createDiagramOptionsModule } from '@eclipse-glsp/client'; import { Container } from 'inversify'; import { makeLoggerMiddleware } from 'inversify-logger-middleware'; @@ -35,14 +33,7 @@ export default function createContainer(options: IDiagramOptions): Container { if (parameters.readonly) { options.editMode = EditMode.READONLY; } - const container = createWorkflowDiagramContainer( - createDiagramOptionsModule(options), - { - add: accessibilityModule, - remove: toolPaletteModule - }, - STANDALONE_MODULE_CONFIG - ); + const container = createWorkflowDiagramContainer(createDiagramOptionsModule(options), STANDALONE_MODULE_CONFIG); bindOrRebind(container, TYPES.ILogger).to(ConsoleLogger).inSingletonScope(); bindOrRebind(container, TYPES.LogLevel).toConstantValue(LogLevel.warn); container.bind(TYPES.IMarqueeBehavior).toConstantValue({ entireEdge: true, entireElement: true }); diff --git a/packages/client/css/debug.css b/packages/client/css/debug.css new file mode 100644 index 000000000..e4aa5e4e7 --- /dev/null +++ b/packages/client/css/debug.css @@ -0,0 +1,25 @@ +/******************************************************************************** + * Copyright (c) 2024 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 + ********************************************************************************/ + +.debug-bounds:has(> .debug-bounds-decoration) { + fill-opacity: 0.5; +} + +.debug-bounds-decoration { + fill: none; + stroke: black; + stroke-width: 1px; +} diff --git a/packages/client/css/grid.css b/packages/client/css/grid.css new file mode 100644 index 000000000..b11ae3117 --- /dev/null +++ b/packages/client/css/grid.css @@ -0,0 +1,24 @@ +/******************************************************************************** + * Copyright (c) 2024 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 + ********************************************************************************/ + +:root { + --grid-background-image: url('data:image/svg+xml;utf8,'); +} + +.grid-background .sprotty-graph { + background: var(--sprotty-background); + background-image: var(--grid-background-image); +} diff --git a/packages/client/css/keyboard-tool-palette.css b/packages/client/css/keyboard-tool-palette.css index 2ce9fa8b2..2b47424d6 100644 --- a/packages/client/css/keyboard-tool-palette.css +++ b/packages/client/css/keyboard-tool-palette.css @@ -16,7 +16,6 @@ .accessibility-tool-palette.tool-palette { top: 48px; - width: 240px; } .accessibility-tool-palette .header-tools i { diff --git a/packages/client/css/tool-palette.css b/packages/client/css/tool-palette.css index d92f46884..856a8cd92 100644 --- a/packages/client/css/tool-palette.css +++ b/packages/client/css/tool-palette.css @@ -20,7 +20,7 @@ right: 40px; top: 25px; text-align: center; - width: 225px; + width: fit-content; display: block; z-index: 1000; border-style: solid; @@ -54,6 +54,7 @@ align-items: center; justify-content: space-between; flex-wrap: wrap; + gap: 20px; } .header-icon { diff --git a/packages/client/src/features/accessibility/keyboard-tool-palette/keyboard-tool-palette.ts b/packages/client/src/features/accessibility/keyboard-tool-palette/keyboard-tool-palette.ts index a20e0d272..35e011d57 100644 --- a/packages/client/src/features/accessibility/keyboard-tool-palette/keyboard-tool-palette.ts +++ b/packages/client/src/features/accessibility/keyboard-tool-palette/keyboard-tool-palette.ts @@ -212,29 +212,42 @@ export class KeyboardToolPalette extends ToolPalette { protected override createHeaderTools(): HTMLElement { this.headerToolsButtonMapping.clear(); + let mappingIndex = 0; const headerTools = document.createElement('div'); headerTools.classList.add('header-tools'); this.defaultToolsButton = this.createDefaultToolButton(); - this.headerToolsButtonMapping.set(0, this.defaultToolsButton); + this.headerToolsButtonMapping.set(mappingIndex++, this.defaultToolsButton); headerTools.appendChild(this.defaultToolsButton); this.deleteToolButton = this.createMouseDeleteToolButton(); - this.headerToolsButtonMapping.set(1, this.deleteToolButton); + this.headerToolsButtonMapping.set(mappingIndex++, this.deleteToolButton); headerTools.appendChild(this.deleteToolButton); this.marqueeToolButton = this.createMarqueeToolButton(); - this.headerToolsButtonMapping.set(2, this.marqueeToolButton); + this.headerToolsButtonMapping.set(mappingIndex++, this.marqueeToolButton); headerTools.appendChild(this.marqueeToolButton); this.validateToolButton = this.createValidateButton(); - this.headerToolsButtonMapping.set(3, this.validateToolButton); + this.headerToolsButtonMapping.set(mappingIndex++, this.validateToolButton); headerTools.appendChild(this.validateToolButton); + if (this.gridManager) { + const toggleGridButton = this.createToggleGridButton(); + this.headerToolsButtonMapping.set(mappingIndex++, toggleGridButton); + headerTools.appendChild(toggleGridButton); + } + + if (this.debugManager) { + const toggleDebugButton = this.createToggleDebugButton(); + this.headerToolsButtonMapping.set(mappingIndex++, toggleDebugButton); + headerTools.appendChild(toggleDebugButton); + } + // Create button for Search this.searchToolButton = this.createSearchButton(); - this.headerToolsButtonMapping.set(4, this.searchToolButton); + this.headerToolsButtonMapping.set(mappingIndex++, this.searchToolButton); headerTools.appendChild(this.searchToolButton); return headerTools; diff --git a/packages/client/src/features/accessibility/resize-key-tool/resize-key-handler.ts b/packages/client/src/features/accessibility/resize-key-tool/resize-key-handler.ts index fa4aef167..ab9d2eea0 100644 --- a/packages/client/src/features/accessibility/resize-key-tool/resize-key-handler.ts +++ b/packages/client/src/features/accessibility/resize-key-tool/resize-key-handler.ts @@ -36,7 +36,7 @@ import { EditorContextService } from '../../../base/editor-context-service'; import { IFeedbackActionDispatcher } from '../../../base/feedback/feedback-action-dispatcher'; import { Resizable, SelectableBoundsAware, getElements, isSelectableAndBoundsAware, toElementAndBounds } from '../../../utils/gmodel-util'; import { isValidMove, isValidSize, minHeight, minWidth } from '../../../utils/layout-utils'; -import { GridSnapper } from '../../change-bounds/snap'; +import { Grid } from '../../grid'; export enum ResizeType { Increase, @@ -80,20 +80,17 @@ export class ResizeElementHandler implements IActionHandler { protected debouncedChangeBounds?: DebouncedFunc<() => void>; protected resizeFeedback: FeedbackEmitter; - // Default x resize used if GridSnapper is not provided + // Default x resize used if grid is not provided static readonly defaultResizeX = 20; - // Default y resize used if GridSnapper is not provided + // Default y resize used if grid is not provided static readonly defaultResizeY = 20; - protected grid = { x: ResizeElementHandler.defaultResizeX, y: ResizeElementHandler.defaultResizeY }; + + @optional() @inject(Grid) protected grid: Grid = { x: ResizeElementHandler.defaultResizeX, y: ResizeElementHandler.defaultResizeY }; protected isEditMode = false; - constructor(@inject(TYPES.ISnapper) @optional() protected readonly snapper?: ISnapper) { - if (snapper instanceof GridSnapper) { - this.grid = snapper.grid; - } - } + constructor(@inject(TYPES.ISnapper) @optional() protected readonly snapper?: ISnapper) {} @postConstruct() protected init(): void { diff --git a/packages/client/src/features/accessibility/view-key-tools/movement-key-tool.ts b/packages/client/src/features/accessibility/view-key-tools/movement-key-tool.ts index ddee99743..6c4329d5f 100644 --- a/packages/client/src/features/accessibility/view-key-tools/movement-key-tool.ts +++ b/packages/client/src/features/accessibility/view-key-tools/movement-key-tool.ts @@ -20,7 +20,8 @@ import { matchesKeystroke } from 'sprotty/lib/utils/keyboard'; import { GLSPActionDispatcher } from '../../../base/action-dispatcher'; import { SelectionService } from '../../../base/selection-service'; import { Tool } from '../../../base/tool-manager/tool'; -import { GridSnapper, unsnapModifier, useSnap } from '../../change-bounds/snap'; +import { unsnapModifier, useSnap } from '../../change-bounds/snap'; +import { Grid } from '../../grid'; import { AccessibleKeyShortcutProvider, SetAccessibleKeyShortcutAction } from '../key-shortcut/accessible-key-shortcut'; import { MoveElementAction, MoveViewportAction } from '../move-zoom/move-handler'; @@ -55,22 +56,18 @@ export class MovementKeyTool implements Tool { } export class MoveKeyListener extends KeyListener implements AccessibleKeyShortcutProvider { - // Default x distance used if GridSnapper is not provided + // Default x distance used if grid is not provided static readonly defaultMoveX = 20; - // Default y distance used if GridSnapper is not provided + // Default y distance used if grid is not provided static readonly defaultMoveY = 20; protected readonly token = MoveKeyListener.name; - protected grid = { x: MoveKeyListener.defaultMoveX, y: MoveKeyListener.defaultMoveY }; + @optional() @inject(Grid) protected grid: Grid = { x: MoveKeyListener.defaultMoveX, y: MoveKeyListener.defaultMoveY }; constructor(protected readonly tool: MovementKeyTool) { super(); - - if (this.tool.snapper instanceof GridSnapper) { - this.grid = this.tool.snapper.grid; - } } registerShortcutKey(): void { diff --git a/packages/client/src/features/change-bounds/snap.spec.ts b/packages/client/src/features/change-bounds/point-position-updater.spec.ts similarity index 87% rename from packages/client/src/features/change-bounds/snap.spec.ts rename to packages/client/src/features/change-bounds/point-position-updater.spec.ts index ca6a82e8c..0d29442e9 100644 --- a/packages/client/src/features/change-bounds/snap.spec.ts +++ b/packages/client/src/features/change-bounds/point-position-updater.spec.ts @@ -16,19 +16,8 @@ import { GModelElement } from '@eclipse-glsp/sprotty'; import { expect } from 'chai'; +import { GridSnapper } from '../grid'; import { PointPositionUpdater } from './point-position-updater'; -import { GridSnapper } from './snap'; - -describe('GridSnapper', () => { - it('snap', () => { - const element = new GModelElement(); - const snapper = new GridSnapper(); - expect(snapper.snap({ x: 0, y: 0 }, element)).to.be.deep.equals({ x: 0, y: 0 }); - expect(snapper.snap({ x: 4, y: 5 }, element)).to.be.deep.equals({ x: 0, y: 10 }); - expect(snapper.snap({ x: 8, y: 11 }, element)).to.be.deep.equals({ x: 10, y: 10 }); - expect(snapper.snap({ x: -7, y: -4 }, element)).to.be.deep.equals({ x: -10, y: -0 }); - }); -}); describe('PointPositionUpdater', () => { it('updatePosition with no last drag position', () => { diff --git a/packages/client/src/features/change-bounds/snap.ts b/packages/client/src/features/change-bounds/snap.ts index 36f218caa..246c07c8c 100644 --- a/packages/client/src/features/change-bounds/snap.ts +++ b/packages/client/src/features/change-bounds/snap.ts @@ -14,30 +14,7 @@ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ /* eslint-disable @typescript-eslint/no-shadow */ -import { GModelElement, ISnapper, KeyboardModifier, Point } from '@eclipse-glsp/sprotty'; -import { injectable } from 'inversify'; - -/** - * A {@link ISnapper} implementation that snaps all elements onto a fixed gride size. - * The default grid size is 10x10 pixel. - * To configure a custom grid size bind the `TYPES.ISnapper` service identifier - * to constant value, e.g: - * - * ```ts - * bind(TYPES.ISnapper).toConstantValue(new GridSnapper({x:25 ,y:25 })); - * ``` - */ -@injectable() -export class GridSnapper implements ISnapper { - constructor(public grid: { x: number; y: number } = { x: 10, y: 10 }) {} - - snap(position: Point, _element: GModelElement): Point { - return { - x: Math.round(position.x / this.grid.x) * this.grid.x, - y: Math.round(position.y / this.grid.y) * this.grid.y - }; - } -} +import { KeyboardModifier } from '@eclipse-glsp/sprotty'; export function useSnap(event: MouseEvent | KeyboardEvent): boolean { return !event.shiftKey; diff --git a/packages/client/src/features/debug/debug-bounds-decorator.tsx b/packages/client/src/features/debug/debug-bounds-decorator.tsx new file mode 100644 index 000000000..3e45c5268 --- /dev/null +++ b/packages/client/src/features/debug/debug-bounds-decorator.tsx @@ -0,0 +1,173 @@ +/******************************************************************************** + * Copyright (c) 2024 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 + ********************************************************************************/ +/* eslint-disable max-len */ + +import { Bounds, GModelElement, IVNodePostprocessor, Point, isSizeable, setClass, svg } from '@eclipse-glsp/sprotty'; +import { inject, injectable, optional } from 'inversify'; +import { VNode } from 'snabbdom'; +import { GGraph } from '../../model'; +import { BoundsAwareModelElement } from '../../utils'; +import { DebugManager } from './debug-manager'; + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +const JSX = { createElement: svg }; + +@injectable() +export class DebugBoundsDecorator implements IVNodePostprocessor { + @optional() + @inject(DebugManager) + protected debugManager?: DebugManager; + + decorate(vnode: VNode, element: GModelElement): VNode { + if (!this.debugManager?.isDebugEnabled) { + return vnode; + } + if (isSizeable(element)) { + this.decorateSizeable(vnode, element); + } + if (element instanceof GGraph) { + this.decorateGraph(vnode, element); + } + return vnode; + } + + postUpdate(): void {} + + protected get decorationSize(): number { + return 5; + } + + protected decorateGraph(vnode: VNode, graph: GGraph): void { + setClass(vnode, 'debug-bounds', true); + const svgChild = vnode.children?.find(child => typeof child !== 'string' && child.sel === 'svg') as VNode | undefined; + const group = svgChild?.children?.find(child => typeof child !== 'string' && child.sel === 'g') as VNode | undefined; + group?.children?.push(this.renderOrigin(graph)); + } + + protected renderOrigin(graph: GGraph): VNode { + return ( + + Origin = x: 0, y: 0 + + ); + } + + protected decorateSizeable(vnode: VNode, element: BoundsAwareModelElement): void { + setClass(vnode, 'debug-bounds', true); + vnode.children?.push(this.renderTopLeftCorner(element)); + vnode.children?.push(this.renderTopRightCorner(element)); + vnode.children?.push(this.renderBottomLeftCorner(element)); + vnode.children?.push(this.renderBottomRightCorner(element)); + vnode.children?.push(this.renderCenter(element)); + } + + protected renderTopLeftCorner(element: BoundsAwareModelElement): VNode { + const position = Bounds.topLeft(element.bounds); + const topLeft = Bounds.topLeft(element.bounds); + const corner = Point.subtract(topLeft, position); + return ( + + + Top Left = x: {topLeft.x}, y: {topLeft.y} + + + ); + } + + protected renderTopRightCorner(element: BoundsAwareModelElement): VNode { + const position = Bounds.topLeft(element.bounds); + const topRight = Bounds.topRight(element.bounds); + const corner = Point.subtract(topRight, position); + return ( + + + Top Right = x: {topRight.x}, y: {topRight.y} + + + ); + } + + protected renderBottomLeftCorner(element: BoundsAwareModelElement): VNode { + const position = Bounds.topLeft(element.bounds); + const bottomLeft = Bounds.bottomLeft(element.bounds); + const corner = Point.subtract(bottomLeft, position); + return ( + + + Bottom Left = x: {bottomLeft.x}, y: {bottomLeft.y} + + + ); + } + + protected renderBottomRightCorner(element: BoundsAwareModelElement): VNode { + const position = Bounds.topLeft(element.bounds); + const bottomRight = Bounds.bottomRight(element.bounds); + const corner = Point.subtract(bottomRight, position); + return ( + + + Bottom Right = x: {bottomRight.x}, y: {bottomRight.y} + + + ); + } + + protected renderCenter(element: BoundsAwareModelElement): VNode { + const bounds = element.bounds; + const position = Bounds.topLeft(bounds); + const center = Bounds.center(bounds); + const corner = Point.subtract(center, position); + return ( + + + Center = x: {center.x}, y: {center.y} + Bounds = x: {bounds.x}, y: {bounds.y}, width: {bounds.width}, height: {bounds.height} + + + + + ); + } +} diff --git a/packages/client/src/features/debug/debug-manager.ts b/packages/client/src/features/debug/debug-manager.ts new file mode 100644 index 000000000..c950dbb5a --- /dev/null +++ b/packages/client/src/features/debug/debug-manager.ts @@ -0,0 +1,56 @@ +/******************************************************************************** + * Copyright (c) 2024 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 { IActionHandler, TYPES } from '@eclipse-glsp/sprotty'; +import { inject, injectable, postConstruct } from 'inversify'; +import { FeedbackEmitter, IFeedbackActionDispatcher } from '../../base'; +import { EnableDebugModeAction } from './debug-model'; + +@injectable() +export class DebugManager implements IActionHandler { + protected _debugEnabled: boolean = false; + protected debugFeedback: FeedbackEmitter; + + @inject(TYPES.IFeedbackActionDispatcher) + protected feedbackDispatcher: IFeedbackActionDispatcher; + + get isDebugEnabled(): boolean { + return this._debugEnabled; + } + + handle(action: EnableDebugModeAction): void { + this._debugEnabled = action.enable; + } + + @postConstruct() + protected init(): void { + this.debugFeedback = this.feedbackDispatcher.createEmitter(); + } + + setDebugEnabled(visible: boolean): void { + if (!visible) { + this.debugFeedback.dispose(); + } else { + this.debugFeedback + .add(EnableDebugModeAction.create({ enable: true }), EnableDebugModeAction.create({ enable: false })) + .submit(); + } + } + + toggleDebugEnabled(): void { + this.setDebugEnabled(!this._debugEnabled); + } +} diff --git a/packages/client/src/features/debug/debug-model.ts b/packages/client/src/features/debug/debug-model.ts new file mode 100644 index 000000000..5c517c4f9 --- /dev/null +++ b/packages/client/src/features/debug/debug-model.ts @@ -0,0 +1,63 @@ +/******************************************************************************** + * Copyright (c) 2024 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, CommandExecutionContext, CommandReturn, GModelRoot, TYPES, hasBooleanProp } from '@eclipse-glsp/sprotty'; +import { inject, injectable } from 'inversify'; +import { FeedbackCommand } from '../../base'; +import { addCssClasses, removeCssClasses } from '../../utils'; + +export interface EnableDebugModeAction extends Action { + kind: typeof EnableDebugModeAction.KIND; + enable: boolean; +} + +export namespace EnableDebugModeAction { + export const KIND = 'enableDebugMode'; + export const CSS_ROOT_CLASS = 'debug-mode'; + + export function is(object: any): object is EnableDebugModeAction { + return Action.hasKind(object, KIND) && hasBooleanProp(object, 'enable'); + } + + export function create(options: { enable: boolean }): EnableDebugModeAction { + return { + kind: EnableDebugModeAction.KIND, + ...options + }; + } +} + +@injectable() +export class EnableDebugModeCommand extends FeedbackCommand { + static readonly KIND = EnableDebugModeAction.KIND; + + constructor(@inject(TYPES.Action) protected readonly action: EnableDebugModeAction) { + super(); + } + + execute(context: CommandExecutionContext): CommandReturn { + return this.setDebugMode(context.root, this.action.enable); + } + + protected setDebugMode(root: GModelRoot, show: boolean): CommandReturn { + if (show) { + addCssClasses(root, [EnableDebugModeAction.CSS_ROOT_CLASS]); + } else { + removeCssClasses(root, [EnableDebugModeAction.CSS_ROOT_CLASS]); + } + return root; + } +} diff --git a/packages/client/src/features/debug/debug-module.ts b/packages/client/src/features/debug/debug-module.ts new file mode 100644 index 000000000..cbc757ea3 --- /dev/null +++ b/packages/client/src/features/debug/debug-module.ts @@ -0,0 +1,32 @@ +/******************************************************************************** + * Copyright (c) 2024 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 { FeatureModule, TYPES, bindAsService, configureActionHandler, configureCommand } from '@eclipse-glsp/sprotty'; +import '../../../css/debug.css'; +import { DebugBoundsDecorator } from './debug-bounds-decorator'; +import { DebugManager } from './debug-manager'; +import { EnableDebugModeAction, EnableDebugModeCommand } from './debug-model'; + +export const debugModule = new FeatureModule((bind, unbind, isBound, rebind) => { + const context = { bind, unbind, isBound, rebind }; + + configureCommand(context, EnableDebugModeCommand); + + bindAsService(bind, TYPES.IDebugManager, DebugManager); + configureActionHandler(context, EnableDebugModeAction.KIND, DebugManager); + + bindAsService(context, TYPES.IVNodePostprocessor, DebugBoundsDecorator); +}); diff --git a/packages/client/src/features/debug/index.ts b/packages/client/src/features/debug/index.ts new file mode 100644 index 000000000..f573c2114 --- /dev/null +++ b/packages/client/src/features/debug/index.ts @@ -0,0 +1,19 @@ +/******************************************************************************** + * Copyright (c) 2024 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 + ********************************************************************************/ +export * from './debug-bounds-decorator'; +export * from './debug-manager'; +export * from './debug-model'; +export * from './debug-module'; diff --git a/packages/client/src/features/grid/grid-background.ts b/packages/client/src/features/grid/grid-background.ts new file mode 100644 index 000000000..719acbfae --- /dev/null +++ b/packages/client/src/features/grid/grid-background.ts @@ -0,0 +1,72 @@ +/******************************************************************************** + * Copyright (c) 2024 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, + IActionHandler, + MaybePromise, + Point, + SetViewportAction, + TYPES, + ViewerOptions, + Viewport, + findParentByFeature, + isViewport +} from '@eclipse-glsp/sprotty'; +import { inject, injectable } from 'inversify'; +import { EditorContextService, IDiagramStartup } from '../../base'; +import { Grid } from './grid'; + +@injectable() +export class GridBackground implements IActionHandler, IDiagramStartup { + @inject(TYPES.ViewerOptions) protected options: ViewerOptions; + @inject(Grid) protected grid: Grid; + @inject(EditorContextService) protected editorContextService: EditorContextService; + + handle(action: Action): void { + if (SetViewportAction.is(action)) { + this.moveGridBackground(action.newViewport); + } + } + + postModelInitialization(): MaybePromise { + this.moveGridBackground(); + const div = document.querySelector(`#${this.options.baseDiv}`); + if (div) { + div.style.setProperty('--grid-background-image', this.getBackgroundImage()); + } + } + + protected getBackgroundImage(): string { + // eslint-disable-next-line max-len + return `url('data:image/svg+xml;utf8, ')`; + } + + protected getViewport(): Viewport { + return findParentByFeature(this.editorContextService.modelRoot, isViewport) ?? { scroll: Point.ORIGIN, zoom: 1 }; + } + + protected moveGridBackground(viewport: Viewport = this.getViewport()): void { + const graphDiv = document.querySelector(`#${this.options.baseDiv} .sprotty-graph`); + if (graphDiv) { + const bgPosition = Point.multiplyScalar(Point.subtract(this.grid, viewport.scroll), viewport.zoom); + const bgSize = Point.multiplyScalar(this.grid, viewport.zoom); + + graphDiv.style.backgroundPosition = `${bgPosition.x}px ${bgPosition.y}px`; + graphDiv.style.backgroundSize = `${bgSize.x}px ${bgSize.y}px`; + } + } +} diff --git a/packages/client/src/features/grid/grid-manager.ts b/packages/client/src/features/grid/grid-manager.ts new file mode 100644 index 000000000..37c9c6c51 --- /dev/null +++ b/packages/client/src/features/grid/grid-manager.ts @@ -0,0 +1,58 @@ +/******************************************************************************** + * Copyright (c) 2024 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 { IActionHandler, TYPES } from '@eclipse-glsp/sprotty'; +import { inject, injectable, postConstruct } from 'inversify'; +import { FeedbackEmitter, IFeedbackActionDispatcher } from '../../base'; +import { Grid } from './grid'; +import { ShowGridAction } from './grid-model'; + +@injectable() +export class GridManager implements IActionHandler { + protected _gridVisible: boolean = false; + protected gridFeedback: FeedbackEmitter; + + @inject(TYPES.IFeedbackActionDispatcher) + protected feedbackDispatcher: IFeedbackActionDispatcher; + + @inject(Grid) + public readonly grid: Grid; + + get isGridVisible(): boolean { + return this._gridVisible; + } + + @postConstruct() + protected init(): void { + this.gridFeedback = this.feedbackDispatcher.createEmitter(); + } + + handle(action: ShowGridAction): void { + this._gridVisible = action.show; + } + + setGridVisible(visible: boolean): void { + if (!visible) { + this.gridFeedback.dispose(); + } else { + this.gridFeedback.add(ShowGridAction.create({ show: true }), ShowGridAction.create({ show: false })).submit(); + } + } + + toggleGridVisible(): void { + this.setGridVisible(!this._gridVisible); + } +} diff --git a/packages/client/src/features/grid/grid-model.ts b/packages/client/src/features/grid/grid-model.ts new file mode 100644 index 000000000..4206b5c2e --- /dev/null +++ b/packages/client/src/features/grid/grid-model.ts @@ -0,0 +1,62 @@ +/******************************************************************************** + * Copyright (c) 2024 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, CommandExecutionContext, CommandReturn, GModelRoot, TYPES, hasBooleanProp } from '@eclipse-glsp/sprotty'; +import { inject, injectable } from 'inversify'; +import { FeedbackCommand } from '../../base'; +import { addCssClasses, removeCssClasses } from '../../utils'; + +export interface ShowGridAction extends Action { + kind: typeof ShowGridCommand.KIND; + show: boolean; +} + +export namespace ShowGridAction { + export const KIND = 'showGrid'; + export const CSS_ROOT_CLASS = 'grid-background'; + + export function is(object: any): object is ShowGridAction { + return Action.hasKind(object, KIND) && hasBooleanProp(object, 'show'); + } + + export function create(options: { show: boolean }): ShowGridAction { + return { + kind: ShowGridCommand.KIND, + ...options + }; + } +} + +@injectable() +export class ShowGridCommand extends FeedbackCommand { + static readonly KIND = ShowGridAction.KIND; + + constructor(@inject(TYPES.Action) protected readonly action: ShowGridAction) { + super(); + } + + execute(context: CommandExecutionContext): CommandReturn { + return this.setGrid(context.root, this.action.show); + } + + protected setGrid(root: GModelRoot, show: boolean): CommandReturn { + if (show) { + addCssClasses(root, [ShowGridAction.CSS_ROOT_CLASS]); + } else { + removeCssClasses(root, [ShowGridAction.CSS_ROOT_CLASS]); + } + return root; + } +} diff --git a/packages/client/src/features/grid/grid-module.ts b/packages/client/src/features/grid/grid-module.ts new file mode 100644 index 000000000..4ee4bb318 --- /dev/null +++ b/packages/client/src/features/grid/grid-module.ts @@ -0,0 +1,40 @@ +/******************************************************************************** + * Copyright (c) 2023 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 { FeatureModule, SetViewportAction, TYPES, bindAsService, configureActionHandler, configureCommand } from '@eclipse-glsp/sprotty'; +import '../../../css/grid.css'; +import { Grid } from './grid'; +import { GridBackground } from './grid-background'; +import { GridManager } from './grid-manager'; +import { ShowGridAction, ShowGridCommand } from './grid-model'; +import { GridSnapper } from './grid-snapper'; + +export const gridModule = new FeatureModule((bind, unbind, isBound, rebind) => { + const context = { bind, unbind, isBound, rebind }; + + bind(Grid).toConstantValue({ x: 10, y: 10 }); + + configureCommand(context, ShowGridCommand); + + bindAsService(bind, TYPES.IGridManager, GridManager); + configureActionHandler(context, ShowGridAction.KIND, GridManager); + + bind(TYPES.ISnapper).to(GridSnapper); + + bind(GridBackground).toSelf().inSingletonScope(); + configureActionHandler(context, SetViewportAction.KIND, GridBackground); + bind(TYPES.IDiagramStartup).toService(GridBackground); +}); diff --git a/packages/client/src/features/grid/grid-snapper.spec.ts b/packages/client/src/features/grid/grid-snapper.spec.ts new file mode 100644 index 000000000..6e99eae2d --- /dev/null +++ b/packages/client/src/features/grid/grid-snapper.spec.ts @@ -0,0 +1,30 @@ +/******************************************************************************** + * Copyright (c) 2022-2023 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 { GModelElement } from '@eclipse-glsp/sprotty'; +import { expect } from 'chai'; +import { GridSnapper } from './grid-snapper'; + +describe('GridSnapper', () => { + it('snap', () => { + const element = new GModelElement(); + const snapper = new GridSnapper(); + expect(snapper.snap({ x: 0, y: 0 }, element)).to.be.deep.equals({ x: 0, y: 0 }); + expect(snapper.snap({ x: 4, y: 5 }, element)).to.be.deep.equals({ x: 0, y: 10 }); + expect(snapper.snap({ x: 8, y: 11 }, element)).to.be.deep.equals({ x: 10, y: 10 }); + expect(snapper.snap({ x: -7, y: -4 }, element)).to.be.deep.equals({ x: -10, y: -0 }); + }); +}); diff --git a/packages/client/src/features/grid/grid-snapper.ts b/packages/client/src/features/grid/grid-snapper.ts new file mode 100644 index 000000000..471ada4c0 --- /dev/null +++ b/packages/client/src/features/grid/grid-snapper.ts @@ -0,0 +1,59 @@ +/******************************************************************************** + * Copyright (c) 2024 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 { GModelElement, ISnapper, Point, SprottyCenterGridSnapper } from '@eclipse-glsp/sprotty'; +import { inject, injectable, optional } from 'inversify'; +import { Grid } from './grid'; + +/** + * A {@link ISnapper} implementation that snaps all elements onto a fixed gride size. + * The default grid size is 10x10 pixel. + * To configure a custom grid size bind the `TYPES.ISnapper` service identifier + * to constant value, e.g: + * + * ```ts + * bind(TYPES.ISnapper).toConstantValue(new GridSnapper({ x: 25, y: 25 })); + * ``` + * + * or use the `Grid` to define the grid size more generically: + * ```ts + * bind(Grid).toConstantValue({ x: 25, y: 25 }); + * bind(TYPES.ISnapper).to(GridSnapper); + * ``` + */ +@injectable() +export class GridSnapper implements ISnapper { + constructor(@optional() @inject(Grid) public readonly grid: Grid = { x: 10, y: 10 }) {} + + snap(position: Point, _element: GModelElement): Point { + return Point.snapToGrid(position, this.grid); + } +} + +@injectable() +export class CenterGridSnapper extends SprottyCenterGridSnapper { + constructor(@optional() @inject(Grid) public readonly grid: Grid = { x: 10, y: 10 }) { + super(); + } + + override get gridX(): number { + return this.grid.x; + } + + override get gridY(): number { + return this.grid.y; + } +} diff --git a/packages/client/src/features/grid/grid.ts b/packages/client/src/features/grid/grid.ts new file mode 100644 index 000000000..e2bc785b4 --- /dev/null +++ b/packages/client/src/features/grid/grid.ts @@ -0,0 +1,19 @@ +/******************************************************************************** + * Copyright (c) 2024 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 { Point } from '@eclipse-glsp/sprotty'; + +export const Grid = Symbol('Grid'); +export type Grid = Point; diff --git a/packages/client/src/features/grid/index.ts b/packages/client/src/features/grid/index.ts new file mode 100644 index 000000000..3608c783e --- /dev/null +++ b/packages/client/src/features/grid/index.ts @@ -0,0 +1,21 @@ +/******************************************************************************** + * Copyright (c) 2023-2024 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 + ********************************************************************************/ +export * from './grid'; +export * from './grid-background'; +export * from './grid-manager'; +export * from './grid-model'; +export * from './grid-module'; +export * from './grid-snapper'; diff --git a/packages/client/src/features/helper-lines/helper-line-manager-default.ts b/packages/client/src/features/helper-lines/helper-line-manager-default.ts index d503d22e4..c5f26fa58 100644 --- a/packages/client/src/features/helper-lines/helper-line-manager-default.ts +++ b/packages/client/src/features/helper-lines/helper-line-manager-default.ts @@ -19,6 +19,7 @@ import { FeedbackEmitter } from '../../base'; import { IFeedbackActionDispatcher } from '../../base/feedback/feedback-action-dispatcher'; import { ISelectionListener, SelectionService } from '../../base/selection-service'; import { SetBoundsFeedbackAction } from '../bounds/set-bounds-feedback-command'; +import { Grid } from '../grid'; import { MoveFinishedEventAction, MoveInitializedEventAction } from '../tools/change-bounds/change-bounds-tool-feedback'; import { AlignmentElementFilter, @@ -47,7 +48,7 @@ export interface IHelperLineOptions { viewportLines?: ViewportLineType[]; /** * The minimum difference between two coordinates - * Defaults to 1. + * Defaults to 1 or zero (perfect match) if the optional grid module is loaded. */ alignmentEpsilon?: number; /** @@ -58,6 +59,7 @@ export interface IHelperLineOptions { /** * The minimum move delta that is necessary for an element to break through a helper line. * Defaults to { x: 1, y: 1 } whereas the x represents the horizontal distance and y represents the vertical distance. + * If the optional grid module is loaded, defaults to twice the grid size, i.e., two grid moves to break through a helper line. */ minimumMoveDelta?: Point; @@ -84,6 +86,7 @@ export class HelperLineManager implements IActionHandler, ISelectionListener, IH @inject(TYPES.IFeedbackActionDispatcher) protected feedbackDispatcher: IFeedbackActionDispatcher; @inject(SelectionService) protected selectionService: SelectionService; @optional() @inject(TYPES.IHelperLineOptions) protected userOptions?: IHelperLineOptions; + @optional() @inject(Grid) protected grid?: Grid; protected options: Required; protected feedback: FeedbackEmitter; @@ -91,7 +94,12 @@ export class HelperLineManager implements IActionHandler, ISelectionListener, IH @postConstruct() protected init(): void { this.feedback = this.feedbackDispatcher.createEmitter(); - this.options = { ...DEFAULT_HELPER_LINE_OPTIONS, ...this.userOptions }; + const dynamicOptions: IHelperLineOptions = {}; + if (this.grid) { + dynamicOptions.alignmentEpsilon = 0; + dynamicOptions.minimumMoveDelta = Point.multiplyScalar(this.grid, 2); + } + this.options = { ...DEFAULT_HELPER_LINE_OPTIONS, ...dynamicOptions, ...this.userOptions }; this.selectionService.onSelectionChanged(change => this.selectionChanged(change.root, change.selectedElements, change.deselectedElements) ); diff --git a/packages/client/src/features/index.ts b/packages/client/src/features/index.ts index 2afb59590..8f8fad345 100644 --- a/packages/client/src/features/index.ts +++ b/packages/client/src/features/index.ts @@ -19,9 +19,11 @@ export * from './change-bounds'; export * from './command-palette'; export * from './context-menu'; export * from './copy-paste'; +export * from './debug'; export * from './decoration'; export * from './element-template'; export * from './export'; +export * from './grid'; export * from './helper-lines'; export * from './hints'; export * from './hover'; diff --git a/packages/client/src/features/tool-palette/tool-palette.ts b/packages/client/src/features/tool-palette/tool-palette.ts index e2ccb21a5..e27610a12 100644 --- a/packages/client/src/features/tool-palette/tool-palette.ts +++ b/packages/client/src/features/tool-palette/tool-palette.ts @@ -26,17 +26,20 @@ import { SetContextActions, SetModelAction, SetUIExtensionVisibilityAction, + TYPES, TriggerNodeCreationAction, UpdateModelAction, codiconCSSClasses, matchesKeystroke } from '@eclipse-glsp/sprotty'; -import { inject, injectable, postConstruct } from 'inversify'; +import { inject, injectable, optional, postConstruct } from 'inversify'; import { GLSPActionDispatcher } from '../../base/action-dispatcher'; import { EditorContextService, IEditModeListener } from '../../base/editor-context-service'; import { FocusTracker } from '../../base/focus/focus-tracker'; import { IDiagramStartup } from '../../base/model/diagram-loader'; import { EnableDefaultToolsAction, EnableToolsAction } from '../../base/tool-manager/tool'; +import { DebugManager } from '../debug'; +import { GridManager } from '../grid'; import { MouseDeleteTool } from '../tools/deletion/delete-tool'; import { MarqueeMouseTool } from '../tools/marquee-selection/marquee-mouse-tool'; @@ -74,6 +77,14 @@ export class ToolPalette extends AbstractUIExtension implements IActionHandler, @inject(FocusTracker) protected focusTracker: FocusTracker; + @optional() + @inject(TYPES.IGridManager) + protected gridManager?: GridManager; + + @optional() + @inject(TYPES.IDebugManager) + protected debugManager?: DebugManager; + protected paletteItems: PaletteItem[]; protected paletteItemsCopy: PaletteItem[] = []; protected dynamic = false; @@ -202,6 +213,16 @@ export class ToolPalette extends AbstractUIExtension implements IActionHandler, const validateActionButton = this.createValidateButton(); headerTools.appendChild(validateActionButton); + if (this.gridManager) { + const toggleGridButton = this.createToggleGridButton(); + headerTools.appendChild(toggleGridButton); + } + + if (this.debugManager) { + const toggleDebugButton = this.createToggleDebugButton(); + headerTools.appendChild(toggleDebugButton); + } + // Create button for Search const searchIcon = this.createSearchButton(); headerTools.appendChild(searchIcon); @@ -250,6 +271,46 @@ export class ToolPalette extends AbstractUIExtension implements IActionHandler, return validateActionButton; } + protected createToggleGridButton(): HTMLElement { + const toggleGridButton = createIcon('symbol-numeric'); + toggleGridButton.title = 'Toggle Grid'; + toggleGridButton.onclick = () => { + if (this.gridManager?.isGridVisible) { + toggleGridButton.classList.remove(CLICKED_CSS_CLASS); + this.gridManager?.setGridVisible(false); + } else { + toggleGridButton.classList.add(CLICKED_CSS_CLASS); + this.gridManager?.setGridVisible(true); + } + }; + if (this.gridManager?.isGridVisible) { + toggleGridButton.classList.add(CLICKED_CSS_CLASS); + } + toggleGridButton.ariaLabel = toggleGridButton.title; + toggleGridButton.tabIndex = 1; + return toggleGridButton; + } + + protected createToggleDebugButton(): HTMLElement { + const toggleDebugButton = createIcon('debug'); + toggleDebugButton.title = 'Debug Mode'; + toggleDebugButton.onclick = () => { + if (this.debugManager?.isDebugEnabled) { + toggleDebugButton.classList.remove(CLICKED_CSS_CLASS); + this.debugManager?.setDebugEnabled(false); + } else { + toggleDebugButton.classList.add(CLICKED_CSS_CLASS); + this.debugManager?.setDebugEnabled(true); + } + }; + if (this.debugManager?.isDebugEnabled) { + toggleDebugButton.classList.add(CLICKED_CSS_CLASS); + } + toggleDebugButton.ariaLabel = toggleDebugButton.title; + toggleDebugButton.tabIndex = 1; + return toggleDebugButton; + } + protected createSearchButton(): HTMLElement { const searchIcon = createIcon(SEARCH_ICON_ID); searchIcon.onclick = _ev => { diff --git a/packages/glsp-sprotty/src/re-exports.ts b/packages/glsp-sprotty/src/re-exports.ts index e08cd1456..7a52fdda6 100644 --- a/packages/glsp-sprotty/src/re-exports.ts +++ b/packages/glsp-sprotty/src/re-exports.ts @@ -184,7 +184,7 @@ export * from 'sprotty/lib/features/edge-intersection/sweepline'; export * from 'sprotty/lib/features/move/model'; export * from 'sprotty/lib/features/move/move'; -export * from 'sprotty/lib/features/move/snap'; +export { ISnapper, CenterGridSnapper as SprottyCenterGridSnapper } from 'sprotty/lib/features/move/snap'; export * from 'sprotty/lib/features/nameable/model'; @@ -326,4 +326,3 @@ export * from 'sprotty/lib/utils/iterable'; export * from 'sprotty/lib/utils/keyboard'; export * from 'sprotty/lib/utils/logging'; export * from 'sprotty/lib/utils/registry'; - diff --git a/packages/glsp-sprotty/src/types.ts b/packages/glsp-sprotty/src/types.ts index bb796d534..9c9df6f06 100644 --- a/packages/glsp-sprotty/src/types.ts +++ b/packages/glsp-sprotty/src/types.ts @@ -47,5 +47,7 @@ export const TYPES = { ILocalElementNavigator: Symbol('ILocalElementNavigator'), IDiagramOptions: Symbol('IDiagramOptions'), IDiagramStartup: Symbol('IDiagramStartup'), - IToolManager: Symbol('IToolManager') + IToolManager: Symbol('IToolManager'), + IDebugManager: Symbol('IDebugManager'), + IGridManager: Symbol('IGridManager') };