This patch implements SDL clipboard integration for QEMU's SDL2 UI backend,
specifically addressing the issue where clipboard functionality becomes
unreliable during host screen lock/unlock scenarios.

The implementation provides:
- Bidirectional clipboard synchronization between guest and host
- Robust screen lock/unlock handling to prevent clipboard conflicts  
- Asynchronous clipboard request processing
- Proper resource cleanup and error handling

This addresses a common usability issue where copy/paste functionality
stops working after the host screen is locked and unlocked, particularly
noticeable on macOS systems.

The patch adds a new build option --enable-sdl-clipboard (enabled by default)
to allow users to disable the feature if needed.

Tested on QEMU 10.0.0 and master branch, passes checkpatch with zero errors.

Signed-off-by: startergo <starte...@protonmail.com>
---
 include/ui/sdl2.h   |  15 ++++
 meson.build         |   3 +
 meson_options.txt   |   2 +
 ui/meson.build      |   3 +
 ui/sdl2-clipboard.c | 208 ++++++++++++++++++++++++++++++++++++++++++++
 ui/sdl2.c           |  15 ++++
 6 files changed, 246 insertions(+)
 create mode 100644 ui/sdl2-clipboard.c

diff --git a/include/ui/sdl2.h b/include/ui/sdl2.h
index dbe6e3d97..e73f83259 100644
--- a/include/ui/sdl2.h
+++ b/include/ui/sdl2.h
@@ -21,6 +21,10 @@
 # include <SDL_image.h>
 #endif
 
+#ifdef CONFIG_SDL_CLIPBOARD
+#include "ui/clipboard.h"
+#endif
+
 #include "ui/kbd-state.h"
 #ifdef CONFIG_OPENGL
 # include "ui/egl-helpers.h"
