diff --git a/src/commands/CommandIDs.tsx b/src/commands/CommandIDs.tsx index bcee2810..f7181e1b 100644 --- a/src/commands/CommandIDs.tsx +++ b/src/commands/CommandIDs.tsx @@ -16,6 +16,9 @@ export const commandIDs = { cutNode: "Xircuit-editor:cut-node", copyNode: "Xircuit-editor:copy-node", pasteNode: "Xircuit-editor:paste-node", + attachNode: "Xircuit-editor:attach-node", + attachAllNodes: "Xircuit-editor:attach-all-nodes", + detachAllNodes: "Xircuit-editor:detach-all-nodes", triggerLoadingAnimation: "Xircuit-editor:trigger-loading-animation", reloadNode: "Xircuit-editor:reload-node", reloadAllNodes: "Xircuit-editor:reload-all-nodes", diff --git a/src/commands/NodeActionCommands.tsx b/src/commands/NodeActionCommands.tsx index a4701b15..5f7c5986 100644 --- a/src/commands/NodeActionCommands.tsx +++ b/src/commands/NodeActionCommands.tsx @@ -928,4 +928,98 @@ export function addNodeActionCommands( } } + + // Add command to attach selected node + commands.addCommand(commandIDs.attachNode, { + execute: async () => { + + const widget = tracker.currentWidget?.content as XircuitsPanel; + const model = widget.xircuitsApp.getDiagramEngine().getModel(); + const selected_entities = model.getSelectedEntities(); + const connected_literals = selected_entities.filter((entity): entity is CustomNodeModel => { + return entity instanceof CustomNodeModel && + entity.getOptions().name.startsWith("Literal ") && + Object.keys(entity.getOutPorts()[0].getLinks()).length > 0; + }); + + connected_literals.forEach(node => { + node.setSelected(false); + node.getOptions().extras.attached = true; + let parameterOutPort = node.getOutPorts()[0] as CustomPortModel; + let connectedNodes = parameterOutPort.getTargetNodes(); + connectedNodes.forEach((node: CustomNodeModel) => node.setSelected(true)) + }); + widget.xircuitsApp.getDiagramEngine().repaintCanvas(); + }, + label: trans.__('attach node') + }); + + // Add command to attach all parameter nodes + commands.addCommand(commandIDs.attachAllNodes, { + execute: async () => { + + const widget = tracker.currentWidget?.content as XircuitsPanel; + const model = widget.xircuitsApp.getDiagramEngine().getModel(); + const selected_entities = model.getSelectedEntities(); + + const literal_nodes = []; + const selected_nodes = selected_entities.filter(entity => entity instanceof NodeModel) as CustomNodeModel[]; + selected_nodes.forEach(node => { + node.setSelected(false); + let inPorts = node.getInPorts(); + Object.values(inPorts).forEach((port: CustomPortModel) => { + let sourceNode = port.getSourceNodes()[0] as CustomNodeModel; + if (sourceNode && sourceNode['name'].startsWith('Literal ') && !sourceNode['extras']['attached']) { + sourceNode.getOptions().extras.attached = true; + sourceNode.setSelected(true); + literal_nodes.push(sourceNode); + } + }) + }); + + literal_nodes.forEach(node => { + let parameterOutPort = node.getOutPorts()[0] as CustomPortModel; + let connectedNodes = parameterOutPort.getTargetNodes(); + connectedNodes.forEach((node: CustomNodeModel) => node.setSelected(true)) + }); + + widget.xircuitsApp.getDiagramEngine().repaintCanvas(); + }, + label: trans.__('attach all nodes') + }); + + // Add command to detach all parameter nodes + commands.addCommand(commandIDs.detachAllNodes, { + execute: async () => { + + const widget = tracker.currentWidget?.content as XircuitsPanel; + const model = widget.xircuitsApp.getDiagramEngine().getModel(); + const selected_entities = model.getSelectedEntities(); + + const literal_nodes = []; + const selected_nodes = selected_entities.filter(entity => entity instanceof NodeModel) as CustomNodeModel[]; + selected_nodes.forEach(node => { + node.setSelected(false); + let inPorts = node.getInPorts(); + Object.values(inPorts).forEach((port: CustomPortModel) => { + let sourceNode = port.getSourceNodes()[0] as CustomNodeModel; + if (sourceNode && sourceNode['name'].startsWith('Literal ') && sourceNode['extras']['attached']) { + sourceNode.getOptions().extras.attached = false; + sourceNode.setSelected(true); + literal_nodes.push(sourceNode); + } + }) + }); + + literal_nodes.forEach(node => { + let parameterOutPort = node.getOutPorts()[0] as CustomPortModel; + let connectedNodes = parameterOutPort.getTargetNodes(); + connectedNodes.forEach((node: CustomNodeModel) => node.setSelected(true)) + }); + + widget.xircuitsApp.getDiagramEngine().repaintCanvas(); + }, + label: trans.__('detach all nodes') + }); + } \ No newline at end of file diff --git a/src/components/port/CustomPortModel.ts b/src/components/port/CustomPortModel.ts index 8f5a7201..dc839d01 100644 --- a/src/components/port/CustomPortModel.ts +++ b/src/components/port/CustomPortModel.ts @@ -1,6 +1,7 @@ import { DefaultPortModel, DefaultPortModelOptions } from "@projectstorm/react-diagrams"; import { DeserializeEvent} from '@projectstorm/react-canvas-core'; import {PortModel} from "@projectstorm/react-diagrams-core"; +import { CustomLinkModel } from "../link/CustomLinkModel"; /** * @author wenfeng xu @@ -365,6 +366,26 @@ export class CustomPortModel extends DefaultPortModel { } + getTargetPorts = () => { + let port: any = this; + return Object.values(port.getLinks()).map((link:CustomLinkModel) => link.getTargetPort()); + } + + getTargetNodes = () => { + let port: any = this; + return Object.values(port.getLinks()).map((link:CustomLinkModel) => link.getTargetPort().getNode()); + } + + getSourcePorts = () => { + let port: any = this; + return Object.values(port.getLinks()).map((link:CustomLinkModel) => link.getSourcePort()); + } + + getSourceNodes = () => { + let port: any = this; + return Object.values(port.getLinks()).map((link:CustomLinkModel) => link.getSourcePort().getNode()); + } + getCustomProps() { const { name, varName, portType, dataType } = this; const id = this.getID(); diff --git a/src/context-menu/CanvasContextMenu.tsx b/src/context-menu/CanvasContextMenu.tsx index 8106200e..2b9749ae 100644 --- a/src/context-menu/CanvasContextMenu.tsx +++ b/src/context-menu/CanvasContextMenu.tsx @@ -5,6 +5,7 @@ import { DiagramEngine, NodeModel, LinkModel } from '@projectstorm/react-diagram import '../../style/ContextMenu.css' import { commandIDs } from "../commands/CommandIDs"; +import { CustomPortModel } from '../components/port/CustomPortModel'; export interface CanvasContextMenuProps { app: JupyterFrontEnd; @@ -30,6 +31,21 @@ export class CanvasContextMenu extends React.Component { }); }; + const handleAttachNode = async () => { + await this.props.app.commands.execute(commandIDs.attachNode); + await this.props.app.commands.execute(commandIDs.reloadNode); + }; + + const handleAllAttachNodes = async () => { + await this.props.app.commands.execute(commandIDs.attachAllNodes); + await this.props.app.commands.execute(commandIDs.reloadNode); + }; + + const handleDetachAllNodes = async () => { + await this.props.app.commands.execute(commandIDs.detachAllNodes); + await this.props.app.commands.execute(commandIDs.reloadNode); + }; + return (
{visibility.showCutCopyPaste && ( @@ -39,6 +55,15 @@ export class CanvasContextMenu extends React.Component {
this.props.app.commands.execute(commandIDs.pasteNode)}>Paste
)} + {visibility.showAttachNode && ( +
Attach
+ )} + {visibility.showAttachAllNodes && ( +
Attach Literals
+ )} + {visibility.showDetachAllNodes && ( +
Detach Literals
+ )} {visibility.showReloadNode && (
Reload Node
)} @@ -79,7 +104,30 @@ export function getMenuOptionsVisibility(models) { } function isComponentNode(node) { - return !isLiteralNode(node) && !isArgumentNode(node); + return node instanceof NodeModel && !isLiteralNode(node) && !isArgumentNode(node); + } + + function isConnected(node): boolean { + let outPorts = node.getOutPorts(); + let inPorts = node.getInPorts(); + return outPorts.some(port => Object.keys(port.getLinks()).length > 0) || + inPorts.some(port => Object.keys(port.getLinks()).length > 0); + } + + function canAttachAllNodes(node) { + let ports = node.getInPorts(); + return ports.some((port) => { + let sourceNode = port.getSourceNodes()[0]; + return sourceNode?.getOptions()?.extras?.attached === false; + }); + } + + function canDetachAllNodes(node) { + let ports = node.getInPorts(); + return ports.some((port) => { + let sourceNode = port.getSourceNodes()[0]; + return sourceNode?.getOptions()?.extras?.attached === true; + }); } function isXircuitsWorkflow(node) { @@ -88,12 +136,16 @@ export function getMenuOptionsVisibility(models) { let isNodeSelected = models.some(model => model instanceof NodeModel); let isLinkSelected = models.some(model => model instanceof LinkModel); + let literalNodes = models.filter(model => isLiteralNode(model)); let parameterNodes = models.filter(model => !isComponentNode(model)); let componentNodes = models.filter(model => isComponentNode(model)); let isSingleParameterNodeSelected = parameterNodes.length === 1; let isSingleComponentNodeSelected = componentNodes.length === 1; let showReloadNode = isNodeSelected && componentNodes.length > 0; - let showopenXircuitsWorkflow = isSingleComponentNodeSelected && models.some(model => isXircuitsWorkflow(model)) + let showopenXircuitsWorkflow = isSingleComponentNodeSelected && models.some(model => isXircuitsWorkflow(model)); + let showAttachNode = literalNodes.length > 0 && literalNodes.some(model => isConnected(model)); + let showAttachAllNodes = componentNodes.some(model => canAttachAllNodes(model)); + let showDetachAllNodes = componentNodes.some(model => canDetachAllNodes(model)); return { showCutCopyPaste: !models.length || isNodeSelected || isLinkSelected, @@ -103,7 +155,10 @@ export function getMenuOptionsVisibility(models) { showopenXircuitsWorkflow: showopenXircuitsWorkflow, showDelete: isNodeSelected || isLinkSelected || parameterNodes.length > 0, showUndoRedo: !models.length, - showAddComment: !models.length + showAddComment: !models.length, + showAttachNode: showAttachNode, + showAttachAllNodes: showAttachAllNodes, + showDetachAllNodes: showDetachAllNodes }; }