Skip to content

Commit

Permalink
Merge pull request #105 from XpressAI/adry/cut-copy-paste
Browse files Browse the repository at this point in the history
✨Enable cut, copy and paste node
  • Loading branch information
MFA-X-AI authored Feb 22, 2022
2 parents 7de8f4d + efcd5ad commit 648c480
Show file tree
Hide file tree
Showing 7 changed files with 197 additions and 32 deletions.
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

0 comments on commit 648c480

Please sign in to comment.