Skip to content

Commit

Permalink
Merge pull request #1227 from ChemicalXandco/box_select
Browse files Browse the repository at this point in the history
[ui] add support for selecting multiple nodes at once
  • Loading branch information
fabiencastan authored May 3, 2021
2 parents cbdad2d + fdfabf0 commit 18be350
Show file tree
Hide file tree
Showing 8 changed files with 285 additions and 127 deletions.
2 changes: 2 additions & 0 deletions meshroom/common/qt.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ def pop(self, key):
############
# List API #
############
@QtCore.Slot(QtCore.QObject)
def append(self, obj):
""" Insert object at the end of the model. """
self.extend([obj])
Expand Down Expand Up @@ -182,6 +183,7 @@ def removeAt(self, i, count=1):
self.endRemoveRows()
self.countChanged.emit()

@QtCore.Slot(QtCore.QObject)
def remove(self, obj):
""" Removes the first occurrence of the given object. Raises a ValueError if not in list. """
if not self.contains(obj):
Expand Down
25 changes: 3 additions & 22 deletions meshroom/core/graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -362,29 +362,15 @@ def copyNode(self, srcNode, withEdges=False):
child.resetValue()
return node, skippedEdges

def duplicateNode(self, srcNode):
""" Duplicate a node in the graph with its connections.
def duplicateNodes(self, srcNodes):
""" Duplicate nodes in the graph with their connections.
Args:
srcNode: the node to duplicate
Returns:
Node: the created node
"""
node, edges = self.copyNode(srcNode, withEdges=True)
return self.addNode(node)

def duplicateNodesFromNode(self, fromNode):
"""
Duplicate 'fromNode' and all the following nodes towards graph's leaves.
Args:
fromNode (Node): the node to start the duplication from
srcNodes: the nodes to duplicate
Returns:
OrderedDict[Node, Node]: the source->duplicate map
"""
srcNodes, srcEdges = self.dfsOnDiscover(startNodes=[fromNode], reverse=True, dependenciesOnly=True)
# use OrderedDict to keep duplicated nodes creation order
duplicates = OrderedDict()

Expand Down Expand Up @@ -1146,11 +1132,6 @@ def clearSubmittedNodes(self):
for node in self.nodes:
node.clearSubmittedChunks()

@Slot(Node)
def clearDataFrom(self, startNode):
for node in self.dfsOnDiscover(startNodes=[startNode], reverse=True, dependenciesOnly=True)[0]:
node.clearData()

def iterChunksByStatus(self, status):
""" Iterate over NodeChunks with the given status """
for node in self.nodes:
Expand Down
1 change: 0 additions & 1 deletion meshroom/core/node.py
Original file line number Diff line number Diff line change
Expand Up @@ -706,7 +706,6 @@ def hasStatus(self, status):
def _isComputed(self):
return self.hasStatus(Status.SUCCESS)

@Slot()
def clearData(self):
""" Delete this Node internal folder.
Status will be reset to Status.NONE
Expand Down
32 changes: 12 additions & 20 deletions meshroom/ui/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ def __init__(self, graph, node, parent=None):

def redoImpl(self):
# only keep outEdges since inEdges are serialized in nodeDict
inEdges, self.outEdges = self.graph.removeNode(self.nodeName)
_, self.outEdges = self.graph.removeNode(self.nodeName)
return True

def undoImpl(self):
Expand All @@ -173,33 +173,25 @@ def undoImpl(self):
self.graph.attribute(dstAttr))


class DuplicateNodeCommand(GraphCommand):
class DuplicateNodesCommand(GraphCommand):
"""
Handle node duplication in a Graph.
"""
def __init__(self, graph, srcNode, duplicateFollowingNodes, parent=None):
super(DuplicateNodeCommand, self).__init__(graph, parent)
self.srcNodeName = srcNode.name
self.duplicateFollowingNodes = duplicateFollowingNodes
self.duplicates = []
def __init__(self, graph, srcNodes, parent=None):
super(DuplicateNodesCommand, self).__init__(graph, parent)
self.srcNodeNames = [ n.name for n in srcNodes ]
self.setText("Duplicate Nodes")

def redoImpl(self):
srcNode = self.graph.node(self.srcNodeName)

