mirror of
https://github.com/alicevision/Meshroom.git
synced 2025-04-29 02:08:08 +02:00
Merge pull request #1227 from ChemicalXandco/box_select
[ui] add support for selecting multiple nodes at once
This commit is contained in:
commit
18be350e6f
8 changed files with 285 additions and 127 deletions
|
@ -113,6 +113,7 @@ class QObjectListModel(QtCore.QAbstractListModel):
|
||||||
############
|
############
|
||||||
# List API #
|
# List API #
|
||||||
############
|
############
|
||||||
|
@QtCore.Slot(QtCore.QObject)
|
||||||
def append(self, obj):
|
def append(self, obj):
|
||||||
""" Insert object at the end of the model. """
|
""" Insert object at the end of the model. """
|
||||||
self.extend([obj])
|
self.extend([obj])
|
||||||
|
@ -182,6 +183,7 @@ class QObjectListModel(QtCore.QAbstractListModel):
|
||||||
self.endRemoveRows()
|
self.endRemoveRows()
|
||||||
self.countChanged.emit()
|
self.countChanged.emit()
|
||||||
|
|
||||||
|
@QtCore.Slot(QtCore.QObject)
|
||||||
def remove(self, obj):
|
def remove(self, obj):
|
||||||
""" Removes the first occurrence of the given object. Raises a ValueError if not in list. """
|
""" Removes the first occurrence of the given object. Raises a ValueError if not in list. """
|
||||||
if not self.contains(obj):
|
if not self.contains(obj):
|
||||||
|
|
|
@ -362,29 +362,15 @@ class Graph(BaseObject):
|
||||||
child.resetValue()
|
child.resetValue()
|
||||||
return node, skippedEdges
|
return node, skippedEdges
|
||||||
|
|
||||||
def duplicateNode(self, srcNode):
|
def duplicateNodes(self, srcNodes):
|
||||||
""" Duplicate a node in the graph with its connections.
|
""" Duplicate nodes in the graph with their connections.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
srcNode: the node to duplicate
|
srcNodes: the nodes 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
|
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
OrderedDict[Node, Node]: the source->duplicate map
|
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
|
# use OrderedDict to keep duplicated nodes creation order
|
||||||
duplicates = OrderedDict()
|
duplicates = OrderedDict()
|
||||||
|
|
||||||
|
@ -1146,11 +1132,6 @@ class Graph(BaseObject):
|
||||||
for node in self.nodes:
|
for node in self.nodes:
|
||||||
node.clearSubmittedChunks()
|
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):
|
def iterChunksByStatus(self, status):
|
||||||
""" Iterate over NodeChunks with the given status """
|
""" Iterate over NodeChunks with the given status """
|
||||||
for node in self.nodes:
|
for node in self.nodes:
|
||||||
|
|
|
@ -706,7 +706,6 @@ class BaseNode(BaseObject):
|
||||||
def _isComputed(self):
|
def _isComputed(self):
|
||||||
return self.hasStatus(Status.SUCCESS)
|
return self.hasStatus(Status.SUCCESS)
|
||||||
|
|
||||||
@Slot()
|
|
||||||
def clearData(self):
|
def clearData(self):
|
||||||
""" Delete this Node internal folder.
|
""" Delete this Node internal folder.
|
||||||
Status will be reset to Status.NONE
|
Status will be reset to Status.NONE
|
||||||
|
|
|
@ -159,7 +159,7 @@ class RemoveNodeCommand(GraphCommand):
|
||||||
|
|
||||||
def redoImpl(self):
|
def redoImpl(self):
|
||||||
# only keep outEdges since inEdges are serialized in nodeDict
|
# 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
|
return True
|
||||||
|
|
||||||
def undoImpl(self):
|
def undoImpl(self):
|
||||||
|
@ -173,33 +173,25 @@ class RemoveNodeCommand(GraphCommand):
|
||||||
self.graph.attribute(dstAttr))
|
self.graph.attribute(dstAttr))
|
||||||
|
|
||||||
|
|
||||||
class DuplicateNodeCommand(GraphCommand):
|
class DuplicateNodesCommand(GraphCommand):
|
||||||
"""
|
"""
|
||||||
Handle node duplication in a Graph.
|
Handle node duplication in a Graph.
|
||||||
"""
|
"""
|
||||||
def __init__(self, graph, srcNode, duplicateFollowingNodes, parent=None):
|
def __init__(self, graph, srcNodes, parent=None):
|
||||||
super(DuplicateNodeCommand, self).__init__(graph, parent)
|
super(DuplicateNodesCommand, self).__init__(graph, parent)
|
||||||
self.srcNodeName = srcNode.name
|
self.srcNodeNames = [ n.name for n in srcNodes ]
|
||||||
self.duplicateFollowingNodes = duplicateFollowingNodes
|
self.setText("Duplicate Nodes")
|
||||||
self.duplicates = []
|
|
||||||
|
|
||||||
def redoImpl(self):
|
def redoImpl(self):
|
||||||
srcNode = self.graph.node(self.srcNodeName)
|
srcNodes = [ self.graph.node(i) for i in self.srcNodeNames ]
|
||||||
|
duplicates = list(self.graph.duplicateNodes(srcNodes).values())
|
||||||
if self.duplicateFollowingNodes:
|
self.duplicates = [ n.name for n in duplicates ]
|
||||||
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]
|
|
||||||
return duplicates
|
return duplicates
|
||||||
|
|
||||||
def undoImpl(self):
|
def undoImpl(self):
|
||||||
# delete all the duplicated nodes
|
# remove all duplicates
|
||||||
for nodeName in self.duplicates:
|
for duplicate in self.duplicates:
|
||||||
self.graph.removeNode(nodeName)
|
self.graph.removeNode(duplicate)
|
||||||
|
|
||||||
|
|
||||||
class SetAttributeCommand(GraphCommand):
|
class SetAttributeCommand(GraphCommand):
|
||||||
|
|
|
@ -273,6 +273,7 @@ class UIGraph(QObject):
|
||||||
self._sortedDFSChunks = QObjectListModel(parent=self)
|
self._sortedDFSChunks = QObjectListModel(parent=self)
|
||||||
self._layout = GraphLayout(self)
|
self._layout = GraphLayout(self)
|
||||||
self._selectedNode = None
|
self._selectedNode = None
|
||||||
|
self._selectedNodes = QObjectListModel(parent=self)
|
||||||
self._hoveredNode = None
|
self._hoveredNode = None
|
||||||
|
|
||||||
self.computeStatusChanged.connect(self.updateLockedUndoStack)
|
self.computeStatusChanged.connect(self.updateLockedUndoStack)
|
||||||
|
@ -499,36 +500,114 @@ class UIGraph(QObject):
|
||||||
position = Position(position.x(), position.y())
|
position = Position(position.x(), position.y())
|
||||||
return self.push(commands.AddNodeCommand(self._graph, nodeType, position=position, **kwargs))
|
return self.push(commands.AddNodeCommand(self._graph, nodeType, position=position, **kwargs))
|
||||||
|
|
||||||
@Slot(Node, QPoint)
|
def filterNodes(self, nodes):
|
||||||
def moveNode(self, node, position):
|
"""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:
|
Args:
|
||||||
node (Node): the node to move
|
node (Node): the node to move
|
||||||
position (QPoint): the target position
|
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):
|
if isinstance(position, QPoint):
|
||||||
position = Position(position.x(), position.y())
|
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(Node)
|
@Slot(QObject)
|
||||||
def removeNode(self, node):
|
def removeNodes(self, nodes):
|
||||||
|
"""
|
||||||
|
Remove 'nodes' from the graph.
|
||||||
|
|
||||||
|
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))
|
self.push(commands.RemoveNodeCommand(self._graph, node))
|
||||||
|
|
||||||
@Slot(Node)
|
@Slot(QObject)
|
||||||
def removeNodesFrom(self, startNode):
|
def removeNodesFrom(self, nodes):
|
||||||
"""
|
"""
|
||||||
Remove all nodes starting from 'startNode' to graph leaves.
|
Remove all nodes starting from 'startNode' to graph leaves.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
startNode (Node): the node to start from.
|
startNode (Node): the node to start from.
|
||||||
"""
|
"""
|
||||||
with self.groupedGraphModification("Remove Nodes from {}".format(startNode.name)):
|
with self.groupedGraphModification("Remove Nodes From Selected Nodes"):
|
||||||
nodes, _ = self._graph.dfsOnDiscover(startNodes=[startNode], reverse=True, dependenciesOnly=True)
|
nodesToRemove, _ = self._graph.dfsOnDiscover(startNodes=nodes, reverse=True, dependenciesOnly=True)
|
||||||
# Perform nodes removal from leaves to start node so that edges
|
# Perform nodes removal from leaves to start node so that edges
|
||||||
# can be re-created in correct order on redo.
|
# can be re-created in correct order on redo.
|
||||||
for node in reversed(nodes):
|
self.removeNodes(list(reversed(nodesToRemove)))
|
||||||
self.removeNode(node)
|
|
||||||
|
@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)
|
@Slot(Attribute, Attribute)
|
||||||
def addEdge(self, src, dst):
|
def addEdge(self, src, dst):
|
||||||
|
@ -563,31 +642,6 @@ class UIGraph(QObject):
|
||||||
""" Reset 'attribute' to its default value """
|
""" Reset 'attribute' to its default value """
|
||||||
self.push(commands.SetAttributeCommand(self._graph, attribute, attribute.defaultValue()))
|
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)
|
@Slot(CompatibilityNode, result=Node)
|
||||||
def upgradeNode(self, node):
|
def upgradeNode(self, node):
|
||||||
""" Upgrade a CompatibilityNode. """
|
""" Upgrade a CompatibilityNode. """
|
||||||
|
@ -621,9 +675,55 @@ class UIGraph(QObject):
|
||||||
def removeAttribute(self, attribute):
|
def removeAttribute(self, attribute):
|
||||||
self.push(commands.ListAttributeRemoveCommand(self._graph, 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):
|
def clearNodeSelection(self):
|
||||||
""" Clear node selection. """
|
""" Clear all node selection. """
|
||||||
self.selectedNode = None
|
self._selectedNode = None
|
||||||
|
self._selectedNodes.clear()
|
||||||
|
self.selectedNodeChanged.emit()
|
||||||
|
self.selectedNodesChanged.emit()
|
||||||
|
|
||||||
def clearNodeHover(self):
|
def clearNodeHover(self):
|
||||||
""" Reset currently hovered node to None. """
|
""" Reset currently hovered node to None. """
|
||||||
|
@ -646,9 +746,13 @@ class UIGraph(QObject):
|
||||||
lockedChanged = Signal()
|
lockedChanged = Signal()
|
||||||
|
|
||||||
selectedNodeChanged = Signal()
|
selectedNodeChanged = Signal()
|
||||||
# Currently selected node
|
# Current main selected node
|
||||||
selectedNode = makeProperty(QObject, "_selectedNode", selectedNodeChanged, resetOnDestroy=True)
|
selectedNode = makeProperty(QObject, "_selectedNode", selectedNodeChanged, resetOnDestroy=True)
|
||||||
|
|
||||||
|
selectedNodesChanged = Signal()
|
||||||
|
# Currently selected nodes
|
||||||
|
selectedNodes = makeProperty(QObject, "_selectedNodes", selectedNodesChanged, resetOnDestroy=True)
|
||||||
|
|
||||||
hoveredNodeChanged = Signal()
|
hoveredNodeChanged = Signal()
|
||||||
# Currently hovered node
|
# Currently hovered node
|
||||||
hoveredNode = makeProperty(QObject, "_hoveredNode", hoveredNodeChanged, resetOnDestroy=True)
|
hoveredNode = makeProperty(QObject, "_hoveredNode", hoveredNodeChanged, resetOnDestroy=True)
|
||||||
|
|
|
@ -55,18 +55,35 @@ Item {
|
||||||
function selectNode(node)
|
function selectNode(node)
|
||||||
{
|
{
|
||||||
uigraph.selectedNode = node
|
uigraph.selectedNode = node
|
||||||
|
if (node !== null) {
|
||||||
|
uigraph.appendSelection(node)
|
||||||
|
uigraph.selectedNodesChanged()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Duplicate a node and optionnally all the following ones
|
/// Duplicate a node and optionnally all the following ones
|
||||||
function duplicateNode(node, duplicateFollowingNodes) {
|
function duplicateNode(duplicateFollowingNodes) {
|
||||||
var nodes = uigraph.duplicateNode(node, duplicateFollowingNodes)
|
if (duplicateFollowingNodes) {
|
||||||
selectNode(nodes[0])
|
var nodes = uigraph.duplicateNodesFrom(uigraph.selectedNodes)
|
||||||
|
} else {
|
||||||
|
var nodes = uigraph.duplicateNodes(uigraph.selectedNodes)
|
||||||
|
}
|
||||||
|
uigraph.clearNodeSelection()
|
||||||
|
uigraph.selectedNode = nodes[0]
|
||||||
|
uigraph.selectNodes(nodes)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
Keys.onPressed: {
|
Keys.onPressed: {
|
||||||
if(event.key === Qt.Key_F)
|
if(event.key === Qt.Key_F)
|
||||||
fit()
|
fit()
|
||||||
|
if(event.key === Qt.Key_Delete)
|
||||||
|
if(event.modifiers == Qt.AltModifier)
|
||||||
|
uigraph.removeNodesFrom(uigraph.selectedNodes)
|
||||||
|
else
|
||||||
|
uigraph.removeNodes(uigraph.selectedNodes)
|
||||||
|
if(event.key === Qt.Key_D)
|
||||||
|
duplicateNode(event.modifiers == Qt.AltModifier)
|
||||||
}
|
}
|
||||||
|
|
||||||
MouseArea {
|
MouseArea {
|
||||||
|
@ -82,7 +99,7 @@ Item {
|
||||||
hoverEnabled: true
|
hoverEnabled: true
|
||||||
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
|
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
|
||||||
drag.threshold: 0
|
drag.threshold: 0
|
||||||
cursorShape: drag.active ? Qt.ClosedHandCursor : Qt.ArrowCursor
|
cursorShape: drag.target == draggable ? Qt.ClosedHandCursor : Qt.ArrowCursor
|
||||||
|
|
||||||
onWheel: {
|
onWheel: {
|
||||||
var zoomFactor = wheel.angleDelta.y > 0 ? factor : 1/factor
|
var zoomFactor = wheel.angleDelta.y > 0 ? factor : 1/factor
|
||||||
|
@ -98,12 +115,20 @@ Item {
|
||||||
}
|
}
|
||||||
|
|
||||||
onPressed: {
|
onPressed: {
|
||||||
if(mouse.button != Qt.MiddleButton && mouse.modifiers == Qt.NoModifier)
|
if (mouse.button != Qt.MiddleButton && mouse.modifiers == Qt.NoModifier) {
|
||||||
selectNode(null)
|
uigraph.clearNodeSelection()
|
||||||
|
}
|
||||||
if(mouse.button == Qt.MiddleButton || (mouse.button & Qt.LeftButton && mouse.modifiers & Qt.ShiftModifier))
|
if (mouse.button == Qt.LeftButton && (mouse.modifiers == Qt.NoModifier || mouse.modifiers == Qt.ControlModifier)) {
|
||||||
|
boxSelect.startX = mouseX
|
||||||
|
boxSelect.startY = mouseY
|
||||||
|
boxSelectDraggable.x = mouseX
|
||||||
|
boxSelectDraggable.y = mouseY
|
||||||
|
drag.target = boxSelectDraggable
|
||||||
|
}
|
||||||
|
if (mouse.button == Qt.MiddleButton || (mouse.button == Qt.LeftButton && mouse.modifiers & Qt.ShiftModifier)) {
|
||||||
drag.target = draggable // start drag
|
drag.target = draggable // start drag
|
||||||
}
|
}
|
||||||
|
}
|
||||||
onReleased: {
|
onReleased: {
|
||||||
drag.target = undefined // stop drag
|
drag.target = undefined // stop drag
|
||||||
root.forceActiveFocus()
|
root.forceActiveFocus()
|
||||||
|
@ -319,6 +344,7 @@ Item {
|
||||||
property bool canComputeNode: currentNode != null && uigraph.graph.canCompute(currentNode)
|
property bool canComputeNode: currentNode != null && uigraph.graph.canCompute(currentNode)
|
||||||
//canSubmitOrCompute: return int n : 0 >= n <= 3 | n=0 cannot submit or compute | n=1 can compute | n=2 can submit | n=3 can compute & submit
|
//canSubmitOrCompute: return int n : 0 >= n <= 3 | n=0 cannot submit or compute | n=1 can compute | n=2 can submit | n=3 can compute & submit
|
||||||
property int canSubmitOrCompute: currentNode != null && uigraph.graph.canSubmitOrCompute(currentNode)
|
property int canSubmitOrCompute: currentNode != null && uigraph.graph.canSubmitOrCompute(currentNode)
|
||||||
|
width: 220
|
||||||
onClosed: currentNode = null
|
onClosed: currentNode = null
|
||||||
|
|
||||||
MenuItem {
|
MenuItem {
|
||||||
|
@ -355,31 +381,31 @@ Item {
|
||||||
}
|
}
|
||||||
MenuSeparator {}
|
MenuSeparator {}
|
||||||
MenuItem {
|
MenuItem {
|
||||||
text: "Duplicate Node" + (duplicateFollowingButton.hovered ? "s From Here" : "")
|
text: "Duplicate Node(s)" + (duplicateFollowingButton.hovered ? " From Here" : "")
|
||||||
enabled: true
|
enabled: true
|
||||||
onTriggered: duplicateNode(nodeMenu.currentNode, false)
|
onTriggered: duplicateNode(false)
|
||||||
MaterialToolButton {
|
MaterialToolButton {
|
||||||
id: duplicateFollowingButton
|
id: duplicateFollowingButton
|
||||||
height: parent.height
|
height: parent.height
|
||||||
anchors { right: parent.right; rightMargin: parent.padding }
|
anchors { right: parent.right; rightMargin: parent.padding }
|
||||||
text: MaterialIcons.fast_forward
|
text: MaterialIcons.fast_forward
|
||||||
onClicked: {
|
onClicked: {
|
||||||
duplicateNode(nodeMenu.currentNode, true);
|
duplicateNode(true);
|
||||||
nodeMenu.close();
|
nodeMenu.close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
MenuItem {
|
MenuItem {
|
||||||
text: "Remove Node" + (removeFollowingButton.hovered ? "s From Here" : "")
|
text: "Remove Node(s)" + (removeFollowingButton.hovered ? " From Here" : "")
|
||||||
enabled: nodeMenu.currentNode ? !nodeMenu.currentNode.locked : false
|
enabled: nodeMenu.currentNode ? !nodeMenu.currentNode.locked : false
|
||||||
onTriggered: uigraph.removeNode(nodeMenu.currentNode)
|
onTriggered: uigraph.removeNodes(uigraph.selectedNodes)
|
||||||
MaterialToolButton {
|
MaterialToolButton {
|
||||||
id: removeFollowingButton
|
id: removeFollowingButton
|
||||||
height: parent.height
|
height: parent.height
|
||||||
anchors { right: parent.right; rightMargin: parent.padding }
|
anchors { right: parent.right; rightMargin: parent.padding }
|
||||||
text: MaterialIcons.fast_forward
|
text: MaterialIcons.fast_forward
|
||||||
onClicked: {
|
onClicked: {
|
||||||
uigraph.removeNodesFrom(nodeMenu.currentNode);
|
uigraph.removeNodesFrom(uigraph.selectedNodes);
|
||||||
nodeMenu.close();
|
nodeMenu.close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -438,9 +464,9 @@ Item {
|
||||||
|
|
||||||
onAccepted: {
|
onAccepted: {
|
||||||
if(deleteFollowing)
|
if(deleteFollowing)
|
||||||
graph.clearDataFrom(node);
|
uigraph.clearDataFrom(uigraph.selectedNodes);
|
||||||
else
|
else
|
||||||
node.clearData();
|
uigraph.clearData(uigraph.selectedNodes);
|
||||||
}
|
}
|
||||||
onClosed: destroy()
|
onClosed: destroy()
|
||||||
}
|
}
|
||||||
|
@ -454,63 +480,111 @@ Item {
|
||||||
|
|
||||||
model: root.graph ? root.graph.nodes : undefined
|
model: root.graph ? root.graph.nodes : undefined
|
||||||
property bool loaded: model ? count === model.count : false
|
property bool loaded: model ? count === model.count : false
|
||||||
|
property bool dragging: false
|
||||||
|
|
||||||
delegate: Node {
|
delegate: Node {
|
||||||
id: nodeDelegate
|
id: nodeDelegate
|
||||||
|
|
||||||
property bool animatePosition: true
|
|
||||||
|
|
||||||
node: object
|
node: object
|
||||||
width: uigraph.layout.nodeWidth
|
width: uigraph.layout.nodeWidth
|
||||||
|
|
||||||
selected: uigraph.selectedNode === node
|
mainSelected: uigraph.selectedNode === node
|
||||||
|
selected: uigraph.selectedNodes.contains(node)
|
||||||
hovered: uigraph.hoveredNode === node
|
hovered: uigraph.hoveredNode === node
|
||||||
onSelectedChanged: if(selected) forceActiveFocus()
|
|
||||||
|
|
||||||
onAttributePinCreated: registerAttributePin(attribute, pin)
|
onAttributePinCreated: registerAttributePin(attribute, pin)
|
||||||
onAttributePinDeleted: unregisterAttributePin(attribute, pin)
|
onAttributePinDeleted: unregisterAttributePin(attribute, pin)
|
||||||
|
|
||||||
onPressed: {
|
onPressed: {
|
||||||
selectNode(node)
|
if (mouse.button == Qt.LeftButton) {
|
||||||
|
if (mouse.modifiers & Qt.ControlModifier && !(mouse.modifiers & Qt.AltModifier)) {
|
||||||
if(mouse.button == Qt.LeftButton && mouse.modifiers & Qt.AltModifier)
|
if (mainSelected && selected) {
|
||||||
{
|
// left clicking a selected node twice with control will deselect it
|
||||||
duplicateNode(node, true)
|
uigraph.selectedNodes.remove(node)
|
||||||
|
uigraph.selectedNodesChanged()
|
||||||
|
selectNode(null)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else if (mouse.modifiers & Qt.AltModifier) {
|
||||||
|
if (!(mouse.modifiers & Qt.ControlModifier)){
|
||||||
|
uigraph.clearNodeSelection()
|
||||||
|
}
|
||||||
|
uigraph.selectFollowing(node)
|
||||||
|
} else if (!mainSelected && !selected) {
|
||||||
|
uigraph.clearNodeSelection()
|
||||||
|
}
|
||||||
|
} else if (mouse.button == Qt.RightButton) {
|
||||||
|
if (!mainSelected && !selected) {
|
||||||
|
uigraph.clearNodeSelection()
|
||||||
}
|
}
|
||||||
if(mouse.button == Qt.RightButton)
|
|
||||||
{
|
|
||||||
nodeMenu.currentNode = node
|
nodeMenu.currentNode = node
|
||||||
nodeMenu.popup()
|
nodeMenu.popup()
|
||||||
}
|
}
|
||||||
|
selectNode(node)
|
||||||
}
|
}
|
||||||
|
|
||||||
onDoubleClicked: root.nodeDoubleClicked(mouse, node)
|
onDoubleClicked: root.nodeDoubleClicked(mouse, node)
|
||||||
|
|
||||||
onMoved: uigraph.moveNode(node, position)
|
onMoved: uigraph.moveNode(node, position, uigraph.selectedNodes)
|
||||||
|
|
||||||
onEntered: uigraph.hoveredNode = node
|
onEntered: uigraph.hoveredNode = node
|
||||||
onExited: uigraph.hoveredNode = null
|
onExited: uigraph.hoveredNode = null
|
||||||
|
|
||||||
Keys.onDeletePressed: {
|
onPositionChanged: {
|
||||||
if(node.locked)
|
if (dragging && uigraph.selectedNodes.contains(node)) {
|
||||||
return
|
// update all selected nodes positions with this node that is being dragged
|
||||||
if(event.modifiers == Qt.AltModifier)
|
for (var i = 0; i < nodeRepeater.count; i++) {
|
||||||
uigraph.removeNodesFrom(node)
|
var otherNode = nodeRepeater.itemAt(i)
|
||||||
else
|
if (uigraph.selectedNodes.contains(otherNode.node) && otherNode.node != node) {
|
||||||
uigraph.removeNode(node)
|
otherNode.x = otherNode.node.x + (x - node.x)
|
||||||
|
otherNode.y = otherNode.node.y + (y - node.y)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// allow all nodes to know if they are being dragged
|
||||||
|
onDraggingChanged: nodeRepeater.dragging = dragging
|
||||||
|
|
||||||
|
// must not be enabled during drag because the other nodes will be slow to match the movement of the node being dragged
|
||||||
Behavior on x {
|
Behavior on x {
|
||||||
enabled: animatePosition
|
enabled: !nodeRepeater.dragging
|
||||||
NumberAnimation { duration: 100 }
|
NumberAnimation { duration: 100 }
|
||||||
}
|
}
|
||||||
Behavior on y {
|
Behavior on y {
|
||||||
enabled: animatePosition
|
enabled: !nodeRepeater.dragging
|
||||||
NumberAnimation { duration: 100 }
|
NumberAnimation { duration: 100 }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: boxSelect
|
||||||
|
property int startX: 0
|
||||||
|
property int startY: 0
|
||||||
|
property int toX: boxSelectDraggable.x - startX
|
||||||
|
property int toY: boxSelectDraggable.y - startY
|
||||||
|
|
||||||
|
x: toX < 0 ? startX + toX : startX
|
||||||
|
y: toY < 0 ? startY + toY : startY
|
||||||
|
width: Math.abs(toX)
|
||||||
|
height: Math.abs(toY)
|
||||||
|
|
||||||
|
color: "transparent"
|
||||||
|
border.color: activePalette.text
|
||||||
|
visible: mouseArea.drag.target == boxSelectDraggable
|
||||||
|
|
||||||
|
onVisibleChanged: {
|
||||||
|
if (!visible) {
|
||||||
|
uigraph.boxSelect(boxSelect, draggable)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: boxSelectDraggable
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Toolbar
|
// Toolbar
|
||||||
|
|
|
@ -20,8 +20,12 @@ Item {
|
||||||
/// Whether the node is in compatibility mode
|
/// Whether the node is in compatibility mode
|
||||||
readonly property bool isCompatibilityNode: node ? node.hasOwnProperty("compatibilityIssue") : false
|
readonly property bool isCompatibilityNode: node ? node.hasOwnProperty("compatibilityIssue") : false
|
||||||
/// Mouse related states
|
/// Mouse related states
|
||||||
|
property bool mainSelected: false
|
||||||
property bool selected: false
|
property bool selected: false
|
||||||
property bool hovered: false
|
property bool hovered: false
|
||||||
|
property bool dragging: mouseArea.drag.active
|
||||||
|
/// Combined x and y
|
||||||
|
property point position: Qt.point(x, y)
|
||||||
/// Styling
|
/// Styling
|
||||||
property color shadowColor: "#cc000000"
|
property color shadowColor: "#cc000000"
|
||||||
readonly property color defaultColor: isCompatibilityNode ? "#444" : activePalette.base
|
readonly property color defaultColor: isCompatibilityNode ? "#444" : activePalette.base
|
||||||
|
@ -93,6 +97,7 @@ Item {
|
||||||
|
|
||||||
// Main Layout
|
// Main Layout
|
||||||
MouseArea {
|
MouseArea {
|
||||||
|
id: mouseArea
|
||||||
width: parent.width
|
width: parent.width
|
||||||
height: body.height
|
height: body.height
|
||||||
drag.target: root
|
drag.target: root
|
||||||
|
@ -117,9 +122,9 @@ Item {
|
||||||
Rectangle {
|
Rectangle {
|
||||||
anchors.fill: nodeContent
|
anchors.fill: nodeContent
|
||||||
anchors.margins: -border.width
|
anchors.margins: -border.width
|
||||||
visible: root.selected || root.hovered
|
visible: root.mainSelected || root.hovered
|
||||||
border.width: 2.5
|
border.width: 2.5
|
||||||
border.color: root.selected ? activePalette.highlight : Qt.darker(activePalette.highlight, 1.5)
|
border.color: root.mainSelected ? activePalette.highlight : Qt.darker(activePalette.highlight, 1.5)
|
||||||
opacity: 0.9
|
opacity: 0.9
|
||||||
radius: background.radius + border.width
|
radius: background.radius + border.width
|
||||||
color: "transparent"
|
color: "transparent"
|
||||||
|
@ -151,7 +156,7 @@ Item {
|
||||||
id: header
|
id: header
|
||||||
width: parent.width
|
width: parent.width
|
||||||
height: headerLayout.height
|
height: headerLayout.height
|
||||||
color: root.selected ? activePalette.highlight : root.baseColor
|
color: root.mainSelected ? activePalette.highlight : root.selected ? Qt.darker(activePalette.highlight, 1.1): root.baseColor
|
||||||
radius: background.radius
|
radius: background.radius
|
||||||
|
|
||||||
// Fill header's bottom radius
|
// Fill header's bottom radius
|
||||||
|
@ -174,7 +179,7 @@ Item {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
text: node ? node.label : ""
|
text: node ? node.label : ""
|
||||||
padding: 4
|
padding: 4
|
||||||
color: root.selected ? "white" : activePalette.text
|
color: root.mainSelected ? "white" : activePalette.text
|
||||||
elide: Text.ElideMiddle
|
elide: Text.ElideMiddle
|
||||||
font.pointSize: 8
|
font.pointSize: 8
|
||||||
}
|
}
|
||||||
|
|
|
@ -264,7 +264,8 @@ def test_duplicate_nodes():
|
||||||
n3 = g.addNewNode('AppendFiles', input=n1.output, input2=n2.output)
|
n3 = g.addNewNode('AppendFiles', input=n1.output, input2=n2.output)
|
||||||
|
|
||||||
# duplicate from n1
|
# duplicate from n1
|
||||||
nMap = g.duplicateNodesFromNode(fromNode=n1)
|
nodes_to_duplicate, _ = g.dfsOnDiscover(startNodes=[n1], reverse=True, dependenciesOnly=True)
|
||||||
|
nMap = g.duplicateNodes(srcNodes=nodes_to_duplicate)
|
||||||
for s, d in nMap.items():
|
for s, d in nMap.items():
|
||||||
assert s.nodeType == d.nodeType
|
assert s.nodeType == d.nodeType
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue