@finos/legend-application-pure-ide
Version:
Legend Pure IDE application core
269 lines (250 loc) • 8.47 kB
text/typescript
/**
* Copyright (c) 2020-present, Goldman Sachs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { CommandRegistrar } from '@finos/legend-application';
import {
type ClassView,
type Diagram,
type Point,
} from '@finos/legend-extension-dsl-diagram/graph';
import {
extractElementNameFromPath,
type PureModel,
} from '@finos/legend-graph';
import {
type GeneratorFn,
generateEnumerableNameFromToken,
guaranteeNonNullable,
} from '@finos/legend-shared';
import {
action,
computed,
flow,
flowResult,
makeObservable,
observable,
} from 'mobx';
import { deserialize } from 'serializr';
import {
type DiagramInfo,
type DiagramClassMetadata,
DiagramClassInfo,
addClassToGraph,
buildGraphFromDiagramInfo,
} from '../server/models/DiagramInfo.js';
import { FileCoordinate, trimPathLeadingSlash } from '../server/models/File.js';
import type { PureIDEStore } from './PureIDEStore.js';
import { PureIDETabState } from './PureIDETabManagerState.js';
import { LEGEND_PURE_IDE_DIAGRAM_EDITOR_COMMAND_KEY } from '../__lib__/LegendPureIDECommand.js';
import type { TabState } from '@finos/legend-lego/application';
import {
DIAGRAM_INTERACTION_MODE,
type DiagramRenderer,
} from '@finos/legend-extension-dsl-diagram/application';
export class DiagramEditorState
extends PureIDETabState
implements CommandRegistrar
{
diagramInfo: DiagramInfo;
_renderer?: DiagramRenderer | undefined;
diagram: Diagram;
diagramClasses: Map<string, DiagramClassMetadata>;
graph: PureModel;
diagramPath: string;
filePath: string;
fileLine: number;
fileColumn: number;
constructor(
ideStore: PureIDEStore,
diagramInfo: DiagramInfo,
diagramPath: string,
filePath: string,
fileLine: number,
fileColumn: number,
) {
super(ideStore);
makeObservable(this, {
_renderer: observable,
diagram: observable,
diagramInfo: observable,
diagramName: computed,
diagramCursorClass: computed,
addClassView: flow,
rebuild: action,
setRenderer: action,
});
this.diagramPath = diagramPath;
this.filePath = filePath;
this.fileLine = fileLine;
this.fileColumn = fileColumn;
this.diagramInfo = diagramInfo;
const [diagram, graph, diagramClasses] =
buildGraphFromDiagramInfo(diagramInfo);
this.diagram = diagram;
this.graph = graph;
this.diagramClasses = diagramClasses;
}
get label(): string {
return trimPathLeadingSlash(this.diagramPath);
}
override get description(): string | undefined {
return `Diagram: ${trimPathLeadingSlash(this.diagramPath)}`;
}
get diagramName(): string {
return extractElementNameFromPath(this.diagramPath);
}
override match(tab: TabState): boolean {
return (
tab instanceof DiagramEditorState && this.diagramPath === tab.diagramPath
);
}
get renderer(): DiagramRenderer {
return guaranteeNonNullable(
this._renderer,
`Diagram renderer must be initialized (this is likely caused by calling this method at the wrong place)`,
);
}
get isDiagramRendererInitialized(): boolean {
return Boolean(this._renderer);
}
// NOTE: we have tried to use React to control the cursor and
// could not overcome the jank/lag problem, so we settle with CSS-based approach
// See https://css-tricks.com/using-css-cursors/
// See https://developer.mozilla.org/en-US/docs/Web/CSS/cursor
get diagramCursorClass(): string {
if (!this.isDiagramRendererInitialized) {
return '';
}
if (this.renderer.middleClick || this.renderer.rightClick) {
return 'diagram-editor__cursor--grabbing';
}
switch (this.renderer.interactionMode) {
case DIAGRAM_INTERACTION_MODE.PAN: {
return this.renderer.leftClick
? 'diagram-editor__cursor--grabbing'
: 'diagram-editor__cursor--grab';
}
case DIAGRAM_INTERACTION_MODE.ZOOM_IN: {
return 'diagram-editor__cursor--zoom-in';
}
case DIAGRAM_INTERACTION_MODE.ZOOM_OUT: {
return 'diagram-editor__cursor--zoom-out';
}
case DIAGRAM_INTERACTION_MODE.LAYOUT: {
if (this.renderer.selectionStart) {
return 'diagram-editor__cursor--crosshair';
} else if (
this.renderer.mouseOverClassCorner ||
this.renderer.selectedClassCorner
) {
return 'diagram-editor__cursor--resize';
} else if (this.renderer.mouseOverClassView) {
return 'diagram-editor__cursor--pointer';
}
return '';
}
default:
return '';
}
}
rebuild(value: DiagramInfo): void {
this.diagramInfo = value;
const [diagram, graph, diagramClasses] = buildGraphFromDiagramInfo(value);
this.diagram = diagram;
this.graph = graph;
this.diagramClasses = diagramClasses;
this.fileLine = value.diagram.sourceInformation.line;
this.fileColumn = value.diagram.sourceInformation.column;
}
setupRenderer(): void {
this.renderer.onClassViewDoubleClick = (classView: ClassView): void => {
const sourceInformation = this.diagramClasses.get(
classView.class.value.path,
)?.sourceInformation;
if (sourceInformation) {
const coordinate = new FileCoordinate(
sourceInformation.sourceId,
sourceInformation.startLine,
sourceInformation.startColumn,
);
flowResult(this.ideStore.executeNavigation(coordinate)).catch(
this.ideStore.applicationStore.alertUnhandledError,
);
}
};
}
setRenderer(val: DiagramRenderer): void {
this._renderer = val;
}
*addClassView(path: string, position: Point | undefined): GeneratorFn<void> {
const diagramClassInfo = deserialize(
DiagramClassInfo,
yield this.ideStore.client.getDiagramClassInfo(path),
);
const _class = addClassToGraph(
diagramClassInfo,
this.graph,
this.diagramClasses,
);
const classView = this.renderer.addClassView(_class, position);
// NOTE: The auto-generated ID by diagram renderer will cause a parser error in Pure
// so we need to rewrite it accordingly
if (classView) {
classView.id = generateEnumerableNameFromToken(
this.diagram.classViews.map((cv) => cv.id),
'cview',
);
}
}
registerCommands(): void {
const DEFAULT_TRIGGER = (): boolean =>
// make sure the current active editor is this diagram editor
this.ideStore.tabManagerState.currentTab === this &&
// make sure the renderer is initialized
this.isDiagramRendererInitialized;
this.ideStore.applicationStore.commandService.registerCommand({
key: LEGEND_PURE_IDE_DIAGRAM_EDITOR_COMMAND_KEY.RECENTER,
trigger: DEFAULT_TRIGGER,
action: () => this.renderer.recenter(),
});
this.ideStore.applicationStore.commandService.registerCommand({
key: LEGEND_PURE_IDE_DIAGRAM_EDITOR_COMMAND_KEY.USE_ZOOM_TOOL,
trigger: DEFAULT_TRIGGER,
action: () => this.renderer.switchToZoomMode(),
});
this.ideStore.applicationStore.commandService.registerCommand({
key: LEGEND_PURE_IDE_DIAGRAM_EDITOR_COMMAND_KEY.USE_VIEW_TOOL,
trigger: DEFAULT_TRIGGER,
action: () => this.renderer.switchToViewMode(),
});
this.ideStore.applicationStore.commandService.registerCommand({
key: LEGEND_PURE_IDE_DIAGRAM_EDITOR_COMMAND_KEY.USE_PAN_TOOL,
trigger: DEFAULT_TRIGGER,
action: () => this.renderer.switchToPanMode(),
});
}
deregisterCommands(): void {
[
LEGEND_PURE_IDE_DIAGRAM_EDITOR_COMMAND_KEY.RECENTER,
LEGEND_PURE_IDE_DIAGRAM_EDITOR_COMMAND_KEY.USE_ZOOM_TOOL,
LEGEND_PURE_IDE_DIAGRAM_EDITOR_COMMAND_KEY.USE_VIEW_TOOL,
LEGEND_PURE_IDE_DIAGRAM_EDITOR_COMMAND_KEY.USE_PAN_TOOL,
].forEach((commandKey) =>
this.ideStore.applicationStore.commandService.deregisterCommand(
commandKey,
),
);
}
}