[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.
This commit is contained in:
Yann Lanthony 2024-12-06 10:13:51 +01:00
parent 6d2e9a2ba9
commit 05eabb2b13
2 changed files with 203 additions and 138 deletions

View file

@ -7,8 +7,19 @@ import json
from enum import Enum
from threading import Thread, Event, Lock
from multiprocessing.pool import ThreadPool
from typing import Iterator
from PySide6.QtCore import Slot, QJsonValue, QObject, QUrl, Property, Signal, QPoint
from PySide6.QtCore import (
Slot,
QJsonValue,
QObject,
QUrl,
Property,
Signal,
QPoint,
QItemSelectionModel,
QItemSelection,
)
from meshroom.core import sessionUid
from meshroom.common.qt import QObjectListModel
@ -359,6 +370,8 @@ class UIGraph(QObject):
self._layout = GraphLayout(self)
self._selectedNode = None
self._selectedNodes = QObjectListModel(parent=self)
self._nodeSelection = QItemSelectionModel(self._graph.nodes, parent=self)
self._nodeSelection.selectionChanged.connect(self.onNodeSelectionChanged)
self._hoveredNode = None
self.submitLabel = "{projectName}"
@ -395,6 +408,8 @@ class UIGraph(QObject):
self._layout.reset()
# clear undo-stack after layout
self._undoStack.clear()
self._nodeSelection.setModel(self._graph.nodes)
self.graphChanged.emit()
def onGraphUpdated(self):
@ -642,27 +657,24 @@ class UIGraph(QObject):
nodes = [nodes]
return [ n for n in nodes if n in self._graph.nodes.values() ]
@Slot(Node, QPoint, QObject)
def moveNode(self, node, position, nodes=None):
def moveNode(self, node: Node, position: Position):
"""
Move 'node' to the given 'position' and also update the positions of 'nodes' if necessary.
Move `node` to the given `position`.
Args:
node (Node): the node to move
position (QPoint): the target position
nodes (list[Node]): the nodes to update the position of
node: The node to move.
position: The target position.
"""
if not nodes:
nodes = [node]
nodes = self.filterNodes(nodes)
if isinstance(position, QPoint):
position = Position(position.x(), position.y())
deltaX = position.x - node.x
deltaY = position.y - node.y
self.push(commands.MoveNodeCommand(self._graph, node, position))
@Slot(QPoint)
def moveSelectedNodesBy(self, offset: QPoint):
"""Move all the selected nodes by the given `offset`."""
with self.groupedGraphModification("Move Selected Nodes"):
for n in nodes:
position = Position(n.x + deltaX, n.y + deltaY)
self.push(commands.MoveNodeCommand(self._graph, n, position))
for node in self.iterSelectedNodes():
position = Position(node.x + offset.x(), node.y + offset.y())
self.moveNode(node, position)
@Slot(QObject)
def removeNodes(self, nodes):
@ -934,23 +946,80 @@ class UIGraph(QObject):
with self.groupedGraphModification("Remove Images From All CameraInit Nodes"):
self.push(commands.RemoveImagesCommand(self._graph, list(self.cameraInits)))
@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)
def onNodeSelectionChanged(self, selected, deselected):
# Update internal cache of selected Node instances.
self._selectedNodes.setObjectList(list(self.iterSelectedNodes()))
self.selectedNodesChanged.emit()
@Slot(list)
@Slot(list, int)
def selectNodes(self, nodes, command=QItemSelectionModel.SelectionFlag.ClearAndSelect):
"""Update selection with `nodes` using the specified `command`."""
indices = [self._graph._nodes.indexOf(node) for node in nodes]
self.selectNodesByIndices(indices, command)
@Slot(Node)
def selectFollowing(self, node):
""" Select all the nodes the depend on 'node'. """
def selectFollowing(self, node: Node):
"""Select all the nodes that depend on `node`."""
self.selectNodes(self._graph.dfsOnDiscover(startNodes=[node], reverse=True, dependenciesOnly=True)[0])
self.selectedNode = node
@Slot(int)
@Slot(int, int)
def selectNodeByIndex(self, index: int, command=QItemSelectionModel.SelectionFlag.ClearAndSelect):
"""Update selection with node at the given `index` using the specified `command`."""
if isinstance(command, int):
command = QItemSelectionModel.SelectionFlag(command)
self.selectNodesByIndices([index], command)
if self._nodeSelection.isRowSelected(index):
self.selectedNode = self._graph.nodes.at(index)
@Slot(list)
@Slot(list, int)
def selectNodesByIndices(
self, indices: list[int], command=QItemSelectionModel.SelectionFlag.ClearAndSelect
):
"""Update selection with node at given `indices` using the specified `command`.
Args:
indices: The list of indices to select.
command: The selection command to use.
"""
if isinstance(command, int):
command = QItemSelectionModel.SelectionFlag(command)
itemSelection = QItemSelection()
for index in indices:
itemSelection.select(
self._graph.nodes.index(index), self._graph.nodes.index(index)
)
self._nodeSelection.select(itemSelection, command)
if self.selectedNode and not self.isSelected(self.selectedNode):
self.selectedNode = None
def iterSelectedNodes(self) -> Iterator[Node]:
"""Iterate over the currently selected nodes."""
for idx in self._nodeSelection.selectedRows():
yield self._graph.nodes.at(idx.row())
@Slot(Node, result=bool)
def isSelected(self, node: Node) -> bool:
"""Whether `node` is part of the current selection."""
return self._nodeSelection.isRowSelected(self._graph.nodes.indexOf(node))
@Slot()
def clearNodeSelection(self):
"""Clear all node selection."""
self.selectedNode = None
self._nodeSelection.clear()
def clearNodeHover(self):
""" Reset currently hovered node to None. """
self.hoveredNode = None
@Slot(str)
def setSelectedNodesColor(self, color: str):
@ -962,48 +1031,12 @@ class UIGraph(QObject):
# Update the color attribute of the nodes which are currently selected
with self.groupedGraphModification("Set Nodes Color"):
# For each of the selected nodes -> Check if the node has a color -> Apply the color if it has
for node in self._selectedNodes:
for node in self.iterSelectedNodes():
if node.hasInternalAttribute("color"):
self.setAttribute(node.internalAttribute("color"), color)
@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 / draggable.scale() for i in [x, y, otherX, otherY] ]
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]):
self.appendSelection(n)
self.selectedNodesChanged.emit()
@Slot()
def clearNodeSelection(self):
""" Clear all node selection. """
self._selectedNode = None
self._selectedNodes.clear()
self.selectedNodeChanged.emit()
self.selectedNodesChanged.emit()
def clearNodeHover(self):
""" Reset currently hovered node to None. """
self.hoveredNode = None
@Slot(result=str)
def getSelectedNodesContent(self):
def getSelectedNodesContent(self) -> str:
"""
Return the content of the currently selected nodes in a string, formatted to JSON.
If no node is currently selected, an empty string is returned.
@ -1158,6 +1191,8 @@ class UIGraph(QObject):
# Currently selected nodes
selectedNodes = makeProperty(QObject, "_selectedNodes", selectedNodesChanged, resetOnDestroy=True)
nodeSelection = makeProperty(QObject, "_nodeSelection")
hoveredNodeChanged = Signal()
# Currently hovered node
hoveredNode = makeProperty(QObject, "_hoveredNode", hoveredNodeChanged, resetOnDestroy=True)