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.
This commit is contained in:
Levi Neely 2026-10-05 09:23:34 +02:00
parent ed65235940
commit 614378ffa4
1 changed files with 26 additions and 32 deletions

View File

@ -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
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
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)