Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve relationship creation tool #53

Merged
merged 2 commits into from Mar 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Expand Up @@ -3,7 +3,14 @@
********************************************************************************/

import { RELATIONSHIP_EDGE_TYPE } from '@crossbreeze/protocol';
import { Command, CreateEdgeOperation, JsonCreateEdgeOperationHandler, ModelState } from '@eclipse-glsp/server';
import {
ActionDispatcher,
Command,
CreateEdgeOperation,
JsonCreateEdgeOperationHandler,
ModelState,
SelectAction
} from '@eclipse-glsp/server';
import { inject, injectable } from 'inversify';
import { URI, Utils as UriUtils } from 'vscode-uri';
import { CrossModelRoot, EntityNode, Relationship, RelationshipEdge, isCrossModelRoot } from '../../../language-server/generated/ast.js';
Expand All @@ -17,6 +24,7 @@ export class SystemDiagramCreateEdgeOperationHandler extends JsonCreateEdgeOpera
elementTypeIds = [RELATIONSHIP_EDGE_TYPE];

@inject(ModelState) protected override modelState!: SystemModelState;
@inject(ActionDispatcher) protected actionDispatcher!: ActionDispatcher;

createCommand(operation: CreateEdgeOperation): Command {
return new CrossModelCommand(this.modelState, () => this.createEdge(operation));
Expand Down Expand Up @@ -48,6 +56,9 @@ export class SystemDiagramCreateEdgeOperationHandler extends JsonCreateEdgeOpera
}
};
this.modelState.systemDiagram.edges.push(edge);
this.actionDispatcher.dispatchAfterNextUpdate(
SelectAction.create({ selectedElementsIDs: [this.modelState.idProvider.getLocalId(edge) ?? edge.id] })
);
}
}
}
Expand All @@ -66,6 +77,7 @@ export class SystemDiagramCreateEdgeOperationHandler extends JsonCreateEdgeOpera
$container: relationshipRoot,
id: this.modelState.idProvider.findNextId(Relationship, source + 'To' + target),
type: '1:1',
attributes: [],
parent: { $refText: sourceNode.entity?.$refText || '' },
child: { $refText: targetNode.entity?.$refText || '' }
};
Expand Down
12 changes: 10 additions & 2 deletions packages/glsp-client/src/browser/crossmodel-diagram-module.ts
Expand Up @@ -2,7 +2,15 @@
* Copyright (c) 2024 CrossBreeze.
********************************************************************************/

import { ConsoleLogger, LogLevel, SetViewportAction, TYPES, bindAsService, configureActionHandler } from '@eclipse-glsp/client';
import {
ConsoleLogger,
LogLevel,
SetViewportAction,
TYPES,
bindAsService,
bindOrRebind,
configureActionHandler
} from '@eclipse-glsp/client';
import { TheiaGLSPSelectionForwarder } from '@eclipse-glsp/theia-integration';
import { ContainerModule, interfaces } from '@theia/core/shared/inversify';
import { GridAlignmentHandler } from './crossmodel-grid-handler';
Expand All @@ -16,7 +24,7 @@ export function createCrossModelDiagramModule(registry: interfaces.ContainerModu
rebind(TYPES.LogLevel).toConstantValue(LogLevel.warn);
bindAsService(bind, CrossModelSelectionDataService, CrossModelGLSPSelectionDataService);
bind(CrossModelTheiaGLSPSelectionForwarder).toSelf().inSingletonScope();
rebind(TheiaGLSPSelectionForwarder).toService(CrossModelTheiaGLSPSelectionForwarder);
bindOrRebind({ bind, isBound, rebind }, TheiaGLSPSelectionForwarder).toService(CrossModelTheiaGLSPSelectionForwarder);
bind(TYPES.ISnapper).to(CrossModelGridSnapper);
configureActionHandler({ bind, isBound }, SetViewportAction.KIND, GridAlignmentHandler);
registry(bind, unbind, isBound, rebind, unbindAsync, onActivation, onDeactivation);
Expand Down
Expand Up @@ -9,6 +9,7 @@ import {
Args,
Bounds,
CursorCSS,
Disposable,
EdgeCreationTool,
EdgeCreationToolMouseListener,
FeedbackEdgeEndMovingMouseListener,
Expand Down Expand Up @@ -156,9 +157,6 @@ export class MappingEdgeCreationToolMouseListener extends EdgeCreationToolMouseL
DrawFeedbackEdgeAction.create({ elementTypeId: this.triggerAction.elementTypeId, sourceId: this.source })
]);
}
[Symbol.dispose](): void {
throw new Error('Method not implemented.');
}

