This MR adds clipboard support to the winewayland driver.
Under Wayland only applications that have the keyboard focus can interact with the clipboard (a.k.a. `wl_data_device`). Such constraints are not a natural fit for Wine's current clipboard infrastructure, which uses a separate thread and window in the desktop process to act as the win32-side clipboard proxy for all native windows.
This MR tries to work within the current Wine clipboard Wine by forwarding relevant clipboard messages to the foreground window which is likely to have the keyboard focus and that can actually handle them. This works well in practice (although there are some edge cases this fails), but I am open to different ideas about implementing the clipboard integration.
Some notes about the MR: 1. Some formats that require special treatment (e.g., CF_HTML, CF_HDROP) are not implemented in this MR to keep the size reasonable (and also to not distract from the goal of this MR which is to propose/discuss the basic design of clipboard integration). 2. Dynamic registration of newly-seen/unknown formats is not supported at the moment. It's not clear to what degree that's useful, since many (most?) Windows clipboard format strings are free-form and don't use the MIME type standard. But perhaps enough Windows apps know about MIME types nowadays to make this addition worth it? 3. Since access to the Wayland clipboard data is performed from the focused window threads (rather than the dedicated clipboard thread), any blocking while waiting for data to be sent/received has more potential to affect the applications. I have implemented reasonable timeouts which are hopefully enough to make this a non-issue. A (more complex) alternative would be a different design that attempts to offload any potentially blocking work to the dedicated clipboard thread (or is there some kind of async read/write mechanism we can use?). I am not convinced the extra complexity is worth it, though.
From: Alexandros Frantzis alexandros.frantzis@collabora.com
--- dlls/winewayland.drv/Makefile.in | 1 + dlls/winewayland.drv/wayland.c | 9 ++++ dlls/winewayland.drv/wayland_data_device.c | 48 ++++++++++++++++++++++ dlls/winewayland.drv/waylanddrv.h | 14 +++++++ 4 files changed, 72 insertions(+) create mode 100644 dlls/winewayland.drv/wayland_data_device.c
diff --git a/dlls/winewayland.drv/Makefile.in b/dlls/winewayland.drv/Makefile.in index 9ad1ad6889d..2db42eaba09 100644 --- a/dlls/winewayland.drv/Makefile.in +++ b/dlls/winewayland.drv/Makefile.in @@ -13,6 +13,7 @@ SOURCES = \ viewporter.xml \ vulkan.c \ wayland.c \ + wayland_data_device.c \ wayland_keyboard.c \ wayland_output.c \ wayland_pointer.c \ diff --git a/dlls/winewayland.drv/wayland.c b/dlls/winewayland.drv/wayland.c index 9432dc934c9..046f6d0f138 100644 --- a/dlls/winewayland.drv/wayland.c +++ b/dlls/winewayland.drv/wayland.c @@ -143,6 +143,7 @@ static void registry_handle_global(void *data, struct wl_registry *registry, seat->global_id = id; wl_seat_add_listener(seat->wl_seat, &seat_listener, NULL); pthread_mutex_unlock(&seat->mutex); + if (process_wayland.wl_data_device_manager) wayland_data_device_init(); } else if (strcmp(interface, "wp_viewporter") == 0) { @@ -164,6 +165,13 @@ static void registry_handle_global(void *data, struct wl_registry *registry, process_wayland.zwp_relative_pointer_manager_v1 = wl_registry_bind(registry, id, &zwp_relative_pointer_manager_v1_interface, 1); } + else if (strcmp(interface, "wl_data_device_manager") == 0) + { + process_wayland.wl_data_device_manager = + wl_registry_bind(registry, id, &wl_data_device_manager_interface, + version < 3 ? version : 3); + if (process_wayland.seat.wl_seat) wayland_data_device_init(); + } }
static void registry_handle_global_remove(void *data, struct wl_registry *registry, @@ -189,6 +197,7 @@ static void registry_handle_global_remove(void *data, struct wl_registry *regist { TRACE("removing seat\n"); if (process_wayland.pointer.wl_pointer) wayland_pointer_deinit(); + if (process_wayland.data_device.wl_data_device) wayland_data_device_deinit(); pthread_mutex_lock(&seat->mutex); wl_seat_release(seat->wl_seat); seat->wl_seat = NULL; diff --git a/dlls/winewayland.drv/wayland_data_device.c b/dlls/winewayland.drv/wayland_data_device.c new file mode 100644 index 00000000000..94d7ac7f36b --- /dev/null +++ b/dlls/winewayland.drv/wayland_data_device.c @@ -0,0 +1,48 @@ +/* + * Wayland data device + * + * Copyright 2025 Alexandros Frantzis for Collabora Ltd + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA + */ + +#if 0 +#pragma makedep unix +#endif + +#include "config.h" + +#include "waylanddrv.h" + +void wayland_data_device_init(void) +{ + struct wayland_data_device *data_device = &process_wayland.data_device; + + data_device->wl_data_device = + wl_data_device_manager_get_data_device(process_wayland.wl_data_device_manager, + process_wayland.seat.wl_seat); +} + +void wayland_data_device_deinit(void) +{ + struct wayland_data_device *data_device = &process_wayland.data_device; + + if (wl_data_device_get_version(data_device->wl_data_device) < 2) + wl_data_device_destroy(data_device->wl_data_device); + else + wl_data_device_release(data_device->wl_data_device); + + data_device->wl_data_device = NULL; +} diff --git a/dlls/winewayland.drv/waylanddrv.h b/dlls/winewayland.drv/waylanddrv.h index 72a37cb3ffb..51a5ddce2f5 100644 --- a/dlls/winewayland.drv/waylanddrv.h +++ b/dlls/winewayland.drv/waylanddrv.h @@ -118,6 +118,11 @@ struct wayland_seat pthread_mutex_t mutex; };
+struct wayland_data_device +{ + struct wl_data_device *wl_data_device; +}; + struct wayland { BOOL initialized; @@ -132,9 +137,11 @@ struct wayland struct wl_subcompositor *wl_subcompositor; struct zwp_pointer_constraints_v1 *zwp_pointer_constraints_v1; struct zwp_relative_pointer_manager_v1 *zwp_relative_pointer_manager_v1; + struct wl_data_device_manager *wl_data_device_manager; struct wayland_seat seat; struct wayland_keyboard keyboard; struct wayland_pointer pointer; + struct wayland_data_device data_device; struct wl_list output_list; /* Protects the output_list and the wayland_output.current states. */ pthread_mutex_t output_mutex; @@ -340,6 +347,13 @@ void wayland_pointer_init(struct wl_pointer *wl_pointer); void wayland_pointer_deinit(void); void wayland_pointer_clear_constraint(void);
+/********************************************************************** + * Wayland data device + */ + +void wayland_data_device_init(void); +void wayland_data_device_deinit(void); + /********************************************************************** * OpenGL */
From: Alexandros Frantzis alexandros.frantzis@collabora.com
Under Wayland only the data device for the seat which has the keyboard focus can set the clipboard selection, so we forward the WM_CLIPBOARDUPDATE message to the proper window thread (as an internal driver window message). --- dlls/winewayland.drv/wayland.c | 1 + dlls/winewayland.drv/wayland_data_device.c | 275 +++++++++++++++++++++ dlls/winewayland.drv/wayland_keyboard.c | 4 + dlls/winewayland.drv/wayland_surface.c | 3 + dlls/winewayland.drv/waylanddrv.h | 17 ++ dlls/winewayland.drv/waylanddrv_main.c | 1 + dlls/winewayland.drv/window.c | 4 + 7 files changed, 305 insertions(+)
diff --git a/dlls/winewayland.drv/wayland.c b/dlls/winewayland.drv/wayland.c index 046f6d0f138..49f4dc9b3a8 100644 --- a/dlls/winewayland.drv/wayland.c +++ b/dlls/winewayland.drv/wayland.c @@ -37,6 +37,7 @@ struct wayland process_wayland = .seat.mutex = PTHREAD_MUTEX_INITIALIZER, .keyboard.mutex = PTHREAD_MUTEX_INITIALIZER, .pointer.mutex = PTHREAD_MUTEX_INITIALIZER, + .data_device.mutex = PTHREAD_MUTEX_INITIALIZER, .output_list = {&process_wayland.output_list, &process_wayland.output_list}, .output_mutex = PTHREAD_MUTEX_INITIALIZER }; diff --git a/dlls/winewayland.drv/wayland_data_device.c b/dlls/winewayland.drv/wayland_data_device.c index 94d7ac7f36b..f41f35c11fc 100644 --- a/dlls/winewayland.drv/wayland_data_device.c +++ b/dlls/winewayland.drv/wayland_data_device.c @@ -24,25 +24,300 @@
#include "config.h"
+#include <errno.h> +#include <fcntl.h> +#include <poll.h> +#include <stdlib.h> +#include <unistd.h> + #include "waylanddrv.h" +#include "wine/debug.h" + +WINE_DEFAULT_DEBUG_CHANNEL(clipboard); + +static HWND desktop_clipboard_hwnd; + +static HWND get_clipboard_hwnd(void) +{ + if (!desktop_clipboard_hwnd) + { + static const WCHAR clipboard_classnameW[] = { + '_','_','w','i','n','e','_','c','l','i','p','b','o','a','r','d','_', + 'm','a','n','a','g','e','r','\0'}; + UNICODE_STRING clipboard_classnameU; + RtlInitUnicodeString(&clipboard_classnameU, clipboard_classnameW); + + desktop_clipboard_hwnd = + NtUserFindWindowEx(HWND_MESSAGE, NULL, &clipboard_classnameU, NULL, 0); + } + + return desktop_clipboard_hwnd; +} + +static int poll_until(int fd, int events, ULONG end_time) +{ + struct pollfd pfd = { .fd = fd, .events = events }; + ULONG now = NtGetTickCount(); + int ret = 0; + + while ((now = NtGetTickCount()) < end_time && + (ret = poll(&pfd, 1, end_time - now)) == -1 && + errno == EINTR) + { + continue; + } + + if (ret <= 0 || !(pfd.revents & events)) + { + TRACE("Failed polling ret=%d errno=%d revents=0x%x\n", + ret, ret == -1 ? errno : 0, pfd.revents); + return ret <= 0 ? ret : -1; + } + + return ret; +} + +static void write_all(int fd, const void *buf, size_t count) +{ + static const ULONG timeout = 500; + size_t nwritten = 0; + int flags; + ULONG end_time; + ssize_t ret; + + if (((flags = fcntl(fd, F_GETFL, 0)) < 0) || + (!(flags & O_NONBLOCK) && fcntl(fd, F_SETFL, flags | O_NONBLOCK) < 0)) + { + TRACE("Failed to make data source send fd non-blocking, " + "will not be able to properly enforce write timeout\n"); + } + + end_time = NtGetTickCount() + timeout; + while (nwritten < count) + { + if (poll_until(fd, POLLOUT, end_time) <= 0) break; + ret = write(fd, (const char*)buf + nwritten, count - nwritten); + if (ret == -1 && errno != EINTR && errno != EAGAIN && errno != EWOULDBLOCK) + { + break; + } + else if (ret > 0) + { + nwritten += ret; + } + } + + if (nwritten < count) + { + WARN("Failed to write all clipboard data, had %zu bytes, wrote %zu bytes\n", + count, nwritten); + } +} + +static void *export_unicode_text(void *data, size_t size, size_t *ret_size) +{ + DWORD byte_count; + char *bytes; + + /* Wayland apps expect strings to not be zero-terminated, so avoid + * zero-terminating the resulting converted string. */ + if (size >= sizeof(WCHAR) && ((WCHAR *)data)[size / sizeof(WCHAR) - 1] == 0) + size -= sizeof(WCHAR); + + RtlUnicodeToUTF8N(NULL, 0, &byte_count, data, size); + if (!(bytes = malloc(byte_count))) return NULL; + RtlUnicodeToUTF8N(bytes, byte_count, &byte_count, data, size); + + *ret_size = byte_count; + return bytes; +} + +/********************************************************************** + * wl_data_source handling + */ + +static void wayland_data_source_export(int32_t fd) +{ + struct get_clipboard_params params = { .data_only = TRUE }; + static const size_t buffer_size = 1024; + HWND clipboard_hwnd = get_clipboard_hwnd(); + void *buffer, *exported = NULL; + size_t exported_size; + + if (!clipboard_hwnd) return; + if (!(buffer = malloc(buffer_size))) return; + if (!NtUserOpenClipboard(clipboard_hwnd, 0)) + { + TRACE("failed to open clipboard for export\n"); + free(buffer); + return; + } + + params.data = buffer; + params.size = buffer_size; + if (NtUserGetClipboardData(CF_UNICODETEXT, ¶ms)) + { + exported = export_unicode_text(params.data, params.size, &exported_size); + } + else if (params.data_size) + { + /* If 'buffer_size' is too small, NtUserGetClipboardData writes the + * minimum size in 'params.data_size', so we retry with that. */ + void *new_buffer = realloc(buffer, params.data_size); + if (new_buffer) + { + buffer = new_buffer; + params.data = new_buffer; + if (NtUserGetClipboardData(CF_UNICODETEXT, ¶ms)) + exported = export_unicode_text(params.data, params.size, &exported_size); + } + } + + NtUserCloseClipboard(); + if (exported) write_all(fd, exported, exported_size); + + free(exported); + free(buffer); +} + +static void data_source_target(void *data, struct wl_data_source *source, + const char *mime_type) +{ +} + +static void data_source_send(void *data, struct wl_data_source *source, + const char *mime_type, int32_t fd) +{ + if (!strcmp(mime_type, "text/plain;charset=utf-8")) + wayland_data_source_export(fd); + close(fd); +} + +static void data_source_cancelled(void *data, struct wl_data_source *source) +{ + struct wayland_data_device *data_device = data; + + pthread_mutex_lock(&data_device->mutex); + wl_data_source_destroy(source); + if (source == data_device->wl_data_source) + data_device->wl_data_source = NULL; + pthread_mutex_unlock(&data_device->mutex); +} + +static void data_source_dnd_drop_performed(void *data, + struct wl_data_source *source) +{ +} + +static void data_source_dnd_finished(void *data, struct wl_data_source *source) +{ +} + +static void data_source_action(void *data, struct wl_data_source *source, + uint32_t dnd_action) +{ +} + +static const struct wl_data_source_listener data_source_listener = +{ + data_source_target, + data_source_send, + data_source_cancelled, + data_source_dnd_drop_performed, + data_source_dnd_finished, + data_source_action, +};
void wayland_data_device_init(void) { struct wayland_data_device *data_device = &process_wayland.data_device;
+ pthread_mutex_lock(&data_device->mutex); data_device->wl_data_device = wl_data_device_manager_get_data_device(process_wayland.wl_data_device_manager, process_wayland.seat.wl_seat); + pthread_mutex_unlock(&data_device->mutex); }
void wayland_data_device_deinit(void) { struct wayland_data_device *data_device = &process_wayland.data_device;
+ pthread_mutex_lock(&data_device->mutex); if (wl_data_device_get_version(data_device->wl_data_device) < 2) wl_data_device_destroy(data_device->wl_data_device); else wl_data_device_release(data_device->wl_data_device);
data_device->wl_data_device = NULL; + pthread_mutex_unlock(&data_device->mutex); +} + +void wayland_data_device_clipboard_update(void) +{ + struct wayland_data_device *data_device = &process_wayland.data_device; + uint32_t enter_serial = process_wayland.keyboard.enter_serial; + HWND clipboard_hwnd = get_clipboard_hwnd(); + struct wl_data_source *source; + UINT clipboard_format = 0; + + TRACE("\n"); + + if (!clipboard_hwnd) return; + + pthread_mutex_lock(&process_wayland.keyboard.mutex); + enter_serial = process_wayland.keyboard.enter_serial; + pthread_mutex_unlock(&process_wayland.keyboard.mutex); + if (!enter_serial) return; + + if (!NtUserOpenClipboard(clipboard_hwnd, 0)) + { + TRACE("failed to open clipboard for update\n"); + return; + } + + source = wl_data_device_manager_create_data_source(process_wayland.wl_data_device_manager); + if (!source) + { + ERR("failed to create data source\n"); + return; + } + + while ((clipboard_format = NtUserEnumClipboardFormats(clipboard_format))) + { + if (clipboard_format == CF_UNICODETEXT) + wl_data_source_offer(source, "text/plain;charset=utf-8"); + } + + wl_data_source_add_listener(source, &data_source_listener, data_device); + pthread_mutex_lock(&data_device->mutex); + if (data_device->wl_data_device) + wl_data_device_set_selection(data_device->wl_data_device, source, enter_serial); + /* Destroy any previous source only after setting the new source, to + * avoid spurious 'selection(nil)' events. */ + if (data_device->wl_data_source) + wl_data_source_destroy(data_device->wl_data_source); + data_device->wl_data_source = source; + pthread_mutex_unlock(&data_device->mutex); + + NtUserCloseClipboard(); +} + +LRESULT WAYLAND_ClipboardWindowProc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) +{ + switch (msg) + { + case WM_NCCREATE: + NtUserAddClipboardFormatListener(hwnd); + desktop_clipboard_hwnd = hwnd; + return TRUE; + case WM_CLIPBOARDUPDATE: + send_message_timeout(NtUserGetForegroundWindow(), + WM_WAYLAND_CLIPBOARD_UPDATE, 0, 0, + SMTO_ABORTIFHUNG, 5000, NULL); + break; + } + + return NtUserMessageCall(hwnd, msg, wparam, lparam, NULL, + NtUserDefWindowProc, FALSE); } diff --git a/dlls/winewayland.drv/wayland_keyboard.c b/dlls/winewayland.drv/wayland_keyboard.c index 8f7b6dce30f..b445ebc0bd5 100644 --- a/dlls/winewayland.drv/wayland_keyboard.c +++ b/dlls/winewayland.drv/wayland_keyboard.c @@ -754,6 +754,7 @@ static void keyboard_handle_enter(void *private, struct wl_keyboard *wl_keyboard
pthread_mutex_lock(&keyboard->mutex); keyboard->focused_hwnd = hwnd; + keyboard->enter_serial = serial; pthread_mutex_unlock(&keyboard->mutex);
NtUserPostMessage(keyboard->focused_hwnd, WM_INPUTLANGCHANGEREQUEST, 0 /*FIXME*/, @@ -789,7 +790,10 @@ static void keyboard_handle_leave(void *data, struct wl_keyboard *wl_keyboard,
pthread_mutex_lock(&keyboard->mutex); if (keyboard->focused_hwnd == hwnd) + { keyboard->focused_hwnd = NULL; + keyboard->enter_serial = 0; + } pthread_mutex_unlock(&keyboard->mutex);
/* The spec for the leave event tells us to treat all keys as released, diff --git a/dlls/winewayland.drv/wayland_surface.c b/dlls/winewayland.drv/wayland_surface.c index 2178f5431cb..448b0ae68ba 100644 --- a/dlls/winewayland.drv/wayland_surface.c +++ b/dlls/winewayland.drv/wayland_surface.c @@ -201,7 +201,10 @@ void wayland_surface_destroy(struct wayland_surface *surface)
pthread_mutex_lock(&process_wayland.keyboard.mutex); if (process_wayland.keyboard.focused_hwnd == surface->hwnd) + { process_wayland.keyboard.focused_hwnd = NULL; + process_wayland.keyboard.enter_serial = 0; + } pthread_mutex_unlock(&process_wayland.keyboard.mutex);
wayland_surface_clear_role(surface); diff --git a/dlls/winewayland.drv/waylanddrv.h b/dlls/winewayland.drv/waylanddrv.h index 51a5ddce2f5..3519c6dbd45 100644 --- a/dlls/winewayland.drv/waylanddrv.h +++ b/dlls/winewayland.drv/waylanddrv.h @@ -63,6 +63,7 @@ enum wayland_window_message WM_WAYLAND_INIT_DISPLAY_DEVICES = WM_WINE_FIRST_DRIVER_MSG, WM_WAYLAND_CONFIGURE, WM_WAYLAND_SET_FOREGROUND, + WM_WAYLAND_CLIPBOARD_UPDATE, };
enum wayland_surface_config_state @@ -86,6 +87,7 @@ struct wayland_keyboard struct xkb_context *xkb_context; struct xkb_state *xkb_state; HWND focused_hwnd; + uint32_t enter_serial; pthread_mutex_t mutex; };
@@ -121,6 +123,8 @@ struct wayland_seat struct wayland_data_device { struct wl_data_device *wl_data_device; + struct wl_data_source *wl_data_source; + pthread_mutex_t mutex; };
struct wayland @@ -353,6 +357,7 @@ void wayland_pointer_clear_constraint(void);
void wayland_data_device_init(void); void wayland_data_device_deinit(void); +void wayland_data_device_clipboard_update(void);
/********************************************************************** * OpenGL @@ -379,6 +384,17 @@ static inline LRESULT send_message(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lp return NtUserMessageCall(hwnd, msg, wparam, lparam, NULL, NtUserSendMessage, FALSE); }
+static inline LRESULT send_message_timeout(HWND hwnd, UINT msg, WPARAM wparam, + LPARAM lparam, UINT flags, UINT timeout, + PDWORD_PTR res_ptr ) +{ + struct send_message_timeout_params params = { .flags = flags, .timeout = timeout }; + LRESULT res = NtUserMessageCall(hwnd, msg, wparam, lparam, ¶ms, + NtUserSendMessageTimeout, FALSE); + if (res_ptr) *res_ptr = params.result; + return res; +} + RGNDATA *get_region_data(HRGN region);
/********************************************************************** @@ -386,6 +402,7 @@ RGNDATA *get_region_data(HRGN region); */
BOOL WAYLAND_ClipCursor(const RECT *clip, BOOL reset); +LRESULT WAYLAND_ClipboardWindowProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp); LRESULT WAYLAND_DesktopWindowProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp); void WAYLAND_DestroyWindow(HWND hwnd); void WAYLAND_SetCursor(HWND hwnd, HCURSOR hcursor); diff --git a/dlls/winewayland.drv/waylanddrv_main.c b/dlls/winewayland.drv/waylanddrv_main.c index 47c1299dd01..d9f13194e9c 100644 --- a/dlls/winewayland.drv/waylanddrv_main.c +++ b/dlls/winewayland.drv/waylanddrv_main.c @@ -35,6 +35,7 @@ char *process_name = NULL;
static const struct user_driver_funcs waylanddrv_funcs = { + .pClipboardWindowProc = WAYLAND_ClipboardWindowProc, .pClipCursor = WAYLAND_ClipCursor, .pDesktopWindowProc = WAYLAND_DesktopWindowProc, .pDestroyWindow = WAYLAND_DestroyWindow, diff --git a/dlls/winewayland.drv/window.c b/dlls/winewayland.drv/window.c index 67718936b53..9c56eb3dda6 100644 --- a/dlls/winewayland.drv/window.c +++ b/dlls/winewayland.drv/window.c @@ -620,6 +620,10 @@ LRESULT WAYLAND_WindowMessage(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) case WM_WAYLAND_SET_FOREGROUND: NtUserSetForegroundWindow(hwnd); return 0; + case WM_WAYLAND_CLIPBOARD_UPDATE: + if (process_wayland.data_device.wl_data_device) + wayland_data_device_clipboard_update(); + return 0; default: FIXME("got window msg %x hwnd %p wp %lx lp %lx\n", msg, hwnd, (long)wp, lp); return 0;
From: Alexandros Frantzis alexandros.frantzis@collabora.com
Introduce the infrastructure to export various clipboard formats in a table-driven manner, similar to what's used by winex11. --- dlls/winewayland.drv/wayland_data_device.c | 62 ++++++++++++++++++---- 1 file changed, 52 insertions(+), 10 deletions(-)
diff --git a/dlls/winewayland.drv/wayland_data_device.c b/dlls/winewayland.drv/wayland_data_device.c index f41f35c11fc..815c9edbed0 100644 --- a/dlls/winewayland.drv/wayland_data_device.c +++ b/dlls/winewayland.drv/wayland_data_device.c @@ -35,6 +35,13 @@
WINE_DEFAULT_DEBUG_CHANNEL(clipboard);
+struct data_device_format +{ + const char *mime_type; + UINT clipboard_format; + void *(*export)(void *data, size_t size, size_t *ret_size); +}; + static HWND desktop_clipboard_hwnd;
static HWND get_clipboard_hwnd(void) @@ -132,11 +139,43 @@ static void *export_unicode_text(void *data, size_t size, size_t *ret_size) return bytes; }
+/* Order is important. When selecting a mime-type for a clipboard format we + * will choose the first entry that matches the specified clipboard format. */ +static struct data_device_format supported_formats[] = +{ + {"text/plain;charset=utf-8", CF_UNICODETEXT, export_unicode_text}, + {NULL, 0, NULL}, +}; + +static struct data_device_format *data_device_format_for_clipboard_format(UINT clipboard_format) +{ + struct data_device_format *format; + + for (format = supported_formats; format->mime_type; ++format) + { + if (format->clipboard_format == clipboard_format) return format; + } + + return NULL; +} + +static struct data_device_format *data_device_format_for_mime_type(const char *mime) +{ + struct data_device_format *format; + + for (format = supported_formats; format->mime_type; ++format) + { + if (!strcmp(mime, format->mime_type)) return format; + } + + return NULL; +} + /********************************************************************** * wl_data_source handling */
-static void wayland_data_source_export(int32_t fd) +static void wayland_data_source_export(struct data_device_format *format, int fd) { struct get_clipboard_params params = { .data_only = TRUE }; static const size_t buffer_size = 1024; @@ -155,9 +194,9 @@ static void wayland_data_source_export(int32_t fd)
params.data = buffer; params.size = buffer_size; - if (NtUserGetClipboardData(CF_UNICODETEXT, ¶ms)) + if (NtUserGetClipboardData(format->clipboard_format, ¶ms)) { - exported = export_unicode_text(params.data, params.size, &exported_size); + exported = format->export(params.data, params.size, &exported_size); } else if (params.data_size) { @@ -168,15 +207,15 @@ static void wayland_data_source_export(int32_t fd) { buffer = new_buffer; params.data = new_buffer; - if (NtUserGetClipboardData(CF_UNICODETEXT, ¶ms)) - exported = export_unicode_text(params.data, params.size, &exported_size); + if (NtUserGetClipboardData(format->clipboard_format, ¶ms)) + exported = format->export(params.data, params.size, &exported_size); } }
NtUserCloseClipboard(); if (exported) write_all(fd, exported, exported_size);
- free(exported); + if (exported != buffer) free(exported); free(buffer); }
@@ -188,8 +227,10 @@ static void data_source_target(void *data, struct wl_data_source *source, static void data_source_send(void *data, struct wl_data_source *source, const char *mime_type, int32_t fd) { - if (!strcmp(mime_type, "text/plain;charset=utf-8")) - wayland_data_source_export(fd); + struct data_device_format *format = + data_device_format_for_mime_type(mime_type); + + if (format) wayland_data_source_export(format, fd); close(fd); }
@@ -285,8 +326,9 @@ void wayland_data_device_clipboard_update(void)
while ((clipboard_format = NtUserEnumClipboardFormats(clipboard_format))) { - if (clipboard_format == CF_UNICODETEXT) - wl_data_source_offer(source, "text/plain;charset=utf-8"); + struct data_device_format *format = + data_device_format_for_clipboard_format(clipboard_format); + if (format) wl_data_source_offer(source, format->mime_type); }
wl_data_source_add_listener(source, &data_source_listener, data_device);
From: Alexandros Frantzis alexandros.frantzis@collabora.com
Add support for some formats commonly used in applications and which do not require special exporting: RTF, TIFF, PNG, JPEG, GIF, SVG, RIFF, WAV. --- dlls/winewayland.drv/wayland_data_device.c | 37 +++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-)
diff --git a/dlls/winewayland.drv/wayland_data_device.c b/dlls/winewayland.drv/wayland_data_device.c index 815c9edbed0..15570b3fe74 100644 --- a/dlls/winewayland.drv/wayland_data_device.c +++ b/dlls/winewayland.drv/wayland_data_device.c @@ -39,10 +39,16 @@ struct data_device_format { const char *mime_type; UINT clipboard_format; + const WCHAR *register_name; void *(*export)(void *data, size_t size, size_t *ret_size); };
static HWND desktop_clipboard_hwnd; +static const WCHAR rich_text_formatW[] = {'R','i','c','h',' ','T','e','x','t',' ','F','o','r','m','a','t',0}; +static const WCHAR pngW[] = {'P','N','G',0}; +static const WCHAR jfifW[] = {'J','F','I','F',0}; +static const WCHAR gifW[] = {'G','I','F',0}; +static const WCHAR image_svg_xmlW[] = {'i','m','a','g','e','/','s','v','g','+','x','m','l',0};
static HWND get_clipboard_hwnd(void) { @@ -139,11 +145,26 @@ static void *export_unicode_text(void *data, size_t size, size_t *ret_size) return bytes; }
+static void *export_data(void *data, size_t size, size_t *ret_size) +{ + *ret_size = size; + return data; +} + /* Order is important. When selecting a mime-type for a clipboard format we * will choose the first entry that matches the specified clipboard format. */ static struct data_device_format supported_formats[] = { - {"text/plain;charset=utf-8", CF_UNICODETEXT, export_unicode_text}, + {"text/plain;charset=utf-8", CF_UNICODETEXT, NULL, export_unicode_text}, + {"text/rtf", 0, rich_text_formatW, export_data}, + {"image/tiff", CF_TIFF, NULL, export_data}, + {"image/png", 0, pngW, export_data}, + {"image/jpeg", 0, jfifW, export_data}, + {"image/gif", 0, gifW, export_data}, + {"image/svg+xml", 0, image_svg_xmlW, export_data}, + {"application/x-riff", CF_RIFF, NULL, export_data}, + {"audio/wav", CF_WAVE, NULL, export_data}, + {"audio/x-wav", CF_WAVE, NULL, export_data}, {NULL, 0, NULL}, };
@@ -171,6 +192,13 @@ static struct data_device_format *data_device_format_for_mime_type(const char *m return NULL; }
+static ATOM register_clipboard_format(const WCHAR *name) +{ + ATOM atom; + if (NtAddAtom(name, lstrlenW(name) * sizeof(WCHAR), &atom)) return 0; + return atom; +} + /********************************************************************** * wl_data_source handling */ @@ -272,12 +300,19 @@ static const struct wl_data_source_listener data_source_listener = void wayland_data_device_init(void) { struct wayland_data_device *data_device = &process_wayland.data_device; + struct data_device_format *format = supported_formats;
pthread_mutex_lock(&data_device->mutex); data_device->wl_data_device = wl_data_device_manager_get_data_device(process_wayland.wl_data_device_manager, process_wayland.seat.wl_seat); pthread_mutex_unlock(&data_device->mutex); + + for (; format->mime_type; ++format) + { + if (format->clipboard_format == 0) + format->clipboard_format = register_clipboard_format(format->register_name); + } }
void wayland_data_device_deinit(void)
From: Alexandros Frantzis alexandros.frantzis@collabora.com
Under Wayland only the data device for the seat which has the keyboard focus has access to the offered clipboard contents, so we forward the WM_RENDERFORMAT and WM_DESTROYCLIPBOARD messages to the proper window thread (as internal driver window messages). --- dlls/winewayland.drv/wayland_data_device.c | 418 ++++++++++++++++++++- dlls/winewayland.drv/waylanddrv.h | 5 + dlls/winewayland.drv/window.c | 8 + 3 files changed, 417 insertions(+), 14 deletions(-)
diff --git a/dlls/winewayland.drv/wayland_data_device.c b/dlls/winewayland.drv/wayland_data_device.c index 15570b3fe74..b0043c57825 100644 --- a/dlls/winewayland.drv/wayland_data_device.c +++ b/dlls/winewayland.drv/wayland_data_device.c @@ -35,12 +35,21 @@
WINE_DEFAULT_DEBUG_CHANNEL(clipboard);
+#define WINEWAYLAND_TAG_MIME_TYPE "application/x.winewayland.tag" + struct data_device_format { const char *mime_type; UINT clipboard_format; const WCHAR *register_name; void *(*export)(void *data, size_t size, size_t *ret_size); + void *(*import)(void *data, size_t size, size_t *ret_size); +}; + +struct wayland_data_offer +{ + struct wl_data_offer *wl_data_offer; + struct wl_array types; };
static HWND desktop_clipboard_hwnd; @@ -69,7 +78,7 @@ static HWND get_clipboard_hwnd(void)
static int poll_until(int fd, int events, ULONG end_time) { - struct pollfd pfd = { .fd = fd, .events = events }; + struct pollfd pfd = { .fd = fd, .events = events & ~POLLHUP }; ULONG now = NtGetTickCount(); int ret = 0;
@@ -127,6 +136,64 @@ static void write_all(int fd, const void *buf, size_t count) } }
+static void *read_all(int fd, size_t *size_out) +{ + static const ULONG receive_timeout = 500; + size_t buffer_size = 4096; + int total = 0; + unsigned char *buffer; + int nread; + ULONG end_time; + + if (!(buffer = malloc(buffer_size))) + { + ERR("failed to allocate read buffer\n"); + goto out; + } + + end_time = NtGetTickCount() + receive_timeout; + do + { + if (poll_until(fd, POLLIN | POLLHUP, end_time) <= 0) goto out; + + nread = read(fd, buffer + total, buffer_size - total); + if (nread == -1 && errno != EINTR) + { + TRACE("failed to read from fd (errno: %d)\n", errno); + total = 0; + goto out; + } + else if (nread > 0) + { + total += nread; + if (total == buffer_size) + { + unsigned char *new_buffer; + buffer_size *= 2; + new_buffer = realloc(buffer, buffer_size); + if (!new_buffer) + { + ERR("failed to reallocate read buffer\n"); + total = 0; + goto out; + } + buffer = new_buffer; + } + } + } while (nread > 0); + + TRACE("read %d bytes\n", total); + +out: + if (total == 0 && buffer != NULL) + { + free(buffer); + buffer = NULL; + } + *size_out = total; + return buffer; +} + static void *export_unicode_text(void *data, size_t size, size_t *ret_size) { DWORD byte_count; @@ -151,30 +218,66 @@ static void *export_data(void *data, size_t size, size_t *ret_size) return data; }
+static void *import_unicode_text(void *data, size_t size, size_t *ret_size) +{ + DWORD wsize; + WCHAR *ret; + + RtlUTF8ToUnicodeN(NULL, 0, &wsize, data, size); + if (!(ret = malloc(wsize + sizeof(WCHAR)))) return NULL; + RtlUTF8ToUnicodeN(ret, wsize, &wsize, data, size); + ret[wsize / sizeof(WCHAR)] = 0; + + *ret_size = wsize + sizeof(WCHAR); + + return ret; +} + +static void *import_data(void *data, size_t size, size_t *ret_size) +{ + *ret_size = size; + return data; +} + /* Order is important. When selecting a mime-type for a clipboard format we * will choose the first entry that matches the specified clipboard format. */ static struct data_device_format supported_formats[] = { - {"text/plain;charset=utf-8", CF_UNICODETEXT, NULL, export_unicode_text}, - {"text/rtf", 0, rich_text_formatW, export_data}, - {"image/tiff", CF_TIFF, NULL, export_data}, - {"image/png", 0, pngW, export_data}, - {"image/jpeg", 0, jfifW, export_data}, - {"image/gif", 0, gifW, export_data}, - {"image/svg+xml", 0, image_svg_xmlW, export_data}, - {"application/x-riff", CF_RIFF, NULL, export_data}, - {"audio/wav", CF_WAVE, NULL, export_data}, - {"audio/x-wav", CF_WAVE, NULL, export_data}, + {"text/plain;charset=utf-8", CF_UNICODETEXT, NULL, export_unicode_text, import_unicode_text}, + {"text/rtf", 0, rich_text_formatW, export_data, import_data}, + {"image/tiff", CF_TIFF, NULL, export_data, import_data}, + {"image/png", 0, pngW, export_data, import_data}, + {"image/jpeg", 0, jfifW, export_data, import_data}, + {"image/gif", 0, gifW, export_data, import_data}, + {"image/svg+xml", 0, image_svg_xmlW, export_data, import_data}, + {"application/x-riff", CF_RIFF, NULL, export_data, import_data}, + {"audio/wav", CF_WAVE, NULL, export_data, import_data}, + {"audio/x-wav", CF_WAVE, NULL, export_data, import_data}, {NULL, 0, NULL}, };
-static struct data_device_format *data_device_format_for_clipboard_format(UINT clipboard_format) +static BOOL string_array_contains(struct wl_array *array, const char *str) +{ + char **p; + + wl_array_for_each(p, array) + if (!strcmp(*p, str)) return TRUE; + + return FALSE; +} + +static struct data_device_format *data_device_format_for_clipboard_format(UINT clipboard_format, + struct wl_array *types) { struct data_device_format *format;
for (format = supported_formats; format->mime_type; ++format) { - if (format->clipboard_format == clipboard_format) return format; + if (format->clipboard_format == clipboard_format && + (!types || string_array_contains(types, format->mime_type))) + { + return format; + } }
return NULL; @@ -297,6 +400,238 @@ static const struct wl_data_source_listener data_source_listener = data_source_action, };
+/********************************************************************** + * wl_data_offer handling + */ + +static void data_offer_offer(void *data, struct wl_data_offer *wl_data_offer, + const char *type) +{ + struct wayland_data_offer *data_offer = data; + const char *type_copy; + const char **p; + + if ((type_copy = strdup(type)) && + (p = wl_array_add(&data_offer->types, sizeof *p))) + { + *p = type_copy; + } +} + +static void data_offer_source_actions(void *data, + struct wl_data_offer *wl_data_offer, + uint32_t source_actions) +{ +} + +static void data_offer_action(void *data, struct wl_data_offer *wl_data_offer, + uint32_t dnd_action) +{ +} + +static const struct wl_data_offer_listener data_offer_listener = +{ + data_offer_offer, + data_offer_source_actions, + data_offer_action +}; + +static void wayland_data_offer_create(struct wl_data_offer *wl_data_offer) +{ + struct wayland_data_offer *data_offer; + + if (!(data_offer = calloc(1, sizeof(*data_offer)))) + { + ERR("Failed to allocate memory for data offer\n"); + return; + } + + data_offer->wl_data_offer = wl_data_offer; + wl_array_init(&data_offer->types); + wl_data_offer_add_listener(data_offer->wl_data_offer, + &data_offer_listener, data_offer); +} + +static void wayland_data_offer_destroy(struct wayland_data_offer *data_offer) +{ + char **p; + + wl_data_offer_destroy(data_offer->wl_data_offer); + wl_array_for_each(p, &data_offer->types) + free(*p); + wl_array_release(&data_offer->types); + free(data_offer); +} + +static int wayland_data_offer_get_import_fd(struct wayland_data_offer *data_offer, + const char *mime_type) +{ + int data_pipe[2]; + +#if HAVE_PIPE2 + if (pipe2(data_pipe, O_CLOEXEC) == -1) +#endif + { + if (pipe(data_pipe) == -1) + { + ERR("failed to create clipboard data pipe\n"); + return -1; + } + fcntl(data_pipe[0], F_SETFD, FD_CLOEXEC); + fcntl(data_pipe[1], F_SETFD, FD_CLOEXEC); + } + + wl_data_offer_receive(data_offer->wl_data_offer, mime_type, data_pipe[1]); + close(data_pipe[1]); + + /* Flush to ensure our receive request reaches the server. */ + wl_display_flush(process_wayland.wl_display); + + return data_pipe[0]; +} + +static void *import_format(int fd, struct data_device_format *format, size_t *ret_size) +{ + size_t size; + void *data, *ret; + + if (!(data = read_all(fd, &size))) return NULL; + ret = format->import(data, size, ret_size); + if (ret != data) free(data); + return ret; +} + +/********************************************************************** + * wl_data_device handling + */ + +static void wayland_data_device_destroy_clipboard_data_offer(struct wayland_data_device *data_device) +{ + if (data_device->clipboard_wl_data_offer) + { + struct wayland_data_offer *data_offer = + wl_data_offer_get_user_data(data_device->clipboard_wl_data_offer); + if (data_offer) wayland_data_offer_destroy(data_offer); + data_device->clipboard_wl_data_offer = NULL; + } +} + +static void data_device_data_offer(void *data, + struct wl_data_device *wl_data_device, + struct wl_data_offer *wl_data_offer) +{ + wayland_data_offer_create(wl_data_offer); +} + +static void data_device_enter(void *data, struct wl_data_device *wl_data_device, + uint32_t serial, struct wl_surface *wl_surface, + wl_fixed_t x_w, wl_fixed_t y_w, + struct wl_data_offer *wl_data_offer) +{ +} + +static void data_device_leave(void *data, struct wl_data_device *wl_data_device) +{ +} + +static void data_device_motion(void *data, struct wl_data_device *wl_data_device, + uint32_t time, wl_fixed_t x_w, wl_fixed_t y_w) +{ +} + +static void data_device_drop(void *data, struct wl_data_device *wl_data_device) +{ +} + +static void data_device_selection(void *data, + struct wl_data_device *wl_data_device, + struct wl_data_offer *wl_data_offer) +{ + struct wayland_data_device *data_device = data; + struct wayland_data_offer *data_offer = NULL; + HWND clipboard_hwnd = get_clipboard_hwnd(); + char **p; + + if (!clipboard_hwnd) return; + + if (!wl_data_offer || + !(data_offer = wl_data_offer_get_user_data(wl_data_offer))) + { + if (NtUserGetClipboardOwner() == clipboard_hwnd) + { + TRACE("null offer, clearing clipboard owned by native app\n"); + NtUserOpenClipboard(clipboard_hwnd, 0); + NtUserEmptyClipboard(); + NtUserCloseClipboard(); + } + else + { + /* We can't tell if the null data offer is triggered by a native + * app or another win32 app shutting down while acting as the Wine + * clipboard data source, so play it safe by assuming the second + * case and becoming the new Wine clipboard data source. */ + TRACE("null offer, updating clipboard owned by win32 app\n"); + wayland_data_device_clipboard_update(); + } + goto done; + } + + /* If this offer contains the special winewayland tag mime-type, it was sent + * by a winewayland process to notify external wayland clients about a Wine + * clipboard update. */ + wl_array_for_each(p, &data_offer->types) + { + if (!strcmp(*p, WINEWAYLAND_TAG_MIME_TYPE)) + { + TRACE("offer sent by winewayland, ignoring\n"); + wayland_data_offer_destroy(data_offer); + data_offer = NULL; + goto done; + } + } + + if (!NtUserOpenClipboard(clipboard_hwnd, 0)) + { + TRACE("failed to open clipboard for selection\n"); + wayland_data_offer_destroy(data_offer); + data_offer = NULL; + goto done; + } + + NtUserEmptyClipboard(); + + /* For each mime type, mark that we have available clipboard data. */ + wl_array_for_each(p, &data_offer->types) + { + struct data_device_format *format = data_device_format_for_mime_type(*p); + if (format) + { + struct set_clipboard_params params = {0}; + TRACE("Available clipboard format for %s => %u\n", + *p, format->clipboard_format); + NtUserSetClipboardData(format->clipboard_format, 0, ¶ms); + } + } + + NtUserCloseClipboard(); + +done: + pthread_mutex_lock(&data_device->mutex); + wayland_data_device_destroy_clipboard_data_offer(data_device); + if (data_offer) data_device->clipboard_wl_data_offer = wl_data_offer; + pthread_mutex_unlock(&data_device->mutex); +} + +static const struct wl_data_device_listener data_device_listener = +{ + data_device_data_offer, + data_device_enter, + data_device_leave, + data_device_motion, + data_device_drop, + data_device_selection, +}; + void wayland_data_device_init(void) { struct wayland_data_device *data_device = &process_wayland.data_device; @@ -306,6 +641,8 @@ void wayland_data_device_init(void) data_device->wl_data_device = wl_data_device_manager_get_data_device(process_wayland.wl_data_device_manager, process_wayland.seat.wl_seat); + wl_data_device_add_listener(data_device->wl_data_device, &data_device_listener, + data_device); pthread_mutex_unlock(&data_device->mutex);
for (; format->mime_type; ++format) @@ -362,10 +699,14 @@ void wayland_data_device_clipboard_update(void) while ((clipboard_format = NtUserEnumClipboardFormats(clipboard_format))) { struct data_device_format *format = - data_device_format_for_clipboard_format(clipboard_format); + data_device_format_for_clipboard_format(clipboard_format, NULL); if (format) wl_data_source_offer(source, format->mime_type); }
+ /* Mark this data source with our special mime type, so we can detect it's + * coming from us. */ + wl_data_source_offer(source, WINEWAYLAND_TAG_MIME_TYPE); + wl_data_source_add_listener(source, &data_source_listener, data_device); pthread_mutex_lock(&data_device->mutex); if (data_device->wl_data_device) @@ -380,6 +721,44 @@ void wayland_data_device_clipboard_update(void) NtUserCloseClipboard(); }
+void wayland_data_device_render_format(UINT clipboard_format) +{ + struct wayland_data_device *data_device = &process_wayland.data_device; + struct wayland_data_offer *data_offer; + struct data_device_format *format; + int import_fd = -1; + + pthread_mutex_lock(&data_device->mutex); + if (data_device->clipboard_wl_data_offer && + (data_offer = wl_data_offer_get_user_data(data_device->clipboard_wl_data_offer)) && + (format = data_device_format_for_clipboard_format(clipboard_format, + &data_offer->types))) + { + import_fd = wayland_data_offer_get_import_fd(data_offer, format->mime_type); + } + pthread_mutex_unlock(&data_device->mutex); + + if (import_fd >= 0) + { + struct set_clipboard_params params = {0}; + if ((params.data = import_format(import_fd, format, ¶ms.size))) + { + NtUserSetClipboardData(format->clipboard_format, 0, ¶ms); + free(params.data); + } + close(import_fd); + } +} + +void wayland_data_device_destroy_clipboard(void) +{ + struct wayland_data_device *data_device = &process_wayland.data_device; + + pthread_mutex_lock(&data_device->mutex); + wayland_data_device_destroy_clipboard_data_offer(data_device); + pthread_mutex_unlock(&data_device->mutex); +} + LRESULT WAYLAND_ClipboardWindowProc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) { switch (msg) @@ -389,10 +768,21 @@ LRESULT WAYLAND_ClipboardWindowProc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM l desktop_clipboard_hwnd = hwnd; return TRUE; case WM_CLIPBOARDUPDATE: + if (NtUserGetClipboardOwner() == desktop_clipboard_hwnd) break; send_message_timeout(NtUserGetForegroundWindow(), WM_WAYLAND_CLIPBOARD_UPDATE, 0, 0, SMTO_ABORTIFHUNG, 5000, NULL); break; + case WM_RENDERFORMAT: + send_message_timeout(NtUserGetForegroundWindow(), + WM_WAYLAND_RENDER_FORMAT, wparam, 0, + SMTO_ABORTIFHUNG, 5000, NULL); + break; + case WM_DESTROYCLIPBOARD: + send_message_timeout(NtUserGetForegroundWindow(), + WM_WAYLAND_DESTROY_CLIPBOARD, wparam, 0, + SMTO_ABORTIFHUNG, 5000, NULL); + break; }
return NtUserMessageCall(hwnd, msg, wparam, lparam, NULL, diff --git a/dlls/winewayland.drv/waylanddrv.h b/dlls/winewayland.drv/waylanddrv.h index 3519c6dbd45..89403641468 100644 --- a/dlls/winewayland.drv/waylanddrv.h +++ b/dlls/winewayland.drv/waylanddrv.h @@ -64,6 +64,8 @@ enum wayland_window_message WM_WAYLAND_CONFIGURE, WM_WAYLAND_SET_FOREGROUND, WM_WAYLAND_CLIPBOARD_UPDATE, + WM_WAYLAND_RENDER_FORMAT, + WM_WAYLAND_DESTROY_CLIPBOARD, };
enum wayland_surface_config_state @@ -124,6 +126,7 @@ struct wayland_data_device { struct wl_data_device *wl_data_device; struct wl_data_source *wl_data_source; + struct wl_data_offer *clipboard_wl_data_offer; pthread_mutex_t mutex; };
@@ -358,6 +361,8 @@ void wayland_pointer_clear_constraint(void); void wayland_data_device_init(void); void wayland_data_device_deinit(void); void wayland_data_device_clipboard_update(void); +void wayland_data_device_render_format(UINT clipboard_format); +void wayland_data_device_destroy_clipboard(void);
/********************************************************************** * OpenGL diff --git a/dlls/winewayland.drv/window.c b/dlls/winewayland.drv/window.c index 9c56eb3dda6..8fd0dcf1156 100644 --- a/dlls/winewayland.drv/window.c +++ b/dlls/winewayland.drv/window.c @@ -624,6 +624,14 @@ LRESULT WAYLAND_WindowMessage(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) if (process_wayland.data_device.wl_data_device) wayland_data_device_clipboard_update(); return 0; + case WM_WAYLAND_RENDER_FORMAT: + if (process_wayland.data_device.wl_data_device) + wayland_data_device_render_format(wp); + return 0; + case WM_WAYLAND_DESTROY_CLIPBOARD: + if (process_wayland.data_device.wl_data_device) + wayland_data_device_destroy_clipboard(); + return 0; default: FIXME("got window msg %x hwnd %p wp %lx lp %lx\n", msg, hwnd, (long)wp, lp); return 0;
From: Alexandros Frantzis alexandros.frantzis@collabora.com
--- dlls/winewayland.drv/wayland_data_device.c | 45 +++++++++++++++++++--- 1 file changed, 39 insertions(+), 6 deletions(-)
diff --git a/dlls/winewayland.drv/wayland_data_device.c b/dlls/winewayland.drv/wayland_data_device.c index b0043c57825..097c3d8c221 100644 --- a/dlls/winewayland.drv/wayland_data_device.c +++ b/dlls/winewayland.drv/wayland_data_device.c @@ -76,6 +76,35 @@ static HWND get_clipboard_hwnd(void) return desktop_clipboard_hwnd; }
+/* Normalize the MIME type string by skipping inconsequential characters, + * such as spaces and double quotes, and convert to lower case. */ +static const char *normalize_mime_type(const char *mime_type) +{ + char *new_mime_type; + const char *cur_read; + char *cur_write; + size_t new_mime_len = 0; + + for (cur_read = mime_type; *cur_read != '\0'; ++cur_read) + { + if (*cur_read != ' ' && *cur_read != '"') + new_mime_len++; + } + + new_mime_type = malloc(new_mime_len + 1); + if (!new_mime_type) return NULL; + + for (cur_read = mime_type, cur_write = new_mime_type; *cur_read != '\0'; ++cur_read) + { + if (*cur_read != ' ' && *cur_read != '"') + *cur_write++ = tolower(*cur_read); + } + + *cur_write = '\0'; + + return new_mime_type; +} + static int poll_until(int fd, int events, ULONG end_time) { struct pollfd pfd = { .fd = fd, .events = events & ~POLLHUP }; @@ -358,10 +387,14 @@ static void data_source_target(void *data, struct wl_data_source *source, static void data_source_send(void *data, struct wl_data_source *source, const char *mime_type, int32_t fd) { - struct data_device_format *format = - data_device_format_for_mime_type(mime_type); + struct data_device_format *format; + const char *normalized;
- if (format) wayland_data_source_export(format, fd); + if ((normalized = normalize_mime_type(mime_type)) && + (format = data_device_format_for_mime_type(normalized))) + { + wayland_data_source_export(format, fd); + } close(fd); }
@@ -408,13 +441,13 @@ static void data_offer_offer(void *data, struct wl_data_offer *wl_data_offer, const char *type) { struct wayland_data_offer *data_offer = data; - const char *type_copy; + const char *normalized; const char **p;
- if ((type_copy = strdup(type)) && + if ((normalized = normalize_mime_type(type)) && (p = wl_array_add(&data_offer->types, sizeof *p))) { - *p = type_copy; + *p = normalized; } }
I think the special mime type should be generated with a random number per wineserver instance so that clipboard transfer between applications in separate wineprefixes will work.
On Tue Feb 4 03:05:37 2025 +0000, Attila Fidan wrote:
I think the special mime type should be generated with a random number per wineserver instance so that clipboard transfer between applications in separate wineprefixes will work.
In what situation is this a problem? Wouldn't the X11 driver as it currently exists behave the same way?
On Tue Feb 4 21:40:31 2025 +0000, Esme Povirk wrote:
In what situation is this a problem? Wouldn't the X11 driver as it currently exists behave the same way?
I am not sure about the intended behaviour of the x11 driver or have experience writing for the x11 clipboard, but interprefix copy/paste works fine on my system (using xwayland). It doesn't work between two patched winewayland prefixes because the receiving prefix will ignore the sending prefix's offer due to the mime tag.
Rémi Bernon (@rbernon) commented about dlls/winewayland.drv/wayland_data_device.c:
- end_time = NtGetTickCount() + timeout;
- while (nwritten < count)
- {
if (poll_until(fd, POLLOUT, end_time) <= 0) break;
ret = write(fd, (const char*)buf + nwritten, count - nwritten);
if (ret == -1 && errno != EINTR && errno != EAGAIN && errno != EWOULDBLOCK)
{
break;
}
else if (ret > 0)
{
nwritten += ret;
}
- }
Why do we need to do all this already (timeouts)? I think it would be much better if it could be avoided entirely.
Rémi Bernon (@rbernon) commented about dlls/winewayland.drv/wayland_data_device.c:
- end_time = NtGetTickCount() + receive_timeout;
- do
- {
if (poll_until(fd, POLLIN | POLLHUP, end_time) <= 0) goto out;
nread = read(fd, buffer + total, buffer_size - total);
if (nread == -1 && errno != EINTR)
{
TRACE("failed to read from fd (errno: %d)\n", errno);
total = 0;
goto out;
}
else if (nread > 0)
{
total += nread;
Same question here.
I'm not very familiar with the clipboard code, might take a moment before I get an idea of how it works.
On Wed Feb 5 10:45:49 2025 +0000, Rémi Bernon wrote:
Why do we need to do all this already (ie: timeouts and non-blocking I/O)? I think it would be much better if it could be avoided entirely.
The Wayland wl_data_device interface involves direct client-to-client data transfers. The file descriptor we write the clipboard data to (sent to us in the `wl_data_source.send` event) is typically the write end of a pipe, whose read end is under the direct control of the Wayland client requesting the data. If that other client is misbehaving, it can potentially cause our write to block indefinitely, e.g., if we need to transfer enough data to fill up the pipe kernel buffer while the other client is not consuming anything. The timeout is a simple mitigation for this scenario.
Another approach used by many Wayland clients is to use some kind of asynchronous write/read which will notify when finished (typically the fds are `poll`ed and read/written in the context of the main event loop). However, this is a lot of extra complexity for likely diminishing returns (IMO) for our case. See also note (3) in the MR description.
On Wed Feb 5 10:44:04 2025 +0000, Rémi Bernon wrote:
Same question here.
The answer is similar to above, with the the reader/writer roles reversed. A misbehaving client that's not sending anything (and not closing its end of the pipe) will cause our read to block indefinitely.
On Wed Feb 5 12:47:07 2025 +0000, Alexandros Frantzis wrote:
The Wayland wl_data_device interface involves direct client-to-client data transfers. The file descriptor we write the clipboard data to (sent to us in the `wl_data_source.send` event) is typically the write end of a pipe, whose read end is under the direct control of the Wayland client requesting the data. If that other client is misbehaving, it can potentially cause our write to block indefinitely, e.g., if we need to transfer enough data to fill up the pipe kernel buffer while the other client is not consuming anything. The timeout is a simple mitigation for this scenario. Another approach used by many Wayland clients is to use some kind of asynchronous write/read which will notify when finished (typically the fds are `poll`ed and read/written in the context of the main event loop). However, this is a lot of extra complexity for likely diminishing returns (IMO) for our case. See also note (3) in the MR description.
Are every Wayland client out there really expected to guard against any possible other badly implemented client out there? This sounds like a truly awful design to me.
IMO bogus applications need to be fixed, not force upon every other the burden of guarding against their bugs.
On Wed Feb 5 13:01:23 2025 +0000, Rémi Bernon wrote:
Are every Wayland client out there really expected to guard against any possible other badly implemented client out there? This sounds like a truly awful design to me. IMO bogus applications need to be fixed, not force upon every other the burden of guarding against their bugs.
Is it practical to expect every client to behave correctly though? I.e. if something is bugged, you don't want your client to be dragged down becasue you expected every other client to be perfect? So defensive measures make sense since bugs always can happen.
On Wed Feb 5 18:49:50 2025 +0000, Shmerl wrote:
Is it practical to expect every client to behave correctly though? I.e. if something is bugged, you don't want your client to be dragged down becasue you expected every other client to be perfect? So defensive measures make sense since bugs always can happen. I.e. it's more reasonable to expect compositor to be compliant / perfect (since there are more limited number of them and they are relatively well known), but I don't think you can make such assumption about some random clients.
Which is why I think a properly designed desktop communication protocol should do something better than just connect clients together through basic IPC to then let them figure out how to communicate, putting the risk and burden of handling badly implemented clients on each one of them.
Every thing I discover with these Wayland review feels backwards, all the freedom we need to properly implement Win32 API is unavailable, and every time I expect something to be slightly better abstracted than before to hide or remove the complexity and implementation burden out of clients, it is not.
I'll look into this just to make sure it works, and if we have to do this kind of thing so be it, but honestly, if this is the future of Linux desktop, it is very depressing to say the least.
Hello, as an user I'd like to report an issue with desktop mode.
After applying this MR on top of wine-10.0, I can successfully copy-paste in notepad.exe with winewayland. However when I tried desktop mode, copy-paste does not work. In X11 the clipboard works fine.
Both `DISPLAY= WINEARCH=win64 wine notepad` and `WINEARCH=win64 wine explorer /desktop=test,3840x2160 notepad` works, but `DISPLAY= WINEARCH=win64 wine explorer /desktop=test,3840x2160 notepad` does not synchronize clipboard
On Fri Feb 7 08:24:39 2025 +0000, littlewu2508 wrote:
Hello, as an user I'd like to report an issue with desktop mode. After applying this MR on top of wine-10.0, I can successfully copy-paste in notepad.exe with winewayland. However when I tried desktop mode, copy-paste does not work. In X11 the clipboard works fine. Both `DISPLAY= WINEARCH=win64 wine notepad` and `WINEARCH=win64 wine explorer /desktop=test,3840x2160 notepad` works, but `DISPLAY= WINEARCH=win64 wine explorer /desktop=test,3840x2160 notepad` does not synchronize clipboard
In general desktop mode is not currently supported by the wayland driver (e.g., no desktop window is actually drawn), so I didn't consider the multi-desktop scenario in this MR.
In particular, the approach used in this MR doesn't work for desktop mode because the clipboard manager window is created in the default desktop, and it can't find (or be found by) and communicate with windows created in other desktops (e.g., the actual foreground window). Some other approach is required if we want to support the multi-desktop use case. I am open to suggestions...
Under Wayland only applications that have the keyboard focus can interact with the clipboard (a.k.a. `wl_data_device`). Such constraints are not a natural fit for Wine's current clipboard infrastructure, which uses a separate thread and window in the desktop process to act as the win32-side clipboard proxy for all native windows.
It seems to me that this is mostly specific for winex11 and winemac use cases, but I don't think it does anything if the driver doesn't implement the ClipboardWindowProc callback.
I think you should be able to use a different method for winewayland, for instance use the UpdateClipboard callbacks directly, and if you need to be notified when clipboard changes, wineserver could post internal wine messages to the foreground window too. Note that the foreground window is not very likely going to be the one with focus as it's not kept in sync.
Some more thoughts...
This MR is based on the standard `wl_data_device`, which comes with all the constraints and complications discussed in the description (needs app focus etc). There are some other approaches which I would like to present for consideration:
1. (Current MR) Use wl_data_device trying to fit into current infrastructure. - + Standard clipboard protocol, universal - - Complexity due to interactions between clipboard manager window and foreground window - - Doesn't work with multiple desktops (see @littlewu2508 comment above)
2. Use wlr-data-control (and it's recent successor [https://gitlab.freedesktop.org/wayland/wayland-protocols/-/merge_requests/33...)): - + Unconstrained clipboard access, reduces winewayland implementation complexity - - Although it's widely supported (https://wayland.app/protocols/wlr-data-control-unstable-v1#compositor-suppor...) it's not universally supported. Notably mutter is not expected to support it any time soon. - - It's considered "privileged", uncertain whether compositors may try to limit which applications can access it (unlikely though)
3. Use wl_data_device with temporary "invisible" windows to get the focus - - Seems fragile and requires some method (compositor specific protocols) to ensure we get the focus
On Fri Feb 7 09:42:37 2025 +0000, Alexandros Frantzis wrote:
Some more thoughts... This MR is based on the standard `wl_data_device`, which comes with all the constraints and complications discussed in the description (needs app focus etc). There are some other approaches which I would like to present for consideration:
- (Current MR) Use wl_data_device trying to fit into current infrastructure.
- + Standard clipboard protocol, universal
- - Complexity due to interactions between clipboard manager window
and foreground window
- - Doesn't work with multiple desktops (see @littlewu2508 comment above)
- Use wlr-data-control (and its recent successor [https://gitlab.freedesktop.org/wayland/wayland-protocols/-/merge_requests/33...)):
- + Unconstrained clipboard access, reduces winewayland
implementation complexity
- - Although it's widely supported
(https://wayland.app/protocols/wlr-data-control-unstable-v1#compositor-suppor...) it's not universally supported. Notably mutter is not expected to support it any time soon.
- - It's considered "privileged", uncertain whether compositors may
try to limit which applications can access it (unlikely though) 3. Use wl_data_device with temporary "invisible" windows to get the focus
- - Seems fragile and requires some method (compositor specific
protocols) to ensure we get the focus
Thanks for clarifying the options. I don't think 3) is right so let's only consider 1) or 2).
To me, it looks like the purpose of protocol 1) is to prevent background windows/processes from accessing the clipboard. In Windows IIUC every process can. Both seem to be in contradiction, and I feel that implementing the protocol in a way where we would allow every Wine process to access the clipboard through the foreground window violates the spirit of the protocol.
Not that I mind too much, but if there's an alternative that's explicitly meant to allow privileged access to the clipboard, and if that's what Windows does, then it looks to be a better match. And if it's easier to implement, then even better.
Still, if we think that having a wider support is better, we could still decide to implement 1) but I'm not sure we should work around its limitation, and we should also only allow the foreground window to access the clipboard in the way it's integrated with the Windows side.
In a general sense IMO at some point compositors will have to adjust their design to their users, and decide to implement things that are required to support some applications. There's only so much we can do on our side and although we're happy to add Wayland support, its limitations make it difficult to have good integration with Wine.
I was even thinking that, instead of implementing protocols that are inherently incompatible with Win32, the best way forward would be to design and expose our own set of protocols that would fit exactly the Win32 model, and let compositors decide if they want to support Wine or not.
On Fri Feb 7 09:44:18 2025 +0000, Rémi Bernon wrote:
Thanks for clarifying the options. I don't think 3) is right so let's only consider 1) or 2). To me, it looks like the purpose of protocol 1) is to prevent background windows/processes from accessing the clipboard. In Windows IIUC every process can. Both seem to be in contradiction, and I feel that implementing the protocol in a way where we would allow every Wine process to access the clipboard through the foreground window violates the spirit of the protocol. Not that I mind too much, but if there's an alternative that's explicitly meant to allow privileged access to the clipboard, and if that's what Windows does, then it looks to be a better match. And if it's easier to implement, then even better. Still, if we think that having a wider support is better, we could still decide to implement 1) but I'm not sure we should work around its limitation, and we should also only allow the foreground window to access the clipboard in the way it's integrated with the Windows side. In a general sense IMO at some point compositors will have to adjust their design to their users, and decide to implement things that are required to support some applications. There's only so much we can do on our side and although we're happy to add Wayland support, its limitations make it difficult to have good integration with Wine. I was even thinking that, instead of implementing protocols that are inherently incompatible with Win32, the best way forward would be to design and expose our own set of protocols that would fit exactly the Win32 model, and let compositors decide if they want to support Wine or not.
Thanks @rbernon. The more I consider `{wlr,ext}-data-control` the more enticing it looks for our use case. So an implementation based primarily on it, plus a *simple* fallback to `wl_data_device` seems like a good way forward.
On Fri Feb 7 10:39:31 2025 +0000, Alexandros Frantzis wrote:
Thanks @rbernon. The more I consider `{wlr,ext}-data-control` the more enticing it looks for our use case. So an implementation based primarily on it, plus a *simple* fallback to `wl_data_device` seems like a good way forward.
Here is the alternative proposal based on the data-control protocols: https://gitlab.winehq.org/wine/wine/-/merge_requests/7336
On Fri Feb 14 11:45:06 2025 +0000, Alexandros Frantzis wrote:
Here is the alternative proposal based on the data-control protocols: https://gitlab.winehq.org/wine/wine/-/merge_requests/7336
Can we back up to explain the specific problems of using wl_data_device, there might be some things we can do within the current constraints.
This MR tries to work within the current Wine clipboard Wine by forwarding relevant clipboard messages to the foreground window which is likely to have the keyboard focus and that can actually handle them
I don't fully understand this part. If you are notified on the data_device about a change, you can still forward this message to all Wine windows whether they have focus or not.
The part about retrieving
- It's considered "privileged", uncertain whether compositors may try to limit which applications can access it (unlikely though)
I can confirm that Kwin already does limit who can access the clipboard manager to any flatpak'd apps. It's not a direction we want applications to go.
That doesn't mean we won't bend over backwards to help with whatever, I would rather explore a design that allow us to set the clipboard even without focus with wl_data_device than to allow all wine apps to read the clipboard without focus via ext-data-control if that's what's needed to solve your problems.
the best way forward would be to design and expose our own set of protocols that would fit exactly the Win32 model, and let compositors decide if they want to support Wine or not
The problem of having to shim an immutable existing API into a completely different awkward Wayland API is not a Wine specific problem. All the cross-platform toolkits have their own set of very similar challenges that we keep having issues with. There's more commonality here than you might think.
On Tue Feb 18 15:25:24 2025 +0000, david edmundson wrote:
Can we back up to explain the specific problems of using wl_data_device, there might be some things we can do within the current constraints.
This MR tries to work within the current Wine clipboard Wine by
forwarding relevant clipboard messages to the foreground window which is likely to have the keyboard focus and that can actually handle them I don't fully understand this part. If you are notified on the data_device about a change, you can still forward this message to all Wine windows whether they have focus or not.
- It's considered "privileged", uncertain whether compositors may try
to limit which applications can access it (unlikely though) I can confirm that Kwin already does limit who can access the clipboard manager to any flatpak'd apps. It's not a direction we want applications to go. That doesn't mean we won't bend over backwards to help with whatever, I would rather explore a design that allow us to set the clipboard even without focus with wl_data_device than to allow all wine apps to read the clipboard without focus via ext-data-control if that's what's needed to solve your problems.
the best way forward would be to design and expose our own set of
protocols that would fit exactly the Win32 model, and let compositors decide if they want to support Wine or not The problem of having to shim an immutable existing API into a completely different awkward Wayland API is not a Wine specific problem. All the cross-platform toolkits have their own set of very similar challenges that we keep having issues with. There's more commonality here than you might think.
Hi David, thanks for your feedback. Long answer ahead :)
Can we back up to explain the specific problems of using wl_data_device, there might be some things we can do within the current constraints.
wl_data_device works OK for many typical uses cases, that's why it was my initial choice (and I still think we should support it, see next paragraph). However, the constraints posed by wl_data_device mean that we are not able to provide the full win32 functionality. For example a win32 application can populate the clipboard or read clipboard contents without the need for a window at all. That's not supported by wl_data_device, but possible with the data-control family of protocols.
The idea behind the alternative MR !7336 is to provide the full win32 functionality if data-control is available, but *also* implement a fallback based on wl_data_device that provides as much functionality as we can within the constraints of that protocol. In this way sandboxed apps will still work OK for the typical clipboard use cases. See the description of !7336 for a link to WIP follow-up MR that implements the wl_data_device fallback.
This MR tries to work within the current Wine clipboard Wine by forwarding relevant clipboard messages to the foreground window which is likely to have the keyboard focus and that can actually handle them
I don't fully understand this part. If you are notified on the data_device about a change, you can still forward this message to all Wine windows whether they have focus or not.
For the wl_data_device scenario in particular, below are notes for the two clipboard directions ((2) is the one relevant for what you asked, but just writing everything down for completeness).
1. Copy data from win32 app, paste into native Wayland app
In order for the win32 clipboard contents to become available to Wayland applications, we need to offer them through (the process that owns) the focused surface. This may not be the process that actually put the data into the clipboard. There are a few options here:
1. Only support the case where the process that places data into the clipboard also has the focus. 2. [This MR] Have a single handler of win32 clipboard updates (in the desktop process), which then notifies the window/process that has the focused surface. 3. [Follow-up to other MR] Have every process listen to win32 clipboard updates, whichever has the focused surface handles the update.
2. Copy data from native Wayland app, paste into win32 app
When we get a new offer from the native Wayland app, the clipboard can be populated with the offered content types by any process (e.g., the one that receives the `wl_data_device.selection` event). However, we need to mark a specific win32 window as the owner of the clipboard, which will then be responsible for providing the data for a particular content type when it receives the WM_RENDERFORMAT window message. The current approach across all drivers is to create a special, message-only win32 window that represents the real native clipboard owner:
1. [This MR] Have a single owner window for all processes, which will then forward any request for clipboard data to the window that actually has access to them (i.e., the one with the Wayland focus). 2. [Follow-up to other MR] Have a separate owner window for each process, which can request the data directly from within the same process. The owner window changes dynamically as Wayland focus moves to different processes.
The design space is not limited to the above and I am happy to explore other avenues if needed. I experimented a bit with some other ideas (e.g., see https://gitlab.winehq.org/wine/wine/-/merge_requests/7236#note_94002), but I am currently most comfortable with the alternative MR approach for wl_data_device, which doesn't deviate much from how the clipboard integration is done in the other Wine drivers.
That doesn't mean we won't bend over backwards to help with whatever, I would rather explore a design that allow us to set the clipboard even without focus with wl_data_device than to allow all wine apps to read the clipboard without focus via ext-data-control if that's what's needed to solve your problems.
What you propose would help in terms of functionality with clipboard direction (1) above. Compared to data-control, though, it would still be both less powerful and more complex, since the main part of the integration complexity comes from direction (2). However, if we do want the fallback anyway, the complexity aspect perhaps is not as important (and in fact supporting only one protocol would be simpler).
In the end I think the decision boils down to just one thing: whether we are happy with the somewhat limited (but mostly adequate) functionality that's possible with wl_data_device, or we want to provide the 100% full win32 clipboard functionality.
--
I understand that the decision to use data-control (or not) involves wider, somewhat not-technical aspects. To me this whole discussion underlines that there are fundamental unresolved issues in the concept of privileged protocols:
1. Without a sandbox it's game over in terms of security anyway (and as you said the sandbox can limit access to privileged protocols). 2. It's a losing game to expose a protocol and then expect applications to just be good Wayland citizens and not use it, without enforcing that restriction somehow. Also, who decides which applications should use which protocol?
But I guess this is a discussion for another forum.
This merge request was closed by Alexandros Frantzis.
The clipboard functionality was merged in !7336. If we want to have wl_data_device based fallback, it will need to be based on the new design, so closing this MR.