Skip to content

Commit f162689

Browse files
committed
Fix keyboard movement wrapping
1 parent e289f43 commit f162689

File tree

4 files changed

+99
-58
lines changed

4 files changed

+99
-58
lines changed

packages/client/src/features/change-bounds/move-element-handler.ts

Lines changed: 77 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ import {
1818
Action,
1919
ChangeBoundsOperation,
2020
ElementAndBounds,
21-
ElementMove,
2221
IActionDispatcher,
2322
IActionHandler,
2423
ICommand,
@@ -27,24 +26,40 @@ import {
2726
MoveViewportAction,
2827
Point,
2928
TYPES,
30-
isBoundsAware
29+
isBoundsAware,
30+
type Bounds,
31+
type ElementMove,
32+
type GModelElement
3133
} from '@eclipse-glsp/sprotty';
3234
import { inject, injectable, optional, postConstruct } from 'inversify';
3335
import { DebouncedFunc, debounce } from 'lodash';
3436
import { EditorContextService } from '../../base/editor-context-service';
3537
import { IFeedbackActionDispatcher } from '../../base/feedback/feedback-action-dispatcher';
3638
import { FeedbackEmitter } from '../../base/feedback/feedback-emitter';
37-
import { SelectableBoundsAware, getElements, isSelectableAndBoundsAware } from '../../utils/gmodel-util';
39+
import {
40+
SelectableBoundsAware,
41+
getElements,
42+
isNonRoutableSelectedMovableBoundsAware,
43+
isNotUndefined,
44+
type MoveableElement
45+
} from '../../utils/gmodel-util';
3846
import { isValidMove } from '../../utils/layout-utils';
3947
import { outsideOfViewport } from '../../utils/viewpoint-util';
4048
import { IMovementRestrictor } from '../change-bounds/movement-restrictor';
49+
import type { IChangeBoundsManager } from '../tools/change-bounds/change-bounds-manager';
50+
import { TrackedElementResize, type MoveTracker } from '../tools/change-bounds/change-bounds-tracker';
51+
import { GResizeHandle } from './model';
4152
import { MoveElementRelativeAction } from './move-element-action';
4253

4354
/**
4455
* Action handler for moving elements.
4556
*/
4657
@injectable()
4758
export class MoveElementHandler implements IActionHandler {
59+
@inject(TYPES.IChangeBoundsManager)
60+
protected readonly changeBoundsManager: IChangeBoundsManager;
61+
protected moveTracker: MoveTracker;
62+
4863
@inject(EditorContextService)
4964
protected editorContextService: EditorContextService;
5065

@@ -68,10 +83,12 @@ export class MoveElementHandler implements IActionHandler {
6883
@postConstruct()
6984
protected init(): void {
7085
this.moveFeedback = this.feedbackDispatcher.createEmitter();
86+
this.moveTracker = this.changeBoundsManager.createMoveTracker();
7187
}
7288

7389
handle(action: Action): void | Action | ICommand {
74-
if (MoveElementRelativeAction.is(action)) {
90+
if (MoveElementRelativeAction.is(action) && action.elementIds.length > 0) {
91+
this.moveTracker.startTracking(this.editorContextService.modelRoot);
7592
this.handleMoveElement(action);
7693
}
7794
}
@@ -84,7 +101,7 @@ export class MoveElementHandler implements IActionHandler {
84101

85102
const viewportActions: Action[] = [];
86103
const elementMoves: ElementMove[] = [];
87-
const elements = getElements(viewport.index, action.elementIds, isSelectableAndBoundsAware);
104+
const elements = getElements(viewport.index, action.elementIds, this.isValidMoveable);
88105
for (const element of elements) {
89106
const newPosition = this.getTargetBounds(element, action);
90107
elementMoves.push({
@@ -103,12 +120,38 @@ export class MoveElementHandler implements IActionHandler {
103120
viewportActions.push(MoveViewportAction.create({ moveX: action.moveX, moveY: action.moveY }));
104121
}
105122
}
106-
107123
this.dispatcher.dispatchAll(viewportActions);
108-
const moveAction = MoveAction.create(elementMoves, { animate: false });
109-
this.moveFeedback.add(moveAction).submit();
124+
this.moveFeedback.add(this.createMoveAction(elementMoves));
125+
126+
const newBounds = elementMoves.map(this.toElementAndBounds.bind(this)).filter(isNotUndefined);
127+
const wraps = this.moveTracker.wrap(
128+
elements.map(element => {
129+
const bounds = newBounds.find(b => b.elementId === element.id)!;
130+
const toBounds: Bounds = {
131+
...element.bounds,
132+
...bounds.newSize,
133+
...bounds.newPosition
134+
};
135+
return {
136+
element: element,
137+
fromBounds: element.bounds,
138+
toBounds
139+
};
140+
})
141+
);
142+
143+
this.moveFeedback.add(TrackedElementResize.createFeedbackActions(Object.values(wraps ?? {})));
144+
this.moveFeedback.submit();
145+
146+
if (Object.keys(wraps).length > 0) {
147+
newBounds.push(
148+
...Object.values(wraps)
149+
.filter(resize => !action.elementIds.includes(resize.element.id))
150+
.map(TrackedElementResize.toElementAndBounds)
151+
);
152+
}
110153

111-
this.scheduleChangeBounds(this.toElementAndBounds(elementMoves));
154+
this.scheduleChangeBounds(newBounds);
112155
}
113156

114157
protected getTargetBounds(element: SelectableBoundsAware, action: MoveElementRelativeAction): Point {
@@ -129,28 +172,35 @@ export class MoveElementHandler implements IActionHandler {
129172
this.moveFeedback.dispose();
130173
this.dispatcher.dispatchAll([ChangeBoundsOperation.create(elementAndBounds)]);
131174
this.debouncedChangeBounds = undefined;
175+
this.moveTracker.dispose();
132176
}, 300);
133177
this.debouncedChangeBounds();
134178
}
135179

136-
protected toElementAndBounds(elementMoves: ElementMove[]): ElementAndBounds[] {
137-
const elementBounds: ElementAndBounds[] = [];
138-
for (const elementMove of elementMoves) {
139-
const element = this.editorContextService.modelRoot.index.getById(elementMove.elementId);
140-
if (element && isBoundsAware(element)) {
141-
elementBounds.push({
142-
elementId: elementMove.elementId,
143-
newSize: {
144-
height: element.bounds.height,
145-
width: element.bounds.width
146-
},
147-
newPosition: {
148-
x: elementMove.toPosition.x,
149-
y: elementMove.toPosition.y
150-
}
151-
});
152-
}
180+
protected createMoveAction(moves: ElementMove[]): Action {
181+
return MoveAction.create(moves, { animate: false });
182+
}
183+
184+
protected isValidMoveable(element?: GModelElement): element is MoveableElement & SelectableBoundsAware {
185+
return !!element && isNonRoutableSelectedMovableBoundsAware(element) && !(element instanceof GResizeHandle);
186+
}
187+
188+
protected toElementAndBounds(elementMove: ElementMove): ElementAndBounds | undefined {
189+
const element = this.editorContextService.modelRoot.index.getById(elementMove.elementId);
190+
if (element && isBoundsAware(element)) {
191+
return {
192+
elementId: elementMove.elementId,
193+
newSize: {
194+
height: element.bounds.height,
195+
width: element.bounds.width
196+
},
197+
newPosition: {
198+
x: elementMove.toPosition.x,
199+
y: elementMove.toPosition.y
200+
}
201+
};
153202
}
154-
return elementBounds;
203+
204+
return undefined;
155205
}
156206
}

packages/client/src/features/tools/change-bounds/change-bounds-tool-move-feedback.ts

Lines changed: 2 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -13,17 +13,7 @@
1313
*
1414
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
1515
********************************************************************************/
16-
import {
17-
Action,
18-
ElementMove,
19-
GModelElement,
20-
GModelRoot,
21-
MoveAction,
22-
Point,
23-
TypeGuard,
24-
findParentByFeature,
25-
type ElementAndBounds
26-
} from '@eclipse-glsp/sprotty';
16+
import { Action, ElementMove, GModelElement, GModelRoot, MoveAction, Point, TypeGuard, findParentByFeature } from '@eclipse-glsp/sprotty';
2717

2818
import { DebouncedFunc, debounce } from 'lodash';
2919
import { DragAwareMouseListener } from '../../../base/drag-aware-mouse-listener';
@@ -38,7 +28,6 @@ import {
3828
isNonRoutableSelectedMovableBoundsAware,
3929
removeDescendants
4030
} from '../../../utils/gmodel-util';
41-
import { SetBoundsFeedbackAction } from '../../bounds/set-bounds-feedback-command';
4231
import { GResizeHandle } from '../../change-bounds/model';
4332
import { ChangeBoundsTool } from './change-bounds-tool';
4433
import { MoveFinishedEventAction, MoveInitializedEventAction } from './change-bounds-tool-feedback';
@@ -154,7 +143,7 @@ export class FeedbackMoveMouseListener extends DragAwareMouseListener implements
154143
// cancel any pending move
155144
this.pendingMoveInitialized?.cancel();
156145
this.moveFeedback.add(this.createMoveAction(move), () => this.resetElementPositions(target));
157-
this.moveFeedback.add(this.createWrapAction(move));
146+
this.moveFeedback.add(TrackedElementResize.createFeedbackActions(Object.values(move.wrapResizes ?? {})));
158147
this.addMoveFeedback(move, target, event);
159148
this.tracker.updateTrackingPosition(move);
160149
this.moveFeedback.submit();
@@ -169,25 +158,10 @@ export class FeedbackMoveMouseListener extends DragAwareMouseListener implements
169158
);
170159
}
171160

172-
protected createWrapAction(tracked: TrackedMove): SetBoundsFeedbackAction {
173-
const wrapResizes = Object.values(tracked.wrapResizes ?? {});
174-
// we do not want to resize elements beyond their valid size, not even for feedback, as the next layout cycle usually corrects this
175-
const elementResizes = wrapResizes.filter(elementResize => elementResize.valid.size);
176-
return SetBoundsFeedbackAction.create(elementResizes.map(elementResize => this.toElementAndBounds(elementResize)));
177-
}
178-
179161
protected addMoveFeedback(trackedMove: TrackedMove, ctx: GModelElement, event: MouseEvent): void {
180162
this.tool.changeBoundsManager.addMoveFeedback(this.moveFeedback, trackedMove, ctx, event);
181163
}
182164

183-
protected toElementAndBounds(elementResize: TrackedElementResize): ElementAndBounds {
184-
return {
185-
elementId: elementResize.element.id,
186-
newSize: elementResize.toBounds,
187-
newPosition: elementResize.toBounds
188-
};
189-
}
190-
191165
protected initializeElementsToMove(root: GModelRoot): void {
192166
const elementsToMove = this.collectElementsToMove(root);
193167
elementsToMove.forEach(element => this.elementId2startPos.set(element.id, element.position));

packages/client/src/features/tools/change-bounds/change-bounds-tool.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -391,8 +391,9 @@ export class ChangeBoundsListener extends DragAwareMouseListener implements ISel
391391
this.initialBounds = undefined;
392392
let newBounds: ElementAndBounds[] = [elementAndBounds];
393393
if (this.resizeTracker.lastTrackedResize?.wrapResizes) {
394-
newBounds = Object.values(this.resizeTracker.lastTrackedResize.wrapResizes).map(resize =>
395-
toElementAndBounds(resize.element)
394+
// The changed element is already part of wrap resizes
395+
newBounds = Object.values(this.resizeTracker.lastTrackedResize.wrapResizes).map(
396+
TrackedElementResize.toElementAndBounds
396397
);
397398
}
398399
return [ChangeBoundsOperation.create(newBounds)];

packages/client/src/features/tools/change-bounds/change-bounds-tracker.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ import {
4444
findDescendants,
4545
getElements
4646
} from '../../../utils/gmodel-util';
47+
import { SetBoundsFeedbackAction } from '../../bounds/set-bounds-feedback-command';
4748
import { GResizeHandle, ResizeHandleLocation, isBoundsAwareMoveable, isResizable } from '../../change-bounds/model';
4849
import { DiagramMovementCalculator } from '../../change-bounds/tracker';
4950
import { ChangeBoundsManager } from './change-bounds-manager';
@@ -171,6 +172,18 @@ export namespace TrackedElementResize {
171172
isBoundsAware(obj.element) && hasObjectProp(obj, 'fromBounds') && hasObjectProp(obj, 'toBounds') && hasObjectProp(obj, 'valid')
172173
);
173174
}
175+
export function toElementAndBounds(resize: TrackedElementResize): ElementAndBounds {
176+
return {
177+
elementId: resize.element.id,
178+
newSize: resize.toBounds,
179+
newPosition: resize.toBounds
180+
};
181+
}
182+
export function createFeedbackActions(resizes: TrackedElementResize[]): SetBoundsFeedbackAction {
183+
// we do not want to resize elements beyond their valid size, not even for feedback, as the next layout cycle usually corrects this
184+
const elementResizes = resizes.filter(elementResize => elementResize.valid.size);
185+
return SetBoundsFeedbackAction.create(elementResizes.map(TrackedElementResize.toElementAndBounds));
186+
}
174187
}
175188

176189
export interface TrackedResize extends Movement {
@@ -239,6 +252,9 @@ export interface BoundsTracker {
239252
stopTracking(): this;
240253
getInitialBoundsTracker(): InitialBoundsTracker;
241254
dispose(): void;
255+
256+
// Wrapping
257+
wrap(changes: ChangeBoundsChanges[]): Record<string, TrackedElementResize>;
242258
}
243259

244260
export interface ResizeTracker extends BoundsTracker {
@@ -574,7 +590,7 @@ export class ChangeBoundsTracker implements MoveTracker, ResizeTracker {
574590
// WRAP
575591
//
576592

577-
protected wrap(changes: ChangeBoundsChanges[]): Record<string, TrackedElementResize> {
593+
wrap(changes: ChangeBoundsChanges[]): Record<string, TrackedElementResize> {
578594
const trackedElementResizes: Record<string, TrackedElementResize> = {};
579595
const initialBounds = this.initialBoundsTracker.getBounds();
580596

0 commit comments

Comments
 (0)