Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
305 changes: 303 additions & 2 deletions examples/workflow-standalone/app/example1.wf

Large diffs are not rendered by default.

7 changes: 1 addition & 6 deletions packages/client/src/features/change-bounds/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,10 @@ import {
Point,
hoverFeedbackFeature,
isBoundsAware,
isMoveable,
isSelectable
} from '@eclipse-glsp/sprotty';
import { CursorCSS } from '../../base/feedback/css-feedback';
import { BoundsAwareModelElement, MoveableElement, ResizableModelElement } from '../../utils/gmodel-util';
import type { ResizableModelElement } from '../../utils/gmodel-util';

export const resizeFeature = Symbol('resizeFeature');

Expand Down Expand Up @@ -105,10 +104,6 @@ export namespace ResizeHandleLocation {
}
}

export function isBoundsAwareMoveable(element: GModelElement): element is BoundsAwareModelElement & MoveableElement {
return isMoveable(element) && isBoundsAware(element);
}

export class GResizeHandle extends GChildElement implements Hoverable {
static readonly TYPE = 'resize-handle';

Expand Down
107 changes: 80 additions & 27 deletions packages/client/src/features/change-bounds/move-element-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ import {
Action,
ChangeBoundsOperation,
ElementAndBounds,
ElementMove,
IActionDispatcher,
IActionHandler,
ICommand,
Expand All @@ -27,24 +26,40 @@ import {
MoveViewportAction,
Point,
TYPES,
isBoundsAware
isBoundsAware,
type Bounds,
type ElementMove,
type GModelElement
} from '@eclipse-glsp/sprotty';
import { inject, injectable, optional, postConstruct } from 'inversify';
import { DebouncedFunc, debounce } from 'lodash';
import { EditorContextService } from '../../base/editor-context-service';
import { IFeedbackActionDispatcher } from '../../base/feedback/feedback-action-dispatcher';
import { FeedbackEmitter } from '../../base/feedback/feedback-emitter';
import { SelectableBoundsAware, getElements, isSelectableAndBoundsAware } from '../../utils/gmodel-util';
import {
SelectableBoundsAware,
getElements,
isNonRoutableSelectedMovableBoundsAware,
isNotUndefined,
type MoveableElement
} from '../../utils/gmodel-util';
import { isValidMove } from '../../utils/layout-utils';
import { outsideOfViewport } from '../../utils/viewpoint-util';
import { IMovementRestrictor } from '../change-bounds/movement-restrictor';
import type { IChangeBoundsManager } from '../tools/change-bounds/change-bounds-manager';
import { TrackedElementResize, type ChangeBoundsTracker } from '../tools/change-bounds/change-bounds-tracker';
import { GResizeHandle } from './model';
import { MoveElementRelativeAction } from './move-element-action';

/**
* Action handler for moving elements.
*/
@injectable()
export class MoveElementHandler implements IActionHandler {
@inject(TYPES.IChangeBoundsManager)
protected readonly changeBoundsManager: IChangeBoundsManager;
protected tracker: ChangeBoundsTracker;

@inject(EditorContextService)
protected editorContextService: EditorContextService;

Expand All @@ -68,10 +83,12 @@ export class MoveElementHandler implements IActionHandler {
@postConstruct()
protected init(): void {
this.moveFeedback = this.feedbackDispatcher.createEmitter();
this.tracker = this.changeBoundsManager.createTracker();
}

handle(action: Action): void | Action | ICommand {
if (MoveElementRelativeAction.is(action)) {
if (MoveElementRelativeAction.is(action) && action.elementIds.length > 0) {
this.tracker.startTracking(this.editorContextService.modelRoot);
this.handleMoveElement(action);
}
}
Expand All @@ -84,7 +101,7 @@ export class MoveElementHandler implements IActionHandler {

const viewportActions: Action[] = [];
const elementMoves: ElementMove[] = [];
const elements = getElements(viewport.index, action.elementIds, isSelectableAndBoundsAware);
const elements = getElements(viewport.index, action.elementIds, this.isValidMoveable);
for (const element of elements) {
const newPosition = this.getTargetBounds(element, action);
elementMoves.push({
Expand All @@ -103,12 +120,41 @@ export class MoveElementHandler implements IActionHandler {
viewportActions.push(MoveViewportAction.create({ moveX: action.moveX, moveY: action.moveY }));
}
}

this.dispatcher.dispatchAll(viewportActions);
const moveAction = MoveAction.create(elementMoves, { animate: false });
this.moveFeedback.add(moveAction).submit();
this.moveFeedback.add(this.createMoveAction(elementMoves));

const newBounds = elementMoves.map(this.toElementAndBounds.bind(this)).filter(isNotUndefined);
const wraps = this.tracker.wrap(
elements.map(element => {
const bounds = newBounds.find(b => b.elementId === element.id)!;
const toBounds: Bounds = {
...element.bounds,
...bounds.newSize,
...bounds.newPosition
};
return {
element: element,
fromBounds: element.bounds,
toBounds
};
}),
{
validate: true
}
);

this.moveFeedback.add(TrackedElementResize.createFeedbackActions(Object.values(wraps ?? {})));
this.moveFeedback.submit();

if (Object.keys(wraps).length > 0) {
newBounds.push(
...Object.values(wraps)
.filter(resize => !action.elementIds.includes(resize.element.id))
.map(TrackedElementResize.toElementAndBounds)
);
}

this.scheduleChangeBounds(this.toElementAndBounds(elementMoves));
this.scheduleChangeBounds(newBounds);
}

protected getTargetBounds(element: SelectableBoundsAware, action: MoveElementRelativeAction): Point {
Expand All @@ -129,28 +175,35 @@ export class MoveElementHandler implements IActionHandler {
this.moveFeedback.dispose();
this.dispatcher.dispatchAll([ChangeBoundsOperation.create(elementAndBounds)]);
this.debouncedChangeBounds = undefined;
this.tracker.dispose();
}, 300);
this.debouncedChangeBounds();
}

protected toElementAndBounds(elementMoves: ElementMove[]): ElementAndBounds[] {
const elementBounds: ElementAndBounds[] = [];
for (const elementMove of elementMoves) {
const element = this.editorContextService.modelRoot.index.getById(elementMove.elementId);
if (element && isBoundsAware(element)) {
elementBounds.push({
elementId: elementMove.elementId,
newSize: {
height: element.bounds.height,
width: element.bounds.width
},
newPosition: {
x: elementMove.toPosition.x,
y: elementMove.toPosition.y
}
});
}
protected createMoveAction(moves: ElementMove[]): Action {
return MoveAction.create(moves, { animate: false });
}

protected isValidMoveable(element?: GModelElement): element is MoveableElement & SelectableBoundsAware {
return !!element && isNonRoutableSelectedMovableBoundsAware(element) && !(element instanceof GResizeHandle);
}

protected toElementAndBounds(elementMove: ElementMove): ElementAndBounds | undefined {
const element = this.editorContextService.modelRoot.index.getById(elementMove.elementId);
if (element && isBoundsAware(element)) {
return {
elementId: elementMove.elementId,
newSize: {
height: element.bounds.height,
width: element.bounds.width
},
newPosition: {
x: elementMove.toPosition.x,
y: elementMove.toPosition.y
}
};
}
return elementBounds;

return undefined;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,15 +34,15 @@ import { getAbsolutePosition } from '../../utils/viewpoint-util';
import { FeedbackAwareTool } from '../tools/base-tools';
import { IChangeBoundsManager } from '../tools/change-bounds/change-bounds-manager';
import { MoveFinishedEventAction } from '../tools/change-bounds/change-bounds-tool-feedback';
import { ChangeBoundsTracker, TrackedMove } from '../tools/change-bounds/change-bounds-tracker';
import { TrackedMove, type ChangeBoundsTracker } from '../tools/change-bounds/change-bounds-tracker';

export interface PositioningTool extends FeedbackAwareTool {
readonly changeBoundsManager: IChangeBoundsManager;
}

export class MouseTrackingElementPositionListener extends DragAwareMouseListener {
protected moveGhostFeedback: FeedbackEmitter;
protected tracker: ChangeBoundsTracker;
protected changeBoundsTracker: ChangeBoundsTracker;
protected toDispose = new DisposableCollection();

constructor(
Expand All @@ -52,7 +52,7 @@ export class MouseTrackingElementPositionListener extends DragAwareMouseListener
protected editorContext?: EditorContextService
) {
super();
this.tracker = this.tool.changeBoundsManager.createTracker();
this.changeBoundsTracker = this.tool.changeBoundsManager.createTracker();
this.moveGhostFeedback = this.tool.createFeedbackEmitter();
this.toDispose.push(this.moveGhostFeedback);
const modelRootChangedListener = editorContext?.onModelRootChanged(newRoot => this.modelRootChanged(newRoot));
Expand All @@ -72,11 +72,16 @@ export class MouseTrackingElementPositionListener extends DragAwareMouseListener
if (!element) {
return [];
}
const isInitializing = !this.tracker.isTracking();
const isInitializing = !this.changeBoundsTracker.isTracking();
if (isInitializing) {
this.initialize(element, ctx, event);
}
const move = this.tracker.moveElements([element], { snap: event, restrict: event, skipStatic: !isInitializing });
const move = this.changeBoundsTracker.moveElements([element], {
snap: event,
restrict: event,
skipStatic: !isInitializing,
wrap: false
});
const elementMove = move.elementMoves[0];
if (!elementMove) {
return [];
Expand All @@ -89,12 +94,12 @@ export class MouseTrackingElementPositionListener extends DragAwareMouseListener
);
this.addMoveFeedback(move, ctx, event);
this.moveGhostFeedback.submit();
this.tracker.updateTrackingPosition(elementMove.moveVector);
this.changeBoundsTracker.updateTrackingPosition(elementMove.moveVector);
return [];
}

protected initialize(element: MoveableElement, target: GModelElement, event: MouseEvent): void {
this.tracker.startTracking();
this.changeBoundsTracker.startTracking(target.root);
element.position = this.initializeElementPosition(element, target, event);
}

Expand All @@ -112,7 +117,7 @@ export class MouseTrackingElementPositionListener extends DragAwareMouseListener

protected modelRootChanged(root: Readonly<GModelRoot>): void {
// stop the tracking once we receive a new model root and ensure proper alignment with next next mouse move
this.tracker.stopTracking();
this.changeBoundsTracker.stopTracking();
}

override dispose(): void {
Expand Down
4 changes: 2 additions & 2 deletions packages/client/src/features/layout/layout-elements-action.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,9 @@ import {
} from '@eclipse-glsp/sprotty';
import { inject, injectable, optional } from 'inversify';
import { SelectionService } from '../../base/selection-service';
import { BoundsAwareModelElement, getElements } from '../../utils/gmodel-util';
import { BoundsAwareModelElement, getElements, isBoundsAwareMoveable } from '../../utils/gmodel-util';
import { toValidElementAndBounds, toValidElementMove } from '../../utils/layout-utils';
import { isBoundsAwareMoveable, isResizable } from '../change-bounds/model';
import { isResizable } from '../change-bounds/model';
import { IMovementRestrictor } from '../change-bounds/movement-restrictor';

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -211,24 +211,24 @@ export class ChangeBoundsManager implements IChangeBoundsManager {
return !isLocateable(element) || isValidMove(element, position ?? element.position, this.movementRestrictor);
}

hasValidSize(element: GModelElement, size?: Dimension): boolean {
hasValidSize(element: GModelElement, size?: Dimension, options?: { useComputedDimensions?: boolean }): boolean {
if (!isBoundsAware(element)) {
return true;
}
const dimension: Dimension = size ?? element.bounds;
const minimum = this.getMinimumSize(element);
const minimum = this.getMinimumSize(element, options);
if (dimension.width < minimum.width || dimension.height < minimum.height) {
return false;
}
return true;
}

getMinimumSize(element: GModelElement): Dimension {
getMinimumSize(element: GModelElement, options: { useComputedDimensions?: boolean } = { useComputedDimensions: true }): Dimension {
if (!isBoundsAware(element)) {
return Dimension.EMPTY;
}
const definedMinimum = minDimensions(element);
const computedMinimum = LayoutAware.getComputedDimensions(element);
const computedMinimum = options.useComputedDimensions ? LayoutAware.getComputedDimensions(element) : undefined;
return computedMinimum
? {
width: Math.max(definedMinimum.width, computedMinimum.width),
Expand Down Expand Up @@ -268,7 +268,14 @@ export class ChangeBoundsManager implements IChangeBoundsManager {

// restriction feedback on each element
trackedMove.elementMoves.forEach(move => this.addMoveRestrictionFeedback(feedback, move, ctx, event));

// restriction feedback on each element
Object.values(trackedMove.wrapResizes ?? {}).forEach(elementResize => {
this.addMoveRestrictionFeedback(feedback, elementResize, ctx, event);
feedback.add(
toggleCssClasses(elementResize.element, !elementResize.valid.size, CSS_RESTRICTED_RESIZE),
deleteCssClasses(elementResize.element, CSS_RESTRICTED_RESIZE)
);
});
return feedback;
}

Expand Down Expand Up @@ -296,6 +303,7 @@ export class ChangeBoundsManager implements IChangeBoundsManager {
deleteCssClasses(elementResize.element, CSS_RESTRICTED_RESIZE)
);
});

return feedback;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ import {
import { GResizeHandle } from '../../change-bounds/model';
import { ChangeBoundsTool } from './change-bounds-tool';
import { MoveFinishedEventAction, MoveInitializedEventAction } from './change-bounds-tool-feedback';
import { ChangeBoundsTracker, TrackedMove } from './change-bounds-tracker';
import { TrackedElementResize, TrackedMove, type ChangeBoundsTracker } from './change-bounds-tracker';

/**
* This mouse listener provides visual feedback for moving by sending client-side
Expand Down Expand Up @@ -79,7 +79,7 @@ export class FeedbackMoveMouseListener extends DragAwareMouseListener implements
}
const moveable = findParentByFeature(target, this.isValidMoveable);
if (moveable !== undefined) {
this.tracker.startTracking();
this.tracker.startTracking(target.root);
this.scheduleMoveInitialized();
} else {
this.tracker.stopTracking();
Expand All @@ -91,7 +91,7 @@ export class FeedbackMoveMouseListener extends DragAwareMouseListener implements
this.pendingMoveInitialized = debounce(() => {
this.moveInitialized();
this.pendingMoveInitialized = undefined;
}, 750);
}, this.moveInitializationTimeout());
this.pendingMoveInitialized();
}

Expand Down Expand Up @@ -143,6 +143,7 @@ export class FeedbackMoveMouseListener extends DragAwareMouseListener implements
// cancel any pending move
this.pendingMoveInitialized?.cancel();
this.moveFeedback.add(this.createMoveAction(move), () => this.resetElementPositions(target));
this.moveFeedback.add(this.createWrapAction(move));
this.addMoveFeedback(move, target, event);
this.tracker.updateTrackingPosition(move);
this.moveFeedback.submit();
Expand All @@ -157,6 +158,10 @@ export class FeedbackMoveMouseListener extends DragAwareMouseListener implements
);
}

protected createWrapAction(trackedMove: TrackedMove): Action {
return TrackedElementResize.createFeedbackActions(Object.values(trackedMove.wrapResizes ?? {}), { validOnly: false });
}

protected addMoveFeedback(trackedMove: TrackedMove, ctx: GModelElement, event: MouseEvent): void {
this.tool.changeBoundsManager.addMoveFeedback(this.moveFeedback, trackedMove, ctx, event);
}
Expand Down Expand Up @@ -220,7 +225,7 @@ export class FeedbackMoveMouseListener extends DragAwareMouseListener implements
this.pendingMoveInitialized?.cancel();
this.moveInitializedFeedback.dispose();
this.moveFeedback.dispose();
this.tracker.dispose();
this.tracker.stopTracking();
this.elementId2startPos.clear();
super.dispose();
}
Expand Down
Loading
Loading