From eba962c3e353b3fd3f11e30a7d6f48585e3a153c Mon Sep 17 00:00:00 2001 From: dec05eba Date: Tue, 16 Nov 2021 10:56:52 +0100 Subject: Window: add set/get clipboard --- include/mgl/window/window.h | 7 ++ src/window/window.c | 193 +++++++++++++++++++++++++++++++++++++++++++- tests/main.c | 11 +++ 3 files changed, 207 insertions(+), 4 deletions(-) diff --git a/include/mgl/window/window.h b/include/mgl/window/window.h index 812a5b2..95e3273 100644 --- a/include/mgl/window/window.h +++ b/include/mgl/window/window.h @@ -7,6 +7,7 @@ #include "key.h" #include "mouse_button.h" #include +#include /* Vsync is automatically set for created windows, if supported by the system */ @@ -32,6 +33,8 @@ struct mgl_window { bool focused; double frame_time_limit; mgl_clock frame_timer; + char *clipboard_string; + size_t clipboard_size; }; typedef struct { @@ -75,4 +78,8 @@ void mgl_window_set_size(mgl_window *self, mgl_vec2i size); /* if |minimum| is (0, 0) then there is no minimum limit, if |maximum| is (0, 0) then there is no maximum limit */ void mgl_window_set_size_limits(mgl_window *self, mgl_vec2i minimum, mgl_vec2i maximum); +void mgl_window_set_clipboard(mgl_window *self, const char *str, size_t size); +/* A malloc'ed and null terminated string is returned in |str|, it should be deallocated with free */ +bool mgl_window_get_clipboard(mgl_window *self, char **str, size_t *size); + #endif /* MGL_WINDOW_H */ diff --git a/src/window/window.c b/src/window/window.c index 15faff3..5599c58 100644 --- a/src/window/window.c +++ b/src/window/window.c @@ -4,6 +4,7 @@ #include "../../include/mgl/system/utf8.h" #include #include +#include #include #include #include @@ -52,6 +53,12 @@ typedef struct { GLXContext glx_context; XIM xim; XIC xic; + Atom clipboard_atom; + Atom targets_atom; + Atom text_atom; + Atom utf8_string_atom; + Atom target_property_atom; + Atom incr_atom; Cursor default_cursor; Cursor invisible_cursor; /* @@ -73,6 +80,12 @@ static int x11_context_init(x11_context *self) { self->glx_context = NULL; self->xim = NULL; self->xic = NULL; + self->clipboard_atom = XInternAtom(context->connection, "CLIPBOARD", False); + self->targets_atom = XInternAtom(context->connection, "TARGETS", False); + self->text_atom = XInternAtom(context->connection, "TEXT", False); + self->utf8_string_atom = XInternAtom(context->connection, "UTF8_STRING", True); + self->target_property_atom = XInternAtom(context->connection, "MGL_CLIPBOARD_TARGET_PROPERTY", False); + self->incr_atom = XInternAtom(context->connection, "INCR", False); self->default_cursor = None; self->invisible_cursor = None; @@ -215,6 +228,8 @@ static int mgl_window_init(mgl_window *self, const char *title, const mgl_window self->focused = false; self->frame_time_limit = 0.0; mgl_clock_init(&self->frame_timer); + self->clipboard_string = NULL; + self->clipboard_size = 0; mgl_vec2i window_size = params ? params->size : (mgl_vec2i){ 0, 0 }; if(window_size.x <= 0 || window_size.y <= 0) { @@ -292,7 +307,7 @@ static int mgl_window_init(mgl_window *self, const char *title, const mgl_window } if(params) - mgl_window_set_size_limits(self, params->min_size, params->min_size); + mgl_window_set_size_limits(self, params->min_size, params->max_size); /* TODO: Call XGetWMProtocols and add wm_delete_window_atom on top, to not overwrite existing wm protocol atoms */ Atom wm_protocol_atoms[2] = { @@ -352,15 +367,24 @@ int mgl_window_init_from_existing_window(mgl_window *self, mgl_window_handle exi void mgl_window_deinit(mgl_window *self) { mgl_context *context = mgl_get_context(); x11_context *x11_context = self->context; + if(x11_context) { x11_context_deinit(x11_context); free(x11_context); self->context = NULL; } + + if(self->clipboard_string) { + free(self->clipboard_string); + self->clipboard_string = NULL; + } + self->clipboard_size = 0; + if(self->window) { XDestroyWindow(context->connection, self->window); self->window = 0; } + self->open = false; } @@ -525,12 +549,12 @@ static void mgl_window_on_receive_event(mgl_window *self, XEvent *xev, mgl_event case FocusIn: { event->type = MGL_EVENT_GAINED_FOCUS; self->focused = true; - break; + return; } case FocusOut: { event->type = MGL_EVENT_LOST_FOCUS; self->focused = false; - break; + return; } case ConfigureNotify: { while(XCheckTypedWindowEvent(context->connection, self->window, ConfigureNotify, xev)) {} @@ -555,6 +579,57 @@ static void mgl_window_on_receive_event(mgl_window *self, XEvent *xev, mgl_event event->mouse_move.y = self->cursor_position.y; return; } + case SelectionClear: { + event->type = MGL_EVENT_UNKNOWN; + return; + } + case SelectionRequest: { + x11_context *x11_context = self->context; + + XSelectionEvent selection_event; + selection_event.type = SelectionNotify; + selection_event.requestor = xev->xselectionrequest.requestor; + selection_event.selection = xev->xselectionrequest.selection; + selection_event.property = xev->xselectionrequest.property; + selection_event.time = xev->xselectionrequest.time; + selection_event.target = xev->xselectionrequest.target; + + if(selection_event.selection == x11_context->clipboard_atom) { + /* TODO: Support ascii text separately by unsetting the 8th bit in the clipboard string? */ + if(selection_event.target == x11_context->targets_atom) { + /* A client requested for our valid conversion targets */ + int num_targets = 3; + Atom targets[4]; + targets[0] = x11_context->targets_atom; + targets[1] = x11_context->text_atom; + targets[2] = XA_STRING; + if(x11_context->utf8_string_atom) { + targets[3] = x11_context->utf8_string_atom; + num_targets = 4; + } + + XChangeProperty(context->connection, selection_event.requestor, selection_event.property, XA_ATOM, 32, PropModeReplace, (unsigned char*)targets, num_targets); + selection_event.target = x11_context->targets_atom; + XSendEvent(context->connection, selection_event.requestor, True, NoEventMask, (XEvent*)&selection_event); + } else if(selection_event.target == XA_STRING || (!x11_context->utf8_string_atom && selection_event.target == x11_context->text_atom)) { + /* A client requested ascii clipboard */ + XChangeProperty(context->connection, selection_event.requestor, selection_event.property, XA_STRING, 8, PropModeReplace, self->clipboard_string, self->clipboard_size); + selection_event.target = XA_STRING; + XSendEvent(context->connection, selection_event.requestor, True, NoEventMask, (XEvent*)&selection_event); + } else if(x11_context->utf8_string_atom && (selection_event.target == x11_context->utf8_string_atom || selection_event.target == x11_context->text_atom)) { + /* A client requested utf8 clipboard */ + XChangeProperty(context->connection, selection_event.requestor, selection_event.property, x11_context->utf8_string_atom, 8, PropModeReplace, self->clipboard_string, self->clipboard_size); + selection_event.target = x11_context->utf8_string_atom; + XSendEvent(context->connection, selection_event.requestor, True, NoEventMask, (XEvent*)&selection_event); + } + } + + /* Repond to the client that wants our clipboard content */ + selection_event.property = None; + XSendEvent(context->connection, selection_event.requestor, True, NoEventMask, (XEvent*)&selection_event); + event->type = MGL_EVENT_UNKNOWN; + return; + } case ClientMessage: { if(xev->xclient.format == 32 && (unsigned long)xev->xclient.data.l[0] == context->wm_delete_window_atom) { event->type = MGL_EVENT_CLOSED; @@ -646,9 +721,9 @@ bool mgl_window_is_mouse_button_pressed(const mgl_window *self, mgl_mouse_button void mgl_window_close(mgl_window *self) { mgl_context *context = mgl_get_context(); - XUnmapWindow(context->connection, self->window); XDestroyWindow(context->connection, self->window); XSync(context->connection, False); + self->window = None; self->open = false; } @@ -703,3 +778,113 @@ void mgl_window_set_size_limits(mgl_window *self, mgl_vec2i minimum, mgl_vec2i m fprintf(stderr, "Warning: failed to set window size hints\n"); } } + +void mgl_window_set_clipboard(mgl_window *self, const char *str, size_t size) { + mgl_context *context = mgl_get_context(); + x11_context *x11_context = self->context; + + if(self->clipboard_string) { + free(self->clipboard_string); + self->clipboard_string = NULL; + self->clipboard_size = 0; + } + + self->clipboard_string = malloc(size + 1); + if(!self->clipboard_string) { + fprintf(stderr, "Error: failed to allocate string for clipboard\n"); + return; + } + memcpy(self->clipboard_string, str, size); + self->clipboard_string[size] = '\0'; + self->clipboard_size = size; + + XSetSelectionOwner(context->connection, x11_context->clipboard_atom, self->window, CurrentTime); + + /* Check if setting the selection owner was successful */ + if(XGetSelectionOwner(context->connection, x11_context->clipboard_atom) != self->window) + fprintf(stderr, "Error: mgl_window_set_clipboard failed\n"); +} + +/* TODO: Right now the returned str is free'd with free, but shouldn't it be free'd with free? */ +bool mgl_window_get_clipboard(mgl_window *self, char **str, size_t *size) { + mgl_context *context = mgl_get_context(); + x11_context *x11_context = self->context; + + *str = NULL; + *size = 0; + + Window selection_owner = XGetSelectionOwner(context->connection, x11_context->clipboard_atom); + + if(!selection_owner) { + char *empty_str = malloc(1); + if(!empty_str) + return false; + + empty_str[0] = '\0'; + return true; + } + + /* Return data immediately if we are the owner of the clipboard, because we can't process other events in the below event loop */ + if(selection_owner == self->window) { + if(!self->clipboard_string) { + char *empty_str = malloc(1); + if(!empty_str) + return false; + + empty_str[0] = '\0'; + return true; + } + + char *data = malloc(self->clipboard_size + 1); + if(!data) + return false; + + memcpy(data, self->clipboard_string, self->clipboard_size + 1); + *str = data; + *size = self->clipboard_size; + return true; + } + + XEvent xev; + while(XCheckTypedWindowEvent(context->connection, self->window, SelectionNotify, &xev)) {} + + XConvertSelection(context->connection, x11_context->clipboard_atom, x11_context->utf8_string_atom ? x11_context->utf8_string_atom : XA_STRING, x11_context->target_property_atom, self->window, CurrentTime); + + mgl_clock timeout_timer; + mgl_clock_init(&timeout_timer); + + const double timeout_seconds = 1.0; + while(mgl_clock_get_elapsed_time_seconds(&timeout_timer) < timeout_seconds) { + while(XCheckTypedWindowEvent(context->connection, self->window, SelectionNotify, &xev)) { + if(!xev.xselection.property || xev.xselection.selection != x11_context->clipboard_atom) + continue; + + Atom type; + int format; + unsigned long items; + unsigned long remaining_bytes; + unsigned char *data = NULL; + if(XGetWindowProperty(context->connection, self->window, x11_context->target_property_atom, 0, INT32_MAX, False, AnyPropertyType, &type, &format, &items, &remaining_bytes, &data) == Success) { + bool got_clipboard = false; + if(type != x11_context->incr_atom) { + if(type == x11_context->utf8_string_atom && format == 8) { + *str = (char*)data; + *size = items; + got_clipboard = true; + } else if(type == XA_STRING && format == 8) { + *str = (char*)data; + *size = items; + got_clipboard = true; + } + } + + XDeleteProperty(context->connection, self->window, x11_context->target_property_atom); + if(got_clipboard) + return true; + } + } + } + + /* Timed out waiting for clipboard */ + return false; +} diff --git a/tests/main.c b/tests/main.c index 51487dc..a9aebec 100644 --- a/tests/main.c +++ b/tests/main.c @@ -255,6 +255,17 @@ int main(int argc, char **argv) { break; } case MGL_EVENT_KEY_PRESSED: { + if(event.key.code == MGL_KEY_C) { + mgl_window_set_clipboard(&window, "hello", 5); + fprintf(stderr, "Copied 'hello' to the clipboard\n"); + } else if(event.key.code == MGL_KEY_V) { + char *str; + size_t size; + if(mgl_window_get_clipboard(&window, &str, &size)) { + fprintf(stderr, "Copied '%s' from the clipboard\n", str); + free(str); + } + } fprintf(stderr, "key press event, code: %u\n", event.key.code); break; } -- cgit v1.2.3