sprotty-theia
Version:
Glue code for Sprotty diagrams in a Theia IDE
270 lines (235 loc) • 9.78 kB
text/typescript
/********************************************************************************
* Copyright (c) 2017-2018 TypeFox 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 {
InitializeCanvasBoundsAction, ServerStatusAction, IActionDispatcher, ModelSource,
TYPES, ViewerOptions, DiagramServerProxy
} from 'sprotty';
import { CenterAction, RequestModelAction } from 'sprotty-protocol';
import { Widget } from "@phosphor/widgets";
import { Message } from "@phosphor/messaging/lib";
import { BaseWidget, codiconArray } from '@theia/core/lib/browser/widgets/widget';
import { StatefulWidget, Navigatable } from '@theia/core/lib/browser';
import URI from '@theia/core/lib/common/uri';
import { TheiaSprottyConnector } from '../sprotty/theia-sprotty-connector';
import { Container } from 'inversify';
import { TheiaDiagramServer } from '../sprotty/theia-diagram-server';
export interface DiagramWidgetOptions {
uri: string
diagramType: string
label: string
iconClass: string
}
export type DiagramWidgetFactory =
(options: DiagramWidgetOptions, widgetId: string, diContainer: Container, connector?: TheiaSprottyConnector) => DiagramWidget;
export const DiagramWidgetFactory = Symbol('DiagramWidgetFactory');
export namespace DiagramWidgetOptions {
export function is(options: any): options is DiagramWidgetOptions {
return options.diagramType
&& options.uri
&& options.label
&& options.iconClass;
}
}
/**
* The DiagramWidget is the container for Sprotty diagrams.
*/
export class DiagramWidget extends BaseWidget implements StatefulWidget, Navigatable {
private diagramContainer?: HTMLDivElement;
private statusIconDiv?: HTMLDivElement;
private statusMessageDiv?: HTMLDivElement;
protected _actionDispatcher: IActionDispatcher;
protected _modelSource: ModelSource;
get uri(): URI {
return new URI(this.options.uri);
}
get actionDispatcher(): IActionDispatcher {
return this.diContainer.get(TYPES.IActionDispatcher);
}
get viewerOptions(): ViewerOptions {
return this.diContainer.get(TYPES.ViewerOptions);
}
get modelSource(): ModelSource {
return this._modelSource;
}
get clientId(): string {
if (this._modelSource instanceof DiagramServerProxy)
return this._modelSource.clientId;
else
return this.widgetId;
}
get id(): string {
return this.widgetId;
}
constructor(protected options: DiagramWidgetOptions,
readonly widgetId: string,
readonly diContainer: Container,
readonly connector?: TheiaSprottyConnector) {
super();
this.title.closable = true;
this.title.label = options.label;
this.title.iconClass = options.iconClass;
}
protected onAfterAttach(msg: Message): void {
if (!this.diagramContainer) {
// Create the container and initialize its content upon first attachment
this.createContainer();
this.initializeSprotty();
}
super.onAfterAttach(msg);
}
protected createContainer(): void {
this.diagramContainer = document.createElement('div');
this.diagramContainer.id = this.viewerOptions.baseDiv;
this.node.appendChild(this.diagramContainer);
const hiddenContainer = document.createElement('div');
hiddenContainer.id = this.viewerOptions.hiddenDiv;
document.body.appendChild(hiddenContainer);
const statusDiv = document.createElement('div');
statusDiv.setAttribute('class', 'sprotty-status');
this.node.appendChild(statusDiv);
this.statusIconDiv = document.createElement('div');
statusDiv.appendChild(this.statusIconDiv);
this.statusMessageDiv = document.createElement('div');
this.statusMessageDiv.setAttribute('class', 'sprotty-status-message');
statusDiv.appendChild(this.statusMessageDiv);
}
protected initializeSprotty(): void {
const modelSource = this.diContainer.get<ModelSource>(TYPES.ModelSource);
this._modelSource = modelSource;
if (modelSource instanceof TheiaDiagramServer && this.connector)
this.connector.connect(modelSource);
this.disposed.connect(() => {
if (modelSource instanceof TheiaDiagramServer && this.connector)
this.connector.disconnect(modelSource);
});
this.requestModel();
}
protected async requestModel(): Promise<void> {
try {
const response = await this.actionDispatcher.request(RequestModelAction.create({
sourceUri: this.options.uri,
diagramType: this.options.diagramType
}));
await this.actionDispatcher.dispatch(response);
await this.initializeView();
} catch (err) {
const status = new ServerStatusAction();
status.message = err instanceof Error ? err.message : err.toString();
status.severity = 'FATAL';
this.setStatus(status);
}
}
protected initializeView(): Promise<void> {
return this.actionDispatcher.dispatch(CenterAction.create([], { animate: false }));
}
protected getBoundsInPage(element: Element) {
const bounds = element.getBoundingClientRect();
return {
x: bounds.left,
y: bounds.top,
width: bounds.width,
height: bounds.height
};
}
protected onResize(msg: Widget.ResizeMessage): void {
super.onResize(msg);
const newBounds = this.getBoundsInPage(this.node as Element);
this.actionDispatcher.dispatch(InitializeCanvasBoundsAction.create(newBounds));
}
protected onActivateRequest(msg: Message): void {
super.onActivateRequest(msg);
const svgElement = this.node.querySelector(`#${this.viewerOptions.baseDiv} svg`) as HTMLElement;
if (svgElement !== null) {
svgElement.focus();
} else {
const tabindex = this.node.getAttribute('tabindex');
if (tabindex === null)
this.node.setAttribute('tabindex', '-1');
this.node.focus();
}
}
/**
* We cannot activate the widget before the SVG element is there, as it takes the focus.
* This should happen within two animation frames, as the action dispatcher issues
* a SetModelCommand in the constructor. OTOH, shell.activateWidget() is synchronous. So
* after creating the widget and before activating it, we use this method to wait for the
* SVG to be appended to the DOM.
*/
async getSvgElement(): Promise<HTMLElement | undefined> {
return new Promise<HTMLElement | undefined>((resolve) => {
let frames = 0;
const waitForSvg = () => {
requestAnimationFrame(() => {
const svgElement = this.node.querySelector(`#${this.viewerOptions.baseDiv} svg`) as HTMLElement;
if (svgElement !== null)
resolve(svgElement);
else if (++frames < 5)
waitForSvg();
else
resolve(undefined);
});
};
waitForSvg();
});
}
setStatus(status: ServerStatusAction): void {
if (this.statusMessageDiv) {
this.statusMessageDiv.textContent = status.message;
this.removeClasses(this.statusMessageDiv, 1);
this.statusMessageDiv.classList.add(status.severity.toLowerCase());
}
if (this.statusIconDiv) {
this.removeClasses(this.statusIconDiv, 0);
const classes = this.statusIconDiv.classList;
classes.add(status.severity.toLowerCase());
switch (status.severity) {
case 'FATAL':
classes.add(...codiconArray('error'));
break;
case 'ERROR':
classes.add(...codiconArray('warning'));
break;
case 'WARNING':
classes.add(...codiconArray('warning'));
break;
case 'INFO':
classes.add(...codiconArray('info'));
break;
}
}
}
protected removeClasses(element: Element, keep: number) {
const classes = element.classList;
while (classes.length > keep) {
const item = classes.item(classes.length - 1);
if (item)
classes.remove(item);
}
}
storeState(): object {
return this.options;
}
restoreState(oldState: object): void {
if (DiagramWidgetOptions.is(oldState))
this.options = oldState;
}
getResourceUri(): URI | undefined {
return this.uri;
}
createMoveToUri(resourceUri: URI): URI | undefined {
return this.uri.withPath(resourceUri.path);
}
}