Skip to content
This repository was archived by the owner on Oct 21, 2025. It is now read-only.

Commit 380f986

Browse files
authored
Merge pull request #171 from DataFlowAnalysis/constraintSelect
Constraint select
2 parents a98e9d9 + a513e13 commit 380f986

File tree

17 files changed

+384
-36
lines changed

17 files changed

+384
-36
lines changed

src/features/constraintMenu/ConstraintMenu.ts

Lines changed: 115 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { inject, injectable, optional } from "inversify";
1+
import { inject, injectable, optional } from "inversify";
22
import "./constraintMenu.css";
33
import { AbstractUIExtension, IActionDispatcher, LocalModelSource, TYPES } from "sprotty";
44
import { ConstraintRegistry } from "./constraintRegistry";
@@ -19,6 +19,7 @@ import { LabelTypeRegistry } from "../labels/labelTypeRegistry";
1919
import { EditorModeController } from "../editorMode/editorModeController";
2020
import { Switchable, ThemeManager } from "../settingsMenu/themeManager";
2121
import { AnalyzeDiagramAction } from "../serialize/analyze";
22+
import { ChooseConstraintAction } from "./actions";
2223

2324
@injectable()
2425
export class ConstraintMenu extends AbstractUIExtension implements Switchable {
@@ -28,6 +29,8 @@ export class ConstraintMenu extends AbstractUIExtension implements Switchable {
2829
private editor?: monaco.editor.IStandaloneCodeEditor;
2930
private tree: AutoCompleteTree;
3031
private forceReadOnly: boolean;
32+
private optionsMenu?: HTMLDivElement;
33+
private ignoreCheckboxChange = false;
3134

3235
constructor(
3336
@inject(ConstraintRegistry) private readonly constraintRegistry: ConstraintRegistry,
@@ -72,6 +75,10 @@ export class ConstraintMenu extends AbstractUIExtension implements Switchable {
7275
</div>
7376
</label>
7477
`;
78+
79+
const title = containerElement.querySelector("#constraint-menu-expand-title") as HTMLElement;
80+
title.appendChild(this.buildOptionsButton());
81+
7582
const accordionContent = document.createElement("div");
7683
accordionContent.classList.add("accordion-content");
7784
const contentDiv = document.createElement("div");
@@ -222,4 +229,111 @@ export class ConstraintMenu extends AbstractUIExtension implements Switchable {
222229
switchTheme(useDark: boolean): void {
223230
this.editor?.updateOptions({ theme: useDark ? "vs-dark" : "vs" });
224231
}
232+
233+
private buildOptionsButton(): HTMLElement {
234+
const btn = document.createElement("button");
235+
btn.id = "constraint-options-button";
236+
btn.title = "Filter…";
237+
btn.innerHTML = '<span class="codicon codicon-kebab-vertical"></span>';
238+
btn.onclick = () => this.toggleOptionsMenu();
239+
return btn;
240+
}
241+
242+
/** show or hide the menu, generate checkboxes on the fly */
243+
private toggleOptionsMenu(): void {
244+
if (this.optionsMenu) {
245+
this.optionsMenu.remove();
246+
this.optionsMenu = undefined;
247+
return;
248+
}
249+
250+
// 1) create container
251+
this.optionsMenu = document.createElement("div");
252+
this.optionsMenu.id = "constraint-options-menu";
253+
254+
// 2) add the “All constraints” checkbox at the top
255+
const allConstraints = document.createElement("label");
256+
allConstraints.classList.add("options-item");
257+
258+
const allCb = document.createElement("input");
259+
allCb.type = "checkbox";
260+
allCb.value = "ALL";
261+
allCb.checked = this.constraintRegistry
262+
.getConstraintList()
263+
.map((c) => c.name)
264+
.every((c) => this.constraintRegistry.getSelectedConstraints().includes(c));
265+
266+
allCb.onchange = () => {
267+
if (!this.optionsMenu) return;
268+
269+
this.ignoreCheckboxChange = true;
270+
try {
271+
if (allCb.checked) {
272+
this.optionsMenu.querySelectorAll<HTMLInputElement>("input[type=checkbox]").forEach((cb) => {
273+
if (cb !== allCb) cb.checked = true;
274+
});
275+
this.dispatcher.dispatch(
276+
ChooseConstraintAction.create(this.constraintRegistry.getConstraintList().map((c) => c.name)),
277+
);
278+
} else {
279+
this.optionsMenu.querySelectorAll<HTMLInputElement>("input[type=checkbox]").forEach((cb) => {
280+
if (cb !== allCb) cb.checked = false;
281+
});
282+
this.dispatcher.dispatch(ChooseConstraintAction.create([]));
283+
}
284+
} finally {
285+
this.ignoreCheckboxChange = false;
286+
}
287+
};
288+
289+
allConstraints.appendChild(allCb);
290+
allConstraints.appendChild(document.createTextNode("All constraints"));
291+
this.optionsMenu.appendChild(allConstraints);
292+
293+
// 2) pull your dynamic items
294+
const items = this.constraintRegistry.getConstraintList();
295+
296+
// 3) for each item build a checkbox
297+
items.forEach((item) => {
298+
const label = document.createElement("label");
299+
label.classList.add("options-item");
300+
301+
const cb = document.createElement("input");
302+
cb.type = "checkbox";
303+
cb.value = item.name;
304+
cb.checked = this.constraintRegistry.getSelectedConstraints().includes(cb.value);
305+
306+
cb.onchange = () => {
307+
if (this.ignoreCheckboxChange) return;
308+
309+
const checkboxes = this.optionsMenu!.querySelectorAll<HTMLInputElement>("input[type=checkbox]");
310+
const individualCheckboxes = Array.from(checkboxes).filter((cb) => cb !== allCb);
311+
const selected = individualCheckboxes.filter((cb) => cb.checked).map((cb) => cb.value);
312+
313+
allCb.checked = individualCheckboxes.every((cb) => cb.checked);
314+
315+
this.dispatcher.dispatch(ChooseConstraintAction.create(selected));
316+
};
317+
318+
label.appendChild(cb);
319+
label.appendChild(document.createTextNode(item.name));
320+
this.optionsMenu!.appendChild(label);
321+
});
322+
323+
this.editorContainer.appendChild(this.optionsMenu);
324+
325+
// optional: click-outside handler
326+
const onClickOutside = (e: MouseEvent) => {
327+
const target = e.target as Node;
328+
if (!this.optionsMenu || this.optionsMenu.contains(target)) return;
329+
330+
const button = document.getElementById("constraint-options-button");
331+
if (button && button.contains(target)) return;
332+
333+
this.optionsMenu.remove();
334+
this.optionsMenu = undefined;
335+
document.removeEventListener("click", onClickOutside);
336+
};
337+
document.addEventListener("click", onClickOutside);
338+
}
225339
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { Action } from "sprotty-protocol";
2+
3+
export interface ChooseConstraintAction extends Action {
4+
kind: typeof ChooseConstraintAction.KIND;
5+
names: string[];
6+
}
7+
8+
export namespace ChooseConstraintAction {
9+
export const KIND = "choose-constraint";
10+
11+
export function create(names: string[]): ChooseConstraintAction {
12+
return { kind: KIND, names };
13+
}
14+
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import { inject, injectable } from "inversify";
2+
import { Command, CommandExecutionContext, CommandReturn, TYPES } from "sprotty";
3+
import { DfdNodeImpl } from "../dfdElements/nodes";
4+
import { ChooseConstraintAction } from "./actions";
5+
import { getBasicType } from "sprotty-protocol";
6+
import { AnnnotationsManager } from "../settingsMenu/annotationManager";
7+
import { ConstraintRegistry } from "./constraintRegistry";
8+
9+
@injectable()
10+
export class ChooseConstraintCommand extends Command {
11+
static readonly KIND = ChooseConstraintAction.KIND;
12+
13+
constructor(
14+
@inject(TYPES.Action) private action: ChooseConstraintAction,
15+
@inject(AnnnotationsManager) private annnotationsManager: AnnnotationsManager,
16+
@inject(ConstraintRegistry) private constraintRegistry: ConstraintRegistry,
17+
) {
18+
super();
19+
}
20+
21+
execute(context: CommandExecutionContext): CommandReturn {
22+
this.annnotationsManager.clearTfgs();
23+
const names = this.action.names;
24+
this.constraintRegistry.setSelectedConstraints(names);
25+
26+
const nodes = context.root.children.filter((node) => getBasicType(node) === "node") as DfdNodeImpl[];
27+
if (names.length === 0) {
28+
nodes.forEach((node) => {
29+
node.setColor("var(--color-primary)");
30+
});
31+
return context.root;
32+
}
33+
34+
nodes.forEach((node) => {
35+
const annotations = node.annotations!;
36+
let wasAdjusted = false;
37+
if (this.constraintRegistry.selectedContainsAllConstraints()) {
38+
annotations.forEach((annotation) => {
39+
if (annotation.message.startsWith("Constraint")) {
40+
wasAdjusted = true;
41+
node.setColor(annotation.color!);
42+
}
43+
});
44+
}
45+
names.forEach((name) => {
46+
annotations.forEach((annotation) => {
47+
if (annotation.message.startsWith("Constraint ") && annotation.message.split(" ")[1] === name) {
48+
node.setColor(annotation.color!);
49+
wasAdjusted = true;
50+
this.annnotationsManager.addTfg(annotation.tfg!);
51+
}
52+
});
53+
});
54+
if (!wasAdjusted) node.setColor("var(--color-primary)");
55+
});
56+
57+
nodes.forEach((node) => {
58+
const inTFG = node.annotations!.filter((annotation) =>
59+
this.annnotationsManager.getSelectedTfgs().has(annotation.tfg!),
60+
);
61+
if (inTFG.length > 0) node.setColor("var(--color-highlighted)", false);
62+
});
63+
64+
return context.root;
65+
}
66+
67+
undo(context: CommandExecutionContext): CommandReturn {
68+
return context.root;
69+
}
70+
redo(context: CommandExecutionContext): CommandReturn {
71+
return context.root;
72+
}
73+
}

src/features/constraintMenu/constraintMenu.css

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,3 +106,40 @@ div.constraint-menu {
106106
align-items: center;
107107
gap: 5px;
108108
}
109+
110+
#constraint-options-button {
111+
position: absolute;
112+
top: 6px;
113+
right: 6px;
114+
background: transparent;
115+
border: none;
116+
font-size: 1.2em;
117+
cursor: pointer;
118+
color: var(--color-foreground);
119+
padding: 2px;
120+
}
121+
122+
#constraint-options-menu {
123+
position: absolute;
124+
top: 30px; /* just under the header */
125+
right: 6px;
126+
background: var(--color-background);
127+
border: 1px solid var(--color-foreground);
128+
border-radius: 4px;
129+
padding: 8px;
130+
z-index: 100;
131+
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.2);
132+
}
133+
134+
#constraint-options-menu .options-item {
135+
display: flex;
136+
align-items: center;
137+
gap: 6px;
138+
margin-bottom: 4px;
139+
font-size: 0.9em;
140+
color: var(--color-foreground);
141+
}
142+
143+
#constraint-options-menu .options-item:last-child {
144+
margin-bottom: 0;
145+
}

src/features/constraintMenu/constraintRegistry.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export interface Constraint {
99
export class ConstraintRegistry {
1010
private constraints: Constraint[] = [];
1111
private updateCallbacks: (() => void)[] = [];
12+
private selectedConstraints: string[] = this.constraints.map((c) => c.name);
1213

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

26+
public setSelectedConstraints(constraints: string[]): void {
27+
this.selectedConstraints = constraints;
28+
}
29+
30+
public getSelectedConstraints(): string[] {
31+
return this.selectedConstraints;
32+
}
33+
2534
public clearConstraints(): void {
2635
this.constraints = [];
2736
this.constraintListChanged();
@@ -43,6 +52,16 @@ export class ConstraintRegistry {
4352
return this.constraints;
4453
}
4554

55+
public selectedContainsAllConstraints(): boolean {
56+
return this.getConstraintList()
57+
.map((c) => c.name)
58+
.every((c) => this.getSelectedConstraints().includes(c));
59+
}
60+
61+
public setAllConstraintsAsSelected(): void {
62+
this.selectedConstraints = this.constraints.map((c) => c.name);
63+
}
64+
4665
private splitIntoConstraintTexts(text: string[]): string[] {
4766
const constraints: string[] = [];
4867
let currentConstraint = "";
Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,23 @@
11
import { ContainerModule } from "inversify";
22
import { EDITOR_TYPES } from "../../utils";
33
import { ConstraintMenu } from "./ConstraintMenu";
4-
import { TYPES } from "sprotty";
4+
import { configureCommand, TYPES } from "sprotty";
55
import { ConstraintRegistry } from "./constraintRegistry";
66
import { SWITCHABLE } from "../settingsMenu/themeManager";
7+
import { ChooseConstraintCommand } from "./commands";
78

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

12-
export const constraintMenuModule = new ContainerModule((bind) => {
13+
export const constraintMenuModule = new ContainerModule((bind, unbind, isBound, rebind) => {
1314
bind(ConstraintRegistry).toSelf().inSingletonScope();
1415

1516
bind(ConstraintMenu).toSelf().inSingletonScope();
1617
bind(TYPES.IUIExtension).toService(ConstraintMenu);
1718
bind(EDITOR_TYPES.DefaultUIElement).toService(ConstraintMenu);
1819
bind(SWITCHABLE).toService(ConstraintMenu);
20+
21+
const context = { bind, unbind, isBound, rebind };
22+
configureCommand(context, ChooseConstraintCommand);
1923
});

src/features/dfdElements/elementStyles.css

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,12 @@
1414
Used as a highlighter to mark nodes with errors.
1515
This is essentially a "optional parameter" to this css rule.
1616
See https://stackoverflow.com/questions/17893823/how-to-pass-parameters-to-css-classes */
17-
stroke: var(--color, var(--color-foreground));
17+
stroke: var(--color-foreground);
1818
stroke-width: 1;
1919
/* Background fill of the node.
2020
When --color is unset this is just --color-primary.
2121
If this node is annotated and --color is set, it will be included in the color mix. */
22-
fill: color-mix(in srgb, var(--color-primary), var(--color, transparent) 25%);
22+
fill: color-mix(in srgb, var(--color-primary), var(--color, transparent) 40%);
2323
}
2424

2525
.sprotty-node .node-label text {

0 commit comments

Comments
 (0)