A startup modal carrying the release notes' "Windows -- yes, but read this
first" section verbatim: Windows is not a supported QBZ platform, the build is
as-is, and the port is open for adoption.
Persisted as two fields, hidden plus the acknowledged version, written in ONE
transaction and read from ONE parsed document. Two separate whole-document
read-modify-writes could leave a torn pair after a crash or a failed rename,
and either half alone re-opens the modal -- the user would tick the box and see
it again. ui_prefs.json is shared with the shipping Slint app, so the discipline
lives in settings_qt with the rest of that file's writers.
A version rather than a bool alone is what makes "reset it on every update"
need no reset step: a build whose version differs simply does not match. A
reinstall of the SAME version does not bring it back, which is accepted -- the
prefs file outlives the installer, and the MSI is where that would be cleared.
Focus is taken in Component.onCompleted, not onVisibleChanged. Item.visible
defaults to true and the Loader only creates this when it should already be
shown, so there is no false->true transition to observe: the handler would
never have run, Escape would never have reached it, and ordinary hotkeys would
have kept working behind the modal. Focus is handed back to the shell root on
destruction, because Qt strands activeFocus on an item being destroyed and that
kills AppShell's key dispatcher until the next click -- the failure
QbzConfirmModal already documents.
Mounted through a Loader gated on isWindows. `visible: false` still constructs
the whole subtree, and Linux and macOS have no reason to build a panel, a
Flickable and fourteen delegates for a modal they can never show.
The body is NOT translated, deliberately: it is the author's statement of
policy and the wording is the point. The chrome around it goes through tr.
cc
Three requirements -- no title bar, working clicks, and window tiling -- and no
combination of public Qt flags delivers all three. Each attempt was measured on
the box rather than reasoned about, and each one taught something the plan had
wrong.
FramelessWindowHint, the previous arm, removes WS_THICKFRAME. THAT is what the
shell keys tiling on -- not WS_CAPTION, which this window never has in any
arrangement -- so Win+Arrow and Aero Snap were dead, along with the DWM shadow
and the Win11 rounded corners. "Hide title bar" was not making the app refuse
the shortcuts; it had stopped being a window the shell could tile.
Adding CustomizeWindowHint restored tiling and stopped Qt painting its own
minimise/maximise/close cluster over the QBZ header. It also made Qt answer
WM_NCHITTEST with HTNOWHERE for EVERY pixel -- centre, header, player bar and
sidebar all returned 0 -- and Windows delivers no mouse input to HTNOWHERE, so
the whole app became unclickable while still running: the tray menu still drove
playback. Dropping the hint fixed clicks and brought the drawn buttons back,
duplicated on top of the ones QBZ already draws itself.
So the hint stays and cxx/win_shell.cpp answers the hit test instead. The
filter defers to DefWindowProcW, which is what an ordinary sizable caption-less
window would return: the eight edge codes near the frame, HTCLIENT everywhere
else. It invents no geometry. Its guard is narrow on purpose -- sizable AND
caption-less, which is exactly the custom-chrome main window -- so menus,
tooltips, popups and any window showing the system title bar keep Qt's own
answer.
Measured after: HTCLIENT at the centre, in the title band, at the edges and in
the corner; Win+Left resized the window to 1720 px, exactly half of a 3440 px
display.
Dragging is unaffected and stays with QML's startSystemMove(): with no caption
there is no HTCAPTION band to inherit, which is the arrangement HeaderBar was
already written for.
NOT restored, and not restorable from flags: the Win11 Snap Layouts hover
flyout. Windows asks for it by hit-testing the maximise button as HTMAXBUTTON,
and QBZ's is a QML item inside the client area. Reporting HTMAXBUTTON over that
one rect from this filter would do it -- named as future work rather than
claimed.
cc
A deliberate per-platform divergence, not a revert of ruling K1. That ruling
removed always-on-top because the owner smoked it on real Plasma Wayland and
the hint did nothing: a Wayland client cannot raise itself above other clients,
there is no such request in the core protocol, and Qt had nothing to send. On
Windows the same hint IS HWND_TOPMOST and does exactly what it says, so the pin
exists where the window manager can honour it and stays absent where it cannot.
Owner decision D8. `Qt::WindowStaysOnTopHint` is applied by SetWindowPos on the
existing HWND, so toggling it does not recreate the native window and costs
neither the geometry nor the transparent surface. The capsule already reserved
the slot, and Qt Rows omit invisible children, so the Linux and macOS layouts
are byte-identical.
Session-scoped by design: the pin does not survive a restart.
cc
The third sibling of the Linux ksni item and the macOS NSStatusItem, wired into
the same `tray_qt` seams and dispatchers, so a tray action behaves identically
on all three. Close-to-tray needs no QML change: `Main.qml:786` already keys on
`trayLive`, which both arms of `init` publish.
The menu is the SAME menu. The plan's C ABI carried no labels, which would have
shipped an English-only menu in an app with eight locales; Linux and macOS
already share five `qbz_i18n` keys, so the translated strings are passed in
from Rust and no new string is introduced.
Win32 details that are load-bearing rather than incidental:
- NIF_SHOWTIP. Version 4 SUPPRESSES the standard tooltip unless asked, so
without it the icon appears, every track sets the tooltip, and nothing ever
shows on hover.
- The NIM_SETVERSION result is checked and creation fails if it is refused.
The window procedure decodes callbacks as version 4 and nothing else, so a
silent negotiation failure would leave a live icon whose clicks and menu do
nothing -- worse than no icon, because close-to-tray would then hide the
window behind it.
- WS_EX_TOOLWINDOW, not HWND_MESSAGE. A message-only window never receives the
broadcast TaskbarCreated, so the icon would never come back after an Explorer
restart. The window is on the GUI thread, so Qt's own loop dispatches it.
- LoadImageW without LR_SHARED returns an icon this process OWNS; it is
destroyed on teardown, while the LoadIconW system fallback is not.
- Failed creation gives back the window and the icon instead of parking them
for the process lifetime, and create is idempotent.
- NIM_SETFOCUS after the menu closes, which the Shell asks for and matters most
when it was dismissed with Escape.
Every mutation is marshalled through `tray_bridge::ui`, exactly as the macOS
theme arm is. The publishers run on whatever thread playback pushed from -- the
1 Hz poll lives in `crate::spawn` -- while this tray keeps process-global C++
state and talks to a GUI-thread window; calling straight through was a data
race, not merely a documentation mismatch. Linux needs no hop because ksni's
handle is a thread-safe channel.
Strings are truncated on the Rust side by UTF-16 CODE UNITS, not by `chars()`.
The buffers are sized in `wchar_t`, an astral character costs two, and
`wcsncpy_s` with `_TRUNCATE` would happily keep a high surrogate whose partner
was cut -- malformed UTF-16 handed to the shell.
The tray-icon-variant row is hidden on Windows: the notification area draws the
executable's own icon and Windows owns light/dark, so the four choices would
render, persist and change nothing.
cc
Three settings and menu entries offered Windows something no code behind them
could do.
DSD playback was opened to WASAPI in fc0d35e29, written before the phase B
spike. The spike then measured this DAC refusing 176400, the DSD64 DoP
carrier, so DoP over exclusive mode lost its target; and both non-convert
modes are cfg(target_os = "linux") in the audio thread and answer "DoP
playback is Linux-only" with a stream error. The row could only offer two ways
to fail. Back to ALSA-only. DSD still plays on Windows, converted to PCM.
Exclusive mode became visible the moment phase B published backendIsWasapi,
but the selection arm keys on backend_type == WasapiExclusive and never reads
exclusive_mode. A live toggle that changes nothing, directly under the
BIT-PERFECT header and directly above the thing being measured.
Open audio CD is dropped on Windows: the CDDA backend is mod unsupported there
and answers NoDrive to everything. Open SACD image stays on every platform --
it opens a file, not a drive, and hiding it would have been a bug.
cc
Window.topPadding..rightPadding and the SafeArea attached type are Qt
6.9; on the Linux pin (6.8 LTS) declaring them on the ApplicationWindow
refuses the whole component at Main.qml:173 ("Cannot assign to
non-existent property"), measured on the CI VM 2026-08-25 and on a Pi 4
with Debian 13's Qt 6.8.2. The paddings are now set imperatively from a
Loader active only on macOS, and HeaderBar reads SafeArea.margins.left
through a macOS-only Loader into a plain property, so no binding names
the type on other platforms. Behaviour on macOS is unchanged.
cc
The mode LED is derived from SETTINGS (output_labels.rs reads
audio.exclusive_mode and the ALSA plugin), so it reports what was ASKED FOR.
Nothing reported what the opened stream actually negotiated, which means
"exclusive worked" and "we asked for exclusive" were indistinguishable - and
that distinction is what phase B's acceptance turns on.
PlaybackEvent already carries bit_perfect_mode, so this is a publish, not new
plumbing: the poll tick maps it to direct | plugin | disabled | unknown and
set_effective_stream dedupes on it alongside the delivered params, so a
steady stream still costs no Qt-thread hop. The quality tooltip appends
"stream: <mode>" on both of its paths; unknown and empty print nothing.
Four technical tokens, so no new translatable string.
cc
Adds isWindows, traySupported, systemNotificationsSupported and
rendererSelectable to the settings document, so Settings QML keeps reading
ONE document the way it already does for isMacos. The two any(...) lists
flip to include Windows when plan A-2 T3/T5 land; they point at that line.
Then the Audio tab, measured on the box with the tab open:
HiFi Wizard was live on Windows. qbz-dac-wizard-core generates a
PipeWire/WirePlumber config and knows no other stack, so the wizard cannot
finish anywhere but Linux. Hidden, not disabled - a disabled row invites a
bug report. This is not the reference's backend-is-pipewire gate returning:
inside Linux every backend still sees it, per the 2026-08-11 ruling in that
file's own comment.
Reserve DAC is D-Bus org.freedesktop.ReserveDevice1, Linux only. On Windows
exclusive mode IS the reservation; on macOS it is Hog Mode.
Exclusive mode was visible with no backend able to honour it. It now follows
the backend: Linux unchanged (always visible, enabled only on ALSA), hidden
on macOS, and on Windows it appears when Phase B publishes backendIsWasapi.
QML reads an absent doc property as undefined and undefined === true is
false, so this is correct today and correct again the day the field lands.
The DSD row follows the same pair.
Main.qml no longer boots the wizard off Linux, where its row is hidden and
the audio-stack probe would reach nothing.
Linux sees no change: every gate is isLinux-true or an added || on a field
that is undefined there. Five QML audits pass.
cc
qt-build-utils writes the collected path verbatim as the qrc alias, and
read_dir joins with a backslash on Windows, so none of the 1469 aliases
resolved: 1106 icons, 9 fonts and 21 shaders would each fail with
"Cannot open". Normalise only when the build host is Windows, since a
backslash is a legal filename character on Unix.
Also probe qsb.exe and QT_ROOT_DIR/bin, because Path::is_file never
appends .exe, and fix the SpectrumBand hand-bake comment, whose command
lacked --hlsl 50 and the GLES variants that build.rs actually passes.
cc
Port the Tauri immediate and accumulated offline policies into the active Qt fire path, retain independent scrobbler sessions across Qobuz logout behind a default-on opt-out, and flush queued Last.fm/ListenBrainz plays only when the effective policy permits it.
cx
Preserve per-disc and collection artwork, multi-value genres and remote metadata across Local, Plex, Jellyfin and Subsonic.
Apply source, format and quality filters before pagination, reject invalid CUE rows, and keep Qt library surfaces aligned with the selected physical version.
cx
Keep segmented controls within their visual well, use the active theme for playback indicators, disconnect inactive pulse receivers and improve near-fit song metadata.
cx
Add the chained genre, artist, and album browser with persistent layout and tab ordering. Reuse the shared album semantics for grouped versions and disc-aware details, enforce active source filters, and bound foreground detail work so chained searches stay responsive.
cx
Hide the waveform controls and force every now-playing bar back to the classic seekbar while preserving the implementation for a later release. Keep full-screen timed lyrics centered, slightly larger and wrapped without changing the approved typography or fill.
cx
Reuse the split lyrics renderer for full-screen timed fill, add a contrast shadow and expose the immersive song card there. Render the compact seekbar as a stable three-sine motif and make the larger waveform use normalized rounded capsules with stronger peaks.
cx
Make immersive scopes responsive and derive readable colors from album artwork. Use distinct full-size bar and compact contour renderers for the seek waveform.
cx
Replace the continuous strip with symmetric gradient bars, periodic spacing, and a persistent centre rail. Use more of the available seekbar height while preserving played and cached clipping in the native scene graph.\n\ncx
Render the oscilloscope as a filled antialiased trace and the goniometer as a persistent point cloud with robust autoscaling. Keep the animated scene graph native and the instrument guides static to avoid the previous low-impact line rendering.\n\ncx
Analyze audible tracks into a progressive 512-bin RMS document, cache complete results, and preserve gapless track boundaries. Render the waveform through a scenegraph item in both player bars behind a default-off preference.\n\ncx
Add a theme-aware goniometer and pitch-locked oscilloscope to the large dock and immersive focus modes.
Gate scope DSP and scenegraph updates by actual panel visibility so hidden modes remain idle, including while shader scenes cover immersive panels.
cx
Give track editing a dense spreadsheet pane, move album tags to an independent sidebar, and isolate remote metadata and artwork lookup in a loading-aware popup.
cx
Promote metadata editing to a routed full-page workflow with tabular per-track fields, ordered artist credits, provider identifiers, and compilation handling.
Add bounded artwork selection and retrieval through MusicBrainz, Discogs, and Last.fm. Direct and sidecar saves preflight immutable rows, verify rich tag writes, cache covers by content, and update exact database ids transactionally.
cx
Consume preflight data when loading clears so the modal cannot spin indefinitely.
Reuse the editor for ephemeral folders and bind sidecar or direct writes to the exact active session.
cx