@peter.prib/renderengine 0.2.3 → 0.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/REFERENCE.md +2227 -2205
- package/dist/renderengine-workspace.umd.js +4073 -3451
- package/dist/renderengine.umd.js +4345 -3919
- package/package.json +1 -1
- package/src/charting/FormDialog.js +1 -1
- package/src/charting/ModelComparisonDialog.js +122 -0
- package/src/charting/diagramModel/ComparedKind.js +67 -0
- package/src/charting/diagramModel/DiagramView.js +11 -0
- package/src/charting/diagramModel/MergeModelCommand.js +37 -0
- package/src/charting/diagramModel/ModelComparison.js +100 -0
- package/src/charting/diagramModel/ViewSetModel.js +260 -243
- package/src/charting/diagramModel/ViewsCompared.js +39 -0
- package/src/charting/diagramModel/index.js +10 -6
- package/src/charting/diagrams/ArchimateModel.js +1170 -1155
- package/src/charting/diagrams/ArchimateViewsCompared.js +70 -0
- package/src/charting/diagrams/ConceptsCompared.js +76 -0
- package/src/charting/modelTree/ModelTreePanel.js +886 -914
- package/src/charting/modelTree/viewFolders.js +31 -0
- package/src/workspace/parts/ArchimateCanvasPart.js +345 -345
- package/src/workspace/parts/DiagramEditorPart.js +135 -135
- package/src/workspace/parts/EditorPart.js +26 -7
- package/src/workspace/parts/ModelsPart.js +58 -1
|
@@ -1,345 +1,345 @@
|
|
|
1
|
-
import { render } from '../dom.js';
|
|
2
|
-
import { EditorShell } from '../EditorShell.js';
|
|
3
|
-
import { EditorPart } from './EditorPart.js';
|
|
4
|
-
import { ArchimateModel } from '../../charting/diagrams/ArchimateModel.js';
|
|
5
|
-
import { ArchimateCanvasEditor, ArchimatePalette, typeTitle } from '../../charting/archimate/index.js';
|
|
6
|
-
import { ArchimateNotation } from '../../archimate.js';
|
|
7
|
-
import { MenuBar } from '../../charting/MenuBar.js';
|
|
8
|
-
import { ToolbarText } from '../../charting/toolbar/index.js';
|
|
9
|
-
import { ViewTabStrip } from '../../charting/ViewTabStrip.js';
|
|
10
|
-
import { FormDialog } from '../../charting/FormDialog.js';
|
|
11
|
-
import { CsvImportDialog } from '../../charting/archimate/CsvImportDialog.js';
|
|
12
|
-
import { pickFile } from '../../charting/toolbar/pickFile.js';
|
|
13
|
-
import { SettingsManager } from '../../charting/SettingsManager.js';
|
|
14
|
-
import { SettingsViewPropertiesDialog } from '../../charting/SettingsViewPropertiesDialog.js';
|
|
15
|
-
import { CanvasTemplateLibrary } from '../../charting/canvas/CanvasTemplateLibrary.js';
|
|
16
|
-
import { CanvasTemplateMenu } from '../../charting/canvas/CanvasTemplateMenu.js';
|
|
17
|
-
import { CanvasPalette } from '../../charting/canvas/CanvasPalette.js';
|
|
18
|
-
import { ARCHIMATE_FILE_FORMATS } from '../../charting/archimate/archimateFileFormats.js';
|
|
19
|
-
import { ModelTree } from '../../charting/modelTree/ModelTree.js';
|
|
20
|
-
import { buildModelTreePanel } from '../../charting/modelTree/ModelTreePanel.js';
|
|
21
|
-
import { buildAnalysisPanel } from '../../charting/modelTree/AnalysisPanel.js';
|
|
22
|
-
import { ArchimateSpecializationRegistry } from '../../archimateSpecializations.js';
|
|
23
|
-
import { HintPanel } from '../../charting/HintPanel.js';
|
|
24
|
-
import { elementMenuItems } from '../../charting/archimate/elementMenuItems.js';
|
|
25
|
-
import { editElementProperties } from '../../charting/archimate/elementProperties.js';
|
|
26
|
-
import { CustomViewpoints } from '../../charting/archimate/CustomViewpoints.js';
|
|
27
|
-
|
|
28
|
-
const SVG_NS = 'http://www.w3.org/2000/svg';
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* ArchimateCanvasPart — the ArchiMate modeller on the diagram editor's own
|
|
32
|
-
* canvas (phase 4 testbed): the model tree on the left, one tab per view and
|
|
33
|
-
* an ArchimateCanvasEditor over the current view, and a palette of element
|
|
34
|
-
* types grouped by layer on the right. Drop or click a palette type to add an
|
|
35
|
-
* element, or drag one from the tree to show it (with its relationships to
|
|
36
|
-
* what is shown); drag between two elements to add a relationship (its kind
|
|
37
|
-
* picked from those the rules allow); every edit is a step on that view's own
|
|
38
|
-
* undo history. The File menu exports and imports CSV, .archimate and the
|
|
39
|
-
* Exchange Format.
|
|
40
|
-
*
|
|
41
|
-
* Spec (JSON): `elements`, `relationships`, `views`, `name` (as ArchimateModel
|
|
42
|
-
* takes them), `archimateVersion` ('4' or '3.2'), `distinctShapes` (draw each
|
|
43
|
-
* element in its notation shape rather than a box), `viewpoint` (a viewpoint name) and
|
|
44
|
-
* `viewpointMode` ('fade' or 'hide': elements outside it), `analysis` (true adds the
|
|
45
|
-
* analysis of the selected element, under the palette).
|
|
46
|
-
*/
|
|
47
|
-
export class ArchimateCanvasPart extends EditorPart {
|
|
48
|
-
static type = 'archimate-canvas';
|
|
49
|
-
static viewNoun = 'view';
|
|
50
|
-
|
|
51
|
-
constructor(spec = {}) {
|
|
52
|
-
super(spec);
|
|
53
|
-
this.version = spec.archimateVersion ?? '4';
|
|
54
|
-
this.distinctShapes = !!spec.distinctShapes;
|
|
55
|
-
this.viewpoint = spec.viewpoint ?? 'All';
|
|
56
|
-
this.viewpointMode = spec.viewpointMode ?? 'fade';
|
|
57
|
-
this.showDerived = !!spec.showDerived;
|
|
58
|
-
this.settings = new SettingsManager();
|
|
59
|
-
// A settings change (Model settings…, a view's properties) redraws the canvas.
|
|
60
|
-
this._unsubscribeSettings = this.settings.onSettingsChange(() => { if (this.mounted) this.editor?.render(); });
|
|
61
|
-
// The user's specializations (or the defaults), as the main editor offers them; one already loaded is kept.
|
|
62
|
-
if (!ArchimateSpecializationRegistry.list().length) ArchimateSpecializationRegistry.loadFrom();
|
|
63
|
-
this._follow(this.model);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* Re-render on every change to `model`'s views (the editor is rebuilt with the canvas on each
|
|
68
|
-
* render), and redraw the tree after every edit (an element renamed, an undo).
|
|
69
|
-
* @private
|
|
70
|
-
*/
|
|
71
|
-
_follow(model) {
|
|
72
|
-
this._unsubscribe?.();
|
|
73
|
-
this._unsubscribeTree?.();
|
|
74
|
-
this._unsubscribe = model.onChange(() => { if (this.mounted) this.render(); });
|
|
75
|
-
this._unsubscribeTree = model.onEdit?.(() => this._treePanel?.refresh?.()) ?? null;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
fileFormats() {
|
|
79
|
-
return ARCHIMATE_FILE_FORMATS;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/** A new file is an Archi file (.archimate). */
|
|
83
|
-
saveFormat() {
|
|
84
|
-
return ARCHIMATE_FILE_FORMATS.find(f => f.featureName === 'Archimate');
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/** The file label in the toolbar (and a ModelsPart's tab) follows the saved state. */
|
|
88
|
-
onFileState() {
|
|
89
|
-
super.onFileState();
|
|
90
|
-
this.menuBar?.refresh?.();
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/** New view…, then New canvas… and New canvas from template. */
|
|
94
|
-
newItems() {
|
|
95
|
-
return [...super.newItems(), ...this.templateMenu.newCanvasItems()];
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/** File > Templates: the open canvas saved as a model template or into the library. */
|
|
99
|
-
ownFileItems() {
|
|
100
|
-
const saveAs = this.model.currentView ? this.templateMenu.saveAsTemplateItems(this.model.currentViewId) : [];
|
|
101
|
-
return EditorPart.menuGroup('Templates', saveAs);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
/** File > Import: an Archi canvas template, and a spreadsheet's rows (CSV with mapping). */
|
|
105
|
-
ownImportItems() {
|
|
106
|
-
const importTemplate = this.templateMenu.importItem();
|
|
107
|
-
return [{ separator: true }, ...(importTemplate ? [{ ...importTemplate, label: 'Archi template…' }] : []), { label: 'CSV with mapping…', action: () => this.importCsvMapped() }];
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
/** Import CSV with mapping…: a spreadsheet's rows into this model (CsvImportDialog), the new elements on the open view. */
|
|
111
|
-
async importCsvMapped() {
|
|
112
|
-
const file = await pickFile('.csv,.txt,.tsv', this.document);
|
|
113
|
-
if (!file) return null;
|
|
114
|
-
return new CsvImportDialog({ model: this.model, view: this.model.currentView, version: this.version, document: this.document, onDone: () => this.render() }).open(await file.text());
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
/** An imported model: follow it, with a fresh tree, and give it a view if it has none. */
|
|
118
|
-
modelReplaced(model) {
|
|
119
|
-
if (!model.views().length) model.createView();
|
|
120
|
-
this._follow(model);
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
/** The model tree over the current model (kept across renders, so its mode and selection stay). */
|
|
124
|
-
get modelTree() {
|
|
125
|
-
if (this.tree?.model !== this.model) {
|
|
126
|
-
this.tree = new ModelTree(this.model, { mode: 'byLayer' });
|
|
127
|
-
// A view chosen in the tree opens it; an element chosen there is selected on the canvas.
|
|
128
|
-
this.tree.onSelectionChange((data) => {
|
|
129
|
-
if (data.kind === 'view' && data.view.id !== this.model.currentViewId) this.model.switchTo(data.view.id);
|
|
130
|
-
if (data.kind === 'element') { this.selectElement(data.id); this._showAnalysis?.(data.id); }
|
|
131
|
-
});
|
|
132
|
-
}
|
|
133
|
-
return this.tree;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
createModel(spec) {
|
|
137
|
-
return new ArchimateModel({
|
|
138
|
-
name: spec.name ?? '',
|
|
139
|
-
elements: spec.elements ?? [],
|
|
140
|
-
relationships: spec.relationships ?? [],
|
|
141
|
-
views: spec.views?.length ? spec.views : [{ id: 'view-1', name: 'View 1', elementIds: [] }],
|
|
142
|
-
});
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
/** A view's properties: name, documentation and its settings (routing, grid, background, ...). */
|
|
146
|
-
showViewProperties(view) {
|
|
147
|
-
return new SettingsViewPropertiesDialog({ model: this.model, view, settingsManager: this.settings, document: this.document }).open();
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
/**
|
|
151
|
-
* Ask which relationship a new connection is, among the kinds the rules
|
|
152
|
-
* allow between the two elements. Resolves to the kind, or null if cancelled.
|
|
153
|
-
*/
|
|
154
|
-
chooseKind(kinds, from, to, { reason = 'connect' } = {}) {
|
|
155
|
-
const nesting = reason === 'nesting';
|
|
156
|
-
return new Promise((resolve) => {
|
|
157
|
-
new FormDialog({
|
|
158
|
-
title: nesting ? `${to.name} nested in ${from.name}` : `New relationship: ${from.name} → ${to.name}`,
|
|
159
|
-
description: nesting
|
|
160
|
-
? `Add the relationship the nesting stands for (${typeTitle(from.type ?? '?')} → ${typeTitle(to.type ?? '?')}), in the model; the nesting shows it, so no line is drawn.`
|
|
161
|
-
: `${typeTitle(from.type ?? '?')} → ${typeTitle(to.type ?? '?')}: ${kinds.length} allowed kind${kinds.length === 1 ? '' : 's'}`,
|
|
162
|
-
fields: [{
|
|
163
|
-
key: 'kind', label: 'Kind', type: 'select', value: kinds[0],
|
|
164
|
-
choices: kinds.map(k => [k, ArchimateNotation.relationshipType(k)?.description ? `${typeTitle(k)}: ${ArchimateNotation.relationshipType(k).description}` : typeTitle(k)]),
|
|
165
|
-
}],
|
|
166
|
-
submitLabel: 'Add',
|
|
167
|
-
...(nesting && { cancelLabel: 'No relationship' }),
|
|
168
|
-
idPrefix: 'archimate-kind-',
|
|
169
|
-
document: this.document,
|
|
170
|
-
onApply: ({ kind }) => resolve(kind),
|
|
171
|
-
onClose: (applied) => { if (!applied) resolve(null); },
|
|
172
|
-
}).open();
|
|
173
|
-
});
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
render() {
|
|
177
|
-
const model = this.model;
|
|
178
|
-
const view = model.currentView;
|
|
179
|
-
const doc = this.document;
|
|
180
|
-
const canvasId = `archimate-canvas-${this.id}`;
|
|
181
|
-
|
|
182
|
-
const canvas = doc.createElementNS(SVG_NS, 'svg');
|
|
183
|
-
canvas.id = canvasId;
|
|
184
|
-
canvas.setAttribute('viewBox', '0 0 1000 700');
|
|
185
|
-
canvas.style.cssText = 'flex: 1; width: 100%; height: 100%; display: block; background: transparent; box-sizing: border-box;';
|
|
186
|
-
const elements = doc.createElementNS(SVG_NS, 'g');
|
|
187
|
-
elements.id = 'elements';
|
|
188
|
-
canvas.appendChild(elements);
|
|
189
|
-
|
|
190
|
-
const tabs = new ViewTabStrip({
|
|
191
|
-
document: doc,
|
|
192
|
-
onSelect: (id) => model.switchTo(id),
|
|
193
|
-
menuItems: (id) => this.viewMenu.items(id),
|
|
194
|
-
}).render(model.views(), model.currentViewId);
|
|
195
|
-
|
|
196
|
-
const { toolbar, side, main, aside } = new EditorShell({ document: doc, sideWidth: '260px', aside: true, asideWidth: this.spec.analysis ? '260px' : '210px' }).mount(this.container);
|
|
197
|
-
Object.assign(aside.style, { padding: '6px', fontSize: '12px' });
|
|
198
|
-
// With `analysis`, the palette shares the right with the analysis of the selected element.
|
|
199
|
-
let paletteHost = aside, analysisHost = null;
|
|
200
|
-
if (this.spec.analysis) {
|
|
201
|
-
Object.assign(aside.style, { display: 'flex', flexDirection: 'column', overflow: 'hidden' });
|
|
202
|
-
paletteHost = aside.appendChild(doc.createElement('div'));
|
|
203
|
-
Object.assign(paletteHost.style, { flex: '1 1 55%', overflowY: 'auto', minHeight: '0' });
|
|
204
|
-
analysisHost = aside.appendChild(doc.createElement('div'));
|
|
205
|
-
analysisHost.setAttribute('data-analysis', '');
|
|
206
|
-
Object.assign(analysisHost.style, { flex: '1 1 45%', overflowY: 'auto', minHeight: '0', borderTop: '1px solid #ddd' });
|
|
207
|
-
}
|
|
208
|
-
side.appendChild(this._treePanel = buildModelTreePanel(doc, {
|
|
209
|
-
modelTree: this.modelTree,
|
|
210
|
-
treeDivId: `archimate-canvas-tree-${this.id}`,
|
|
211
|
-
viewMenu: this.viewMenu,
|
|
212
|
-
templateMenu: this.templateMenu,
|
|
213
|
-
elementMenu: (element) => this.elementMenu(element),
|
|
214
|
-
search: true,
|
|
215
|
-
version: this.version,
|
|
216
|
-
}));
|
|
217
|
-
main.style.flexDirection = 'column';
|
|
218
|
-
render(main, [tabs, { style: { flex: '1', display: 'flex', flexDirection: 'column', minHeight: '0' }, children: [canvas] }], doc);
|
|
219
|
-
|
|
220
|
-
const customViewpoints = CustomViewpoints.load();
|
|
221
|
-
const previous = this.editor;
|
|
222
|
-
previous?.dispose();
|
|
223
|
-
this.editor = new ArchimateCanvasEditor({
|
|
224
|
-
diagramContainerId: canvasId,
|
|
225
|
-
view,
|
|
226
|
-
model,
|
|
227
|
-
nodeOptions: { archimateVersion: this.version, useDistinctShapes: this.distinctShapes, viewpoint: this.viewpoint, viewpointMode: this.viewpointMode, showDerived: this.showDerived },
|
|
228
|
-
chooseKind: (kinds, from, to, options) => this.chooseKind(kinds, from, to, options),
|
|
229
|
-
// The view's settings: background, title, watermark, how invalid relationships show.
|
|
230
|
-
viewSettings: () => this.settings.getEffectiveSettings(this.model.currentView, 'default'),
|
|
231
|
-
settingsManager: this.settings,
|
|
232
|
-
// The user's own viewpoints, after the standard ones.
|
|
233
|
-
customViewpoints,
|
|
234
|
-
// Canvas templates: placed on a canvas, saved from its shapes (the browser's library).
|
|
235
|
-
templateMenu: this.templateMenu,
|
|
236
|
-
// Version and distinct shapes (⚙ Settings) change the palette too.
|
|
237
|
-
onNodeOptionsChange: ({ archimateVersion, useDistinctShapes, viewpoint, viewpointMode, showDerived }) => {
|
|
238
|
-
this.showDerived = !!showDerived;
|
|
239
|
-
this.version = archimateVersion;
|
|
240
|
-
this.distinctShapes = !!useDistinctShapes;
|
|
241
|
-
this.viewpoint = viewpoint;
|
|
242
|
-
this.viewpointMode = viewpointMode;
|
|
243
|
-
this.render();
|
|
244
|
-
},
|
|
245
|
-
});
|
|
246
|
-
if (previous) {
|
|
247
|
-
this.editor.showGrid = previous.showGrid;
|
|
248
|
-
this.editor.gridSize = previous.gridSize;
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
// A canvas's palette is Archi's canvas palette; another view's, ArchiMate's element types.
|
|
252
|
-
const isCanvas = view.constructor.kind === 'canvas';
|
|
253
|
-
const Palette = isCanvas ? CanvasPalette : ArchimatePalette;
|
|
254
|
-
new Palette({
|
|
255
|
-
version: this.version,
|
|
256
|
-
viewpoint: this.viewpoint,
|
|
257
|
-
customViewpoints,
|
|
258
|
-
document: doc,
|
|
259
|
-
// A click adds the type (or box) in the middle of the visible canvas.
|
|
260
|
-
onPick: (key) => {
|
|
261
|
-
const r = canvas.getBoundingClientRect();
|
|
262
|
-
const at = this.editor._clientToDiagram(r.left + r.width / 2, r.top + r.height / 2) ?? { x: 400, y: 300 };
|
|
263
|
-
if (isCanvas) this.editor.addCanvasBox(key, at);
|
|
264
|
-
else this.editor.addElement(key, at);
|
|
265
|
-
},
|
|
266
|
-
}).render(paletteHost);
|
|
267
|
-
|
|
268
|
-
// The tree follows the canvas's selection; a canvas shows its hint, or the selected box's.
|
|
269
|
-
const hints = (this.hintPanel ??= new HintPanel({ document: doc }));
|
|
270
|
-
const showHint = (templateId) => hints.mount(canvas.parentElement).show(view.constructor.kind === 'canvas' ? (templateId ? view.hintFor(templateId) : view.hint) : null);
|
|
271
|
-
const showAnalysis = this._showAnalysis = (elementId) => {
|
|
272
|
-
if (!analysisHost) return;
|
|
273
|
-
analysisHost.replaceChildren(buildAnalysisPanel(doc, {
|
|
274
|
-
model, elementId: elementId && model.getElement(elementId) ? elementId : null,
|
|
275
|
-
onSelectItem: (info) => this.modelTree.select(info.id, info.kind),
|
|
276
|
-
}));
|
|
277
|
-
};
|
|
278
|
-
this.editor.onSelectionChange(({ templateId }) => {
|
|
279
|
-
const elementId = templateId && this.editor.templates.get(templateId)?.instance?.elementId;
|
|
280
|
-
if (elementId && this.modelTree.getSelectedId() !== elementId && this.model.getElement(elementId)) this.modelTree.select(elementId, 'element');
|
|
281
|
-
showHint(templateId);
|
|
282
|
-
showAnalysis(elementId);
|
|
283
|
-
});
|
|
284
|
-
showHint(null);
|
|
285
|
-
showAnalysis(this.modelTree.getSelectedKind() === 'element' ? this.modelTree.getSelectedId() : null);
|
|
286
|
-
// The tree's filters dim what they leave out on the canvas too.
|
|
287
|
-
this._unsubscribeFilter?.();
|
|
288
|
-
const dim = () => this.editor?.setElementFilter(this.modelTree.matchingElementIds());
|
|
289
|
-
this._unsubscribeFilter = this.modelTree.onFilterChange(dim);
|
|
290
|
-
if (this.modelTree.filtering) dim();
|
|
291
|
-
|
|
292
|
-
this.menuBar?.dispose();
|
|
293
|
-
this.menuBar = new MenuBar({
|
|
294
|
-
container: toolbar,
|
|
295
|
-
editor: this.editor,
|
|
296
|
-
items: [this.fileMenu(), this.editMenu(), ...this.editor.menuBarItems(), new ToolbarText(() => this.fileLabel, { marginLeft: 'auto', color: '#555', fontSize: '12px' })],
|
|
297
|
-
}).render();
|
|
298
|
-
this.editor.render();
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
/**
|
|
302
|
-
* A model element's menu in the tree (elementMenuItems): its properties,
|
|
303
|
-
* the views it is on, adding it to this one, a view generated for it.
|
|
304
|
-
*/
|
|
305
|
-
elementMenu(element) {
|
|
306
|
-
const model = this.model;
|
|
307
|
-
return elementMenuItems(element, {
|
|
308
|
-
model, view: model.currentView,
|
|
309
|
-
editProperties: (el) => editElementProperties(el, {
|
|
310
|
-
history: model.currentView?.history, version: this.version, document: this.document,
|
|
311
|
-
onSaved: () => this.render(),
|
|
312
|
-
}),
|
|
313
|
-
showIn: (view, instanceId) => {
|
|
314
|
-
if (view.id !== model.currentViewId) model.switchTo(view.id); // the part redraws for the new view
|
|
315
|
-
this.editor?.selectTemplate(instanceId);
|
|
316
|
-
},
|
|
317
|
-
addToView: (el) => this.editor?.showElement(el.id, this.editor.visibleCentre()),
|
|
318
|
-
});
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
/** Select the first box of a model element on the current view (nothing if it is not shown). */
|
|
322
|
-
selectElement(elementId) {
|
|
323
|
-
const editor = this.editor;
|
|
324
|
-
if (!editor) return;
|
|
325
|
-
const box = [...editor.templates.values()].find(t => t.instance?.elementId === elementId);
|
|
326
|
-
if (box && editor.selectedTemplateId !== box.id) editor.selectTemplate(box.id);
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
/** Canvas templates over the current model: placed on a canvas, saved from its shapes (the browser's library). */
|
|
330
|
-
get templateMenu() {
|
|
331
|
-
if (this._templateMenu?.model !== this.model) {
|
|
332
|
-
this._templateMenu = new CanvasTemplateMenu(this.model, { library: (this.templateLibrary ??= new CanvasTemplateLibrary()), document: () => this.document });
|
|
333
|
-
}
|
|
334
|
-
return this._templateMenu;
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
dispose() {
|
|
338
|
-
this.menuBar?.dispose();
|
|
339
|
-
this._unsubscribeSettings?.();
|
|
340
|
-
this._unsubscribeFilter?.();
|
|
341
|
-
this._unsubscribe();
|
|
342
|
-
this._unsubscribeTree?.();
|
|
343
|
-
super.dispose();
|
|
344
|
-
}
|
|
345
|
-
}
|
|
1
|
+
import { render } from '../dom.js';
|
|
2
|
+
import { EditorShell } from '../EditorShell.js';
|
|
3
|
+
import { EditorPart } from './EditorPart.js';
|
|
4
|
+
import { ArchimateModel } from '../../charting/diagrams/ArchimateModel.js';
|
|
5
|
+
import { ArchimateCanvasEditor, ArchimatePalette, typeTitle } from '../../charting/archimate/index.js';
|
|
6
|
+
import { ArchimateNotation } from '../../archimate.js';
|
|
7
|
+
import { MenuBar } from '../../charting/MenuBar.js';
|
|
8
|
+
import { ToolbarText } from '../../charting/toolbar/index.js';
|
|
9
|
+
import { ViewTabStrip } from '../../charting/ViewTabStrip.js';
|
|
10
|
+
import { FormDialog } from '../../charting/FormDialog.js';
|
|
11
|
+
import { CsvImportDialog } from '../../charting/archimate/CsvImportDialog.js';
|
|
12
|
+
import { pickFile } from '../../charting/toolbar/pickFile.js';
|
|
13
|
+
import { SettingsManager } from '../../charting/SettingsManager.js';
|
|
14
|
+
import { SettingsViewPropertiesDialog } from '../../charting/SettingsViewPropertiesDialog.js';
|
|
15
|
+
import { CanvasTemplateLibrary } from '../../charting/canvas/CanvasTemplateLibrary.js';
|
|
16
|
+
import { CanvasTemplateMenu } from '../../charting/canvas/CanvasTemplateMenu.js';
|
|
17
|
+
import { CanvasPalette } from '../../charting/canvas/CanvasPalette.js';
|
|
18
|
+
import { ARCHIMATE_FILE_FORMATS } from '../../charting/archimate/archimateFileFormats.js';
|
|
19
|
+
import { ModelTree } from '../../charting/modelTree/ModelTree.js';
|
|
20
|
+
import { buildModelTreePanel } from '../../charting/modelTree/ModelTreePanel.js';
|
|
21
|
+
import { buildAnalysisPanel } from '../../charting/modelTree/AnalysisPanel.js';
|
|
22
|
+
import { ArchimateSpecializationRegistry } from '../../archimateSpecializations.js';
|
|
23
|
+
import { HintPanel } from '../../charting/HintPanel.js';
|
|
24
|
+
import { elementMenuItems } from '../../charting/archimate/elementMenuItems.js';
|
|
25
|
+
import { editElementProperties } from '../../charting/archimate/elementProperties.js';
|
|
26
|
+
import { CustomViewpoints } from '../../charting/archimate/CustomViewpoints.js';
|
|
27
|
+
|
|
28
|
+
const SVG_NS = 'http://www.w3.org/2000/svg';
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* ArchimateCanvasPart — the ArchiMate modeller on the diagram editor's own
|
|
32
|
+
* canvas (phase 4 testbed): the model tree on the left, one tab per view and
|
|
33
|
+
* an ArchimateCanvasEditor over the current view, and a palette of element
|
|
34
|
+
* types grouped by layer on the right. Drop or click a palette type to add an
|
|
35
|
+
* element, or drag one from the tree to show it (with its relationships to
|
|
36
|
+
* what is shown); drag between two elements to add a relationship (its kind
|
|
37
|
+
* picked from those the rules allow); every edit is a step on that view's own
|
|
38
|
+
* undo history. The File menu exports and imports CSV, .archimate and the
|
|
39
|
+
* Exchange Format.
|
|
40
|
+
*
|
|
41
|
+
* Spec (JSON): `elements`, `relationships`, `views`, `name` (as ArchimateModel
|
|
42
|
+
* takes them), `archimateVersion` ('4' or '3.2'), `distinctShapes` (draw each
|
|
43
|
+
* element in its notation shape rather than a box), `viewpoint` (a viewpoint name) and
|
|
44
|
+
* `viewpointMode` ('fade' or 'hide': elements outside it), `analysis` (true adds the
|
|
45
|
+
* analysis of the selected element, under the palette).
|
|
46
|
+
*/
|
|
47
|
+
export class ArchimateCanvasPart extends EditorPart {
|
|
48
|
+
static type = 'archimate-canvas';
|
|
49
|
+
static viewNoun = 'view';
|
|
50
|
+
|
|
51
|
+
constructor(spec = {}) {
|
|
52
|
+
super(spec);
|
|
53
|
+
this.version = spec.archimateVersion ?? '4';
|
|
54
|
+
this.distinctShapes = !!spec.distinctShapes;
|
|
55
|
+
this.viewpoint = spec.viewpoint ?? 'All';
|
|
56
|
+
this.viewpointMode = spec.viewpointMode ?? 'fade';
|
|
57
|
+
this.showDerived = !!spec.showDerived;
|
|
58
|
+
this.settings = new SettingsManager();
|
|
59
|
+
// A settings change (Model settings…, a view's properties) redraws the canvas.
|
|
60
|
+
this._unsubscribeSettings = this.settings.onSettingsChange(() => { if (this.mounted) this.editor?.render(); });
|
|
61
|
+
// The user's specializations (or the defaults), as the main editor offers them; one already loaded is kept.
|
|
62
|
+
if (!ArchimateSpecializationRegistry.list().length) ArchimateSpecializationRegistry.loadFrom();
|
|
63
|
+
this._follow(this.model);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Re-render on every change to `model`'s views (the editor is rebuilt with the canvas on each
|
|
68
|
+
* render), and redraw the tree after every edit (an element renamed, an undo).
|
|
69
|
+
* @private
|
|
70
|
+
*/
|
|
71
|
+
_follow(model) {
|
|
72
|
+
this._unsubscribe?.();
|
|
73
|
+
this._unsubscribeTree?.();
|
|
74
|
+
this._unsubscribe = model.onChange(() => { if (this.mounted) this.render(); });
|
|
75
|
+
this._unsubscribeTree = model.onEdit?.(() => this._treePanel?.refresh?.()) ?? null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
fileFormats() {
|
|
79
|
+
return ARCHIMATE_FILE_FORMATS;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** A new file is an Archi file (.archimate). */
|
|
83
|
+
saveFormat() {
|
|
84
|
+
return ARCHIMATE_FILE_FORMATS.find(f => f.featureName === 'Archimate');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** The file label in the toolbar (and a ModelsPart's tab) follows the saved state. */
|
|
88
|
+
onFileState() {
|
|
89
|
+
super.onFileState();
|
|
90
|
+
this.menuBar?.refresh?.();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** New view…, then New canvas… and New canvas from template. */
|
|
94
|
+
newItems() {
|
|
95
|
+
return [...super.newItems(), ...this.templateMenu.newCanvasItems()];
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** File > Templates: the open canvas saved as a model template or into the library. */
|
|
99
|
+
ownFileItems() {
|
|
100
|
+
const saveAs = this.model.currentView ? this.templateMenu.saveAsTemplateItems(this.model.currentViewId) : [];
|
|
101
|
+
return EditorPart.menuGroup('Templates', saveAs);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** File > Import: an Archi canvas template, and a spreadsheet's rows (CSV with mapping). */
|
|
105
|
+
ownImportItems() {
|
|
106
|
+
const importTemplate = this.templateMenu.importItem();
|
|
107
|
+
return [{ separator: true }, ...(importTemplate ? [{ ...importTemplate, label: 'Archi template…' }] : []), { label: 'CSV with mapping…', action: () => this.importCsvMapped() }];
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Import CSV with mapping…: a spreadsheet's rows into this model (CsvImportDialog), the new elements on the open view. */
|
|
111
|
+
async importCsvMapped() {
|
|
112
|
+
const file = await pickFile('.csv,.txt,.tsv', this.document);
|
|
113
|
+
if (!file) return null;
|
|
114
|
+
return new CsvImportDialog({ model: this.model, view: this.model.currentView, version: this.version, document: this.document, onDone: () => this.render() }).open(await file.text());
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** An imported model: follow it, with a fresh tree, and give it a view if it has none. */
|
|
118
|
+
modelReplaced(model) {
|
|
119
|
+
if (!model.views().length) model.createView();
|
|
120
|
+
this._follow(model);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** The model tree over the current model (kept across renders, so its mode and selection stay). */
|
|
124
|
+
get modelTree() {
|
|
125
|
+
if (this.tree?.model !== this.model) {
|
|
126
|
+
this.tree = new ModelTree(this.model, { mode: 'byLayer' });
|
|
127
|
+
// A view chosen in the tree opens it; an element chosen there is selected on the canvas.
|
|
128
|
+
this.tree.onSelectionChange((data) => {
|
|
129
|
+
if (data.kind === 'view' && data.view.id !== this.model.currentViewId) this.model.switchTo(data.view.id);
|
|
130
|
+
if (data.kind === 'element') { this.selectElement(data.id); this._showAnalysis?.(data.id); }
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
return this.tree;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
createModel(spec) {
|
|
137
|
+
return new ArchimateModel({
|
|
138
|
+
name: spec.name ?? '',
|
|
139
|
+
elements: spec.elements ?? [],
|
|
140
|
+
relationships: spec.relationships ?? [],
|
|
141
|
+
views: spec.views?.length ? spec.views : [{ id: 'view-1', name: 'View 1', elementIds: [] }],
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** A view's properties: name, documentation and its settings (routing, grid, background, ...). */
|
|
146
|
+
showViewProperties(view) {
|
|
147
|
+
return new SettingsViewPropertiesDialog({ model: this.model, view, settingsManager: this.settings, document: this.document }).open();
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Ask which relationship a new connection is, among the kinds the rules
|
|
152
|
+
* allow between the two elements. Resolves to the kind, or null if cancelled.
|
|
153
|
+
*/
|
|
154
|
+
chooseKind(kinds, from, to, { reason = 'connect' } = {}) {
|
|
155
|
+
const nesting = reason === 'nesting';
|
|
156
|
+
return new Promise((resolve) => {
|
|
157
|
+
new FormDialog({
|
|
158
|
+
title: nesting ? `${to.name} nested in ${from.name}` : `New relationship: ${from.name} → ${to.name}`,
|
|
159
|
+
description: nesting
|
|
160
|
+
? `Add the relationship the nesting stands for (${typeTitle(from.type ?? '?')} → ${typeTitle(to.type ?? '?')}), in the model; the nesting shows it, so no line is drawn.`
|
|
161
|
+
: `${typeTitle(from.type ?? '?')} → ${typeTitle(to.type ?? '?')}: ${kinds.length} allowed kind${kinds.length === 1 ? '' : 's'}`,
|
|
162
|
+
fields: [{
|
|
163
|
+
key: 'kind', label: 'Kind', type: 'select', value: kinds[0],
|
|
164
|
+
choices: kinds.map(k => [k, ArchimateNotation.relationshipType(k)?.description ? `${typeTitle(k)}: ${ArchimateNotation.relationshipType(k).description}` : typeTitle(k)]),
|
|
165
|
+
}],
|
|
166
|
+
submitLabel: 'Add',
|
|
167
|
+
...(nesting && { cancelLabel: 'No relationship' }),
|
|
168
|
+
idPrefix: 'archimate-kind-',
|
|
169
|
+
document: this.document,
|
|
170
|
+
onApply: ({ kind }) => resolve(kind),
|
|
171
|
+
onClose: (applied) => { if (!applied) resolve(null); },
|
|
172
|
+
}).open();
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
render() {
|
|
177
|
+
const model = this.model;
|
|
178
|
+
const view = model.currentView;
|
|
179
|
+
const doc = this.document;
|
|
180
|
+
const canvasId = `archimate-canvas-${this.id}`;
|
|
181
|
+
|
|
182
|
+
const canvas = doc.createElementNS(SVG_NS, 'svg');
|
|
183
|
+
canvas.id = canvasId;
|
|
184
|
+
canvas.setAttribute('viewBox', '0 0 1000 700');
|
|
185
|
+
canvas.style.cssText = 'flex: 1; width: 100%; height: 100%; display: block; background: transparent; box-sizing: border-box;';
|
|
186
|
+
const elements = doc.createElementNS(SVG_NS, 'g');
|
|
187
|
+
elements.id = 'elements';
|
|
188
|
+
canvas.appendChild(elements);
|
|
189
|
+
|
|
190
|
+
const tabs = new ViewTabStrip({
|
|
191
|
+
document: doc,
|
|
192
|
+
onSelect: (id) => model.switchTo(id),
|
|
193
|
+
menuItems: (id) => this.viewMenu.items(id),
|
|
194
|
+
}).render(model.views(), model.currentViewId);
|
|
195
|
+
|
|
196
|
+
const { toolbar, side, main, aside } = new EditorShell({ document: doc, sideWidth: '260px', aside: true, asideWidth: this.spec.analysis ? '260px' : '210px' }).mount(this.container);
|
|
197
|
+
Object.assign(aside.style, { padding: '6px', fontSize: '12px' });
|
|
198
|
+
// With `analysis`, the palette shares the right with the analysis of the selected element.
|
|
199
|
+
let paletteHost = aside, analysisHost = null;
|
|
200
|
+
if (this.spec.analysis) {
|
|
201
|
+
Object.assign(aside.style, { display: 'flex', flexDirection: 'column', overflow: 'hidden' });
|
|
202
|
+
paletteHost = aside.appendChild(doc.createElement('div'));
|
|
203
|
+
Object.assign(paletteHost.style, { flex: '1 1 55%', overflowY: 'auto', minHeight: '0' });
|
|
204
|
+
analysisHost = aside.appendChild(doc.createElement('div'));
|
|
205
|
+
analysisHost.setAttribute('data-analysis', '');
|
|
206
|
+
Object.assign(analysisHost.style, { flex: '1 1 45%', overflowY: 'auto', minHeight: '0', borderTop: '1px solid #ddd' });
|
|
207
|
+
}
|
|
208
|
+
side.appendChild(this._treePanel = buildModelTreePanel(doc, {
|
|
209
|
+
modelTree: this.modelTree,
|
|
210
|
+
treeDivId: `archimate-canvas-tree-${this.id}`,
|
|
211
|
+
viewMenu: this.viewMenu,
|
|
212
|
+
templateMenu: this.templateMenu,
|
|
213
|
+
elementMenu: (element) => this.elementMenu(element),
|
|
214
|
+
search: true,
|
|
215
|
+
version: this.version,
|
|
216
|
+
}));
|
|
217
|
+
main.style.flexDirection = 'column';
|
|
218
|
+
render(main, [tabs, { style: { flex: '1', display: 'flex', flexDirection: 'column', minHeight: '0' }, children: [canvas] }], doc);
|
|
219
|
+
|
|
220
|
+
const customViewpoints = CustomViewpoints.load();
|
|
221
|
+
const previous = this.editor;
|
|
222
|
+
previous?.dispose();
|
|
223
|
+
this.editor = new ArchimateCanvasEditor({
|
|
224
|
+
diagramContainerId: canvasId,
|
|
225
|
+
view,
|
|
226
|
+
model,
|
|
227
|
+
nodeOptions: { archimateVersion: this.version, useDistinctShapes: this.distinctShapes, viewpoint: this.viewpoint, viewpointMode: this.viewpointMode, showDerived: this.showDerived },
|
|
228
|
+
chooseKind: (kinds, from, to, options) => this.chooseKind(kinds, from, to, options),
|
|
229
|
+
// The view's settings: background, title, watermark, how invalid relationships show.
|
|
230
|
+
viewSettings: () => this.settings.getEffectiveSettings(this.model.currentView, 'default'),
|
|
231
|
+
settingsManager: this.settings,
|
|
232
|
+
// The user's own viewpoints, after the standard ones.
|
|
233
|
+
customViewpoints,
|
|
234
|
+
// Canvas templates: placed on a canvas, saved from its shapes (the browser's library).
|
|
235
|
+
templateMenu: this.templateMenu,
|
|
236
|
+
// Version and distinct shapes (⚙ Settings) change the palette too.
|
|
237
|
+
onNodeOptionsChange: ({ archimateVersion, useDistinctShapes, viewpoint, viewpointMode, showDerived }) => {
|
|
238
|
+
this.showDerived = !!showDerived;
|
|
239
|
+
this.version = archimateVersion;
|
|
240
|
+
this.distinctShapes = !!useDistinctShapes;
|
|
241
|
+
this.viewpoint = viewpoint;
|
|
242
|
+
this.viewpointMode = viewpointMode;
|
|
243
|
+
this.render();
|
|
244
|
+
},
|
|
245
|
+
});
|
|
246
|
+
if (previous) {
|
|
247
|
+
this.editor.showGrid = previous.showGrid;
|
|
248
|
+
this.editor.gridSize = previous.gridSize;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// A canvas's palette is Archi's canvas palette; another view's, ArchiMate's element types.
|
|
252
|
+
const isCanvas = view.constructor.kind === 'canvas';
|
|
253
|
+
const Palette = isCanvas ? CanvasPalette : ArchimatePalette;
|
|
254
|
+
new Palette({
|
|
255
|
+
version: this.version,
|
|
256
|
+
viewpoint: this.viewpoint,
|
|
257
|
+
customViewpoints,
|
|
258
|
+
document: doc,
|
|
259
|
+
// A click adds the type (or box) in the middle of the visible canvas.
|
|
260
|
+
onPick: (key) => {
|
|
261
|
+
const r = canvas.getBoundingClientRect();
|
|
262
|
+
const at = this.editor._clientToDiagram(r.left + r.width / 2, r.top + r.height / 2) ?? { x: 400, y: 300 };
|
|
263
|
+
if (isCanvas) this.editor.addCanvasBox(key, at);
|
|
264
|
+
else this.editor.addElement(key, at);
|
|
265
|
+
},
|
|
266
|
+
}).render(paletteHost);
|
|
267
|
+
|
|
268
|
+
// The tree follows the canvas's selection; a canvas shows its hint, or the selected box's.
|
|
269
|
+
const hints = (this.hintPanel ??= new HintPanel({ document: doc }));
|
|
270
|
+
const showHint = (templateId) => hints.mount(canvas.parentElement).show(view.constructor.kind === 'canvas' ? (templateId ? view.hintFor(templateId) : view.hint) : null);
|
|
271
|
+
const showAnalysis = this._showAnalysis = (elementId) => {
|
|
272
|
+
if (!analysisHost) return;
|
|
273
|
+
analysisHost.replaceChildren(buildAnalysisPanel(doc, {
|
|
274
|
+
model, elementId: elementId && model.getElement(elementId) ? elementId : null,
|
|
275
|
+
onSelectItem: (info) => this.modelTree.select(info.id, info.kind),
|
|
276
|
+
}));
|
|
277
|
+
};
|
|
278
|
+
this.editor.onSelectionChange(({ templateId }) => {
|
|
279
|
+
const elementId = templateId && this.editor.templates.get(templateId)?.instance?.elementId;
|
|
280
|
+
if (elementId && this.modelTree.getSelectedId() !== elementId && this.model.getElement(elementId)) this.modelTree.select(elementId, 'element');
|
|
281
|
+
showHint(templateId);
|
|
282
|
+
showAnalysis(elementId);
|
|
283
|
+
});
|
|
284
|
+
showHint(null);
|
|
285
|
+
showAnalysis(this.modelTree.getSelectedKind() === 'element' ? this.modelTree.getSelectedId() : null);
|
|
286
|
+
// The tree's filters dim what they leave out on the canvas too.
|
|
287
|
+
this._unsubscribeFilter?.();
|
|
288
|
+
const dim = () => this.editor?.setElementFilter(this.modelTree.matchingElementIds());
|
|
289
|
+
this._unsubscribeFilter = this.modelTree.onFilterChange(dim);
|
|
290
|
+
if (this.modelTree.filtering) dim();
|
|
291
|
+
|
|
292
|
+
this.menuBar?.dispose();
|
|
293
|
+
this.menuBar = new MenuBar({
|
|
294
|
+
container: toolbar,
|
|
295
|
+
editor: this.editor,
|
|
296
|
+
items: [this.fileMenu(), this.editMenu(), ...this.hostMenus(), ...this.editor.menuBarItems(), new ToolbarText(() => this.fileLabel, { marginLeft: 'auto', color: '#555', fontSize: '12px' })],
|
|
297
|
+
}).render();
|
|
298
|
+
this.editor.render();
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* A model element's menu in the tree (elementMenuItems): its properties,
|
|
303
|
+
* the views it is on, adding it to this one, a view generated for it.
|
|
304
|
+
*/
|
|
305
|
+
elementMenu(element) {
|
|
306
|
+
const model = this.model;
|
|
307
|
+
return elementMenuItems(element, {
|
|
308
|
+
model, view: model.currentView,
|
|
309
|
+
editProperties: (el) => editElementProperties(el, {
|
|
310
|
+
history: model.currentView?.history, version: this.version, document: this.document,
|
|
311
|
+
onSaved: () => this.render(),
|
|
312
|
+
}),
|
|
313
|
+
showIn: (view, instanceId) => {
|
|
314
|
+
if (view.id !== model.currentViewId) model.switchTo(view.id); // the part redraws for the new view
|
|
315
|
+
this.editor?.selectTemplate(instanceId);
|
|
316
|
+
},
|
|
317
|
+
addToView: (el) => this.editor?.showElement(el.id, this.editor.visibleCentre()),
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/** Select the first box of a model element on the current view (nothing if it is not shown). */
|
|
322
|
+
selectElement(elementId) {
|
|
323
|
+
const editor = this.editor;
|
|
324
|
+
if (!editor) return;
|
|
325
|
+
const box = [...editor.templates.values()].find(t => t.instance?.elementId === elementId);
|
|
326
|
+
if (box && editor.selectedTemplateId !== box.id) editor.selectTemplate(box.id);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/** Canvas templates over the current model: placed on a canvas, saved from its shapes (the browser's library). */
|
|
330
|
+
get templateMenu() {
|
|
331
|
+
if (this._templateMenu?.model !== this.model) {
|
|
332
|
+
this._templateMenu = new CanvasTemplateMenu(this.model, { library: (this.templateLibrary ??= new CanvasTemplateLibrary()), document: () => this.document });
|
|
333
|
+
}
|
|
334
|
+
return this._templateMenu;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
dispose() {
|
|
338
|
+
this.menuBar?.dispose();
|
|
339
|
+
this._unsubscribeSettings?.();
|
|
340
|
+
this._unsubscribeFilter?.();
|
|
341
|
+
this._unsubscribe();
|
|
342
|
+
this._unsubscribeTree?.();
|
|
343
|
+
super.dispose();
|
|
344
|
+
}
|
|
345
|
+
}
|