Skip to content

Commit 67bd43e

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 cdfa618 commit 67bd43e

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."""
@@ -1046,19 +1042,17 @@ def setSelectedNodesColor(self, color: str):
10461042
@Slot(result=str)
10471043
def getSelectedNodesContent(self) -> str:
10481044
"""
1049-
Return the content of the currently selected nodes in a string, formatted to JSON.
1050-
If no node is currently selected, an empty string is returned.
1045+
Serialize the current node selection and return it as JSON formatted string.
1046+
1047+
Returns an empty string if the selection is empty.
10511048
"""
1052-
if self._selectedNodes:
1053-
d = self._graph.toDict()
1054-
selection = {}
1055-
for node in self._selectedNodes:
1056-
selection[node.name] = d[node.name]
1057-
return json.dumps(selection, indent=4)
1058-
return ''
1059-
1060-
@Slot(str, QPoint, bool, result="QVariantList")
1061-
def pasteNodes(self, clipboardContent, position=None, centerPosition=False):
1049+
if not self._nodeSelection.hasSelection():
1050+
return ""
1051+
serializedSelection = {node.name: node.toDict() for node in self.iterSelectedNodes()}
1052+
return json.dumps(serializedSelection, indent=4)
1053+
1054+
@Slot(str, QPoint, bool, result=list)
1055+
def pasteNodes(self, clipboardContent, position=None, centerPosition=False) -> list[Node]:
10621056
"""
10631057
Parse the content of the clipboard to see whether it contains
10641058
valid node descriptions. If that is the case, the nodes described
@@ -1195,10 +1189,6 @@ def pasteNodes(self, clipboardContent, position=None, centerPosition=False):
11951189
# Current main selected node
11961190
selectedNode = makeProperty(QObject, "_selectedNode", selectedNodeChanged, resetOnDestroy=True)
11971191

1198-
selectedNodesChanged = Signal()
1199-
# Currently selected nodes
1200-
selectedNodes = makeProperty(QObject, "_selectedNodes", selectedNodesChanged, resetOnDestroy=True)
1201-
12021192
nodeSelection = makeProperty(QObject, "_nodeSelection")
12031193

12041194
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
}
@@ -637,11 +635,11 @@ Item {
637635
nodeMenuLoader.showDataDeletionDialog(
638636
false,
639637
function(request, uigraph) {
640-
request(uigraph.selectedNodes);
638+
request(uigraph.getSelectedNodes());
641639
}.bind(null, computeRequest, uigraph)
642640
);
643641
} else {
644-
computeRequest(uigraph.selectedNodes);
642+
computeRequest(uigraph.getSelectedNodes());
645643
}
646644
}
647645
}
@@ -658,11 +656,11 @@ Item {
658656
nodeMenuLoader.showDataDeletionDialog(
659657
false,
660658
function(request, uigraph) {
661-
request(uigraph.selectedNodes);
659+
request(uigraph.getSelectedNodes());
662660
}.bind(null, submitRequest, uigraph)
663661
);
664662
} else {
665-
submitRequest(uigraph.selectedNodes);
663+
submitRequest(uigraph.getSelectedNodes());
666664
}
667665
}
668666
}
@@ -747,7 +745,7 @@ Item {
747745
}
748746
text: MaterialIcons.fast_forward
749747
onClicked: {
750-
uigraph.removeNodesFrom(uigraph.selectedNodes)
748+
uigraph.removeNodesFrom(uigraph.getSelectedNodes())
751749
nodeMenu.close()
752750
}
753751
}
@@ -807,13 +805,13 @@ Item {
807805
modal: false
808806
header.visible: false
809807

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

814812
onAccepted: {
815813
if (deleteFollowing)
816-
uigraph.clearDataFrom(uigraph.selectedNodes);
814+
uigraph.clearDataFrom(uigraph.getSelectedNodes());
817815
else
818816
uigraph.clearSelectedNodesData();
819817
dataDeleted();

0 commit comments

Comments
 (0)