feat(media-controls): SMTC with a real HWND, and a native cover path
Windows media keys never worked because SMTC never started. souvlaki's Windows
backend does `config.hwnd.expect(..)`, which PANICS rather than degrading, and
`MediaControls::new` runs inside a tokio task, so a missing handle aborted the
task instead of returning the Err the caller was written for. Phase A bowed out
before reaching it and logged "SMTC disabled: no window handle wired yet".
`spawn` now takes a `NativeWindow`, and `cxx/win_shell.cpp` supplies the
top-level HWND through `QGuiApplication::topLevelWindows()`. The guard stays:
a missing integration is a missing feature, a panic in a tokio task is a dead
session.
Windows reads the handle from a bounded retry on the GUI thread. Both things it
waits for -- the tray bridge's Qt thread and a shown window -- are "not yet",
never "not ever": `tray_bridge::ui` is a no-op until `QbzTray::boot` registers
the thread, and `QbzSession.boot()` runs first (Main.qml:254 vs :380).
NON-WINDOWS IS UNCHANGED, deliberately. An earlier draft routed every platform
through that hop and it was a Linux/macOS regression three ways: initialization
could be dropped outright before boot, MPRIS would start only when the Qt queue
drained (losing the first metadata push), and `main_window_hwnd` calls
`QWindow::winId()`, which CREATES the platform window as a side effect on
platforms with no reason to.
Cover art needed its own fix. souvlaki 0.8.3 branches on
`url.starts_with("file://")`, then trims exactly that prefix and hands the rest
to `GetFileFromPathAsync`. So `file:///C:/x.jpg` arrives as `/C:/x.jpg` -- a
slash before a drive letter, which never opens -- while a bare `C:\x.jpg` no
longer matches the prefix and takes the remote-URI branch instead. The value
that works is the hybrid `file://C:\x.jpg`. UNC is handled too: `file://nas/m/a`
is a real Windows path and was being passed through as the relative `nas/m/a`.
The percent-decode is full and applied once: `art_url` arrives from
`url::Url::from_file_path` (space as %20) and from `fs_url::file_url` (only
% # ? escaped), and one full decode reads both -- `%2520` gives back a literal
`%20` rather than a space. `fs_url::local_path` is deliberately not used here;
its three-escape decode would leave the other producer's %20 intact.
Eight tests cover the matrix. Reviewed adversarially before commit, which is
where the inverted cover conversion and the non-Windows regression were caught;
the souvlaki branch was then read at the source to confirm.
cc
This commit is contained in:
10 files changed
+397
-42
No files matched your search
@@ -0,0 +1,52 @@
|
||||
// Windows shell helpers with no Q_OBJECT of their own.
|
||||
//
|
||||
// Compiled on EVERY platform on purpose: the bodies are portable Qt and only
|
||||
// the caller's use of the WId is Windows-specific. Keeping it unconditional
|
||||
// means the file is type-checked by the Linux and macOS builds too, which is
|
||||
// where a signature drift would otherwise go unnoticed until a Windows CI run.
|
||||
|
||||
#include <QtCore/QObject>
|
||||
#include <QtGui/QGuiApplication>
|
||||
#include <QtGui/QSessionManager>
|
||||
#include <QtGui/QWindow>
|
||||
|
||||
// The top-level window's native handle, or nullptr before one is shown.
|
||||
//
|
||||
// MUST NOT be called before exec(): QWindow::winId() CREATES the platform
|
||||
// window as a side effect, so an early call would hand back a handle for a
|
||||
// window Qt has not finished setting up, and the real one would then get a
|
||||
// different HWND. Callers hop to the GUI thread first; winId() is not
|
||||
// thread-safe.
|
||||
extern "C" void *qbz_main_window_hwnd()
|
||||
{
|
||||
const auto windows = QGuiApplication::topLevelWindows();
|
||||
for (QWindow *w : windows)
|
||||
if (w->isVisible() && w->type() == Qt::Window)
|
||||
return reinterpret_cast<void *>(w->winId());
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// WM_QUERYENDSESSION -> QGuiApplication::commitDataRequest, via Qt's Windows
|
||||
// session manager. It fires BEFORE Windows decides whether to proceed with the
|
||||
// logoff and Qt blocks until the handler returns, which makes it the one safe
|
||||
// place to persist synchronously: anything deferred to a queued connection or
|
||||
// another thread races the process being killed.
|
||||
extern "C" void qbz_install_commit_data_handler(void (*cb)())
|
||||
{
|
||||
#ifndef QT_NO_SESSIONMANAGER
|
||||
// Once. Qt::UniqueConnection does NOT deduplicate lambdas -- each connect
|
||||
// makes an independent connection -- so a second install would run the
|
||||
// session persist twice on every logoff.
|
||||
static bool installed = false;
|
||||
if (installed)
|
||||
return;
|
||||
installed = true;
|
||||
|
||||
QObject::connect(qApp, &QGuiApplication::commitDataRequest, qApp,
|
||||
[cb](QSessionManager &) { cb(); });
|
||||
#else
|
||||
// A Qt built without the session manager has no commitDataRequest at all,
|
||||
// and this file compiles on every platform.
|
||||
(void)cb;
|
||||
#endif
|
||||
}
|
||||
Reference in new issue
Block a user