From 614378ffa4f82f68865f280e0bdbb2980487424b Mon Sep 17 00:00:00 2001 From: Levi Neely Date: Mon, 5 Oct 2026 09:23:34 +0200 Subject: [PATCH] Make the drag grip own its gesture; fix scene mapping Two drag fixes: - The gutter grip still fought the ListView (a Flickable) for the drag. Make it a dedicated control: pressing the grip sets outline.interactive = false for the duration of the press (via a TapHandler), so the Flickable cannot scroll and the grip's DragHandler takes the grab cleanly. Flicking is restored on release. Drops the earlier press-and-hold arming (and the now-unused dragArmed property); a plain press on the grip starts the drag. - Fix a runtime TypeError on every drag move: Item has no mapFromScene(). Use contentItem.mapFromItem(null, x, y), where a null source item means scene coordinates, to convert the DragHandler centroid to ListView content coordinates. GUI builds clean. --- ui/qml/OutlineView.qml | 58 +++++++++++++++++++----------------------- 1 file changed, 26 insertions(+), 32 deletions(-) diff --git a/ui/qml/OutlineView.qml b/ui/qml/OutlineView.qml index 296a955..fd13b24 100644 --- a/ui/qml/OutlineView.qml +++ b/ui/qml/OutlineView.qml @@ -26,10 +26,6 @@ ListView { property var dropInfo: null // {valid, level, parentId, parentTitle, beforeId} property real dropIndicatorY: 0 // content-y of the insertion bar property int dropGapAbove: -2 // visible row above the gap (-1 = top) - // Row whose grip has been "armed" by a press-and-hold, so its DragHandler - // may start a structural drag. This gates the drag behind a hold so a - // quick press-drag still scrolls the ListView (Flickable) instead. - property int dragArmed: -1 // Recompute the drop target from a pointer position in content coords. function updateDrop(contentX, contentY) { @@ -521,53 +517,49 @@ ListView { Layout.fillWidth: true spacing: 8 - // Drag grip (left gutter): press-and-hold to arm, then drag to - // restructure the section (and its subtree). A live indicator - // shows the resolved drop target; releasing commits a refile. - // Horizontal position chooses the depth, vertical the gap. - // - // Gating the drag behind a hold resolves the conflict with the - // ListView (a Flickable): a quick press-drag scrolls the list; - // a hold arms the grip so its DragHandler takes the grab. + // Drag grip (left gutter): a dedicated drag control. Pressing + // it disables ListView flicking for the duration of the press, + // so the grip — not the enclosing Flickable — owns the gesture; + // dragging then restructures the section (and its subtree) and + // releasing commits a refile. Horizontal position chooses the + // depth, vertical the gap. Flicking is restored on release. Rectangle { id: gripLabel Layout.preferredWidth: outline.baseFontSize + 8 Layout.preferredHeight: outline.baseFontSize + 8 radius: 3 - readonly property bool armed: outline.dragArmed === del.index - || outline.dragRow === del.index - color: armed ? theme.highlightColor - : (gripHover.hovered ? theme.alternateBaseColor : "transparent") + readonly property bool active: outline.dragRow === del.index + || gripPress.pressed + color: active ? theme.highlightColor + : (gripHover.hovered ? theme.alternateBaseColor : "transparent") Label { anchors.centerIn: parent text: "⠿" - color: gripLabel.armed ? theme.highlightedTextColor - : theme.placeholderTextColor + color: gripLabel.active ? theme.highlightedTextColor + : theme.placeholderTextColor font.pointSize: outline.baseFontSize - 1 - opacity: gripHover.hovered || gripLabel.armed ? 1.0 : 0.4 + opacity: gripHover.hovered || gripLabel.active ? 1.0 : 0.4 } HoverHandler { id: gripHover - cursorShape: gripLabel.armed ? Qt.SizeAllCursor : Qt.OpenHandCursor + cursorShape: gripLabel.active ? Qt.ClosedHandCursor : Qt.OpenHandCursor } - // Press-and-hold to arm the drag without moving. + // Own the gesture: while the grip is pressed, the ListView + // must not flick. This is what makes the gutter behave as a + // separate control rather than fighting the Flickable. TapHandler { - id: gripHold - longPressThreshold: 0.35 // seconds - onLongPressed: outline.dragArmed = del.index - // A plain tap (no hold) shouldn't arm anything. - onTapped: outline.dragArmed = -1 + id: gripPress + onPressedChanged: outline.interactive = !pressed } - // The structural drag itself. Only grabs once armed, so the - // Flickable keeps scrolling for un-armed press-drags. + // The structural drag. The Flickable is non-interactive + // while pressed, so the handler takes the grab cleanly. DragHandler { id: gripDrag target: null // track only; don't move the item - enabled: gripLabel.armed dragThreshold: 3 grabPermissions: PointerHandler.CanTakeOverFromItems | PointerHandler.CanTakeOverFromHandlersOfDifferentType @@ -577,14 +569,16 @@ ListView { outline.dragRow = del.index } else { outline.commitDrop() - outline.dragArmed = -1 + outline.interactive = true // belt-and-suspenders } } onCentroidChanged: { if (!active || outline.dragRow < 0) return // Map the scene pointer position to ListView content - // coordinates and resolve the drop. - var p = outline.contentItem.mapFromScene( + // coordinates. mapFromItem(null, ...) treats the + // source coordinates as scene coordinates. + var p = outline.contentItem.mapFromItem( + null, centroid.scenePosition.x, centroid.scenePosition.y) outline.updateDrop(p.x, p.y)