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

✨ Add Attach - Detach Options to Canvas Context Menu #342

Merged
merged 5 commits into from
Jul 16, 2024
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
3 changes: 3 additions & 0 deletions src/commands/CommandIDs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
94 changes: 94 additions & 0 deletions src/commands/NodeActionCommands.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -928,4 +928,98 @@ export function addNodeActionCommands(

}
}

// Add command to attach selected node
commands.addCommand(commandIDs.attachNode, {
execute: async () => {
MFA-X-AI marked this conversation as resolved.
Show resolved Hide resolved

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')
});

}
21 changes: 21 additions & 0 deletions src/components/port/CustomPortModel.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -365,6 +366,26 @@ export class CustomPortModel extends DefaultPortModel {

}

getTargetPorts = () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Neat, these functions could cut down on some of the repetition I've got in the addHover and removeHover functions, as well as the ones in the copy & paste support.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For this one, I've noticed that the selectAllRelevantNodes also highlights the links, and the addHover - removeHover accesses the properties through their names.

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();
Expand Down
61 changes: 58 additions & 3 deletions src/context-menu/CanvasContextMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -30,6 +31,21 @@ export class CanvasContextMenu extends React.Component<CanvasContextMenuProps> {
});
};

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 (
<div className="context-menu" onClick={this.hideCanvasContextMenu.bind(this)}>
{visibility.showCutCopyPaste && (
Expand All @@ -39,6 +55,15 @@ export class CanvasContextMenu extends React.Component<CanvasContextMenuProps> {
<div className="context-menu-option" onClick={() => this.props.app.commands.execute(commandIDs.pasteNode)}>Paste</div>
</>
)}
{visibility.showAttachNode && (
<div className="context-menu-option" onClick={handleAttachNode}>Attach</div>
)}
{visibility.showAttachAllNodes && (
<div className="context-menu-option" onClick={handleAllAttachNodes}>Attach Literals</div>
)}
{visibility.showDetachAllNodes && (
<div className="context-menu-option" onClick={handleDetachAllNodes}>Detach Literals</div>
)}
{visibility.showReloadNode && (
<div className="context-menu-option" onClick={handleReloadNode}>Reload Node</div>
)}
Expand Down Expand Up @@ -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) {
Expand All @@ -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,
Expand All @@ -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
};
}

Expand Down