@@ -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
192193impl 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
375388impl 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 ) ]
472506struct 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}
0 commit comments