override mouseOver(target: GModelElement, _event: MouseEvent): Action[] {
const otherAttributeParent = revertAttributeParent(this.triggerAction.args.sourceAttributeParent);
Expand Down
@@ -0,0 +1,14 @@
/********************************************************************************
* Copyright (c) 2024 CrossBreeze.
********************************************************************************/

import { EdgeCreationTool, FeatureModule, edgeCreationToolModule, viewportModule } from '@eclipse-glsp/client';
import { SystemEdgeCreationTool } from './system-edge-creation-tool';

export const systemEdgeCreationToolModule = new FeatureModule(
(bind, unbind, isBound, rebind) => {
const context = { bind, unbind, isBound, rebind };
context.rebind(EdgeCreationTool).to(SystemEdgeCreationTool).inSingletonScope();
},
{ requires: [edgeCreationToolModule, viewportModule] }
);
@@ -0,0 +1,227 @@
/********************************************************************************
* Copyright (c) 2024 CrossBreeze.
********************************************************************************/

import {
Action,
Connectable,
CreateEdgeOperation,
CursorCSS,
Disposable,
DragAwareMouseListener,
EdgeCreationTool,
EnableDefaultToolsAction,
FeedbackEdgeEndMovingMouseListener,
GEdge,
GLSPActionDispatcher,
GModelElement,
HoverFeedbackAction,
ITypeHintProvider,
ModifyCSSFeedbackAction,
Point,
TriggerEdgeCreationAction,
cursorFeedbackAction,
findParentByFeature,
isConnectable,
isCtrlOrCmd
} from '@eclipse-glsp/client';
import {
DrawFeedbackEdgeAction,
RemoveFeedbackEdgeAction
} from '@eclipse-glsp/client/lib/features/tools/edge-creation/dangling-edge-feedback';
import { injectable } from '@theia/core/shared/inversify';

const CSS_EDGE_CREATION = 'edge-creation';

@injectable()
export class SystemEdgeCreationTool extends EdgeCreationTool {
override doEnable(): void {
const mouseMovingFeedback = new FeedbackEdgeEndMovingMouseListener(this.anchorRegistry, this.feedbackDispatcher);
const listener = new SystemEdgeCreationToolMouseListener(this.triggerAction, this.actionDispatcher, this.typeHintProvider, this);
this.toDisposeOnDisable.push(
listener,
mouseMovingFeedback,
this.mouseTool.registerListener(listener),
this.mouseTool.registerListener(mouseMovingFeedback),
this.registerFeedback([], this, [
RemoveFeedbackEdgeAction.create(),
cursorFeedbackAction(),
ModifyCSSFeedbackAction.create({ remove: [CSS_EDGE_CREATION] })
])
);
}
}

export interface ConnectionContext {
element?: GModelElement & Connectable;
canConnect?: boolean;
}

export interface DragConnectionContext {
element: GModelElement & Connectable;
dragStart: Point;
}

export class SystemEdgeCreationToolMouseListener extends DragAwareMouseListener implements Disposable {
protected source?: string;
protected target?: string;
protected proxyEdge: GEdge;

protected dragContext?: DragConnectionContext;
protected mouseMoveFeedback?: Disposable;
protected sourceFeedback?: Disposable;

constructor(
protected triggerAction: TriggerEdgeCreationAction,
protected actionDispatcher: GLSPActionDispatcher,
protected typeHintProvider: ITypeHintProvider,
protected tool: EdgeCreationTool
) {
super();
this.proxyEdge = new GEdge();
this.proxyEdge.type = triggerAction.elementTypeId;
}

protected isSourceSelected(): boolean {
return this.source !== undefined;
}

protected isTargetSelected(): boolean {
return this.target !== undefined;
}

override mouseDown(target: GModelElement, event: MouseEvent): Action[] {
const result = super.mouseDown(target, event);
if (event.button === 0 && !this.isSourceSelected()) {
// update the current target
const context = this.calculateContext(target, event);
if (context.element && context.canConnect) {
this.dragContext = { element: context.element, dragStart: { x: event.clientX, y: event.clientY } };
}
}
return result;
}

override mouseMove(target: GModelElement, event: MouseEvent): Action[] {
const result = super.mouseMove(target, event);
if (this.isMouseDrag && this.dragContext && !this.isSourceSelected()) {
const dragDistance = Point.maxDistance(this.dragContext.dragStart, { x: event.clientX, y: event.clientY });
if (dragDistance > 3) {
// assign source if possible
this.source = this.dragContext.element.id;
this.tool.registerFeedback([
DrawFeedbackEdgeAction.create({ elementTypeId: this.triggerAction.elementTypeId, sourceId: this.source })
]);
this.dragContext = undefined;
}
}
this.updateFeedback(target, event);
return result;
}

override draggingMouseUp(target: GModelElement, event: MouseEvent): Action[] {
const result = super.draggingMouseUp(target, event);
if (this.isSourceSelected()) {
const context = this.calculateContext(target, event);
if (context.element && context.canConnect) {
this.target = context.element.id;
result.push(
CreateEdgeOperation.create({
elementTypeId: this.triggerAction.elementTypeId,
sourceElementId: this.source!,
targetElementId: this.target,
args: this.triggerAction.args
})
);
if (!isCtrlOrCmd(event)) {
result.push(EnableDefaultToolsAction.create());
}
}
}
this.reinitialize();
return result;
}

override nonDraggingMouseUp(_element: GModelElement, event: MouseEvent): Action[] {
this.reinitialize();
return [EnableDefaultToolsAction.create()];
}

protected canConnect(element: GModelElement | undefined, role: 'source' | 'target'): boolean {
return (
!!element &&
!!isConnectable(element) &&
element.canConnect(this.proxyEdge, role) &&
(role !== 'target' || this.source !== element?.id)
);
}

protected updateFeedback(target: GModelElement, event: MouseEvent): void {
const context = this.calculateContext(target, event);

// source element feedback
if (this.isSourceSelected()) {
this.sourceFeedback = this.tool.registerFeedback(
[HoverFeedbackAction.create({ mouseoverElement: this.source!, mouseIsOver: true })],
this.proxyEdge,
[HoverFeedbackAction.create({ mouseoverElement: this.source!, mouseIsOver: false })]
);
}

// cursor feedback
if (!context.element || context.element?.id === this.source) {
// by default we want to use the edge creation CSS when the tool is active
this.registerFeedback(
[ModifyCSSFeedbackAction.create({ add: [CSS_EDGE_CREATION] })],
[ModifyCSSFeedbackAction.create({ remove: [CSS_EDGE_CREATION] })]
);
return;
}

if (!context.canConnect) {
this.registerFeedback([cursorFeedbackAction(CursorCSS.OPERATION_NOT_ALLOWED)], [cursorFeedbackAction()]);
return;
}

const cursorCss = this.isSourceSelected() ? CursorCSS.EDGE_CREATION_TARGET : CursorCSS.EDGE_CREATION_SOURCE;
this.registerFeedback(
[cursorFeedbackAction(cursorCss), HoverFeedbackAction.create({ mouseoverElement: context.element.id, mouseIsOver: true })],
[cursorFeedbackAction(), HoverFeedbackAction.create({ mouseoverElement: context.element.id, mouseIsOver: false })]
);
}

protected registerFeedback(feedbackActions: Action[], cleanupActions?: Action[]): Disposable {
this.mouseMoveFeedback?.dispose();
this.mouseMoveFeedback = this.tool.registerFeedback(feedbackActions, this, cleanupActions);
return this.mouseMoveFeedback;
}

protected calculateContext(target: GModelElement, event: MouseEvent, previousContext?: ConnectionContext): ConnectionContext {
const context: ConnectionContext = {};
context.element = findParentByFeature(target, isConnectable);
if (previousContext && previousContext.element === context.element) {
return previousContext;
}
if (!this.isSourceSelected()) {
context.canConnect = this.canConnect(context.element, 'source');
} else if (!this.isTargetSelected()) {
context.canConnect = this.canConnect(context.element, 'target');
} else {
context.canConnect = false;
}
return context;
}

protected reinitialize(): void {
this.source = undefined;
this.target = undefined;
this.tool.registerFeedback([RemoveFeedbackEdgeAction.create()]);
this.dragContext = undefined;
this.mouseMoveFeedback?.dispose();
this.sourceFeedback?.dispose();
}

dispose(): void {
this.reinitialize();
}
}
@@ -0,0 +1,23 @@
/********************************************************************************
* Copyright (c) 2024 CrossBreeze.
********************************************************************************/
import {
FeatureModule,
RankedSelectMouseListener,
SelectAllCommand,
SelectCommand,
SelectFeedbackCommand,
TYPES,
bindAsService,
configureCommand
} from '@eclipse-glsp/client';
import { SystemSelectTool } from './select-tool';

export const systemSelectModule = new FeatureModule((bind, _unbind, isBound) => {
const context = { bind, isBound };
configureCommand(context, SelectCommand);
configureCommand(context, SelectAllCommand);
configureCommand(context, SelectFeedbackCommand);
bindAsService(context, TYPES.IDefaultTool, SystemSelectTool);
bind(RankedSelectMouseListener).toSelf().inSingletonScope();
});
@@ -0,0 +1,25 @@
/********************************************************************************
* Copyright (c) 2024 CrossBreeze.
********************************************************************************/

import { GLSPMouseTool, RankedSelectMouseListener, Tool } from '@eclipse-glsp/client';
import { inject, injectable } from '@theia/core/shared/inversify';

@injectable()
export class SystemSelectTool implements Tool {
static ID = 'tool_system_select';

id = SystemSelectTool.ID;
isEditTool = false;

@inject(GLSPMouseTool) protected mouseTool: GLSPMouseTool;
@inject(RankedSelectMouseListener) protected listener: RankedSelectMouseListener;

enable(): void {
this.mouseTool.registerListener(this.listener);
}

disable(): void {
this.mouseTool.deregister(this.listener);
}
}