UNPKG

@eclipse-glsp/client

Version:

A sprotty-based client for GLSP

361 lines 18.5 kB
"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