Merge pull request #1227 from ChemicalXandco/box_select

[ui] add support for selecting multiple nodes at once
This commit is contained in:
Fabien Castan 2021-05-03 23:42:11 +02:00 committed by GitHub
commit 18be350e6f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 285 additions and 127 deletions

View file

@ -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):

View file

@ -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:

View file

@ -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

View file

@ -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):

View file

@ -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)

View file

@ -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

View file

@ -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
} }

View file

@ -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