fix(qt): four defects the independent deferral hunt found

An agent that did not write this code audited it against the contract's §9,
§7 and §11.3. Four of its five findings are fixed here (the fifth, the sync()
ordering, landed in the previous commit).

1. QBZ_KIOSK_FULLSCREEN was INERT and the log LIED about it. fullscreen_at_boot()
   had exactly one caller -- choosing a log string -- so an appliance boot with
   the env set stayed windowed while printing "-> fullscreen-at-boot". The
   reference does the real thing at qbz/src/main.rs:8611. Now a seeded-at-
   construction QbzShell.kioskFullscreenBoot drives Main.qml's `visibility`
   declaratively, so it is part of the FIRST mapped frame. It is one-shot by
   construction and requires BOTH the profile and the env, which is what keeps
   a desktop user who toggles kiosk and restarts from being trapped in a shell
   with no titlebar control (incident 2026-07-11).

2. Shift+I was a reachable DEAD STATE. The rebindable ui.focusMode action
   reaches the hotkeys dispatcher from the kiosk root like any other non-nav
   key, so QbzImmersive.open went true over the sanctioned EMPTY mount -- and
   the kiosk root then rejects every arrow, Enter and Esc while it is true, so
   the whole zone model froze against a surface that renders nothing. The
   visualizer tile already declined to open it for this exact reason; the
   guard now sits in apply_open, the funnel every entry passes through,
   because the tile was not the only door. It refuses to OPEN, never to close.

3. Kiosk chrome on the small bar was half-ported. The reference HIDES the
   Lyrics and Queue buttons in kiosk (PlayerBarSmall.slint:846, :983); the
   port gated only their click handlers, so both still rendered and still
   toggled side panels the kiosk shell never mounts -- inert affordances,
   which §9.2 forbids.

4. "Kiosk mode" and "Desktop mode" shipped untranslated. My check_msgids.py
   compared against the 31 @tr strings in the Slint KIOSK sources, which do
   not include the two strings this port adds to the view-mode menu -- the
   checker was right and its input was incomplete. Both now in all 7
   translated locales.

Verified by driving, in one frame: the small bar's right cluster no longer
carries the Lyrics and Queue glyphs, and after Shift+I two Rights still move
the shelf cursor (scratchpad/kiosk/f-02-after-shiftI.png). The fullscreen arm
is verified only as far as a headless run can go -- both env branches log
correctly and the binding resolves -- since actual fullscreen needs a real
display, which is Pi/owner-smoke territory.

cc
This commit is contained in:
vicrodh committed 2026-08-04 00:25:49 -06:00
1 parent bf74e77f6b
commit 28d66352cb
11 files changed
+119 -2

No files matched your search

