Skip to content

Commit 1f44e8c

Browse files
authored
Merge branch 'main' into readd/image
2 parents a07b8fa + 0016e5b commit 1f44e8c

File tree

14 files changed

+458
-20
lines changed

14 files changed

+458
-20
lines changed

frontend/webEditor/src/accordionUiExtension/index.ts

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,23 +7,25 @@ import "./accordion.css";
77
*/
88
@injectable()
99
export abstract class AccordionUiExtension extends AbstractUIExtension {
10+
private readonly mainCheckbox: HTMLInputElement;
11+
1012
constructor(
1113
private chevronPosition: "left" | "right",
1214
private chevronOrientation: "up" | "down",
1315
) {
1416
super();
17+
this.mainCheckbox = document.createElement("input");
1518
}
1619

1720
protected initializeContents(containerElement: HTMLElement): void {
1821
containerElement.classList.add("ui-float");
1922

2023
// create hidden checkbox used for toggling
21-
const checkbox = document.createElement("input");
22-
checkbox.type = "checkbox";
24+
this.mainCheckbox.type = "checkbox";
2325
const checkboxId = this.id() + "-checkbox";
24-
checkbox.id = checkboxId;
25-
checkbox.classList.add("accordion-state");
26-
checkbox.hidden = true;
26+
this.mainCheckbox.id = checkboxId;
27+
this.mainCheckbox.classList.add("accordion-state");
28+
this.mainCheckbox.hidden = true;
2729

2830
// create clickable label for the checkbox
2931
const label = document.createElement("label");
@@ -44,7 +46,7 @@ export abstract class AccordionUiExtension extends AbstractUIExtension {
4446
this.initializeHidableContent(contentHolder);
4547
accordionContent.appendChild(contentHolder);
4648

47-
containerElement.appendChild(checkbox);
49+
containerElement.appendChild(this.mainCheckbox);
4850
containerElement.appendChild(label);
4951
containerElement.appendChild(accordionContent);
5052
}
@@ -60,4 +62,8 @@ export abstract class AccordionUiExtension extends AbstractUIExtension {
6062
* @param contentElement The containing element of the header
6163
*/
6264
protected abstract initializeHeaderContent(headerElement: HTMLElement): void;
65+
66+
protected toggleStatus() {
67+
this.mainCheckbox.checked = !this.mainCheckbox.checked;
68+
}
6369
}

frontend/webEditor/src/deleteKey/di.config.ts

Lines changed: 0 additions & 8 deletions
This file was deleted.

frontend/webEditor/src/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ import { constraintModule } from "./constraint/di.config";
2525
import { assignmentModule } from "./assignment/di.config";
2626
import { editorModeOverwritesModule } from "./editModeOverwrites/di.config";
2727
import { loadingIndicatorModule } from "./loadingIndicator/di.config";
28-
import { deleteKeyModule } from "./deleteKey/di.config";
28+
import { keyListenerModule } from "./keyListeners/di.config";
2929

3030
const container = new Container();
3131

@@ -54,7 +54,7 @@ container.load(
5454
assignmentModule,
5555
editorModeOverwritesModule,
5656
loadingIndicatorModule,
57-
deleteKeyModule,
57+
keyListenerModule,
5858
);
5959

6060
const startUpAgents = container.getAll<IStartUpAgent>(StartUpAgent);
Lines changed: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,246 @@
1+
import { injectable, inject } from "inversify";
2+
import {
3+
KeyListener,
4+
SModelElementImpl,
5+
MousePositionTracker,
6+
SModelRootImpl,
7+
isSelected,
8+
CommitModelAction,
9+
CommandExecutionContext,
10+
Command,
11+
CommandReturn,
12+
SChildElementImpl,
13+
SEdgeImpl,
14+
SNodeImpl,
15+
TYPES,
16+
} from "sprotty";
17+
import { Action, Point, SEdge, SModelElement } from "sprotty-protocol";
18+
import { matchesKeystroke } from "sprotty/lib/utils/keyboard";
19+
import { DfdNodeImpl, DfdNode } from "../diagram/nodes/common";
20+
import { EditorModeController } from "../settings/editorMode";
21+
import { generateRandomSprottyId } from "../utils/idGenerator";
22+
import { SETTINGS } from "../settings/Settings";
23+
import { LoadJsonCommand } from "../serialize/loadJson";
24+
25+
/**
26+
* This class is responsible for listening to ctrl+c and ctrl+v events.
27+
* On copy the selected elements are copied into an internal array.
28+
* On paste the {@link PasteElementsAction} is executed to paste the elements.
29+
* This is done inside a command, so that it can be undone/redone.
30+
*/
31+
@injectable()
32+
export class CopyPasteKeyListener implements KeyListener {
33+
private copyElements: SModelElementImpl[] = [];
34+
35+
constructor(@inject(MousePositionTracker) private readonly mousePositionTracker: MousePositionTracker) {}
36+
37+
keyUp(): Action[] {
38+
return [];
39+
}
40+
41+
keyDown(element: SModelElementImpl, event: KeyboardEvent): Action[] {
42+
if (matchesKeystroke(event, "KeyC", "ctrl")) {
43+
return this.copy(element.root);
44+
} else if (matchesKeystroke(event, "KeyV", "ctrl")) {
45+
return this.paste();
46+
}
47+
48+
return [];
49+
}
50+
51+
/**
52+
* Copy all selected elements into the "clipboard" (the internal element array)
53+
*/
54+
private copy(root: SModelRootImpl): Action[] {
55+
this.copyElements = []; // Clear the clipboard
56+
57+
// Find selected elements
58+
root.index
59+
.all()
60+
.filter((element) => isSelected(element))
61+
.forEach((e) => this.copyElements.push(e));
62+
63+
return [];
64+
}
65+
66+
/**
67+
* Pastes elements by creating new elements and copying the properties of the copied elements.
68+
* This is done inside a command, so that it can be undone/redone.
69+
*/
70+
private paste(): Action[] {
71+
const targetPosition = this.mousePositionTracker.lastPositionOnDiagram ?? { x: 0, y: 0 };
72+
return [PasteElementsAction.create(this.copyElements, targetPosition), CommitModelAction.create()];
73+
}
74+
}
75+
76+
export interface PasteElementsAction extends Action {
77+
kind: typeof PasteElementsAction.KIND;
78+
copyElements: SModelElementImpl[];
79+
targetPosition: Point;
80+
}
81+
export namespace PasteElementsAction {
82+
export const KIND = "paste-clipboard-elements";
83+
export function create(copyElements: SModelElementImpl[], targetPosition: Point): PasteElementsAction {
84+
return {
85+
kind: KIND,
86+
copyElements,
87+
targetPosition,
88+
};
89+
}
90+
}
91+
92+
/**
93+
* This command is used to paste elements that were copied by the CopyPasteFeature.
94+
* It creates new elements and copies the properties of the copied elements.
95+
* This is done inside a command, so that it can be undone/redone.
96+
*/
97+
@injectable()
98+
export class PasteElementsCommand extends Command {
99+
public static readonly KIND = PasteElementsAction.KIND;
100+
101+
private newElements: SChildElementImpl[] = [];
102+
// This maps the element id of the copy source element to the
103+
// id that the newly created copy target element has.
104+
private copyElementIdMapping: Record<string, string> = {};
105+
106+
constructor(
107+
@inject(TYPES.Action) private readonly action: PasteElementsAction,
108+
@inject(SETTINGS.Mode) private readonly editorModeController: EditorModeController,
109+
) {
110+
super();
111+
}
112+
113+
/**
114+
* Calculates the offset between the copy source elements and the set paste target position.
115+
* Does this by finding the top left position of the copy source elements and subtracting it from the target position.
116+
*
117+
* @returns The offset between the top left position of the copy source elements and the target position.
118+
*/
119+
private computeElementOffset(): Point {
120+
const sourcePosition = { x: Infinity, y: Infinity };
121+
122+
this.action.copyElements.forEach((element) => {
123+
if (!(element instanceof SNodeImpl)) {
124+
return;
125+
}
126+
127+
if (element.position.x < sourcePosition.x) {
128+
sourcePosition.x = element.position.x;
129+
}
130+
if (element.position.y < sourcePosition.y) {
131+
sourcePosition.y = element.position.y;
132+
}
133+
});
134+
135+
if (sourcePosition.x === Infinity || sourcePosition.y === Infinity) {
136+
return { x: 0, y: 0 };
137+
}
138+
139+
// Compute delta between top left position of copy source elements and the target position
140+
return Point.subtract(this.action.targetPosition, sourcePosition);
141+
}
142+
143+
execute(context: CommandExecutionContext): CommandReturn {
144+
if (this.editorModeController?.isReadOnly()) {
145+
return context.root;
146+
}
147+
148+
// Step 1: copy nodes and their ports
149+
const positionOffset = this.computeElementOffset();
150+
this.action.copyElements.forEach((element) => {
151+
if (!(element instanceof SNodeImpl)) {
152+
return;
153+
}
154+
155+
// createSchema only does a shallow copy, so we need to do an additional deep copy here because
156+
// we want to support copying elements with objects and arrays in them.
157+
const schema = JSON.parse(JSON.stringify(context.modelFactory.createSchema(element))) as SModelElement;
158+
if ("features" in schema) {
159+
schema.features = undefined;
160+
}
161+
162+
schema.id = generateRandomSprottyId();
163+
this.copyElementIdMapping[element.id] = schema.id;
164+
if ("position" in schema) {
165+
schema.position = Point.add(element.position, positionOffset);
166+
}
167+
168+
if (element instanceof DfdNodeImpl) {
169+
// Special case for DfdNodes: copy ports and give the nodes new ids.
170+
(schema as DfdNode).ports.forEach((port) => {
171+
const oldPortId = port.id;
172+
port.id = generateRandomSprottyId();
173+
this.copyElementIdMapping[oldPortId] = port.id;
174+
});
175+
}
176+
177+
const newElement = context.modelFactory.createElement(schema, context.root);
178+
this.newElements.push(newElement);
179+
});
180+
181+
// Step 2: copy edges
182+
// If the source and target element of an edge are copied, the edge can be copied as well.
183+
// If only one of them is copied, the edge is not copied.
184+
this.action.copyElements.forEach((element) => {
185+
if (!(element instanceof SEdgeImpl)) {
186+
return;
187+
}
188+
189+
const newSourceId = this.copyElementIdMapping[element.sourceId];
190+
const newTargetId = this.copyElementIdMapping[element.targetId];
191+
192+
if (!newSourceId || !newTargetId) {
193+
// Not both source and target are copied, ignore this edge
194+
return;
195+
}
196+
197+
const schema = JSON.parse(JSON.stringify(context.modelFactory.createSchema(element))) as SEdge;
198+
LoadJsonCommand.preprocessModelSchema(schema);
199+
200+
schema.id = generateRandomSprottyId();
201+
this.copyElementIdMapping[element.id] = schema.id;
202+
203+
schema.sourceId = newSourceId;
204+
schema.targetId = newTargetId;
205+
206+
const newElement = context.modelFactory.createElement(schema);
207+
this.newElements.push(newElement);
208+
});
209+
210+
// Step 3: add new elements to the model and select them
211+
this.newElements.forEach((element) => {
212+
context.root.add(element);
213+
});
214+
//this.setSelection(context, "new");
215+
216+
return context.root;
217+
}
218+
219+
undo(context: CommandExecutionContext): CommandReturn {
220+
if (this.editorModeController?.isReadOnly()) {
221+
return context.root;
222+
}
223+
224+
// Remove elements from the model
225+
this.newElements.forEach((element) => {
226+
context.root.remove(element);
227+
});
228+
// Select the old elements
229+
//this.setSelection(context, "old");
230+
231+
return context.root;
232+
}
233+
234+
redo(context: CommandExecutionContext): CommandReturn {
235+
if (this.editorModeController?.isReadOnly()) {
236+
return context.root;
237+
}
238+
239+
this.newElements.forEach((element) => {
240+
context.root.add(element);
241+
});
242+
//this.setSelection(context, "new");
243+
244+
return context.root;
245+
}
246+
}

frontend/webEditor/src/deleteKey/deleteKeyListener.ts renamed to frontend/webEditor/src/keyListeners/deleteKeyListener.ts

File renamed without changes.
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { ContainerModule } from "inversify";
2+
import { DeleteKeyListener } from "./deleteKeyListener";
3+
import { CenterKeyboardListener, configureCommand, TYPES } from "sprotty";
4+
import { CopyPasteKeyListener, PasteElementsCommand } from "./copyPasteKeyListener";
5+
import { SerializeKeyListener } from "./serializeKeyListener";
6+
import { FitToScreenKeyListener } from "./fitToScreenKeyListener";
7+
8+
export const keyListenerModule = new ContainerModule((bind, unbind, isBound, rebind) => {
9+
bind(DeleteKeyListener).toSelf().inSingletonScope();
10+
bind(TYPES.KeyListener).toService(DeleteKeyListener);
11+
12+
const context = { bind, unbind, isBound, rebind };
13+
bind(TYPES.KeyListener).to(CopyPasteKeyListener).inSingletonScope();
14+
configureCommand(context, PasteElementsCommand);
15+
16+
bind(TYPES.KeyListener).to(SerializeKeyListener).inSingletonScope();
17+
18+
bind(FitToScreenKeyListener).toSelf().inSingletonScope();
19+
rebind(CenterKeyboardListener).toService(FitToScreenKeyListener);
20+
});
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { KeyListener, SModelElementImpl } from "sprotty";
2+
import { Action, CenterAction, FitToScreenAction } from "sprotty-protocol";
3+
import { matchesKeystroke } from "sprotty/lib/utils/keyboard";
4+
5+
/**
6+
* Key listener that fits the diagram to the screen when pressing Ctrl+Shift+F
7+
* and centers the diagram when pressing Ctrl+Shift+C.
8+
*
9+
* Custom version of the CenterKeyboardListener from sprotty because that one
10+
* does not allow setting a padding.
11+
*/
12+
export class FitToScreenKeyListener extends KeyListener {
13+
override keyDown(element: SModelElementImpl, event: KeyboardEvent): Action[] {
14+
if (matchesKeystroke(event, "KeyC", "ctrlCmd", "shift")) {
15+
return [CenterAction.create([])];
16+
}
17+
18+
if (matchesKeystroke(event, "KeyF", "ctrlCmd", "shift")) {
19+
return [FitToScreenAction.create([element.root.id])];
20+
}
21+
22+
return [];
23+
}
24+
}

0 commit comments

Comments
 (0)