From f07eda195dfe073d7fea675de3c181f88e22872e Mon Sep 17 00:00:00 2001 From: qwx Date: Thu, 12 Sep 2024 10:58:28 +0200 Subject: sokol_app: x11 clipboard support based on #998 and https://github.com/X11-good-tools/x11clipboard --- sokol_app.h | 124 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 123 insertions(+), 1 deletion(-) diff --git a/sokol_app.h b/sokol_app.h index 7cd29bf8..a3ecf25a 100644 --- a/sokol_app.h +++ b/sokol_app.h @@ -1603,6 +1603,7 @@ typedef struct sapp_allocator { _SAPP_LOGITEM_XMACRO(LINUX_X11_OPEN_DISPLAY_FAILED, "XOpenDisplay() failed") \ _SAPP_LOGITEM_XMACRO(LINUX_X11_QUERY_SYSTEM_DPI_FAILED, "failed to query system dpi value, assuming default 96.0") \ _SAPP_LOGITEM_XMACRO(LINUX_X11_DROPPED_FILE_URI_WRONG_SCHEME, "dropped file URL doesn't start with 'file://'") \ + _SAPP_LOGITEM_XMACRO(LINUX_X11_FAILED_TO_BECOME_OWNER_OF_CLIPBOARD, "X11: Failed to become owner of clipboard selection") \ _SAPP_LOGITEM_XMACRO(ANDROID_UNSUPPORTED_INPUT_EVENT_INPUT_CB, "unsupported input event encountered in _sapp_android_input_cb()") \ _SAPP_LOGITEM_XMACRO(ANDROID_UNSUPPORTED_INPUT_EVENT_MAIN_CB, "unsupported input event encountered in _sapp_android_main_cb()") \ _SAPP_LOGITEM_XMACRO(ANDROID_READ_MSG_FAILED, "failed to read message in _sapp_android_main_cb()") \ @@ -2162,6 +2163,7 @@ inline void sapp_run(const sapp_desc& desc) { return sapp_run(&desc); } #include /* LONG_MAX */ #include /* only used a linker-guard, search for _sapp_linux_run() and see first comment */ #include + #include #endif #if defined(_SAPP_APPLE) @@ -2752,6 +2754,8 @@ typedef struct { float dpi; unsigned char error_code; Atom UTF8_STRING; + Atom CLIPBOARD; + Atom TARGETS; Atom WM_PROTOCOLS; Atom WM_DELETE_WINDOW; Atom WM_STATE; @@ -9609,7 +9613,11 @@ _SOKOL_PRIVATE void _sapp_x11_release_error_handler(void) { } _SOKOL_PRIVATE void _sapp_x11_init_extensions(void) { - _sapp.x11.UTF8_STRING = XInternAtom(_sapp.x11.display, "UTF8_STRING", False); + if (_sapp.clipboard.enabled) { + _sapp.x11.UTF8_STRING = XInternAtom(_sapp.x11.display, "UTF8_STRING", False); + _sapp.x11.CLIPBOARD = XInternAtom(_sapp.x11.display, "CLIPBOARD", False); + _sapp.x11.TARGETS = XInternAtom(_sapp.x11.display, "TARGETS", False); + } _sapp.x11.WM_PROTOCOLS = XInternAtom(_sapp.x11.display, "WM_PROTOCOLS", False); _sapp.x11.WM_DELETE_WINDOW = XInternAtom(_sapp.x11.display, "WM_DELETE_WINDOW", False); _sapp.x11.WM_STATE = XInternAtom(_sapp.x11.display, "WM_STATE", False); @@ -10463,6 +10471,78 @@ _SOKOL_PRIVATE void _sapp_x11_lock_mouse(bool lock) { XFlush(_sapp.x11.display); } +_SOKOL_PRIVATE void _sapp_x11_set_clipboard_string(const char* str) { + SOKOL_ASSERT(_sapp.clipboard.enabled && _sapp.clipboard.buffer); + if (strlen(str) >= (size_t)_sapp.clipboard.buf_size) { + _SAPP_ERROR(CLIPBOARD_STRING_TOO_BIG); + } + XSetSelectionOwner(_sapp.x11.display, _sapp.x11.CLIPBOARD, _sapp.x11.window, CurrentTime); + if (XGetSelectionOwner(_sapp.x11.display, _sapp.x11.CLIPBOARD) != _sapp.x11.window) { + _SAPP_ERROR(LINUX_X11_FAILED_TO_BECOME_OWNER_OF_CLIPBOARD); + } +} + +_SOKOL_PRIVATE const char* _sapp_x11_get_clipboard_string(void) { + SOKOL_ASSERT(_sapp.clipboard.enabled && _sapp.clipboard.buffer); + Atom none = XInternAtom(_sapp.x11.display, "SAPP_SELECTION", False); + Atom incremental = XInternAtom(_sapp.x11.display, "INCR", False); + if (XGetSelectionOwner(_sapp.x11.display, _sapp.x11.CLIPBOARD) == _sapp.x11.window) { + // Instead of doing a large number of X round-trips just to put this + // string into a window property and then read it back, just return it + return _sapp.clipboard.buffer; + } + XConvertSelection(_sapp.x11.display, + _sapp.x11.CLIPBOARD, + _sapp.x11.UTF8_STRING, + none, + _sapp.x11.window, + CurrentTime); + XEvent event; + while (!XCheckTypedWindowEvent(_sapp.x11.display, + _sapp.x11.window, + SelectionNotify, + &event)) { + // Wait for event data to arrive on the X11 display socket + struct pollfd fd = { ConnectionNumber(_sapp.x11.display), POLLIN }; + while (!XPending(_sapp.x11.display)) { + poll(&fd, 1, -1); + } + } + if (event.xselection.property == None) { + return NULL; + } + char* data = NULL; + Atom actualType; + int actualFormat; + unsigned long itemCount, bytesAfter; + const bool ret = XGetWindowProperty(_sapp.x11.display, + event.xselection.requestor, + event.xselection.property, + 0, + LONG_MAX, + True, + _sapp.x11.UTF8_STRING, + &actualType, + &actualFormat, + &itemCount, + &bytesAfter, + (unsigned char**) &data); + if (ret != Success || data == NULL) { + if (data != NULL) { + XFree(data); + } + return NULL; + } + if (actualType == incremental || itemCount >= (size_t)_sapp.clipboard.buf_size) { + _SAPP_ERROR(CLIPBOARD_STRING_TOO_BIG); + XFree(data); + return NULL; + } + _sapp_strcpy(data, _sapp.clipboard.buffer, _sapp.clipboard.buf_size); + XFree(data); + return _sapp.clipboard.buffer; +} + _SOKOL_PRIVATE void _sapp_x11_update_window_title(void) { Xutf8SetWMProperties(_sapp.x11.display, _sapp.x11.window, @@ -11243,6 +11323,44 @@ _SOKOL_PRIVATE void _sapp_x11_process_event(XEvent* event) { case SelectionNotify: _sapp_x11_on_selectionnotify(event); break; + case SelectionRequest: { + XSelectionRequestEvent* req = &event->xselectionrequest; + if (req->selection != _sapp.x11.CLIPBOARD) { + break; + } + XSelectionEvent reply = { + .type = SelectionNotify, + .display = req->display, + .requestor = req->requestor, + .selection = req->selection, + .target = req->target, + .property = req->property, + .time = req->time + }; + if (req->target == _sapp.x11.UTF8_STRING) { + XChangeProperty(_sapp.x11.display, + req->requestor, + req->property, + _sapp.x11.UTF8_STRING, + 8, + PropModeReplace, + (unsigned char*) _sapp.clipboard.buffer, + strlen(_sapp.clipboard.buffer)); + } else if (req->target == _sapp.x11.TARGETS) { + XChangeProperty(_sapp.x11.display, + req->requestor, + req->property, + XA_ATOM, + 32, + PropModeReplace, + (unsigned char*) &_sapp.x11.UTF8_STRING, + 1); + } else { + reply.property = None; + } + XSendEvent(_sapp.x11.display, req->requestor, False, 0, (XEvent*) &reply); + break; + } case DestroyNotify: // not a bug break; @@ -11723,6 +11841,8 @@ SOKOL_API_IMPL void sapp_set_clipboard_string(const char* str) { _sapp_emsc_set_clipboard_string(str); #elif defined(_SAPP_WIN32) _sapp_win32_set_clipboard_string(str); + #elif defined(_SAPP_LINUX) + _sapp_x11_set_clipboard_string(str); #else /* not implemented */ #endif @@ -11739,6 +11859,8 @@ SOKOL_API_IMPL const char* sapp_get_clipboard_string(void) { return _sapp.clipboard.buffer; #elif defined(_SAPP_WIN32) return _sapp_win32_get_clipboard_string(); + #elif defined(_SAPP_LINUX) + return _sapp_x11_get_clipboard_string(); #else /* not implemented */ return _sapp.clipboard.buffer; -- cgit v1.2.3 From ebcf50ead8d454ce4d4c00d562cb17f17dec313d Mon Sep 17 00:00:00 2001 From: Andre Weissflog Date: Mon, 16 Sep 2024 16:53:34 +0200 Subject: sokol_app.h x11: PR fix, initialize UTF8_STRING also when clipboard is not enabled --- sokol_app.h | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/sokol_app.h b/sokol_app.h index a3ecf25a..fed6f05c 100644 --- a/sokol_app.h +++ b/sokol_app.h @@ -9613,11 +9613,7 @@ _SOKOL_PRIVATE void _sapp_x11_release_error_handler(void) { } _SOKOL_PRIVATE void _sapp_x11_init_extensions(void) { - if (_sapp.clipboard.enabled) { - _sapp.x11.UTF8_STRING = XInternAtom(_sapp.x11.display, "UTF8_STRING", False); - _sapp.x11.CLIPBOARD = XInternAtom(_sapp.x11.display, "CLIPBOARD", False); - _sapp.x11.TARGETS = XInternAtom(_sapp.x11.display, "TARGETS", False); - } + _sapp.x11.UTF8_STRING = XInternAtom(_sapp.x11.display, "UTF8_STRING", False); _sapp.x11.WM_PROTOCOLS = XInternAtom(_sapp.x11.display, "WM_PROTOCOLS", False); _sapp.x11.WM_DELETE_WINDOW = XInternAtom(_sapp.x11.display, "WM_DELETE_WINDOW", False); _sapp.x11.WM_STATE = XInternAtom(_sapp.x11.display, "WM_STATE", False); @@ -9626,6 +9622,10 @@ _SOKOL_PRIVATE void _sapp_x11_init_extensions(void) { _sapp.x11.NET_WM_ICON = XInternAtom(_sapp.x11.display, "_NET_WM_ICON", False); _sapp.x11.NET_WM_STATE = XInternAtom(_sapp.x11.display, "_NET_WM_STATE", False); _sapp.x11.NET_WM_STATE_FULLSCREEN = XInternAtom(_sapp.x11.display, "_NET_WM_STATE_FULLSCREEN", False); + if (_sapp.clipboard.enabled) { + _sapp.x11.CLIPBOARD = XInternAtom(_sapp.x11.display, "CLIPBOARD", False); + _sapp.x11.TARGETS = XInternAtom(_sapp.x11.display, "TARGETS", False); + } if (_sapp.drop.enabled) { _sapp.x11.xdnd.XdndAware = XInternAtom(_sapp.x11.display, "XdndAware", False); _sapp.x11.xdnd.XdndEnter = XInternAtom(_sapp.x11.display, "XdndEnter", False); -- cgit v1.2.3 From 3907e180a890c3ef939c3c5749e09af222882d8f Mon Sep 17 00:00:00 2001 From: Andre Weissflog Date: Mon, 16 Sep 2024 17:17:37 +0200 Subject: sokol_app.h x11: PR fix, coding style --- sokol_app.h | 84 ++++++++++++++++++++++++++++++------------------------------- 1 file changed, 42 insertions(+), 42 deletions(-) diff --git a/sokol_app.h b/sokol_app.h index fed6f05c..cefab74e 100644 --- a/sokol_app.h +++ b/sokol_app.h @@ -10498,10 +10498,7 @@ _SOKOL_PRIVATE const char* _sapp_x11_get_clipboard_string(void) { _sapp.x11.window, CurrentTime); XEvent event; - while (!XCheckTypedWindowEvent(_sapp.x11.display, - _sapp.x11.window, - SelectionNotify, - &event)) { + while (!XCheckTypedWindowEvent(_sapp.x11.display, _sapp.x11.window, SelectionNotify, &event)) { // Wait for event data to arrive on the X11 display socket struct pollfd fd = { ConnectionNumber(_sapp.x11.display), POLLIN }; while (!XPending(_sapp.x11.display)) { @@ -10533,7 +10530,7 @@ _SOKOL_PRIVATE const char* _sapp_x11_get_clipboard_string(void) { } return NULL; } - if (actualType == incremental || itemCount >= (size_t)_sapp.clipboard.buf_size) { + if ((actualType == incremental) || (itemCount >= (size_t)_sapp.clipboard.buf_size)) { _SAPP_ERROR(CLIPBOARD_STRING_TOO_BIG); XFree(data); return NULL; @@ -11282,6 +11279,44 @@ _SOKOL_PRIVATE void _sapp_x11_on_clientmessage(XEvent* event) { } } +_SOKOL_PRIVATE void _sapp_x11_on_selectionrequest(XEvent* event) { + XSelectionRequestEvent* req = &event->xselectionrequest; + if (req->selection != _sapp.x11.CLIPBOARD) { + return; + } + XSelectionEvent reply; + _sapp_clear(&reply, sizeof(reply)); + reply.type = SelectionNotify; + reply.display = req->display; + reply.requestor = req->requestor; + reply.selection = req->selection; + reply.target = req->target; + reply.property = req->property; + reply.time = req->time; + if (req->target == _sapp.x11.UTF8_STRING) { + XChangeProperty(_sapp.x11.display, + req->requestor, + req->property, + _sapp.x11.UTF8_STRING, + 8, + PropModeReplace, + (unsigned char*) _sapp.clipboard.buffer, + strlen(_sapp.clipboard.buffer)); + } else if (req->target == _sapp.x11.TARGETS) { + XChangeProperty(_sapp.x11.display, + req->requestor, + req->property, + XA_ATOM, + 32, + PropModeReplace, + (unsigned char*) &_sapp.x11.UTF8_STRING, + 1); + } else { + reply.property = None; + } + XSendEvent(_sapp.x11.display, req->requestor, False, 0, (XEvent*) &reply); +} + _SOKOL_PRIVATE void _sapp_x11_process_event(XEvent* event) { switch (event->type) { case GenericEvent: @@ -11323,44 +11358,9 @@ _SOKOL_PRIVATE void _sapp_x11_process_event(XEvent* event) { case SelectionNotify: _sapp_x11_on_selectionnotify(event); break; - case SelectionRequest: { - XSelectionRequestEvent* req = &event->xselectionrequest; - if (req->selection != _sapp.x11.CLIPBOARD) { - break; - } - XSelectionEvent reply = { - .type = SelectionNotify, - .display = req->display, - .requestor = req->requestor, - .selection = req->selection, - .target = req->target, - .property = req->property, - .time = req->time - }; - if (req->target == _sapp.x11.UTF8_STRING) { - XChangeProperty(_sapp.x11.display, - req->requestor, - req->property, - _sapp.x11.UTF8_STRING, - 8, - PropModeReplace, - (unsigned char*) _sapp.clipboard.buffer, - strlen(_sapp.clipboard.buffer)); - } else if (req->target == _sapp.x11.TARGETS) { - XChangeProperty(_sapp.x11.display, - req->requestor, - req->property, - XA_ATOM, - 32, - PropModeReplace, - (unsigned char*) &_sapp.x11.UTF8_STRING, - 1); - } else { - reply.property = None; - } - XSendEvent(_sapp.x11.display, req->requestor, False, 0, (XEvent*) &reply); + case SelectionRequest: + _sapp_x11_on_selectionrequest(event); break; - } case DestroyNotify: // not a bug break; -- cgit v1.2.3 From 7729e396e399c349bb3b8b36f79ddce23f871d22 Mon Sep 17 00:00:00 2001 From: Andre Weissflog Date: Tue, 17 Sep 2024 18:56:34 +0200 Subject: sokol_app.h x11: protect from accessing uninitialized _sapp.clipboard.buffer --- sokol_app.h | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/sokol_app.h b/sokol_app.h index cefab74e..66242f38 100644 --- a/sokol_app.h +++ b/sokol_app.h @@ -9622,10 +9622,8 @@ _SOKOL_PRIVATE void _sapp_x11_init_extensions(void) { _sapp.x11.NET_WM_ICON = XInternAtom(_sapp.x11.display, "_NET_WM_ICON", False); _sapp.x11.NET_WM_STATE = XInternAtom(_sapp.x11.display, "_NET_WM_STATE", False); _sapp.x11.NET_WM_STATE_FULLSCREEN = XInternAtom(_sapp.x11.display, "_NET_WM_STATE_FULLSCREEN", False); - if (_sapp.clipboard.enabled) { - _sapp.x11.CLIPBOARD = XInternAtom(_sapp.x11.display, "CLIPBOARD", False); - _sapp.x11.TARGETS = XInternAtom(_sapp.x11.display, "TARGETS", False); - } + _sapp.x11.CLIPBOARD = XInternAtom(_sapp.x11.display, "CLIPBOARD", False); + _sapp.x11.TARGETS = XInternAtom(_sapp.x11.display, "TARGETS", False); if (_sapp.drop.enabled) { _sapp.x11.xdnd.XdndAware = XInternAtom(_sapp.x11.display, "XdndAware", False); _sapp.x11.xdnd.XdndEnter = XInternAtom(_sapp.x11.display, "XdndEnter", False); @@ -11284,6 +11282,10 @@ _SOKOL_PRIVATE void _sapp_x11_on_selectionrequest(XEvent* event) { if (req->selection != _sapp.x11.CLIPBOARD) { return; } + if (!_sapp.clipboard.enabled) { + return; + } + SOKOL_ASSERT(_sapp.clipboard.buffer); XSelectionEvent reply; _sapp_clear(&reply, sizeof(reply)); reply.type = SelectionNotify; -- cgit v1.2.3 From b19ab8b92b7350ed7f63d24bccec43e51d374d16 Mon Sep 17 00:00:00 2001 From: Andre Weissflog Date: Tue, 17 Sep 2024 19:03:05 +0200 Subject: update sokol_app.h feature matrix, readme and changelog (Linux clipboard support) --- CHANGELOG.md | 8 ++++++++ README.md | 2 +- sokol_app.h | 2 +- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3e685621..05b7bd4f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ ## Updates +### 17-Sep-2024 + +- The sokol_app.h Linux backend now has clipboard support. Many thanks to + @Dvad for the initial PR with most of the work and @qwx9 for the addtional + updates. See PR https://github.com/floooh/sokol/pull/1108 for details (this + isn't quite what ended up in sokol_app.h, because I did a couple of code + cleanup changes in during the merge). + ### 10-Sep-2024 - Update sokol_imgui.h for Dear ImGui and cimgui version 1.91.1. This diff --git a/README.md b/README.md index cf4cab0b..4c456797 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ # Sokol -[**See what's new**](https://github.com/floooh/sokol/blob/master/CHANGELOG.md) (**02-Sep-2024** in sokol_gfx.h, SG_FILTER_NONE has been removed) +[**See what's new**](https://github.com/floooh/sokol/blob/master/CHANGELOG.md) (**17-Sep-2024** sokol_app.h: clipboard support for Linux) [![Build](/../../actions/workflows/main.yml/badge.svg)](/../../actions/workflows/main.yml) [![Bindings](/../../actions/workflows/gen_bindings.yml/badge.svg)](/../../actions/workflows/gen_bindings.yml) [![build](https://github.com/floooh/sokol-zig/actions/workflows/main.yml/badge.svg)](https://github.com/floooh/sokol-zig/actions/workflows/main.yml) [![build](https://github.com/floooh/sokol-nim/actions/workflows/main.yml/badge.svg)](https://github.com/floooh/sokol-nim/actions/workflows/main.yml) [![Odin](https://github.com/floooh/sokol-odin/actions/workflows/main.yml/badge.svg)](https://github.com/floooh/sokol-odin/actions/workflows/main.yml)[![Rust](https://github.com/floooh/sokol-rust/actions/workflows/main.yml/badge.svg)](https://github.com/floooh/sokol-rust/actions/workflows/main.yml)[![Dlang](https://github.com/kassane/sokol-d/actions/workflows/build.yml/badge.svg)](https://github.com/kassane/sokol-d/actions/workflows/build.yml) diff --git a/sokol_app.h b/sokol_app.h index 66242f38..97f72ab3 100644 --- a/sokol_app.h +++ b/sokol_app.h @@ -130,7 +130,7 @@ screen keyboard | --- | --- | --- | YES | TODO | YES swap interval | YES | YES | YES | YES | TODO | YES high-dpi | YES | YES | TODO | YES | YES | YES - clipboard | YES | YES | TODO | --- | --- | YES + clipboard | YES | YES | YES | --- | --- | YES MSAA | YES | YES | YES | YES | YES | YES drag'n'drop | YES | YES | YES | --- | --- | YES window icon | YES | YES(1)| YES | --- | --- | YES -- cgit v1.2.3