if self.duplicateFollowingNodes:
duplicates = list(self.graph.duplicateNodesFromNode(srcNode).values())
self.setText("Duplicate {} nodes from {}".format(len(duplicates), self.srcNodeName))
else:
duplicates = [self.graph.duplicateNode(srcNode)]
self.setText("Duplicate {}".format(self.srcNodeName))

self.duplicates = [n.name for n in duplicates]
srcNodes = [ self.graph.node(i) for i in self.srcNodeNames ]
duplicates = list(self.graph.duplicateNodes(srcNodes).values())
self.duplicates = [ n.name for n in duplicates ]
return duplicates

def undoImpl(self):
# delete all the duplicated nodes
for nodeName in self.duplicates:
self.graph.removeNode(nodeName)
# remove all duplicates
for duplicate in self.duplicates:
self.graph.removeNode(duplicate)


class SetAttributeCommand(GraphCommand):
Expand Down
186 changes: 145 additions & 41 deletions meshroom/ui/graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,7 @@ def __init__(self, undoStack, taskManager, parent=None):
self._sortedDFSChunks = QObjectListModel(parent=self)
self._layout = GraphLayout(self)
self._selectedNode = None
self._selectedNodes = QObjectListModel(parent=self)
self._hoveredNode = None

self.computeStatusChanged.connect(self.updateLockedUndoStack)
Expand Down Expand Up @@ -499,36 +500,114 @@ def addNewNode(self, nodeType, position=None, **kwargs):
position = Position(position.x(), position.y())
return self.push(commands.AddNodeCommand(self._graph, nodeType, position=position, **kwargs))

@Slot(Node, QPoint)
def moveNode(self, node, position):
def filterNodes(self, nodes):
"""Filter out the nodes that do not exist on the graph."""
return [ n for n in nodes if n in self._graph.nodes.values() ]

@Slot(Node, QPoint, QObject)
def moveNode(self, node, position, nodes=None):
"""
Move 'node' to the given 'position'.
Move 'node' to the given 'position' and also update the positions of 'nodes' if neccessary.
Args:
node (Node): the node to move
position (QPoint): the target position
nodes (list[Node]): the nodes to update the position of
"""
if not nodes:
nodes = [node]
nodes = self.filterNodes(nodes)
if isinstance(position, QPoint):
position = Position(position.x(), position.y())
self.push(commands.MoveNodeCommand(self._graph, node, position))
deltaX = position.x - node.x
deltaY = position.y - node.y
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))

@Slot(QObject)
def removeNodes(self, nodes):
"""
Remove 'nodes' from the graph.
@Slot(Node)
def removeNode(self, node):
self.push(commands.RemoveNodeCommand(self._graph, node))
Args:
nodes (list[Node]): the nodes to remove
"""
nodes = self.filterNodes(nodes)
if any([ n.locked for n in nodes ]):
return
with self.groupedGraphModification("Remove Selected Nodes"):
for node in nodes:
self.push(commands.RemoveNodeCommand(self._graph, node))

@Slot(Node)
def removeNodesFrom(self, startNode):
@Slot(QObject)
def removeNodesFrom(self, nodes):
"""
Remove all nodes starting from 'startNode' to graph leaves.
Args:
startNode (Node): the node to start from.
"""
with self.groupedGraphModification("Remove Nodes from {}".format(startNode.name)):
nodes, _ = self._graph.dfsOnDiscover(startNodes=[startNode], reverse=True, dependenciesOnly=True)
with self.groupedGraphModification("Remove Nodes From Selected Nodes"):
nodesToRemove, _ = self._graph.dfsOnDiscover(startNodes=nodes, reverse=True, dependenciesOnly=True)
# Perform nodes removal from leaves to start node so that edges
# can be re-created in correct order on redo.
for node in reversed(nodes):
self.removeNode(node)
self.removeNodes(list(reversed(nodesToRemove)))

