diff --git a/crates/qbz-qt/qml/views/local/LocalTracksTab.qml b/crates/qbz-qt/qml/views/local/LocalTracksTab.qml index b45cc2734..497045c05 100644 --- a/crates/qbz-qt/qml/views/local/LocalTracksTab.qml +++ b/crates/qbz-qt/qml/views/local/LocalTracksTab.qml @@ -33,6 +33,9 @@ Item { // contentY change before that property turns true. All of those changes // describe the same page miss and must collapse into one bridge call. property bool pageRequestPending: false + // Programmatic viewport restoration emits the same Flickable signals as + // user input. Keep those signals from recursively requesting another page. + property bool restoringPageAnchor: false // Invalidates a deferred viewport restore when another document/model // replacement wins before Qt's next event-loop turn. property int entriesEpoch: 0 @@ -40,6 +43,7 @@ Item { function requestNextPage(force) { if (root.nativeModelActive || root.pageRequestPending + || root.restoringPageAnchor || !QbzLocal.localTracksHasMore || QbzLocal.localTracksLoadingMore || QbzLocal.localTracksLoading) @@ -88,8 +92,12 @@ Item { return { "id": String(root.entries[i].row.id), // Position inside the viewport, including a partially clipped - // first row or a group header immediately above it. - "screenY": cell ? cell.mapToItem(list, 0, 0).y : 0 + // first row or a group header immediately above it. A ListView + // delegate's y is in contentItem coordinates, so subtracting the + // viewport's contentY is the visual coordinate. mapToItem(list) + // preserves the content coordinate here and would add thousands + // of rows back into the restore calculation. + "screenY": cell ? cell.y - list.contentY : 0 } } @@ -115,15 +123,17 @@ Item { Qt.callLater(function () { if (epoch !== root.entriesEpoch || root.nativeModelActive) return + root.restoringPageAnchor = true list.forceLayout() list.positionViewAtIndex(target, ListView.Beginning) list.forceLayout() var cell = list.itemAtIndex(target) - var currentScreenY = cell ? cell.mapToItem(list, 0, 0).y : 0 + var currentScreenY = cell ? cell.y - list.contentY : 0 var wanted = list.contentY + currentScreenY - anchor.screenY var minY = list.originY var maxY = minY + Math.max(0, list.contentHeight - list.height) list.contentY = Math.max(minY, Math.min(wanted, maxY)) + root.restoringPageAnchor = false root.reportSoon() }) } diff --git a/crates/qbz-qt/src/local_state.rs b/crates/qbz-qt/src/local_state.rs index 6a86c15a5..f4f2eeb26 100644 --- a/crates/qbz-qt/src/local_state.rs +++ b/crates/qbz-qt/src/local_state.rs @@ -528,7 +528,12 @@ mod phase_a_tests { let qml = include_str!("../qml/views/local/LocalTracksTab.qml"); assert!(qml.contains("function capturePageAnchor()")); assert!(qml.contains("String(root.entries[i].row.id)")); - assert!(qml.contains("cell.mapToItem(list, 0, 0).y")); + assert!(qml.contains("cell.y - list.contentY")); + assert!(!qml.contains("cell.mapToItem(list")); + assert!(qml.contains("|| root.restoringPageAnchor")); + assert!(qml.contains( + "var wanted = list.contentY + currentScreenY - anchor.screenY" + )); assert!(qml.contains("function restorePageAnchor(anchor, nextEntries, epoch)")); assert!(qml.contains("Qt.callLater(function ()")); assert!(qml.contains("list.positionViewAtIndex(target, ListView.Beginning)"));