UNPKG

@kieler/klighd-core

Version:

Core KLighD diagram visualization with Sprotty

637 lines (429 loc) 18.1 kB
/* * KIELER - Kiel Integrated Environment for Layout Eclipse RichClient * * http://rtsys.informatik.uni-kiel.de/kieler * * Copyright 2021-2024 by * + Kiel University * + Department of Computer Science * + Real-Time and Embedded Systems Group * * This program and the accompanying materials are made available under the * terms of the Eclipse Public License 2.0 which is available at * http://www.eclipse.org/legal/epl-2.0. * * SPDX-License-Identifier: EPL-2.0 */ import { inject, injectable, postConstruct } from 'inversify' import { ICommand } from 'sprotty' import { Action, UpdateModelAction } from 'sprotty-protocol' import { Registry } from '../base/registry' import { PersistenceStorage, ServiceTypes } from '../services' import { ResetRenderOptionsAction, SetRenderOptionAction } from './actions' import { RangeOption, RenderOption, TransformationOptionType } from './option-models' /** * Whether the sidebar panel is pinned or not. */ export class PinSidebarOption implements RenderOption { static readonly ID: string = 'pin-sidebar' static readonly NAME: string = 'Pin Sidebar' static readonly DEFAULT: boolean = true readonly id: string = PinSidebarOption.ID readonly name: string = PinSidebarOption.NAME readonly type: TransformationOptionType = TransformationOptionType.CHECK readonly initialValue: boolean = PinSidebarOption.DEFAULT currentValue = PinSidebarOption.DEFAULT debug = true } /** * Resize the diagram to fit the viewport if it is redrawn after a model update * or a viewport resize. * This has to have the same id as the corresponding FitToScreenAction. */ export class ResizeToFit implements RenderOption { static readonly ID: string = 'fit' static readonly NAME: string = 'Resize To Fit on Refresh' static readonly DEFAULT: boolean = true readonly id: string = ResizeToFit.ID readonly name: string = ResizeToFit.NAME // readonly tooltip: string = ResizeToFit.TOOLTIP; readonly type: TransformationOptionType = TransformationOptionType.CHECK readonly initialValue: boolean = ResizeToFit.DEFAULT readonly description = 'Always resize to fit after diagram refresh.' currentValue = ResizeToFit.DEFAULT debug = true } /** * Uses a light background instead of an applied theme. */ export class ForceLightBackground implements RenderOption { static readonly ID: string = 'force-light-background' static readonly NAME: string = 'Use Light Background' static readonly DEFAULT: boolean = false readonly id: string = ForceLightBackground.ID readonly name: string = ForceLightBackground.NAME readonly type: TransformationOptionType = TransformationOptionType.CHECK readonly initialValue: boolean = ForceLightBackground.DEFAULT readonly renderCategory: string = Appearance.ID readonly description = 'Use light background regardless of the color scheme.' currentValue = ForceLightBackground.DEFAULT } export class ShowConstraintOption implements RenderOption { static readonly ID: string = 'show-constraints' static readonly NAME: string = 'Show Constraint' readonly id: string = ShowConstraintOption.ID readonly name: string = ShowConstraintOption.NAME readonly type: TransformationOptionType = TransformationOptionType.CHECK readonly initialValue: boolean = false readonly renderCategory: string = Appearance.ID readonly description = 'Show marker for nodes that have interactive layout constraints set.' currentValue = false } export class Appearance implements RenderOption { static readonly ID: string = 'appearance' static readonly NAME: string = 'Appearance' readonly id: string = Appearance.ID readonly name: string = Appearance.NAME readonly type: TransformationOptionType = TransformationOptionType.CATEGORY readonly initialValue: boolean = true readonly description = 'Appearance Category' currentValue = true } /** * Smart Zoom category. */ export class SmartZoom implements RenderOption { static readonly ID: string = 'smart-zoom' static readonly NAME: string = 'Smart Zoom' readonly id: string = SmartZoom.ID readonly name: string = SmartZoom.NAME readonly type: TransformationOptionType = TransformationOptionType.CATEGORY readonly initialValue: boolean = true readonly renderCategory: string = Appearance.ID readonly description = 'Smart Zoom Category' currentValue = true debug = true } /** * Boolean option to enable and disable the smart zoom feature. * This corresponds to the automatic detail level of regions and states * as well as limiting visible elements. */ export class UseSmartZoom implements RenderOption { static readonly ID: string = 'use-smart-zoom' static readonly NAME: string = 'Smart Zoom' readonly id: string = UseSmartZoom.ID readonly name: string = UseSmartZoom.NAME readonly type: TransformationOptionType = TransformationOptionType.CHECK readonly initialValue: boolean = true readonly renderCategory: string = Appearance.ID readonly description = 'Enables Smart Zoom' currentValue = true } /** * Threshold for full detail level. * Corresponds to the regions size compared to the current viewport. */ export class FullDetailRelativeThreshold implements RangeOption { static readonly ID: string = 'full-detail-relative-threshold' static readonly NAME: string = 'Full Detail Relative Threshold' static readonly DEFAULT: number = 0.15 readonly id: string = FullDetailRelativeThreshold.ID readonly name: string = FullDetailRelativeThreshold.NAME readonly type: TransformationOptionType = TransformationOptionType.RANGE readonly values: any[] = [] readonly range = { first: 0.01, second: 1, } readonly stepSize = 0.01 readonly initialValue: number = FullDetailRelativeThreshold.DEFAULT readonly renderCategory: string = SmartZoom.ID readonly description = 'Shows all children of an element that uses at least the amount of the canvas.' + 'A value of 0.2 means an element is shown if its parent has at least 0.2 the size (minimum of width and height) of the canvas.' currentValue = 0.2 debug = true } /** * Threshold for full detail level. * Corresponds to the regions scale using the current viewport. */ export class FullDetailScaleThreshold implements RangeOption { static readonly ID: string = 'full-detail-scale-threshold' static readonly NAME: string = 'Full Detail Scale Threshold' static readonly DEFAULT: number = 0.25 readonly id: string = FullDetailScaleThreshold.ID readonly name: string = FullDetailScaleThreshold.NAME readonly type: TransformationOptionType = TransformationOptionType.RANGE readonly values: any[] = [] readonly range = { first: 0.01, second: 1, } readonly stepSize = 0.01 readonly initialValue: number = FullDetailScaleThreshold.DEFAULT readonly renderCategory: string = SmartZoom.ID readonly description = 'Show an element if it can be rendered in at least the given amount of it original size.' + 'A value of 0.25 means an element is shown if it can be drawn in a fourth of its original height or width.' currentValue = 0.25 debug = true } /** * Boolean option toggling the use of text element replacement with rectangles. */ export class SimplifySmallText implements RenderOption { static readonly ID: string = 'simplify-small-text' static readonly NAME: string = 'Simplify Small Text' readonly id: string = SimplifySmallText.ID readonly name: string = SimplifySmallText.NAME readonly type: TransformationOptionType = TransformationOptionType.CHECK readonly initialValue: boolean = true readonly renderCategory: string = SmartZoom.ID readonly description = 'Whether illegible text is simplified to colored rectangles.' currentValue = true debug = true } /** * Threshold under which text element simplification occurs in pixels. */ export class TextSimplificationThreshold implements RangeOption { static readonly ID: string = 'text-simplification-threshold' static readonly NAME: string = 'Text Simplification Threshold' static readonly DEFAULT: number = 3 readonly id: string = TextSimplificationThreshold.ID readonly name: string = TextSimplificationThreshold.NAME readonly type: TransformationOptionType = TransformationOptionType.RANGE readonly values: any[] = [] readonly range = { first: 1, second: 10, } readonly stepSize = 0.1 readonly initialValue: number = TextSimplificationThreshold.DEFAULT readonly renderCategory: string = SmartZoom.ID readonly description = 'The threshold font size to simplify text.\nIf set to 3 a text which is 3 or less pixel high is simplified.' currentValue = 3 debug = true } /** * The factor by which titles of collapsed regions get scaled by * in relation to their size at native resolution. */ export class TitleScalingFactor implements RangeOption { static readonly ID: string = 'title-scaling-factor' static readonly NAME: string = 'Title Scaling Factor' static readonly DEFAULT: number = 1 readonly id: string = TitleScalingFactor.ID readonly name: string = TitleScalingFactor.NAME readonly type: TransformationOptionType = TransformationOptionType.RANGE readonly values: any[] = [] readonly range = { first: 0.5, second: 3, } readonly stepSize = 0.01 readonly initialValue: number = TitleScalingFactor.DEFAULT readonly renderCategory: string = SmartZoom.ID readonly description = 'Factor to scale region titles compared to their original size.' + 'If set to 1 a region title be its original size (if the space permits) regardless of the zoom level.' + 'If it was 10 pixel high before it will always be 10 pixel high if the label can fit the region.' currentValue = 1 debug = true } /** * Boolean option to toggle the scaling of lines based on zoom level. */ export class UseMinimumLineWidth implements RenderOption { static readonly ID: string = 'use-minimum-line-width' static readonly NAME: string = 'Minimum Line Width' readonly id: string = UseMinimumLineWidth.ID readonly name: string = UseMinimumLineWidth.NAME readonly type: TransformationOptionType = TransformationOptionType.CHECK readonly initialValue: boolean = true readonly renderCategory: string = SmartZoom.ID readonly description = "Whether all borders and lines are at least as wide as set by the corresponding 'Minimum Line Width' option." currentValue = true debug = true } /** * The size scaled lines should have as a minimum at any zoom level in pixels. */ export class MinimumLineWidth implements RangeOption { static readonly ID: string = 'minimum-line-width' static readonly NAME: string = 'Minimum Line Width' static readonly DEFAULT: number = 1 readonly id: string = MinimumLineWidth.ID readonly name: string = MinimumLineWidth.NAME readonly type: TransformationOptionType = TransformationOptionType.RANGE readonly values: any[] = [] readonly range = { first: 0.1, second: 3, } readonly stepSize = 0.01 readonly initialValue: number = 0.5 readonly renderCategory: string = SmartZoom.ID readonly description = 'The minium border or line width.\nIf set to 0.5 each edge or border is at least 0.5 pixel wide.' currentValue = 0.5 debug = true } export enum ShadowOption { /** A real svg shadow. */ PAPER_MODE = 'Paper Mode', /** The shape of the node drawn with different opacity multiple times behind the node. */ KIELER_STYLE = 'KIELER Style', } /** * The style shadows should be drawn in, either the paper mode shadows (nice, but slow in * performance) or in default KIELER-style (fast, not as nice looking). */ export class Shadows implements RenderOption { static readonly ID: string = 'paper-shadows' static readonly NAME: string = 'Shadow Mode' static readonly DEFAULT: ShadowOption = ShadowOption.KIELER_STYLE readonly id: string = Shadows.ID readonly name: string = Shadows.NAME readonly type: TransformationOptionType = TransformationOptionType.CHOICE readonly initialValue: ShadowOption = Shadows.DEFAULT readonly renderCategory: string = Appearance.ID readonly values? = [ShadowOption.PAPER_MODE, ShadowOption.KIELER_STYLE] readonly description = 'The style shadows should be drawn in, either the paper mode shadows (nice, but slow in performance)' + 'or in default KIELER Style (fast, not as nice looking).' + 'KIELER Style draws multiple shapes in form of the node behind the node.' + 'Paper Mode uses SVG shadows.' currentValue = Shadows.DEFAULT } /** * Whether going to a Bookmark should be animated */ export class AnimateGoToBookmark implements RenderOption { static readonly ID: string = 'animate-go-to-bookmark' static readonly NAME: string = 'Animate Go To Bookmark' static readonly DEFAULT: boolean = true readonly id: string = AnimateGoToBookmark.ID readonly name: string = AnimateGoToBookmark.NAME readonly type: TransformationOptionType = TransformationOptionType.CHECK readonly initialValue: boolean = AnimateGoToBookmark.DEFAULT currentValue = true } /** * Boolean option to toggle debug options. */ export class DebugOptions implements RenderOption { static readonly ID: string = 'debug-options' static readonly NAME: string = 'Debug Options' readonly id: string = DebugOptions.ID readonly name: string = DebugOptions.NAME readonly type: TransformationOptionType = TransformationOptionType.CHECK readonly initialValue: boolean = false readonly description = 'Whether debug options should be shown.' currentValue = false } export interface RenderOptionType { readonly ID: string readonly NAME: string new (): RenderOption } export interface RenderOptionDefault extends RenderOptionType { readonly DEFAULT: any } /** {@link Registry} that stores and updates different render options. */ @injectable() export class RenderOptionsRegistry extends Registry { private _renderOptions: Map<string, RenderOption> = new Map() @inject(ServiceTypes.PersistenceStorage) private storage: PersistenceStorage constructor() { super() // Add available render options to this registry // Debug this.register(DebugOptions) this.register(ResizeToFit) this.register(PinSidebarOption) this.register(AnimateGoToBookmark) // Appearance this.register(Appearance) this.register(ForceLightBackground) this.register(ShowConstraintOption) this.register(Shadows) // Smart Zoom this.register(SmartZoom) this.register(UseSmartZoom) this.register(FullDetailRelativeThreshold) this.register(FullDetailScaleThreshold) this.register(SimplifySmallText) this.register(TextSimplificationThreshold) this.register(TitleScalingFactor) this.register(UseMinimumLineWidth) this.register(MinimumLineWidth) } @postConstruct() init(): void { this.storage.getItem<Record<string, unknown>>('render').then((data) => { if (data) this.loadPersistedData(data) }) } /** * Restores options that where previously persisted in storage. Since render * options are not provided by the server, they have to be retrieved from storage. */ private loadPersistedData(data: Record<string, unknown>) { for (const entry of Object.entries(data)) { const option = this._renderOptions.get(entry[0]) if (option) { // eslint-disable-next-line prefer-destructuring option.currentValue = entry[1] } } this.notifyListeners() } /** Registers a single render option. */ register(Option: RenderOptionType): void { this._renderOptions.set(Option.ID, new Option()) } /** Convenience method to register all given options in order. */ registerAll(...Options: RenderOptionType[]): void { Options.forEach((Option) => this.register(Option)) } /** Unregisters a single render option. */ unregister(Option: RenderOptionType): boolean { return this._renderOptions.delete(Option.ID) } /** Convenience method to unregister all given options in order. */ unregisterAll(...Options: RenderOptionType[]): boolean { return Options.every((Option) => this.unregister(Option)) } /** Handles the render options actions. */ // eslint-disable-next-line consistent-return handle(action: Action): void | Action | ICommand { if (SetRenderOptionAction.isThisAction(action)) { const option = this._renderOptions.get(action.id) if (option) { option.currentValue = action.value this.notifyListeners() } } else if (ResetRenderOptionsAction.isThisAction(action)) { this._renderOptions.forEach((option) => { option.currentValue = option.initialValue }) this.notifyListeners() } else { return UpdateModelAction.create([], { animate: false, cause: action }) } } get allRenderOptions(): RenderOption[] { return Array.from(this._renderOptions.values()) } getValue(Option: RenderOptionType): any | undefined { return this._renderOptions.get(Option.ID)?.currentValue } getValueOrDefault(Option: RenderOptionDefault): any { return this.getValue(Option) ?? Option.DEFAULT } }