Skip to content

Commit 757ad6c

Browse files
committed
GLSP-1531: Introduce viewport change event
Fixes eclipse-glsp/glsp/issues/1531
1 parent 2f70b50 commit 757ad6c

File tree

2 files changed

+62
-5
lines changed

2 files changed

+62
-5
lines changed

packages/client/src/base/command-stack.ts

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/********************************************************************************
2-
* Copyright (c) 2019-2024 EclipseSource and others.
2+
* Copyright (c) 2019-2025 EclipseSource and others.
33
*
44
* This program and the accompanying materials are made available under the
55
* terms of the Eclipse Public License v. 2.0 which is available at
@@ -14,6 +14,7 @@
1414
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
1515
********************************************************************************/
1616
import {
17+
BoundsAwareViewportCommand,
1718
CommandExecutionContext,
1819
CommandStack,
1920
Disposable,
@@ -23,7 +24,11 @@ import {
2324
ICommand,
2425
LazyInjector,
2526
SetModelCommand,
26-
UpdateModelCommand
27+
SetViewportCommand,
28+
UpdateModelCommand,
29+
Viewport,
30+
almostEquals,
31+
isViewport
2732
} from '@eclipse-glsp/sprotty';
2833
import { inject, injectable, preDestroy } from 'inversify';
2934
import { EditorContextService } from './editor-context-service';
@@ -33,6 +38,7 @@ export class GLSPCommandStack extends CommandStack implements Disposable {
3338
@inject(LazyInjector)
3439
protected lazyInjector: LazyInjector;
3540
protected toDispose = new DisposableCollection();
41+
protected currentViewport?: Readonly<Viewport>;
3642

3743
@preDestroy()
3844
dispose(): void {
@@ -95,13 +101,40 @@ export class GLSPCommandStack extends CommandStack implements Disposable {
95101
}
96102
override async execute(command: ICommand): Promise<GModelRoot> {
97103
const result = await super.execute(command);
98-
if (command instanceof SetModelCommand || command instanceof UpdateModelCommand) {
104+
if (this.isModelChangeCommand(command)) {
99105
this.notifyListeners(result);
106+
} else if (this.isViewportChangeCommand(command)) {
107+
this.notifyListeners(result, true);
100108
}
101109
return result;
102110
}
103111

104-
protected notifyListeners(root: Readonly<GModelRoot>): void {
105-
this.editorContext.notifyModelRootChanged(root, this);
112+
protected isModelChangeCommand(command: ICommand): boolean {
113+
return command instanceof SetModelCommand || command instanceof UpdateModelCommand;
114+
}
115+
116+
protected isViewportChangeCommand(command: ICommand): boolean {
117+
return command instanceof SetViewportCommand || command instanceof BoundsAwareViewportCommand;
118+
}
119+
120+
protected notifyListeners(root: Readonly<GModelRoot>, isViewportChange = false): void {
121+
if (!isViewportChange) {
122+
this.currentViewport = isViewport(root) ? { scroll: root.scroll, zoom: root.zoom } : undefined;
123+
this.editorContext.notifyModelRootChanged(root, this);
124+
} else if (isViewport(root) && this.hasViewportChanged(root)) {
125+
this.currentViewport = { scroll: root.scroll, zoom: root.zoom };
126+
this.editorContext.notifyViewportChanged(root, this);
127+
}
128+
}
129+
130+
protected hasViewportChanged(newViewport: Readonly<Viewport>): boolean {
131+
if (!this.currentViewport) {
132+
return true;
133+
}
134+
return !(
135+
almostEquals(newViewport.zoom, this.currentViewport.zoom) &&
136+
almostEquals(newViewport.scroll.x, this.currentViewport.scroll.x) &&
137+
almostEquals(newViewport.scroll.y, this.currentViewport.scroll.y)
138+
);
106139
}
107140
}

packages/client/src/base/editor-context-service.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,15 @@ export class EditorContextService implements IActionHandler, Disposable, IDiagra
143143
return this.selectionService.onSelectionChanged;
144144
}
145145

146+
protected onViewportChangedEmitter = new Emitter<Readonly<Viewport>>();
147+
/**
148+
* Event that is fired when the viewport of the diagram changes i.e. after the `CommandStack` has processed a viewport update.
149+
* By default, this event is only fired if the viewport was changed via a `SetViewportCommand` or `BoundsAwareViewportCommand`
150+
*/
151+
get onViewportChanged(): Event<Readonly<Viewport>> {
152+
return this.onViewportChangedEmitter.event;
153+
}
154+
146155
protected toDispose = new DisposableCollection();
147156

148157
@postConstruct()
@@ -196,6 +205,21 @@ export class EditorContextService implements IActionHandler, Disposable, IDiagra
196205
this.onModelRootChangedEmitter.fire(root);
197206
}
198207

208+
/**
209+
* Notifies the service about a viewport change. This method should not be called
210+
* directly. It is called by the `CommandStack` after a viewport modifying command has been processed.
211+
* @throws an error if the notifier is not a `CommandStack`
212+
* @param viewport the new viewport
213+
* @param notifier the object that triggered the viewport change
214+
*/
215+
notifyViewportChanged(viewport: Readonly<Viewport>, notifier: AnyObject): void {
216+
if (!(notifier instanceof CommandStack)) {
217+
throw new Error('Invalid viewport change notification. Notifier is not an instance of `CommandStack`.');
218+
}
219+
220+
this.onViewportChangedEmitter.fire(viewport);
221+
}
222+
199223
handle(action: Action): void {
200224
if (SetEditModeAction.is(action)) {
201225
this.handleSetEditModeAction(action);

0 commit comments

Comments
 (0)