Skip to content
Open
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
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
109 changes: 82 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,42 @@ 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.
*
* Examples: nudging with arrow keys
*/
@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 +85,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 +103,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 +122,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 +177,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,7 +34,7 @@ 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;
Expand Down Expand Up @@ -76,7 +76,13 @@ export class MouseTrackingElementPositionListener extends DragAwareMouseListener
if (isInitializing) {
this.initialize(element, ctx, event);
}
const move = this.tracker.moveElements([element], { snap: event, restrict: event, skipStatic: !isInitializing });
const move = this.tracker.moveElements([element], {
snap: event,
restrict: event,
skipStatic: !isInitializing,
// Ghost element is feedback-only, so no need to consider wraps
wrap: false
});
const elementMove = move.elementMoves[0];
if (!elementMove) {
return [];
Expand All @@ -94,7 +100,7 @@ export class MouseTrackingElementPositionListener extends DragAwareMouseListener
}

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

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 @@ -98,14 +98,14 @@ export interface IChangeBoundsManager {
* @param size - The size to check.
* @returns True if the element has a valid size, false otherwise.
*/
hasValidSize(element: GModelElement, size?: Dimension): boolean;
hasValidSize(element: GModelElement, size?: Dimension, options?: ChangeBoundsManagerSizeOptions): boolean;

/**
* Get the minimum size of an element for changing bounds.
* @param element - The element to get the minimum size for.
* @returns The minimum size of the element.
*/
getMinimumSize(element: GModelElement): Dimension;
getMinimumSize(element: GModelElement, options?: ChangeBoundsManagerSizeOptions): Dimension;

/**
* Determine whether to use movement restriction for changing bounds.
Expand Down Expand Up @@ -177,6 +177,10 @@ export interface IChangeBoundsManager {
createTracker(): ChangeBoundsTracker;
}

export interface ChangeBoundsManagerSizeOptions {
useComputedDimensions?: boolean;
}

/**
* The default {@link IChangeBoundsManager} implementation. It is responsible for managing
* the change of bounds for {@link GModelElement}s.
Expand Down Expand Up @@ -211,24 +215,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?: ChangeBoundsManagerSizeOptions): 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: ChangeBoundsManagerSizeOptions = { 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 @@ -269,6 +273,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 wrapped element (mostly ancestors)
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 +308,7 @@ export class ChangeBoundsManager implements IChangeBoundsManager {
deleteCssClasses(elementResize.element, CSS_RESTRICTED_RESIZE)
);
});

return feedback;
}

Expand Down
Loading