diff --git a/meshroom/core/node.py b/meshroom/core/node.py index f9191ad4d6..394877df34 100644 --- a/meshroom/core/node.py +++ b/meshroom/core/node.py @@ -1878,6 +1878,9 @@ def _isCompatibilityNode(self): def _isInputNode(self): return isinstance(self.nodeDesc, desc.InputNode) + def _isInitNode(self): + return isinstance(self.nodeDesc, desc.InitNode) + def _isBackdropNode(self) -> bool: return False @@ -2173,6 +2176,7 @@ def _hasDisplayableShape(self): notify=globalStatusChanged) isCompatibilityNode = Property(bool, lambda self: self._isCompatibilityNode(), constant=True) isInputNode = Property(bool, lambda self: self._isInputNode(), constant=True) + isInitNode = Property(bool, lambda self: self._isInitNode(), constant=True) isBackdropNode = Property(bool, lambda self: self._isBackdropNode(), constant=True) globalExecMode = Property(str, globalExecMode.fget, notify=globalStatusChanged) diff --git a/meshroom/ui/qml/GraphEditor/GraphEditor.qml b/meshroom/ui/qml/GraphEditor/GraphEditor.qml index ca5ff8b3e2..9e5a94e45b 100755 --- a/meshroom/ui/qml/GraphEditor/GraphEditor.qml +++ b/meshroom/ui/qml/GraphEditor/GraphEditor.qml @@ -59,6 +59,20 @@ Item { return undefined } + /// Get the InitNode delegate at the given position in 'draggable' coordinates, or null + function initNodeDelegateAt(draggableX, draggableY) { + for (var i = 0; i < nodeRepeater.count; ++i) { + var delegate = nodeRepeater.getItemAt(i) + if (delegate && delegate.node && delegate.node.isInitNode && !delegate.readOnly) { + if (draggableX >= delegate.x && draggableX <= delegate.x + delegate.width && + draggableY >= delegate.y && draggableY <= delegate.y + delegate.height) { + return delegate + } + } + } + return null + } + /// Duplicate a node and optionally all the following ones function duplicateNode(duplicateFollowingNodes) { var nodes @@ -1238,6 +1252,10 @@ Item { id: dropArea anchors.fill: parent keys: ["text/uri-list"] + + /// The InitNode delegate currently being hovered during a file drag, if any + property var hoveredInitNodeDelegate: null + onEntered: function(drag) { nbMeshroomScenes = 0 nbDraggedFiles = drag.urls.length @@ -1247,9 +1265,50 @@ Item { nbMeshroomScenes++ } }) + + // Check if the drag enters directly over an InitNode + var draggablePos = mapToItem(draggable, drag.x, drag.y) + hoveredInitNodeDelegate = initNodeDelegateAt(draggablePos.x, draggablePos.y) + if (hoveredInitNodeDelegate) { + hoveredInitNodeDelegate.initNodeDragHover = true + } + } + + onPositionChanged: function(drag) { + // Update which InitNode (if any) the drag is currently over + var draggablePos = mapToItem(draggable, drag.x, drag.y) + var newDelegate = initNodeDelegateAt(draggablePos.x, draggablePos.y) + if (newDelegate !== hoveredInitNodeDelegate) { + if (hoveredInitNodeDelegate) { + hoveredInitNodeDelegate.initNodeDragHover = false + } + hoveredInitNodeDelegate = newDelegate + if (hoveredInitNodeDelegate) { + hoveredInitNodeDelegate.initNodeDragHover = true + } + } + } + + onExited: { + if (hoveredInitNodeDelegate) { + hoveredInitNodeDelegate.initNodeDragHover = false + hoveredInitNodeDelegate = null + } } onDropped: function(drop) { + // Clear visual feedback + if (hoveredInitNodeDelegate) { + hoveredInitNodeDelegate.initNodeDragHover = false + } + + // If dropped on an InitNode, forward the files to its initialize() method + if (hoveredInitNodeDelegate) { + _currentScene.initializeNode(hoveredInitNodeDelegate.node, drop.urls) + hoveredInitNodeDelegate = null + return + } + if (nbMeshroomScenes == nbDraggedFiles || nbMeshroomScenes == 0) { // Retrieve mouse position and convert coordinate system // from pixel values to graph reference system diff --git a/meshroom/ui/qml/GraphEditor/Node.qml b/meshroom/ui/qml/GraphEditor/Node.qml index 7e5bb44ea9..85463103b9 100755 --- a/meshroom/ui/qml/GraphEditor/Node.qml +++ b/meshroom/ui/qml/GraphEditor/Node.qml @@ -877,4 +877,16 @@ Item { } } } + + /// Set to true by the GraphEditor's DropArea when files are dragged over this InitNode + property bool initNodeDragHover: false + + // Highlight overlay shown when files are dragged over this InitNode + Rectangle { + anchors.fill: mouseArea + visible: root.node && root.node.isInitNode && root.initNodeDragHover + color: Colors.sysPalette.highlight + opacity: 0.35 + radius: background.radius + } } diff --git a/meshroom/ui/scene.py b/meshroom/ui/scene.py index b13334551b..c6a907e3e4 100755 --- a/meshroom/ui/scene.py +++ b/meshroom/ui/scene.py @@ -821,6 +821,24 @@ def getFilesByTypeFromDrop(self, urls): "meshroomScenes": filesByType.meshroomScenes, "other": filesByType.other} + @Slot(QObject, "QList") + def initializeNode(self, node, urls): + """ + Initialize an InitNode with the provided list of dropped files/URLs. + + Converts the list of QUrls to local file paths and calls the node descriptor's + initialize() method with those paths as inputs. + + Args: + node (Node): the InitNode to initialize + urls (list of QUrl): the list of dropped file/directory URLs + """ + if not isinstance(node.nodeDesc, meshroom.core.desc.InitNode): + logging.warning(f"initializeNode called on non-InitNode '{node.name}' — ignoring.") + return + inputs = [localFile for url in urls if (localFile := url.toLocalFile())] + node.nodeDesc.initialize(node, inputs, []) + def importImagesFromFolder(self, path, recursive=False): """