@@ -45,6 +49,11 @@ struct sdl2_console {
     bool gui_keysym;
     SDL_GLContext winctx;
     QKbdState *kbd;
+#ifdef CONFIG_SDL_CLIPBOARD
+    QemuClipboardPeer cbpeer;
+    bool clipboard_active;
+    uint32_t last_focus_time;
+#endif
 #ifdef CONFIG_OPENGL
     QemuGLShader *gls;
     egl_fb guest_fb;
@@ -97,4 +106,10 @@ void sdl2_gl_scanout_texture(DisplayChangeListener *dcl,
 void sdl2_gl_scanout_flush(DisplayChangeListener *dcl,
                            uint32_t x, uint32_t y, uint32_t w, uint32_t h);
 
+#ifdef CONFIG_SDL_CLIPBOARD
+void sdl2_clipboard_init(struct sdl2_console *scon);
+void sdl2_clipboard_handle_focus_change(struct sdl2_console *scon, bool 
gained_focus);
+void sdl2_clipboard_handle_request(struct sdl2_console *scon);
+#endif
+
 #endif /* SDL2_H */
diff --git a/meson.build b/meson.build
index 41f68d380..4a37df966 100644
--- a/meson.build
+++ b/meson.build
@@ -1596,6 +1596,8 @@ else
   sdl_image = not_found
 endif
 
+have_sdl_clipboard = sdl.found() and get_option('sdl_clipboard')
+
 rbd = not_found
 if not get_option('rbd').auto() or have_block
   librados = cc.find_library('rados', required: get_option('rbd'))
@@ -2511,6 +2513,7 @@ config_host_data.set('CONFIG_RELOCATABLE', 
get_option('relocatable'))
 config_host_data.set('CONFIG_SAFESTACK', get_option('safe_stack'))
 config_host_data.set('CONFIG_SDL', sdl.found())
 config_host_data.set('CONFIG_SDL_IMAGE', sdl_image.found())
+config_host_data.set('CONFIG_SDL_CLIPBOARD', have_sdl_clipboard)
 config_host_data.set('CONFIG_SECCOMP', seccomp.found())
 if seccomp.found()
   config_host_data.set('CONFIG_SECCOMP_SYSRAWRC', seccomp_has_sysrawrc)
diff --git a/meson_options.txt b/meson_options.txt
index 59d973bca..be2cba3a3 100644
--- a/meson_options.txt
+++ b/meson_options.txt
@@ -212,6 +212,8 @@ option('sdl', type : 'feature', value : 'auto',
        description: 'SDL user interface')
 option('sdl_image', type : 'feature', value : 'auto',
        description: 'SDL Image support for icons')
+option('sdl_clipboard', type : 'boolean', value : true,
+       description: 'SDL clipboard support')
 option('seccomp', type : 'feature', value : 'auto',
        description: 'seccomp support')
 option('smartcard', type : 'feature', value : 'auto',
diff --git a/ui/meson.build b/ui/meson.build
index 35fb04cad..6d1bf3477 100644
--- a/ui/meson.build
+++ b/ui/meson.build
@@ -126,6 +126,9 @@ if sdl.found()
     'sdl2-input.c',
     'sdl2.c',
   ))
+  if have_sdl_clipboard
+    sdl_ss.add(files('sdl2-clipboard.c'))
+  endif
   sdl_ss.add(when: opengl, if_true: files('sdl2-gl.c'))
   sdl_ss.add(when: x11, if_true: files('x_keymap.c'))
   ui_modules += {'sdl' : sdl_ss}
diff --git a/ui/sdl2-clipboard.c b/ui/sdl2-clipboard.c
new file mode 100644
index 000000000..6cc0fd79c
--- /dev/null
+++ b/ui/sdl2-clipboard.c
@@ -0,0 +1,208 @@
+/*
+ * SDL UI -- clipboard support with screen lock handling
+ *
+ * Copyright (C) 2023 Kamay Xutax <ad...@xutaxkamay.com>
+ * Copyright (C) 2025 startergo <starte...@protonmail.com>
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ */
+
+#include "qemu/osdep.h"
+#include "ui/console.h"
+#include "ui/clipboard.h"
+#include "ui/sdl2.h"
+#include "qemu/log.h"
+
+#ifdef CONFIG_SDL_CLIPBOARD
+
+/* Track pending clipboard requests to handle async data */
+typedef struct {
+    struct sdl2_console *scon;
+    QemuClipboardInfo *info;
+    QemuClipboardType type;
+    uint32_t timestamp;
+} SDLClipboardRequest;
+
+static SDLClipboardRequest *pending_request;
+
+static void sdl2_clipboard_clear_pending(void)
+{
+    if (pending_request) {
+        if (pending_request->info) {
+            qemu_clipboard_info_unref(pending_request->info);
+        }
+        g_free(pending_request);
+        pending_request = NULL;
+    }
+}
+
+static void sdl2_clipboard_reset_state(struct sdl2_console *scon)
+{
+    /* Clear any pending requests when clipboard state is reset */
+    sdl2_clipboard_clear_pending();
+
+    /* Force a fresh clipboard check after reconnection */
+    if (scon->clipboard_active) {
+        scon->last_focus_time = SDL_GetTicks();
+    }
+}
+
+static void sdl2_clipboard_notify(Notifier *notifier, void *data)
+{
+    QemuClipboardNotify *notify = data;
+    struct sdl2_console *scon =
+        container_of(notifier, struct sdl2_console, cbpeer.notifier);
+    bool self_update = notify->info->owner == &scon->cbpeer;
+    const char *text_data;
+    size_t text_size;
+
+    /* Skip processing if clipboard is not active (e.g., during screen lock) */
+    if (!scon->clipboard_active) {
+        return;
+    }
+
+    switch (notify->type) {
+    case QEMU_CLIPBOARD_UPDATE_INFO:
+        {
+            /* Skip self-updates to avoid clipboard manager conflicts */
+            if (self_update) {
+                return;
+            }
+
+            if (!notify->info->types[QEMU_CLIPBOARD_TYPE_TEXT].available) {
+                return;
+            }
+
+            /* Check if this is completion of our pending request */
+            if (pending_request && pending_request->info == notify->info &&
+                pending_request->type == QEMU_CLIPBOARD_TYPE_TEXT) {
+                sdl2_clipboard_clear_pending();
+            }
+
+            /* Check if data is available, request asynchronously if not */
+            if (!notify->info->types[QEMU_CLIPBOARD_TYPE_TEXT].data) {
+                if (!pending_request) {
+                    pending_request = g_new0(SDLClipboardRequest, 1);
+                    pending_request->scon = scon;
+                    pending_request->info =
+                        qemu_clipboard_info_ref(notify->info);
+                    pending_request->type = QEMU_CLIPBOARD_TYPE_TEXT;
+                    pending_request->timestamp = SDL_GetTicks();
+                    qemu_clipboard_request(notify->info,
+                                           QEMU_CLIPBOARD_TYPE_TEXT);
+                }
+                return;
+            }
+
+            /* Process available data */
+            text_size = notify->info->types[QEMU_CLIPBOARD_TYPE_TEXT].size;
+            if (text_size == 0) {
+                return;
+            }
+
+            text_data = (const char *)
+                notify->info->types[QEMU_CLIPBOARD_TYPE_TEXT].data;
+
+            /* Ensure null termination for SDL clipboard */
+            g_autofree char *text = g_strndup(text_data, text_size);
+            if (text && text[0] != '\0') {
+                if (SDL_SetClipboardText(text) < 0) {
+                    qemu_log_mask(LOG_GUEST_ERROR,
+                                  "SDL clipboard: Failed to set clipboard 
text: %s\n",
+                                  SDL_GetError());
+                }
+            } else if (!text) {
+                qemu_log_mask(LOG_GUEST_ERROR,
+                              "SDL clipboard: Failed to allocate memory for 
clipboard text\n");
+            }
+            break;
+        }
+    case QEMU_CLIPBOARD_RESET_SERIAL:
+        sdl2_clipboard_reset_state(scon);
+        break;
+    }
+}
+
+static void sdl2_clipboard_request(QemuClipboardInfo *info,
+                                   QemuClipboardType type)
+{
+    g_autofree char *text = NULL;
+
+    if (type != QEMU_CLIPBOARD_TYPE_TEXT) {
+        return;
+    }
+
+    text = SDL_GetClipboardText();
+    if (!text) {
+        qemu_log_mask(LOG_GUEST_ERROR,
+                      "SDL clipboard: Failed to get clipboard text: %s\n",
+                      SDL_GetError());
+        return;
+    }
+
+    qemu_clipboard_set_data(info->owner, info, type,
+                            strlen(text), text, true);
+}
+
+void sdl2_clipboard_init(struct sdl2_console *scon)
+{
+    scon->cbpeer.name = "sdl2-clipboard";
+    scon->cbpeer.notifier.notify = sdl2_clipboard_notify;
+    scon->cbpeer.request = sdl2_clipboard_request;
+    scon->clipboard_active = true;
+    scon->last_focus_time = SDL_GetTicks();
+
+    qemu_clipboard_peer_register(&scon->cbpeer);
+}
+
+void sdl2_clipboard_handle_focus_change(struct sdl2_console *scon, bool 
gained_focus)
+{
+    uint32_t current_time = SDL_GetTicks();
+
+    if (gained_focus) {
+        /* Reactivate clipboard after regaining focus */
+        scon->clipboard_active = true;
+        scon->last_focus_time = current_time;
+
+        /* Clear any stale pending requests */
+        sdl2_clipboard_clear_pending();
+
+        /* Force a fresh clipboard sync after focus is regained */
+        sdl2_clipboard_handle_request(scon);
+    } else {
+        /* Deactivate clipboard when losing focus to prevent conflicts */
+        scon->clipboard_active = false;
+        sdl2_clipboard_clear_pending();
+    }
+}
+
+void sdl2_clipboard_handle_request(struct sdl2_console *scon)
+{
+    g_autofree char *text = NULL;
+    QemuClipboardInfo *info;
+
+    /* Skip if clipboard is not active */
+    if (!scon->clipboard_active) {
+        return;
+    }
+
+    text = SDL_GetClipboardText();
+    if (!text) {
+        qemu_log_mask(LOG_GUEST_ERROR,
+                      "SDL clipboard: Failed to get clipboard text: %s\n",
+                      SDL_GetError());
+        return;
+    }
+
+    if (text[0] == '\0') {
+        return; /* Ignore empty clipboard */
+    }
+
+    info = qemu_clipboard_info_new(&scon->cbpeer,
+                                   QEMU_CLIPBOARD_SELECTION_CLIPBOARD);
+    qemu_clipboard_set_data(&scon->cbpeer, info, QEMU_CLIPBOARD_TYPE_TEXT,
+                            strlen(text), text, true);
+    qemu_clipboard_info_unref(info);
+}
+
+#endif /* CONFIG_SDL_CLIPBOARD */
diff --git a/ui/sdl2.c b/ui/sdl2.c
index cda4293a5..d89ac16dd 100644
--- a/ui/sdl2.c
+++ b/ui/sdl2.c
@@ -606,11 +606,17 @@ static void handle_windowevent(SDL_Event *ev)
          * key is released.
          */
         scon->ignore_hotkeys = get_mod_state();
+#ifdef CONFIG_SDL_CLIPBOARD
+        sdl2_clipboard_handle_focus_change(scon, true);
+#endif
         break;
     case SDL_WINDOWEVENT_FOCUS_LOST:
         if (gui_grab && !gui_fullscreen) {
             sdl_grab_end(scon);
         }
+#ifdef CONFIG_SDL_CLIPBOARD
+        sdl2_clipboard_handle_focus_change(scon, false);
+#endif
         break;
     case SDL_WINDOWEVENT_RESTORED:
         update_displaychangelistener(&scon->dcl, GUI_REFRESH_INTERVAL_DEFAULT);
@@ -691,6 +697,11 @@ void sdl2_poll_events(struct sdl2_console *scon)
         case SDL_WINDOWEVENT:
             handle_windowevent(ev);
             break;
+#ifdef CONFIG_SDL_CLIPBOARD
+        case SDL_CLIPBOARDUPDATE:
+            sdl2_clipboard_handle_request(scon);
+            break;
+#endif
         default:
             break;
         }
@@ -901,6 +912,10 @@ static void sdl2_display_init(DisplayState *ds, 
DisplayOptions *o)
         }
         register_displaychangelistener(&sdl2_console[i].dcl);
 
+#ifdef CONFIG_SDL_CLIPBOARD
+        sdl2_clipboard_init(&sdl2_console[i]);
+#endif
+
 #if defined(SDL_VIDEO_DRIVER_WINDOWS) || defined(SDL_VIDEO_DRIVER_X11)
         if (SDL_GetWindowWMInfo(sdl2_console[i].real_window, &info)) {
 #if defined(SDL_VIDEO_DRIVER_WINDOWS)
-- 
2.50.1



Reply via email to