+12 -1
View File
@@ -78,7 +78,18 @@ ApplicationWindow {
// user toggles — which is the wanted behaviour, not a leak.
// Windowed (not AutomaticVisibility) on the false branch: a fresh window
// must land in its natural state, matching "only ever re-apply a true".
visibility: QbzShell.windowMaximized ? Window.Maximized : Window.Windowed
// The kiosk appliance arm comes FIRST and is one-shot by construction:
// `kioskFullscreenBoot` is seeded at bridge construction from
// `QBZ_PROFILE=kiosk` AND `QBZ_KIOSK_FULLSCREEN`, and nothing ever writes
// it, so this can never re-evaluate and trap a desktop session
// (incident 2026-07-11: the kiosk shell has no titlebar control and
// neither Esc nor F11 leave fullscreen). Declarative for the same reason
// the maximized arm is: QQuickWindowQmlImpl defers the show until
// componentComplete, so this is part of the first mapped frame instead of
// a visible jump afterwards.
visibility: QbzShell.kioskFullscreenBoot
? Window.FullScreen
: (QbzShell.windowMaximized ? Window.Maximized : Window.Windowed)
title: "QBZ"
// Custom chrome (phase 7/12): frameless but OPAQUE — the phase-7
// translucent window was a misread: the Slint MAIN window keeps an
+12 -1
View File
@@ -490,7 +490,13 @@ Rectangle {
smallViewMenu.openBelowRight(smallViewBtn)
}
}
// HIDDEN in kiosk (PlayerBarSmall.slint:846 gates the
// whole block on `!ShellState.kiosk-profile`). The kiosk
// shell mounts no lyrics side panel, so the button would
// toggle a surface that does not exist -- an inert
// affordance, which the kiosk contract forbids (§9.2).
QbzIconButton {
visible: !QbzShell.kioskProfile
name: "mic-vocal"
active: QbzShell.lyricsOpen
anchors.verticalCenter: parent.verticalCenter
@@ -525,8 +531,13 @@ Rectangle {
Item { width: 4; height: 1 }
// Queue panel toggle.
// Queue panel toggle. HIDDEN in kiosk for the same
// reason as the lyrics button above
// (PlayerBarSmall.slint:983) -- the kiosk reaches its
// queue through the Now Playing view's Up Next tab, and
// the side panel is never mounted.
QbzIconButton {
visible: !QbzShell.kioskProfile
name: "list-ordered"
active: QbzShell.queueOpen
anchors.verticalCenter: parent.verticalCenter
+20
View File
@@ -233,6 +233,26 @@ fn pinned_view(default_key: &str) -> Option<(i32, i32)> {
/// the §3.3a viz keep-alive on both edges.
fn apply_open(mut this: Pin<&mut QbzImmersive>, value: bool) {
use cxx_qt::CxxQtType as _;
// The KIOSK shell has no Immersive surface yet: its mount
// (`qml/shell/KioskShell.qml`) is the sanctioned empty Loader of the
// 2026-08-02 kiosk-port contract (§4.6 / ruling R2), waiting for the
// Immersive task. Opening over it produces a reachable DEAD STATE — the
// kiosk root rejects every arrow, Enter and Esc while `open` is true
// (§7.11), so the whole zone model freezes against a surface that renders
// nothing.
//
// The visualizer TILE already declines to open it for exactly this reason.
// The guard belongs HERE rather than at that call site because this is the
// funnel every entry passes through, and Shift+I — the rebindable
// `ui.focusMode` action, which reaches the dispatcher from the kiosk root
// like any other non-nav key — proved the tile is not the only door.
//
// Refuse to OPEN, never to close: a session that somehow got in must
// always be able to get out.
if value && crate::kiosk_profile_qt::active() {
log::debug!("[qbz-qt] immersive open refused: the kiosk shell has no immersive surface");
return;
}
if this.open == value {
return;
}
+19
View File
@@ -160,6 +160,22 @@ pub mod qbz_shell {
/// which in kiosk collapses to the profile row alone
/// (`shell/PlayerBarSmall.slint:772,780,788,796,811,819,826,847,983`).
#[qproperty(bool, kiosk_profile)]
/// Should the window map FULLSCREEN at boot?
///
/// True only when the kiosk profile is active AND
/// `QBZ_KIOSK_FULLSCREEN` is set — an appliance image sets it so the
/// panel owns the whole screen (`qbz/src/main.rs:8607-8617`).
///
/// Deliberately NOT derived from the persisted profile alone: a user
/// who toggles kiosk on the desktop and restarts would be TRAPPED,
/// because the kiosk shell has no titlebar control and neither Esc nor
/// F11 leave fullscreen (incident 2026-07-11). Windowed by default
/// keeps the OS titlebar reachable.
///
/// Seeded at construction and never rewritten: `Main.qml`'s
/// `visibility` binding must be correct on the FIRST mapped frame, and
/// a later push would arrive after the window is already on screen.
#[qproperty(bool, kiosk_fullscreen_boot)]
#[qproperty(i32, ambient_mode)]
// Album-art triad (ambient_qt.rs), pushed on track change.
#[qproperty(QString, ambient_primary)]
@@ -426,6 +442,7 @@ pub struct QbzShellRust {
window_min_height: f32,
reduce_motion: bool,
kiosk_profile: bool,
kiosk_fullscreen_boot: bool,
ambient_mode: i32,
ambient_primary: QString,
ambient_secondary: QString,
@@ -489,6 +506,8 @@ impl Default for QbzShellRust {
window_min_height: crate::settings_qt::WINDOW_MIN_HEIGHT,
reduce_motion: reduce_motion_at_boot(),
kiosk_profile: crate::kiosk_profile_qt::active(),
kiosk_fullscreen_boot: crate::kiosk_profile_qt::active()
&& crate::kiosk_profile_qt::fullscreen_at_boot(),
ambient_mode: crate::settings_qt::app_background_mode(),
// The Slint ImmersiveState default triad (pre-artwork colors).
ambient_primary: QString::from("#00dcc8"),