Skip to content

Commit 1ff54d1

Browse files
committed
[ui] Graph: remove selectedNodes model
Expose `getSelectedNode` that relies on the QItemSelectionModel for imperative code in QML that still requires to access the selected node instances.
1 parent 3e77ebc commit 1ff54d1

File tree

2 files changed

+47
-59
lines changed

2 files changed

+47
-59
lines changed

meshroom/ui/graph.py

Lines changed: 37 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from enum import Enum
88
from threading import Thread, Event, Lock
99
from multiprocessing.pool import ThreadPool
10-
from typing import Iterator
10+
from typing import Iterator, Optional, Union
1111

1212
from PySide6.QtCore import (
1313
Slot,
@@ -369,9 +369,7 @@ def __init__(self, undoStack, taskManager, parent=None):
369369
self._sortedDFSChunks = QObjectListModel(parent=self)
370370
self._layout = GraphLayout(self)
371371
self._selectedNode = None
372-
self._selectedNodes = QObjectListModel(parent=self)
373372
self._nodeSelection = QItemSelectionModel(self._graph.nodes, parent=self)
374-
self._nodeSelection.selectionChanged.connect(self.onNodeSelectionChanged)
375373
self._hoveredNode = None
376374

377375
self.submitLabel = "{projectName}"
@@ -516,9 +514,10 @@ def updateLockedUndoStack(self):
516514
else:
517515
self._undoStack.unlock()
518516

519-
@Slot(QObjectListModel)
517+
@Slot()
520518
@Slot(Node)
521-
def execute(self, nodes=None):
519+
@Slot(list)
520+
def execute(self, nodes: Optional[Union[list[Node], Node]] = None):
522521
nodes = [nodes] if not isinstance(nodes, Iterable) and nodes else nodes
523522
self._taskManager.compute(self._graph, nodes)
524523
self.updateLockedUndoStack() # explicitly call the update while it is already computing
@@ -554,9 +553,10 @@ def cancelNodeComputation(self, node):
554553
n.clearSubmittedChunks()
555554
self._taskManager.removeNode(n, displayList=True, processList=True)
556555

557-
@Slot(QObjectListModel)
556+
@Slot()
558557
@Slot(Node)
559-
def submit(self, nodes=None):
558+
@Slot(list)
559+
def submit(self, nodes: Optional[Union[list[Node], Node]] = None):
560560
""" Submit the graph to the default Submitter.
561561
If a node is specified, submit this node and its uncomputed predecessors.
562562
Otherwise, submit the whole
@@ -696,16 +696,14 @@ def removeNodes(self, nodes: list[Node]):
696696
for node in nodes:
697697
self.push(commands.RemoveNodeCommand(self._graph, node))
698698

699-
@Slot(QObject)
700-
def removeNodesFrom(self, nodes):
699+
@Slot(list)
700+
def removeNodesFrom(self, nodes: list[Node]):
701701
"""
702-
Remove all nodes starting from 'startNode' to graph leaves.
702+
Remove all nodes starting from 'nodes' to graph leaves.
703703
704704
Args:
705-
startNode (Node): the node to start from.
705+
nodes: the nodes to start from.
706706
"""
707-
if isinstance(nodes, Node):
708-
nodes = [nodes]
709707
with self.groupedGraphModification("Remove Nodes From Selected Nodes"):
710708
nodesToRemove, _ = self._graph.dfsOnDiscover(startNodes=nodes, reverse=True, dependenciesOnly=True)
711709
# filter out nodes that will be removed more than once
@@ -714,17 +712,17 @@ def removeNodesFrom(self, nodes):
714712
# can be re-created in correct order on redo.
715713
self.removeNodes(list(reversed(uniqueNodesToRemove)))
716714

717-
@Slot(QObject, result="QVariantList")
718-
def duplicateNodes(self, nodes):
715+
@Slot(list, result=list)
716+
def duplicateNodes(self, nodes: list[Node]) -> list[Node]:
719717
"""
720718
Duplicate 'nodes'.
721719
722720
Args:
723-
nodes (list[Node]): the nodes to duplicate
721+
nodes: the nodes to duplicate.
722+
724723
Returns:
725-
list[Node]: the list of duplicated nodes
724+
The list of duplicated nodes.
726725
"""
727-
nodes = self.filterNodes(nodes)
728726
nPositions = [(n.x, n.y) for n in self._graph.nodes]
729727
# enable updates between duplication and layout to get correct depths during layout
730728
with self.groupedGraphModification("Duplicate Selected Nodes", disableUpdates=False):
@@ -747,18 +745,16 @@ def duplicateNodes(self, nodes):
747745

748746
return duplicates
749747

750-
@Slot(QObject, result="QVariantList")
751-
def duplicateNodesFrom(self, nodes):
748+
@Slot(list, result=list)
749+
def duplicateNodesFrom(self, nodes: list[Node]) -> list[Node]:
752750
"""
753751
Duplicate all nodes starting from 'nodes' to graph leaves.
754752
755753
Args:
756-
nodes (list[Node]): the nodes to start from.
754+
node: The nodes to start from.
757755
Returns:
758-
list[Node]: the list of duplicated nodes
756+
The list of duplicated nodes.
759757
"""
760-
if isinstance(nodes, Node):
761-
nodes = [nodes]
762758
with self.groupedGraphModification("Duplicate Nodes From Selected Nodes"):
763759
nodesToDuplicate, _ = self._graph.dfsOnDiscover(startNodes=nodes, reverse=True, dependenciesOnly=True)
764760
# filter out nodes that will be duplicated more than once
@@ -789,7 +785,7 @@ def expandForLoop(self, currentEdge):
789785
dst = currentEdge.dst
790786

791787
for i in range(1, len(listAttribute)):
792-
duplicates = self.duplicateNodesFrom(dst.node)
788+
duplicates = self.duplicateNodesFrom([dst.node])
793789
newNode = duplicates[0]
794790
previousEdge = self.graph.edge(newNode.attribute(dst.name))
795791
self.replaceEdge(previousEdge, listAttribute.at(i), previousEdge.dst)
@@ -809,7 +805,7 @@ def collapseForLoop(self, currentEdge):
809805
continue
810806
occurence = allSrc.index(listAttribute.at(i)) if listAttribute.at(i) in allSrc else -1
811807
if occurence != -1:
812-
self.removeNodesFrom(self.graph.edges.at(occurence).dst.node)
808+
self.removeNodesFrom([self.graph.edges.at(occurence).dst.node])
813809
# update the edges from allSrc
814810
allSrc = [e.src for e in self._graph.edges.values()]
815811

@@ -954,11 +950,6 @@ def removeImagesFromAllGroups(self):
954950
with self.groupedGraphModification("Remove Images From All CameraInit Nodes"):
955951
self.push(commands.RemoveImagesCommand(self._graph, list(self.cameraInits)))
956952

957-
def onNodeSelectionChanged(self, selected, deselected):
958-
# Update internal cache of selected Node instances.
959-
self._selectedNodes.setObjectList(list(self.iterSelectedNodes()))
960-
self.selectedNodesChanged.emit()
961-
962953
@Slot(list)
963954
@Slot(list, int)
964955
def selectNodes(self, nodes, command=QItemSelectionModel.SelectionFlag.ClearAndSelect):
@@ -1014,6 +1005,11 @@ def iterSelectedNodes(self) -> Iterator[Node]:
10141005
for idx in self._nodeSelection.selectedRows():
10151006
yield self._graph.nodes.at(idx.row())
10161007

1008+
@Slot(result=list)
1009+
def getSelectedNodes(self) -> list[Node]:
1010+
"""Return the list of selected Node instances."""
1011+
return list(self.iterSelectedNodes())
1012+
10171013
@Slot(Node, result=bool)
10181014
def isSelected(self, node: Node) -> bool:
10191015
"""Whether `node` is part of the current selection."""
@@ -1032,19 +1028,17 @@ def clearNodeHover(self):
10321028
@Slot(result=str)
10331029
def getSelectedNodesContent(self) -> str:
10341030
"""
1035-
Return the content of the currently selected nodes in a string, formatted to JSON.
1036-
If no node is currently selected, an empty string is returned.
1031+
Serialize the current node selection and return it as JSON formatted string.
1032+
1033+
Returns an empty string if the selection is empty.
10371034
"""
1038-
if self._selectedNodes:
1039-
d = self._graph.toDict()
1040-
selection = {}
1041-
for node in self._selectedNodes:
1042-
selection[node.name] = d[node.name]
1043-
return json.dumps(selection, indent=4)
1044-
return ''
1045-
1046-
@Slot(str, QPoint, bool, result="QVariantList")
1047-
def pasteNodes(self, clipboardContent, position=None, centerPosition=False):
1035+
if not self._nodeSelection.hasSelection():
1036+
return ""
1037+
serializedSelection = {node.name: node.toDict() for node in self.iterSelectedNodes()}
1038+
return json.dumps(serializedSelection, indent=4)
1039+
1040+
@Slot(str, QPoint, bool, result=list)
1041+
def pasteNodes(self, clipboardContent, position=None, centerPosition=False) -> list[Node]:
10481042
"""
10491043
Parse the content of the clipboard to see whether it contains
10501044
valid node descriptions. If that is the case, the nodes described
@@ -1181,10 +1175,6 @@ def pasteNodes(self, clipboardContent, position=None, centerPosition=False):
11811175
# Current main selected node
11821176
selectedNode = makeProperty(QObject, "_selectedNode", selectedNodeChanged, resetOnDestroy=True)
11831177

1184-
selectedNodesChanged = Signal()
1185-
# Currently selected nodes
1186-
selectedNodes = makeProperty(QObject, "_selectedNodes", selectedNodesChanged, resetOnDestroy=True)
1187-
11881178
nodeSelection = makeProperty(QObject, "_nodeSelection")
11891179

11901180
hoveredNodeChanged = Signal()

meshroom/ui/qml/GraphEditor/GraphEditor.qml

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -63,11 +63,10 @@ Item {
6363
function duplicateNode(duplicateFollowingNodes) {
6464
var nodes
6565
if (duplicateFollowingNodes) {
66-
nodes = uigraph.duplicateNodesFrom(uigraph.selectedNodes)
66+
nodes = uigraph.duplicateNodesFrom(uigraph.getSelectedNodes())
6767
} else {
68-
nodes = uigraph.duplicateNodes(uigraph.selectedNodes)
68+
nodes = uigraph.duplicateNodes(uigraph.getSelectedNodes())
6969
}
70-
uigraph.clearNodeSelection()
7170
uigraph.selectedNode = nodes[0]
7271
uigraph.selectNodes(nodes)
7372
}
@@ -100,7 +99,6 @@ Item {
10099
var copiedContent = Clipboard.getText()
101100
var nodes = uigraph.pasteNodes(copiedContent, finalPosition, centerPosition)
102101
if (nodes.length > 0) {
103-
uigraph.clearNodeSelection()
104102
uigraph.selectedNode = nodes[0]
105103
uigraph.selectNodes(nodes)
106104
}
@@ -116,7 +114,7 @@ Item {
116114
fit()
117115
} else if (event.key === Qt.Key_Delete) {
118116
if (event.modifiers === Qt.AltModifier) {
119-
uigraph.removeNodesFrom(uigraph.selectedNodes)
117+
uigraph.removeNodesFrom(uigraph.getSelectedNodes())
120118
} else {
121119
uigraph.removeSelectedNodes()
122120
}
@@ -632,11 +630,11 @@ Item {
632630
nodeMenuLoader.showDataDeletionDialog(
633631
false,
634632
function(request, uigraph) {
635-
request(uigraph.selectedNodes);
633+
request(uigraph.getSelectedNodes());
636634
}.bind(null, computeRequest, uigraph)
637635
);
638636
} else {
639-
computeRequest(uigraph.selectedNodes);
637+
computeRequest(uigraph.getSelectedNodes());
640638
}
641639
}
642640
}
@@ -653,11 +651,11 @@ Item {
653651
nodeMenuLoader.showDataDeletionDialog(
654652
false,
655653
function(request, uigraph) {
656-
request(uigraph.selectedNodes);
654+
request(uigraph.getSelectedNodes());
657655
}.bind(null, submitRequest, uigraph)
658656
);
659657
} else {
660-
submitRequest(uigraph.selectedNodes);
658+
submitRequest(uigraph.getSelectedNodes());
661659
}
662660
}
663661
}
@@ -742,7 +740,7 @@ Item {
742740
}
743741
text: MaterialIcons.fast_forward
744742
onClicked: {
745-
uigraph.removeNodesFrom(uigraph.selectedNodes)
743+
uigraph.removeNodesFrom(uigraph.getSelectedNodes())
746744
nodeMenu.close()
747745
}
748746
}
@@ -802,13 +800,13 @@ Item {
802800
modal: false
803801
header.visible: false
804802

805-
text: "Delete Data of '" + node.label + "'" + (uigraph.selectedNodes.count > 1 ? " and other selected Nodes" : "") + (deleteFollowing ? " and following Nodes?" : "?")
803+
text: "Delete Data of '" + node.label + "'" + (uigraph.nodeSelection.selectedIndexes.length > 1 ? " and other selected Nodes" : "") + (deleteFollowing ? " and following Nodes?" : "?")
806804
helperText: "Warning: This operation cannot be undone."
807805
standardButtons: Dialog.Yes | Dialog.Cancel
808806

809807
onAccepted: {
810808
if (deleteFollowing)
811-
uigraph.clearDataFrom(uigraph.selectedNodes);
809+
uigraph.clearDataFrom(uigraph.getSelectedNodes());
812810
else
813811
uigraph.clearSelectedNodesData();
814812
dataDeleted();

0 commit comments

Comments
 (0)