mirror of
https://github.com/alicevision/Meshroom.git
synced 2025-05-19 12:06:28 +02:00
[ui] add support for selecting multiple nodes at once
This commit is contained in:
parent
c502ee1e73
commit
c253e7d363
7 changed files with 224 additions and 42 deletions
|
@ -113,6 +113,7 @@ class QObjectListModel(QtCore.QAbstractListModel):
|
|||
############
|
||||
# List API #
|
||||
############
|
||||
@QtCore.Slot(QtCore.QObject)
|
||||
def append(self, obj):
|
||||
""" Insert object at the end of the model. """
|
||||
self.extend([obj])
|
||||
|
@ -182,6 +183,7 @@ class QObjectListModel(QtCore.QAbstractListModel):
|
|||
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):
|
||||
|
|
|
@ -385,6 +385,22 @@ class Graph(BaseObject):
|
|||
OrderedDict[Node, Node]: the source->duplicate map
|
||||
"""
|
||||
srcNodes, srcEdges = self.dfsOnDiscover(startNodes=[fromNode], reverse=True, dependenciesOnly=True)
|
||||
return self.duplicateNodes(srcNodes, srcEdges)
|
||||
|
||||
def duplicateNodesFromList(self, nodes):
|
||||
"""
|
||||
Duplicate 'nodes'.
|
||||
|
||||
Args:
|
||||
nodes (list[Node]): the nodes to duplicate
|
||||
|
||||
Returns:
|
||||
OrderedDict[Node, Node]: the source->duplicate map
|
||||
"""
|
||||
srcEdges = [ self.nodeInEdges(n) for n in nodes ]
|
||||
return self.duplicateNodes(nodes, srcEdges)
|
||||
|
||||
def duplicateNodes(self, srcNodes, srcEdges):
|
||||
# use OrderedDict to keep duplicated nodes creation order
|
||||
duplicates = OrderedDict()
|
||||
|
||||
|
|
|
@ -684,7 +684,6 @@ class BaseNode(BaseObject):
|
|||
def _isComputed(self):
|
||||
return self.hasStatus(Status.SUCCESS)
|
||||
|
||||
@Slot()
|
||||
def clearData(self):
|
||||
""" Delete this Node internal folder.
|
||||
Status will be reset to Status.NONE
|
||||
|
|
|
@ -173,7 +173,18 @@ class RemoveNodeCommand(GraphCommand):
|
|||
self.graph.attribute(dstAttr))
|
||||
|
||||
|
||||
class DuplicateNodeCommand(GraphCommand):
|
||||
class _DuplicateNodes(GraphCommand):
|
||||
def __init__(self, graph, parent=None):
|
||||
super(_DuplicateNodes, self).__init__(graph, parent)
|
||||
self.duplicates = []
|
||||
|
||||
def undoImpl(self):
|
||||
# delete all the duplicated nodes
|
||||
for nodeName in self.duplicates:
|
||||
self.graph.removeNode(nodeName)
|
||||
|
||||
|
||||
class DuplicateNodeCommand(_DuplicateNodes):
|
||||
"""
|
||||
Handle node duplication in a Graph.
|
||||
"""
|
||||
|
@ -181,7 +192,6 @@ class DuplicateNodeCommand(GraphCommand):
|
|||
super(DuplicateNodeCommand, self).__init__(graph, parent)
|
||||
self.srcNodeName = srcNode.name
|
||||
self.duplicateFollowingNodes = duplicateFollowingNodes
|
||||
self.duplicates = []
|
||||
|
||||
def redoImpl(self):
|
||||
srcNode = self.graph.node(self.srcNodeName)
|
||||
|
@ -196,10 +206,21 @@ class DuplicateNodeCommand(GraphCommand):
|
|||
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)
|
||||
|
||||
class DuplicateNodeListCommand(_DuplicateNodes):
|
||||
"""
|
||||
Handle node duplication in a Graph.
|
||||
"""
|
||||
def __init__(self, graph, srcNodes, parent=None):
|
||||
super(DuplicateNodeListCommand, self).__init__(graph, parent)
|
||||
self.srcNodeNames = [ srcNode.name for srcNode in srcNodes ]
|
||||
self.setText("Duplicate selected nodes")
|
||||
|
||||
def redoImpl(self):
|
||||
srcNodes = [ self.graph.node(srcNodeName) for srcNodeName in self.srcNodeNames ]
|
||||
duplicates = list(self.graph.duplicateNodesFromList(srcNodes).values())
|
||||
self.duplicates = [ n.name for n in duplicates ]
|
||||
return duplicates
|
||||
|
||||
|
||||
class SetAttributeCommand(GraphCommand):
|
||||
|
|
|
@ -273,6 +273,7 @@ class UIGraph(QObject):
|
|||
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)
|
||||
|
@ -499,6 +500,12 @@ class UIGraph(QObject):
|
|||
position = Position(position.x(), position.y())
|
||||
return self.push(commands.AddNodeCommand(self._graph, nodeType, position=position, **kwargs))
|
||||
|
||||
@Slot(QObject, result=bool)
|
||||
def nodeSelection(self, node):
|
||||
""" If the node is part of the selection or not. """
|
||||
length = len(self._selectedNodes) > 1
|
||||
return length and self._selectedNodes.contains(node) if node else length
|
||||
|
||||
@Slot(Node, QPoint)
|
||||
def moveNode(self, node, position):
|
||||
"""
|
||||
|
@ -509,13 +516,33 @@ class UIGraph(QObject):
|
|||
position (QPoint): the target position
|
||||
"""
|
||||
if isinstance(position, QPoint):
|
||||
if self.nodeSelection(node):
|
||||
self.moveSelectedNodes(position.x() - node.x, position.y() - node.y)
|
||||
return
|
||||
position = Position(position.x(), position.y())
|
||||
self.push(commands.MoveNodeCommand(self._graph, node, position))
|
||||
|
||||
def moveSelectedNodes(self, deltaX, deltaY):
|
||||
with self.groupedGraphModification("Move Selected Nodes"):
|
||||
for node in self._selectedNodes:
|
||||
position = Position(node.x + deltaX, node.y + deltaY)
|
||||
self.push(commands.MoveNodeCommand(self._graph, node, position))
|
||||
|
||||
@Slot(Node)
|
||||
def removeNode(self, node):
|
||||
if self.nodeSelection(node):
|
||||
self.removeSelectedNodes()
|
||||
return
|
||||
if node.locked:
|
||||
return
|
||||
self.push(commands.RemoveNodeCommand(self._graph, node))
|
||||
|
||||
def removeSelectedNodes(self):
|
||||
with self.groupedGraphModification("Remove Selected Nodes"):
|
||||
for node in self._selectedNodes:
|
||||
if not node.locked:
|
||||
self.push(commands.RemoveNodeCommand(self._graph, node))
|
||||
|
||||
@Slot(Node)
|
||||
def removeNodesFrom(self, startNode):
|
||||
"""
|
||||
|
@ -523,12 +550,15 @@ class UIGraph(QObject):
|
|||
Args:
|
||||
startNode (Node): the node to start from.
|
||||
"""
|
||||
if not startNode:
|
||||
return
|
||||
with self.groupedGraphModification("Remove Nodes from {}".format(startNode.name)):
|
||||
nodes, _ = self._graph.dfsOnDiscover(startNodes=[startNode], 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)
|
||||
if not node.locked:
|
||||
self.removeNode(node)
|
||||
|
||||
@Slot(Attribute, Attribute)
|
||||
def addEdge(self, src, dst):
|
||||
|
@ -560,7 +590,7 @@ class UIGraph(QObject):
|
|||
@Slot(Node, bool, result="QVariantList")
|
||||
def duplicateNode(self, srcNode, duplicateFollowingNodes=False):
|
||||
"""
|
||||
Duplicate a node an optionally all the following nodes to graph leaves.
|
||||
Duplicate a node and optionally all the following nodes to graph leaves.
|
||||
|
||||
Args:
|
||||
srcNode (Node): node to start the duplication from
|
||||
|
@ -569,12 +599,18 @@ class UIGraph(QObject):
|
|||
Returns:
|
||||
[Nodes]: the list of duplicated nodes
|
||||
"""
|
||||
title = "Duplicate Nodes from {}" if duplicateFollowingNodes else "Duplicate {}"
|
||||
if duplicateFollowingNodes: title = "Duplicate Nodes from {}"
|
||||
elif self.nodeSelection(srcNode): title = "Duplicate selected nodes"
|
||||
else: title = "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))
|
||||
if self.nodeSelection(srcNode) and not duplicateFollowingNodes:
|
||||
command = commands.DuplicateNodeListCommand(self._graph, self._selectedNodes)
|
||||
else:
|
||||
command = commands.DuplicateNodeCommand(self._graph, srcNode, duplicateFollowingNodes)
|
||||
duplicates = self.push(command)
|
||||
# move nodes below the bounding box formed by the duplicated node(s)
|
||||
bbox = self._layout.boundingBox(duplicates)
|
||||
for n in duplicates:
|
||||
|
@ -582,6 +618,14 @@ class UIGraph(QObject):
|
|||
|
||||
return duplicates
|
||||
|
||||
@Slot(QObject)
|
||||
def clearData(self, node):
|
||||
if self.nodeSelection(node):
|
||||
for n in self._selectedNodes:
|
||||
n.clearData()
|
||||
return
|
||||
node.clearData()
|
||||
|
||||
@Slot(CompatibilityNode, result=Node)
|
||||
def upgradeNode(self, node):
|
||||
""" Upgrade a CompatibilityNode. """
|
||||
|
@ -615,10 +659,33 @@ class UIGraph(QObject):
|
|||
def removeAttribute(self, attribute):
|
||||
self.push(commands.ListAttributeRemoveCommand(self._graph, attribute))
|
||||
|
||||
@Slot(QObject, QObject)
|
||||
def boxSelect(self, selection, draggable):
|
||||
x = selection.x() - draggable.x()
|
||||
y = selection.y() - draggable.y()
|
||||
otherX = x + selection.width()
|
||||
otherY = y + selection.height()
|
||||
x, y, otherX, otherY = [ i / j for i, j in zip([x, y, otherX, otherY], [draggable.scale()] * 4) ]
|
||||
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]):
|
||||
if not self._selectedNodes.contains(n):
|
||||
self._selectedNodes.append(n)
|
||||
self.selectedNodesChanged.emit()
|
||||
|
||||
def clearNodeSelection(self):
|
||||
""" Clear node selection. """
|
||||
self.selectedNode = None
|
||||
|
||||
@Slot()
|
||||
def clearNodesSelections(self):
|
||||
""" Clear multiple nodes selection. """
|
||||
self._selectedNodes.clear()
|
||||
self.selectedNodesChanged.emit()
|
||||
|
||||
def clearNodeHover(self):
|
||||
""" Reset currently hovered node to None. """
|
||||
self.hoveredNode = None
|
||||
|
@ -643,6 +710,10 @@ class UIGraph(QObject):
|
|||
# Currently selected node
|
||||
selectedNode = makeProperty(QObject, "_selectedNode", selectedNodeChanged, resetOnDestroy=True)
|
||||
|
||||
selectedNodesChanged = Signal()
|
||||
# Currently selected nodes to drag
|
||||
selectedNodes = makeProperty(QObject, "_selectedNodes", selectedNodesChanged, resetOnDestroy=True)
|
||||
|
||||
hoveredNodeChanged = Signal()
|
||||
# Currently hovered node
|
||||
hoveredNode = makeProperty(QObject, "_hoveredNode", hoveredNodeChanged, resetOnDestroy=True)
|
||||
|
|
|
@ -67,6 +67,11 @@ Item {
|
|||
Keys.onPressed: {
|
||||
if(event.key === Qt.Key_F)
|
||||
fit()
|
||||
if(event.key === Qt.Key_Delete)
|
||||
if(event.modifiers == Qt.AltModifier)
|
||||
uigraph.removeNodesFrom(uigraph.selectedNode)
|
||||
else
|
||||
uigraph.removeNode(uigraph.selectedNode)
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
|
@ -98,11 +103,20 @@ Item {
|
|||
}
|
||||
|
||||
onPressed: {
|
||||
if(mouse.button != Qt.MiddleButton && mouse.modifiers == Qt.NoModifier)
|
||||
if (mouse.button != Qt.MiddleButton && mouse.modifiers == Qt.NoModifier) {
|
||||
selectNode(null)
|
||||
|
||||
if(mouse.button == Qt.MiddleButton || (mouse.button & Qt.LeftButton && mouse.modifiers & Qt.ShiftModifier))
|
||||
uigraph.clearNodesSelections()
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
onReleased: {
|
||||
drag.target = undefined // stop drag
|
||||
|
@ -310,7 +324,7 @@ Item {
|
|||
}
|
||||
MenuSeparator {}
|
||||
MenuItem {
|
||||
text: "Duplicate Node" + (duplicateFollowingButton.hovered ? "s From Here" : "")
|
||||
text: "Duplicate Node" + (duplicateFollowingButton.hovered ? "s From Here" : uigraph.nodeSelection(nodeMenu.currentNode) ? "s" : "")
|
||||
enabled: true
|
||||
onTriggered: duplicateNode(nodeMenu.currentNode, false)
|
||||
MaterialToolButton {
|
||||
|
@ -325,7 +339,7 @@ Item {
|
|||
}
|
||||
}
|
||||
MenuItem {
|
||||
text: "Remove Node" + (removeFollowingButton.hovered ? "s From Here" : "")
|
||||
text: "Remove Node" + (removeFollowingButton.hovered ? "s From Here" : uigraph.nodeSelection(nodeMenu.currentNode) ? "s" : "")
|
||||
enabled: nodeMenu.currentNode ? !nodeMenu.currentNode.locked : false
|
||||
onTriggered: uigraph.removeNode(nodeMenu.currentNode)
|
||||
MaterialToolButton {
|
||||
|
@ -395,7 +409,7 @@ Item {
|
|||
if(deleteFollowing)
|
||||
graph.clearDataFrom(node);
|
||||
else
|
||||
node.clearData();
|
||||
uigraph.clearData(node);
|
||||
}
|
||||
onClosed: destroy()
|
||||
}
|
||||
|
@ -409,34 +423,47 @@ Item {
|
|||
|
||||
model: root.graph ? root.graph.nodes : undefined
|
||||
property bool loaded: model ? count === model.count : false
|
||||
property bool dragging: false
|
||||
|
||||
delegate: Node {
|
||||
id: nodeDelegate
|
||||
|
||||
property bool animatePosition: true
|
||||
|
||||
node: object
|
||||
width: uigraph.layout.nodeWidth
|
||||
|
||||
selected: uigraph.selectedNode === node
|
||||
mainSelected: uigraph.selectedNode === node
|
||||
selected: uigraph.selectedNodes.contains(node)
|
||||
hovered: uigraph.hoveredNode === node
|
||||
onSelectedChanged: if(selected) forceActiveFocus()
|
||||
|
||||
onAttributePinCreated: registerAttributePin(attribute, pin)
|
||||
onAttributePinDeleted: unregisterAttributePin(attribute, pin)
|
||||
|
||||
onPressed: {
|
||||
selectNode(node)
|
||||
|
||||
if(mouse.button == Qt.LeftButton && mouse.modifiers & Qt.AltModifier)
|
||||
{
|
||||
duplicateNode(node, true)
|
||||
}
|
||||
if(mouse.button == Qt.RightButton)
|
||||
{
|
||||
if (mouse.button == Qt.LeftButton) {
|
||||
if (mouse.modifiers & Qt.ControlModifier) {
|
||||
if (mainSelected) {
|
||||
// left clicking a selected node twice with control will deselect it
|
||||
uigraph.selectedNodes.remove(node)
|
||||
uigraph.selectedNodesChanged()
|
||||
selectNode(null)
|
||||
return
|
||||
} else if (!selected) {
|
||||
uigraph.selectedNodes.append(node)
|
||||
uigraph.selectedNodesChanged()
|
||||
}
|
||||
} else if (mouse.modifiers & Qt.AltModifier) {
|
||||
duplicateNode(node, true)
|
||||
} else if (!mainSelected && !selected) {
|
||||
uigraph.clearNodesSelections()
|
||||
}
|
||||
} else if (mouse.button == Qt.RightButton) {
|
||||
if (!mainSelected && !selected) {
|
||||
uigraph.clearNodesSelections()
|
||||
}
|
||||
nodeMenu.currentNode = node
|
||||
nodeMenu.popup()
|
||||
}
|
||||
selectNode(node)
|
||||
}
|
||||
|
||||
onDoubleClicked: root.nodeDoubleClicked(mouse, node)
|
||||
|
@ -446,26 +473,67 @@ Item {
|
|||
onEntered: uigraph.hoveredNode = node
|
||||
onExited: uigraph.hoveredNode = null
|
||||
|
||||
Keys.onDeletePressed: {
|
||||
if(node.locked)
|
||||
return
|
||||
if(event.modifiers == Qt.AltModifier)
|
||||
uigraph.removeNodesFrom(node)
|
||||
else
|
||||
uigraph.removeNode(node)
|
||||
onPositionChanged: {
|
||||
if (dragging && uigraph.selectedNodes.contains(node)) {
|
||||
// update all selected nodes positions with this node that is being dragged
|
||||
for (var i = 0; i < nodeRepeater.count; i++) {
|
||||
var otherNode = nodeRepeater.itemAt(i)
|
||||
if (uigraph.selectedNodes.contains(otherNode.node) && otherNode.node != 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: {
|
||||
if (dragging) {
|
||||
nodeRepeater.dragging = true
|
||||
} else {
|
||||
nodeRepeater.dragging = false
|
||||
}
|
||||
}
|
||||
|
||||
// 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 {
|
||||
enabled: animatePosition
|
||||
enabled: !nodeRepeater.dragging
|
||||
NumberAnimation { duration: 100 }
|
||||
}
|
||||
Behavior on y {
|
||||
enabled: animatePosition
|
||||
enabled: !nodeRepeater.dragging
|
||||
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
|
||||
|
|
|
@ -20,8 +20,12 @@ Item {
|
|||
/// Whether the node is in compatibility mode
|
||||
readonly property bool isCompatibilityNode: node ? node.hasOwnProperty("compatibilityIssue") : false
|
||||
/// Mouse related states
|
||||
property bool mainSelected: false
|
||||
property bool selected: false
|
||||
property bool hovered: false
|
||||
property bool dragging: mouseArea.drag.active
|
||||
/// Combined x and y
|
||||
property point position: Qt.point(x, y)
|
||||
/// Styling
|
||||
property color shadowColor: "#cc000000"
|
||||
readonly property color defaultColor: isCompatibilityNode ? "#444" : activePalette.base
|
||||
|
@ -93,6 +97,7 @@ Item {
|
|||
|
||||
// Main Layout
|
||||
MouseArea {
|
||||
id: mouseArea
|
||||
width: parent.width
|
||||
height: body.height
|
||||
drag.target: root
|
||||
|
@ -117,9 +122,9 @@ Item {
|
|||
Rectangle {
|
||||
anchors.fill: nodeContent
|
||||
anchors.margins: -border.width
|
||||
visible: root.selected || root.hovered
|
||||
visible: root.mainSelected || root.hovered
|
||||
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
|
||||
radius: background.radius
|
||||
color: "transparent"
|
||||
|
@ -151,7 +156,7 @@ Item {
|
|||
id: header
|
||||
width: parent.width
|
||||
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
|
||||
|
||||
// Fill header's bottom radius
|
||||
|
@ -174,7 +179,7 @@ Item {
|
|||
Layout.fillWidth: true
|
||||
text: node ? node.label : ""
|
||||
padding: 4
|
||||
color: root.selected ? "white" : activePalette.text
|
||||
color: root.mainSelected ? "white" : activePalette.text
|
||||
elide: Text.ElideMiddle
|
||||
font.pointSize: 8
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue