Skip to content

Commit 8dd2e04

Browse files
committed
[ui] Graph: Node selection refactor (1)
Switch selection management backend to a QItemSelectionModel, while keeping the current 'selectedNodes' API for now. Use DelegateSectionBox for node selection in the graph, and rewrite the handling of node selection / displacement.
1 parent 02087f5 commit 8dd2e04

File tree

2 files changed

+194
-129
lines changed

2 files changed

+194
-129
lines changed

meshroom/ui/graph.py

Lines changed: 94 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,19 @@
77
from enum import Enum
88
from threading import Thread, Event, Lock
99
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+
)
1223

1324
from meshroom.core import sessionUid
1425
from meshroom.common.qt import QObjectListModel
@@ -359,6 +370,8 @@ def __init__(self, undoStack, taskManager, parent=None):
359370
self._layout = GraphLayout(self)
360371
self._selectedNode = None
361372
self._selectedNodes = QObjectListModel(parent=self)
373+
self._nodeSelection = QItemSelectionModel(self._graph.nodes, parent=self)
374+
self._nodeSelection.selectionChanged.connect(self.onNodeSelectionChanged)
362375
self._hoveredNode = None
363376

364377
self.submitLabel = "{projectName}"
@@ -395,6 +408,8 @@ def setGraph(self, g):
395408
self._layout.reset()
396409
# clear undo-stack after layout
397410
self._undoStack.clear()
411+
412+
self._nodeSelection.setModel(self._graph.nodes)
398413
self.graphChanged.emit()
399414

400415
def onGraphUpdated(self):
@@ -642,27 +657,24 @@ def filterNodes(self, nodes):
642657
nodes = [nodes]
643658
return [ n for n in nodes if n in self._graph.nodes.values() ]
644659

645-
@Slot(Node, QPoint, QObject)
646-
def moveNode(self, node, position, nodes=None):
660+
def moveNode(self, node: Node, position: Position):
647661
"""
648-
Move 'node' to the given 'position' and also update the positions of 'nodes' if necessary.
662+
Move `node` to the given `position`.
649663
650664
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.
654667
"""
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+
662674
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)
666678

667679
@Slot(QObject)
668680
def removeNodes(self, nodes):
@@ -934,62 +946,83 @@ def removeImagesFromAllGroups(self):
934946
with self.groupedGraphModification("Remove Images From All CameraInit Nodes"):
935947
self.push(commands.RemoveImagesCommand(self._graph, list(self.cameraInits)))
936948

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()))
948952
self.selectedNodesChanged.emit()
949953

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+
950961
@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`."""
953964
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+
961986
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.
964989
"""
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))
9781013

9791014
@Slot()
9801015
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()
9861019

9871020
def clearNodeHover(self):
9881021
""" Reset currently hovered node to None. """
9891022
self.hoveredNode = None
9901023

9911024
@Slot(result=str)
992-
def getSelectedNodesContent(self):
1025+
def getSelectedNodesContent(self) -> str:
9931026
"""
9941027
Return the content of the currently selected nodes in a string, formatted to JSON.
9951028
If no node is currently selected, an empty string is returned.
@@ -1144,6 +1177,8 @@ def pasteNodes(self, clipboardContent, position=None, centerPosition=False):
11441177
# Currently selected nodes
11451178
selectedNodes = makeProperty(QObject, "_selectedNodes", selectedNodesChanged, resetOnDestroy=True)
11461179

1180+
nodeSelection = makeProperty(QObject, "_nodeSelection")
1181+
11471182
hoveredNodeChanged = Signal()
11481183
# Currently hovered node
11491184
hoveredNode = makeProperty(QObject, "_hoveredNode", hoveredNodeChanged, resetOnDestroy=True)

0 commit comments

Comments
 (0)