Skip to content

Commit e68cf0d

Browse files
committed
fix: handle Wayland guest grabs and transient empty echoes
1 parent d1d01ba commit e68cf0d

4 files changed

Lines changed: 84 additions & 5 deletions

File tree

src/viewer/clipboard.rs

Lines changed: 72 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,7 @@ struct SelectionUiState {
187187
ignored_remote_content: Option<ClipboardContent>,
188188
last_seen_content: Option<ClipboardContent>,
189189
pending_guest_content: Option<ClipboardContent>,
190+
awaiting_remote_echo: bool,
190191
}
191192

192193
impl ClipboardUiState {
@@ -262,9 +263,17 @@ pub(super) fn install_host_clipboard_bridge(
262263
let input_tx = input_tx.clone();
263264
move |_| {
264265
debug("viewer focus entered");
266+
let _ = input_tx.send(InputEvent::ClipboardViewerFocused(true));
265267
refresh_clipboard_state(&picture, &ui_state, &input_tx)
266268
}
267269
});
270+
focus.connect_leave({
271+
let input_tx = input_tx.clone();
272+
move |_| {
273+
debug("viewer focus left");
274+
let _ = input_tx.send(InputEvent::ClipboardViewerFocused(false));
275+
}
276+
});
268277
picture.add_controller(focus);
269278

270279
let paste_shortcuts = gtk::EventControllerKey::new();
@@ -293,6 +302,9 @@ pub(super) fn install_host_clipboard_bridge(
293302
if window.is_active() {
294303
debug("window became active");
295304
refresh_clipboard_state(&picture, &ui_state, &input_tx);
305+
} else {
306+
debug("window became inactive");
307+
let _ = input_tx.send(InputEvent::ClipboardViewerFocused(false));
296308
}
297309
}
298310
});
@@ -330,6 +342,7 @@ pub(super) fn apply_guest_clipboard(
330342
selection_state.ignored_remote_content = Some(content.clone());
331343
selection_state.last_seen_content = (!content.is_empty()).then_some(content.clone());
332344
selection_state.pending_guest_content = None;
345+
selection_state.awaiting_remote_echo = true;
333346
}
334347
debug(format!("gtk set_content succeeded for {selection:?}"));
335348
Ok(())
@@ -373,6 +386,14 @@ pub(super) struct ClipboardSession {
373386
}
374387

