Skip to content
This repository was archived by the owner on Oct 21, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
116 changes: 115 additions & 1 deletion src/features/constraintMenu/ConstraintMenu.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { inject, injectable, optional } from "inversify";
import { inject, injectable, optional } from "inversify";
import "./constraintMenu.css";
import { AbstractUIExtension, IActionDispatcher, LocalModelSource, TYPES } from "sprotty";
import { ConstraintRegistry } from "./constraintRegistry";
Expand All @@ -19,6 +19,7 @@ import { LabelTypeRegistry } from "../labels/labelTypeRegistry";
import { EditorModeController } from "../editorMode/editorModeController";
import { Switchable, ThemeManager } from "../settingsMenu/themeManager";
import { AnalyzeDiagramAction } from "../serialize/analyze";
import { ChooseConstraintAction } from "./actions";

@injectable()
export class ConstraintMenu extends AbstractUIExtension implements Switchable {
Expand All @@ -28,6 +29,8 @@ export class ConstraintMenu extends AbstractUIExtension implements Switchable {
private editor?: monaco.editor.IStandaloneCodeEditor;
private tree: AutoCompleteTree;
private forceReadOnly: boolean;
private optionsMenu?: HTMLDivElement;
private ignoreCheckboxChange = false;

constructor(
@inject(ConstraintRegistry) private readonly constraintRegistry: ConstraintRegistry,
Expand Down Expand Up @@ -72,6 +75,10 @@ export class ConstraintMenu extends AbstractUIExtension implements Switchable {
</div>
</label>
`;

const title = containerElement.querySelector("#constraint-menu-expand-title") as HTMLElement;
title.appendChild(this.buildOptionsButton());

const accordionContent = document.createElement("div");
accordionContent.classList.add("accordion-content");
const contentDiv = document.createElement("div");
Expand Down Expand Up @@ -222,4 +229,111 @@ export class ConstraintMenu extends AbstractUIExtension implements Switchable {
switchTheme(useDark: boolean): void {
this.editor?.updateOptions({ theme: useDark ? "vs-dark" : "vs" });
}

private buildOptionsButton(): HTMLElement {
const btn = document.createElement("button");
btn.id = "constraint-options-button";
btn.title = "Filter…";
btn.innerHTML = '<span class="codicon codicon-kebab-vertical"></span>';
btn.onclick = () => this.toggleOptionsMenu();
return btn;
}

/** show or hide the menu, generate checkboxes on the fly */
private toggleOptionsMenu(): void {
if (this.optionsMenu) {
this.optionsMenu.remove();
this.optionsMenu = undefined;
return;
}

// 1) create container
this.optionsMenu = document.createElement("div");
this.optionsMenu.id = "constraint-options-menu";

// 2) add the “All constraints” checkbox at the top
const allConstraints = document.createElement("label");
allConstraints.classList.add("options-item");

const allCb = document.createElement("input");
allCb.type = "checkbox";
allCb.value = "ALL";
allCb.checked = this.constraintRegistry
.getConstraintList()
.map((c) => c.name)
.every((c) => this.constraintRegistry.getSelectedConstraints().includes(c));

allCb.onchange = () => {
if (!this.optionsMenu) return;

this.ignoreCheckboxChange = true;
try {
if (allCb.checked) {
this.optionsMenu.querySelectorAll<HTMLInputElement>("input[type=checkbox]").forEach((cb) => {
if (cb !== allCb) cb.checked = true;
});
this.dispatcher.dispatch(
ChooseConstraintAction.create(this.constraintRegistry.getConstraintList().map((c) => c.name)),
);
} else {
this.optionsMenu.querySelectorAll<HTMLInputElement>("input[type=checkbox]").forEach((cb) => {
if (cb !== allCb) cb.checked = false;
});
this.dispatcher.dispatch(ChooseConstraintAction.create([]));
}
} finally {
this.ignoreCheckboxChange = false;
}
};

allConstraints.appendChild(allCb);
allConstraints.appendChild(document.createTextNode("All constraints"));
this.optionsMenu.appendChild(allConstraints);

// 2) pull your dynamic items
const items = this.constraintRegistry.getConstraintList();

// 3) for each item build a checkbox
items.forEach((item) => {
const label = document.createElement("label");
label.classList.add("options-item");

const cb = document.createElement("input");
cb.type = "checkbox";
cb.value = item.name;
cb.checked = this.constraintRegistry.getSelectedConstraints().includes(cb.value);

cb.onchange = () => {
if (this.ignoreCheckboxChange) return;

const checkboxes = this.optionsMenu!.querySelectorAll<HTMLInputElement>("input[type=checkbox]");
const individualCheckboxes = Array.from(checkboxes).filter((cb) => cb !== allCb);
const selected = individualCheckboxes.filter((cb) => cb.checked).map((cb) => cb.value);

allCb.checked = individualCheckboxes.every((cb) => cb.checked);

this.dispatcher.dispatch(ChooseConstraintAction.create(selected));
};

label.appendChild(cb);
label.appendChild(document.createTextNode(item.name));
this.optionsMenu!.appendChild(label);
});

this.editorContainer.appendChild(this.optionsMenu);

// optional: click-outside handler
const onClickOutside = (e: MouseEvent) => {
const target = e.target as Node;
if (!this.optionsMenu || this.optionsMenu.contains(target)) return;

const button = document.getElementById("constraint-options-button");
if (button && button.contains(target)) return;

this.optionsMenu.remove();
this.optionsMenu = undefined;
document.removeEventListener("click", onClickOutside);
};
document.addEventListener("click", onClickOutside);
}
}
14 changes: 14 additions & 0 deletions src/features/constraintMenu/actions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { Action } from "sprotty-protocol";

export interface ChooseConstraintAction extends Action {
kind: typeof ChooseConstraintAction.KIND;
names: string[];
}

export namespace ChooseConstraintAction {
export const KIND = "choose-constraint";

export function create(names: string[]): ChooseConstraintAction {
return { kind: KIND, names };
}
}
73 changes: 73 additions & 0 deletions src/features/constraintMenu/commands.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { inject, injectable } from "inversify";
import { Command, CommandExecutionContext, CommandReturn, TYPES } from "sprotty";
import { DfdNodeImpl } from "../dfdElements/nodes";
import { ChooseConstraintAction } from "./actions";
import { getBasicType } from "sprotty-protocol";
import { AnnnotationsManager } from "../settingsMenu/annotationManager";
import { ConstraintRegistry } from "./constraintRegistry";

@injectable()
export class ChooseConstraintCommand extends Command {
static readonly KIND = ChooseConstraintAction.KIND;

constructor(
@inject(TYPES.Action) private action: ChooseConstraintAction,
@inject(AnnnotationsManager) private annnotationsManager: AnnnotationsManager,
@inject(ConstraintRegistry) private constraintRegistry: ConstraintRegistry,
) {
super();
}

execute(context: CommandExecutionContext): CommandReturn {
this.annnotationsManager.clearTfgs();
const names = this.action.names;
this.constraintRegistry.setSelectedConstraints(names);

const nodes = context.root.children.filter((node) => getBasicType(node) === "node") as DfdNodeImpl[];
if (names.length === 0) {
nodes.forEach((node) => {
node.setColor("var(--color-primary)");
});
return context.root;
}

nodes.forEach((node) => {
const annotations = node.annotations!;
let wasAdjusted = false;
if (this.constraintRegistry.selectedContainsAllConstraints()) {
annotations.forEach((annotation) => {
if (annotation.message.startsWith("Constraint")) {
wasAdjusted = true;
node.setColor(annotation.color!);
}
});
}
names.forEach((name) => {
annotations.forEach((annotation) => {
if (annotation.message.startsWith("Constraint ") && annotation.message.split(" ")[1] === name) {
node.setColor(annotation.color!);
wasAdjusted = true;
this.annnotationsManager.addTfg(annotation.tfg!);
}
});
});
if (!wasAdjusted) node.setColor("var(--color-primary)");
});

nodes.forEach((node) => {
const inTFG = node.annotations!.filter((annotation) =>
this.annnotationsManager.getSelectedTfgs().has(annotation.tfg!),
);
if (inTFG.length > 0) node.setColor("var(--color-highlighted)", false);
});

return context.root;
}

undo(context: CommandExecutionContext): CommandReturn {
return context.root;
}
redo(context: CommandExecutionContext): CommandReturn {
return context.root;
}
}
37 changes: 37 additions & 0 deletions src/features/constraintMenu/constraintMenu.css
Original file line number Diff line number Diff line change
Expand Up @@ -106,3 +106,40 @@ div.constraint-menu {
align-items: center;
gap: 5px;
}

#constraint-options-button {
position: absolute;
top: 6px;
right: 6px;
background: transparent;
border: none;
font-size: 1.2em;
cursor: pointer;
color: var(--color-foreground);
padding: 2px;
}

#constraint-options-menu {
position: absolute;
top: 30px; /* just under the header */
right: 6px;
background: var(--color-background);
border: 1px solid var(--color-foreground);
border-radius: 4px;
padding: 8px;
z-index: 100;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.2);
}

#constraint-options-menu .options-item {
display: flex;
align-items: center;
gap: 6px;
margin-bottom: 4px;
font-size: 0.9em;
color: var(--color-foreground);
}

#constraint-options-menu .options-item:last-child {
margin-bottom: 0;
}
19 changes: 19 additions & 0 deletions src/features/constraintMenu/constraintRegistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export interface Constraint {
export class ConstraintRegistry {
private constraints: Constraint[] = [];
private updateCallbacks: (() => void)[] = [];
private selectedConstraints: string[] = this.constraints.map((c) => c.name);

public setConstraints(constraints: string[]): void {
this.constraints = this.splitIntoConstraintTexts(constraints).map((c) => this.mapToConstraint(c));
Expand All @@ -22,6 +23,14 @@ export class ConstraintRegistry {
this.constraintListChanged();
}

public setSelectedConstraints(constraints: string[]): void {
this.selectedConstraints = constraints;
}

public getSelectedConstraints(): string[] {
return this.selectedConstraints;
}

public clearConstraints(): void {
this.constraints = [];
this.constraintListChanged();
Expand All @@ -43,6 +52,16 @@ export class ConstraintRegistry {
return this.constraints;
}

public selectedContainsAllConstraints(): boolean {
return this.getConstraintList()
.map((c) => c.name)
.every((c) => this.getSelectedConstraints().includes(c));
}

public setAllConstraintsAsSelected(): void {
this.selectedConstraints = this.constraints.map((c) => c.name);
}

private splitIntoConstraintTexts(text: string[]): string[] {
const constraints: string[] = [];
let currentConstraint = "";
Expand Down
8 changes: 6 additions & 2 deletions src/features/constraintMenu/di.config.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,23 @@
import { ContainerModule } from "inversify";
import { EDITOR_TYPES } from "../../utils";
import { ConstraintMenu } from "./ConstraintMenu";
import { TYPES } from "sprotty";
import { configureCommand, TYPES } from "sprotty";
import { ConstraintRegistry } from "./constraintRegistry";
import { SWITCHABLE } from "../settingsMenu/themeManager";
import { ChooseConstraintCommand } from "./commands";

// This module contains an UI extension that adds a tool palette to the editor.
// This tool palette allows the user to create new nodes and edges.
// Additionally it contains the tools that are used to create the nodes and edges.

export const constraintMenuModule = new ContainerModule((bind) => {
export const constraintMenuModule = new ContainerModule((bind, unbind, isBound, rebind) => {
bind(ConstraintRegistry).toSelf().inSingletonScope();

bind(ConstraintMenu).toSelf().inSingletonScope();
bind(TYPES.IUIExtension).toService(ConstraintMenu);
bind(EDITOR_TYPES.DefaultUIElement).toService(ConstraintMenu);
bind(SWITCHABLE).toService(ConstraintMenu);

const context = { bind, unbind, isBound, rebind };
configureCommand(context, ChooseConstraintCommand);
});
4 changes: 2 additions & 2 deletions src/features/dfdElements/elementStyles.css
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,12 @@
Used as a highlighter to mark nodes with errors.
This is essentially a "optional parameter" to this css rule.
See https://stackoverflow.com/questions/17893823/how-to-pass-parameters-to-css-classes */
stroke: var(--color, var(--color-foreground));
stroke: var(--color-foreground);
stroke-width: 1;
/* Background fill of the node.
When --color is unset this is just --color-primary.
If this node is annotated and --color is set, it will be included in the color mix. */
fill: color-mix(in srgb, var(--color-primary), var(--color, transparent) 25%);
fill: color-mix(in srgb, var(--color-primary), var(--color, transparent) 40%);
}

.sprotty-node .node-label text {
Expand Down
Loading