From 461238bd6fa5e462ad92147d4efb2a882f73364a Mon Sep 17 00:00:00 2001 From: vicrodh <103399283+vicrodh@users.noreply.github.com> Date: Mon, 14 Sep 2026 05:22:44 -0600 Subject: [PATCH] fix(qt): modal forms open clean, never with the last session's text QbzLineEdit re-seeds its box only when `text` CHANGES and only while the box is not focused; QbzTextArea only while unfocused. Modals that reset their drafts on the close edge (the box still holds focus for that frame) or reopen with the seed they closed on produced neither, so the box kept the previous input while the draft said otherwise. Both controls gain `reset(value)`, which writes the box directly, and every modal form calls it from its open handler: playlist edit and create, MyQBZ create / rename / description, folder create and edit, the link resolver, the rip wizard and the library folder editor. The MyQBZ create name now resets on the open edge, and its edit panel re-seeds on every open, also for the same collection. Renaming a Qobuz playlist also patches the playlist manager's warm cache, which the editor seeds from before any fetch: reopening the editor right after Save showed the pre-save name until the background reload landed, and forever while offline. cc --- .../qbz-qt/qml/controls/FolderEditPanel.qml | 2 ++ crates/qbz-qt/qml/controls/FolderModals.qml | 1 + .../qbz-qt/qml/controls/LinkResolverModal.qml | 3 +++ crates/qbz-qt/qml/controls/MyQbzModals.qml | 25 ++++++++++++++++--- .../qml/controls/PlaylistCreateModal.qml | 5 ++++ .../qbz-qt/qml/controls/PlaylistEditModal.qml | 6 +++++ crates/qbz-qt/qml/controls/QbzLineEdit.qml | 14 +++++++++++ crates/qbz-qt/qml/controls/QbzTextArea.qml | 6 +++++ crates/qbz-qt/qml/controls/RipWizardModal.qml | 8 ++++++ .../qml/settings/LibFolderEditModal.qml | 2 +- crates/qbz-qt/src/playlist_qt.rs | 20 +++++++++++++++ 11 files changed, 88 insertions(+), 4 deletions(-) diff --git a/crates/qbz-qt/qml/controls/FolderEditPanel.qml b/crates/qbz-qt/qml/controls/FolderEditPanel.qml index 1075750cd..b564c3846 100644 --- a/crates/qbz-qt/qml/controls/FolderEditPanel.qml +++ b/crates/qbz-qt/qml/controls/FolderEditPanel.qml @@ -95,6 +95,8 @@ Item { root.draftPreset = root.doc.iconPreset || "folder" root.draftColor = root.doc.iconColor || "" root.draftHidden = root.doc.isHidden === true + // The box as well as the draft (QbzLineEdit.reset()). + nameField.reset() } /// Focus the name field. Split out so the host can call it AFTER the panel diff --git a/crates/qbz-qt/qml/controls/FolderModals.qml b/crates/qbz-qt/qml/controls/FolderModals.qml index b25432249..d49064a6a 100644 --- a/crates/qbz-qt/qml/controls/FolderModals.qml +++ b/crates/qbz-qt/qml/controls/FolderModals.qml @@ -103,6 +103,7 @@ Item { if (root.createOpen) { // Its OWN draft, reset on its OWN open transition. root.draftCreateName = "" + createField.reset("") createScope.forceActiveFocus() createField.focusField() } else { diff --git a/crates/qbz-qt/qml/controls/LinkResolverModal.qml b/crates/qbz-qt/qml/controls/LinkResolverModal.qml index 6d328a7a5..77133b299 100644 --- a/crates/qbz-qt/qml/controls/LinkResolverModal.qml +++ b/crates/qbz-qt/qml/controls/LinkResolverModal.qml @@ -25,6 +25,9 @@ Item { onVisibleChanged: { if (visible) { + // Rust cleared `url` before opening; sync the box regardless of + // where focus sat at the last close (QbzLineEdit.reset()). + urlInput.reset(QbzLink.url) urlInput.focusField() } else { // Return focus only if it still belongs to this modal. A search diff --git a/crates/qbz-qt/qml/controls/MyQbzModals.qml b/crates/qbz-qt/qml/controls/MyQbzModals.qml index 139236af4..29b0facb7 100644 --- a/crates/qbz-qt/qml/controls/MyQbzModals.qml +++ b/crates/qbz-qt/qml/controls/MyQbzModals.qml @@ -76,10 +76,19 @@ Item { property string name: "" readonly property bool canCreate: createPanel.name !== "" && !createPanel.creating - // A fresh open starts with an empty field. + // A fresh open starts with an empty field — reset on the OPEN edge, + // never the close edge: at close the box still holds focus for a + // frame, its focus-gated re-seed skips, and the next open found the + // draft already "" with the old text still in the box (the + // dirty-form report of 2026-09-14). `reset()` syncs the box itself. Connections { target: root - function onCreateOpenChanged() { createPanel.name = "" } + function onCreateOpenChanged() { + if (!root.createOpen) + return + createPanel.name = "" + createNameField.reset("") + } } Rectangle { @@ -166,6 +175,7 @@ Item { font.letterSpacing: 1.5 } QbzLineEdit { + id: createNameField width: parent.width height: 34 text: createPanel.name @@ -255,10 +265,17 @@ Item { // frame, and then `open` never changes and the prefill would be stale. // Keying it on the whole document instead would clobber what the user // is typing every time an unrelated field (`busy`) flips. - readonly property string editKey: editPanel.mode + "|" + (root.editDoc.name || "") + // The open flag rides along so a reopen on the SAME collection + // re-seeds too (a cancelled rename left its draft behind). + readonly property string editKey: (root.editOpen ? "1" : "0") + "|" + + editPanel.mode + "|" + (root.editDoc.name || "") onEditKeyChanged: { editPanel.draftName = root.editDoc.name || "" editPanel.draftDescription = root.editDoc.description || "" + if (root.editOpen) { + renameField.reset() + descriptionField.reset() + } } Rectangle { @@ -338,6 +355,7 @@ Item { // --- Body: rename QbzLineEdit { + id: renameField visible: editPanel.isRename width: parent.width height: visible ? 34 : 0 @@ -350,6 +368,7 @@ Item { // --- Body: description QbzTextArea { + id: descriptionField visible: editPanel.isDescription width: parent.width // A FLAT 96, never `visible ? 96 : 0`: a Column does not diff --git a/crates/qbz-qt/qml/controls/PlaylistCreateModal.qml b/crates/qbz-qt/qml/controls/PlaylistCreateModal.qml index 3209e23bf..d2eaba1cd 100644 --- a/crates/qbz-qt/qml/controls/PlaylistCreateModal.qml +++ b/crates/qbz-qt/qml/controls/PlaylistCreateModal.qml @@ -103,6 +103,10 @@ Item { root.draftIsPublic = false // Offline forces it ON and locks it (reference :184-216). root.draftOfflineOnly = root.offlineLocked + // The boxes too (QbzLineEdit.reset()): "" is also what the + // drafts held after the last cancel, so nothing above re-seeds. + nameField.reset("") + descriptionField.reset("") scope.forceActiveFocus() nameField.focusField() } else { @@ -274,6 +278,7 @@ Item { font.weight: theme.weightMedium } QbzTextArea { + id: descriptionField width: parent.width height: 80 text: root.draftDescription diff --git a/crates/qbz-qt/qml/controls/PlaylistEditModal.qml b/crates/qbz-qt/qml/controls/PlaylistEditModal.qml index b1b580efd..449518fd5 100644 --- a/crates/qbz-qt/qml/controls/PlaylistEditModal.qml +++ b/crates/qbz-qt/qml/controls/PlaylistEditModal.qml @@ -135,6 +135,11 @@ Item { root.draftDescription = root.doc.description || "" root.draftOfflineOnly = root.doc.offlineOnly === true root.draftFolderId = root.doc.folderId || "" + // The boxes, not just the drafts: a seed equal to the previous + // one changes nothing above, and a box that was still focused + // at close never re-seeded (QbzLineEdit.reset()). + nameField.reset() + descriptionField.reset() scope.forceActiveFocus() nameField.focusField() } @@ -332,6 +337,7 @@ Item { font.pixelSize: theme.fontLegal } QbzTextArea { + id: descriptionField width: parent.width height: 90 text: root.draftDescription diff --git a/crates/qbz-qt/qml/controls/QbzLineEdit.qml b/crates/qbz-qt/qml/controls/QbzLineEdit.qml index 5eda4ce41..2122fd7e4 100644 --- a/crates/qbz-qt/qml/controls/QbzLineEdit.qml +++ b/crates/qbz-qt/qml/controls/QbzLineEdit.qml @@ -101,6 +101,20 @@ Rectangle { property bool _cleared: false onTextChanged: root._cleared = false + /// Re-seed the box from `value` (default: `text`) NOW, focus or not. + /// + /// The re-seed below reacts only to a CHANGE of `text`, and only while + /// the box is not being edited. A modal that resets its draft on the + /// CLOSE edge (the input still holds focus for that frame), or that + /// reopens with the same seed it closed on, produces neither — and the + /// box kept the last thing typed while the draft said otherwise (the + /// dirty-form report of 2026-09-14). Every modal form calls this from + /// its open handler, after seeding its drafts. + function reset(value) { + root._cleared = false + input.text = (value === undefined) ? root.text : String(value) + } + function clearSearch() { input.text = "" root._cleared = true diff --git a/crates/qbz-qt/qml/controls/QbzTextArea.qml b/crates/qbz-qt/qml/controls/QbzTextArea.qml index 67bbe2c57..169cfdbed 100644 --- a/crates/qbz-qt/qml/controls/QbzTextArea.qml +++ b/crates/qbz-qt/qml/controls/QbzTextArea.qml @@ -28,6 +28,12 @@ Rectangle { property int wrapMode: TextEdit.WordWrap signal edited(string value) + /// Re-seed the body from `value` (default: `text`) NOW, focus or not — + /// the QbzLineEdit.reset() twin (the why is documented there). + function reset(value) { + input.text = (value === undefined) ? root.text : String(value) + } + QbzTheme { id: theme } width: 240 diff --git a/crates/qbz-qt/qml/controls/RipWizardModal.qml b/crates/qbz-qt/qml/controls/RipWizardModal.qml index 7acbee6a2..2d08f9484 100644 --- a/crates/qbz-qt/qml/controls/RipWizardModal.qml +++ b/crates/qbz-qt/qml/controls/RipWizardModal.qml @@ -75,6 +75,11 @@ Item { album = doc.album || "" albumArtist = doc.albumArtist || "" year = doc.year || "" + // The boxes too (QbzLineEdit.reset()): a disc re-read with the same + // tags changes no draft above. + albumField.reset() + yearField.reset() + albumArtistField.reset() libraryChoice = "none" var copy = [] var src = doc.tracks || [] @@ -254,6 +259,7 @@ Item { font.pixelSize: theme.fontLegal } QbzLineEdit { width: parent.width + id: albumField text: root.album onEdited: function (v) { root.album = v } } @@ -265,6 +271,7 @@ Item { font.pixelSize: theme.fontLegal } QbzLineEdit { width: parent.width + id: yearField text: root.year onEdited: function (v) { root.year = v } } @@ -277,6 +284,7 @@ Item { font.pixelSize: theme.fontLegal } QbzLineEdit { width: parent.width + id: albumArtistField text: root.albumArtist onEdited: function (v) { root.albumArtist = v } } diff --git a/crates/qbz-qt/qml/settings/LibFolderEditModal.qml b/crates/qbz-qt/qml/settings/LibFolderEditModal.qml index 20bf749b1..eb3f63a16 100644 --- a/crates/qbz-qt/qml/settings/LibFolderEditModal.qml +++ b/crates/qbz-qt/qml/settings/LibFolderEditModal.qml @@ -64,7 +64,7 @@ Item { root.draftIsNetwork = root.st.isNetwork === true root.draftOverride = root.st.userOverrideNetwork === true root.draftFsIndex = root.st.fsTypeIndex || 0 - aliasField.text = root.draftAlias + aliasField.reset(root.draftAlias) closeButton.forceActiveFocus() } diff --git a/crates/qbz-qt/src/playlist_qt.rs b/crates/qbz-qt/src/playlist_qt.rs index b19d6bf51..393b842d8 100644 --- a/crates/qbz-qt/src/playlist_qt.rs +++ b/crates/qbz-qt/src/playlist_qt.rs @@ -1874,6 +1874,26 @@ pub async fn rename_by_id( .await .map_err(|e| format!("rename playlist {pid} failed: {e}"))?; + // The editor seeds a Qobuz playlist from the manager's WARM cache before + // it fetches (`playlist_manager_qt::cached_playlist_seed`), and the + // reload `after_write` orders lands later — or never, offline. Patch the + // cache now, so an editor reopened right after Save never shows the + // pre-save name or description (the dirty-form report of 2026-09-14). + { + let cache_name = name.clone(); + let cache_desc = description.map(|d| d.trim().to_string()); + if crate::playlist_manager_qt::patch_cache(|data| { + if let Some(p) = data.playlists.iter_mut().find(|p| p.id == pid) { + p.name = cache_name; + if let Some(desc) = cache_desc { + p.description = Some(desc); + } + } + }) { + crate::playlist_manager_qt::publish_document(); + } + } + let target = pid.to_string(); let patched = with_doc(|d| { if d.id != target {