From 128b9e1cd0678b1d8c775d001528fb426224c63d Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 5 Sep 2018 17:54:26 +0530 Subject: [PATCH] Wayland: Implement support for clipboard copy/paste Fix #855 --- docs/changelog.rst | 2 + glfw/internal.h | 2 + glfw/wl_init.c | 13 +++++ glfw/wl_platform.h | 15 ++++- glfw/wl_window.c | 138 ++++++++++++++++++++++++++++++++++++++++++--- 5 files changed, 161 insertions(+), 9 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 21e997f41..72388da86 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -15,6 +15,8 @@ Changelog - Linux: Fix numpad arrow keys not working when num lock is off (:iss:`857`) +- Wayland: Implement support for clipboard copy/paste (:iss:`855`) + - Allow mapping shortcuts using the raw key code from the OS (:iss:`848`) - Allow mapping of individual keypresses without modifiers as shortcuts diff --git a/glfw/internal.h b/glfw/internal.h index 6cee0b7cd..efdcf9c66 100644 --- a/glfw/internal.h +++ b/glfw/internal.h @@ -31,6 +31,8 @@ #include "glfw_config.h" #endif +#define arraysz(x) (sizeof(x)/sizeof(x[0])) + #if defined(GLFW_INCLUDE_GLCOREARB) || \ defined(GLFW_INCLUDE_ES1) || \ defined(GLFW_INCLUDE_ES2) || \ diff --git a/glfw/wl_init.c b/glfw/wl_init.c index 941648aa1..b244e0f56 100644 --- a/glfw/wl_init.c +++ b/glfw/wl_init.c @@ -560,6 +560,9 @@ static void registryHandleGlobal(void* data, _glfw.wl.seatVersion); wl_seat_add_listener(_glfw.wl.seat, &seatListener, NULL); } + if (_glfw.wl.seat && _glfw.wl.dataDeviceManager && !_glfw.wl.dataDevice) { + _glfwSetupWaylandDataDevice(); + } } else if (strcmp(interface, "xdg_wm_base") == 0) { @@ -599,6 +602,9 @@ static void registryHandleGlobal(void* data, wl_registry_bind(registry, name, &wl_data_device_manager_interface, 1); + if (_glfw.wl.seat && _glfw.wl.dataDeviceManager && !_glfw.wl.dataDevice) { + _glfwSetupWaylandDataDevice(); + } } } @@ -771,6 +777,11 @@ void _glfwPlatformTerminate(void) zwp_idle_inhibit_manager_v1_destroy(_glfw.wl.idleInhibitManager); if (_glfw.wl.dataSourceForClipboard) wl_data_source_destroy(_glfw.wl.dataSourceForClipboard); + for (size_t doi=0; doi < arraysz(_glfw.wl.dataOffers); doi++) { + if (_glfw.wl.dataOffers[doi].id) { + wl_data_offer_destroy(_glfw.wl.dataOffers[doi].id); + } + } if (_glfw.wl.dataDevice) wl_data_device_destroy(_glfw.wl.dataDevice); if (_glfw.wl.dataDeviceManager) @@ -784,6 +795,8 @@ void _glfwPlatformTerminate(void) } closeFds(_glfw.wl.eventLoopData.wakeupFds, sizeof(_glfw.wl.eventLoopData.wakeupFds)/sizeof(_glfw.wl.eventLoopData.wakeupFds[0])); free(_glfw.wl.clipboardString); _glfw.wl.clipboardString = NULL; + free(_glfw.wl.clipboardSourceString); _glfw.wl.clipboardSourceString = NULL; + } const char* _glfwPlatformGetVersionString(void) diff --git a/glfw/wl_platform.h b/glfw/wl_platform.h index f778e0370..827044355 100644 --- a/glfw/wl_platform.h +++ b/glfw/wl_platform.h @@ -175,6 +175,14 @@ typedef struct _GLFWwindowWayland } _GLFWwindowWayland; +typedef struct _GLFWWaylandDataOffer +{ + struct wl_data_offer *id; + const char *mime; + int offer_type; + size_t idx; +} _GLFWWaylandDataOffer; + // Wayland-specific global data // typedef struct _GLFWlibraryWayland @@ -235,7 +243,11 @@ typedef struct _GLFWlibraryWayland } egl; EventLoopData eventLoopData; - char *clipboardString; + char* clipboardString; + char* clipboardSourceString; + struct wl_data_offer* clipboardSourceOffer; + size_t dataOffersCounter; + _GLFWWaylandDataOffer dataOffers[8]; } _GLFWlibraryWayland; // Wayland-specific per-monitor data @@ -264,3 +276,4 @@ typedef struct _GLFWcursorWayland void _glfwAddOutputWayland(uint32_t name, uint32_t version); +void _glfwSetupWaylandDataDevice(); diff --git a/glfw/wl_window.c b/glfw/wl_window.c index 016726e31..c891d30cb 100644 --- a/glfw/wl_window.c +++ b/glfw/wl_window.c @@ -1457,10 +1457,126 @@ static void _glfwSendClipboardText(void *data, struct wl_data_source *data_sourc close(fd); } -const struct wl_data_source_listener data_source_listener = { +static const char* _glfwReceiveClipboardText(struct wl_data_offer *data_offer, const char *mime) +{ + if (_glfw.wl.clipboardSourceOffer == data_offer && _glfw.wl.clipboardSourceString) + return _glfw.wl.clipboardSourceString; + int pipefd[2]; + if (pipe2(pipefd, O_CLOEXEC) != 0) return NULL; + wl_data_offer_receive(data_offer, mime, pipefd[1]); + close(pipefd[1]); + wl_display_flush(_glfw.wl.display); + size_t sz = 0, capacity = 0; + free(_glfw.wl.clipboardSourceString); + _glfw.wl.clipboardSourceString = NULL; + struct pollfd fds; + fds.fd = pipefd[0]; + fds.events = POLLIN; + double start = glfwGetTime(); +#define bail(...) { \ + _glfwInputError(GLFW_PLATFORM_ERROR, __VA_ARGS__); \ + free(_glfw.wl.clipboardSourceString); _glfw.wl.clipboardSourceString = NULL; \ + close(pipefd[0]); \ + return NULL; \ +} + + while (glfwGetTime() - start < 2) { + int ret = poll(&fds, 1, 2000); + if (ret == -1) { + if (errno == EINTR) continue; + bail("Wayland: Failed to poll clipboard data from pipe with error: %s", strerror(errno)); + } + if (!ret) { + bail("Wayland: Failed to read clipboard data from pipe (timed out)"); + } + if (capacity <= sz || capacity - sz <= 64) { + capacity += 4096; + _glfw.wl.clipboardSourceString = realloc(_glfw.wl.clipboardSourceString, capacity); + if (!_glfw.wl.clipboardSourceString) { + bail("Wayland: Failed to allocate memory to read clipboard data"); + } + } + ret = read(pipefd[0], _glfw.wl.clipboardSourceString + sz, capacity - sz - 1); + if (ret == -1) { + if (errno == EINTR || errno == EAGAIN || errno == EWOULDBLOCK) continue; + bail("Wayland: Failed to read clipboard data from pipe with error: %s", strerror(errno)); + } + if (ret == 0) { close(pipefd[0]); _glfw.wl.clipboardSourceString[sz] = 0; return _glfw.wl.clipboardSourceString; } + sz += ret; + start = glfwGetTime(); + } + bail("Wayland: Failed to read clipboard data from pipe (timed out)"); +#undef bail +} + +const static struct wl_data_source_listener data_source_listener = { .send = _glfwSendClipboardText, }; +static void prune_unclaimed_data_offers() { + for (size_t i = 0; i < arraysz(_glfw.wl.dataOffers); i++) { + if (_glfw.wl.dataOffers[i].id && !_glfw.wl.dataOffers[i].offer_type) { + wl_data_offer_destroy(_glfw.wl.dataOffers[i].id); + memset(_glfw.wl.dataOffers + i, 0, sizeof(_glfw.wl.dataOffers[0])); + } + } +} + +static void mark_selection_offer(void *data, struct wl_data_device *data_device, struct wl_data_offer *data_offer) +{ + for (size_t i = 0; i < arraysz(_glfw.wl.dataOffers); i++) { + if (_glfw.wl.dataOffers[i].id == data_offer) { + _glfw.wl.dataOffers[i].offer_type = 1; + } else if (_glfw.wl.dataOffers[i].offer_type == 1) { + _glfw.wl.dataOffers[i].offer_type = 0; // previous selection offer + } + } + prune_unclaimed_data_offers(); +} + +static void handle_offer_mimetype(void *data, struct wl_data_offer* id, const char *mime) { + for (size_t i = 0; i < arraysz(_glfw.wl.dataOffers); i++) { + if (_glfw.wl.dataOffers[i].id == id) { + if (strcmp(mime, "text/plain;charset=utf-8") == 0) + _glfw.wl.dataOffers[i].mime = "text/plain;charset=utf-8"; + else if (!_glfw.wl.dataOffers[i].mime && strcmp(mime, "text/plain")) + _glfw.wl.dataOffers[i].mime = "text/plain"; + break; + } + } +} + +static const struct wl_data_offer_listener data_offer_listener = { + .offer = handle_offer_mimetype, +}; + +static void handle_data_offer(void *data, struct wl_data_device *wl_data_device, struct wl_data_offer *id) { + size_t smallest_idx = SIZE_MAX, pos = 0; + for (size_t i = 0; i < arraysz(_glfw.wl.dataOffers); i++) { + if (_glfw.wl.dataOffers[i].idx && _glfw.wl.dataOffers[i].idx < smallest_idx) { + smallest_idx = _glfw.wl.dataOffers[i].idx; + pos = i; + } + if (_glfw.wl.dataOffers[i].id == NULL) { + _glfw.wl.dataOffers[i].id = id; + _glfw.wl.dataOffers[i].idx = ++_glfw.wl.dataOffersCounter; + goto end; + } + } + if (_glfw.wl.dataOffers[pos].id) wl_data_offer_destroy(_glfw.wl.dataOffers[pos].id); + memset(_glfw.wl.dataOffers + pos, 0, sizeof(_glfw.wl.dataOffers[0])); + _glfw.wl.dataOffers[pos].id = id; + _glfw.wl.dataOffers[pos].idx = ++_glfw.wl.dataOffersCounter; +end: + wl_data_offer_add_listener(id, &data_offer_listener, NULL); +} + +const static struct wl_data_device_listener data_device_listener = { + .data_offer = handle_data_offer, + .selection = mark_selection_offer, +}; + + static void copy_callback_done(void *data, struct wl_callback *callback, uint32_t serial) { if (!_glfw.wl.dataDevice) return; @@ -1469,15 +1585,20 @@ copy_callback_done(void *data, struct wl_callback *callback, uint32_t serial) { } } -const struct wl_callback_listener copy_callback_listener = { +const static struct wl_callback_listener copy_callback_listener = { .done = copy_callback_done }; +void _glfwSetupWaylandDataDevice() { + _glfw.wl.dataDevice = wl_data_device_manager_get_data_device(_glfw.wl.dataDeviceManager, _glfw.wl.seat); + if (_glfw.wl.dataDevice) wl_data_device_add_listener(_glfw.wl.dataDevice, &data_device_listener, NULL); +} + static inline GLFWbool _glfwEnsureDataDevice() { if (!_glfw.wl.dataDeviceManager) { _glfwInputError(GLFW_PLATFORM_ERROR, - "Wayland: Cannot use clipboard data device manager is not ready"); + "Wayland: Cannot use clipboard, data device manager is not ready"); return GLFW_FALSE; } @@ -1486,10 +1607,9 @@ static inline GLFWbool _glfwEnsureDataDevice() { if (!_glfw.wl.seat) { _glfwInputError(GLFW_PLATFORM_ERROR, - "Wayland: Cannot use clipboard seat is not ready"); + "Wayland: Cannot use clipboard, seat is not ready"); return GLFW_FALSE; } - _glfw.wl.dataDevice = wl_data_device_manager_get_data_device(_glfw.wl.dataDeviceManager, _glfw.wl.seat); if (!_glfw.wl.dataDevice) { _glfwInputError(GLFW_PLATFORM_ERROR, @@ -1526,9 +1646,11 @@ void _glfwPlatformSetClipboardString(const char* string) const char* _glfwPlatformGetClipboardString(void) { - // TODO - _glfwInputError(GLFW_PLATFORM_ERROR, - "Wayland: Clipboard getting not implemented yet"); + for (size_t i = 0; i < arraysz(_glfw.wl.dataOffers); i++) { + if (_glfw.wl.dataOffers[i].id && _glfw.wl.dataOffers[i].mime && _glfw.wl.dataOffers[i].offer_type == 1) { + return _glfwReceiveClipboardText(_glfw.wl.dataOffers[i].id, _glfw.wl.dataOffers[i].mime); + } + } return NULL; }