From fb6c38cbfd21f7c190d17a480666ee4d0bd4f592 Mon Sep 17 00:00:00 2001 From: vicrodh <103399283+vicrodh@users.noreply.github.com> Date: Wed, 26 Aug 2026 02:29:30 -0600 Subject: [PATCH] fix(qbz-qt): preserve collection builder viewport cx --- .../qml/views/myqbz/DiscoBuilderView.qml | 62 ++++++++++++------- 1 file changed, 38 insertions(+), 24 deletions(-) diff --git a/crates/qbz-qt/qml/views/myqbz/DiscoBuilderView.qml b/crates/qbz-qt/qml/views/myqbz/DiscoBuilderView.qml index 1465d6c5f..301b1e397 100644 --- a/crates/qbz-qt/qml/views/myqbz/DiscoBuilderView.qml +++ b/crates/qbz-qt/qml/views/myqbz/DiscoBuilderView.qml @@ -41,7 +41,7 @@ Rectangle { QbzTheme { id: theme } - readonly property var doc: parseDoc() + property var doc: ({}) function parseDoc() { try { return JSON.parse(QbzDisco.discoJson) @@ -49,6 +49,33 @@ Rectangle { return ({}) } } + + // `rows` is a plain JS array, so every Rust publish assigns a brand-new + // ListView model. Capture the viewport BEFORE assigning `doc`; trying to + // recover it from onRowsChanged is already too late because Qt may have + // reset contentY during the binding update. + property int documentEpoch: 0 + function applyDocument() { + var next = root.parseDoc() + var currentArtist = root.doc.artistId || "" + var nextArtist = next.artistId || "" + var preserve = nextArtist !== "" && nextArtist === currentArtist + && next.loading !== true + var savedY = preserve ? rowList.contentY : 0 + root.documentEpoch += 1 + var epoch = root.documentEpoch + root.doc = next + Qt.callLater(function () { + if (epoch !== root.documentEpoch) + return + rowList.forceLayout() + var minY = rowList.originY + var maxY = minY + Math.max(0, + rowList.contentHeight - rowList.height) + rowList.contentY = preserve + ? Math.max(minY, Math.min(savedY, maxY)) : minY + }) + } function t(s) { return QbzSession.tr(s, QbzSession.trRev) } // --- Document slices (spec 02 §5.8 DiscoDoc) -------------------------- @@ -95,14 +122,12 @@ Rectangle { onLoadingChanged: if (root.loading) { root.nameEdited = false root.collectionName = "" - // A fresh load is a fresh table: without this the preserved offset - // (below) survives into the NEXT artist and `restoreScroll` opens his - // discography already scrolled to the previous one's row. Slint's - // Flickable lands at the top because `reset` empties the viewport. - root.keepY = 0 } - Component.onCompleted: if (!root.nameEdited && root.collectionName === "") - root.collectionName = root.defaultName + Component.onCompleted: { + root.applyDocument() + if (!root.nameEdited && root.collectionName === "") + root.collectionName = root.defaultName + } // Footer count. Rust pre-renders the ONE true plural in this domain with // `qbz_i18n::tf` and publishes it as `footerCount` (spec 01 §4/§8.7) — @@ -143,21 +168,6 @@ Rectangle { ] } - // --- Scroll preservation ---------------------------------------------- - // Every mutation (a checkbox, an override, a re-sort) republishes the whole - // DiscoDoc, so `groups` is a NEW array and QQuickItemView::setModel() - // resets contentY to 0 — one checkbox click would throw the user from row - // 40 to the top. Slint's Flickable keeps its viewport-y across the same - // republish, so restoring it IS the parity behaviour. - property real keepY: 0 - onRowsChanged: Qt.callLater(root.restoreScroll) - function restoreScroll() { - if (root.keepY <= 0 || rowList.contentY > 0) - return - rowList.contentY = Math.min( - root.keepY, Math.max(0, rowList.contentHeight - rowList.height)) - } - // --- Everything above the table (the ListView header) ------------------ Component { id: pageHead @@ -409,7 +419,6 @@ Rectangle { model: root.populated ? root.rows : [] header: pageHead footer: bottomPad - onContentYChanged: if (contentY > 0) root.keepY = contentY // ONE row — a group's primary, or one of its alternates (:705-722). // Every field comes off the Rust row descriptor: `restOpacity` is @@ -427,6 +436,11 @@ Rectangle { } } + Connections { + target: QbzDisco + function onDiscoJsonChanged() { root.applyDocument() } + } + // Back/forward scroll memory (controls/ScrollMemory.qml): reports // this container's offset while it is the live page, and restores it // when a back/forward step arms this route.