375388
impl ClipboardSession {
389+
pub(super) fn set_viewer_focused(&self, focused: bool) {
390+
let mut shared = self.shared.lock().expect("clipboard mutex was poisoned");
391+
if shared.viewer_focused != focused {
392+
shared.viewer_focused = focused;
393+
debug(format!("viewer clipboard focus -> {focused}"));
394+
}
395+
}
396+
376397
/// Publish the current host clipboard to the guest or release ownership if
377398
/// the host selection no longer contains any MIME types QD2 can forward.
378399
pub(super) async fn update_host_content(
@@ -468,10 +489,24 @@ impl ClipboardSession {
468489
}
469490
}
470491

492+
fn should_accept_remote_grab(
493+
current_serial: u32,
494+
local_owner: bool,
495+
viewer_focused: bool,
496+
incoming_serial: u32,
497+
) -> bool {
498+
if viewer_focused && local_owner {
499+
return true;
500+
}
501+
502+
incoming_serial > current_serial || (incoming_serial == current_serial && !local_owner)
503+
}
504+
471505
#[derive(Default)]
472506
struct ClipboardBridgeState {
473507
clipboard: SelectionBridgeState,
474508
primary: SelectionBridgeState,
509+
viewer_focused: bool,
475510
}
476511

477512
#[derive(Default)]
@@ -547,22 +582,35 @@ impl ClipboardHandler for ClipboardListener {
547582

548583
{
549584
let mut shared = self.shared.lock().expect("clipboard mutex was poisoned");
585+
let viewer_focused = shared.viewer_focused;
550586
let Some(selection_state) = shared.selection_mut(selection) else {
551587
debug(format!(
552588
"ignoring unsupported guest clipboard selection {selection:?}"
553589
));
554590
return;
555591
};
556592

557-
if serial < selection_state.current_serial
558-
|| (serial == selection_state.current_serial && selection_state.local_owner)
559-
{
593+
if !should_accept_remote_grab(
594+
selection_state.current_serial,
595+
selection_state.local_owner,
596+
viewer_focused,
597+
serial,
598+
) {
560599
debug(format!(
561600
"ignoring stale/conflicting grab: current_serial={} local_owner={}",
562601
selection_state.current_serial, selection_state.local_owner
563602
));
564603
return;
565604
}
605+
if viewer_focused
606+
&& selection_state.local_owner
607+
&& serial <= selection_state.current_serial
608+
{
609+
debug(format!(
610+
"accepting guest grab while viewer is focused despite local serial {}",
611+
selection_state.current_serial
612+
));
613+
}
566614
selection_state.current_serial = serial;
567615
selection_state.local_owner = false;
568616
}
@@ -939,10 +987,23 @@ fn finish_host_snapshot(
939987
"ignoring GTK clipboard echo from remote-set content for {selection:?}"
940988
));
941989
selection_state.ignored_remote_content = None;
990+
selection_state.awaiting_remote_echo = false;
942991
selection_state.last_seen_content = (!content.is_empty()).then_some(content);
943992
return;
944993
}
945994

995+
if selection_state.awaiting_remote_echo && content.is_empty() {
996+
debug(format!(
997+
"ignoring transient empty GTK clipboard snapshot after remote set for {selection:?}"
998+
));
999+
return;
1000+
}
1001+
1002+
if selection_state.awaiting_remote_echo {
1003+
selection_state.awaiting_remote_echo = false;
1004+
selection_state.ignored_remote_content = None;
1005+
}
1006+
9461007
if selection_state.last_seen_content.as_ref() == Some(&content) {
9471008
debug(format!("gtk clipboard content unchanged for {selection:?}"));
9481009
return;
@@ -1106,7 +1167,7 @@ mod tests {
11061167
use super::{
11071168
ClipboardContent, ClipboardSelection, IMAGE_PNG, STRING, TEXT, TEXT_HTML, TEXT_PLAIN,
11081169
TEXT_PLAIN_UTF8, TEXT_URI_LIST, UTF8_STRING, canonical_rich_mime,
1109-
preferred_text_request_mimes, remote_fetch_plan,
1170+
preferred_text_request_mimes, remote_fetch_plan, should_accept_remote_grab,
11101171
};
11111172

11121173
#[test]
@@ -1188,4 +1249,11 @@ mod tests {
11881249
assert_eq!(ClipboardSelection::Primary as u32, 1);
11891250
assert_eq!(plan[0].requested_mimes, vec![TEXT_PLAIN_UTF8]);
11901251
}
1252+
1253+
#[test]
1254+
fn focused_viewer_allows_guest_grab_to_override_local_owner() {
1255+
assert!(should_accept_remote_grab(5, true, true, 0));
1256+
assert!(!should_accept_remote_grab(5, true, false, 0));
1257+
assert!(should_accept_remote_grab(5, false, false, 6));
1258+
}
11911259
}

src/viewer/listener/remote.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,9 @@ impl RemoteConsole {
9191
.release(keycode)
9292
.await
9393
.with_context(|| format!("failed to send key release for qnum {keycode}")),
94-
InputEvent::ClipboardHostChanged(_, _) => Ok(()),
94+
InputEvent::ClipboardViewerFocused(_) | InputEvent::ClipboardHostChanged(_, _) => {
95+
Ok(())
96+
}
9597
InputEvent::MousePress(button) => self
9698
.mouse
9799
.press(button)

src/viewer/listener/session.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,14 @@ pub(super) async fn listener_session(
101101
}
102102
maybe_input = input_rx.recv() => match maybe_input {
103103
Some(input) => {
104+
if let InputEvent::ClipboardViewerFocused(focused) = &input {
105+
clipboard::debug(format!("listener received ClipboardViewerFocused({focused})"));
106+
if let Some(clipboard) = &clipboard {
107+
clipboard.set_viewer_focused(*focused);
108+
}
109+
continue;
110+
}
111+
104112
if let InputEvent::ClipboardHostChanged(selection, content) = &input {
105113
clipboard::debug(format!(
106114
"listener received ClipboardHostChanged selection={selection:?}: {}",

src/viewer/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -972,6 +972,7 @@ enum ViewerEvent {
972972
enum InputEvent {
973973
KeyPress(u32),
974974
KeyRelease(u32),
975+
ClipboardViewerFocused(bool),
975976
ClipboardHostChanged(ClipboardSelection, Option<clipboard::ClipboardContent>),
976977
MousePress(MouseButton),
977978
MouseRelease(MouseButton),

0 commit comments

Comments
 (0)