feat(qt): close-to-tray across every exit, and the macOS tray settings rows

Block B6 of the miniplayer/tray contract. B5 landed a tray whose clicks
reached Rust and stopped there; this is the other half.

qml/Main.qml — hideToTray(), showFromTray() and the ONE close choreography
every exit now calls, plus the handlers for the tray bridge's four signals
and the mini's close-app signal. The close predicate is both conditions,
trayLive && closeToTray: with the tray off a close QUITS, because gating on
the setting alone strands the process with no window on a desktop that has no
SNI host. close.accepted = false happens before the hide — hiding never arms
Qt's quit check, an accepted close does.

The miniplayer's enter and exit are rewired onto those two verbs, which is
what makes the tray toggle honest: the reference hides the main window behind
the tray's back and its second tray click brings the main window up beside a
live mini.

THE EXITS ARE FIVE, NOT FOUR. The contract enumerated four and gave the grep
that finds them; re-run today it returns a fifth, because the kiosk shell
grew its own drawn close button after the contract was written. It goes
through the same choreography. Four POC-NOTEs die here.

showFromTray also un-iconifies. Qt's hide() preserves windowStates where the
reference destroys the surface, so a window minimized before it was hidden
would otherwise come back minimized — present in the taskbar, invisible on
screen, which reads as the tray having done nothing.

qml/settings/AppearanceSettings.qml + src/settings_qt.rs — the three macOS
rows: the MENU BAR header swap, the macOS wording on Close-to-tray, and the
Hide-Dock row, all absent on Linux. minimize_to_tray gets storage and no row,
which is the owner's standing decision. And the tray-close-to-tray write arm
now pushes to the bridge beside its persist, so toggling it changes the very
next close with no restart.

src/mini_bridge.rs — close_app and the signal it routes through, so the
mini's red X will honour close-to-tray like every other exit. Its button is
B3's, so the chain has no caller yet.

Driven, not assumed: with the tray live and close-to-tray just pushed false,
the app-menu Close row QUIT the process — which can only happen if the push
reached the bridge, since the seed had set it true. And Shift+M then Escape
returns the main window pixel-identical, zero of 1.6 M pixels changed, with
the mini alone on a black framebuffer in between: the main window really is
hidden, and Qt's hide/show preserves its geometry with no restore call.

cc
This commit is contained in:
vicrodh committed 2026-08-04 05:21:10 -06:00
1 parent cd89b40945
commit 088387f2c1
6 files changed
+324 -35

No files matched your search

