UNPKG

@finos/legend-application-pure-ide

Version:
269 lines (250 loc) 8.47 kB
/** * 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, ), ); } }