Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
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
18 changes: 12 additions & 6 deletions frontend/webEditor/src/accordionUiExtension/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,23 +7,25 @@ import "./accordion.css";
*/
@injectable()
export abstract class AccordionUiExtension extends AbstractUIExtension {
private readonly mainCheckbox: HTMLInputElement;

constructor(
private chevronPosition: "left" | "right",
private chevronOrientation: "up" | "down",
) {
super();
this.mainCheckbox = document.createElement("input");
}

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

// create hidden checkbox used for toggling
const checkbox = document.createElement("input");
checkbox.type = "checkbox";
this.mainCheckbox.type = "checkbox";
const checkboxId = this.id() + "-checkbox";
checkbox.id = checkboxId;
checkbox.classList.add("accordion-state");
checkbox.hidden = true;
this.mainCheckbox.id = checkboxId;
this.mainCheckbox.classList.add("accordion-state");
this.mainCheckbox.hidden = true;

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

containerElement.appendChild(checkbox);
containerElement.appendChild(this.mainCheckbox);
containerElement.appendChild(label);
containerElement.appendChild(accordionContent);
}
Expand All @@ -60,4 +62,8 @@ export abstract class AccordionUiExtension extends AbstractUIExtension {
* @param contentElement The containing element of the header
*/
protected abstract initializeHeaderContent(headerElement: HTMLElement): void;

protected toggleStatus() {
this.mainCheckbox.checked = !this.mainCheckbox.checked;
}
}
8 changes: 0 additions & 8 deletions frontend/webEditor/src/deleteKey/di.config.ts

This file was deleted.

4 changes: 2 additions & 2 deletions frontend/webEditor/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import { constraintModule } from "./constraint/di.config";
import { assignmentModule } from "./assignment/di.config";
import { editorModeOverwritesModule } from "./editModeOverwrites/di.config";
import { loadingIndicatorModule } from "./loadingIndicator/di.config";
import { deleteKeyModule } from "./deleteKey/di.config";
import { keyListenerModule } from "./keyListeners/di.config";

const container = new Container();

Expand Down Expand Up @@ -54,7 +54,7 @@ container.load(
assignmentModule,
editorModeOverwritesModule,
loadingIndicatorModule,
deleteKeyModule,
keyListenerModule,
);

const startUpAgents = container.getAll<IStartUpAgent>(StartUpAgent);
Expand Down
243 changes: 243 additions & 0 deletions frontend/webEditor/src/keyListeners/copyPasteKeyListener.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,243 @@
import { injectable, inject } from "inversify";
import {
KeyListener,
SModelElementImpl,
MousePositionTracker,
SModelRootImpl,
isSelected,
CommitModelAction,
CommandExecutionContext,
Command,
CommandReturn,
SChildElementImpl,
SEdgeImpl,
SNodeImpl,
TYPES,
} from "sprotty";
import { Action, Point, SEdge, SModelElement } from "sprotty-protocol";
import { matchesKeystroke } from "sprotty/lib/utils/keyboard";
import { DfdNodeImpl, DfdNode } from "../diagram/nodes/common";
import { EditorModeController } from "../settings/editorMode";
import { generateRandomSprottyId } from "../utils/idGenerator";
import { SETTINGS } from "../settings/Settings";
import { LoadJsonCommand } from "../serialize/loadJson";

/**
* This class is responsible for listening to ctrl+c and ctrl+v events.
* On copy the selected elements are copied into an internal array.
* On paste the {@link PasteElementsAction} is executed to paste the elements.
* This is done inside a command, so that it can be undone/redone.
*/
@injectable()
export class CopyPasteKeyListener implements KeyListener {
private copyElements: SModelElementImpl[] = [];

constructor(@inject(MousePositionTracker) private readonly mousePositionTracker: MousePositionTracker) {}

keyUp(): Action[] {
return [];
}

keyDown(element: SModelElementImpl, event: KeyboardEvent): Action[] {
if (matchesKeystroke(event, "KeyC", "ctrl")) {
return this.copy(element.root);
} else if (matchesKeystroke(event, "KeyV", "ctrl")) {
return this.paste();
}

return [];
}

/**
* Copy all selected elements into the "clipboard" (the internal element array)
*/
private copy(root: SModelRootImpl): Action[] {
this.copyElements = []; // Clear the clipboard

// Find selected elements
root.index
.all()
.filter((element) => isSelected(element))
.forEach((e) => this.copyElements.push(e));

return [];
}

/**
* Pastes elements by creating new elements and copying the properties of the copied elements.
* This is done inside a command, so that it can be undone/redone.
*/
private paste(): Action[] {
const targetPosition = this.mousePositionTracker.lastPositionOnDiagram ?? { x: 0, y: 0 };
return [PasteElementsAction.create(this.copyElements, targetPosition), CommitModelAction.create()];
}
}

