Skip to content

Commit

Permalink
[ui] Graph: Node selection refactor (1)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
yann-lty committed Nov 25, 2024
1 parent 02087f5 commit 8dd2e04
Show file tree
Hide file tree
Showing 2 changed files with 194 additions and 129 deletions.
153 changes: 94 additions & 59 deletions meshroom/ui/graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,19 @@
from enum import Enum
from threading import Thread, Event, Lock
from multiprocessing.pool import ThreadPool

from PySide6.QtCore import Slot, QJsonValue, QObject, QUrl, Property, Signal, QPoint
from typing import Iterator

from PySide6.QtCore import (
Slot,
QJsonValue,
QObject,
QUrl,
Property,
Signal,
QPoint,
QItemSelectionModel,
QItemSelection,
)

from meshroom.core import sessionUid
from meshroom.common.qt import QObjectListModel
Expand Down Expand Up @@ -359,6 +370,8 @@ def __init__(self, undoStack, taskManager, parent=None):
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}"
Expand Down Expand Up @@ -395,6 +408,8 @@ def setGraph(self, g):
self._layout.reset()
# clear undo-stack after layout
self._undoStack.clear()

self._nodeSelection.setModel(self._graph.nodes)
self.graphChanged.emit()

def onGraphUpdated(self):
Expand Down Expand Up @@ -642,27 +657,24 @@ def filterNodes(self, nodes):
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):
Expand Down Expand Up @@ -934,62 +946,83 @@ def removeImagesFromAllGroups(self):
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])

@Slot(QObject, QObject)
def boxSelect(self, selection, draggable):
"""
Select nodes that overlap with 'selection'.
Takes into account the zoom and position of 'draggable'.
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:
selection: the rectangle selection widget.
draggable: the parent widget that has position and scale data.
indices: The list of indices to select.
command: The selection command to use.
"""
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()
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._selectedNodes.clear()
self.selectedNodeChanged.emit()
self.selectedNodesChanged.emit()
"""Clear all node selection."""
self.selectedNode = None
self._nodeSelection.clear()

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.
Expand Down Expand Up @@ -1144,6 +1177,8 @@ def pasteNodes(self, clipboardContent, position=None, centerPosition=False):
# 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)
Expand Down
Loading

0 comments on commit 8dd2e04

Please sign in to comment.