|
7 | 7 | from enum import Enum |
8 | 8 | from threading import Thread, Event, Lock |
9 | 9 | from multiprocessing.pool import ThreadPool |
10 | | - |
11 | | -from PySide6.QtCore import Slot, QJsonValue, QObject, QUrl, Property, Signal, QPoint |
| 10 | +from typing import Iterator |
| 11 | + |
| 12 | +from PySide6.QtCore import ( |
| 13 | + Slot, |
| 14 | + QJsonValue, |
| 15 | + QObject, |
| 16 | + QUrl, |
| 17 | + Property, |
| 18 | + Signal, |
| 19 | + QPoint, |
| 20 | + QItemSelectionModel, |
| 21 | + QItemSelection, |
| 22 | +) |
12 | 23 |
|
13 | 24 | from meshroom.core import sessionUid |
14 | 25 | from meshroom.common.qt import QObjectListModel |
@@ -359,6 +370,8 @@ def __init__(self, undoStack, taskManager, parent=None): |
359 | 370 | self._layout = GraphLayout(self) |
360 | 371 | self._selectedNode = None |
361 | 372 | self._selectedNodes = QObjectListModel(parent=self) |
| 373 | + self._nodeSelection = QItemSelectionModel(self._graph.nodes, parent=self) |
| 374 | + self._nodeSelection.selectionChanged.connect(self.onNodeSelectionChanged) |
362 | 375 | self._hoveredNode = None |
363 | 376 |
|
364 | 377 | self.submitLabel = "{projectName}" |
@@ -395,6 +408,8 @@ def setGraph(self, g): |
395 | 408 | self._layout.reset() |
396 | 409 | # clear undo-stack after layout |
397 | 410 | self._undoStack.clear() |
| 411 | + |
| 412 | + self._nodeSelection.setModel(self._graph.nodes) |
398 | 413 | self.graphChanged.emit() |
399 | 414 |
|
400 | 415 | def onGraphUpdated(self): |
@@ -642,27 +657,24 @@ def filterNodes(self, nodes): |
642 | 657 | nodes = [nodes] |
643 | 658 | return [ n for n in nodes if n in self._graph.nodes.values() ] |
644 | 659 |
|
645 | | - @Slot(Node, QPoint, QObject) |
646 | | - def moveNode(self, node, position, nodes=None): |
| 660 | + def moveNode(self, node: Node, position: Position): |
647 | 661 | """ |
648 | | - Move 'node' to the given 'position' and also update the positions of 'nodes' if necessary. |
| 662 | + Move `node` to the given `position`. |
649 | 663 |
|
650 | 664 | Args: |
651 | | - node (Node): the node to move |
652 | | - position (QPoint): the target position |
653 | | - nodes (list[Node]): the nodes to update the position of |
| 665 | + node: The node to move. |
| 666 | + position: The target position. |
654 | 667 | """ |
655 | | - if not nodes: |
656 | | - nodes = [node] |
657 | | - nodes = self.filterNodes(nodes) |
658 | | - if isinstance(position, QPoint): |
659 | | - position = Position(position.x(), position.y()) |
660 | | - deltaX = position.x - node.x |
661 | | - deltaY = position.y - node.y |
| 668 | + self.push(commands.MoveNodeCommand(self._graph, node, position)) |
| 669 | + |
| 670 | + @Slot(QPoint) |
| 671 | + def moveSelectedNodesBy(self, offset: QPoint): |
| 672 | + """Move all the selected nodes by the given `offset`.""" |
| 673 | + |
662 | 674 | with self.groupedGraphModification("Move Selected Nodes"): |
663 | | - for n in nodes: |
664 | | - position = Position(n.x + deltaX, n.y + deltaY) |
665 | | - self.push(commands.MoveNodeCommand(self._graph, n, position)) |
| 675 | + for node in self.iterSelectedNodes(): |
| 676 | + position = Position(node.x + offset.x(), node.y + offset.y()) |
| 677 | + self.moveNode(node, position) |
666 | 678 |
|
667 | 679 | @Slot(QObject) |
668 | 680 | def removeNodes(self, nodes): |
@@ -934,62 +946,83 @@ def removeImagesFromAllGroups(self): |
934 | 946 | with self.groupedGraphModification("Remove Images From All CameraInit Nodes"): |
935 | 947 | self.push(commands.RemoveImagesCommand(self._graph, list(self.cameraInits))) |
936 | 948 |
|
937 | | - @Slot(Node) |
938 | | - def appendSelection(self, node): |
939 | | - """ Append 'node' to the selection if it is not already part of the selection. """ |
940 | | - if not self._selectedNodes.contains(node): |
941 | | - self._selectedNodes.append(node) |
942 | | - |
943 | | - @Slot("QVariantList") |
944 | | - def selectNodes(self, nodes): |
945 | | - """ Append 'nodes' to the selection. """ |
946 | | - for node in nodes: |
947 | | - self.appendSelection(node) |
| 949 | + def onNodeSelectionChanged(self, selected, deselected): |
| 950 | + # Update internal cache of selected Node instances. |
| 951 | + self._selectedNodes.setObjectList(list(self.iterSelectedNodes())) |
948 | 952 | self.selectedNodesChanged.emit() |
949 | 953 |
|
| 954 | + @Slot(list) |
| 955 | + @Slot(list, int) |
| 956 | + def selectNodes(self, nodes, command=QItemSelectionModel.SelectionFlag.ClearAndSelect): |
| 957 | + """Update selection with `nodes` using the specified `command`.""" |
| 958 | + indices = [self._graph._nodes.indexOf(node) for node in nodes] |
| 959 | + self.selectNodesByIndices(indices, command) |
| 960 | + |
950 | 961 | @Slot(Node) |
951 | | - def selectFollowing(self, node): |
952 | | - """ Select all the nodes the depend on 'node'. """ |
| 962 | + def selectFollowing(self, node: Node): |
| 963 | + """Select all the nodes that depend on `node`.""" |
953 | 964 | self.selectNodes(self._graph.dfsOnDiscover(startNodes=[node], reverse=True, dependenciesOnly=True)[0]) |
954 | | - |
955 | | - @Slot(QObject, QObject) |
956 | | - def boxSelect(self, selection, draggable): |
957 | | - """ |
958 | | - Select nodes that overlap with 'selection'. |
959 | | - Takes into account the zoom and position of 'draggable'. |
960 | | -
|
| 965 | + self.selectedNode = node |
| 966 | + |
| 967 | + @Slot(int) |
| 968 | + @Slot(int, int) |
| 969 | + def selectNodeByIndex(self, index: int, command=QItemSelectionModel.SelectionFlag.ClearAndSelect): |
| 970 | + """Update selection with node at the given `index` using the specified `command`.""" |
| 971 | + if isinstance(command, int): |
| 972 | + command = QItemSelectionModel.SelectionFlag(command) |
| 973 | + |
| 974 | + self.selectNodesByIndices([index], command) |
| 975 | + |
| 976 | + if self._nodeSelection.isRowSelected(index): |
| 977 | + self.selectedNode = self._graph.nodes.at(index) |
| 978 | + |
| 979 | + @Slot(list) |
| 980 | + @Slot(list, int) |
| 981 | + def selectNodesByIndices( |
| 982 | + self, indices: list[int], command=QItemSelectionModel.SelectionFlag.ClearAndSelect |
| 983 | + ): |
| 984 | + """Update selection with node at given `indices` using the specified `command`. |
| 985 | + |
961 | 986 | Args: |
962 | | - selection: the rectangle selection widget. |
963 | | - draggable: the parent widget that has position and scale data. |
| 987 | + indices: The list of indices to select. |
| 988 | + command: The selection command to use. |
964 | 989 | """ |
965 | | - x = selection.x() - draggable.x() |
966 | | - y = selection.y() - draggable.y() |
967 | | - otherX = x + selection.width() |
968 | | - otherY = y + selection.height() |
969 | | - x, y, otherX, otherY = [ i / draggable.scale() for i in [x, y, otherX, otherY] ] |
970 | | - if x == otherX or y == otherY: |
971 | | - return |
972 | | - for n in self._graph.nodes: |
973 | | - bbox = self._layout.boundingBox([n]) |
974 | | - # evaluate if the selection and node intersect |
975 | | - if not (x > bbox[2] + bbox[0] or otherX < bbox[0] or y > bbox[3] + bbox[1] or otherY < bbox[1]): |
976 | | - self.appendSelection(n) |
977 | | - self.selectedNodesChanged.emit() |
| 990 | + if isinstance(command, int): |
| 991 | + command = QItemSelectionModel.SelectionFlag(command) |
| 992 | + |
| 993 | + itemSelection = QItemSelection() |
| 994 | + for index in indices: |
| 995 | + itemSelection.select( |
| 996 | + self._graph.nodes.index(index), self._graph.nodes.index(index) |
| 997 | + ) |
| 998 | + |
| 999 | + self._nodeSelection.select(itemSelection, command) |
| 1000 | + |
| 1001 | + if self.selectedNode and not self.isSelected(self.selectedNode): |
| 1002 | + self.selectedNode = None |
| 1003 | + |
| 1004 | + def iterSelectedNodes(self) -> Iterator[Node]: |
| 1005 | + """Iterate over the currently selected nodes.""" |
| 1006 | + for idx in self._nodeSelection.selectedRows(): |
| 1007 | + yield self._graph.nodes.at(idx.row()) |
| 1008 | + |
| 1009 | + @Slot(Node, result=bool) |
| 1010 | + def isSelected(self, node: Node) -> bool: |
| 1011 | + """Whether `node` is part of the current selection.""" |
| 1012 | + return self._nodeSelection.isRowSelected(self._graph.nodes.indexOf(node)) |
978 | 1013 |
|
979 | 1014 | @Slot() |
980 | 1015 | def clearNodeSelection(self): |
981 | | - """ Clear all node selection. """ |
982 | | - self._selectedNode = None |
983 | | - self._selectedNodes.clear() |
984 | | - self.selectedNodeChanged.emit() |
985 | | - self.selectedNodesChanged.emit() |
| 1016 | + """Clear all node selection.""" |
| 1017 | + self.selectedNode = None |
| 1018 | + self._nodeSelection.clear() |
986 | 1019 |
|
987 | 1020 | def clearNodeHover(self): |
988 | 1021 | """ Reset currently hovered node to None. """ |
989 | 1022 | self.hoveredNode = None |
990 | 1023 |
|
991 | 1024 | @Slot(result=str) |
992 | | - def getSelectedNodesContent(self): |
| 1025 | + def getSelectedNodesContent(self) -> str: |
993 | 1026 | """ |
994 | 1027 | Return the content of the currently selected nodes in a string, formatted to JSON. |
995 | 1028 | If no node is currently selected, an empty string is returned. |
@@ -1144,6 +1177,8 @@ def pasteNodes(self, clipboardContent, position=None, centerPosition=False): |
1144 | 1177 | # Currently selected nodes |
1145 | 1178 | selectedNodes = makeProperty(QObject, "_selectedNodes", selectedNodesChanged, resetOnDestroy=True) |
1146 | 1179 |
|
| 1180 | + nodeSelection = makeProperty(QObject, "_nodeSelection") |
| 1181 | + |
1147 | 1182 | hoveredNodeChanged = Signal() |
1148 | 1183 | # Currently hovered node |
1149 | 1184 | hoveredNode = makeProperty(QObject, "_hoveredNode", hoveredNodeChanged, resetOnDestroy=True) |
|
0 commit comments