export interface PasteElementsAction extends Action {
kind: typeof PasteElementsAction.KIND;
copyElements: SModelElementImpl[];
targetPosition: Point;
}
export namespace PasteElementsAction {
export const KIND = "paste-clipboard-elements";
export function create(copyElements: SModelElementImpl[], targetPosition: Point): PasteElementsAction {
return {
kind: KIND,
copyElements,
targetPosition,
};
}
}

/**
* This command is used to paste elements that were copied by the CopyPasteFeature.
* It creates new elements and copies the properties of the copied elements.
* This is done inside a command, so that it can be undone/redone.
*/
@injectable()
export class PasteElementsCommand extends Command {
public static readonly KIND = PasteElementsAction.KIND;

private newElements: SChildElementImpl[] = [];
// This maps the element id of the copy source element to the
// id that the newly created copy target element has.
private copyElementIdMapping: Record<string, string> = {};

constructor(
@inject(TYPES.Action) private readonly action: PasteElementsAction,
@inject(SETTINGS.Mode) private readonly editorModeController: EditorModeController,
) {
super();
}

/**
* Calculates the offset between the copy source elements and the set paste target position.
* Does this by finding the top left position of the copy source elements and subtracting it from the target position.
*
* @returns The offset between the top left position of the copy source elements and the target position.
*/
private computeElementOffset(): Point {
const sourcePosition = { x: Infinity, y: Infinity };

this.action.copyElements.forEach((element) => {
if (!(element instanceof SNodeImpl)) {
return;
}

if (element.position.x < sourcePosition.x) {
sourcePosition.x = element.position.x;
}
if (element.position.y < sourcePosition.y) {
sourcePosition.y = element.position.y;
}
});

if (sourcePosition.x === Infinity || sourcePosition.y === Infinity) {
return { x: 0, y: 0 };
}

// Compute delta between top left position of copy source elements and the target position
return Point.subtract(this.action.targetPosition, sourcePosition);
}

execute(context: CommandExecutionContext): CommandReturn {
if (this.editorModeController?.isReadOnly()) {
return context.root;
}

// Step 1: copy nodes and their ports
const positionOffset = this.computeElementOffset();
this.action.copyElements.forEach((element) => {
if (!(element instanceof SNodeImpl)) {
return;
}

// createSchema only does a shallow copy, so we need to do an additional deep copy here because
// we want to support copying elements with objects and arrays in them.
const schema = JSON.parse(JSON.stringify(context.modelFactory.createSchema(element))) as SModelElement;

schema.id = generateRandomSprottyId();
this.copyElementIdMapping[element.id] = schema.id;
if ("position" in schema) {
schema.position = Point.add(element.position, positionOffset);
}

if (element instanceof DfdNodeImpl) {
// Special case for DfdNodes: copy ports and give the nodes new ids.
(schema as DfdNode).ports.forEach((port) => {
const oldPortId = port.id;
port.id = generateRandomSprottyId();
this.copyElementIdMapping[oldPortId] = port.id;
});
}

const newElement = context.modelFactory.createElement(schema);
this.newElements.push(newElement);
});

// Step 2: copy edges
// If the source and target element of an edge are copied, the edge can be copied as well.
// If only one of them is copied, the edge is not copied.
this.action.copyElements.forEach((element) => {
if (!(element instanceof SEdgeImpl)) {
return;
}

const newSourceId = this.copyElementIdMapping[element.sourceId];
const newTargetId = this.copyElementIdMapping[element.targetId];

if (!newSourceId || !newTargetId) {
// Not both source and target are copied, ignore this edge
return;
}

const schema = JSON.parse(JSON.stringify(context.modelFactory.createSchema(element))) as SEdge;
LoadJsonCommand.preprocessModelSchema(schema);

schema.id = generateRandomSprottyId();
this.copyElementIdMapping[element.id] = schema.id;

schema.sourceId = newSourceId;
schema.targetId = newTargetId;

const newElement = context.modelFactory.createElement(schema);
this.newElements.push(newElement);
});

// Step 3: add new elements to the model and select them
this.newElements.forEach((element) => {
context.root.add(element);
});
//this.setSelection(context, "new");

return context.root;
}

undo(context: CommandExecutionContext): CommandReturn {
if (this.editorModeController?.isReadOnly()) {
return context.root;
}

// Remove elements from the model
this.newElements.forEach((element) => {
context.root.remove(element);
});
// Select the old elements
//this.setSelection(context, "old");

return context.root;
}

redo(context: CommandExecutionContext): CommandReturn {
if (this.editorModeController?.isReadOnly()) {
return context.root;
}

this.newElements.forEach((element) => {
context.root.add(element);
});
//this.setSelection(context, "new");

return context.root;
}
}
20 changes: 20 additions & 0 deletions frontend/webEditor/src/keyListeners/di.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { ContainerModule } from "inversify";
import { DeleteKeyListener } from "./deleteKeyListener";
import { CenterKeyboardListener, configureCommand, TYPES } from "sprotty";
import { CopyPasteKeyListener, PasteElementsCommand } from "./copyPasteKeyListener";
import { SerializeKeyListener } from "./serializeKeyListener";
import { FitToScreenKeyListener } from "./fitToScreenKeyListener";

