clipboard_xlib.c (8351B)
1 /* 2 * Copyright (c) 2023-2024 lumidify <nobody@lumidify.org> 3 * 4 * Permission to use, copy, modify, and/or distribute this software for any 5 * purpose with or without fee is hereby granted, provided that the above 6 * copyright notice and this permission notice appear in all copies. 7 * 8 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 9 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 10 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 11 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 12 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 13 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 14 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 15 */ 16 17 /* Copied almost exactly from ledit. */ 18 19 #include <stdio.h> 20 #include <stdlib.h> 21 #include <time.h> 22 23 #include <X11/X.h> 24 #include <X11/Xatom.h> 25 #include <X11/Xlib.h> 26 27 #include "clipboard_xlib.h" 28 #include "clipboard.h" 29 #include "ctrlsel.h" 30 #include "graphics.h" 31 #include "macros.h" 32 #include "memory.h" 33 #include "txtbuf.h" 34 #include "graphics_xlib.h" 35 36 /* Some *inspiration* taken from SDL (https://libsdl.org), mainly 37 the idea to create a separate window just for clipboard handling. */ 38 39 static Window get_clipboard_window(ltk_clipboard *clip); 40 static Bool check_window(Display *dpy, XEvent *event, XPointer arg); 41 static txtbuf *get_text(ltk_clipboard *clip, int primary); 42 43 struct ltk_clipboard { 44 txtbuf *primary; 45 txtbuf *clipboard; 46 txtbuf *rbuf; 47 ltk_renderdata *renderdata; 48 Window window; 49 struct CtrlSelTarget starget; 50 struct CtrlSelTarget rtarget; 51 CtrlSelContext *scontext; 52 Atom xtarget; 53 }; 54 55 ltk_clipboard * 56 ltk_clipboard_create(ltk_renderdata *renderdata) { 57 ltk_clipboard *clip = ltk_malloc(sizeof(ltk_clipboard)); 58 clip->primary = txtbuf_new(); 59 clip->clipboard = txtbuf_new(); 60 clip->rbuf = txtbuf_new(); 61 clip->renderdata = renderdata; 62 clip->window = None; 63 clip->xtarget = None; 64 #ifdef X_HAVE_UTF8_STRING 65 clip->xtarget = XInternAtom(renderdata->dpy, "UTF8_STRING", False); 66 #else 67 clip->xtarget = XA_STRING; 68 #endif 69 clip->scontext = NULL; 70 return clip; 71 } 72 73 void 74 ltk_clipboard_destroy(ltk_clipboard *clip) { 75 txtbuf_destroy(clip->primary); 76 txtbuf_destroy(clip->clipboard); 77 txtbuf_destroy(clip->rbuf); 78 if (clip->scontext) 79 ctrlsel_disown(clip->scontext); 80 if (clip->window != None) 81 XDestroyWindow(clip->renderdata->dpy, clip->window); 82 free(clip); 83 } 84 85 static Window 86 get_clipboard_window(ltk_clipboard *clip) { 87 if (clip->window == None) { 88 clip->window = XCreateWindow( 89 clip->renderdata->dpy, DefaultRootWindow(clip->renderdata->dpy), 90 -10, -10, 1, 1, 0, CopyFromParent, InputOnly, CopyFromParent, 0, NULL 91 ); 92 XFlush(clip->renderdata->dpy); 93 } 94 return clip->window; 95 } 96 97 void 98 ltk_clipboard_set_primary_text(ltk_clipboard *clip, char *text) { 99 txtbuf_set_text(clip->primary, text); 100 ltk_clipboard_set_primary_selection_owner(clip); 101 } 102 103 txtbuf * 104 ltk_clipboard_get_primary_buffer(ltk_clipboard *clip) { 105 return clip->primary; 106 } 107 108 void 109 ltk_clipboard_set_primary_selection_owner(ltk_clipboard *clip) { 110 Window window = get_clipboard_window(clip); 111 if (clip->scontext) 112 ctrlsel_disown(clip->scontext); 113 clip->scontext = NULL; 114 /* FIXME: is it fine to cast to unsigned char everywhere? */ 115 ctrlsel_filltarget(clip->xtarget, clip->xtarget, 8, (unsigned char *)clip->primary->text, clip->primary->len, &clip->starget); 116 /* FIXME: use proper time */ 117 clip->scontext = ctrlsel_setowner(clip->renderdata->dpy, window, XA_PRIMARY, CurrentTime, 0, &clip->starget, 1); 118 if (!clip->scontext) 119 fprintf(stderr, "WARNING: Could not own primary selection.\n"); 120 } 121 122 void 123 ltk_clipboard_set_clipboard_text(ltk_clipboard *clip, char *text) { 124 txtbuf_set_text(clip->clipboard, text); 125 ltk_clipboard_set_clipboard_selection_owner(clip); 126 } 127 128 txtbuf * 129 ltk_clipboard_get_clipboard_buffer(ltk_clipboard *clip) { 130 return clip->clipboard; 131 } 132 133 void 134 ltk_clipboard_set_clipboard_selection_owner(ltk_clipboard *clip) { 135 Atom clip_atom; 136 Window window = get_clipboard_window(clip); 137 clip_atom = XInternAtom(clip->renderdata->dpy, "CLIPBOARD", False); 138 if (clip->scontext) 139 ctrlsel_disown(clip->scontext); 140 clip->scontext = NULL; 141 /* FIXME: see clipboard_set_primary_selection_owner */ 142 ctrlsel_filltarget(clip->xtarget, clip->xtarget, 8, (unsigned char *)clip->clipboard->text, clip->clipboard->len, &clip->starget); 143 /* FIXME: use proper time */ 144 clip->scontext = ctrlsel_setowner(clip->renderdata->dpy, window, clip_atom, CurrentTime, 0, &clip->starget, 1); 145 if (!clip->scontext) 146 fprintf(stderr, "WARNING: Could not own clipboard selection.\n"); 147 } 148 149 void 150 ltk_clipboard_primary_to_clipboard(ltk_clipboard *clip) { 151 if (clip->primary->len > 0) { 152 txtbuf_copy(clip->clipboard, clip->primary); 153 ltk_clipboard_set_clipboard_selection_owner(clip); 154 } 155 } 156 157 int 158 ltk_clipboard_filter_event(ltk_clipboard *clip, XEvent *e) { 159 if (clip->window != None && e->xany.window == clip->window) { 160 if (clip->scontext) 161 ctrlsel_send(clip->scontext, e); 162 /* other events are discarded since there 163 was no request to get the clipboard text */ 164 return 1; 165 } 166 return 0; 167 } 168 169 static Bool 170 check_window(Display *dpy, XEvent *event, XPointer arg) { 171 (void)dpy; 172 return *(Window *)arg == event->xany.window; 173 } 174 175 /* WARNING: The returned txtbuf needs to be copied before further processing! */ 176 static txtbuf * 177 get_text(ltk_clipboard *clip, int primary) { 178 CtrlSelContext *context; 179 Window window = get_clipboard_window(clip); 180 ctrlsel_filltarget(clip->xtarget, clip->xtarget, 0, NULL, 0, &clip->rtarget); 181 Atom clip_atom = primary ? XA_PRIMARY : XInternAtom(clip->renderdata->dpy, "CLIPBOARD", False); 182 /* FIXME: use proper time here */ 183 context = ctrlsel_request(clip->renderdata->dpy, window, clip_atom, CurrentTime, &clip->rtarget, 1); 184 /* FIXME: show error in window? */ 185 if (!context) { 186 fprintf(stderr, "WARNING: Unable to request selection.\n"); 187 return NULL; 188 } 189 190 struct timespec now, elapsed, last, start, sleep_time; 191 sleep_time.tv_sec = 0; 192 clock_gettime(CLOCK_MONOTONIC, &start); 193 last = start; 194 XEvent event; 195 while (1) { 196 /* FIXME: I have no idea how inefficient this is */ 197 if (XCheckIfEvent(clip->renderdata->dpy, &event, &check_window, (XPointer)&window)) { 198 switch (ctrlsel_receive(context, &event)) { 199 case CTRLSEL_RECEIVED: 200 goto done; 201 case CTRLSEL_ERROR: 202 fprintf(stderr, "WARNING: Could not get selection.\n"); 203 ctrlsel_cancel(context); 204 return NULL; 205 default: 206 continue; 207 } 208 } 209 clock_gettime(CLOCK_MONOTONIC, &now); 210 ltk_timespecsub(&now, &start, &elapsed); 211 /* Timeout if it takes too long. When that happens, become the selection owner to 212 avoid further timeouts in the future (I think I copied this behavior from SDL). */ 213 /* FIXME: configure timeout */ 214 if (elapsed.tv_sec > 0) { 215 if (primary) 216 ltk_clipboard_set_primary_text(clip, ""); 217 else 218 ltk_clipboard_set_clipboard_text(clip, ""); 219 return NULL; 220 } 221 ltk_timespecsub(&now, &last, &elapsed); 222 /* FIXME: configure nanoseconds */ 223 if (elapsed.tv_sec == 0 && elapsed.tv_nsec < 20000000) { 224 sleep_time.tv_nsec = 20000000 - elapsed.tv_nsec; 225 nanosleep(&sleep_time, NULL); 226 } 227 last = now; 228 } 229 return NULL; 230 done: 231 /* FIXME: this is a bit ugly because it fiddles around with txtbuf internals */ 232 free(clip->rbuf->text); 233 clip->rbuf->cap = clip->rbuf->len = clip->rtarget.bufsize; 234 /* FIXME: again weird conversion between char and unsigned char */ 235 clip->rbuf->text = (char *)clip->rtarget.buffer; 236 clip->rtarget.buffer = NULL; /* important so ctrlsel_cancel doesn't free it */ 237 ctrlsel_cancel(context); 238 return clip->rbuf; 239 } 240 241 txtbuf * 242 ltk_clipboard_get_clipboard_text(ltk_clipboard *clip) { 243 Atom clip_atom; 244 clip_atom = XInternAtom(clip->renderdata->dpy, "CLIPBOARD", False); 245 Window window = get_clipboard_window(clip); 246 Window owner = XGetSelectionOwner(clip->renderdata->dpy, clip_atom); 247 if (owner == None) { 248 return NULL; 249 } else if (owner == window) { 250 return clip->clipboard; 251 } else { 252 return get_text(clip, 0); 253 } 254 } 255 256 txtbuf * 257 ltk_clipboard_get_primary_text(ltk_clipboard *clip) { 258 Window window = get_clipboard_window(clip); 259 Window owner = XGetSelectionOwner(clip->renderdata->dpy, XA_PRIMARY); 260 if (owner == None) { 261 return NULL; 262 } else if (owner == window) { 263 return clip->primary; 264 } else { 265 return get_text(clip, 1); 266 } 267 }