@eclipse-glsp/client
Version:
A sprotty-based client for GLSP
361 lines • 18.5 kB
JavaScript
"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
var ChangeBoundsTool_1;
Object.defineProperty(exports, "__esModule", { value: true });
exports.ChangeBoundsListener = exports.ChangeBoundsTool = void 0;
/********************************************************************************
* Copyright (c) 2019-2025 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
********************************************************************************/
const inversify_1 = require("inversify");
const sprotty_1 = require("@eclipse-glsp/sprotty");
const drag_aware_mouse_listener_1 = require("../../../base/drag-aware-mouse-listener");
const messages_1 = require("../../../base/messages");
const selection_service_1 = require("../../../base/selection-service");
const gmodel_util_1 = require("../../../utils/gmodel-util");
const local_bounds_1 = require("../../bounds/local-bounds");
const set_bounds_feedback_command_1 = require("../../bounds/set-bounds-feedback-command");
const model_1 = require("../../change-bounds/model");
const move_element_key_listener_1 = require("../../change-bounds/move-element-key-listener");
const grid_1 = require("../../grid/grid");
const base_tools_1 = require("../base-tools");
const change_bounds_manager_1 = require("./change-bounds-manager");
const change_bounds_tool_feedback_1 = require("./change-bounds-tool-feedback");
const change_bounds_tool_move_feedback_1 = require("./change-bounds-tool-move-feedback");
/**
* The change bounds tool has the license to move multiple elements or resize a single element by implementing the ChangeBounds operation.
* In contrast to Sprotty's implementation this tool only sends a `ChangeBoundsOperationAction` when an operation has finished and does not
* provide client-side live updates to improve performance.
*
* | Operation | Client Update | Server Update
* +-----------+------------------+----------------------------
* | Move | MoveAction | ChangeBoundsOperationAction
* | Resize | SetBoundsAction | ChangeBoundsOperationAction
*
* To provide a visual client updates during move we install the `FeedbackMoveMouseListener` and to provide visual client updates during
* resize and send the server updates we install the `ChangeBoundsListener`.
*/
let ChangeBoundsTool = ChangeBoundsTool_1 = class ChangeBoundsTool extends base_tools_1.BaseEditTool {
constructor() {
super(...arguments);
this.movementOptions = { allElementsNeedToBeValid: true };
}
get id() {
return ChangeBoundsTool_1.ID;
}
enable() {
// install feedback move mouse listener for client-side move updates
const feedbackMoveMouseListener = this.createMoveMouseListener();
this.toDisposeOnDisable.push(this.mouseTool.registerListener(feedbackMoveMouseListener));
if (sprotty_1.Disposable.is(feedbackMoveMouseListener)) {
this.toDisposeOnDisable.push(feedbackMoveMouseListener);
}
if (selection_service_1.ISelectionListener.is(feedbackMoveMouseListener)) {
this.toDisposeOnDisable.push(this.selectionService.addListener(feedbackMoveMouseListener));
}
// install move key listener for client-side move updates
const createMoveKeyListener = this.createMoveKeyListener();
this.toDisposeOnDisable.push(this.keyTool.registerListener(createMoveKeyListener), (0, messages_1.repeatOnMessagesUpdated)(() => this.shortcutManager.register(ChangeBoundsTool_1.TOKEN, [
{
shortcuts: ['⬅ ⬆ ➡ ⬇'],
description: messages_1.messages.move.shortcut_move,
group: messages_1.messages.shortcut.group_move,
position: 0
}
])));
if (sprotty_1.Disposable.is(createMoveKeyListener)) {
this.toDisposeOnDisable.push(createMoveKeyListener);
}
// install change bounds listener for client-side resize updates and server-side updates
const changeBoundsListener = this.createChangeBoundsListener();
this.toDisposeOnDisable.push(this.mouseTool.registerListener(changeBoundsListener));
if (sprotty_1.Disposable.is(changeBoundsListener)) {
this.toDisposeOnDisable.push(changeBoundsListener);
}
if (selection_service_1.ISelectionListener.is(changeBoundsListener)) {
this.toDisposeOnDisable.push(this.selectionService.addListener(changeBoundsListener));
}
}
createChangeBoundsTracker() {
return this.changeBoundsManager.createTracker();
}
createMoveMouseListener() {
return new change_bounds_tool_move_feedback_1.FeedbackMoveMouseListener(this);
}
createMoveKeyListener() {
return new move_element_key_listener_1.MoveElementKeyListener(this.selectionService, this.changeBoundsManager, this.grid);
}
createChangeBoundsListener() {
return new ChangeBoundsListener(this);
}
};
exports.ChangeBoundsTool = ChangeBoundsTool;
ChangeBoundsTool.ID = 'glsp.change-bounds-tool';
ChangeBoundsTool.TOKEN = Symbol.for(ChangeBoundsTool_1.ID);
__decorate([
(0, inversify_1.inject)(selection_service_1.SelectionService),
__metadata("design:type", selection_service_1.SelectionService)
], ChangeBoundsTool.prototype, "selectionService", void 0);
__decorate([
(0, inversify_1.inject)(sprotty_1.EdgeRouterRegistry),
(0, inversify_1.optional)(),
__metadata("design:type", sprotty_1.EdgeRouterRegistry)
], ChangeBoundsTool.prototype, "edgeRouterRegistry", void 0);
__decorate([
(0, inversify_1.inject)(sprotty_1.TYPES.IMovementRestrictor),
(0, inversify_1.optional)(),
__metadata("design:type", Object)
], ChangeBoundsTool.prototype, "movementRestrictor", void 0);
__decorate([
(0, inversify_1.inject)(sprotty_1.TYPES.IChangeBoundsManager),
__metadata("design:type", Object)
], ChangeBoundsTool.prototype, "changeBoundsManager", void 0);
__decorate([
(0, inversify_1.inject)(sprotty_1.TYPES.IMovementOptions),
(0, inversify_1.optional)(),
__metadata("design:type", Object)
], ChangeBoundsTool.prototype, "movementOptions", void 0);
__decorate([
(0, inversify_1.inject)(sprotty_1.TYPES.Grid),
(0, inversify_1.optional)(),
__metadata("design:type", Object)
], ChangeBoundsTool.prototype, "grid", void 0);
__decorate([
(0, inversify_1.inject)(sprotty_1.TYPES.IShortcutManager),
__metadata("design:type", Object)
], ChangeBoundsTool.prototype, "shortcutManager", void 0);
exports.ChangeBoundsTool = ChangeBoundsTool = ChangeBoundsTool_1 = __decorate([
(0, inversify_1.injectable)()
], ChangeBoundsTool);
class ChangeBoundsListener extends drag_aware_mouse_listener_1.DragAwareMouseListener {
constructor(tool) {
super();
this.tool = tool;
this.tracker = tool.createChangeBoundsTracker();
this.handleFeedback = tool.createFeedbackEmitter();
this.resizeFeedback = tool.createFeedbackEmitter();
}
mouseDown(target, event) {
super.mouseDown(target, event);
// If another button than the left mouse button was clicked or we are
// still on the root element we don't need to execute the tool behavior
if (event.button !== 0 || target instanceof sprotty_1.GModelRoot) {
return [];
}
// check if we have a resize handle (only single-selection)
this.updateResizeElement(target, event);
return [];
}
updateResizeElement(target, event) {
var _a, _b;
this.activeResizeHandle = target instanceof model_1.GResizeHandle ? target : undefined;
this.activeResizeElement = (_b = (_a = this.activeResizeHandle) === null || _a === void 0 ? void 0 : _a.parent) !== null && _b !== void 0 ? _b : this.findResizeElement(target);
if (this.activeResizeElement) {
if (event) {
this.tracker.startTracking();
}
this.initialBounds = {
newSize: this.activeResizeElement.bounds,
newPosition: this.activeResizeElement.bounds,
elementId: this.activeResizeElement.id
};
// we trigger the local bounds calculation once to get the correct layout information for reszing
// for any sub-sequent calls the layout information will be updated automatically
this.tool
.createFeedbackEmitter()
.add(local_bounds_1.LocalRequestBoundsAction.create(target.root, [this.activeResizeElement.id]))
.submit()
.dispose();
this.handleFeedback.add(change_bounds_tool_feedback_1.ShowChangeBoundsToolResizeFeedbackAction.create({ elementId: this.activeResizeElement.id }), change_bounds_tool_feedback_1.HideChangeBoundsToolResizeFeedbackAction.create());
this.handleFeedback.submit();
return true;
}
else {
this.disposeResize();
return false;
}
}
findResizeElement(target) {
// check if we have a selected, moveable element (multi-selection allowed)
// but only allow one element to have the element resize handles
return (0, sprotty_1.findParentByFeature)(target, model_1.isResizable);
}
draggingMouseMove(target, event) {
// rely on the FeedbackMoveMouseListener to update the element bounds of selected elements
// consider resize handles ourselves
if (this.activeResizeHandle && this.tracker.isTracking()) {
const resize = this.tracker.resizeElements(this.activeResizeHandle, { snap: event, symmetric: event, restrict: event });
const resizeAction = this.resizeBoundsAction(resize);
if (resizeAction.bounds.length > 0) {
this.resizeFeedback.add(resizeAction, () => this.resetBounds());
this.tracker.updateTrackingPosition(resize.handleMove.moveVector);
this.addResizeFeedback(resize, target, event);
this.resizeFeedback.submit();
}
}
return super.draggingMouseMove(target, event);
}
resizeBoundsAction(resize) {
// we do not want to resize elements beyond their valid size, not even for feedback, as the next layout cycle usually corrects this
const elementResizes = resize.elementResizes.filter(elementResize => elementResize.valid.size);
return set_bounds_feedback_command_1.SetBoundsFeedbackAction.create(elementResizes.map(elementResize => this.toElementAndBounds(elementResize)));
}
toElementAndBounds(elementResize) {
return {
elementId: elementResize.element.id,
newSize: elementResize.toBounds,
newPosition: elementResize.toBounds
};
}
addResizeFeedback(resize, target, event) {
this.tool.changeBoundsManager.addResizeFeedback(this.resizeFeedback, resize, target, event);
}
resetBounds() {
// reset the bounds to the initial bounds and ensure that we do not show helper line feedback anymore (MoveFinishedEventAction)
return this.initialBounds
? [set_bounds_feedback_command_1.SetBoundsFeedbackAction.create([this.initialBounds]), change_bounds_tool_feedback_1.MoveFinishedEventAction.create()]
: [change_bounds_tool_feedback_1.MoveFinishedEventAction.create()];
}
draggingMouseUp(target, event) {
const actions = [];
if (this.activeResizeHandle) {
actions.push(...this.handleResizeOnServer(this.activeResizeHandle));
}
else {
// since the move feedback is handled by another class we just see whether there is something to move
actions.push(...this.handleMoveOnServer(target));
}
this.disposeResize({ keepHandles: true });
return actions;
}
nonDraggingMouseUp(element, event) {
this.disposeResize({ keepHandles: true });
return super.nonDraggingMouseUp(element, event);
}
handleMoveOnServer(target) {
const operations = [];
const elementToMove = this.getElementsToMove(target);
operations.push(...this.handleMoveElementsOnServer(elementToMove));
operations.push(...this.handleMoveRoutingPointsOnServer(elementToMove));
return operations.length > 0 ? [sprotty_1.CompoundOperation.create(operations)] : [];
}
getElementsToMove(target) {
const selectedElements = (0, gmodel_util_1.getMatchingElements)(target.index, gmodel_util_1.isNonRoutableSelectedMovableBoundsAware);
const selectionSet = new Set(selectedElements);
const elementsToMove = selectedElements.filter(element => this.isValidMove(element, selectionSet));
if (this.tool.movementOptions.allElementsNeedToBeValid && elementsToMove.length !== selectionSet.size) {
return [];
}
return elementsToMove;
}
handleMoveElementsOnServer(elementsToMove) {
const newBounds = elementsToMove.map(gmodel_util_1.toElementAndBounds);
return newBounds.length > 0 ? [sprotty_1.ChangeBoundsOperation.create(newBounds)] : [];
}
isValidMove(element, selectedElements = new Set()) {
return this.tool.changeBoundsManager.hasValidPosition(element) && !this.isChildOfSelected(selectedElements, element);
}
isChildOfSelected(selectedElements, element) {
if (selectedElements.size === 0) {
return false;
}
while (element instanceof sprotty_1.GChildElement) {
element = element.parent;
if (selectedElements.has(element)) {
return true;
}
}
return false;
}
handleMoveRoutingPointsOnServer(elementsToMove) {
const newRoutingPoints = [];
const routerRegistry = this.tool.edgeRouterRegistry;
if (routerRegistry) {
// If client routing is enabled -> delegate routing points of connected edges to server
elementsToMove.forEach(element => {
if (element instanceof sprotty_1.GConnectableElement) {
element.incomingEdges
.map(connectable => (0, gmodel_util_1.calcElementAndRoutingPoints)(connectable, routerRegistry))
.forEach(ear => newRoutingPoints.push(ear));
element.outgoingEdges
.map(connectable => (0, gmodel_util_1.calcElementAndRoutingPoints)(connectable, routerRegistry))
.forEach(ear => newRoutingPoints.push(ear));
}
});
}
return newRoutingPoints.length > 0 ? [sprotty_1.ChangeRoutingPointsOperation.create(newRoutingPoints)] : [];
}
handleResizeOnServer(activeResizeHandle) {
if (this.initialBounds && this.isValidResize(activeResizeHandle.parent)) {
const elementAndBounds = (0, gmodel_util_1.toElementAndBounds)(activeResizeHandle.parent);
if (!this.initialBounds.newPosition || !elementAndBounds.newPosition) {
return [];
}
if (!sprotty_1.Point.equals(this.initialBounds.newPosition, elementAndBounds.newPosition) ||
!sprotty_1.Dimension.equals(this.initialBounds.newSize, elementAndBounds.newSize)) {
// UX: we do not want the element positions to be reset to their start as they will be moved to their start and
// only afterwards moved by the move action again, leading to a ping-pong movement.
// We therefore clear our element map so that they cannot be reset.
this.initialBounds = undefined;
return [sprotty_1.ChangeBoundsOperation.create([elementAndBounds])];
}
}
return [];
}
isValidResize(element) {
return this.tool.changeBoundsManager.isValid(element);
}
selectionChanged(root, selectedElements) {
if (this.activeResizeElement && selectedElements.includes(this.activeResizeElement.id)) {
// our active element is still selected, nothing to do
return;
}
// try to find some other selected element and mark that active
for (const elementId of selectedElements.reverse()) {
const element = root.index.getById(elementId);
if (element && this.updateResizeElement(element)) {
return;
}
}
this.dispose();
}
isActiveResizeElement(element) {
return element !== undefined && this.activeResizeElement !== undefined && element.id === this.activeResizeElement.id;
}
disposeResize(opts = { keepHandles: false }) {
if (!opts.keepHandles) {
this.handleFeedback.dispose();
}
this.resizeFeedback.dispose();
this.tracker.dispose();
this.activeResizeElement = undefined;
this.activeResizeHandle = undefined;
this.initialBounds = undefined;
}
dispose() {
this.disposeResize();
super.dispose();
}
}
exports.ChangeBoundsListener = ChangeBoundsListener;
ChangeBoundsListener.CSS_CLASS_ACTIVE = change_bounds_manager_1.CSS_ACTIVE_HANDLE;
//# sourceMappingURL=change-bounds-tool.js.map