diff --git a/meshroom/ui/graph.py b/meshroom/ui/graph.py index c588810a72..749b6d16dc 100644 --- a/meshroom/ui/graph.py +++ b/meshroom/ui/graph.py @@ -22,6 +22,7 @@ QPoint, QItemSelectionModel, QItemSelection, + QPointF ) from meshroom.core import sessionUid @@ -1692,6 +1693,9 @@ def canSubmitNode(self, node: Node) -> bool: selectedChunkChanged = Signal() selectedChunk = makeProperty(QObject, "_selectedChunk", selectedChunkChanged, resetOnDestroy=True) + edgeDraggingChanged = Signal(bool) + edgeDragMousePosChanged = Signal(float, float) + nodeSelection = makeProperty(QObject, "_nodeSelection") hoveredNodeChanged = Signal() diff --git a/meshroom/ui/qml/GraphEditor/AttributePin.qml b/meshroom/ui/qml/GraphEditor/AttributePin.qml index 0e2ce054c3..fb2f63922a 100755 --- a/meshroom/ui/qml/GraphEditor/AttributePin.qml +++ b/meshroom/ui/qml/GraphEditor/AttributePin.qml @@ -318,6 +318,10 @@ RowLayout { property double initialX: 0.0 property double initialY: 0.0 + onClicked: function() { + root.clicked() + } + onPressed: function(mouse) { root.pressed(mouse) isPressed = true @@ -329,19 +333,27 @@ RowLayout { inputDragTarget.Drag.drop() isPressed = false dragTriggered = false + _currentScene.edgeDraggingChanged(false) } - onClicked: function() { - root.clicked() + onCanceled: { + isPressed = false + dragTriggered = false + _currentScene.edgeDraggingChanged(false) } onPositionChanged: function(mouse) { // If there has been a significant move (5px along the -X or -Y axis) while the // mouse is being pressed, then we can consider being in the dragging state if (isPressed && (Math.abs(mouse.x - initialX) >= 5.0 || Math.abs(mouse.y - initialY) >= 5.0)) { - dragTriggered = true + if (!dragTriggered) { + dragTriggered = true + _currentScene.edgeDraggingChanged(true) + } + var windowCoords = inputConnectMA.mapToItem(null, mouse.x, mouse.y) // null will map to window + _currentScene.edgeDragMousePosChanged(windowCoords.x, windowCoords.y) } - } + } } Edge { @@ -570,21 +582,31 @@ RowLayout { initialY = mouse.y } + onClicked: function() { + root.clicked() + } + onReleased: function(mouse) { outputDragTarget.Drag.drop() isPressed = false dragTriggered = false + _currentScene.edgeDraggingChanged(false) } - onClicked: function() { - root.clicked() + onCanceled: { + isPressed = false + dragTriggered = false + _currentScene.edgeDraggingChanged(false) } onPositionChanged: function(mouse) { - // If there has been a significant move (5px along the -X or -Y axis) while the mouse is being - // pressed, then we can consider being in the dragging state. if (isPressed && (Math.abs(mouse.x - initialX) >= 5.0 || Math.abs(mouse.y - initialY) >= 5.0)) { - dragTriggered = true + if (!dragTriggered) { + dragTriggered = true + _currentScene.edgeDraggingChanged(true) + } + var windowCoords = outputConnectMA.mapToItem(null, mouse.x, mouse.y) // null will map to window + _currentScene.edgeDragMousePosChanged(windowCoords.x, windowCoords.y) } } } diff --git a/meshroom/ui/qml/GraphEditor/GraphEditor.qml b/meshroom/ui/qml/GraphEditor/GraphEditor.qml index e08ed27d03..51a4cc67aa 100755 --- a/meshroom/ui/qml/GraphEditor/GraphEditor.qml +++ b/meshroom/ui/qml/GraphEditor/GraphEditor.qml @@ -1,6 +1,7 @@ import QtQuick import QtQuick.Controls import QtQuick.Layouts +import Qt5Compat.GraphicalEffects import Controls 1.0 import MaterialIcons 2.2 @@ -18,6 +19,22 @@ Item { property variant nodeTypesModel: null /// The list of node types that can be instantiated property real maxZoom: 2.0 property real minZoom: 0.1 + + // Autoscroll properties + readonly property int autoscrollMargin: 100 // Border thickness (width in pixels) + readonly property int autoscrollSpeed: 15 // Speed factor + readonly property real maxScrollSpeed: 3.0 // Clamp scroll speed + property bool isDraggingEdge: false // Tracks whether we are dragging an AttributePin + property point dragMousePos: Qt.point(0, 0) // Position of the mouse during edge dragging + + readonly property real borderHighlightOpacity: 0.05 + readonly property color borderHighlightColor: Colors.blue + readonly property real borderHighlightWidth: 3 + property bool highlightedBorderLeft: false + property bool highlightedBorderRight: false + property bool highlightedBorderTop: false + property bool highlightedBorderBottom: false + property var edgeAboutToBeRemoved: undefined property var _attributeToDelegate: ({}) @@ -156,6 +173,102 @@ Item { } } + // Listen to the UIGraph edge drag infos + Connections { + target: _currentScene + ignoreUnknownSignals: true + + function onEdgeDraggingChanged(dragging) { + root.isDraggingEdge = dragging + root.highlightedBorderRight=false + root.highlightedBorderLeft=false + root.highlightedBorderTop=false + root.highlightedBorderBottom=false + } + + function onEdgeDragMousePosChanged(windowX, windowY) { + // Translate window coordinates into the GraphEditor coordinates + var localPos = root.mapFromItem(null, windowX, windowY) + root.dragMousePos = Qt.point(localPos.x, localPos.y) + } + } + + // Add a timer to auto-scroll on the graph when edge dragging is active + Timer { + id: autoscrollTimer + interval: 20 + running: root.isDraggingEdge + repeat: true + + onTriggered: { + var mouseX = root.dragMousePos.x + var mouseY = root.dragMousePos.y + var deltaX = 0 + var deltaY = 0 + + // Left + if (mouseX < root.autoscrollMargin) { + var factorX = Math.min(root.maxScrollSpeed, (root.autoscrollMargin - mouseX) / root.autoscrollMargin) + deltaX = root.autoscrollSpeed * factorX + } + // Right + else if (mouseX > root.width - root.autoscrollMargin) { + var factorX = Math.min(root.maxScrollSpeed, (mouseX - (root.width - root.autoscrollMargin)) / root.autoscrollMargin) + deltaX = -root.autoscrollSpeed * factorX + } + // Top + if (mouseY < root.autoscrollMargin) { + var factorY = Math.min(root.maxScrollSpeed, (root.autoscrollMargin - mouseY) / root.autoscrollMargin) + deltaY = root.autoscrollSpeed * factorY + } + // Bottom + else if (mouseY > root.height - root.autoscrollMargin) { + var factorY = Math.min(root.maxScrollSpeed, (mouseY - (root.height - root.autoscrollMargin)) / root.autoscrollMargin) + deltaY = -root.autoscrollSpeed * factorY + } + + root.highlightedBorderRight=false + root.highlightedBorderLeft=false + root.highlightedBorderTop=false + root.highlightedBorderBottom=false + + // Apply scroll on workspaxe + if (deltaX !== 0 || deltaY !== 0) { + draggable.x += deltaX + draggable.y += deltaY + workspaceMoved() + + if (deltaX>0) { root.highlightedBorderLeft=true } + else if (deltaX<0) { root.highlightedBorderRight=true } + if (deltaY>0) { root.highlightedBorderTop=true } + else if (deltaY<0) { root.highlightedBorderBottom=true } + } + } + } + + // Display scroll direction + Rectangle { + enabled: root.isDraggingEdge + z: 100 + color: "transparent" + border.color: root.borderHighlightColor + border.width: root.borderHighlightWidth + + // Combine fill and margins into one anchor block + anchors { + fill: parent + leftMargin: root.highlightedBorderLeft ? 0 : -root.borderHighlightWidth + rightMargin: root.highlightedBorderRight ? 0 : -root.borderHighlightWidth + topMargin: root.highlightedBorderTop ? 0 : -root.borderHighlightWidth + bottomMargin: root.highlightedBorderBottom ? 0 : -root.borderHighlightWidth + } + + Behavior on anchors.leftMargin { NumberAnimation { duration: 200; easing.type: Easing.OutQuad } } + Behavior on anchors.rightMargin { NumberAnimation { duration: 200; easing.type: Easing.OutQuad } } + Behavior on anchors.topMargin { NumberAnimation { duration: 200; easing.type: Easing.OutQuad } } + Behavior on anchors.bottomMargin { NumberAnimation { duration: 200; easing.type: Easing.OutQuad } } + } + MouseArea { id: mouseArea anchors.fill: parent @@ -208,8 +321,8 @@ Item { root.forceActiveFocus() workspaceClicked() } - - onPositionChanged: { + + onPositionChanged: function(mouse) { if (drag.active) workspaceMoved() }