fix(share): copy links through QClipboard, not arboard (#684)
Copy link failed on GNOME + Flatpak: arboard's Wayland path needs the
wlr/ext-data-control protocol, which Mutter does not implement, so it fell
back to X11 — reachable natively through XWayland, unreachable inside the
sandbox ("clipboard unavailable: X11 server connection timed out"). KWin
has the protocol, which is why the failure never reproduced on Plasma.
Replace it with a C++ shim (cxx/clipboard.cpp) that queues the write onto
the GUI thread and hands it to QClipboard: the base wl_data_device protocol
every compositor implements, no extra sandbox permission, and the offer
lives as long as the application (the #514 lifetime rule, without the
long-lived arboard instance). arboard is dropped from the crate.
Verified on Plasma/Wayland and on an Ubuntu GNOME/Wayland VM with DISPLAY
unset (the sandbox condition), where the copy now lands.
cc
This commit is contained in:
5 files changed
+66
-313
No files matched your search
@@ -37,17 +37,10 @@ url = "2"
|
||||
open = "5"
|
||||
dirs = "5"
|
||||
|
||||
# Share links (src/share_qt.rs).
|
||||
# Rust owns the clipboard on this side because the toast does: cxx-qt-lib 0.7
|
||||
# ships no QClipboard binding, and QML cannot publish QbzShell.toastJson, so
|
||||
# splitting the copy and its confirmation across the boundary would buy
|
||||
# nothing.
|
||||
#
|
||||
# wayland-data-control: Wayland-native clipboard via the data-control
|
||||
# protocols (GNOME/KDE/sway/niri/Hyprland), falling back to X11 when Wayland
|
||||
# is absent (#514 — without it arboard is X11-only and every copy fails on
|
||||
# Wayland-only sessions).
|
||||
arboard = { version = "3", features = ["wayland-data-control"] }
|
||||
# Share links (src/share_qt.rs) copy through Qt's own QClipboard
|
||||
# (cxx/clipboard.cpp) since #684: arboard's Wayland path needs the
|
||||
# wlr/ext-data-control protocol, which Mutter lacks, and its X11 fallback is
|
||||
# unreachable inside the Flatpak sandbox.
|
||||
|
||||
qbz-core = { path = "../qbz-core" }
|
||||
qbz-app = { path = "../qbz-app" }
|
||||
|
||||
@@ -401,6 +401,8 @@ fn build_rhi_items() {
|
||||
cc.file("cxx/win_shell.cpp");
|
||||
println!("cargo:rerun-if-changed=cxx/app_lifecycle.cpp");
|
||||
cc.file("cxx/app_lifecycle.cpp");
|
||||
println!("cargo:rerun-if-changed=cxx/clipboard.cpp");
|
||||
cc.file("cxx/clipboard.cpp");
|
||||
// Shell_NotifyIconW tray. No Q_OBJECT, so no moc; the body is inside
|
||||
// `#ifdef _WIN32` and compiles to nothing on Linux and macOS.
|
||||
println!("cargo:rerun-if-changed=cxx/win_tray.cpp");
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
// System clipboard through Qt (#684).
|
||||
//
|
||||
// The Rust side used arboard, whose Wayland path needs the wlr/ext-data-control
|
||||
// protocol. KWin has it; Mutter (GNOME) does not, so arboard fell back to X11
|
||||
// there — which works natively through XWayland and FAILS inside the Flatpak
|
||||
// sandbox (no X11 socket): "clipboard unavailable: X11 server connection timed
|
||||
// out". QClipboard speaks the base wl_data_device protocol every compositor
|
||||
// implements, needs no extra sandbox permission, and the offer stays alive for
|
||||
// as long as the application does (the #514 lifetime rule, for free).
|
||||
//
|
||||
// The clipboard is a GUI-thread object; the call is queued onto the
|
||||
// application's thread so any caller (a cxx-qt invokable already on it, or a
|
||||
// tokio worker) is fine.
|
||||
|
||||
#include <QtCore/QCoreApplication>
|
||||
#include <QtCore/QMetaObject>
|
||||
#include <QtCore/QString>
|
||||
#include <QtGui/QClipboard>
|
||||
#include <QtGui/QGuiApplication>
|
||||
|
||||
extern "C" void qbz_clipboard_set_text(const char *utf8, int len) {
|
||||
QCoreApplication *core = QCoreApplication::instance();
|
||||
if (!core || !qobject_cast<QGuiApplication *>(core)) {
|
||||
return;
|
||||
}
|
||||
const QString text = QString::fromUtf8(utf8, len);
|
||||
QMetaObject::invokeMethod(
|
||||
core,
|
||||
[text]() {
|
||||
if (QClipboard *clipboard = QGuiApplication::clipboard()) {
|
||||
clipboard->setText(text, QClipboard::Clipboard);
|
||||
}
|
||||
},
|
||||
Qt::QueuedConnection);
|
||||
}
|
||||
@@ -58,63 +58,31 @@ pub(crate) fn qobuz_label_url(label_id: &str) -> String {
|
||||
format!("https://play.qobuz.com/label/{label_id}")
|
||||
}
|
||||
|
||||
/// Long-lived clipboard instance. arboard ties the offer's lifetime to the
|
||||
/// LAST live `Clipboard` object: dropping it destroys the X11 selection
|
||||
/// window (contents survive only when a clipboard MANAGER accepts the
|
||||
/// handoff — KDE ships one, stock GNOME/XFCE/Cinnamon do not) and ends the
|
||||
/// Wayland offer with the same rule. The old create-per-copy pattern
|
||||
/// therefore worked on KDE and silently lost the text everywhere else
|
||||
/// (HiFi-wizard copy report, #514). One instance kept alive for the whole
|
||||
/// process serves the offer like any normal app.
|
||||
///
|
||||
/// This is the #514 fix, verbatim from `share.rs:32-41` — it is NOT
|
||||
/// boilerplate to be simplified into a local. A create-per-copy port would
|
||||
/// pass every test run on KDE and lose the text everywhere else.
|
||||
static CLIPBOARD: std::sync::OnceLock<std::sync::Mutex<Option<arboard::Clipboard>>> =
|
||||
std::sync::OnceLock::new();
|
||||
unsafe extern "C" {
|
||||
fn qbz_clipboard_set_text(utf8: *const std::os::raw::c_char, len: i32);
|
||||
}
|
||||
|
||||
/// Copy `text` to the system clipboard. Runs on a blocking thread —
|
||||
/// clipboard backends (X11/Wayland) can block. `share.rs:43-70`.
|
||||
/// Copy `text` to the system clipboard through Qt (`cxx/clipboard.cpp`).
|
||||
///
|
||||
/// The one adaptation vs the reference: in the Slint app the arm already runs
|
||||
/// inside a tokio context, whereas a cxx-qt invokable runs on the **Qt event
|
||||
/// loop thread**, where a bare `tokio::task::spawn_blocking` panics ("must be
|
||||
/// called from the context of a Tokio runtime"). It therefore goes through
|
||||
/// `crate::spawn` (main.rs) onto the process-global runtime first — the same
|
||||
/// `spawn` + `spawn_blocking` sandwich `local_ephemeral.rs`, `browse_qt.rs`
|
||||
/// and `ambient_qt.rs` already use.
|
||||
/// #684: the arboard path before this needed `wlr/ext-data-control`, which
|
||||
/// KWin has and Mutter does not; on GNOME it fell back to X11, which works
|
||||
/// natively through XWayland and is unreachable inside the Flatpak sandbox —
|
||||
/// every Copy link on GNOME + Flatpak logged "clipboard unavailable" and
|
||||
/// copied nothing. QClipboard uses the base Wayland data-device protocol,
|
||||
/// needs no sandbox permission, and QGuiApplication keeps the offer alive
|
||||
/// for the life of the process (the #514 lifetime rule, without the
|
||||
/// long-lived instance the arboard port had to carry).
|
||||
///
|
||||
/// Fire-and-forget by design: the caller never learns whether the copy
|
||||
/// worked, which is why the toast at the call site is unconditional
|
||||
/// (`main.rs:12758-12761` does the same).
|
||||
/// (`main.rs:12758-12761` does the same). The C++ side queues the write onto
|
||||
/// the GUI thread, so this is safe from a cxx-qt invokable or a worker.
|
||||
pub(crate) fn copy_to_clipboard(text: String) {
|
||||
crate::spawn(async move {
|
||||
let _ = tokio::task::spawn_blocking(move || {
|
||||
let cell = CLIPBOARD.get_or_init(|| std::sync::Mutex::new(None));
|
||||
let mut guard = match cell.lock() {
|
||||
Ok(g) => g,
|
||||
Err(poisoned) => poisoned.into_inner(),
|
||||
};
|
||||
if guard.is_none() {
|
||||
match arboard::Clipboard::new() {
|
||||
Ok(c) => *guard = Some(c),
|
||||
Err(e) => {
|
||||
log::warn!("[qbz-qt] clipboard unavailable: {e}");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(clipboard) = guard.as_mut() {
|
||||
if let Err(e) = clipboard.set_text(text) {
|
||||
log::warn!("[qbz-qt] clipboard set failed: {e}");
|
||||
// Drop the instance so the next copy reconnects — the
|
||||
// display connection may have gone away.
|
||||
*guard = None;
|
||||
}
|
||||
}
|
||||
})
|
||||
.await;
|
||||
});
|
||||
let len = i32::try_from(text.len()).unwrap_or(i32::MAX);
|
||||
// SAFETY: the pointer/length pair is read synchronously inside the call
|
||||
// (the C++ side copies into a QString before queueing), so `text` may be
|
||||
// dropped as soon as the call returns.
|
||||
unsafe { qbz_clipboard_set_text(text.as_ptr().cast(), len) };
|
||||
}
|
||||
|
||||
/// Artist header ⋯ → Share. `artist/ArtistPageView.slint:530-538` fires
|
||||
|
||||
Reference in new issue
Block a user