Skip to content

Commit

Permalink
Introduce an optional grid module to deal with a grid layout
Browse files Browse the repository at this point in the history
- 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 eclipse-glsp/glsp#1336
  • Loading branch information
martin-fleck-at committed May 13, 2024
1 parent bf4b88a commit 0a6a3c6
Show file tree
Hide file tree
Showing 32 changed files with 913 additions and 88 deletions.
1 change: 1 addition & 0 deletions examples/workflow-glsp/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
24 changes: 14 additions & 10 deletions examples/workflow-glsp/src/workflow-diagram-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,8 @@ import {
GLSPProjectionView,
GLabel,
GLabelView,
GridSnapper,
IHelperLineOptions,
ISnapper,
LogLevel,
Point,
RectangularNodeView,
RevealNamedElementActionProvider,
RoundedCornerNodeView,
Expand All @@ -41,7 +38,9 @@ import {
bindOrRebind,
configureDefaultModelElements,
configureModelElement,
debugModule,
editLabelFeature,
gridModule,
helperLineModule,
initializeDiagramContainer
} from '@eclipse-glsp/client';
Expand All @@ -51,14 +50,14 @@ 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) => {
const context = { bind, unbind, isBound, rebind };

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);

Expand All @@ -82,22 +81,27 @@ export const workflowDiagramModule = new ContainerModule((bind, unbind, isBound,

bind<IHelperLineOptions>(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<ISnapper>(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 {
return initializeWorkflowDiagramContainer(new Container(), ...containerConfiguration);
}

export function initializeWorkflowDiagramContainer(container: Container, ...containerConfiguration: ContainerConfiguration): Container {
return initializeDiagramContainer(container, workflowDiagramModule, directTaskEditor, helperLineModule, ...containerConfiguration);
return initializeDiagramContainer(
container,
workflowDiagramModule,
directTaskEditor,
helperLineModule,
gridModule,
debugModule,
...containerConfiguration
);
}
30 changes: 30 additions & 0 deletions examples/workflow-glsp/src/workflow-startup.ts
Original file line number Diff line number Diff line change
@@ -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<void> {
this.gridManager.setGridVisible(true);
}
}
3 changes: 2 additions & 1 deletion examples/workflow-standalone/css/diagram.css
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@
--glsp-info-foreground: blue;
}

.sprotty-graph {
.sprotty-graph,
.grid-background {
background: rgb(179, 196, 202);
}

Expand Down
13 changes: 2 additions & 11 deletions examples/workflow-standalone/src/di.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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 });
Expand Down
25 changes: 25 additions & 0 deletions packages/client/css/debug.css
Original file line number Diff line number Diff line change
@@ -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;
}
24 changes: 24 additions & 0 deletions packages/client/css/grid.css
Original file line number Diff line number Diff line change
@@ -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,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 10 10"><rect class="grid-background-grid" width="10" height="10" x="0" y="0" fill="none" stroke="black" stroke-width="1" stroke-opacity="0.10" /></svg>');
}

.grid-background .sprotty-graph {
background: var(--sprotty-background);
background-image: var(--grid-background-image);
}
1 change: 0 additions & 1 deletion packages/client/css/keyboard-tool-palette.css
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@

.accessibility-tool-palette.tool-palette {
top: 48px;
width: 240px;
}

.accessibility-tool-palette .header-tools i {
Expand Down
3 changes: 2 additions & 1 deletion packages/client/css/tool-palette.css
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
right: 40px;
top: 25px;
text-align: center;
width: 225px;
width: fit-content;
display: block;
z-index: 1000;
border-style: solid;
Expand Down Expand Up @@ -54,6 +54,7 @@
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
gap: 20px;
}

.header-icon {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
25 changes: 1 addition & 24 deletions packages/client/src/features/change-bounds/snap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading

0 comments on commit 0a6a3c6

Please sign in to comment.