From c253e7d36365f83af63b5d26cc6d54de6e3a1d92 Mon Sep 17 00:00:00 2001 From: ChemicalXandco <32775248+ChemicalXandco@users.noreply.github.com> Date: Fri, 15 Jan 2021 18:16:45 +0000 Subject: [PATCH 1/7] [ui] add support for selecting multiple nodes at once --- meshroom/common/qt.py | 2 + meshroom/core/graph.py | 16 +++ meshroom/core/node.py | 1 - meshroom/ui/commands.py | 33 +++++- meshroom/ui/graph.py | 79 ++++++++++++- meshroom/ui/qml/GraphEditor/GraphEditor.qml | 122 +++++++++++++++----- meshroom/ui/qml/GraphEditor/Node.qml | 13 ++- 7 files changed, 224 insertions(+), 42 deletions(-) diff --git a/meshroom/common/qt.py b/meshroom/common/qt.py index 0c4c7b5d72..8b6e324a6d 100644 --- a/meshroom/common/qt.py +++ b/meshroom/common/qt.py @@ -113,6 +113,7 @@ def pop(self, key): ############ # List API # ############ + @QtCore.Slot(QtCore.QObject) def append(self, obj): """ Insert object at the end of the model. """ self.extend([obj]) @@ -182,6 +183,7 @@ def removeAt(self, i, count=1): self.endRemoveRows() self.countChanged.emit() + @QtCore.Slot(QtCore.QObject) def remove(self, obj): """ Removes the first occurrence of the given object. Raises a ValueError if not in list. """ if not self.contains(obj): diff --git a/meshroom/core/graph.py b/meshroom/core/graph.py index 27d2e56e94..cedabe6ecf 100644 --- a/meshroom/core/graph.py +++ b/meshroom/core/graph.py @@ -385,6 +385,22 @@ def duplicateNodesFromNode(self, fromNode): 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/core/node.py b/meshroom/core/node.py index 00646e6c6e..0b3745dad8 100644 --- a/meshroom/core/node.py +++ b/meshroom/core/node.py @@ -684,7 +684,6 @@ def hasStatus(self, status): def _isComputed(self): return self.hasStatus(Status.SUCCESS) - @Slot() def clearData(self): """ Delete this Node internal folder. Status will be reset to Status.NONE diff --git a/meshroom/ui/commands.py b/meshroom/ui/commands.py index f5429c0ea0..b70dcff521 100755 --- a/meshroom/ui/commands.py +++ b/meshroom/ui/commands.py @@ -173,7 +173,18 @@ def undoImpl(self): self.graph.attribute(dstAttr)) -class DuplicateNodeCommand(GraphCommand): +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. """ @@ -181,7 +192,6 @@ def __init__(self, graph, srcNode, duplicateFollowingNodes, parent=None): super(DuplicateNodeCommand, self).__init__(graph, parent) self.srcNodeName = srcNode.name self.duplicateFollowingNodes = duplicateFollowingNodes - self.duplicates = [] def redoImpl(self): srcNode = self.graph.node(self.srcNodeName) @@ -196,10 +206,21 @@ def redoImpl(self): self.duplicates = [n.name for n in duplicates] return duplicates - def undoImpl(self): - # delete all the duplicated nodes - for nodeName in self.duplicates: - self.graph.removeNode(nodeName) + +class DuplicateNodeListCommand(_DuplicateNodes): + """ + 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") + + def redoImpl(self): + srcNodes = [ self.graph.node(srcNodeName) for srcNodeName in self.srcNodeNames ] + duplicates = list(self.graph.duplicateNodesFromList(srcNodes).values()) + self.duplicates = [ n.name for n in duplicates ] + return duplicates class SetAttributeCommand(GraphCommand): diff --git a/meshroom/ui/graph.py b/meshroom/ui/graph.py index 73aed58ca3..63fba55bab 100644 --- a/meshroom/ui/graph.py +++ b/meshroom/ui/graph.py @@ -273,6 +273,7 @@ def __init__(self, undoStack, taskManager, parent=None): self._sortedDFSChunks = QObjectListModel(parent=self) self._layout = GraphLayout(self) self._selectedNode = None + self._selectedNodes = QObjectListModel(parent=self) self._hoveredNode = None self.computeStatusChanged.connect(self.updateLockedUndoStack) @@ -499,6 +500,12 @@ def addNewNode(self, nodeType, position=None, **kwargs): 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 + @Slot(Node, QPoint) def moveNode(self, node, position): """ @@ -509,13 +516,33 @@ def moveNode(self, node, position): position (QPoint): the target position """ 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): + 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)) + @Slot(Node) def removeNode(self, node): + if self.nodeSelection(node): + self.removeSelectedNodes() + return + if node.locked: + return self.push(commands.RemoveNodeCommand(self._graph, node)) + def removeSelectedNodes(self): + with self.groupedGraphModification("Remove Selected Nodes"): + for node in self._selectedNodes: + if not node.locked: + self.push(commands.RemoveNodeCommand(self._graph, node)) + @Slot(Node) def removeNodesFrom(self, startNode): """ @@ -523,12 +550,15 @@ def removeNodesFrom(self, startNode): Args: startNode (Node): the node to start from. """ + if not startNode: + return with self.groupedGraphModification("Remove Nodes from {}".format(startNode.name)): 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): - self.removeNode(node) + if not node.locked: + self.removeNode(node) @Slot(Attribute, Attribute) def addEdge(self, src, dst): @@ -560,7 +590,7 @@ def resetAttribute(self, attribute): @Slot(Node, bool, result="QVariantList") def duplicateNode(self, srcNode, duplicateFollowingNodes=False): """ - Duplicate a node an optionally all the following nodes to graph leaves. + Duplicate a node and optionally all the following nodes to graph leaves. Args: srcNode (Node): node to start the duplication from @@ -569,12 +599,18 @@ def duplicateNode(self, srcNode, duplicateFollowingNodes=False): Returns: [Nodes]: the list of duplicated nodes """ - title = "Duplicate Nodes from {}" if duplicateFollowingNodes else "Duplicate {}" + 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): - duplicates = self.push(commands.DuplicateNodeCommand(self._graph, srcNode, duplicateFollowingNodes)) + 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: @@ -582,6 +618,14 @@ def duplicateNode(self, srcNode, duplicateFollowingNodes=False): 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. """ @@ -615,10 +659,33 @@ def appendAttribute(self, attribute, value=QJsonValue()): def removeAttribute(self, attribute): self.push(commands.ListAttributeRemoveCommand(self._graph, attribute)) + @Slot(QObject, QObject) + def boxSelect(self, selection, draggable): + x = selection.x() - draggable.x() + y = selection.y() - draggable.y() + otherX = x + selection.width() + otherY = y + selection.height() + x, y, otherX, otherY = [ i / j for i, j in zip([x, y, otherX, otherY], [draggable.scale()] * 4) ] + if x == otherX or y == otherY: + return + for n in self._graph.nodes: + bbox = self._layout.boundingBox([n]) + # evaluate if the selection and node intersect + if not (x > bbox[2] + bbox[0] or otherX < bbox[0] or y > bbox[3] + bbox[1] or otherY < bbox[1]): + if not self._selectedNodes.contains(n): + self._selectedNodes.append(n) + self.selectedNodesChanged.emit() + def clearNodeSelection(self): """ Clear node selection. """ self.selectedNode = None + @Slot() + def clearNodesSelections(self): + """ Clear multiple nodes selection. """ + self._selectedNodes.clear() + self.selectedNodesChanged.emit() + def clearNodeHover(self): """ Reset currently hovered node to None. """ self.hoveredNode = None @@ -643,6 +710,10 @@ def clearNodeHover(self): # Currently selected node selectedNode = makeProperty(QObject, "_selectedNode", selectedNodeChanged, resetOnDestroy=True) + selectedNodesChanged = Signal() + # Currently selected nodes to drag + selectedNodes = makeProperty(QObject, "_selectedNodes", selectedNodesChanged, resetOnDestroy=True) + hoveredNodeChanged = Signal() # Currently hovered node hoveredNode = makeProperty(QObject, "_hoveredNode", hoveredNodeChanged, resetOnDestroy=True) diff --git a/meshroom/ui/qml/GraphEditor/GraphEditor.qml b/meshroom/ui/qml/GraphEditor/GraphEditor.qml index 9b6c2e11d9..c5e1466930 100755 --- a/meshroom/ui/qml/GraphEditor/GraphEditor.qml +++ b/meshroom/ui/qml/GraphEditor/GraphEditor.qml @@ -67,6 +67,11 @@ Item { Keys.onPressed: { if(event.key === Qt.Key_F) fit() + if(event.key === Qt.Key_Delete) + if(event.modifiers == Qt.AltModifier) + uigraph.removeNodesFrom(uigraph.selectedNode) + else + uigraph.removeNode(uigraph.selectedNode) } MouseArea { @@ -98,11 +103,20 @@ Item { } onPressed: { - if(mouse.button != Qt.MiddleButton && mouse.modifiers == Qt.NoModifier) + if (mouse.button != Qt.MiddleButton && mouse.modifiers == Qt.NoModifier) { selectNode(null) - - if(mouse.button == Qt.MiddleButton || (mouse.button & Qt.LeftButton && mouse.modifiers & Qt.ShiftModifier)) + uigraph.clearNodesSelections() + } + if (mouse.button == Qt.LeftButton && (mouse.modifiers == Qt.NoModifier || mouse.modifiers == Qt.ControlModifier)) { + boxSelect.startX = mouseX + boxSelect.startY = mouseY + boxSelectDraggable.x = mouseX + boxSelectDraggable.y = mouseY + drag.target = boxSelectDraggable + } + if (mouse.button == Qt.MiddleButton || (mouse.button == Qt.LeftButton && mouse.modifiers & Qt.ShiftModifier)) { drag.target = draggable // start drag + } } onReleased: { drag.target = undefined // stop drag @@ -310,7 +324,7 @@ Item { } MenuSeparator {} MenuItem { - text: "Duplicate Node" + (duplicateFollowingButton.hovered ? "s From Here" : "") + text: "Duplicate Node" + (duplicateFollowingButton.hovered ? "s From Here" : uigraph.nodeSelection(nodeMenu.currentNode) ? "s" : "") enabled: true onTriggered: duplicateNode(nodeMenu.currentNode, false) MaterialToolButton { @@ -325,7 +339,7 @@ Item { } } MenuItem { - text: "Remove Node" + (removeFollowingButton.hovered ? "s From Here" : "") + text: "Remove Node" + (removeFollowingButton.hovered ? "s From Here" : uigraph.nodeSelection(nodeMenu.currentNode) ? "s" : "") enabled: nodeMenu.currentNode ? !nodeMenu.currentNode.locked : false onTriggered: uigraph.removeNode(nodeMenu.currentNode) MaterialToolButton { @@ -395,7 +409,7 @@ Item { if(deleteFollowing) graph.clearDataFrom(node); else - node.clearData(); + uigraph.clearData(node); } onClosed: destroy() } @@ -409,34 +423,47 @@ Item { model: root.graph ? root.graph.nodes : undefined property bool loaded: model ? count === model.count : false + property bool dragging: false delegate: Node { id: nodeDelegate - property bool animatePosition: true - node: object width: uigraph.layout.nodeWidth - selected: uigraph.selectedNode === node + mainSelected: uigraph.selectedNode === node + selected: uigraph.selectedNodes.contains(node) hovered: uigraph.hoveredNode === node - onSelectedChanged: if(selected) forceActiveFocus() onAttributePinCreated: registerAttributePin(attribute, pin) onAttributePinDeleted: unregisterAttributePin(attribute, pin) onPressed: { - selectNode(node) - - if(mouse.button == Qt.LeftButton && mouse.modifiers & Qt.AltModifier) - { - duplicateNode(node, true) - } - if(mouse.button == Qt.RightButton) - { + if (mouse.button == Qt.LeftButton) { + if (mouse.modifiers & Qt.ControlModifier) { + if (mainSelected) { + // left clicking a selected node twice with control will deselect it + uigraph.selectedNodes.remove(node) + uigraph.selectedNodesChanged() + selectNode(null) + return + } else if (!selected) { + uigraph.selectedNodes.append(node) + uigraph.selectedNodesChanged() + } + } else if (mouse.modifiers & Qt.AltModifier) { + duplicateNode(node, true) + } else if (!mainSelected && !selected) { + uigraph.clearNodesSelections() + } + } else if (mouse.button == Qt.RightButton) { + if (!mainSelected && !selected) { + uigraph.clearNodesSelections() + } nodeMenu.currentNode = node nodeMenu.popup() } + selectNode(node) } onDoubleClicked: root.nodeDoubleClicked(mouse, node) @@ -446,26 +473,67 @@ Item { onEntered: uigraph.hoveredNode = node onExited: uigraph.hoveredNode = null - Keys.onDeletePressed: { - if(node.locked) - return - if(event.modifiers == Qt.AltModifier) - uigraph.removeNodesFrom(node) - else - uigraph.removeNode(node) + onPositionChanged: { + if (dragging && uigraph.selectedNodes.contains(node)) { + // update all selected nodes positions with this node that is being dragged + for (var i = 0; i < nodeRepeater.count; i++) { + var otherNode = nodeRepeater.itemAt(i) + if (uigraph.selectedNodes.contains(otherNode.node) && otherNode.node != node) { + otherNode.x = otherNode.node.x + (x - node.x) + otherNode.y = otherNode.node.y + (y - node.y) + } + } + } } + // allow all nodes to know if they are being dragged + onDraggingChanged: { + if (dragging) { + nodeRepeater.dragging = true + } else { + nodeRepeater.dragging = false + } + } + + // must not be enabled during drag because the other nodes will be slow to match the movement of the node being dragged Behavior on x { - enabled: animatePosition + enabled: !nodeRepeater.dragging NumberAnimation { duration: 100 } } Behavior on y { - enabled: animatePosition + enabled: !nodeRepeater.dragging NumberAnimation { duration: 100 } } } } } + + Rectangle { + id: boxSelect + property int startX: 0 + property int startY: 0 + property int toX: boxSelectDraggable.x - startX + property int toY: boxSelectDraggable.y - startY + + x: toX < 0 ? startX + toX : startX + y: toY < 0 ? startY + toY : startY + width: Math.abs(toX) + height: Math.abs(toY) + + color: "transparent" + border.color: activePalette.text + visible: mouseArea.drag.target == boxSelectDraggable + + onVisibleChanged: { + if (!visible) { + uigraph.boxSelect(boxSelect, draggable) + } + } + } + + Item { + id: boxSelectDraggable + } } // Toolbar diff --git a/meshroom/ui/qml/GraphEditor/Node.qml b/meshroom/ui/qml/GraphEditor/Node.qml index 2825dbbce7..e1651416c1 100755 --- a/meshroom/ui/qml/GraphEditor/Node.qml +++ b/meshroom/ui/qml/GraphEditor/Node.qml @@ -20,8 +20,12 @@ Item { /// Whether the node is in compatibility mode readonly property bool isCompatibilityNode: node ? node.hasOwnProperty("compatibilityIssue") : false /// Mouse related states + property bool mainSelected: false property bool selected: false property bool hovered: false + property bool dragging: mouseArea.drag.active + /// Combined x and y + property point position: Qt.point(x, y) /// Styling property color shadowColor: "#cc000000" readonly property color defaultColor: isCompatibilityNode ? "#444" : activePalette.base @@ -93,6 +97,7 @@ Item { // Main Layout MouseArea { + id: mouseArea width: parent.width height: body.height drag.target: root @@ -117,9 +122,9 @@ Item { Rectangle { anchors.fill: nodeContent anchors.margins: -border.width - visible: root.selected || root.hovered + visible: root.mainSelected || root.hovered border.width: 2.5 - border.color: root.selected ? activePalette.highlight : Qt.darker(activePalette.highlight, 1.5) + border.color: root.mainSelected ? activePalette.highlight : Qt.darker(activePalette.highlight, 1.5) opacity: 0.9 radius: background.radius color: "transparent" @@ -151,7 +156,7 @@ Item { id: header width: parent.width height: headerLayout.height - color: root.selected ? activePalette.highlight : root.baseColor + color: root.mainSelected ? activePalette.highlight : root.selected ? Qt.darker(activePalette.highlight, 1.1): root.baseColor radius: background.radius // Fill header's bottom radius @@ -174,7 +179,7 @@ Item { Layout.fillWidth: true text: node ? node.label : "" padding: 4 - color: root.selected ? "white" : activePalette.text + color: root.mainSelected ? "white" : activePalette.text elide: Text.ElideMiddle font.pointSize: 8 } From 6f50f652c2af75486774ddd5073b3562d7a91450 Mon Sep 17 00:00:00 2001 From: ChemicalXandco <32775248+ChemicalXandco@users.noreply.github.com> Date: Fri, 15 Jan 2021 20:16:14 +0000 Subject: [PATCH 2/7] [ui] GraphEditor: fix cursor shape cursor is only 'closed hand' when scrolling the graph --- meshroom/ui/qml/GraphEditor/GraphEditor.qml | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/meshroom/ui/qml/GraphEditor/GraphEditor.qml b/meshroom/ui/qml/GraphEditor/GraphEditor.qml index c5e1466930..862ad667e6 100755 --- a/meshroom/ui/qml/GraphEditor/GraphEditor.qml +++ b/meshroom/ui/qml/GraphEditor/GraphEditor.qml @@ -87,7 +87,7 @@ Item { hoverEnabled: true acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton drag.threshold: 0 - cursorShape: drag.active ? Qt.ClosedHandCursor : Qt.ArrowCursor + cursorShape: drag.target == draggable ? Qt.ClosedHandCursor : Qt.ArrowCursor onWheel: { var zoomFactor = wheel.angleDelta.y > 0 ? factor : 1/factor @@ -487,13 +487,7 @@ Item { } // allow all nodes to know if they are being dragged - onDraggingChanged: { - if (dragging) { - nodeRepeater.dragging = true - } else { - nodeRepeater.dragging = false - } - } + onDraggingChanged: nodeRepeater.dragging = dragging // must not be enabled during drag because the other nodes will be slow to match the movement of the node being dragged Behavior on x { From aef50d9bde9735fc1b994e23bbc0f1762f2a5f3c Mon Sep 17 00:00:00 2001 From: ChemicalXandco <32775248+ChemicalXandco@users.noreply.github.com> Date: Fri, 22 Jan 2021 22:49:44 +0000 Subject: [PATCH 3/7] [ui] GraphEditor: fix error when node is clicked with control and not selected --- meshroom/ui/qml/GraphEditor/GraphEditor.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meshroom/ui/qml/GraphEditor/GraphEditor.qml b/meshroom/ui/qml/GraphEditor/GraphEditor.qml index 862ad667e6..d2f2bf018e 100755 --- a/meshroom/ui/qml/GraphEditor/GraphEditor.qml +++ b/meshroom/ui/qml/GraphEditor/GraphEditor.qml @@ -441,7 +441,7 @@ Item { onPressed: { if (mouse.button == Qt.LeftButton) { if (mouse.modifiers & Qt.ControlModifier) { - if (mainSelected) { + if (mainSelected && selected) { // left clicking a selected node twice with control will deselect it uigraph.selectedNodes.remove(node) uigraph.selectedNodesChanged() From acf1bf21160ee5749e4262c887b5d987d2cb9220 Mon Sep 17 00:00:00 2001 From: ChemicalXandco <32775248+ChemicalXandco@users.noreply.github.com> Date: Tue, 26 Jan 2021 21:13:16 +0000 Subject: [PATCH 4/7] [ui] grapheditor: explicitly pass selected nodes argument --- meshroom/core/graph.py | 36 +---- meshroom/ui/commands.py | 53 ++----- meshroom/ui/graph.py | 147 ++++++++++---------- meshroom/ui/qml/GraphEditor/GraphEditor.qml | 26 ++-- 4 files changed, 106 insertions(+), 156 deletions(-) diff --git a/meshroom/core/graph.py b/meshroom/core/graph.py index cedabe6ecf..9ee928e312 100644 --- a/meshroom/core/graph.py +++ b/meshroom/core/graph.py @@ -362,45 +362,15 @@ def copyNode(self, srcNode, withEdges=False): 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 - - 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 + srcNodes: 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 b70dcff521..f0a2324d3c 100755 --- a/meshroom/ui/commands.py +++ b/meshroom/ui/commands.py @@ -159,7 +159,7 @@ def __init__(self, graph, node, parent=None): 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 @@ def undoImpl(self): 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 63fba55bab..c9c984217e 100644 --- a/meshroom/ui/graph.py +++ b/meshroom/ui/graph.py @@ -500,53 +500,52 @@ def addNewNode(self, nodeType, position=None, **kwargs): 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 - - @Slot(Node, QPoint) - def moveNode(self, node, position): + 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, 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 @@ def removeNodesFrom(self, startNode): 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 @@ def resetAttribute(self, attribute): """ 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 d2f2bf018e..468459fbac 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 From 55b16bc6d39f613b17beed5b4445b47768a5ca9f Mon Sep 17 00:00:00 2001 From: ChemicalXandco <32775248+ChemicalXandco@users.noreply.github.com> Date: Wed, 3 Feb 2021 22:29:31 +0000 Subject: [PATCH 5/7] [tests] update node duplication test --- meshroom/ui/graph.py | 8 +++----- meshroom/ui/qml/GraphEditor/GraphEditor.qml | 7 +++---- tests/test_graph.py | 3 ++- 3 files changed, 8 insertions(+), 10 deletions(-) diff --git a/meshroom/ui/graph.py b/meshroom/ui/graph.py index c9c984217e..27f47765e1 100644 --- a/meshroom/ui/graph.py +++ b/meshroom/ui/graph.py @@ -679,14 +679,12 @@ def boxSelect(self, selection, draggable): self._selectedNodes.append(n) self.selectedNodesChanged.emit() + @Slot() def clearNodeSelection(self): - """ Clear node selection. """ + """Clear all node selection.""" self.selectedNode = None - - @Slot() - def clearNodesSelections(self): - """ Clear multiple nodes selection. """ self._selectedNodes.clear() + self.selectedNodeChanged.emit() self.selectedNodesChanged.emit() def clearNodeHover(self): diff --git a/meshroom/ui/qml/GraphEditor/GraphEditor.qml b/meshroom/ui/qml/GraphEditor/GraphEditor.qml index 468459fbac..ce6dc79522 100755 --- a/meshroom/ui/qml/GraphEditor/GraphEditor.qml +++ b/meshroom/ui/qml/GraphEditor/GraphEditor.qml @@ -112,8 +112,7 @@ Item { onPressed: { if (mouse.button != Qt.MiddleButton && mouse.modifiers == Qt.NoModifier) { - selectNode(null) - uigraph.clearNodesSelections() + uigraph.clearNodeSelection() } if (mouse.button == Qt.LeftButton && (mouse.modifiers == Qt.NoModifier || mouse.modifiers == Qt.ControlModifier)) { boxSelect.startX = mouseX @@ -460,11 +459,11 @@ Item { } else if (mouse.modifiers & Qt.AltModifier) { duplicateNode(node, true) } else if (!mainSelected && !selected) { - uigraph.clearNodesSelections() + uigraph.clearNodeSelection() } } else if (mouse.button == Qt.RightButton) { if (!mainSelected && !selected) { - uigraph.clearNodesSelections() + uigraph.clearNodeSelection() } nodeMenu.currentNode = node nodeMenu.popup() diff --git a/tests/test_graph.py b/tests/test_graph.py index ed92447524..f202a0f3b3 100644 --- a/tests/test_graph.py +++ b/tests/test_graph.py @@ -264,7 +264,8 @@ def test_duplicate_nodes(): n3 = g.addNewNode('AppendFiles', input=n1.output, input2=n2.output) # duplicate from n1 - nMap = g.duplicateNodesFromNode(fromNode=n1) + nodes_to_duplicate, _ = g.dfsOnDiscover(startNodes=[n1], reverse=True, dependenciesOnly=True) + nMap = g.duplicateNodes(srcNodes=nodes_to_duplicate) for s, d in nMap.items(): assert s.nodeType == d.nodeType From e8a5491178c73b58dd765deb5f483384707b019b Mon Sep 17 00:00:00 2001 From: ChemicalXandco <32775248+ChemicalXandco@users.noreply.github.com> Date: Wed, 14 Apr 2021 23:14:51 +0100 Subject: [PATCH 6/7] [ui] GraphEditor: select duplicated nodes and select following nodes on alt + left click --- meshroom/ui/graph.py | 14 ++++++++++++-- meshroom/ui/qml/GraphEditor/GraphEditor.qml | 15 +++++++++++---- 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/meshroom/ui/graph.py b/meshroom/ui/graph.py index 27f47765e1..7807b561ec 100644 --- a/meshroom/ui/graph.py +++ b/meshroom/ui/graph.py @@ -662,6 +662,17 @@ def appendAttribute(self, attribute, value=QJsonValue()): def removeAttribute(self, attribute): self.push(commands.ListAttributeRemoveCommand(self._graph, attribute)) + @Slot(Node) + def appendSelection(self, node): + if not self._selectedNodes.contains(node): + self._selectedNodes.append(node) + + @Slot(Node) + def selectFollowing(self, node): + for n in self._graph.dfsOnDiscover(startNodes=[node], reverse=True, dependenciesOnly=True)[0]: + self.appendSelection(n) + self.selectedNodesChanged.emit() + @Slot(QObject, QObject) def boxSelect(self, selection, draggable): x = selection.x() - draggable.x() @@ -675,8 +686,7 @@ def boxSelect(self, selection, draggable): bbox = self._layout.boundingBox([n]) # evaluate if the selection and node intersect if not (x > bbox[2] + bbox[0] or otherX < bbox[0] or y > bbox[3] + bbox[1] or otherY < bbox[1]): - if not self._selectedNodes.contains(n): - self._selectedNodes.append(n) + self.appendSelection(n) self.selectedNodesChanged.emit() @Slot() diff --git a/meshroom/ui/qml/GraphEditor/GraphEditor.qml b/meshroom/ui/qml/GraphEditor/GraphEditor.qml index ce6dc79522..ecfa90e1ce 100755 --- a/meshroom/ui/qml/GraphEditor/GraphEditor.qml +++ b/meshroom/ui/qml/GraphEditor/GraphEditor.qml @@ -55,8 +55,8 @@ Item { function selectNode(node) { uigraph.selectedNode = node - if (!uigraph.selectedNodes.contains(node) && node !== null) { - uigraph.selectedNodes.append(node) + if (node !== null) { + uigraph.appendSelection(node) uigraph.selectedNodesChanged() } } @@ -68,7 +68,9 @@ Item { } else { var nodes = uigraph.duplicateNodes(uigraph.selectedNodes) } + uigraph.clearNodeSelection() selectNode(nodes[0]) + uigraph.selectFollowing(nodes[0]) } @@ -80,6 +82,8 @@ Item { uigraph.removeNodesFrom(uigraph.selectedNode) else uigraph.removeNodes(uigraph.selectedNodes) + if(event.key === Qt.Key_D) + duplicateNode(uigraph.selectedNode, event.modifiers == Qt.AltModifier) } MouseArea { @@ -448,7 +452,7 @@ Item { onPressed: { if (mouse.button == Qt.LeftButton) { - if (mouse.modifiers & Qt.ControlModifier) { + if (mouse.modifiers & Qt.ControlModifier && !(mouse.modifiers & Qt.AltModifier)) { if (mainSelected && selected) { // left clicking a selected node twice with control will deselect it uigraph.selectedNodes.remove(node) @@ -457,7 +461,10 @@ Item { return } } else if (mouse.modifiers & Qt.AltModifier) { - duplicateNode(node, true) + if (!(mouse.modifiers & Qt.ControlModifier)){ + uigraph.clearNodeSelection() + } + uigraph.selectFollowing(node) } else if (!mainSelected && !selected) { uigraph.clearNodeSelection() } From fdfabf0066092d1e2e5545075881d09de6ad1c44 Mon Sep 17 00:00:00 2001 From: ChemicalXandco <32775248+ChemicalXandco@users.noreply.github.com> Date: Sun, 18 Apr 2021 14:33:17 +0100 Subject: [PATCH 7/7] [ui] GraphEditor: use entire node selection when handling '_ from here' operations --- meshroom/core/graph.py | 5 -- meshroom/ui/graph.py | 70 ++++++++++++++------- meshroom/ui/qml/GraphEditor/GraphEditor.qml | 20 +++--- 3 files changed, 56 insertions(+), 39 deletions(-) diff --git a/meshroom/core/graph.py b/meshroom/core/graph.py index 9ee928e312..601024c815 100644 --- a/meshroom/core/graph.py +++ b/meshroom/core/graph.py @@ -1132,11 +1132,6 @@ def clearSubmittedNodes(self): for node in self.nodes: node.clearSubmittedChunks() - @Slot(Node) - def clearDataFrom(self, startNode): - for node in self.dfsOnDiscover(startNodes=[startNode], reverse=True, dependenciesOnly=True)[0]: - node.clearData() - def iterChunksByStatus(self, status): """ Iterate over NodeChunks with the given status """ for node in self.nodes: diff --git a/meshroom/ui/graph.py b/meshroom/ui/graph.py index 7807b561ec..ef5b2600cf 100644 --- a/meshroom/ui/graph.py +++ b/meshroom/ui/graph.py @@ -541,21 +541,19 @@ def removeNodes(self, nodes): for node in nodes: self.push(commands.RemoveNodeCommand(self._graph, node)) - @Slot(Node) - def removeNodesFrom(self, startNode): + @Slot(QObject) + def removeNodesFrom(self, nodes): """ Remove all nodes starting from 'startNode' to graph leaves. Args: startNode (Node): the node to start from. """ - if not startNode: - return - with self.groupedGraphModification("Remove Nodes from {}".format(startNode.name)): - nodes, _ = self._graph.dfsOnDiscover(startNodes=[startNode], reverse=True, dependenciesOnly=True) + with self.groupedGraphModification("Remove Nodes From Selected Nodes"): + nodesToRemove, _ = self._graph.dfsOnDiscover(startNodes=nodes, reverse=True, dependenciesOnly=True) # Perform nodes removal from leaves to start node so that edges # can be re-created in correct order on redo. - self.removeNodes(list(reversed(nodes))) + self.removeNodes(list(reversed(nodesToRemove))) @Slot(QObject, result="QVariantList") def duplicateNodes(self, nodes): @@ -579,29 +577,38 @@ def duplicateNodes(self, nodes): self.moveNode(n, Position(n.x, bbox[3] + self.layout.gridSpacing + n.y)) return duplicates - @Slot(Node, result="QVariantList") - def duplicateNodesFrom(self, startNode): + @Slot(QObject, result="QVariantList") + def duplicateNodesFrom(self, nodes): """ - Duplicate all nodes starting from 'startNode' to graph leaves. + Duplicate all nodes starting from 'nodes' to graph leaves. Args: - startNode (Node): the node to start from. + nodes (list[Node]): the nodes 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) + with self.groupedGraphModification("Duplicate Nodes From Selected Nodes"): + nodesToDuplicate, _ = self._graph.dfsOnDiscover(startNodes=nodes, reverse=True, dependenciesOnly=True) + duplicates = self.duplicateNodes(nodesToDuplicate) return duplicates @Slot(QObject) def clearData(self, nodes): + """ Clear data from 'nodes'. """ nodes = self.filterNodes(nodes) for n in nodes: n.clearData() + @Slot(QObject) + def clearDataFrom(self, nodes): + """ + Clear data from all nodes starting from 'nodes' to graph leaves. + + Args: + nodes (list[Node]): the nodes to start from. + """ + self.clearData(self._graph.dfsOnDiscover(startNodes=nodes, reverse=True, dependenciesOnly=True)[0]) + @Slot(Attribute, Attribute) def addEdge(self, src, dst): if isinstance(dst, ListAttribute) and not isinstance(src, ListAttribute): @@ -664,22 +671,37 @@ def removeAttribute(self, attribute): @Slot(Node) def appendSelection(self, node): + """ Append 'node' to the selection if it is not already part of the selection. """ if not self._selectedNodes.contains(node): self._selectedNodes.append(node) + @Slot("QVariantList") + def selectNodes(self, nodes): + """ Append 'nodes' to the selection. """ + for node in nodes: + self.appendSelection(node) + self.selectedNodesChanged.emit() + @Slot(Node) def selectFollowing(self, node): - for n in self._graph.dfsOnDiscover(startNodes=[node], reverse=True, dependenciesOnly=True)[0]: - self.appendSelection(n) - self.selectedNodesChanged.emit() + """ Select all the nodes the depend on 'node'. """ + self.selectNodes(self._graph.dfsOnDiscover(startNodes=[node], reverse=True, dependenciesOnly=True)[0]) @Slot(QObject, QObject) def boxSelect(self, selection, draggable): + """ + Select nodes that overlap with 'selection'. + Takes into account the zoom and position of 'draggable'. + + Args: + selection: the rectangle selection widget. + draggable: the parent widget that has position and scale data. + """ x = selection.x() - draggable.x() y = selection.y() - draggable.y() otherX = x + selection.width() otherY = y + selection.height() - x, y, otherX, otherY = [ i / j for i, j in zip([x, y, otherX, otherY], [draggable.scale()] * 4) ] + x, y, otherX, otherY = [ i / draggable.scale() for i in [x, y, otherX, otherY] ] if x == otherX or y == otherY: return for n in self._graph.nodes: @@ -691,8 +713,8 @@ def boxSelect(self, selection, draggable): @Slot() def clearNodeSelection(self): - """Clear all node selection.""" - self.selectedNode = None + """ Clear all node selection. """ + self._selectedNode = None self._selectedNodes.clear() self.selectedNodeChanged.emit() self.selectedNodesChanged.emit() @@ -718,11 +740,11 @@ def clearNodeHover(self): lockedChanged = Signal() selectedNodeChanged = Signal() - # Currently selected node + # Current main selected node selectedNode = makeProperty(QObject, "_selectedNode", selectedNodeChanged, resetOnDestroy=True) selectedNodesChanged = Signal() - # Currently selected nodes to drag + # Currently selected nodes selectedNodes = makeProperty(QObject, "_selectedNodes", selectedNodesChanged, resetOnDestroy=True) hoveredNodeChanged = Signal() diff --git a/meshroom/ui/qml/GraphEditor/GraphEditor.qml b/meshroom/ui/qml/GraphEditor/GraphEditor.qml index ecfa90e1ce..b9b16dd1fd 100755 --- a/meshroom/ui/qml/GraphEditor/GraphEditor.qml +++ b/meshroom/ui/qml/GraphEditor/GraphEditor.qml @@ -62,15 +62,15 @@ Item { } /// Duplicate a node and optionnally all the following ones - function duplicateNode(node, duplicateFollowingNodes) { + function duplicateNode(duplicateFollowingNodes) { if (duplicateFollowingNodes) { - var nodes = uigraph.duplicateNodesFrom(node) + var nodes = uigraph.duplicateNodesFrom(uigraph.selectedNodes) } else { var nodes = uigraph.duplicateNodes(uigraph.selectedNodes) } uigraph.clearNodeSelection() - selectNode(nodes[0]) - uigraph.selectFollowing(nodes[0]) + uigraph.selectedNode = nodes[0] + uigraph.selectNodes(nodes) } @@ -79,11 +79,11 @@ Item { fit() if(event.key === Qt.Key_Delete) if(event.modifiers == Qt.AltModifier) - uigraph.removeNodesFrom(uigraph.selectedNode) + uigraph.removeNodesFrom(uigraph.selectedNodes) else uigraph.removeNodes(uigraph.selectedNodes) if(event.key === Qt.Key_D) - duplicateNode(uigraph.selectedNode, event.modifiers == Qt.AltModifier) + duplicateNode(event.modifiers == Qt.AltModifier) } MouseArea { @@ -338,14 +338,14 @@ Item { MenuItem { text: "Duplicate Node(s)" + (duplicateFollowingButton.hovered ? " From Here" : "") enabled: true - onTriggered: duplicateNode(nodeMenu.currentNode, false) + onTriggered: duplicateNode(false) MaterialToolButton { id: duplicateFollowingButton height: parent.height anchors { right: parent.right; rightMargin: parent.padding } text: MaterialIcons.fast_forward onClicked: { - duplicateNode(nodeMenu.currentNode, true); + duplicateNode(true); nodeMenu.close(); } } @@ -360,7 +360,7 @@ Item { anchors { right: parent.right; rightMargin: parent.padding } text: MaterialIcons.fast_forward onClicked: { - uigraph.removeNodesFrom(nodeMenu.currentNode); + uigraph.removeNodesFrom(uigraph.selectedNodes); nodeMenu.close(); } } @@ -419,7 +419,7 @@ Item { onAccepted: { if(deleteFollowing) - graph.clearDataFrom(node); + uigraph.clearDataFrom(uigraph.selectedNodes); else uigraph.clearData(uigraph.selectedNodes); }