Skip to content

Commit

Permalink
Refactor copy and paste into own feature
Browse files Browse the repository at this point in the history
  • Loading branch information
hlxid committed Oct 21, 2023
1 parent 3cd4a60 commit 5026248
Show file tree
Hide file tree
Showing 5 changed files with 88 additions and 74 deletions.
10 changes: 4 additions & 6 deletions src/common/di.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,10 @@ import { DeleteKeyListener } from "./deleteKeyListener";
import { EDITOR_TYPES } from "../utils";
import { DynamicChildrenProcessor } from "../features/dfdElements/dynamicChildren";
import { FitToScreenKeyListener as CenterDiagramKeyListener } from "./fitToScreenKeyListener";
import { CopyPasteFeature, PasteClipboardCommand } from "./copyPaste";

import "./commonStyling.css";

export const dfdCommonModule = new ContainerModule((bind, unbind, isBound, rebind) => {
const context = { bind, unbind, isBound, rebind };

bind(ServerCommandPaletteActionProvider).toSelf().inSingletonScope();
bind(TYPES.ICommandPaletteActionProvider).toService(ServerCommandPaletteActionProvider);

Expand All @@ -31,9 +28,6 @@ export const dfdCommonModule = new ContainerModule((bind, unbind, isBound, rebin
bind(CenterDiagramKeyListener).toSelf().inSingletonScope();
rebind(CenterKeyboardListener).toService(CenterDiagramKeyListener);

bind(TYPES.KeyListener).to(CopyPasteFeature).inSingletonScope();
configureCommand(context, PasteClipboardCommand);

bind(HelpUI).toSelf().inSingletonScope();
bind(TYPES.IUIExtension).toService(HelpUI);
bind(EDITOR_TYPES.DefaultUIElement).toService(HelpUI);
Expand All @@ -45,8 +39,12 @@ export const dfdCommonModule = new ContainerModule((bind, unbind, isBound, rebin
bind(DynamicChildrenProcessor).toSelf().inSingletonScope();

// For some reason the CreateElementAction and Command exist but in no sprotty module is the command registered, so we need to do this here.
const context = { bind, unbind, isBound, rebind };
configureCommand(context, CreateElementCommand);

// Configure zoom limits
// Without these you could zoom in/out to infinity by accident resulting in your diagram being "gone".
// You can still get back to the diagram using the fit to screen action but these zoom limits prevents this from happening in the most cases.
configureViewerOptions(context, {
zoomLimits: { min: 0.05, max: 20 },
});
Expand Down
16 changes: 16 additions & 0 deletions src/features/copyPaste/di.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { ContainerModule } from "inversify";
import { TYPES, configureCommand } from "sprotty";
import { CopyPasteKeyListener } from "./keyListener";
import { PasteElementsCommand } from "./pasteCommand";

/**
* This feature allows the user to copy and paste elements.
* When ctrl+c is pressed, all selected elements are copied into an internal array.
* When ctrl+v is pressed, all elements in the internal array are pasted with an fixed offset.
* Nodes are copied with their ports and edges are copied if source and target were copied as well.
*/
export const copyPasteModule = new ContainerModule((bind, unbind, isBound, rebind) => {
const context = { bind, unbind, isBound, rebind };
bind(TYPES.KeyListener).to(CopyPasteKeyListener).inSingletonScope();
configureCommand(context, PasteElementsCommand);
});
53 changes: 53 additions & 0 deletions src/features/copyPaste/keyListener.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { injectable } from "inversify";
import { PasteElementsAction } from "./pasteCommand";
import { CommitModelAction, KeyListener, SModelElementImpl, SModelRootImpl, isSelected } from "sprotty";
import { Action } from "sprotty-protocol";
import { matchesKeystroke } from "sprotty/lib/utils/keyboard";

/**
* 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[] = [];

keyUp(_element: SModelElementImpl, _event: KeyboardEvent): 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[] {
return [PasteElementsAction.create(this.copyElements), CommitModelAction.create()];
}
}
81 changes: 13 additions & 68 deletions src/common/copyPaste.ts → src/features/copyPaste/pasteCommand.ts
Original file line number Diff line number Diff line change
@@ -1,81 +1,28 @@
import { inject, injectable } from "inversify";
import {
Command,
CommandExecutionContext,
CommandReturn,
CommitModelAction,
KeyListener,
SChildElementImpl,
SEdgeImpl,
SModelElementImpl,
SModelRootImpl,
SNodeImpl,
TYPES,
isSelectable,
isSelected,
} from "sprotty";
import { DynamicChildrenProcessor } from "../dfdElements/dynamicChildren";
import { generateRandomSprottyId } from "../../utils";
import { DfdNode, DfdNodeImpl } from "../dfdElements/nodes";
import { Action, SPort } from "sprotty-protocol";
import { matchesKeystroke } from "sprotty/lib/utils/keyboard";
import { generateRandomSprottyId } from "../utils";
import { DynamicChildrenProcessor } from "../features/dfdElements/dynamicChildren";
import { injectable, inject } from "inversify";
import { DfdNode, DfdNodeImpl } from "../features/dfdElements/nodes";
import { ArrowEdge, ArrowEdgeImpl } from "../features/dfdElements/edges";
import { ArrowEdge, ArrowEdgeImpl } from "../dfdElements/edges";

/**
* This feature allows the user to copy and paste elements.
* When ctrl+c is pressed, all selected elements are copied into an internal array.
* When ctrl+v is pressed, all elements in the internal array are pasted with an fixed offset.
* Nodes are copied with their ports and edges are copied if source and target were copied as well.
*/
@injectable()
export class CopyPasteFeature implements KeyListener {
private copyElements: SModelElementImpl[] = [];

keyUp(_element: SModelElementImpl, _event: KeyboardEvent): 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[] {
return [PasteClipboardAction.create(this.copyElements), CommitModelAction.create()];
}
}

interface PasteClipboardAction extends Action {
kind: typeof PasteClipboardAction.KIND;
export interface PasteElementsAction extends Action {
kind: typeof PasteElementsAction.KIND;
copyElements: SModelElementImpl[];
}
export namespace PasteClipboardAction {
export const KIND = "paste-clipboard";
export function create(copyElements: SModelElementImpl[]): PasteClipboardAction {
export namespace PasteElementsAction {
export const KIND = "paste-clipboard-elements";
export function create(copyElements: SModelElementImpl[]): PasteElementsAction {
return {
kind: KIND,
copyElements,
Expand All @@ -89,8 +36,8 @@ export namespace PasteClipboardAction {
* This is done inside a command, so that it can be undone/redone.
*/
@injectable()
export class PasteClipboardCommand extends Command {
public static readonly KIND = PasteClipboardAction.KIND;
export class PasteElementsCommand extends Command {
public static readonly KIND = PasteElementsAction.KIND;

@inject(DynamicChildrenProcessor)
private dynamicChildrenProcessor: DynamicChildrenProcessor = new DynamicChildrenProcessor();
Expand All @@ -99,7 +46,7 @@ export class PasteClipboardCommand extends Command {
// id that the newly created copy target element has.
private copyElementIdMapping: Record<string, string> = {};

constructor(@inject(TYPES.Action) private readonly action: PasteClipboardAction) {
constructor(@inject(TYPES.Action) private readonly action: PasteElementsAction) {
super();
}

Expand Down Expand Up @@ -176,8 +123,6 @@ export class PasteClipboardCommand extends Command {
const newSourceId = this.copyElementIdMapping[element.sourceId];
const newTargetId = this.copyElementIdMapping[element.targetId];

console.log("edge", newSourceId, newTargetId, element);

if (!newSourceId || !newTargetId) {
// Not both source and target are copied, ignore this edge
return;
Expand Down
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import { toolPaletteModule } from "./features/toolPalette/di.config";
import { serializeModule } from "./features/serialize/di.config";
import { LoadDefaultDiagramAction } from "./features/serialize/loadDefaultDiagram";
import { dfdElementsModule } from "./features/dfdElements/di.config";
import { copyPasteModule } from "./features/copyPaste/di.config";
import { EDITOR_TYPES } from "./utils";

import "sprotty/css/sprotty.css";
Expand Down Expand Up @@ -82,6 +83,7 @@ container.load(
serializeModule,
dfdLabelModule,
toolPaletteModule,
copyPasteModule,
);

const dispatcher = container.get<ActionDispatcher>(TYPES.IActionDispatcher);
Expand Down

0 comments on commit 5026248

Please sign in to comment.