From 7e10d0e88f98301e066f7b5882b20211bb7a284d Mon Sep 17 00:00:00 2001 From: Alex | Kronox Date: Sun, 5 Jan 2025 15:32:28 +0100 Subject: [PATCH] basic layout choice menu --- src/common/commandPalette.ts | 7 ++- src/common/di.config.ts | 6 +++ src/common/lightDarkSwitch.css | 2 +- src/common/settingsMenu.css | 30 +++++++++++++ src/common/settingsMenu.ts | 70 +++++++++++++++++++++++++++++ src/features/autoLayout/layouter.ts | 53 +++++++++++++++++----- 6 files changed, 154 insertions(+), 14 deletions(-) create mode 100644 src/common/settingsMenu.css create mode 100644 src/common/settingsMenu.ts diff --git a/src/common/commandPalette.ts b/src/common/commandPalette.ts index d18c258..bfe9cc4 100644 --- a/src/common/commandPalette.ts +++ b/src/common/commandPalette.ts @@ -1,4 +1,4 @@ -import { injectable } from "inversify"; +import { inject, injectable } from "inversify"; import { ICommandPaletteActionProvider, LabeledAction, SModelRootImpl, CommitModelAction } from "sprotty"; import { Point } from "sprotty-protocol"; import { LoadDiagramAction } from "../features/serialize/load"; @@ -10,12 +10,15 @@ import { LayoutModelAction } from "../features/autoLayout/command"; import "@vscode/codicons/dist/codicon.css"; import "sprotty/css/command-palette.css"; import "./commandPalette.css"; +import { SettingsManager } from "./settingsMenu"; /** * Provides possible actions for the command palette. */ @injectable() export class ServerCommandPaletteActionProvider implements ICommandPaletteActionProvider { + constructor(@inject(SettingsManager) protected readonly settings: SettingsManager) {} + async getActions( root: Readonly, _text: string, @@ -31,7 +34,7 @@ export class ServerCommandPaletteActionProvider implements ICommandPaletteAction new LabeledAction("Load diagram from JSON", [LoadDiagramAction.create(), commitAction], "go-to-file"), new LabeledAction("Load default diagram", [LoadDefaultDiagramAction.create(), commitAction], "clear-all"), new LabeledAction( - "Layout diagram", + "Layout diagram (Method: " + this.settings.layoutMethod + ")", [LayoutModelAction.create(), commitAction, fitToScreenAction], "layout", ), diff --git a/src/common/di.config.ts b/src/common/di.config.ts index bf6d548..63acc5e 100644 --- a/src/common/di.config.ts +++ b/src/common/di.config.ts @@ -20,6 +20,7 @@ import { DiagramModificationCommandStack } from "./customCommandStack"; import "./commonStyling.css"; import { LightDarkSwitch } from "./lightDarkSwitch"; +import { SettingsManager, SettingsUI } from "./settingsMenu"; export const commonModule = new ContainerModule((bind, unbind, isBound, rebind) => { bind(ServerCommandPaletteActionProvider).toSelf().inSingletonScope(); @@ -34,6 +35,11 @@ export const commonModule = new ContainerModule((bind, unbind, isBound, rebind) bind(TYPES.IUIExtension).toService(HelpUI); bind(EDITOR_TYPES.DefaultUIElement).toService(HelpUI); + bind(SettingsManager).toSelf().inSingletonScope(); + bind(SettingsUI).toSelf().inSingletonScope(); + bind(TYPES.IUIExtension).toService(SettingsUI); + bind(EDITOR_TYPES.DefaultUIElement).toService(SettingsUI); + bind(LightDarkSwitch).toSelf().inSingletonScope(); bind(TYPES.IUIExtension).toService(LightDarkSwitch); bind(EDITOR_TYPES.DefaultUIElement).toService(LightDarkSwitch); diff --git a/src/common/lightDarkSwitch.css b/src/common/lightDarkSwitch.css index 7e5f62d..16574d6 100644 --- a/src/common/lightDarkSwitch.css +++ b/src/common/lightDarkSwitch.css @@ -1,5 +1,5 @@ div.light-dark-switch { - left: 20px; + left: 242px; bottom: 70px; padding: 10px 10px; } diff --git a/src/common/settingsMenu.css b/src/common/settingsMenu.css new file mode 100644 index 0000000..58a9ac0 --- /dev/null +++ b/src/common/settingsMenu.css @@ -0,0 +1,30 @@ +div.settings-ui { + left: 20px; + bottom: 70px; + padding: 10px 10px; + min-width: 190px; +} + +#settings-ui-accordion-label .accordion-button::before { + content: ""; + background-image: url("@fortawesome/fontawesome-free/svgs/solid/gear.svg"); + display: inline-block; + filter: invert(var(--dark-mode)); + height: 16px; + width: 16px; + background-size: 16px 16px; + vertical-align: text-top; +} + +#settings-content { + display: grid; +} + +#settings-content > label { + grid-column-start: 1; +} + +#settings-content > input, +#settings-content > select { + grid-column-start: 2; +} diff --git a/src/common/settingsMenu.ts b/src/common/settingsMenu.ts new file mode 100644 index 0000000..24aa917 --- /dev/null +++ b/src/common/settingsMenu.ts @@ -0,0 +1,70 @@ +import { AbstractUIExtension } from "sprotty"; +import { inject, injectable } from "inversify"; + +import "./settingsMenu.css"; + +@injectable() +export class SettingsManager { + public layoutMethod: LayoutMethod = LayoutMethod.LINES; +} + +@injectable() +export class SettingsUI extends AbstractUIExtension { + static readonly ID = "settings-ui"; + + constructor(@inject(SettingsManager) protected readonly settings: SettingsManager) { + super(); + } + + id(): string { + return SettingsUI.ID; + } + + containerClass(): string { + return SettingsUI.ID; + } + + protected initializeContents(containerElement: HTMLElement): void { + containerElement.classList.add("ui-float"); + containerElement.innerHTML = ` + + +
+
+ + +
+
+ `; + + // Set `settings-enabled` class on body element when keyboard shortcut overview is open. + const checkbox = containerElement.querySelector("#accordion-state-settings") as HTMLInputElement; + const bodyElement = document.querySelector("body") as HTMLBodyElement; + checkbox.addEventListener("change", () => { + if (checkbox.checked) { + bodyElement.classList.add("settings-enabled"); + } else { + bodyElement.classList.remove("settings-enabled"); + } + }); + + const layoutOptionSelect = containerElement.querySelector("#setting-layout-option") as HTMLSelectElement; + layoutOptionSelect.addEventListener("change", () => { + this.settings.layoutMethod = layoutOptionSelect.value as LayoutMethod; + }); + } +} + +export enum LayoutMethod { + LINES = "Lines", + WRAPPING = "Wrapping Lines", + CIRCLES = "Circles", +} diff --git a/src/features/autoLayout/layouter.ts b/src/features/autoLayout/layouter.ts index cc8c461..bfef8a4 100644 --- a/src/features/autoLayout/layouter.ts +++ b/src/features/autoLayout/layouter.ts @@ -10,27 +10,58 @@ import { import { SChildElementImpl, SShapeElementImpl, isBoundsAware } from "sprotty"; import { SShapeElement, SGraph, SModelIndex } from "sprotty-protocol"; import { ElkShape, LayoutOptions } from "elkjs"; +import { LayoutMethod, SettingsManager } from "../../common/settingsMenu"; export class DfdLayoutConfigurator extends DefaultLayoutConfigurator { + constructor(@inject(SettingsManager) protected readonly settings: SettingsManager) { + super(); + } + protected override graphOptions(_sgraph: SGraph, _index: SModelIndex): LayoutOptions { // Elk settings. See https://eclipse.dev/elk/reference.html for available options. return { - "org.eclipse.elk.algorithm": "org.eclipse.elk.layered", - "org.eclipse.elk.layered.spacing.nodeNodeBetweenLayers": "30.0", - "org.eclipse.elk.layered.spacing.edgeNodeBetweenLayers": "20.0", - "org.eclipse.elk.port.borderOffset": "14.0", - // Do not do micro layout for nodes, which includes the node dimensions etc. - // These are all automatically determined by our dfd node views - "org.eclipse.elk.omitNodeMicroLayout": "true", - // Balanced graph > straight edges - "org.eclipse.elk.layered.nodePlacement.favorStraightEdges": "false", - }; + [LayoutMethod.LINES]: { + "org.eclipse.elk.algorithm": "org.eclipse.elk.layered", + "org.eclipse.elk.layered.spacing.nodeNodeBetweenLayers": "30.0", + "org.eclipse.elk.layered.spacing.edgeNodeBetweenLayers": "20.0", + "org.eclipse.elk.port.borderOffset": "14.0", + // Do not do micro layout for nodes, which includes the node dimensions etc. + // These are all automatically determined by our dfd node views + "org.eclipse.elk.omitNodeMicroLayout": "true", + // Balanced graph > straight edges + "org.eclipse.elk.layered.nodePlacement.favorStraightEdges": "false", + }, + [LayoutMethod.WRAPPING]: { + "org.eclipse.elk.algorithm": "org.eclipse.elk.layered", + "org.eclipse.elk.layered.spacing.nodeNodeBetweenLayers": "10.0", //Save more space between layers (long names might break this!) + "org.eclipse.elk.layered.spacing.edgeNodeBetweenLayers": "5.0", //Save more space between layers (long names might break this!) + "org.eclipse.elk.edgeRouting": "ORTHOGONAL", //Edges should be routed orthogonal to each another + "org.eclipse.elk.layered.layering.strategy": "COFFMAN_GRAHAM", + "org.eclipse.elk.layered.compaction.postCompaction.strategy": "LEFT_RIGHT_CONSTRAINT_LOCKING", //Compact the resulting graph horizontally + "org.eclipse.elk.layered.wrapping.strategy": "MULTI_EDGE", //Allow wrapping of multiple edges + "org.eclipse.elk.layered.wrapping.correctionFactor": "2.0", //Allow the wrapping to occur earlier + // Do not do micro layout for nodes, which includes the node dimensions etc. + // These are all automatically determined by our dfd node views + "org.eclipse.elk.omitNodeMicroLayout": "true", + }, + [LayoutMethod.CIRCLES]: { + "org.eclipse.elk.algorithm": "org.eclipse.elk.stress", + "org.eclipse.elk.force.repulsion": "5.0", + "org.eclipse.elk.force.iterations": "100", //Reduce iterations for faster formatting, did not notice differences with more iterations + "org.eclipse.elk.force.repulsivePower": "1", //Edges should repel vertices as well + "org.eclipse.elk.port.borderOffset": "14.0", + // Do not do micro layout for nodes, which includes the node dimensions etc. + // These are all automatically determined by our dfd node views + "org.eclipse.elk.omitNodeMicroLayout": "true", + // Balanced graph > straight edges + }, + }[this.settings.layoutMethod]; } } export const elkFactory = () => new ElkConstructor({ - algorithms: ["layered"], + algorithms: ["layered", "stress"], }); /**