Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

✨Enable cut, copy and paste node #105

Merged
merged 6 commits into from
Feb 22, 2022
Merged
Show file tree
Hide file tree
Changes from all 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
19 changes: 17 additions & 2 deletions schema/plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,30 @@
"jupyter.lab.menus": {
"context": [
{
"command": "Xircuit-editor:edit-node",
"command": "Xircuit-editor:cut-node",
"selector": ".xircuits-editor",
"rank": 0
},
{
"command": "Xircuit-editor:delete-node",
"command": "Xircuit-editor:copy-node",
"selector": ".xircuits-editor",
"rank": 1
},
{
"command": "Xircuit-editor:paste-node",
"selector": ".xircuits-editor",
"rank": 2
},
{
"command": "Xircuit-editor:edit-node",
"selector": ".xircuits-editor",
"rank": 3
},
{
"command": "Xircuit-editor:delete-node",
"selector": ".xircuits-editor",
"rank": 4
},
{
"type": "separator",
"selector": ".xircuits-editor",
Expand Down
147 changes: 146 additions & 1 deletion src/commands/ContextMenu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { CustomNodeModel } from '../components/CustomNodeModel';
import { XPipePanel } from '../xircuitWidget';
import { Dialog, showDialog } from '@jupyterlab/apputils';
import { DefaultLinkModel } from '@projectstorm/react-diagrams';
import { BaseModel, BaseModelGenerics } from '@projectstorm/react-canvas-core';

/**
* Add the commands for the xircuits's context menu.
Expand All @@ -29,6 +30,50 @@ export function addContextMenuCommands(
);
}

//Add command to cut node
commands.addCommand(commandIDs.cutNode, {
execute: cutNode,
label: trans.__('Cut'),
isEnabled: () => {
const widget = tracker.currentWidget?.content as XPipePanel;
const selectedEntities = widget.xircuitsApp.getDiagramEngine().getModel().getSelectedEntities();
let isNodeSelected: boolean;
if (selectedEntities.length > 0) {
isNodeSelected = true
}
return isNodeSelected ?? false;
}
});

//Add command to copy node
commands.addCommand(commandIDs.copyNode, {
execute: copyNode,
label: trans.__('Copy'),
isEnabled: () => {
const widget = tracker.currentWidget?.content as XPipePanel;
const selectedEntities = widget.xircuitsApp.getDiagramEngine().getModel().getSelectedEntities();
let isNodeSelected: boolean;
if (selectedEntities.length > 0) {
isNodeSelected = true
}
return isNodeSelected ?? false;
}
});

//Add command to paste node
commands.addCommand(commandIDs.pasteNode, {
execute: pasteNode,
label: trans.__('Paste'),
isEnabled: () => {
const clipboard = JSON.parse(localStorage.getItem('clipboard'));
let isClipboardFilled: boolean
if (clipboard) {
isClipboardFilled = true
}
return isClipboardFilled ?? false;
}
});

//Add command to edit literal component
commands.addCommand(commandIDs.editNode, {
execute: editLiteral,
Expand Down Expand Up @@ -61,6 +106,106 @@ export function addContextMenuCommands(
}
});

function cutNode(): void {
const widget = tracker.currentWidget?.content as XPipePanel;

if (widget) {
const engine = widget.xircuitsApp.getDiagramEngine();
const selected = widget.xircuitsApp.getDiagramEngine().getModel().getSelectedEntities()
const copies = selected.map(entity =>
entity.clone().serialize()
);

// TODO: Need to make this event working to be on the command manager, so the user can undo
// and redo it.
// engine.fireEvent(
// {
// nodes: selected,
// links: selected.reduce(
// (arr, node) => [...arr, ...node.getAllLinks()],
// [],
// ),
// },
// 'entitiesRemoved',
// );
selected.forEach(node => node.remove());
engine.repaintCanvas();

localStorage.setItem('clipboard', JSON.stringify(copies));

}
}

function copyNode(): void {
const widget = tracker.currentWidget?.content as XPipePanel;

if (widget) {
const copies = widget.xircuitsApp.getDiagramEngine().getModel().getSelectedEntities().map(entity =>
entity.clone().serialize(),
);

localStorage.setItem('clipboard', JSON.stringify(copies));

}
}

function pasteNode(): void {
const widget = tracker.currentWidget?.content as XPipePanel;

if (widget) {
const engine = widget.xircuitsApp.getDiagramEngine();
const model = widget.xircuitsApp.getDiagramEngine().getModel();

const clipboard = JSON.parse(localStorage.getItem('clipboard'));
if (!clipboard) return;

model.clearSelection();

const models = clipboard.map(serialized => {
const modelInstance = model
.getActiveNodeLayer()
.getChildModelFactoryBank(engine)
.getFactory(serialized.type)
.generateModel({ initialConfig: serialized });

modelInstance.deserialize({
engine: engine,
data: serialized,
registerModel: () => { },
getModel: function <T extends BaseModel<BaseModelGenerics>>(id: string): Promise<T> {
throw new Error('Function not implemented.');
}
});

return modelInstance;
});

models.forEach(modelInstance => {
const oldX = modelInstance.getX();
const oldY = modelInstance.getY();

modelInstance.setPosition(oldX + 10, oldY + 10)
model.addNode(modelInstance);
// Remove any empty/default node
if(modelInstance.getOptions()['type'] == 'default') model.removeNode(modelInstance)
modelInstance.setSelected(true);
});

localStorage.setItem(
'clipboard',
JSON.stringify(
models.map(modelInstance =>
modelInstance.clone().serialize(),
),
),
);
// TODO: Need to make this event working to be on the command manager, so the user can undo
// and redo it.
// engine.fireEvent({ nodes: models }, 'componentsAdded');
widget.xircuitsApp.getDiagramEngine().repaintCanvas();
}
}

function editLiteral(): void {
const widget = tracker.currentWidget?.content as XPipePanel;

Expand All @@ -74,7 +219,7 @@ export function addContextMenuCommands(

// Prompt the user to enter new value
let theResponse = window.prompt('Enter New Value (Without Quotes):', oldValue);
if(theResponse == null || theResponse == "" || theResponse == oldValue){
if (theResponse == null || theResponse == "" || theResponse == oldValue) {
// When Cancel is clicked or no input provided, just return
return
}
Expand Down
27 changes: 27 additions & 0 deletions src/commands/CustomActionEvent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { Action, ActionEvent, InputType } from '@projectstorm/react-canvas-core';
import { JupyterFrontEnd } from '@jupyterlab/application';
import { commandIDs } from '../components/xircuitBodyWidget';

interface CustomActionEventOptions {
app: JupyterFrontEnd;
}

export class CustomActionEvent extends Action {
constructor(options: CustomActionEventOptions) {
super({
type: InputType.KEY_DOWN,
fire: (event: ActionEvent<React.KeyboardEvent>) => {
const app = options.app;
const keyCode = event.event.key;
const ctrlKey = event.event.ctrlKey;

// Comment this first until the TODO below is fix
// if (ctrlKey && keyCode === 'x') app.commands.execute(commandIDs.cutNode);
// if (ctrlKey && keyCode === 'c') app.commands.execute(commandIDs.copyNode);
// TODO: Fix this paste issue where it paste multiple times.
// if (ctrlKey && keyCode === 'v') app.commands.execute(commandIDs.pasteNode);
if (keyCode == 'Delete' || keyCode == 'Backspace') app.commands.execute(commandIDs.deleteNode);
}
});
}
}
26 changes: 0 additions & 26 deletions src/components/CustomNodeWidget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import ImageGallery from 'react-image-gallery';
import ToolTip from 'react-portal-tooltip';
import { Pagination } from "krc-pagination";
import 'krc-pagination/styles.css';
import { Action, ActionEvent, InputType } from '@projectstorm/react-canvas-core';
import Toggle from 'react-toggle'
import { JupyterFrontEnd } from '@jupyterlab/application';
import { commandIDs } from './xircuitBodyWidget';
Expand Down Expand Up @@ -69,31 +68,6 @@ export interface DefaultNodeProps {
app: JupyterFrontEnd;
}

interface CustomDeleteItemsActionOptions {
keyCodes?: number[];
customDelete?: CustomNodeWidget;
app: JupyterFrontEnd;
}

export class CustomDeleteItemsAction extends Action {
constructor(options: CustomDeleteItemsActionOptions) {
options = {
keyCodes: [46, 8],
...options
};

super({
type: InputType.KEY_DOWN,
fire: (event: ActionEvent<React.KeyboardEvent>) => {
if (options.keyCodes.indexOf(event.event.keyCode) !== -1) {
const app = options.app;
app.commands.execute(commandIDs.deleteNode)
}
}
});
}
}

/**
* Default node that models the DefaultNodeModel. It creates two columns
* for both all the input ports on the left, and the output ports on the right.
Expand Down
4 changes: 2 additions & 2 deletions src/components/XircuitsApp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import * as SRD from '@projectstorm/react-diagrams';
import { CustomNodeFactory } from "./CustomNodeFactory";
import { CustomNodeModel } from './CustomNodeModel';
import { ZoomCanvasAction } from '@projectstorm/react-canvas-core';
import { CustomDeleteItemsAction } from './CustomNodeWidget';
import { CustomActionEvent } from '../commands/CustomActionEvent';
import { JupyterFrontEnd } from '@jupyterlab/application';

export class XircuitsApplication {
Expand All @@ -17,7 +17,7 @@ export class XircuitsApplication {
this.activeModel = new SRD.DiagramModel();
this.diagramEngine.getNodeFactories().registerFactory(new CustomNodeFactory(app));
this.diagramEngine.getActionEventBus().registerAction(new ZoomCanvasAction({ inverseZoom: true }))
this.diagramEngine.getActionEventBus().registerAction(new CustomDeleteItemsAction({ app }));
this.diagramEngine.getActionEventBus().registerAction(new CustomActionEvent({ app }));

let startNode = new CustomNodeModel({ name: 'Start', color: 'rgb(255,102,102)', extras: { "type": "Start" } });
startNode.addOutPortEnhance('▶', 'out-0');
Expand Down
3 changes: 3 additions & 0 deletions src/components/xircuitBodyWidget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,9 @@ export const commandIDs = {
runXircuit: 'Xircuit-editor:run-node',
debugXircuit: 'Xircuit-editor:debug-node',
lockXircuit: 'Xircuit-editor:lock-node',
cutNode: 'Xircuit-editor:cut-node',
copyNode: 'Xircuit-editor:copy-node',
pasteNode: 'Xircuit-editor:paste-node',
editNode: 'Xircuit-editor:edit-node',
deleteNode: 'Xircuit-editor:delete-node',
createArbitraryFile: 'Xircuit-editor:create-arbitrary-file',
Expand Down
3 changes: 2 additions & 1 deletion src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ const xircuits: JupyterFrontEndPlugin<void> = {

// Add a command for creating a new xircuits file.
app.commands.addCommand(commandIDs.createNewXircuit, {
label: 'Create New Xircuits',
label: (args) => (args['isLauncher'] ? 'Xircuits File' : 'Create New Xircuits'),
icon: xircuitsIcon,
caption: 'Create a new xircuits file',
execute: () => {
Expand Down Expand Up @@ -366,6 +366,7 @@ const xircuits: JupyterFrontEndPlugin<void> = {
launcher.add({
command: commandIDs.createNewXircuit,
rank: 1,
args: { isLauncher: true },
category: 'Other'
});
}
Expand Down