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