export const keyListenerModule = new ContainerModule((bind, unbind, isBound, rebind) => {
bind(DeleteKeyListener).toSelf().inSingletonScope();
bind(TYPES.KeyListener).toService(DeleteKeyListener);

const context = { bind, unbind, isBound, rebind };
bind(TYPES.KeyListener).to(CopyPasteKeyListener).inSingletonScope();
configureCommand(context, PasteElementsCommand);

bind(TYPES.KeyListener).to(SerializeKeyListener).inSingletonScope();

bind(FitToScreenKeyListener).toSelf().inSingletonScope();
rebind(CenterKeyboardListener).toService(FitToScreenKeyListener);
});
24 changes: 24 additions & 0 deletions frontend/webEditor/src/keyListeners/fitToScreenKeyListener.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { KeyListener, SModelElementImpl } from "sprotty";
import { Action, CenterAction, FitToScreenAction } from "sprotty-protocol";
import { matchesKeystroke } from "sprotty/lib/utils/keyboard";

/**
* Key listener that fits the diagram to the screen when pressing Ctrl+Shift+F
* and centers the diagram when pressing Ctrl+Shift+C.
*
* Custom version of the CenterKeyboardListener from sprotty because that one
* does not allow setting a padding.
*/
export class FitToScreenKeyListener extends KeyListener {
override keyDown(element: SModelElementImpl, event: KeyboardEvent): Action[] {
if (matchesKeystroke(event, "KeyC", "ctrlCmd", "shift")) {
return [CenterAction.create([])];
}

if (matchesKeystroke(event, "KeyF", "ctrlCmd", "shift")) {
return [FitToScreenAction.create([element.root.id])];
}

return [];
}
}
31 changes: 31 additions & 0 deletions frontend/webEditor/src/keyListeners/serializeKeyListener.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { injectable } from "inversify";
import { KeyListener, SModelElementImpl, CommitModelAction } from "sprotty";
import { Action } from "sprotty-protocol";
import { matchesKeystroke } from "sprotty/lib/utils/keyboard";
import { LoadDefaultDiagramAction } from "../serialize/loadDefaultDiagram";
import { LoadJsonFileAction } from "../serialize/loadJsonFile";
import { SaveJsonFileAction } from "../serialize/saveJsonFile";
import { AnalyzeAction } from "../serialize/analyze";

@injectable()
export class SerializeKeyListener extends KeyListener {
keyDown(_element: SModelElementImpl, event: KeyboardEvent): Action[] {
if (matchesKeystroke(event, "KeyO", "ctrlCmd")) {
// Prevent the browser file open dialog from opening
event.preventDefault();

return [LoadJsonFileAction.create(), CommitModelAction.create()];
} else if (matchesKeystroke(event, "KeyO", "ctrlCmd", "shift")) {
event.preventDefault();
return [LoadDefaultDiagramAction.create(), CommitModelAction.create()];
} else if (matchesKeystroke(event, "KeyS", "ctrlCmd")) {
event.preventDefault();
return [SaveJsonFileAction.create()];
} else if (matchesKeystroke(event, "KeyA", "ctrlCmd", "shift")) {
event.preventDefault();
return [AnalyzeAction.create(), CommitModelAction.create()];
}

return [];
}
}
Loading