@Slot(QObject, result="QVariantList")
def duplicateNodes(self, nodes):
"""
Duplicate 'nodes'.
Args:
nodes (list[Node]): the nodes to duplicate
Returns:
list[Node]: the list of duplicated nodes
"""
nodes = self.filterNodes(nodes)
# enable updates between duplication and layout to get correct depths during layout
with self.groupedGraphModification("Duplicate Selected Nodes", disableUpdates=False):
# disable graph updates during duplication
with self.groupedGraphModification("Node duplication", disableUpdates=True):
duplicates = self.push(commands.DuplicateNodesCommand(self._graph, nodes))
# move nodes below the bounding box formed by the duplicated node(s)
bbox = self._layout.boundingBox(duplicates)
for n in duplicates:
self.moveNode(n, Position(n.x, bbox[3] + self.layout.gridSpacing + n.y))
return duplicates

@Slot(QObject, result="QVariantList")
def duplicateNodesFrom(self, nodes):
"""
Duplicate all nodes starting from 'nodes' to graph leaves.
Args:
nodes (list[Node]): the nodes to start from.
Returns:
list[Node]: the list of duplicated nodes
"""
with self.groupedGraphModification("Duplicate Nodes From Selected Nodes"):
nodesToDuplicate, _ = self._graph.dfsOnDiscover(startNodes=nodes, reverse=True, dependenciesOnly=True)
duplicates = self.duplicateNodes(nodesToDuplicate)
return duplicates

@Slot(QObject)
def clearData(self, nodes):
""" Clear data from 'nodes'. """
nodes = self.filterNodes(nodes)
for n in nodes:
n.clearData()

@Slot(QObject)
def clearDataFrom(self, nodes):
"""
Clear data from all nodes starting from 'nodes' to graph leaves.
Args:
nodes (list[Node]): the nodes to start from.
"""
self.clearData(self._graph.dfsOnDiscover(startNodes=nodes, reverse=True, dependenciesOnly=True)[0])

@Slot(Attribute, Attribute)
def addEdge(self, src, dst):
Expand Down Expand Up @@ -563,31 +642,6 @@ def resetAttribute(self, attribute):
""" Reset 'attribute' to its default value """
self.push(commands.SetAttributeCommand(self._graph, attribute, attribute.defaultValue()))

@Slot(Node, bool, result="QVariantList")
def duplicateNode(self, srcNode, duplicateFollowingNodes=False):
"""
Duplicate a node an optionally all the following nodes to graph leaves.
Args:
srcNode (Node): node to start the duplication from
duplicateFollowingNodes (bool): whether to duplicate all the following nodes to graph leaves
Returns:
[Nodes]: the list of duplicated nodes
"""
title = "Duplicate Nodes from {}" if duplicateFollowingNodes else "Duplicate {}"
# enable updates between duplication and layout to get correct depths during layout
with self.groupedGraphModification(title.format(srcNode.name), disableUpdates=False):
# disable graph updates during duplication
with self.groupedGraphModification("Node duplication", disableUpdates=True):
duplicates = self.push(commands.DuplicateNodeCommand(self._graph, srcNode, duplicateFollowingNodes))
# move nodes below the bounding box formed by the duplicated node(s)
bbox = self._layout.boundingBox(duplicates)
for n in duplicates:
self.moveNode(n, Position(n.x, bbox[3] + self.layout.gridSpacing + n.y))

return duplicates

@Slot(CompatibilityNode, result=Node)
def upgradeNode(self, node):
""" Upgrade a CompatibilityNode. """
Expand Down Expand Up @@ -621,9 +675,55 @@ def appendAttribute(self, attribute, value=QJsonValue()):
def removeAttribute(self, attribute):
self.push(commands.ListAttributeRemoveCommand(self._graph, attribute))

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

@Slot(Node)
def selectFollowing(self, node):
""" Select all the nodes the 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'.
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 node selection. """
self.selectedNode = None
""" 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. """
Expand All @@ -646,9 +746,13 @@ def clearNodeHover(self):
lockedChanged = Signal()

selectedNodeChanged = Signal()
# Currently selected node
# Current main selected node
selectedNode = makeProperty(QObject, "_selectedNode", selectedNodeChanged, resetOnDestroy=True)

selectedNodesChanged = Signal()
# Currently selected nodes
selectedNodes = makeProperty(QObject, "_selectedNodes", selectedNodesChanged, resetOnDestroy=True)

hoveredNodeChanged = Signal()
# Currently hovered node
hoveredNode = makeProperty(QObject, "_hoveredNode", hoveredNodeChanged, resetOnDestroy=True)
Loading

0 comments on commit 18be350

Please sign in to comment.