@eclipse-glsp/client
Version:
A sprotty-based client for GLSP
193 lines (168 loc) • 6.84 kB
text/typescript
/********************************************************************************
* Copyright (c) 2023-2025 Business Informatics Group (TU Wien) 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
********************************************************************************/
import {
Action,
Bounds,
DOMHelper,
findParentByFeature,
GChildElement,
GModelElement,
GModelRoot,
IActionDispatcher,
IActionHandler,
ICommand,
isViewport,
MoveViewportAction,
Point,
SetViewportAction,
TYPES,
Viewport
} from '@eclipse-glsp/sprotty';
import { inject, injectable } from 'inversify';
import { EditorContextService } from '../../base/editor-context-service';
import { FocusTracker } from '../../base/focus/focus-tracker';
import { IDiagramStartup } from '../../base/model/diagram-loader';
import { EnableDefaultToolsAction } from '../../base/tool-manager/tool';
import { getElements, isSelectableAndBoundsAware, SelectableBoundsAware } from '../../utils/gmodel-util';
import { FocusDomAction } from '../accessibility/actions';
import { ZoomAction } from './zoom-viewport-action';
/**
* Focuses the graph on different actions.
*/
export class RestoreViewportHandler implements IActionHandler, IDiagramStartup {
protected domHelper: DOMHelper;
protected focusTracker: FocusTracker;
protected editorContext: EditorContextService;
handle(action: Action): void | Action {
if (EnableDefaultToolsAction.is(action) || (FocusDomAction.is(action) && action.id === 'graph')) {
this.focusGraph();
}
}
get graphSelector(): string {
const rootId = CSS.escape(this.domHelper.createUniqueDOMElementId(this.editorContext.modelRoot));
return `#${rootId}`;
}
async postRequestModel(): Promise<void> {
await this.waitForElement(this.graphSelector);
this.focusGraph();
}
protected focusGraph(): void {
if (this.focusTracker.hasFocus) {
const container = this.focusTracker.diagramElement?.querySelector<HTMLElement>(this.graphSelector);
container?.focus();
}
}
protected waitForElement(selector: string): Promise<Element | null> {
return new Promise(resolve => {
if (document.querySelector(selector)) {
return resolve(document.querySelector(selector));
}
const observer = new MutationObserver(mutations => {
if (document.querySelector(selector)) {
observer.disconnect();
resolve(document.querySelector(selector));
}
});
observer.observe(document.body, {
childList: true,
subtree: true
});
});
}
}
/**
* Handles moving the viewport.
*/
export class MoveViewportHandler implements IActionHandler {
protected readonly editorContextService: EditorContextService;
handle(action: MoveViewportAction): void | Action | ICommand {
return this.handleMoveViewport(action);
}
protected handleMoveViewport(action: MoveViewportAction): Action | undefined {
const viewport = findParentByFeature(this.editorContextService.modelRoot, isViewport);
if (!viewport) {
return;
}
const newViewport: Viewport = {
scroll: {
x: viewport.scroll.x + action.moveX,
y: viewport.scroll.y + action.moveY
},
zoom: viewport.zoom
};
return SetViewportAction.create(viewport.id, newViewport, { animate: false });
}
}
/*
* Handles zooming in and out of the viewport.
*/
export class ZoomHandler implements IActionHandler {
protected readonly editorContextService: EditorContextService;
protected readonly actionDispatcher: IActionDispatcher;
handle(action: ZoomAction): Action | void {
if (action.elementIds) {
return this.handleZoomElement(action.elementIds, action.zoomFactor);
} else {
return this.handleZoomViewport(action.zoomFactor);
}
}
protected handleZoomViewport(zoomFactor: number): Action | undefined {
const viewport = this.editorContextService.viewport;
if (!viewport) {
return;
}
return SetViewportAction.create(viewport.id, { scroll: viewport.scroll, zoom: viewport.zoom * zoomFactor }, { animate: false });
}
protected handleZoomElement(elementIds: string[], zoomFactor: number): Action | undefined {
const viewport = this.editorContextService.viewport;
if (!viewport) {
return;
}
const elements = getElements(viewport.index, elementIds, isSelectableAndBoundsAware);
const center = this.getCenter(viewport, elements);
const newZoom = viewport.zoom * zoomFactor;
const newViewport = {
scroll: {
x: center.x - (0.5 * viewport.canvasBounds.width) / newZoom,
y: center.y - (0.5 * viewport.canvasBounds.height) / newZoom
},
zoom: newZoom
};
return SetViewportAction.create(viewport.id, newViewport, { animate: false });
}
protected getCenter(viewport: GModelRoot & Viewport, selectedElements: SelectableBoundsAware[]): Point {
// Get bounds of elements based on the viewport
const allBounds = selectedElements.map(e => this.boundsInViewport(viewport, e, e.bounds));
const mergedBounds = allBounds.reduce((b0, b1) => Bounds.combine(b0, b1));
return Bounds.center(mergedBounds);
}
protected boundsInViewport(viewport: GModelRoot & Viewport, element: GModelElement, bounds: Bounds): Bounds {
if (element instanceof GChildElement && element.parent !== viewport) {
return this.boundsInViewport(viewport, element.parent, element.parent.localToParent(bounds) as Bounds);
} else {
return bounds;
}
}
}