+143 -15
View File
@@ -200,8 +200,8 @@ ApplicationWindow {
// Same reason, and it bites harder here: the tray's clicks reach QML
// ONLY through this bridge's signals, so an unbooted QbzTray is a tray
// whose every click does nothing, with zero evidence in the log
// (contract §15 trap 27). The handlers for those signals arrive with
// B6, together with the window verbs they call.
// (contract §15 trap 27). The Connections that answer those four
// signals are below, beside the window verbs they call.
QbzTray.boot()
// Seed the maximized latch ONCE, imperatively — see its declaration for
@@ -447,20 +447,131 @@ ApplicationWindow {
geometrySaveTimer.restart()
}
// Two shutdown paths, because the header's close button calls Qt.quit()
// (shell/HeaderBar.qml) which never raises `closing`, while a WM close or
// Alt+F4 does. Both end up in the same dirty check, so the duplicate is
// free — and either way a resize in the last 400ms still lands, including
// the taskbar close of a minimized window (that is what the exit variant
// and the floating cache are for).
onClosing: window.persistWindowGeometryOnExit()
// The WM close / Alt+F4 exit, routed into the ONE close choreography
// (A-26, below). It is the ONLY exit that delivers a close EVENT, and
// `closeOrHide` refuses that event before hiding — §3.1 measured that
// QGuiApplicationPrivate::maybeLastWindowClosed is reached from exactly two
// sites, both inside QWindow::event and both gated on the accepted flag, so
// hiding never arms Qt's quit check but an ACCEPTED close does.
//
// `onAboutToQuit` is still not a duplicate of it: `Qt.quit()` never raises
// `closing` (§15 trap 26), so the quit arm of `closeOrHide`, the tray's
// `quit_requested` handler and any teardown that skips QML reach the
// geometry flush only through here. Where both do run the second write is
// free (Rust's >0.5px dirty check drops it), and either way a resize in the
// last 400ms still lands, including the taskbar close of a minimized window
// (that is what the exit variant and the floating cache are for).
onClosing: function (close) { window.closeOrHide(close) }
Connections {
target: Qt.application
function onAboutToQuit() { window.persistWindowGeometryOnExit() }
}
// --- Close-to-tray, and the ONE visibility owner (contract B6) ---------
//
// THE INVARIANT (§5.8.1, §13-D18, AC-24, stage-0 S0-4): the main window is
// hidden and shown ONLY by these two functions, and they are the ONLY
// callers of QbzTray.setWindowShown. The reference has a real desync
// exactly here — the miniplayer's enter()/exit() call m.hide()/m.show()
// directly (crates/qbz/src/miniplayer.rs:186, :207) instead of
// tray::hide_window/show_window, so WINDOW_SHOWN goes stale and, while the
// mini is up, the first tray left-click hides an already-hidden window and
// the second brings the MAIN window back beside the live mini (#559/#618).
// A bare window.hide()/window.show() anywhere in this file re-opens that
// hole; the miniplayer handler below calls these verbs for that reason.
//
// ⚠ ONE WRITER THE S0-4 GREP CANNOT SEE, named here so a green grep is not
// read as more than it proves: the drawn MINIMIZE buttons call
// `hostWindow.showMinimized()` (shell/HeaderBar.qml, shell/KioskShell.qml)
// and deliberately do NOT report. That is correct — a minimized window is
// still SHOWN, not hidden-to-tray, and owner ruling K5 keeps that button an
// unconditional WM minimize. The consequence is real and inherited from the
// reference, which tracks the same flag the same way: with the window
// ICONIFIED, `WINDOW_SHOWN` is still true, so the next tray left-click
// takes the hide arm rather than restoring. `showFromTray()` below at least
// un-iconifies, so the SECOND click genuinely brings it back.
function hideToTray() {
// Both reference close arms flush the session BEFORE hiding, "even when
// only hiding — the process may be killed from the tray / shell without
// a real quit afterwards" (crates/qbz/src/main.rs:23255-23256). The Qt
// port has no session_persist module, so the geometry flush is the
// whole of what is owed. On a miniplayer enter it is harmless: it
// writes the size the exit is about to restore.
window.persistWindowGeometryOnExit()
window.hide()
QbzTray.setWindowShown(false)
}
function showFromTray() {
window.show()
// Un-iconify. Qt's hide() preserves windowStates, unlike the
// reference's surface destruction (crates/qbz/src/tray/mod.rs:253-265),
// so a window that was MINIMIZED before it was hidden comes back
// minimized — visible in the taskbar, invisible on screen, which reads
// as "the tray did nothing". Restoring to the maximized latch rather
// than to Windowed keeps a maximized session maximized.
if (window.visibility === Window.Minimized)
window.visibility = window.maximizedLatch ? Window.Maximized
: Window.Windowed
// raise() + requestActivate() is §13-D12, carried by the VERB so no
// call site has to remember it: the reference's tray::show_window
// focuses (crates/qbz/src/tray/mod.rs:226-229) while its miniplayer
// exit() does not, and the port focuses on both paths.
window.raise()
window.requestActivate()
QbzTray.setWindowShown(true)
}
// THE ONE close choreography (A-26, §5.7). Five exits reach it: this
// window's onClosing above, the drawn X and the app-menu Close row in
// shell/HeaderBar.qml, the kiosk shell's own drawn X in
// shell/KioskShell.qml, and the miniplayer's closeApp through the
// QbzMini.closeAppRequested handler below.
//
// The predicate is BOTH conditions, ported verbatim from
// crates/qbz/src/main.rs:23253 — `tray_settings::get().close_to_tray &&
// tray::handle().is_some()`. TRAY OFF => CLOSE QUITS: `handle()` is None
// when the tray is disabled, suppressed under gamescope or failed to
// initialise, and gating on the setting alone would strand the process with
// no window and no way back on a desktop with no SNI host (§15 trap 22).
//
// `closeEvent` is the QQuickCloseEvent on the one path that carries one and
// null on the other four.
function closeOrHide(closeEvent) {
if (QbzTray.trayLive && QbzTray.closeToTray) {
if (closeEvent)
closeEvent.accepted = false
window.hideToTray()
return
}
window.persistWindowGeometryOnExit()
Qt.quit()
}
// The tray's four signals (src/tray_bridge.rs:87-100). Rust owns no window
// (§2.3), so every tray click hops ksni thread -> tray_qt verb ->
// tray_bridge::ui() -> qsignal -> here, and this block is where it finally
// reaches one.
Connections {
target: QbzTray
function onWindowShowRequested() { window.showFromTray() }
function onWindowHideRequested() { window.hideToTray() }
// The tray's left-click degenerates to present() while the mini is open
// (§5.8.2, §13-D21): raise the MINI, and do NOT bring the main window up
// beside it. No setWindowShown — the mini is not the main window, and
// the flag still honestly says the main one is hidden.
function onMiniPresentRequested() {
var mini = window.ensureMiniWindow()
if (mini !== null) {
mini.show()
mini.raise()
mini.requestActivate()
}
}
function onQuitRequested() {
window.persistWindowGeometryOnExit()
Qt.quit()
}
}
// --- The miniplayer window (2026-08-03 miniplayer/tray contract, B2) ---
//
@@ -492,12 +603,18 @@ ApplicationWindow {
// §4.6's step ordering and the only place either window is shown or hidden
// for the miniplayer.
//
// Step 7 of enter() ("m.hide() LAST, avoid a blank flash") and its exit
// twin are window.hideToTray() / window.showFromTray() (A-25), which land
// with block B6 — so on this commit the mini opens with the main window
// still visible behind it. That is §14.2's stated ordering constraint, not
// a defect: a bare window.hide() here would be a second visibility owner
// and §5.8.1's invariant (stage-0 S0-4) forbids it.
// Step 7 of enter() is "hide the main window only after the mini is visible
// (avoid a blank flash)" (crates/qbz/src/miniplayer.rs:185-187), so
// hideToTray() runs LAST here and showFromTray() runs after the mini is
// down. They are the A-25 verbs, never a bare hide()/show(): the mini IS
// where the reference's WINDOW_SHOWN desync comes from, and reporting
// shown=false/true from these two paths is what makes the tray toggle
// honest (§13-D18, stage-0 S0-4).
//
// Re-entering while already open forces an openChanged (mini_bridge.rs's
// enter_now), so this runs again and raises the mini; hideToTray() on an
// already-hidden window is a no-op plus one geometry write Rust's dirty
// check drops.
Connections {
target: QbzMini
function onOpenChanged() {
@@ -508,10 +625,21 @@ ApplicationWindow {
mini.raise()
mini.requestActivate()
}
window.hideToTray()
} else if (window.miniWindow !== null) {
window.miniWindow.hide()
window.showFromTray()
}
}
// The mini's own close verb (A-12). QbzMini.closeApp() exits the mini
// FIRST — which runs the handler above and brings the main window back
// — and only then emits this, so the app's ONE close choreography
// decides between hide-to-tray and quit. The brief show-then-hide is
// 1:1 with the reference: miniplayer.rs:235-251 hides the mini, shows
// the main window and restores its geometry before calling
// invoke_close_app, because "the window becomes user-visible when
// close-to-tray keeps the app alive" (:244-246).
function onCloseAppRequested() { window.closeOrHide(null) }
}
// The immersive toggle Shortcut is GONE (2026-08-03 hotkeys-port §1.3):
@@ -16,7 +16,6 @@
// - The Custom-theme editor (seed-from-current, the dark-polarity toggle and
// the base-token swatch grid): the port applies custom_theme.json read-only
// and has no ColorPicker primitive.
// - "Hide Dock icon when closed to menu bar" (macOS-only in the Slint too).
// - The commented-out Slint blocks (immersive background / panels / FPS,
// window-title template) stay out, 1:1 the owner's cut.
@@ -412,8 +411,13 @@ Column {
SettingsDivider { }
SettingsSpacer { }
// ========================== SYSTEM TRAY ==============================
GroupHeader { text: QbzSession.tr("SYSTEM TRAY", QbzSession.trRev) }
// ==================== SYSTEM TRAY (macOS: MENU BAR) =================
// The header swaps on the platform, 1:1 with AppearanceSettings.slint:884.
GroupHeader {
text: root.doc.isMacos === true
? QbzSession.tr("MENU BAR", QbzSession.trRev)
: QbzSession.tr("SYSTEM TRAY", QbzSession.trRev)
}
SettingRow {
label: QbzSession.tr("Enable tray icon", QbzSession.trRev)
description: QbzSession.tr("Show icon in system tray (requires restart)", QbzSession.trRev)
@@ -422,9 +426,21 @@ Column {
onToggled: function (v) { QbzBridge.settingsBool("tray-enable", v) }
}
}
// A separate "Minimize to tray" toggle is deliberately absent, 1:1 with
// AppearanceSettings.slint:897-903: redirecting the window-manager minimize
// button to the tray means owning that button, and Wayland forbids a client
// from intercepting the compositor's minimize. The reference HIDES the
// option rather than showing it disabled, and this row reads "Close to
// tray". The setting still round-trips through the store (the
// "tray-minimize-to-tray" write arm, src/settings_qt.rs) so a Qt session
// cannot drop a value the Slint build wrote.
SettingRow {
label: QbzSession.tr("Close to tray", QbzSession.trRev)
description: QbzSession.tr("Keep playing in the tray instead of quitting when you close the window", QbzSession.trRev)
// macOS says "menu bar"; the LABEL stays "Close to tray" on both
// platforms (AppearanceSettings.slint:905-908).
description: root.doc.isMacos === true
? QbzSession.tr("Keep playing in the menu bar instead of quitting when you close the window", QbzSession.trRev)
: QbzSession.tr("Keep playing in the tray instead of quitting when you close the window", QbzSession.trRev)
rowEnabled: root.doc.trayEnable === true
QbzToggle {
enabled: root.doc.trayEnable === true
@@ -432,6 +448,24 @@ Column {
onToggled: function (v) { QbzBridge.settingsBool("tray-close-to-tray", v) }
}
}
// macOS only: switch the activation policy to .accessory while closed to
// the menu bar (no Dock icon). Off = Spotify-style, keep the Dock icon.
// `visible` is inherited Item.visible and the parent is a Column, which
// skips invisible children — so on Linux this leaves no gap at all. The
// enable condition is DOUBLED on the row and on the toggle because
// SettingRow's `rowEnabled` only dims its own label column
// (controls/SettingRow.qml:26); it does not reach the control.
SettingRow {
visible: root.doc.isMacos === true
label: QbzSession.tr("Hide Dock icon when closed to menu bar", QbzSession.trRev)
description: QbzSession.tr("Run as a menu-bar-only app while the window is closed. Off keeps the Dock icon (like Spotify)", QbzSession.trRev)
rowEnabled: root.doc.trayEnable === true && root.doc.trayCloseToTray === true
QbzToggle {
enabled: root.doc.trayEnable === true && root.doc.trayCloseToTray === true
checked: root.doc.trayMacHideDock === true
onToggled: function (v) { QbzBridge.settingsBool("tray-mac-hide-dock", v) }
}
}
SettingRow {
label: QbzSession.tr("Tray icon variant", QbzSession.trRev)
description: QbzSession.tr("Pick a mono glyph to match your panel (Plasma, GNOME's permanently dark top bar) or the full colour vinyl logo", QbzSession.trRev)
+27 -13
View File
@@ -27,9 +27,12 @@
// Slint, each needing a surface the port does not have yet: Open Music Link,
// Report an Issue, What's New, About QBZ.
//
// POC-NOTE: the custom window-chrome parts (drag surface, drawn
// min/max/close WindowControls) are skipped — the POC keeps NATIVE window
// decorations.
// The custom window chrome IS built (the note claiming it was skipped was a
// fossil): the header itself is the drag surface (`startSystemMove()` below),
// and the drawn min/max/close WindowControls Row renders whenever
// `use_system_title_bar` is OFF. Native decorations are the OTHER arm of that
// same pref, not the port's ceiling. Close routes through Main.qml's
// closeOrHide, so it honours close-to-tray like every other exit.
//
// FILE LENGTH (>500): this is ONE bar with one placement state machine —
// `navInSidebar` x `navHeaderCompact` x the three sidebar widths decide which
@@ -992,17 +995,22 @@ Rectangle {
id: wcCloseArea
anchors.fill: parent
hoverEnabled: true
// POC-NOTE: the Slint app hides-to-tray on close; the POC
// has no tray — close quits.
onClicked: Qt.quit()
// Close-to-tray lives in ONE place — Main.qml's closeOrHide
// (2026-08-03 miniplayer/tray contract, A-26 / §5.7) — which
// hides to the tray while QbzTray.trayLive && QbzTray.closeToTray
// and quits otherwise. A drawn button delivers no close EVENT
// (Qt.quit() never raises `closing` either), hence the null.
onClicked: if (root.hostWindow) root.hostWindow.closeOrHide(null)
}
}
}
// --- App menu (user block + Settings + Log Out + Close) ---------------
// POC-NOTE: the Slint menu also carries Open Music Link / Keyboard
// Shortcuts / Documentation / Report an Issue / What's New / About QBZ
// — omitted here (no views/dialogs to open yet).
// --- App menu (user block + Settings + Keyboard Shortcuts +
// Documentation + Log Out + Close) -------------------------------------
// Still missing against the Slint menu, each needing a surface this port
// does not have yet: Open Music Link, Report an Issue, What's New,
// About QBZ. (The older note here also listed Keyboard Shortcuts and
// Documentation, which have since landed.)
Popup {
id: appMenu
x: root.width - 234 - theme.spacingMd
@@ -1142,9 +1150,15 @@ Rectangle {
AppMenuItem {
name: "x"
label: QbzSession.tr("Close", QbzSession.trRev)
// POC-NOTE: the Slint app hides to tray / follows the
// platform close behavior; the POC just quits.
onClicked: Qt.quit()
// The same ONE close choreography as the drawn X above
// (A-26, §5.7). appMenu.close() FIRST, like the Log Out row:
// when close-to-tray only HIDES the window, a popup left open
// comes back up with it on the next show.
onClicked: {
appMenu.close()
if (root.hostWindow)
root.hostWindow.closeOrHide(null)
}
}
}
}
+9 -1
View File
@@ -372,7 +372,15 @@ Rectangle {
id: wcCloseArea
anchors.fill: wcClose
hoverEnabled: true
onClicked: Qt.quit()
// The FIFTH exit. §5.7 of the 2026-08-03 miniplayer/
// tray contract enumerates four; this shell grew its
// own drawn X after that contract was written, and an
// exit still calling Qt.quit() directly is precisely
// the half-port the enumeration exists to prevent. Same
// choreography as the desktop chrome: Main.qml's
// closeOrHide hides to the tray while QbzTray.trayLive
// && QbzTray.closeToTray and quits otherwise.
onClicked: if (root.hostWindow) root.hostWindow.closeOrHide(null)
}
}
}
+39
View File
@@ -95,6 +95,32 @@ pub mod qbz_mini {
#[qinvokable]
fn exit(self: Pin<&mut QbzMini>);
/// Close the whole APP from the mini (contract A-12, §4.6). Exits the
/// mini first, then routes to the ONE close choreography by emitting
/// `close_app_requested` below.
#[qinvokable]
fn close_app(self: Pin<&mut QbzMini>);
/// `close_app`'s second half: `Main.qml` answers with
/// `window.closeOrHide(null)`, so the mini's red X honours
/// close-to-tray exactly like every other exit.
///
/// **This signal is an ADDITION to the contract's §2.2 must-add list**,
/// and it is logged as one. A-12 says `close_app` "routes to the ONE
/// close choreography (§5.7)", but that choreography is a QML function
/// on `Main.qml`'s root: `QbzMini` had no signals at all, the mini's
/// QML tree holds no reference to the main window (`MiniWindow.qml`
/// passes `hostWindow: root`, i.e. the MINI), and none of `QbzTray`'s
/// four signals is a "close requested" — routing through
/// `quit_requested` would bypass close-to-tray outright. The only
/// alternative that adds no symbol is deciding hide-vs-quit in Rust,
/// which would put a SECOND copy of the close predicate in the tree
/// (the first is QML's `trayLive && closeToTray`) — the
/// two-functions-that-should-mirror-each-other smell LESSONS L5 names.
/// One signal keeps exactly one copy of the predicate.
#[qsignal]
fn close_app_requested(self: Pin<&mut QbzMini>);
/// The MINI window's own key dispatcher (contract A-15, §4.9), 1:1 with
/// the reference's `dispatch_mini`
/// (`crates/qbz/src/keybindings.rs:623-648`). **No
@@ -323,6 +349,19 @@ impl qbz_mini::QbzMini {
exit_now(self);
}
/// Contract A-12 — close the APP from the mini's red X.
///
/// The order is the reference's (`crates/qbz/src/miniplayer.rs:235-251`):
/// leave the mini FIRST — which, through `Main.qml`'s `onOpenChanged`,
/// hides the mini and brings the main window back — and only then ask for
/// the close, because "the window becomes user-visible when close-to-tray
/// keeps the app alive" (`:244-246`). `open_changed()` emits synchronously,
/// so the main window is already up by the time the signal below is queued.
pub fn close_app(mut self: Pin<&mut Self>) {
exit_now(self.as_mut());
self.close_app_requested();
}
/// Contract A-15 — the mini's key dispatcher, the table at §4.9.
///
/// Resolution goes through `hotkeys_qt::action_for_mini_key` (A-16), which
+68 -2
View File
@@ -1275,6 +1275,33 @@ pub struct SettingsDoc {
pub tray_enable: bool,
#[serde(rename = "trayCloseToTray")]
pub tray_close_to_tray: bool,
/// STORAGE ONLY — there is no Settings row and no reader (owner ruling K5).
/// The Slint sheet documents why the reference hides the toggle rather than
/// showing it disabled (`crates/qbz-ui/ui/settings/AppearanceSettings.slint:897-903`):
/// redirecting the window-manager minimize button to the tray means owning
/// that button, and Wayland forbids a client from intercepting the
/// compositor's minimize.
///
/// **Nothing in QML writes this key** (`grep -rn 'tray-minimize-to-tray' qml/`
/// → 0), and that is the point of K5, not an oversight: the field carries
/// the value into the settings DOCUMENT so a reader can see it, and the
/// write arm exists so the key becomes reachable the day a row is added
/// without a second edit. It is NOT protecting the value from loss — the
/// per-user store is written by `qbz-app`, not by this document, so a Qt
/// session could not drop what the Slint build wrote in any case.
#[serde(rename = "trayMinimizeToTray")]
pub tray_minimize_to_tray: bool,
/// macOS only: while closed to the menu bar, switch the activation policy to
/// `.accessory` (no Dock icon). Off keeps the Dock icon, Spotify-style. The
/// row is `visible: isMacos`, so on Linux it renders nothing at all.
#[serde(rename = "trayMacHideDock")]
pub tray_mac_hide_dock: bool,
/// Platform flag for the three macOS-shaped bits of the SYSTEM TRAY group:
/// the "MENU BAR" header swap, the Close-to-tray description arm and the
/// Hide-Dock row's `visible`. Compile-time, exactly like the Slint's
/// `AppearanceState.is-macos`.
#[serde(rename = "isMacos")]
pub is_macos: bool,
#[serde(rename = "trayIconThemes")]
pub tray_icon_themes: Vec<String>,
#[serde(rename = "trayIconThemeIndex")]
@@ -1665,6 +1692,19 @@ pub async fn publish_snapshot() {
.get_settings()
.map(|t| t.close_to_tray)
.unwrap_or(true),
// Both shared defaults are `false` (`qbz-app/src/settings/tray.rs:59-63`),
// so unlike the two reads above these fall back to `false` — the
// fallback must match the struct's own default, which is the whole
// point of the S1 fix directly above.
tray_minimize_to_tray: tray()
.get_settings()
.map(|t| t.minimize_to_tray)
.unwrap_or(false),
tray_mac_hide_dock: tray()
.get_settings()
.map(|t| t.mac_hide_dock)
.unwrap_or(false),
is_macos: cfg!(target_os = "macos"),
tray_icon_themes: TRAY_ICON_LABELS
.iter()
.map(|l| qbz_i18n::t(l))
@@ -1930,8 +1970,34 @@ pub async fn settings_bool(runtime: &Arc<AppRuntime<LoggingAdapter>>, key: &str,
.set_enable_tray(value)
.map_err(|e| e.to_string())
.map(|_| Apply::None),
"tray-close-to-tray" => tray()
.set_close_to_tray(value)
"tray-close-to-tray" => {
let persisted = tray().set_close_to_tray(value).map_err(|e| e.to_string());
// A-21b(c), and the FIFTH live-update row of §5.6. Without this the
// `QbzTray.closeToTray` property never leaves the value it was
// seeded with and the toggle is COSMETIC until the next launch —
// the "renders and persists and drives nothing" defect this whole
// diff exists to end. The line above is the storage; this is the
// running app, and after it the very next close behaves the new way.
//
// Gated on the persist: a failed write (no active session) must not
// tell the running app a value the store does not hold.
if persisted.is_ok() {
crate::tray_bridge::ui(move |mut t| {
t.as_mut().set_close_to_tray(value);
});
}
persisted.map(|_| Apply::None)
}
// STORAGE ONLY (owner ruling K5): no Settings row, no reader. The value
// round-trips through the per-user store the Slint build shares instead
// of a Qt session dropping it — see the doc field for why the reference
// hides the toggle.
"tray-minimize-to-tray" => tray()
.set_minimize_to_tray(value)
.map_err(|e| e.to_string())
.map(|_| Apply::None),
"tray-mac-hide-dock" => tray()
.set_mac_hide_dock(value)
.map_err(|e| e.to_string())
.map(|_| Apply::None),
// --- Integrations (phase 19) --------------------------------------