Skip to content

Commit

Permalink
Support rendering of ghost element when creating elements (#301)
Browse files Browse the repository at this point in the history
* Support rendering of ghost element when creating elements

- Introduce new element template module
-- Adding new elements to the diagram based on templates
-- Removing templates again

- Support local bounds calculation for client-only added elements
-- Must trigger RequestBounds for hidden view calculation
-- Must not modify main view until ComputedBounds response is handled
-- Mark ResizeHandles as not to be considered for bounds calculation
-- Fix positioning of decorations for hidden view calculation

- Add ghost element extension to tool palette
-- Optional ghost element field in the trigger node creation action
-- Ghost element is added as feedback to the mouse cursor
-- If ghost element is dynamic, templates (palette items) are reloaded

- Bonus: Re-calculate local bounds for resize behavior

Fixes eclipse-glsp/glsp#1159
  • Loading branch information
martin-fleck-at authored Nov 20, 2023
1 parent 0c22eb6 commit 545a20c
Show file tree
Hide file tree
Showing 29 changed files with 730 additions and 87 deletions.
10 changes: 9 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
# Eclipse GLSP Client Changelog

## v2.0.0 - 14/10/2023
## v2.10.0 - active

### Changes

- [diagram] Remove unused handleSetContextActions from ToolPalette [#301](https://github.com/eclipse-glsp/glsp-client/pull/301)

### Breaking Changes

## [v2.0.0 - 14/10/2023](https://github.com/eclipse-glsp/glsp-client/releases/tag/v2.0.0)

### Changes

Expand Down
25 changes: 25 additions & 0 deletions packages/client/css/ghost-element.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/********************************************************************************
* 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
********************************************************************************/

.ghost-element {
/* we are a true ghost so we do not want to be used as a target for any mouse event */
pointer-events: none;
opacity: 0.8;
}

.ghost-element.hidden {
display: none;
}
5 changes: 5 additions & 0 deletions packages/client/css/glsp-sprotty.css
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,11 @@
fill: #1d80d1;
}

.sprotty-hidden .sprotty-resize-handle {
/** resize handles should not be considered as part of the elements bounds */
display: none;
}

.sprotty-edge {
fill: none;
stroke-width: 1.5px;
Expand Down
14 changes: 12 additions & 2 deletions packages/client/src/base/args-feature.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,20 @@ export interface ArgsAware {
args?: Args;
}

export function isArgsAware(element: GModelElement): element is GModelElement & ArgsAware {
return element.hasFeature(argsFeature);
export function isArgsAware(element?: GModelElement): element is GModelElement & ArgsAware {
return element !== undefined && element.hasFeature(argsFeature);
}

export function hasArgs(element?: GModelElement): element is GModelElement & Required<ArgsAware> {
return element !== undefined && isArgsAware(element) && element.args !== undefined;
}

export function ensureArgs(element?: GModelElement): element is GModelElement & Required<ArgsAware> {
if (!isArgsAware(element)) {
return false;
}
if (element.args === undefined) {
element.args = {};
}
return true;
}
5 changes: 4 additions & 1 deletion packages/client/src/base/feedback/css-feedback.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@
*
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
********************************************************************************/
import { inject, injectable } from 'inversify';
import { Action, CommandExecutionContext, GModelElement, GModelRoot, TYPES, hasArrayProp } from '@eclipse-glsp/sprotty';
import { inject, injectable } from 'inversify';
import { addCssClasses, getElements, removeCssClasses } from '../../utils/gmodel-util';
import { FeedbackCommand } from './feedback-command';

Expand Down Expand Up @@ -69,6 +69,9 @@ export class ModifyCssFeedbackCommand extends FeedbackCommand {
}
}

export const CSS_GHOST_ELEMENT = 'ghost-element';
export const CSS_HIDDEN = 'hidden';

export enum CursorCSS {
DEFAULT = 'default-mode',
OVERLAP_FORBIDDEN = 'overlap-forbidden-mode',
Expand Down
2 changes: 2 additions & 0 deletions packages/client/src/default-modules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import { commandPaletteModule } from './features/command-palette/command-palette
import { contextMenuModule } from './features/context-menu/context-menu-module';
import { copyPasteModule } from './features/copy-paste/copy-paste-modules';
import { decorationModule } from './features/decoration/decoration-module';
import { elementTemplateModule } from './features/element-template/element-template-module';
import { exportModule } from './features/export/export-modules';
import { typeHintsModule } from './features/hints/type-hints-module';
import { hoverModule } from './features/hover/hover-module';
Expand Down Expand Up @@ -90,6 +91,7 @@ export const DEFAULT_MODULES = [
edgeCreationToolModule,
edgeEditToolModule,
deletionToolModule,
elementTemplateModule,
nodeCreationToolModule,
changeBoundsToolModule,
marqueeSelectionToolModule,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,19 @@
********************************************************************************/
import '../../../../css/keyboard-tool-palette.css';

import { configureActionHandler, TYPES, bindAsService, BindingContext, FeatureModule } from '@eclipse-glsp/sprotty';
import {
BindingContext,
FeatureModule,
SetModelAction,
TYPES,
UpdateModelAction,
bindAsService,
configureActionHandler
} from '@eclipse-glsp/sprotty';
import { EnableDefaultToolsAction } from '../../../base/tool-manager/tool';
import { EnableToolPaletteAction } from '../../tool-palette/tool-palette';
import { KeyboardToolPalette } from './keyboard-tool-palette';
import { FocusDomAction } from '../actions';
import { EnableDefaultToolsAction } from '../../../base/tool-manager/tool';
import { KeyboardToolPalette } from './keyboard-tool-palette';

export const keyboardToolPaletteModule = new FeatureModule((bind, unbind, isBound, rebind) => {
const context = { bind, unbind, isBound, rebind };
Expand All @@ -32,4 +40,6 @@ export function configureKeyboardToolPaletteTool(context: BindingContext): void
configureActionHandler(context, EnableDefaultToolsAction.KIND, KeyboardToolPalette);
configureActionHandler(context, FocusDomAction.KIND, KeyboardToolPalette);
configureActionHandler(context, EnableToolPaletteAction.KIND, KeyboardToolPalette);
configureActionHandler(context, UpdateModelAction.KIND, KeyboardToolPalette);
configureActionHandler(context, SetModelAction.KIND, KeyboardToolPalette);
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,35 +13,35 @@
*
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
********************************************************************************/
import { injectable } from 'inversify';
import {
ICommand,
SetUIExtensionVisibilityAction,
Action,
ICommand,
PaletteItem,
RequestContextActions,
RequestMarkersAction,
SetContextActions,
SetUIExtensionVisibilityAction,
TriggerNodeCreationAction
} from '@eclipse-glsp/sprotty';
import { injectable } from 'inversify';
import { KeyCode, matchesKeystroke } from 'sprotty/lib/utils/keyboard';
import { MouseDeleteTool } from '../../tools/deletion/delete-tool';
import { MarqueeMouseTool } from '../../tools/marquee-selection/marquee-mouse-tool';
import { EnableDefaultToolsAction, EnableToolsAction } from '../../../base/tool-manager/tool';
import {
createIcon,
changeCodiconClass,
createToolGroup,
EnableToolPaletteAction,
ToolPalette,
changeCodiconClass,
compare,
EnableToolPaletteAction
createIcon,
createToolGroup
} from '../../tool-palette/tool-palette';
import { KeyboardNodeGridMetadata } from '../keyboard-grid/constants';
import { MouseDeleteTool } from '../../tools/deletion/delete-tool';
import { MarqueeMouseTool } from '../../tools/marquee-selection/marquee-mouse-tool';
import { FocusDomAction } from '../actions';
import { EdgeAutocompletePaletteMetadata } from '../edge-autocomplete/edge-autocomplete-palette';
import { EnableDefaultToolsAction, EnableToolsAction } from '../../../base/tool-manager/tool';
import { ShowToastMessageAction } from '../toast/toast-handler';
import { ElementNavigatorKeyListener } from '../element-navigation/diagram-navigation-tool';
import { KeyboardNodeGridMetadata } from '../keyboard-grid/constants';
import * as messages from '../toast/messages.json';
import { ShowToastMessageAction } from '../toast/toast-handler';

const SEARCH_ICON_ID = 'search';
const PALETTE_ICON_ID = 'symbol-color';
Expand Down Expand Up @@ -119,7 +119,7 @@ export class KeyboardToolPalette extends ToolPalette {
}

override handle(action: Action): ICommand | Action | void {
if (action.kind === EnableToolPaletteAction.KIND) {
if (EnableToolPaletteAction.is(action)) {
const requestAction = RequestContextActions.create({
contextId: ToolPalette.ID,
editorContext: {
Expand All @@ -134,16 +134,15 @@ export class KeyboardToolPalette extends ToolPalette {
]);
}
});
} else if (action.kind === EnableDefaultToolsAction.KIND) {
this.changeActiveButton();
this.restoreFocus();
} else if (FocusDomAction.is(action) && action.id === ToolPalette.ID) {
if (this.containerElement.contains(document.activeElement)) {
this.toggleShortcutVisibility();
} else {
this.showShortcuts();
}
this.containerElement.focus();
} else {
super.handle(action);
}
}

Expand Down
6 changes: 6 additions & 0 deletions packages/client/src/features/bounds/bounds-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ import { FreeFormLayouter } from './freeform-layout';
import { GLSPHiddenBoundsUpdater } from './glsp-hidden-bounds-updater';
import { HBoxLayouterExt } from './hbox-layout';
import { LayouterExt } from './layouter';
import { LocalComputedBoundsCommand } from './local-bounds';
import { SetBoundsFeebackCommand } from './set-bounds-feedback-command';
import { VBoxLayouterExt } from './vbox-layout';

export const boundsModule = new FeatureModule((bind, _unbind, isBound, _rebind) => {
Expand All @@ -38,6 +40,10 @@ export const boundsModule = new FeatureModule((bind, _unbind, isBound, _rebind)
configureCommand(context, RequestBoundsCommand);
bind(HiddenBoundsUpdater).toSelf().inSingletonScope();
bindAsService(context, TYPES.HiddenVNodePostprocessor, GLSPHiddenBoundsUpdater);

configureCommand(context, LocalComputedBoundsCommand);
configureCommand(context, SetBoundsFeebackCommand);

bind(TYPES.Layouter).to(LayouterExt).inSingletonScope();
bind(TYPES.LayoutRegistry).to(LayoutRegistry).inSingletonScope();

Expand Down
18 changes: 11 additions & 7 deletions packages/client/src/features/bounds/glsp-hidden-bounds-updater.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,22 +14,23 @@
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
********************************************************************************/

import { inject, injectable, optional } from 'inversify';
import { VNode } from 'snabbdom';
import {
Action,
ComputedBoundsAction,
Deferred,
EdgeRouterRegistry,
ElementAndRoutingPoints,
GModelElement,
GRoutableElement,
HiddenBoundsUpdater,
IActionDispatcher,
RequestAction,
ResponseAction,
GModelElement,
GRoutableElement
ResponseAction
} from '@eclipse-glsp/sprotty';
import { inject, injectable, optional } from 'inversify';
import { VNode } from 'snabbdom';
import { calcElementAndRoute, isRoutable } from '../../utils/gmodel-util';
import { LocalComputedBoundsAction, LocalRequestBoundsAction } from './local-bounds';

/**
* Grabs the bounds from hidden SVG DOM elements, applies layouts, collects routes and fires {@link ComputedBoundsAction}s.
Expand All @@ -56,7 +57,7 @@ export class GLSPHiddenBoundsUpdater extends HiddenBoundsUpdater {
const actions = this.captureActions(() => super.postUpdate(cause));
actions
.filter(action => ComputedBoundsAction.is(action))
.forEach(action => this.actionDispatcher.dispatch(this.enhanceAction(action as ComputedBoundsAction)));
.forEach(action => this.actionDispatcher.dispatch(this.enhanceAction(action as ComputedBoundsAction, cause)));
this.element2route = [];
}

Expand All @@ -72,7 +73,10 @@ export class GLSPHiddenBoundsUpdater extends HiddenBoundsUpdater {
}
}

protected enhanceAction(action: ComputedBoundsAction): ComputedBoundsAction {
protected enhanceAction(action: ComputedBoundsAction, cause?: Action): ComputedBoundsAction {
if (LocalRequestBoundsAction.is(cause)) {
LocalComputedBoundsAction.mark(action);
}
action.routes = this.element2route.length === 0 ? undefined : this.element2route;
return action;
}
Expand Down
91 changes: 91 additions & 0 deletions packages/client/src/features/bounds/local-bounds.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/********************************************************************************
* 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 {
Action,
ActionDispatcher,
Command,
CommandExecutionContext,
CommandResult,
CommandReturn,
ComputedBoundsAction,
ComputedBoundsApplicator,
GModelRoot,
GModelRootSchema,
RequestBoundsAction,
TYPES
} from '@eclipse-glsp/sprotty';
import { inject, injectable } from 'inversify';
import { ServerAction } from '../../base/model/glsp-model-source';

export namespace LocalRequestBoundsAction {
export function is(object: unknown): object is RequestBoundsAction {
return RequestBoundsAction.is(object) && !ServerAction.is(object);
}

export function fromCommand(context: CommandExecutionContext, actionDispatcher: ActionDispatcher, cause?: Action): CommandResult {
// do not modify the main model (modelChanged = false) but request local bounds calculation on hidden model
actionDispatcher.dispatch(RequestBoundsAction.create(context.root as unknown as GModelRootSchema));
return {
model: context.root,
modelChanged: false,
cause
};
}
}

export namespace LocalComputedBoundsAction {
export function is(object: unknown): object is RequestBoundsAction {
return ComputedBoundsAction.is(object) && ServerAction.is(object);
}

export function mark(action: ComputedBoundsAction): ComputedBoundsAction {
// mimic: we mark the computed bounds action as coming from the server so it is not sent to the server and handled locally
ServerAction.mark(action);
return action;
}
}

@injectable()
export class LocalComputedBoundsCommand extends Command {
static readonly KIND: string = ComputedBoundsAction.KIND;

@inject(ComputedBoundsApplicator) protected readonly computedBoundsApplicator: ComputedBoundsApplicator;

constructor(@inject(TYPES.Action) readonly action: ComputedBoundsAction) {
super();
}

override execute(context: CommandExecutionContext): GModelRoot | CommandResult {
if (LocalComputedBoundsAction.is(this.action)) {
// apply computed bounds from the hidden model and return updated model to render new main model
this.computedBoundsApplicator.apply(context.root as unknown as GModelRootSchema, this.action);
return context.root;
}
// computed bounds action from server -> we do not care and do not trigger any update of the main model
return {
model: context.root,
modelChanged: false
};
}

override undo(context: CommandExecutionContext): CommandReturn {
return context.root;
}

override redo(context: CommandExecutionContext): CommandReturn {
return context.root;
}
}
Loading

0 comments on commit 545a20c

Please sign in to comment.