diff --git a/meshroom/core/graph.py b/meshroom/core/graph.py index cedabe6e..9ee928e3 100644 --- a/meshroom/core/graph.py +++ b/meshroom/core/graph.py @@ -362,45 +362,15 @@ class Graph(BaseObject): child.resetValue() return node, skippedEdges - def duplicateNode(self, srcNode): - """ Duplicate a node in the graph with its connections. + def duplicateNodes(self, srcNodes): + """ Duplicate nodes in the graph with their connections. Args: - srcNode: the node to duplicate - - Returns: - Node: the created node - """ - node, edges = self.copyNode(srcNode, withEdges=True) - return self.addNode(node) - - def duplicateNodesFromNode(self, fromNode): - """ - Duplicate 'fromNode' and all the following nodes towards graph's leaves. - - Args: - fromNode (Node): the node to start the duplication from + srcNodes: the nodes to duplicate Returns: OrderedDict[Node, Node]: the source->duplicate map """ - srcNodes, srcEdges = self.dfsOnDiscover(startNodes=[fromNode], reverse=True, dependenciesOnly=True) - return self.duplicateNodes(srcNodes, srcEdges) - - def duplicateNodesFromList(self, nodes): - """ - Duplicate 'nodes'. - - Args: - nodes (list[Node]): the nodes to duplicate - - Returns: - OrderedDict[Node, Node]: the source->duplicate map - """ - srcEdges = [ self.nodeInEdges(n) for n in nodes ] - return self.duplicateNodes(nodes, srcEdges) - - def duplicateNodes(self, srcNodes, srcEdges): # use OrderedDict to keep duplicated nodes creation order duplicates = OrderedDict() diff --git a/meshroom/ui/commands.py b/meshroom/ui/commands.py index b70dcff5..f0a2324d 100755 --- a/meshroom/ui/commands.py +++ b/meshroom/ui/commands.py @@ -159,7 +159,7 @@ class RemoveNodeCommand(GraphCommand): def redoImpl(self): # only keep outEdges since inEdges are serialized in nodeDict - inEdges, self.outEdges = self.graph.removeNode(self.nodeName) + _, self.outEdges = self.graph.removeNode(self.nodeName) return True def undoImpl(self): @@ -173,55 +173,26 @@ class RemoveNodeCommand(GraphCommand): self.graph.attribute(dstAttr)) -class _DuplicateNodes(GraphCommand): - def __init__(self, graph, parent=None): - super(_DuplicateNodes, self).__init__(graph, parent) - self.duplicates = [] - - def undoImpl(self): - # delete all the duplicated nodes - for nodeName in self.duplicates: - self.graph.removeNode(nodeName) - - -class DuplicateNodeCommand(_DuplicateNodes): - """ - Handle node duplication in a Graph. - """ - def __init__(self, graph, srcNode, duplicateFollowingNodes, parent=None): - super(DuplicateNodeCommand, self).__init__(graph, parent) - self.srcNodeName = srcNode.name - self.duplicateFollowingNodes = duplicateFollowingNodes - - def redoImpl(self): - srcNode = self.graph.node(self.srcNodeName) - - if self.duplicateFollowingNodes: - duplicates = list(self.graph.duplicateNodesFromNode(srcNode).values()) - self.setText("Duplicate {} nodes from {}".format(len(duplicates), self.srcNodeName)) - else: - duplicates = [self.graph.duplicateNode(srcNode)] - self.setText("Duplicate {}".format(self.srcNodeName)) - - self.duplicates = [n.name for n in duplicates] - return duplicates - - -class DuplicateNodeListCommand(_DuplicateNodes): +class DuplicateNodesCommand(GraphCommand): """ Handle node duplication in a Graph. """ def __init__(self, graph, srcNodes, parent=None): - super(DuplicateNodeListCommand, self).__init__(graph, parent) - self.srcNodeNames = [ srcNode.name for srcNode in srcNodes ] - self.setText("Duplicate selected nodes") + super(DuplicateNodesCommand, self).__init__(graph, parent) + self.srcNodeNames = [ n.name for n in srcNodes ] + self.setText("Duplicate Nodes") def redoImpl(self): - srcNodes = [ self.graph.node(srcNodeName) for srcNodeName in self.srcNodeNames ] - duplicates = list(self.graph.duplicateNodesFromList(srcNodes).values()) + srcNodes = [ self.graph.node(i) for i in self.srcNodeNames ] + duplicates = list(self.graph.duplicateNodes(srcNodes).values()) self.duplicates = [ n.name for n in duplicates ] return duplicates + def undoImpl(self): + # remove all duplicates + for duplicate in self.duplicates: + self.graph.removeNode(duplicate) + class SetAttributeCommand(GraphCommand): def __init__(self, graph, attribute, value, parent=None): diff --git a/meshroom/ui/graph.py b/meshroom/ui/graph.py index 63fba55b..c9c98421 100644 --- a/meshroom/ui/graph.py +++ b/meshroom/ui/graph.py @@ -500,53 +500,52 @@ class UIGraph(QObject): position = Position(position.x(), position.y()) return self.push(commands.AddNodeCommand(self._graph, nodeType, position=position, **kwargs)) - @Slot(QObject, result=bool) - def nodeSelection(self, node): - """ If the node is part of the selection or not. """ - length = len(self._selectedNodes) > 1 - return length and self._selectedNodes.contains(node) if node else length + def filterNodes(self, nodes): + """Filter out the nodes that do not exist on the graph.""" + return [ n for n in nodes if n in self._graph.nodes.values() ] - @Slot(Node, QPoint) - def moveNode(self, node, position): + @Slot(Node, QPoint, QObject) + def moveNode(self, node, position, nodes=None): """ - Move 'node' to the given 'position'. + Move 'node' to the given 'position' and also update the positions of 'nodes' if neccessary. Args: node (Node): the node to move position (QPoint): the target position + nodes (list[Node]): the nodes to update the position of """ + if not nodes: + nodes = [node] + nodes = self.filterNodes(nodes) if isinstance(position, QPoint): - if self.nodeSelection(node): - self.moveSelectedNodes(position.x() - node.x, position.y() - node.y) - return position = Position(position.x(), position.y()) - self.push(commands.MoveNodeCommand(self._graph, node, position)) - - def moveSelectedNodes(self, deltaX, deltaY): + deltaX = position.x - node.x + deltaY = position.y - node.y with self.groupedGraphModification("Move Selected Nodes"): - for node in self._selectedNodes: - position = Position(node.x + deltaX, node.y + deltaY) - self.push(commands.MoveNodeCommand(self._graph, node, position)) + for n in nodes: + position = Position(n.x + deltaX, n.y + deltaY) + self.push(commands.MoveNodeCommand(self._graph, n, position)) - @Slot(Node) - def removeNode(self, node): - if self.nodeSelection(node): - self.removeSelectedNodes() - return - if node.locked: - return - self.push(commands.RemoveNodeCommand(self._graph, node)) + @Slot(QObject) + def removeNodes(self, nodes): + """ + Remove 'nodes' from the graph. - def removeSelectedNodes(self): + Args: + nodes (list[Node]): the nodes to remove + """ + nodes = self.filterNodes(nodes) + if any([ n.locked for n in nodes ]): + return with self.groupedGraphModification("Remove Selected Nodes"): - for node in self._selectedNodes: - if not node.locked: - self.push(commands.RemoveNodeCommand(self._graph, node)) + for node in nodes: + self.push(commands.RemoveNodeCommand(self._graph, node)) @Slot(Node) def removeNodesFrom(self, startNode): """ Remove all nodes starting from 'startNode' to graph leaves. + Args: startNode (Node): the node to start from. """ @@ -556,9 +555,52 @@ class UIGraph(QObject): nodes, _ = self._graph.dfsOnDiscover(startNodes=[startNode], reverse=True, dependenciesOnly=True) # Perform nodes removal from leaves to start node so that edges # can be re-created in correct order on redo. - for node in reversed(nodes): - if not node.locked: - self.removeNode(node) + self.removeNodes(list(reversed(nodes))) + + @Slot(QObject, result="QVariantList") + def duplicateNodes(self, nodes): + """ + Duplicate 'nodes'. + + Args: + nodes (list[Node]): the nodes to duplicate + Returns: + list[Node]: the list of duplicated nodes + """ + nodes = self.filterNodes(nodes) + # enable updates between duplication and layout to get correct depths during layout + with self.groupedGraphModification("Duplicate Selected Nodes", disableUpdates=False): + # disable graph updates during duplication + with self.groupedGraphModification("Node duplication", disableUpdates=True): + duplicates = self.push(commands.DuplicateNodesCommand(self._graph, nodes)) + # move nodes below the bounding box formed by the duplicated node(s) + bbox = self._layout.boundingBox(duplicates) + for n in duplicates: + self.moveNode(n, Position(n.x, bbox[3] + self.layout.gridSpacing + n.y)) + return duplicates + + @Slot(Node, result="QVariantList") + def duplicateNodesFrom(self, startNode): + """ + Duplicate all nodes starting from 'startNode' to graph leaves. + + Args: + startNode (Node): the node to start from. + Returns: + list[Node]: the list of duplicated nodes + """ + if not startNode: + return + with self.groupedGraphModification("Duplicate Nodes from {}".format(startNode.name)): + nodes, _ = self._graph.dfsOnDiscover(startNodes=[startNode], reverse=True, dependenciesOnly=True) + duplicates = self.duplicateNodes(nodes) + return duplicates + + @Slot(QObject) + def clearData(self, nodes): + nodes = self.filterNodes(nodes) + for n in nodes: + n.clearData() @Slot(Attribute, Attribute) def addEdge(self, src, dst): @@ -587,45 +629,6 @@ class UIGraph(QObject): """ Reset 'attribute' to its default value """ self.push(commands.SetAttributeCommand(self._graph, attribute, attribute.defaultValue())) - @Slot(Node, bool, result="QVariantList") - def duplicateNode(self, srcNode, duplicateFollowingNodes=False): - """ - Duplicate a node and optionally all the following nodes to graph leaves. - - Args: - srcNode (Node): node to start the duplication from - duplicateFollowingNodes (bool): whether to duplicate all the following nodes to graph leaves - - Returns: - [Nodes]: the list of duplicated nodes - """ - if duplicateFollowingNodes: title = "Duplicate Nodes from {}" - elif self.nodeSelection(srcNode): title = "Duplicate selected nodes" - else: title = "Duplicate {}" - # enable updates between duplication and layout to get correct depths during layout - with self.groupedGraphModification(title.format(srcNode.name), disableUpdates=False): - # disable graph updates during duplication - with self.groupedGraphModification("Node duplication", disableUpdates=True): - if self.nodeSelection(srcNode) and not duplicateFollowingNodes: - command = commands.DuplicateNodeListCommand(self._graph, self._selectedNodes) - else: - command = commands.DuplicateNodeCommand(self._graph, srcNode, duplicateFollowingNodes) - duplicates = self.push(command) - # move nodes below the bounding box formed by the duplicated node(s) - bbox = self._layout.boundingBox(duplicates) - for n in duplicates: - self.moveNode(n, Position(n.x, bbox[3] + self.layout.gridSpacing + n.y)) - - return duplicates - - @Slot(QObject) - def clearData(self, node): - if self.nodeSelection(node): - for n in self._selectedNodes: - n.clearData() - return - node.clearData() - @Slot(CompatibilityNode, result=Node) def upgradeNode(self, node): """ Upgrade a CompatibilityNode. """ diff --git a/meshroom/ui/qml/GraphEditor/GraphEditor.qml b/meshroom/ui/qml/GraphEditor/GraphEditor.qml index d2f2bf01..468459fb 100755 --- a/meshroom/ui/qml/GraphEditor/GraphEditor.qml +++ b/meshroom/ui/qml/GraphEditor/GraphEditor.qml @@ -55,11 +55,19 @@ Item { function selectNode(node) { uigraph.selectedNode = node + if (!uigraph.selectedNodes.contains(node) && node !== null) { + uigraph.selectedNodes.append(node) + uigraph.selectedNodesChanged() + } } /// Duplicate a node and optionnally all the following ones function duplicateNode(node, duplicateFollowingNodes) { - var nodes = uigraph.duplicateNode(node, duplicateFollowingNodes) + if (duplicateFollowingNodes) { + var nodes = uigraph.duplicateNodesFrom(node) + } else { + var nodes = uigraph.duplicateNodes(uigraph.selectedNodes) + } selectNode(nodes[0]) } @@ -71,7 +79,7 @@ Item { if(event.modifiers == Qt.AltModifier) uigraph.removeNodesFrom(uigraph.selectedNode) else - uigraph.removeNode(uigraph.selectedNode) + uigraph.removeNodes(uigraph.selectedNodes) } MouseArea { @@ -288,6 +296,7 @@ Item { property bool canComputeNode: currentNode != null && uigraph.graph.canCompute(currentNode) //canSubmitOrCompute: return int n : 0 >= n <= 3 | n=0 cannot submit or compute | n=1 can compute | n=2 can submit | n=3 can compute & submit property int canSubmitOrCompute: currentNode != null && uigraph.graph.canSubmitOrCompute(currentNode) + width: 220 onClosed: currentNode = null MenuItem { @@ -324,7 +333,7 @@ Item { } MenuSeparator {} MenuItem { - text: "Duplicate Node" + (duplicateFollowingButton.hovered ? "s From Here" : uigraph.nodeSelection(nodeMenu.currentNode) ? "s" : "") + text: "Duplicate Node(s)" + (duplicateFollowingButton.hovered ? " From Here" : "") enabled: true onTriggered: duplicateNode(nodeMenu.currentNode, false) MaterialToolButton { @@ -339,9 +348,9 @@ Item { } } MenuItem { - text: "Remove Node" + (removeFollowingButton.hovered ? "s From Here" : uigraph.nodeSelection(nodeMenu.currentNode) ? "s" : "") + text: "Remove Node(s)" + (removeFollowingButton.hovered ? " From Here" : "") enabled: nodeMenu.currentNode ? !nodeMenu.currentNode.locked : false - onTriggered: uigraph.removeNode(nodeMenu.currentNode) + onTriggered: uigraph.removeNodes(uigraph.selectedNodes) MaterialToolButton { id: removeFollowingButton height: parent.height @@ -409,7 +418,7 @@ Item { if(deleteFollowing) graph.clearDataFrom(node); else - uigraph.clearData(node); + uigraph.clearData(uigraph.selectedNodes); } onClosed: destroy() } @@ -447,9 +456,6 @@ Item { uigraph.selectedNodesChanged() selectNode(null) return - } else if (!selected) { - uigraph.selectedNodes.append(node) - uigraph.selectedNodesChanged() } } else if (mouse.modifiers & Qt.AltModifier) { duplicateNode(node, true) @@ -468,7 +474,7 @@ Item { onDoubleClicked: root.nodeDoubleClicked(mouse, node) - onMoved: uigraph.moveNode(node, position) + onMoved: uigraph.moveNode(node, position, uigraph.selectedNodes) onEntered: uigraph.hoveredNode = node onExited: uigraph.hoveredNode = null