mirror of
https://github.com/alicevision/Meshroom.git
synced 2025-08-06 10:18:42 +02:00
Merge pull request #2605 from alicevision/fix/nodeSelectionPerfs
Refactor Node selection for better UX and performance
This commit is contained in:
commit
2d56016770
11 changed files with 670 additions and 513 deletions
|
@ -1,3 +1,5 @@
|
||||||
|
# [GraphEditor] Indentation fix
|
||||||
|
87c0cef605e4ef2b359d7e678155e79b65b2e762
|
||||||
# [qt6][qml] Clean-up code and harmonize comments
|
# [qt6][qml] Clean-up code and harmonize comments
|
||||||
5a0b1c0c9547b0d00f3f10fae6994d6d8ea0b45e
|
5a0b1c0c9547b0d00f3f10fae6994d6d8ea0b45e
|
||||||
# [nodes] Linting: Clean-up files
|
# [nodes] Linting: Clean-up files
|
||||||
|
|
|
@ -283,6 +283,15 @@ class QObjectListModel(QtCore.QAbstractListModel):
|
||||||
|
|
||||||
self._objectByKey[key] = item
|
self._objectByKey[key] = item
|
||||||
|
|
||||||
|
@QtCore.Slot(int, result=QtCore.QModelIndex)
|
||||||
|
def index(self, row: int, column: int = 0, parent=QtCore.QModelIndex()):
|
||||||
|
""" Returns the model index for the given row, column and parent index. """
|
||||||
|
if parent.isValid() or column != 0:
|
||||||
|
return QtCore.QModelIndex()
|
||||||
|
if row < 0 or row >= self.size():
|
||||||
|
return QtCore.QModelIndex()
|
||||||
|
return self.createIndex(row, column, self._objects[row])
|
||||||
|
|
||||||
def _dereferenceItem(self, item):
|
def _dereferenceItem(self, item):
|
||||||
# Ask for object deletion if parented to the model
|
# Ask for object deletion if parented to the model
|
||||||
if shiboken6.isValid(item) and item.parent() == self:
|
if shiboken6.isValid(item) and item.parent() == self:
|
||||||
|
|
|
@ -1,11 +1,12 @@
|
||||||
|
|
||||||
def registerTypes():
|
def registerTypes():
|
||||||
from PySide6.QtQml import qmlRegisterType
|
from PySide6.QtQml import qmlRegisterType, qmlRegisterSingletonType
|
||||||
from meshroom.ui.components.clipboard import ClipboardHelper
|
from meshroom.ui.components.clipboard import ClipboardHelper
|
||||||
from meshroom.ui.components.edge import EdgeMouseArea
|
from meshroom.ui.components.edge import EdgeMouseArea
|
||||||
from meshroom.ui.components.filepath import FilepathHelper
|
from meshroom.ui.components.filepath import FilepathHelper
|
||||||
from meshroom.ui.components.scene3D import Scene3DHelper, TrackballController, Transformations3DHelper
|
from meshroom.ui.components.scene3D import Scene3DHelper, TrackballController, Transformations3DHelper
|
||||||
from meshroom.ui.components.csvData import CsvData
|
from meshroom.ui.components.csvData import CsvData
|
||||||
|
from meshroom.ui.components.geom2D import Geom2D
|
||||||
|
|
||||||
qmlRegisterType(EdgeMouseArea, "GraphEditor", 1, 0, "EdgeMouseArea")
|
qmlRegisterType(EdgeMouseArea, "GraphEditor", 1, 0, "EdgeMouseArea")
|
||||||
qmlRegisterType(ClipboardHelper, "Meshroom.Helpers", 1, 0, "ClipboardHelper") # TODO: uncreatable
|
qmlRegisterType(ClipboardHelper, "Meshroom.Helpers", 1, 0, "ClipboardHelper") # TODO: uncreatable
|
||||||
|
@ -14,3 +15,5 @@ def registerTypes():
|
||||||
qmlRegisterType(Transformations3DHelper, "Meshroom.Helpers", 1, 0, "Transformations3DHelper") # TODO: uncreatable
|
qmlRegisterType(Transformations3DHelper, "Meshroom.Helpers", 1, 0, "Transformations3DHelper") # TODO: uncreatable
|
||||||
qmlRegisterType(TrackballController, "Meshroom.Helpers", 1, 0, "TrackballController")
|
qmlRegisterType(TrackballController, "Meshroom.Helpers", 1, 0, "TrackballController")
|
||||||
qmlRegisterType(CsvData, "DataObjects", 1, 0, "CsvData")
|
qmlRegisterType(CsvData, "DataObjects", 1, 0, "CsvData")
|
||||||
|
|
||||||
|
qmlRegisterSingletonType(Geom2D, "Meshroom.Helpers", 1, 0, "Geom2D")
|
||||||
|
|
8
meshroom/ui/components/geom2D.py
Normal file
8
meshroom/ui/components/geom2D.py
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
from PySide6.QtCore import QObject, Slot, QRectF
|
||||||
|
|
||||||
|
|
||||||
|
class Geom2D(QObject):
|
||||||
|
@Slot(QRectF, QRectF, result=bool)
|
||||||
|
def rectRectIntersect(self, rect1: QRectF, rect2: QRectF) -> bool:
|
||||||
|
"""Check if two rectangles intersect."""
|
||||||
|
return rect1.intersects(rect2)
|
|
@ -7,8 +7,19 @@ import json
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from threading import Thread, Event, Lock
|
from threading import Thread, Event, Lock
|
||||||
from multiprocessing.pool import ThreadPool
|
from multiprocessing.pool import ThreadPool
|
||||||
|
from typing import Iterator, Optional, Union
|
||||||
|
|
||||||
from PySide6.QtCore import Slot, QJsonValue, QObject, QUrl, Property, Signal, QPoint
|
from PySide6.QtCore import (
|
||||||
|
Slot,
|
||||||
|
QJsonValue,
|
||||||
|
QObject,
|
||||||
|
QUrl,
|
||||||
|
Property,
|
||||||
|
Signal,
|
||||||
|
QPoint,
|
||||||
|
QItemSelectionModel,
|
||||||
|
QItemSelection,
|
||||||
|
)
|
||||||
|
|
||||||
from meshroom.core import sessionUid
|
from meshroom.core import sessionUid
|
||||||
from meshroom.common.qt import QObjectListModel
|
from meshroom.common.qt import QObjectListModel
|
||||||
|
@ -358,7 +369,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._nodeSelection = QItemSelectionModel(self._graph.nodes, parent=self)
|
||||||
self._hoveredNode = None
|
self._hoveredNode = None
|
||||||
|
|
||||||
self.submitLabel = "{projectName}"
|
self.submitLabel = "{projectName}"
|
||||||
|
@ -395,6 +406,8 @@ class UIGraph(QObject):
|
||||||
self._layout.reset()
|
self._layout.reset()
|
||||||
# clear undo-stack after layout
|
# clear undo-stack after layout
|
||||||
self._undoStack.clear()
|
self._undoStack.clear()
|
||||||
|
|
||||||
|
self._nodeSelection.setModel(self._graph.nodes)
|
||||||
self.graphChanged.emit()
|
self.graphChanged.emit()
|
||||||
|
|
||||||
def onGraphUpdated(self):
|
def onGraphUpdated(self):
|
||||||
|
@ -501,9 +514,10 @@ class UIGraph(QObject):
|
||||||
else:
|
else:
|
||||||
self._undoStack.unlock()
|
self._undoStack.unlock()
|
||||||
|
|
||||||
@Slot(QObjectListModel)
|
@Slot()
|
||||||
@Slot(Node)
|
@Slot(Node)
|
||||||
def execute(self, nodes=None):
|
@Slot(list)
|
||||||
|
def execute(self, nodes: Optional[Union[list[Node], Node]] = None):
|
||||||
nodes = [nodes] if not isinstance(nodes, Iterable) and nodes else nodes
|
nodes = [nodes] if not isinstance(nodes, Iterable) and nodes else nodes
|
||||||
self._taskManager.compute(self._graph, nodes)
|
self._taskManager.compute(self._graph, nodes)
|
||||||
self.updateLockedUndoStack() # explicitly call the update while it is already computing
|
self.updateLockedUndoStack() # explicitly call the update while it is already computing
|
||||||
|
@ -539,9 +553,10 @@ class UIGraph(QObject):
|
||||||
n.clearSubmittedChunks()
|
n.clearSubmittedChunks()
|
||||||
self._taskManager.removeNode(n, displayList=True, processList=True)
|
self._taskManager.removeNode(n, displayList=True, processList=True)
|
||||||
|
|
||||||
@Slot(QObjectListModel)
|
@Slot()
|
||||||
@Slot(Node)
|
@Slot(Node)
|
||||||
def submit(self, nodes=None):
|
@Slot(list)
|
||||||
|
def submit(self, nodes: Optional[Union[list[Node], Node]] = None):
|
||||||
""" Submit the graph to the default Submitter.
|
""" Submit the graph to the default Submitter.
|
||||||
If a node is specified, submit this node and its uncomputed predecessors.
|
If a node is specified, submit this node and its uncomputed predecessors.
|
||||||
Otherwise, submit the whole
|
Otherwise, submit the whole
|
||||||
|
@ -636,59 +651,53 @@ 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))
|
||||||
|
|
||||||
def filterNodes(self, nodes):
|
def moveNode(self, node: Node, position: Position):
|
||||||
"""Filter out the nodes that do not exist on the graph."""
|
|
||||||
if not isinstance(nodes, Iterable):
|
|
||||||
nodes = [nodes]
|
|
||||||
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' and also update the positions of 'nodes' if necessary.
|
Move `node` to the given `position`.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
node (Node): the node to move
|
node: The node to move.
|
||||||
position (QPoint): the target position
|
position: The target position.
|
||||||
nodes (list[Node]): the nodes to update the position of
|
|
||||||
"""
|
"""
|
||||||
if not nodes:
|
self.push(commands.MoveNodeCommand(self._graph, node, position))
|
||||||
nodes = [node]
|
|
||||||
nodes = self.filterNodes(nodes)
|
|
||||||
if isinstance(position, QPoint):
|
|
||||||
position = Position(position.x(), position.y())
|
|
||||||
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)
|
@Slot(QPoint)
|
||||||
def removeNodes(self, nodes):
|
def moveSelectedNodesBy(self, offset: QPoint):
|
||||||
|
"""Move all the selected nodes by the given `offset`."""
|
||||||
|
|
||||||
|
with self.groupedGraphModification("Move Selected Nodes"):
|
||||||
|
for node in self.iterSelectedNodes():
|
||||||
|
position = Position(node.x + offset.x(), node.y + offset.y())
|
||||||
|
self.moveNode(node, position)
|
||||||
|
|
||||||
|
@Slot()
|
||||||
|
def removeSelectedNodes(self):
|
||||||
|
"""Remove selected nodes from the graph."""
|
||||||
|
self.removeNodes(list(self.iterSelectedNodes()))
|
||||||
|
|
||||||
|
@Slot(list)
|
||||||
|
def removeNodes(self, nodes: list[Node]):
|
||||||
"""
|
"""
|
||||||
Remove 'nodes' from the graph.
|
Remove 'nodes' from the graph.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
nodes (list[Node]): the nodes to remove
|
nodes: The nodes to remove.
|
||||||
"""
|
"""
|
||||||
nodes = self.filterNodes(nodes)
|
if any(n.locked for n in nodes):
|
||||||
if any([ n.locked for n in nodes ]):
|
|
||||||
return
|
return
|
||||||
with self.groupedGraphModification("Remove Selected Nodes"):
|
|
||||||
|
with self.groupedGraphModification("Remove Nodes"):
|
||||||
for node in nodes:
|
for node in nodes:
|
||||||
self.push(commands.RemoveNodeCommand(self._graph, node))
|
self.push(commands.RemoveNodeCommand(self._graph, node))
|
||||||
|
|
||||||
@Slot(QObject)
|
@Slot(list)
|
||||||
def removeNodesFrom(self, nodes):
|
def removeNodesFrom(self, nodes: list[Node]):
|
||||||
"""
|
"""
|
||||||
Remove all nodes starting from 'startNode' to graph leaves.
|
Remove all nodes starting from 'nodes' to graph leaves.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
startNode (Node): the node to start from.
|
nodes: the nodes to start from.
|
||||||
"""
|
"""
|
||||||
if isinstance(nodes, Node):
|
|
||||||
nodes = [nodes]
|
|
||||||
with self.groupedGraphModification("Remove Nodes From Selected Nodes"):
|
with self.groupedGraphModification("Remove Nodes From Selected Nodes"):
|
||||||
nodesToRemove, _ = self._graph.dfsOnDiscover(startNodes=nodes, reverse=True, dependenciesOnly=True)
|
nodesToRemove, _ = self._graph.dfsOnDiscover(startNodes=nodes, reverse=True, dependenciesOnly=True)
|
||||||
# filter out nodes that will be removed more than once
|
# filter out nodes that will be removed more than once
|
||||||
|
@ -697,17 +706,17 @@ class UIGraph(QObject):
|
||||||
# can be re-created in correct order on redo.
|
# can be re-created in correct order on redo.
|
||||||
self.removeNodes(list(reversed(uniqueNodesToRemove)))
|
self.removeNodes(list(reversed(uniqueNodesToRemove)))
|
||||||
|
|
||||||
@Slot(QObject, result="QVariantList")
|
@Slot(list, result=list)
|
||||||
def duplicateNodes(self, nodes):
|
def duplicateNodes(self, nodes: list[Node]) -> list[Node]:
|
||||||
"""
|
"""
|
||||||
Duplicate 'nodes'.
|
Duplicate 'nodes'.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
nodes (list[Node]): the nodes to duplicate
|
nodes: the nodes to duplicate.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
list[Node]: the list of duplicated nodes
|
The list of duplicated nodes.
|
||||||
"""
|
"""
|
||||||
nodes = self.filterNodes(nodes)
|
|
||||||
nPositions = [(n.x, n.y) for n in self._graph.nodes]
|
nPositions = [(n.x, n.y) for n in self._graph.nodes]
|
||||||
# enable updates between duplication and layout to get correct depths during layout
|
# enable updates between duplication and layout to get correct depths during layout
|
||||||
with self.groupedGraphModification("Duplicate Selected Nodes", disableUpdates=False):
|
with self.groupedGraphModification("Duplicate Selected Nodes", disableUpdates=False):
|
||||||
|
@ -730,18 +739,16 @@ class UIGraph(QObject):
|
||||||
|
|
||||||
return duplicates
|
return duplicates
|
||||||
|
|
||||||
@Slot(QObject, result="QVariantList")
|
@Slot(list, result=list)
|
||||||
def duplicateNodesFrom(self, nodes):
|
def duplicateNodesFrom(self, nodes: list[Node]) -> list[Node]:
|
||||||
"""
|
"""
|
||||||
Duplicate all nodes starting from 'nodes' to graph leaves.
|
Duplicate all nodes starting from 'nodes' to graph leaves.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
nodes (list[Node]): the nodes to start from.
|
node: The nodes to start from.
|
||||||
Returns:
|
Returns:
|
||||||
list[Node]: the list of duplicated nodes
|
The list of duplicated nodes.
|
||||||
"""
|
"""
|
||||||
if isinstance(nodes, Node):
|
|
||||||
nodes = [nodes]
|
|
||||||
with self.groupedGraphModification("Duplicate Nodes From Selected Nodes"):
|
with self.groupedGraphModification("Duplicate Nodes From Selected Nodes"):
|
||||||
nodesToDuplicate, _ = self._graph.dfsOnDiscover(startNodes=nodes, reverse=True, dependenciesOnly=True)
|
nodesToDuplicate, _ = self._graph.dfsOnDiscover(startNodes=nodes, reverse=True, dependenciesOnly=True)
|
||||||
# filter out nodes that will be duplicated more than once
|
# filter out nodes that will be duplicated more than once
|
||||||
|
@ -772,7 +779,7 @@ class UIGraph(QObject):
|
||||||
dst = currentEdge.dst
|
dst = currentEdge.dst
|
||||||
|
|
||||||
for i in range(1, len(listAttribute)):
|
for i in range(1, len(listAttribute)):
|
||||||
duplicates = self.duplicateNodesFrom(dst.node)
|
duplicates = self.duplicateNodesFrom([dst.node])
|
||||||
newNode = duplicates[0]
|
newNode = duplicates[0]
|
||||||
previousEdge = self.graph.edge(newNode.attribute(dst.name))
|
previousEdge = self.graph.edge(newNode.attribute(dst.name))
|
||||||
self.replaceEdge(previousEdge, listAttribute.at(i), previousEdge.dst)
|
self.replaceEdge(previousEdge, listAttribute.at(i), previousEdge.dst)
|
||||||
|
@ -792,25 +799,28 @@ class UIGraph(QObject):
|
||||||
continue
|
continue
|
||||||
occurence = allSrc.index(listAttribute.at(i)) if listAttribute.at(i) in allSrc else -1
|
occurence = allSrc.index(listAttribute.at(i)) if listAttribute.at(i) in allSrc else -1
|
||||||
if occurence != -1:
|
if occurence != -1:
|
||||||
self.removeNodesFrom(self.graph.edges.at(occurence).dst.node)
|
self.removeNodesFrom([self.graph.edges.at(occurence).dst.node])
|
||||||
# update the edges from allSrc
|
# update the edges from allSrc
|
||||||
allSrc = [e.src for e in self._graph.edges.values()]
|
allSrc = [e.src for e in self._graph.edges.values()]
|
||||||
|
|
||||||
|
@Slot()
|
||||||
|
def clearSelectedNodesData(self):
|
||||||
|
"""Clear data from all selected nodes."""
|
||||||
|
self.clearData(self.iterSelectedNodes())
|
||||||
|
|
||||||
@Slot(QObject)
|
@Slot(list)
|
||||||
def clearData(self, nodes):
|
def clearData(self, nodes: list[Node]):
|
||||||
""" Clear data from 'nodes'. """
|
""" Clear data from 'nodes'. """
|
||||||
nodes = self.filterNodes(nodes)
|
|
||||||
for n in nodes:
|
for n in nodes:
|
||||||
n.clearData()
|
n.clearData()
|
||||||
|
|
||||||
@Slot(QObject)
|
@Slot(list)
|
||||||
def clearDataFrom(self, nodes):
|
def clearDataFrom(self, nodes: list[Node]):
|
||||||
"""
|
"""
|
||||||
Clear data from all nodes starting from 'nodes' to graph leaves.
|
Clear data from all nodes starting from 'nodes' to graph leaves.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
nodes (list[Node]): the nodes to start from.
|
nodes: The nodes to start from.
|
||||||
"""
|
"""
|
||||||
self.clearData(self._graph.dfsOnDiscover(startNodes=nodes, reverse=True, dependenciesOnly=True)[0])
|
self.clearData(self._graph.dfsOnDiscover(startNodes=nodes, reverse=True, dependenciesOnly=True)[0])
|
||||||
|
|
||||||
|
@ -934,23 +944,83 @@ class UIGraph(QObject):
|
||||||
with self.groupedGraphModification("Remove Images From All CameraInit Nodes"):
|
with self.groupedGraphModification("Remove Images From All CameraInit Nodes"):
|
||||||
self.push(commands.RemoveImagesCommand(self._graph, list(self.cameraInits)))
|
self.push(commands.RemoveImagesCommand(self._graph, list(self.cameraInits)))
|
||||||
|
|
||||||
@Slot(Node)
|
@Slot(list)
|
||||||
def appendSelection(self, node):
|
@Slot(list, int)
|
||||||
""" Append 'node' to the selection if it is not already part of the selection. """
|
def selectNodes(self, nodes, command=QItemSelectionModel.SelectionFlag.ClearAndSelect):
|
||||||
if not self._selectedNodes.contains(node):
|
"""Update selection with `nodes` using the specified `command`."""
|
||||||
self._selectedNodes.append(node)
|
indices = [self._graph._nodes.indexOf(node) for node in nodes]
|
||||||
|
self.selectNodesByIndices(indices, command)
|
||||||
@Slot("QVariantList")
|
|
||||||
def selectNodes(self, nodes):
|
|
||||||
""" Append 'nodes' to the selection. """
|
|
||||||
for node in nodes:
|
|
||||||
self.appendSelection(node)
|
|
||||||
self.selectedNodesChanged.emit()
|
|
||||||
|
|
||||||
@Slot(Node)
|
@Slot(Node)
|
||||||
def selectFollowing(self, node):
|
@Slot(Node, int)
|
||||||
""" Select all the nodes the depend on 'node'. """
|
def selectFollowing(self, node: Node, command=QItemSelectionModel.SelectionFlag.ClearAndSelect):
|
||||||
self.selectNodes(self._graph.dfsOnDiscover(startNodes=[node], reverse=True, dependenciesOnly=True)[0])
|
"""Select all the nodes that depend on `node`."""
|
||||||
|
self.selectNodes(
|
||||||
|
self._graph.dfsOnDiscover(startNodes=[node], reverse=True, dependenciesOnly=True)[0], command
|
||||||
|
)
|
||||||
|
self.selectedNode = node
|
||||||
|
|
||||||
|
@Slot(int)
|
||||||
|
@Slot(int, int)
|
||||||
|
def selectNodeByIndex(self, index: int, command=QItemSelectionModel.SelectionFlag.ClearAndSelect):
|
||||||
|
"""Update selection with node at the given `index` using the specified `command`."""
|
||||||
|
if isinstance(command, int):
|
||||||
|
command = QItemSelectionModel.SelectionFlag(command)
|
||||||
|
|
||||||
|
self.selectNodesByIndices([index], command)
|
||||||
|
|
||||||
|
if self._nodeSelection.isRowSelected(index):
|
||||||
|
self.selectedNode = self._graph.nodes.at(index)
|
||||||
|
|
||||||
|
@Slot(list)
|
||||||
|
@Slot(list, int)
|
||||||
|
def selectNodesByIndices(
|
||||||
|
self, indices: list[int], command=QItemSelectionModel.SelectionFlag.ClearAndSelect
|
||||||
|
):
|
||||||
|
"""Update selection with node at given `indices` using the specified `command`.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
indices: The list of indices to select.
|
||||||
|
command: The selection command to use.
|
||||||
|
"""
|
||||||
|
if isinstance(command, int):
|
||||||
|
command = QItemSelectionModel.SelectionFlag(command)
|
||||||
|
|
||||||
|
itemSelection = QItemSelection()
|
||||||
|
for index in indices:
|
||||||
|
itemSelection.select(
|
||||||
|
self._graph.nodes.index(index), self._graph.nodes.index(index)
|
||||||
|
)
|
||||||
|
|
||||||
|
self._nodeSelection.select(itemSelection, command)
|
||||||
|
|
||||||
|
if self.selectedNode and not self.isSelected(self.selectedNode):
|
||||||
|
self.selectedNode = None
|
||||||
|
|
||||||
|
def iterSelectedNodes(self) -> Iterator[Node]:
|
||||||
|
"""Iterate over the currently selected nodes."""
|
||||||
|
for idx in self._nodeSelection.selectedRows():
|
||||||
|
yield self._graph.nodes.at(idx.row())
|
||||||
|
|
||||||
|
@Slot(result=list)
|
||||||
|
def getSelectedNodes(self) -> list[Node]:
|
||||||
|
"""Return the list of selected Node instances."""
|
||||||
|
return list(self.iterSelectedNodes())
|
||||||
|
|
||||||
|
@Slot(Node, result=bool)
|
||||||
|
def isSelected(self, node: Node) -> bool:
|
||||||
|
"""Whether `node` is part of the current selection."""
|
||||||
|
return self._nodeSelection.isRowSelected(self._graph.nodes.indexOf(node))
|
||||||
|
|
||||||
|
@Slot()
|
||||||
|
def clearNodeSelection(self):
|
||||||
|
"""Clear all node selection."""
|
||||||
|
self.selectedNode = None
|
||||||
|
self._nodeSelection.clear()
|
||||||
|
|
||||||
|
def clearNodeHover(self):
|
||||||
|
""" Reset currently hovered node to None. """
|
||||||
|
self.hoveredNode = None
|
||||||
|
|
||||||
@Slot(str)
|
@Slot(str)
|
||||||
def setSelectedNodesColor(self, color: str):
|
def setSelectedNodesColor(self, color: str):
|
||||||
|
@ -962,62 +1032,24 @@ class UIGraph(QObject):
|
||||||
# Update the color attribute of the nodes which are currently selected
|
# Update the color attribute of the nodes which are currently selected
|
||||||
with self.groupedGraphModification("Set Nodes Color"):
|
with self.groupedGraphModification("Set Nodes Color"):
|
||||||
# For each of the selected nodes -> Check if the node has a color -> Apply the color if it has
|
# For each of the selected nodes -> Check if the node has a color -> Apply the color if it has
|
||||||
for node in self._selectedNodes:
|
for node in self.iterSelectedNodes():
|
||||||
if node.hasInternalAttribute("color"):
|
if node.hasInternalAttribute("color"):
|
||||||
self.setAttribute(node.internalAttribute("color"), color)
|
self.setAttribute(node.internalAttribute("color"), color)
|
||||||
|
|
||||||
@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 all node selection. """
|
|
||||||
self._selectedNode = None
|
|
||||||
self._selectedNodes.clear()
|
|
||||||
self.selectedNodeChanged.emit()
|
|
||||||
self.selectedNodesChanged.emit()
|
|
||||||
|
|
||||||
def clearNodeHover(self):
|
|
||||||
""" Reset currently hovered node to None. """
|
|
||||||
self.hoveredNode = None
|
|
||||||
|
|
||||||
@Slot(result=str)
|
@Slot(result=str)
|
||||||
def getSelectedNodesContent(self):
|
def getSelectedNodesContent(self) -> str:
|
||||||
"""
|
"""
|
||||||
Return the content of the currently selected nodes in a string, formatted to JSON.
|
Serialize the current node selection and return it as JSON formatted string.
|
||||||
If no node is currently selected, an empty string is returned.
|
|
||||||
"""
|
|
||||||
if self._selectedNodes:
|
|
||||||
d = self._graph.toDict()
|
|
||||||
selection = {}
|
|
||||||
for node in self._selectedNodes:
|
|
||||||
selection[node.name] = d[node.name]
|
|
||||||
return json.dumps(selection, indent=4)
|
|
||||||
return ''
|
|
||||||
|
|
||||||
@Slot(str, QPoint, bool, result="QVariantList")
|
Returns an empty string if the selection is empty.
|
||||||
def pasteNodes(self, clipboardContent, position=None, centerPosition=False):
|
"""
|
||||||
|
if not self._nodeSelection.hasSelection():
|
||||||
|
return ""
|
||||||
|
serializedSelection = {node.name: node.toDict() for node in self.iterSelectedNodes()}
|
||||||
|
return json.dumps(serializedSelection, indent=4)
|
||||||
|
|
||||||
|
@Slot(str, QPoint, bool, result=list)
|
||||||
|
def pasteNodes(self, clipboardContent, position=None, centerPosition=False) -> list[Node]:
|
||||||
"""
|
"""
|
||||||
Parse the content of the clipboard to see whether it contains
|
Parse the content of the clipboard to see whether it contains
|
||||||
valid node descriptions. If that is the case, the nodes described
|
valid node descriptions. If that is the case, the nodes described
|
||||||
|
@ -1154,9 +1186,7 @@ class UIGraph(QObject):
|
||||||
# Current main selected node
|
# Current main selected node
|
||||||
selectedNode = makeProperty(QObject, "_selectedNode", selectedNodeChanged, resetOnDestroy=True)
|
selectedNode = makeProperty(QObject, "_selectedNode", selectedNodeChanged, resetOnDestroy=True)
|
||||||
|
|
||||||
selectedNodesChanged = Signal()
|
nodeSelection = makeProperty(QObject, "_nodeSelection")
|
||||||
# Currently selected nodes
|
|
||||||
selectedNodes = makeProperty(QObject, "_selectedNodes", selectedNodesChanged, resetOnDestroy=True)
|
|
||||||
|
|
||||||
hoveredNodeChanged = Signal()
|
hoveredNodeChanged = Signal()
|
||||||
# Currently hovered node
|
# Currently hovered node
|
||||||
|
|
|
@ -30,21 +30,6 @@ Page {
|
||||||
property alias showImageGallery: imageGalleryVisibilityCB.checked
|
property alias showImageGallery: imageGalleryVisibilityCB.checked
|
||||||
}
|
}
|
||||||
|
|
||||||
// Utility functions for elements in the menubar
|
|
||||||
function getSelectedNodesName() {
|
|
||||||
if (!_reconstruction)
|
|
||||||
return ""
|
|
||||||
var nodesName = ""
|
|
||||||
for (var i = 0; i < _reconstruction.selectedNodes.count; i++) {
|
|
||||||
if (nodesName !== "")
|
|
||||||
nodesName += ", "
|
|
||||||
var node = _reconstruction.selectedNodes.at(i)
|
|
||||||
if(node) {
|
|
||||||
nodesName += node.name
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nodesName
|
|
||||||
}
|
|
||||||
|
|
||||||
property url imagesFolder: {
|
property url imagesFolder: {
|
||||||
var recentImportedImagesFolders = MeshroomApp.recentImportedImagesFolders
|
var recentImportedImagesFolders = MeshroomApp.recentImportedImagesFolders
|
||||||
|
@ -531,31 +516,21 @@ Page {
|
||||||
Action {
|
Action {
|
||||||
id: cutAction
|
id: cutAction
|
||||||
|
|
||||||
property string tooltip: {
|
property string tooltip: "Cut Selected Node(s)"
|
||||||
var s = "Copy selected node"
|
text: "Cut Node(s)"
|
||||||
s += (_reconstruction && _reconstruction.selectedNodes.count > 1 ? "s (" : " (") + getSelectedNodesName()
|
enabled: _reconstruction ? _reconstruction.nodeSelection.hasSelection : false
|
||||||
s += ") to the clipboard and remove them from the graph"
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
text: "Cut Node" + (_reconstruction && _reconstruction.selectedNodes.count > 1 ? "s " : " ")
|
|
||||||
enabled: _reconstruction ? _reconstruction.selectedNodes.count > 0 : false
|
|
||||||
onTriggered: {
|
onTriggered: {
|
||||||
graphEditor.copyNodes()
|
graphEditor.copyNodes()
|
||||||
graphEditor.uigraph.removeNodes(graphEditor.uigraph.selectedNodes)
|
graphEditor.uigraph.removeSelectedNodes()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Action {
|
Action {
|
||||||
id: copyAction
|
id: copyAction
|
||||||
|
|
||||||
property string tooltip: {
|
property string tooltip: "Copy Selected Node(s)"
|
||||||
var s = "Copy selected node"
|
text: "Copy Node(s)"
|
||||||
s += (_reconstruction && _reconstruction.selectedNodes.count > 1 ? "s (" : " (") + getSelectedNodesName()
|
enabled: _reconstruction ? _reconstruction.nodeSelection.hasSelection : false
|
||||||
s += ") to the clipboard"
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
text: "Copy Node" + (_reconstruction && _reconstruction.selectedNodes.count > 1 ? "s " : " ")
|
|
||||||
enabled: _reconstruction ? _reconstruction.selectedNodes.count > 0 : false
|
|
||||||
onTriggered: graphEditor.copyNodes()
|
onTriggered: graphEditor.copyNodes()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
32
meshroom/ui/qml/Controls/DelegateSelectionBox.qml
Normal file
32
meshroom/ui/qml/Controls/DelegateSelectionBox.qml
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
import QtQuick
|
||||||
|
import Meshroom.Helpers
|
||||||
|
|
||||||
|
/*
|
||||||
|
A SelectionBox that can be used to select delegates in a model instantiator (Repeater, ListView...).
|
||||||
|
Interesection test is done in the coordinate system of the container Item, using delegate's bounding boxes.
|
||||||
|
The list of selected indices is emitted when the selection ends.
|
||||||
|
*/
|
||||||
|
|
||||||
|
SelectionBox {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
// The Item instantiating the delegates.
|
||||||
|
property Item modelInstantiator
|
||||||
|
// The Item containing the delegates (used for coordinate mapping).
|
||||||
|
property Item container
|
||||||
|
// Emitted when the selection has ended, with the list of selected indices and modifiers.
|
||||||
|
signal delegateSelectionEnded(list<int> indices, int modifiers)
|
||||||
|
|
||||||
|
onSelectionEnded: function(selectionRect, modifiers) {
|
||||||
|
let selectedIndices = [];
|
||||||
|
const mappedSelectionRect = mapToItem(container, selectionRect);
|
||||||
|
for (var i = 0; i < modelInstantiator.count; ++i) {
|
||||||
|
const delegate = modelInstantiator.itemAt(i);
|
||||||
|
const delegateRect = Qt.rect(delegate.x, delegate.y, delegate.width, delegate.height);
|
||||||
|
if (Geom2D.rectRectIntersect(mappedSelectionRect, delegateRect)) {
|
||||||
|
selectedIndices.push(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
delegateSelectionEnded(selectedIndices, modifiers);
|
||||||
|
}
|
||||||
|
}
|
60
meshroom/ui/qml/Controls/SelectionBox.qml
Normal file
60
meshroom/ui/qml/Controls/SelectionBox.qml
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
import QtQuick
|
||||||
|
|
||||||
|
/*
|
||||||
|
Simple selection box that can be used by a MouseArea.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
1. Create a MouseArea and a SelectionBox.
|
||||||
|
2. Bind the SelectionBox to the MouseArea by setting the `mouseArea` property.
|
||||||
|
3. Call startSelection() with coordinates when the selection starts.
|
||||||
|
4. Call endSelection() when the selection ends.
|
||||||
|
5. Listen to the selectionEnded signal to get the selection rectangle.
|
||||||
|
*/
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property MouseArea mouseArea
|
||||||
|
property alias color: selectionBox.color
|
||||||
|
property alias border: selectionBox.border
|
||||||
|
|
||||||
|
readonly property bool active: mouseArea.drag.target == dragTarget
|
||||||
|
|
||||||
|
signal selectionEnded(rect selectionRect, int modifiers)
|
||||||
|
|
||||||
|
function startSelection(mouse) {
|
||||||
|
dragTarget.startPos.x = dragTarget.x = mouse.x;
|
||||||
|
dragTarget.startPos.y = dragTarget.y = mouse.y;
|
||||||
|
dragTarget.modifiers = mouse.modifiers;
|
||||||
|
mouseArea.drag.target = dragTarget;
|
||||||
|
}
|
||||||
|
|
||||||
|
function endSelection() {
|
||||||
|
if (!active) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
mouseArea.drag.target = null;
|
||||||
|
const rect = Qt.rect(selectionBox.x, selectionBox.y, selectionBox.width, selectionBox.height)
|
||||||
|
selectionEnded(rect, dragTarget.modifiers);
|
||||||
|
}
|
||||||
|
|
||||||
|
visible: active
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: selectionBox
|
||||||
|
color: "#109b9b9b"
|
||||||
|
border.width: 1
|
||||||
|
border.color: "#b4b4b4"
|
||||||
|
|
||||||
|
x: Math.min(dragTarget.startPos.x, dragTarget.x)
|
||||||
|
y: Math.min(dragTarget.startPos.y, dragTarget.y)
|
||||||
|
width: Math.abs(dragTarget.x - dragTarget.startPos.x)
|
||||||
|
height: Math.abs(dragTarget.y - dragTarget.startPos.y)
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: dragTarget
|
||||||
|
property point startPos
|
||||||
|
property var modifiers
|
||||||
|
}
|
||||||
|
}
|
|
@ -17,3 +17,5 @@ IntSelector 1.0 IntSelector.qml
|
||||||
MScrollBar 1.0 MScrollBar.qml
|
MScrollBar 1.0 MScrollBar.qml
|
||||||
MSplitView 1.0 MSplitView.qml
|
MSplitView 1.0 MSplitView.qml
|
||||||
DirectionalLightPane 1.0 DirectionalLightPane.qml
|
DirectionalLightPane 1.0 DirectionalLightPane.qml
|
||||||
|
SelectionBox 1.0 SelectionBox.qml
|
||||||
|
DelegateSelectionBox 1.0 DelegateSelectionBox.qml
|
|
@ -31,8 +31,6 @@ Item {
|
||||||
signal computeRequest(var nodes)
|
signal computeRequest(var nodes)
|
||||||
signal submitRequest(var nodes)
|
signal submitRequest(var nodes)
|
||||||
|
|
||||||
signal dataDeleted()
|
|
||||||
|
|
||||||
property int nbMeshroomScenes: 0
|
property int nbMeshroomScenes: 0
|
||||||
property int nbDraggedFiles: 0
|
property int nbDraggedFiles: 0
|
||||||
signal filesDropped(var drop, var mousePosition) // Files have been dropped
|
signal filesDropped(var drop, var mousePosition) // Files have been dropped
|
||||||
|
@ -61,35 +59,14 @@ Item {
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Select node delegate
|
|
||||||
function selectNode(node) {
|
|
||||||
uigraph.selectedNode = node
|
|
||||||
if (node !== null) {
|
|
||||||
uigraph.appendSelection(node)
|
|
||||||
uigraph.selectedNodesChanged()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onDataDeleted: {
|
|
||||||
if (computeMenuItem.recompute) {
|
|
||||||
computeRequest(uigraph.selectedNodes)
|
|
||||||
computeMenuItem.recompute = false
|
|
||||||
}
|
|
||||||
else if (submitMenuItem.resubmit) {
|
|
||||||
submitRequest(uigraph.selectedNodes)
|
|
||||||
submitMenuItem.resubmit = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Duplicate a node and optionally all the following ones
|
/// Duplicate a node and optionally all the following ones
|
||||||
function duplicateNode(duplicateFollowingNodes) {
|
function duplicateNode(duplicateFollowingNodes) {
|
||||||
var nodes
|
var nodes
|
||||||
if (duplicateFollowingNodes) {
|
if (duplicateFollowingNodes) {
|
||||||
nodes = uigraph.duplicateNodesFrom(uigraph.selectedNodes)
|
nodes = uigraph.duplicateNodesFrom(uigraph.getSelectedNodes())
|
||||||
} else {
|
} else {
|
||||||
nodes = uigraph.duplicateNodes(uigraph.selectedNodes)
|
nodes = uigraph.duplicateNodes(uigraph.getSelectedNodes())
|
||||||
}
|
}
|
||||||
uigraph.clearNodeSelection()
|
|
||||||
uigraph.selectedNode = nodes[0]
|
uigraph.selectedNode = nodes[0]
|
||||||
uigraph.selectNodes(nodes)
|
uigraph.selectNodes(nodes)
|
||||||
}
|
}
|
||||||
|
@ -122,7 +99,6 @@ Item {
|
||||||
var copiedContent = Clipboard.getText()
|
var copiedContent = Clipboard.getText()
|
||||||
var nodes = uigraph.pasteNodes(copiedContent, finalPosition, centerPosition)
|
var nodes = uigraph.pasteNodes(copiedContent, finalPosition, centerPosition)
|
||||||
if (nodes.length > 0) {
|
if (nodes.length > 0) {
|
||||||
uigraph.clearNodeSelection()
|
|
||||||
uigraph.selectedNode = nodes[0]
|
uigraph.selectedNode = nodes[0]
|
||||||
uigraph.selectNodes(nodes)
|
uigraph.selectNodes(nodes)
|
||||||
}
|
}
|
||||||
|
@ -138,15 +114,15 @@ Item {
|
||||||
fit()
|
fit()
|
||||||
} else if (event.key === Qt.Key_Delete) {
|
} else if (event.key === Qt.Key_Delete) {
|
||||||
if (event.modifiers === Qt.AltModifier) {
|
if (event.modifiers === Qt.AltModifier) {
|
||||||
uigraph.removeNodesFrom(uigraph.selectedNodes)
|
uigraph.removeNodesFrom(uigraph.getSelectedNodes())
|
||||||
} else {
|
} else {
|
||||||
uigraph.removeNodes(uigraph.selectedNodes)
|
uigraph.removeSelectedNodes()
|
||||||
}
|
}
|
||||||
} else if (event.key === Qt.Key_D) {
|
} else if (event.key === Qt.Key_D) {
|
||||||
duplicateNode(event.modifiers === Qt.AltModifier)
|
duplicateNode(event.modifiers === Qt.AltModifier)
|
||||||
} else if (event.key === Qt.Key_X && event.modifiers === Qt.ControlModifier) {
|
} else if (event.key === Qt.Key_X && event.modifiers === Qt.ControlModifier) {
|
||||||
copyNodes()
|
copyNodes()
|
||||||
uigraph.removeNodes(uigraph.selectedNodes)
|
uigraph.removeSelectedNodes()
|
||||||
} else if (event.key === Qt.Key_C) {
|
} else if (event.key === Qt.Key_C) {
|
||||||
if (event.modifiers === Qt.ControlModifier) {
|
if (event.modifiers === Qt.ControlModifier) {
|
||||||
copyNodes()
|
copyNodes()
|
||||||
|
@ -195,20 +171,17 @@ Item {
|
||||||
if (mouse.button != Qt.MiddleButton && mouse.modifiers == Qt.NoModifier) {
|
if (mouse.button != Qt.MiddleButton && mouse.modifiers == Qt.NoModifier) {
|
||||||
uigraph.clearNodeSelection()
|
uigraph.clearNodeSelection()
|
||||||
}
|
}
|
||||||
if (mouse.button == Qt.LeftButton && (mouse.modifiers == Qt.NoModifier || mouse.modifiers == Qt.ControlModifier)) {
|
if (mouse.button == Qt.LeftButton && (mouse.modifiers == Qt.NoModifier || mouse.modifiers & (Qt.ControlModifier | Qt.ShiftModifier))) {
|
||||||
boxSelect.startX = mouseX
|
nodeSelectionBox.startSelection(mouse);
|
||||||
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)) {
|
if (mouse.button == Qt.MiddleButton || (mouse.button == Qt.LeftButton && mouse.modifiers & Qt.AltModifier)) {
|
||||||
drag.target = draggable // start drag
|
drag.target = draggable // start drag
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onReleased: {
|
onReleased: {
|
||||||
drag.target = undefined // stop drag
|
nodeSelectionBox.endSelection();
|
||||||
|
drag.target = null;
|
||||||
root.forceActiveFocus()
|
root.forceActiveFocus()
|
||||||
workspaceClicked()
|
workspaceClicked()
|
||||||
}
|
}
|
||||||
|
@ -235,14 +208,13 @@ Item {
|
||||||
height: searchBar.height + nodeMenuRepeater.height + instantiator.height
|
height: searchBar.height + nodeMenuRepeater.height + instantiator.height
|
||||||
|
|
||||||
function createNode(nodeType) {
|
function createNode(nodeType) {
|
||||||
uigraph.clearNodeSelection() // Ensures that only the created node / imported pipeline will be selected
|
|
||||||
|
|
||||||
// "nodeType" might be a pipeline (artificially added in the "Pipelines" category) instead of a node
|
// "nodeType" might be a pipeline (artificially added in the "Pipelines" category) instead of a node
|
||||||
// If it is not a pipeline to import, then it must be a node
|
// If it is not a pipeline to import, then it must be a node
|
||||||
if (!importPipeline(nodeType)) {
|
if (!importPipeline(nodeType)) {
|
||||||
// Add node via the proper command in uigraph
|
// Add node via the proper command in uigraph
|
||||||
var node = uigraph.addNewNode(nodeType, spawnPosition)
|
var node = uigraph.addNewNode(nodeType, spawnPosition);
|
||||||
selectNode(node)
|
uigraph.selectedNode = node;
|
||||||
|
uigraph.selectNodes([node])
|
||||||
}
|
}
|
||||||
close()
|
close()
|
||||||
}
|
}
|
||||||
|
@ -552,134 +524,168 @@ Item {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loader {
|
||||||
|
id: nodeMenuLoader
|
||||||
|
property var currentNode: null
|
||||||
|
active: currentNode != null
|
||||||
|
sourceComponent: nodeMenuComponent
|
||||||
|
|
||||||
|
function load(node) {
|
||||||
|
currentNode = node;
|
||||||
|
}
|
||||||
|
|
||||||
|
function unload() {
|
||||||
|
currentNode = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function showDataDeletionDialog(deleteFollowing: bool, callback) {
|
||||||
|
uigraph.forceNodesStatusUpdate();
|
||||||
|
const dialog = deleteDataDialog.createObject(
|
||||||
|
root,
|
||||||
|
{
|
||||||
|
"node": currentNode,
|
||||||
|
"deleteFollowing": deleteFollowing
|
||||||
|
}
|
||||||
|
);
|
||||||
|
dialog.open();
|
||||||
|
if(callback)
|
||||||
|
dialog.dataDeleted.connect(callback);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Component {
|
||||||
|
id: nodeMenuComponent
|
||||||
Menu {
|
Menu {
|
||||||
id: nodeMenu
|
id: nodeMenu
|
||||||
property var currentNode: null
|
|
||||||
property bool canComputeNode: currentNode != null && uigraph.graph.canComputeTopologically(currentNode)
|
property var currentNode: nodeMenuLoader.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
|
|
||||||
property int canSubmitOrCompute: currentNode != null && uigraph.graph.canSubmitOrCompute(currentNode)
|
// Cache computatibility/submitability status of each selected node.
|
||||||
property bool isComputed: {
|
readonly property var nodeSubmitOrComputeStatus: {
|
||||||
var count = 0
|
var collectedStatus = ({});
|
||||||
for (var i = 0; i < uigraph.selectedNodes.count; ++i) {
|
uigraph.nodeSelection.selectedIndexes.forEach(function(idx) {
|
||||||
var node = uigraph.selectedNodes.at(i)
|
const node = uigraph.graph.nodes.at(idx.row);
|
||||||
if (!node)
|
collectedStatus[node] = uigraph.graph.canSubmitOrCompute(node);
|
||||||
continue
|
});
|
||||||
if (!node.isComputed)
|
return collectedStatus;
|
||||||
return false
|
|
||||||
count += 1
|
|
||||||
}
|
}
|
||||||
return count > 0
|
|
||||||
|
readonly property bool isSelectionFullyComputed: {
|
||||||
|
return uigraph.nodeSelection.selectedIndexes.every(function(idx) {
|
||||||
|
return uigraph.graph.nodes.at(idx.row).isComputed;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
readonly property bool isSelectionOnlyComputableNodes: {
|
||||||
|
return uigraph.nodeSelection.selectedIndexes.every(function(idx) {
|
||||||
|
const node = uigraph.graph.nodes.at(idx.row);
|
||||||
|
return (
|
||||||
|
node.isComputable
|
||||||
|
&& uigraph.graph.canComputeTopologically(node)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
readonly property bool canSelectionBeComputed: {
|
||||||
|
if(!isSelectionOnlyComputableNodes)
|
||||||
|
return false;
|
||||||
|
if(isSelectionFullyComputed)
|
||||||
|
return true;
|
||||||
|
return uigraph.nodeSelection.selectedIndexes.every(function(idx) {
|
||||||
|
const node = uigraph.graph.nodes.at(idx.row);
|
||||||
|
return (
|
||||||
|
node.isComputed
|
||||||
|
// canCompute if canSubmitOrCompute == 1(can compute) or 3(can compute & submit)
|
||||||
|
|| nodeSubmitOrComputeStatus[node] % 2 == 1
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
readonly property bool isSelectionSubmittable: uigraph.canSubmit && isSelectionOnlyComputableNodes
|
||||||
|
|
||||||
|
readonly property bool canSelectionBeSubmitted: {
|
||||||
|
if(!isSelectionOnlyComputableNodes)
|
||||||
|
return false;
|
||||||
|
if(isSelectionFullyComputed)
|
||||||
|
return true;
|
||||||
|
return uigraph.nodeSelection.selectedIndexes.every(function(idx) {
|
||||||
|
const node = uigraph.graph.nodes.at(idx.row);
|
||||||
|
return (
|
||||||
|
node.isComputed
|
||||||
|
// canSubmit if canSubmitOrCompute == 2(can submit) or 3(can compute & submit)
|
||||||
|
|| nodeSubmitOrComputeStatus[node] > 1
|
||||||
|
)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
width: 220
|
width: 220
|
||||||
onClosed: currentNode = null
|
|
||||||
|
Component.onCompleted: popup()
|
||||||
|
onClosed: nodeMenuLoader.unload()
|
||||||
|
|
||||||
MenuItem {
|
MenuItem {
|
||||||
id: computeMenuItem
|
id: computeMenuItem
|
||||||
property bool recompute: false
|
text: nodeMenu.isSelectionFullyComputed ? "Recompute" : "Compute"
|
||||||
text: nodeMenu.isComputed ? "Recompute" : "Compute"
|
visible: nodeMenu.isSelectionOnlyComputableNodes
|
||||||
visible: {
|
|
||||||
var count = 0
|
|
||||||
for (var i = 0; i < uigraph.selectedNodes.count; ++i) {
|
|
||||||
var node = uigraph.selectedNodes.at(i)
|
|
||||||
if (!node)
|
|
||||||
continue
|
|
||||||
if (!node.isComputable)
|
|
||||||
return false
|
|
||||||
count += 1
|
|
||||||
}
|
|
||||||
return count > 0
|
|
||||||
}
|
|
||||||
height: visible ? implicitHeight : 0
|
height: visible ? implicitHeight : 0
|
||||||
|
enabled: nodeMenu.canSelectionBeComputed
|
||||||
enabled: {
|
|
||||||
var canCompute = false
|
|
||||||
for (var i = 0; i < uigraph.selectedNodes.count; ++i) {
|
|
||||||
var node = uigraph.selectedNodes.at(i)
|
|
||||||
if (!node)
|
|
||||||
continue
|
|
||||||
if (uigraph.graph.canComputeTopologically(node)) {
|
|
||||||
if (nodeMenu.isComputed) {
|
|
||||||
canCompute = true
|
|
||||||
} else if (uigraph.graph.canSubmitOrCompute(node) % 2 == 1) {
|
|
||||||
canCompute = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return canCompute // canSubmit if canSubmitOrCompute == 1(can compute) or 3(can compute & submit)
|
|
||||||
}
|
|
||||||
|
|
||||||
onTriggered: {
|
onTriggered: {
|
||||||
if (nodeMenu.isComputed) {
|
if (nodeMenu.isSelectionFullyComputed) {
|
||||||
recompute = true
|
nodeMenuLoader.showDataDeletionDialog(
|
||||||
deleteDataMenuItem.showConfirmationDialog(false)
|
false,
|
||||||
|
function(request, uigraph) {
|
||||||
|
request(uigraph.getSelectedNodes());
|
||||||
|
}.bind(null, computeRequest, uigraph)
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
computeRequest(uigraph.selectedNodes)
|
computeRequest(uigraph.getSelectedNodes());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
MenuItem {
|
MenuItem {
|
||||||
id: submitMenuItem
|
id: submitMenuItem
|
||||||
property bool resubmit: false
|
|
||||||
text: nodeMenu.isComputed ? "Re-Submit" : "Submit"
|
|
||||||
visible: {
|
|
||||||
var count = 0
|
|
||||||
for (var i = 0; i < uigraph.selectedNodes.count; ++i) {
|
|
||||||
var node = uigraph.selectedNodes.at(i)
|
|
||||||
if (node && !node.isComputable)
|
|
||||||
return false
|
|
||||||
count += 1
|
|
||||||
}
|
|
||||||
return count > 0 || uigraph.canSubmit
|
|
||||||
}
|
|
||||||
height: visible ? implicitHeight : 0
|
|
||||||
|
|
||||||
enabled: {
|
text: nodeMenu.isSelectionFullyComputed ? "Re-Submit" : "Submit"
|
||||||
var canSubmit = false
|
visible: nodeMenu.isSelectionSubmittable
|
||||||
for (var i = 0; i < uigraph.selectedNodes.count; ++i) {
|
height: visible ? implicitHeight : 0
|
||||||
var node = uigraph.selectedNodes.at(i)
|
enabled: nodeMenu.canSelectionBeSubmitted
|
||||||
if (!node)
|
|
||||||
continue
|
|
||||||
if (uigraph.graph.canComputeTopologically(node)) {
|
|
||||||
if (nodeMenu.isComputed) {
|
|
||||||
canSubmit = true
|
|
||||||
} else if (uigraph.graph.canSubmitOrCompute(node) > 1) {
|
|
||||||
canSubmit = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return canSubmit
|
|
||||||
}
|
|
||||||
onTriggered: {
|
onTriggered: {
|
||||||
if (nodeMenu.isComputed) {
|
if (nodeMenu.isSelectionFullyComputed) {
|
||||||
resubmit = true
|
nodeMenuLoader.showDataDeletionDialog(
|
||||||
deleteDataMenuItem.showConfirmationDialog(false)
|
false,
|
||||||
|
function(request, uigraph) {
|
||||||
|
request(uigraph.getSelectedNodes());
|
||||||
|
}.bind(null, submitRequest, uigraph)
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
submitRequest(uigraph.selectedNodes)
|
submitRequest(uigraph.getSelectedNodes());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
MenuItem {
|
MenuItem {
|
||||||
text: "Stop Computation"
|
text: "Stop Computation"
|
||||||
enabled: nodeMenu.currentNode ? nodeMenu.currentNode.canBeStopped() : false
|
enabled: nodeMenu.currentNode.canBeStopped()
|
||||||
visible: enabled
|
visible: enabled
|
||||||
height: visible ? implicitHeight : 0
|
height: visible ? implicitHeight : 0
|
||||||
onTriggered: uigraph.stopNodeComputation(nodeMenu.currentNode)
|
onTriggered: uigraph.stopNodeComputation(nodeMenu.currentNode)
|
||||||
}
|
}
|
||||||
MenuItem {
|
MenuItem {
|
||||||
text: "Cancel Computation"
|
text: "Cancel Computation"
|
||||||
enabled: nodeMenu.currentNode ? nodeMenu.currentNode.canBeCanceled() : false
|
enabled: nodeMenu.currentNode.canBeCanceled()
|
||||||
visible: enabled
|
visible: enabled
|
||||||
height: visible ? implicitHeight : 0
|
height: visible ? implicitHeight : 0
|
||||||
onTriggered: uigraph.cancelNodeComputation(nodeMenu.currentNode)
|
onTriggered: uigraph.cancelNodeComputation(nodeMenu.currentNode)
|
||||||
}
|
}
|
||||||
MenuItem {
|
MenuItem {
|
||||||
text: "Open Folder"
|
text: "Open Folder"
|
||||||
visible: nodeMenu.currentNode ? nodeMenu.currentNode.isComputable : false
|
visible: nodeMenu.currentNode.isComputable
|
||||||
height: visible ? implicitHeight : 0
|
height: visible ? implicitHeight : 0
|
||||||
onTriggered: Qt.openUrlExternally(Filepath.stringToUrl(nodeMenu.currentNode.internalFolder))
|
onTriggered: Qt.openUrlExternally(Filepath.stringToUrl(nodeMenu.currentNode.internalFolder))
|
||||||
}
|
}
|
||||||
MenuSeparator {
|
MenuSeparator {
|
||||||
visible: nodeMenu.currentNode ? nodeMenu.currentNode.isComputable : false
|
visible: nodeMenu.currentNode.isComputable
|
||||||
}
|
}
|
||||||
MenuItem {
|
MenuItem {
|
||||||
text: "Cut Node(s)"
|
text: "Cut Node(s)"
|
||||||
|
@ -688,7 +694,7 @@ Item {
|
||||||
ToolTip.visible: hovered
|
ToolTip.visible: hovered
|
||||||
onTriggered: {
|
onTriggered: {
|
||||||
copyNodes()
|
copyNodes()
|
||||||
uigraph.removeNodes(uigraph.selectedNodes)
|
uigraph.removeSelectedNodes()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
MenuItem {
|
MenuItem {
|
||||||
|
@ -728,8 +734,8 @@ Item {
|
||||||
}
|
}
|
||||||
MenuItem {
|
MenuItem {
|
||||||
text: "Remove Node(s)" + (removeFollowingButton.hovered ? " From Here" : "")
|
text: "Remove Node(s)" + (removeFollowingButton.hovered ? " From Here" : "")
|
||||||
enabled: nodeMenu.currentNode ? !nodeMenu.currentNode.locked : false
|
enabled: !nodeMenu.currentNode.locked
|
||||||
onTriggered: uigraph.removeNodes(uigraph.selectedNodes)
|
onTriggered: uigraph.removeSelectedNodes()
|
||||||
MaterialToolButton {
|
MaterialToolButton {
|
||||||
id: removeFollowingButton
|
id: removeFollowingButton
|
||||||
height: parent.height
|
height: parent.height
|
||||||
|
@ -739,18 +745,18 @@ Item {
|
||||||
}
|
}
|
||||||
text: MaterialIcons.fast_forward
|
text: MaterialIcons.fast_forward
|
||||||
onClicked: {
|
onClicked: {
|
||||||
uigraph.removeNodesFrom(uigraph.selectedNodes)
|
uigraph.removeNodesFrom(uigraph.getSelectedNodes())
|
||||||
nodeMenu.close()
|
nodeMenu.close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
MenuSeparator {
|
MenuSeparator {
|
||||||
visible: nodeMenu.currentNode ? nodeMenu.currentNode.isComputable : false
|
visible: nodeMenu.currentNode.isComputable
|
||||||
}
|
}
|
||||||
MenuItem {
|
MenuItem {
|
||||||
id: deleteDataMenuItem
|
id: deleteDataMenuItem
|
||||||
text: "Delete Data" + (deleteFollowingButton.hovered ? " From Here" : "" ) + "..."
|
text: "Delete Data" + (deleteFollowingButton.hovered ? " From Here" : "" ) + "..."
|
||||||
visible: nodeMenu.currentNode ? nodeMenu.currentNode.isComputable : false
|
visible: nodeMenu.currentNode.isComputable
|
||||||
height: visible ? implicitHeight : 0
|
height: visible ? implicitHeight : 0
|
||||||
enabled: {
|
enabled: {
|
||||||
if (!nodeMenu.currentNode)
|
if (!nodeMenu.currentNode)
|
||||||
|
@ -766,18 +772,7 @@ Item {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
function showConfirmationDialog(deleteFollowing) {
|
onTriggered: nodeMenuLoader.showDataDeletionDialog(false)
|
||||||
uigraph.forceNodesStatusUpdate()
|
|
||||||
var obj = deleteDataDialog.createObject(root,
|
|
||||||
{
|
|
||||||
"node": nodeMenu.currentNode,
|
|
||||||
"deleteFollowing": deleteFollowing
|
|
||||||
})
|
|
||||||
obj.open()
|
|
||||||
nodeMenu.close()
|
|
||||||
}
|
|
||||||
|
|
||||||
onTriggered: showConfirmationDialog(false)
|
|
||||||
|
|
||||||
MaterialToolButton {
|
MaterialToolButton {
|
||||||
id: deleteFollowingButton
|
id: deleteFollowingButton
|
||||||
|
@ -787,7 +782,14 @@ Item {
|
||||||
}
|
}
|
||||||
height: parent.height
|
height: parent.height
|
||||||
text: MaterialIcons.fast_forward
|
text: MaterialIcons.fast_forward
|
||||||
onClicked: parent.showConfirmationDialog(true)
|
onClicked: {
|
||||||
|
nodeMenuLoader.showDataDeletionDialog(true);
|
||||||
|
nodeMenu.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Confirmation dialog for node cache deletion
|
// Confirmation dialog for node cache deletion
|
||||||
|
@ -797,27 +799,26 @@ Item {
|
||||||
property var node
|
property var node
|
||||||
property bool deleteFollowing: false
|
property bool deleteFollowing: false
|
||||||
|
|
||||||
|
signal dataDeleted()
|
||||||
|
|
||||||
focus: true
|
focus: true
|
||||||
modal: false
|
modal: false
|
||||||
header.visible: false
|
header.visible: false
|
||||||
|
|
||||||
text: "Delete Data of '" + node.label + "'" + (uigraph.selectedNodes.count > 1 ? " and other selected Nodes" : "") + (deleteFollowing ? " and following Nodes?" : "?")
|
text: "Delete Data of '" + node.label + "'" + (uigraph.nodeSelection.selectedIndexes.length > 1 ? " and other selected Nodes" : "") + (deleteFollowing ? " and following Nodes?" : "?")
|
||||||
helperText: "Warning: This operation cannot be undone."
|
helperText: "Warning: This operation cannot be undone."
|
||||||
standardButtons: Dialog.Yes | Dialog.Cancel
|
standardButtons: Dialog.Yes | Dialog.Cancel
|
||||||
|
|
||||||
onAccepted: {
|
onAccepted: {
|
||||||
if (deleteFollowing)
|
if (deleteFollowing)
|
||||||
uigraph.clearDataFrom(uigraph.selectedNodes)
|
uigraph.clearDataFrom(uigraph.getSelectedNodes());
|
||||||
else
|
else
|
||||||
uigraph.clearData(uigraph.selectedNodes)
|
uigraph.clearSelectedNodesData();
|
||||||
|
dataDeleted();
|
||||||
root.dataDeleted()
|
|
||||||
}
|
}
|
||||||
onClosed: destroy()
|
onClosed: destroy()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Nodes
|
// Nodes
|
||||||
Repeater {
|
Repeater {
|
||||||
|
@ -826,7 +827,8 @@ 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
|
property bool ongoingDrag: false
|
||||||
|
property bool updateSelectionOnClick: false
|
||||||
property var temporaryEdgeAboutToBeRemoved: undefined
|
property var temporaryEdgeAboutToBeRemoved: undefined
|
||||||
|
|
||||||
delegate: Node {
|
delegate: Node {
|
||||||
|
@ -836,44 +838,79 @@ Item {
|
||||||
width: uigraph.layout.nodeWidth
|
width: uigraph.layout.nodeWidth
|
||||||
|
|
||||||
mainSelected: uigraph.selectedNode === node
|
mainSelected: uigraph.selectedNode === node
|
||||||
selected: uigraph.selectedNodes.contains(node)
|
|
||||||
hovered: uigraph.hoveredNode === node
|
hovered: uigraph.hoveredNode === node
|
||||||
|
|
||||||
|
// ItemSelectionModel.hasSelection triggers updates anytime the selectionChanged() signal is emitted.
|
||||||
|
selected: uigraph.nodeSelection.hasSelection ? uigraph.nodeSelection.isRowSelected(index) : false
|
||||||
|
|
||||||
onAttributePinCreated: function(attribute, pin) { registerAttributePin(attribute, pin) }
|
onAttributePinCreated: function(attribute, pin) { registerAttributePin(attribute, pin) }
|
||||||
onAttributePinDeleted: function(attribute, pin) { unregisterAttributePin(attribute, pin) }
|
onAttributePinDeleted: function(attribute, pin) { unregisterAttributePin(attribute, pin) }
|
||||||
|
|
||||||
onPressed: function(mouse) {
|
onPressed: function(mouse) {
|
||||||
|
nodeRepeater.updateSelectionOnClick = true;
|
||||||
|
nodeRepeater.ongoingDrag = true;
|
||||||
|
|
||||||
|
let selectionMode = ItemSelectionModel.NoUpdate;
|
||||||
|
|
||||||
|
if(!selected) {
|
||||||
|
selectionMode = ItemSelectionModel.ClearAndSelect;
|
||||||
|
}
|
||||||
|
|
||||||
if (mouse.button === Qt.LeftButton) {
|
if (mouse.button === Qt.LeftButton) {
|
||||||
if (mouse.modifiers & Qt.ControlModifier && !(mouse.modifiers & Qt.AltModifier)) {
|
if(mouse.modifiers & Qt.ShiftModifier) {
|
||||||
if (mainSelected && selected) {
|
selectionMode = ItemSelectionModel.Select;
|
||||||
// Left clicking a selected node twice with control will deselect it
|
|
||||||
uigraph.selectedNodes.remove(node)
|
|
||||||
uigraph.selectedNodesChanged()
|
|
||||||
selectNode(null)
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
} else if (mouse.modifiers & Qt.AltModifier) {
|
if(mouse.modifiers & Qt.ControlModifier) {
|
||||||
if (!(mouse.modifiers & Qt.ControlModifier)) {
|
selectionMode = ItemSelectionModel.Toggle;
|
||||||
uigraph.clearNodeSelection()
|
|
||||||
}
|
}
|
||||||
uigraph.selectFollowing(node)
|
if(mouse.modifiers & Qt.AltModifier) {
|
||||||
} else if (!mainSelected && !selected) {
|
let selectFollowingMode = ItemSelectionModel.ClearAndSelect;
|
||||||
uigraph.clearNodeSelection()
|
if(mouse.modifiers & Qt.ShiftModifier) {
|
||||||
|
selectFollowingMode = ItemSelectionModel.Select;
|
||||||
}
|
}
|
||||||
} else if (mouse.button === Qt.RightButton) {
|
uigraph.selectFollowing(node, selectFollowingMode);
|
||||||
if (!mainSelected && !selected) {
|
// Indicate selection has been dealt with by setting conservative Select mode.
|
||||||
uigraph.clearNodeSelection()
|
selectionMode = ItemSelectionModel.Select;
|
||||||
}
|
}
|
||||||
nodeMenu.currentNode = node
|
|
||||||
nodeMenu.popup()
|
|
||||||
}
|
}
|
||||||
selectNode(node)
|
else if (mouse.button === Qt.RightButton) {
|
||||||
|
if(selected) {
|
||||||
|
// Keep the full selection when right-clicking on an already selected node.
|
||||||
|
nodeRepeater.updateSelectionOnClick = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(selectionMode != ItemSelectionModel.NoUpdate) {
|
||||||
|
nodeRepeater.updateSelectionOnClick = false;
|
||||||
|
uigraph.selectNodeByIndex(index, selectionMode);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the node is selected after this, make it the active selected node.
|
||||||
|
if(selected) {
|
||||||
|
uigraph.selectedNode = node;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open the node context menu once selection has been updated.
|
||||||
|
if(mouse.button == Qt.RightButton) {
|
||||||
|
nodeMenuLoader.load(node)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
onReleased: function(mouse, wasDragged) {
|
||||||
|
nodeRepeater.ongoingDrag = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only called when the node has not been dragged.
|
||||||
|
onClicked: function(mouse) {
|
||||||
|
if(!nodeRepeater.updateSelectionOnClick) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
uigraph.selectNodeByIndex(index);
|
||||||
}
|
}
|
||||||
|
|
||||||
onDoubleClicked: function(mouse) { root.nodeDoubleClicked(mouse, node) }
|
onDoubleClicked: function(mouse) { root.nodeDoubleClicked(mouse, node) }
|
||||||
|
|
||||||
onMoved: function(position) { uigraph.moveNode(node, position, uigraph.selectedNodes) }
|
|
||||||
|
|
||||||
onEntered: uigraph.hoveredNode = node
|
onEntered: uigraph.hoveredNode = node
|
||||||
onExited: uigraph.hoveredNode = null
|
onExited: uigraph.hoveredNode = null
|
||||||
|
|
||||||
|
@ -899,62 +936,57 @@ Item {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Interactive dragging: move the visual delegates
|
||||||
onPositionChanged: {
|
onPositionChanged: {
|
||||||
if (dragging && uigraph.selectedNodes.contains(node)) {
|
if(!selected || !dragging) {
|
||||||
// Update all selected nodes positions with this node that is being dragged
|
return;
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
// Compute offset between the delegate and the stored node position.
|
||||||
|
const offset = Qt.point(x - node.x, y - node.y);
|
||||||
|
|
||||||
|
uigraph.nodeSelection.selectedIndexes.forEach(function(idx) {
|
||||||
|
if(idx != index) {
|
||||||
|
const delegate = nodeRepeater.itemAt(idx.row);
|
||||||
|
delegate.x = delegate.node.x + offset.x;
|
||||||
|
delegate.y = delegate.node.y + offset.y;
|
||||||
}
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Allow all nodes to know if they are being dragged
|
// After drag: apply the final offset to all selected nodes
|
||||||
onDraggingChanged: nodeRepeater.dragging = dragging
|
onMoved: function(position) {
|
||||||
|
const offset = Qt.point(position.x - node.x, position.y - node.y);
|
||||||
|
uigraph.moveSelectedNodesBy(offset);
|
||||||
|
}
|
||||||
|
|
||||||
// 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: !nodeRepeater.dragging
|
enabled: !nodeRepeater.ongoingDrag
|
||||||
NumberAnimation { duration: 100 }
|
NumberAnimation { duration: 100 }
|
||||||
}
|
}
|
||||||
Behavior on y {
|
Behavior on y {
|
||||||
enabled: !nodeRepeater.dragging
|
enabled: !nodeRepeater.ongoingDrag
|
||||||
NumberAnimation { duration: 100 }
|
NumberAnimation { duration: 100 }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Rectangle {
|
DelegateSelectionBox {
|
||||||
id: boxSelect
|
id: nodeSelectionBox
|
||||||
property int startX: 0
|
mouseArea: mouseArea
|
||||||
property int startY: 0
|
modelInstantiator: nodeRepeater
|
||||||
property int toX: boxSelectDraggable.x - startX
|
container: draggable
|
||||||
property int toY: boxSelectDraggable.y - startY
|
onDelegateSelectionEnded: function(selectedIndices, modifiers) {
|
||||||
|
let selectionMode = ItemSelectionModel.ClearAndSelect;
|
||||||
x: toX < 0 ? startX + toX : startX
|
if(modifiers & Qt.ShiftModifier) {
|
||||||
y: toY < 0 ? startY + toY : startY
|
selectionMode = ItemSelectionModel.Select;
|
||||||
width: Math.abs(toX)
|
} else if(modifiers & Qt.ControlModifier) {
|
||||||
height: Math.abs(toY)
|
selectionMode = ItemSelectionModel.Deselect;
|
||||||
|
|
||||||
color: "transparent"
|
|
||||||
border.color: activePalette.text
|
|
||||||
visible: mouseArea.drag.target == boxSelectDraggable
|
|
||||||
|
|
||||||
onVisibleChanged: {
|
|
||||||
if (!visible) {
|
|
||||||
uigraph.boxSelect(boxSelect, draggable)
|
|
||||||
}
|
}
|
||||||
|
uigraph.selectNodesByIndices(selectedIndices, selectionMode);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Item {
|
|
||||||
id: boxSelectDraggable
|
|
||||||
}
|
|
||||||
|
|
||||||
DropArea {
|
DropArea {
|
||||||
id: dropArea
|
id: dropArea
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
|
|
|
@ -40,6 +40,8 @@ Item {
|
||||||
|
|
||||||
// Mouse interaction related signals
|
// Mouse interaction related signals
|
||||||
signal pressed(var mouse)
|
signal pressed(var mouse)
|
||||||
|
signal released(var mouse)
|
||||||
|
signal clicked(var mouse)
|
||||||
signal doubleClicked(var mouse)
|
signal doubleClicked(var mouse)
|
||||||
signal moved(var position)
|
signal moved(var position)
|
||||||
signal entered()
|
signal entered()
|
||||||
|
@ -125,8 +127,10 @@ Item {
|
||||||
drag.threshold: 2
|
drag.threshold: 2
|
||||||
hoverEnabled: true
|
hoverEnabled: true
|
||||||
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||||
onPressed: function(mouse) { root.pressed(mouse) }
|
onPressed: (mouse) => root.pressed(mouse)
|
||||||
onDoubleClicked: function(mouse) { root.doubleClicked(mouse) }
|
onReleased: (mouse) => root.released(mouse)
|
||||||
|
onClicked: (mouse) => root.clicked(mouse)
|
||||||
|
onDoubleClicked: (mouse) => root.doubleClicked(mouse)
|
||||||
onEntered: root.entered()
|
onEntered: root.entered()
|
||||||
onExited: root.exited()
|
onExited: root.exited()
|
||||||
drag.onActiveChanged: {
|
drag.onActiveChanged: {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue