ltk

Socket-based GUI for X11 (WIP)
git clone git://lumidify.org/ltk.git (fast, but not encrypted)
git clone https://lumidify.org/git/ltk.git (encrypted, but very slow)
Log | Files | Refs | README | LICENSE

commit 5bcc196ebfd966a0d6479164d02e4a05d10b38fa
parent 004ac7555f2e18a6f439e80a8aca23e52f838621
Author: lumidify <nobody@lumidify.org>
Date:   Mon, 21 Aug 2023 20:25:53 +0200

Add clipboard support to text entry

Diffstat:
M.ltk/ltk.cfg | 2++
MLICENSE | 6+++---
MMakefile | 13++++++++++---
Asrc/clipboard.h | 26++++++++++++++++++++++++++
Asrc/clipboard_xlib.c | 252+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/clipboard_xlib.h | 11+++++++++++
Asrc/ctrlsel.c | 1645+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/ctrlsel.h | 131+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/entry.c | 82+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------
Msrc/entry.h | 2+-
Msrc/event.h | 3++-
Msrc/event_xlib.c | 9++++++---
Msrc/ltk.h | 2++
Msrc/ltkd.c | 6++++--
Asrc/txtbuf.c | 145+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/txtbuf.h | 99+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
16 files changed, 2411 insertions(+), 23 deletions(-)

diff --git a/.ltk/ltk.cfg b/.ltk/ltk.cfg @@ -41,6 +41,8 @@ bind-keypress delete-char-backwards sym backspace bind-keypress delete-char-forwards sym delete bind-keypress expand-selection-left sym left mods shift bind-keypress expand-selection-right sym right mods shift +bind-keypress selection-to-clipboard text c mods ctrl +bind-keypress paste-clipboard text v mods ctrl # default mapping (just to silence warnings) [key-mapping] diff --git a/LICENSE b/LICENSE @@ -1,10 +1,10 @@ -See src/khash.h, src/ini.*, src/stb_truetype.*, and src/strtonum.c -for third-party licenses. +See src/khash.h, src/ini.*, src/stb_truetype.*, src/strtonum.c, +and src/ctrlsel.* for third-party licenses. ISC License The Lumidify ToolKit (LTK) -Copyright (c) 2016-2022 lumidify <nobody@lumidify.org> +Copyright (c) 2016-2023 lumidify <nobody@lumidify.org> Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above diff --git a/Makefile b/Makefile @@ -35,8 +35,8 @@ EXTRA_OBJ = $(EXTRA_OBJ_$(USE_PANGO)) EXTRA_CFLAGS = $(SANITIZE_FLAGS_$(SANITIZE)) $(DEV_CFLAGS_$(DEV)) $(EXTRA_CFLAGS_$(USE_PANGO)) EXTRA_LDFLAGS = $(SANITIZE_FLAGS_$(SANITIZE)) $(DEV_LDFLAGS_$(DEV)) $(EXTRA_LDFLAGS_$(USE_PANGO)) -LTK_CFLAGS = $(EXTRA_CFLAGS) -DUSE_PANGO=$(USE_PANGO) -DDEV=$(DEV) -DMEMDEBUG=$(MEMDEBUG) -std=c99 `pkg-config --cflags x11 fontconfig xext` -D_POSIX_C_SOURCE=200809L -LTK_LDFLAGS = $(EXTRA_LDFLAGS) -lm `pkg-config --libs x11 fontconfig xext` +LTK_CFLAGS = $(EXTRA_CFLAGS) -DUSE_PANGO=$(USE_PANGO) -DDEV=$(DEV) -DMEMDEBUG=$(MEMDEBUG) -std=c99 `pkg-config --cflags x11 fontconfig xext xcursor` -D_POSIX_C_SOURCE=200809L +LTK_LDFLAGS = $(EXTRA_LDFLAGS) -lm `pkg-config --libs x11 fontconfig xext xcursor` OBJ = \ src/strtonum.o \ @@ -60,6 +60,9 @@ OBJ = \ src/event_xlib.o \ src/err.o \ src/config.o \ + src/clipboard_xlib.o \ + src/txtbuf.o \ + src/ctrlsel.o \ $(EXTRA_OBJ) # Note: This could be improved so a change in a header only causes the .c files @@ -94,7 +97,11 @@ HDR = \ src/proto_types.h \ src/config.h \ src/array.h \ - src/keys.h + src/keys.h \ + src/clipboard_xlib.h \ + src/clipboard.h \ + src/txtbuf.h \ + src/ctrlsel.h all: src/ltkd src/ltkc diff --git a/src/clipboard.h b/src/clipboard.h @@ -0,0 +1,26 @@ +#ifndef LTK_CLIPBOARD_H +#define LTK_CLIPBOARD_H + +#include "txtbuf.h" +#include "graphics.h" + +typedef struct ltk_clipboard ltk_clipboard; + +ltk_clipboard *ltk_clipboard_create(ltk_renderdata *data); +void ltk_clipboard_destroy(ltk_clipboard *clip); +void ltk_clipboard_set_primary_text(ltk_clipboard *clip, char *text); +txtbuf *ltk_clipboard_get_primary_buffer(ltk_clipboard *clip); +void ltk_clipboard_set_primary_selection_owner(ltk_clipboard *clip); +void ltk_clipboard_set_clipboard_text(ltk_clipboard *clip, char *text); +txtbuf *ltk_clipboard_get_clipboard_buffer(ltk_clipboard *clip); +void ltk_clipboard_set_clipboard_selection_owner(ltk_clipboard *clip); +void ltk_clipboard_primary_to_clipboard(ltk_clipboard *clip); + +/* FIXME: configure timeout for getting text */ +/* WARNING: The returned txtbuf is owned by the clipboard and must + be copied before further processing and especially before any + further clipboard functions are called. */ +txtbuf *ltk_clipboard_get_clipboard_text(ltk_clipboard *clip); +txtbuf *ltk_clipboard_get_primary_text(ltk_clipboard *clip); + +#endif /* LTK_CLIPBOARD_H */ diff --git a/src/clipboard_xlib.c b/src/clipboard_xlib.c @@ -0,0 +1,252 @@ +/* Copied almost exactly from ledit. */ + +#include <time.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +#include <X11/Xlib.h> +#include <X11/Xatom.h> + +#include "util.h" +#include "memory.h" +#include "graphics.h" +#include "clipboard.h" +#include "clipboard_xlib.h" +#include "xlib_shared.h" +#include "macros.h" +#include "config.h" +#include "ctrlsel.h" + +/* Some *inspiration* taken from SDL (https://libsdl.org), mainly + the idea to create a separate window just for clipboard handling. */ + +static Window get_clipboard_window(ltk_clipboard *clip); +static Bool check_window(Display *dpy, XEvent *event, XPointer arg); +static txtbuf *get_text(ltk_clipboard *clip, int primary); + +struct ltk_clipboard { + txtbuf *primary; + txtbuf *clipboard; + txtbuf *rbuf; + ltk_renderdata *renderdata; + Window window; + struct CtrlSelTarget starget; + struct CtrlSelTarget rtarget; + CtrlSelContext *scontext; + Atom xtarget; +}; + +ltk_clipboard * +ltk_clipboard_create(ltk_renderdata *renderdata) { + ltk_clipboard *clip = ltk_malloc(sizeof(ltk_clipboard)); + clip->primary = txtbuf_new(); + clip->clipboard = txtbuf_new(); + clip->rbuf = txtbuf_new(); + clip->renderdata = renderdata; + clip->window = None; + clip->xtarget = None; + #ifdef X_HAVE_UTF8_STRING + clip->xtarget = XInternAtom(renderdata->dpy, "UTF8_STRING", False); + #else + clip->xtarget = XA_STRING; + #endif + clip->scontext = NULL; + return clip; +} + +void +ltk_clipboard_destroy(ltk_clipboard *clip) { + txtbuf_destroy(clip->primary); + txtbuf_destroy(clip->clipboard); + txtbuf_destroy(clip->rbuf); + if (clip->scontext) + ctrlsel_disown(clip->scontext); + if (clip->window != None) + XDestroyWindow(clip->renderdata->dpy, clip->window); + free(clip); +} + +static Window +get_clipboard_window(ltk_clipboard *clip) { + if (clip->window == None) { + clip->window = XCreateWindow( + clip->renderdata->dpy, DefaultRootWindow(clip->renderdata->dpy), + -10, -10, 1, 1, 0, CopyFromParent, InputOnly, CopyFromParent, 0, NULL + ); + XFlush(clip->renderdata->dpy); + } + return clip->window; +} + +void +ltk_clipboard_set_primary_text(ltk_clipboard *clip, char *text) { + txtbuf_set_text(clip->primary, text); + ltk_clipboard_set_primary_selection_owner(clip); +} + +txtbuf * +ltk_clipboard_get_primary_buffer(ltk_clipboard *clip) { + return clip->primary; +} + +void +ltk_clipboard_set_primary_selection_owner(ltk_clipboard *clip) { + Window window = get_clipboard_window(clip); + if (clip->scontext) + ctrlsel_disown(clip->scontext); + clip->scontext = NULL; + /* FIXME: is it fine to cast to unsigned char everywhere? */ + ctrlsel_filltarget(clip->xtarget, clip->xtarget, 8, (unsigned char *)clip->primary->text, clip->primary->len, &clip->starget); + /* FIXME: use proper time */ + clip->scontext = ctrlsel_setowner(clip->renderdata->dpy, window, XA_PRIMARY, CurrentTime, 0, &clip->starget, 1); + if (!clip->scontext) + fprintf(stderr, "WARNING: Could not own primary selection.\n"); +} + +void +ltk_clipboard_set_clipboard_text(ltk_clipboard *clip, char *text) { + txtbuf_set_text(clip->clipboard, text); + ltk_clipboard_set_clipboard_selection_owner(clip); +} + +txtbuf * +ltk_clipboard_get_clipboard_buffer(ltk_clipboard *clip) { + return clip->clipboard; +} + +void +ltk_clipboard_set_clipboard_selection_owner(ltk_clipboard *clip) { + Atom clip_atom; + Window window = get_clipboard_window(clip); + clip_atom = XInternAtom(clip->renderdata->dpy, "CLIPBOARD", False); + if (clip->scontext) + ctrlsel_disown(clip->scontext); + clip->scontext = NULL; + /* FIXME: see clipboard_set_primary_selection_owner */ + ctrlsel_filltarget(clip->xtarget, clip->xtarget, 8, (unsigned char *)clip->clipboard->text, clip->clipboard->len, &clip->starget); + /* FIXME: use proper time */ + clip->scontext = ctrlsel_setowner(clip->renderdata->dpy, window, clip_atom, CurrentTime, 0, &clip->starget, 1); + if (!clip->scontext) + fprintf(stderr, "WARNING: Could not own clipboard selection.\n"); +} + +void +ltk_clipboard_primary_to_clipboard(ltk_clipboard *clip) { + if (clip->primary->len > 0) { + txtbuf_copy(clip->clipboard, clip->primary); + ltk_clipboard_set_clipboard_selection_owner(clip); + } +} + +int +ltk_clipboard_filter_event(ltk_clipboard *clip, XEvent *e) { + if (clip->window != None && e->xany.window == clip->window) { + if (clip->scontext) + ctrlsel_send(clip->scontext, e); + /* other events are discarded since there + was no request to get the clipboard text */ + return 1; + } + return 0; +} + +static Bool +check_window(Display *dpy, XEvent *event, XPointer arg) { + (void)dpy; + return *(Window *)arg == event->xany.window; +} + +/* WARNING: The returned txtbuf needs to be copied before further processing! */ +static txtbuf * +get_text(ltk_clipboard *clip, int primary) { + CtrlSelContext *context; + Window window = get_clipboard_window(clip); + ctrlsel_filltarget(clip->xtarget, clip->xtarget, 0, NULL, 0, &clip->rtarget); + Atom clip_atom = primary ? XA_PRIMARY : XInternAtom(clip->renderdata->dpy, "CLIPBOARD", False); + /* FIXME: use proper time here */ + context = ctrlsel_request(clip->renderdata->dpy, window, clip_atom, CurrentTime, &clip->rtarget, 1); + /* FIXME: show error in window? */ + if (!context) { + fprintf(stderr, "WARNING: Unable to request selection.\n"); + return NULL; + } + + struct timespec now, elapsed, last, start, sleep_time; + sleep_time.tv_sec = 0; + clock_gettime(CLOCK_MONOTONIC, &start); + last = start; + XEvent event; + while (1) { + /* FIXME: I have no idea how inefficient this is */ + if (XCheckIfEvent(clip->renderdata->dpy, &event, &check_window, (XPointer)&window)) { + switch (ctrlsel_receive(context, &event)) { + case CTRLSEL_RECEIVED: + goto done; + case CTRLSEL_ERROR: + fprintf(stderr, "WARNING: Could not get selection.\n"); + ctrlsel_cancel(context); + return NULL; + default: + continue; + } + } + clock_gettime(CLOCK_MONOTONIC, &now); + ltk_timespecsub(&now, &start, &elapsed); + /* Timeout if it takes too long. When that happens, become the selection owner to + avoid further timeouts in the future (I think I copied this behavior from SDL). */ + /* FIXME: configure timeout */ + if (elapsed.tv_sec > 0) { + if (primary) + ltk_clipboard_set_primary_text(clip, ""); + else + ltk_clipboard_set_clipboard_text(clip, ""); + return NULL; + } + ltk_timespecsub(&now, &last, &elapsed); + /* FIXME: configure nanoseconds */ + if (elapsed.tv_sec == 0 && elapsed.tv_nsec < 20000000) { + sleep_time.tv_nsec = 20000000 - elapsed.tv_nsec; + nanosleep(&sleep_time, NULL); + } + last = now; + } + return NULL; +done: + /* FIXME: this is a bit ugly because it fiddles around with txtbuf internals */ + free(clip->rbuf->text); + clip->rbuf->cap = clip->rbuf->len = clip->rtarget.bufsize; + /* FIXME: again weird conversion between char and unsigned char */ + clip->rbuf->text = (char *)clip->rtarget.buffer; + clip->rtarget.buffer = NULL; /* important so ctrlsel_cancel doesn't free it */ + ctrlsel_cancel(context); + return clip->rbuf; +} + +txtbuf * +ltk_clipboard_get_clipboard_text(ltk_clipboard *clip) { + Atom clip_atom; + clip_atom = XInternAtom(clip->renderdata->dpy, "CLIPBOARD", False); + Window window = get_clipboard_window(clip); + Window owner = XGetSelectionOwner(clip->renderdata->dpy, clip_atom); + if (owner == None) { + return NULL; + } else if (owner == window) { + return clip->clipboard; + } else { + return get_text(clip, 0); + } +} + +txtbuf * +ltk_clipboard_get_primary_text(ltk_clipboard *clip) { + Window window = get_clipboard_window(clip); + Window owner = XGetSelectionOwner(clip->renderdata->dpy, XA_PRIMARY); + if (owner == None) { + return NULL; + } else if (owner == window) { + return clip->primary; + } else { + return get_text(clip, 1); + } +} diff --git a/src/clipboard_xlib.h b/src/clipboard_xlib.h @@ -0,0 +1,11 @@ +#ifndef LTK_CLIPBOARD_XLIB_H +#define LTK_CLIPBOARD_XLIB_H + +#include <X11/Xlib.h> +#include "clipboard.h" +#include "txtbuf.h" + +/* 1 means the event was used by the clipboard, 0 means it wasn't */ +int ltk_clipboard_filter_event(ltk_clipboard *clip, XEvent *e); + +#endif /* LTK_CLIPBOARD_XLIB_H */ diff --git a/src/ctrlsel.c b/src/ctrlsel.c @@ -0,0 +1,1645 @@ +/* + * MIT/X Consortium License + * + * © 2022-2023 Lucas de Sena <lucas at seninha dot org> + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include <stdlib.h> +#include <string.h> + +#include <X11/Xlib.h> +#include <X11/Xatom.h> +#include <X11/keysym.h> +#include <X11/cursorfont.h> +#include <X11/Xcursor/Xcursor.h> + +#include "ctrlsel.h" + +#define _TIMESTAMP_PROP "_TIMESTAMP_PROP" +#define TIMESTAMP "TIMESTAMP" +#define ATOM_PAIR "ATOM_PAIR" +#define MULTIPLE "MULTIPLE" +#define MANAGER "MANAGER" +#define TARGETS "TARGETS" +#define INCR "INCR" +#define SELDEFSIZE 0x4000 +#define FLAG(f, b) (((f) & (b)) == (b)) +#define MOTION_TIME 32 +#define DND_DISTANCE 8 /* distance from pointer to dnd miniwindow */ +#define XDND_VERSION 5 /* XDND protocol version */ +#define NCLIENTMSG_DATA 5 /* number of members on a the .data.l[] array of a XClientMessageEvent */ + +enum { + CONTENT_INCR, + CONTENT_ZERO, + CONTENT_ERROR, + CONTENT_SUCCESS, +}; + +enum { + PAIR_TARGET, + PAIR_PROPERTY, + PAIR_LAST +}; + +enum { + /* xdnd window properties */ + XDND_AWARE, + + /* xdnd selections */ + XDND_SELECTION, + + /* xdnd client messages */ + XDND_ENTER, + XDND_POSITION, + XDND_STATUS, + XDND_LEAVE, + XDND_DROP, + XDND_FINISHED, + + /* xdnd actions */ + XDND_ACTION_COPY, + XDND_ACTION_MOVE, + XDND_ACTION_LINK, + XDND_ACTION_ASK, + XDND_ACTION_PRIVATE, + + XDND_ATOM_LAST, +}; + +enum { + CURSOR_TARGET, + CURSOR_PIRATE, + CURSOR_DRAG, + CURSOR_COPY, + CURSOR_MOVE, + CURSOR_LINK, + CURSOR_NODROP, + CURSOR_LAST, +}; + +struct Transfer { + /* + * When a client request the clipboard but its content is too + * large, we perform incremental transfer. We keep track of + * each incremental transfer in a list of transfers. + */ + struct Transfer *prev, *next; + struct CtrlSelTarget *target; + Window requestor; + Atom property; + unsigned long size; /* how much have we transferred */ +}; + +struct PredArg { + CtrlSelContext *context; + Window window; + Atom message_type; +}; + +struct CtrlSelContext { + Display *display; + Window window; + Atom selection; + Time time; + unsigned long ntargets; + struct CtrlSelTarget *targets; + + /* + * Items below are used internally to keep track of any + * incremental transference in progress. + */ + unsigned long selmaxsize; + unsigned long ndone; + void *transfers; + + /* + * Items below are used internally for drag-and-dropping. + */ + Window dndwindow; + unsigned int dndactions, dndresult; +}; + +static char *atomnames[XDND_ATOM_LAST] = { + [XDND_AWARE] = "XdndAware", + [XDND_SELECTION] = "XdndSelection", + [XDND_ENTER] = "XdndEnter", + [XDND_POSITION] = "XdndPosition", + [XDND_STATUS] = "XdndStatus", + [XDND_LEAVE] = "XdndLeave", + [XDND_DROP] = "XdndDrop", + [XDND_FINISHED] = "XdndFinished", + [XDND_ACTION_COPY] = "XdndActionCopy", + [XDND_ACTION_MOVE] = "XdndActionMove", + [XDND_ACTION_LINK] = "XdndActionLink", + [XDND_ACTION_ASK] = "XdndActionAsk", + [XDND_ACTION_PRIVATE] = "XdndActionPrivate", +}; + +static int +between(int x, int y, int x0, int y0, int w0, int h0) +{ + return x >= x0 && x < x0 + w0 && y >= y0 && y < y0 + h0; +} + +static void +clientmsg(Display *dpy, Window win, Atom atom, long d[5]) +{ + XEvent ev; + + ev.xclient.type = ClientMessage; + ev.xclient.display = dpy; + ev.xclient.serial = 0; + ev.xclient.send_event = True; + ev.xclient.message_type = atom; + ev.xclient.window = win; + ev.xclient.format = 32; + ev.xclient.data.l[0] = d[0]; + ev.xclient.data.l[1] = d[1]; + ev.xclient.data.l[2] = d[2]; + ev.xclient.data.l[3] = d[3]; + ev.xclient.data.l[4] = d[4]; + (void)XSendEvent(dpy, win, False, 0x0, &ev); +} + +static unsigned long +getselmaxsize(Display *display) +{ + unsigned long n; + + if ((n = XExtendedMaxRequestSize(display)) > 0) + return n; + if ((n = XMaxRequestSize(display)) > 0) + return n; + return SELDEFSIZE; +} + +static int +getservertime(Display *display, Time *time) +{ + XEvent xev; + Window window; + Atom timeprop; + + /* + * According to ICCCM, a client wishing to acquire ownership of + * a selection should set the specfied time to some time between + * the current last-change time of the selection concerned and + * the current server time. + * + * Those clients should not set the time value to `CurrentTime`, + * because if they do so, they have no way of finding when they + * gained ownership of the selection. + * + * In the case that an event triggers the acquisition of the + * selection, this time value can be obtained from the event + * itself. + * + * In the case that the client must unconditionally acquire the + * ownership of a selection (which is our case), a zero-length + * append to a property is a way to obtain a timestamp for this + * purpose. The timestamp is in the corresponding + * `PropertyNotify` event. + */ + + if (time != CurrentTime) + return 1; + timeprop = XInternAtom(display, _TIMESTAMP_PROP, False); + if (timeprop == None) + goto error; + window = XCreateWindow( + display, + DefaultRootWindow(display), + 0, 0, 1, 1, 0, + CopyFromParent, CopyFromParent, CopyFromParent, + CWEventMask, + &(XSetWindowAttributes){ + .event_mask = PropertyChangeMask, + } + ); + if (window == None) + goto error; + XChangeProperty( + display, window, + timeprop, timeprop, + 8L, PropModeAppend, NULL, 0 + ); + while (!XWindowEvent(display, window, PropertyChangeMask, &xev)) { + if (xev.type == PropertyNotify && + xev.xproperty.window == window && + xev.xproperty.atom == timeprop) { + *time = xev.xproperty.time; + break; + } + } + (void)XDestroyWindow(display, window); + return 1; +error: + return 0; +} + +static int +nbytes(int format) +{ + switch (format) { + default: return sizeof(char); + case 16: return sizeof(short); + case 32: return sizeof(long); + } +} + +static int +getcontent(struct CtrlSelTarget *target, Display *display, Window window, Atom property) +{ + unsigned char *p, *q; + unsigned long len, addsize, size; + unsigned long dl; /* dummy variable */ + int status; + Atom incr; + + incr = XInternAtom(display, INCR, False), + status = XGetWindowProperty( + display, + window, + property, + 0L, 0x1FFFFFFF, + True, + AnyPropertyType, + &target->type, + &target->format, + &len, &dl, &p + ); + if (target->format != 32 && target->format != 16) + target->format = 8; + if (target->type == incr) { + XFree(p); + return CONTENT_INCR; + } + if (len == 0) { + XFree(p); + return CONTENT_ZERO; + } + if (status != Success) { + XFree(p); + return CONTENT_ERROR; + } + if (p == NULL) { + XFree(p); + return CONTENT_ERROR; + } + addsize = len * nbytes(target->format); + size = addsize; + if (target->buffer != NULL) { + /* append buffer */ + size += target->bufsize; + if ((q = realloc(target->buffer, size + 1)) == NULL) { + XFree(p); + return CONTENT_ERROR; + } + memcpy(q + target->bufsize, p, addsize); + target->buffer = q; + target->bufsize = size; + target->nitems += len; + } else { + /* new buffer */ + if ((q = malloc(size + 1)) == NULL) { + XFree(p); + return CONTENT_ERROR; + } + memcpy(q, p, addsize); + target->buffer = q; + target->bufsize = size; + target->nitems = len; + } + target->buffer[size] = '\0'; + XFree(p); + return CONTENT_SUCCESS; +} + +static void +deltransfer(CtrlSelContext *context, struct Transfer *transfer) +{ + if (transfer->prev != NULL) { + transfer->prev->next = transfer->next; + } else { + context->transfers = transfer->next; + } + if (transfer->next != NULL) { + transfer->next->prev = transfer->prev; + } +} + +static void +freetransferences(CtrlSelContext *context) +{ + struct Transfer *transfer; + + while (context->transfers != NULL) { + transfer = (struct Transfer *)context->transfers; + context->transfers = ((struct Transfer *)context->transfers)->next; + XDeleteProperty( + context->display, + transfer->requestor, + transfer->property + ); + free(transfer); + } + context->transfers = NULL; +} + +static void +freebuffers(CtrlSelContext *context) +{ + unsigned long i; + + for (i = 0; i < context->ntargets; i++) { + free(context->targets[i].buffer); + context->targets[i].buffer = NULL; + context->targets[i].nitems = 0; + context->targets[i].bufsize = 0; + } +} + +static unsigned long +getatomsprop(Display *display, Window window, Atom property, Atom type, Atom **atoms) +{ + unsigned char *p; + unsigned long len; + unsigned long dl; /* dummy variable */ + int format; + Atom gottype; + unsigned long size; + int success; + + success = XGetWindowProperty( + display, + window, + property, + 0L, 0x1FFFFFFF, + False, + type, &gottype, + &format, &len, + &dl, &p + ); + if (success != Success || len == 0 || p == NULL || format != 32) + goto error; + if (type != AnyPropertyType && type != gottype) + goto error; + size = len * sizeof(**atoms); + if ((*atoms = malloc(size)) == NULL) + goto error; + memcpy(*atoms, p, size); + XFree(p); + return len; +error: + XFree(p); + *atoms = NULL; + return 0; +} + +static int +newtransfer(CtrlSelContext *context, struct CtrlSelTarget *target, Window requestor, Atom property) +{ + struct Transfer *transfer; + + transfer = malloc(sizeof(*transfer)); + if (transfer == NULL) + return 0; + *transfer = (struct Transfer){ + .prev = NULL, + .next = (struct Transfer *)context->transfers, + .requestor = requestor, + .property = property, + .target = target, + .size = 0, + }; + if (context->transfers != NULL) + ((struct Transfer *)context->transfers)->prev = transfer; + context->transfers = transfer; + return 1; +} + +static Bool +convert(CtrlSelContext *context, Window requestor, Atom target, Atom property) +{ + Atom multiple, timestamp, targets, incr; + Atom *supported; + unsigned long i; + int nsupported; + + incr = XInternAtom(context->display, INCR, False); + targets = XInternAtom(context->display, TARGETS, False); + multiple = XInternAtom(context->display, MULTIPLE, False); + timestamp = XInternAtom(context->display, TIMESTAMP, False); + if (target == multiple) { + /* A MULTIPLE should be handled when processing a + * SelectionRequest event. We do not support nested + * MULTIPLE targets. + */ + return False; + } + if (target == timestamp) { + /* + * According to ICCCM, to avoid some race conditions, it + * is important that requestors be able to discover the + * timestamp the owner used to acquire ownership. + * Requestors do that by requesting selection owners to + * convert the `TIMESTAMP` target. Selection owners + * must return the timestamp as an `XA_INTEGER`. + */ + XChangeProperty( + context->display, + requestor, + property, + XA_INTEGER, 32, + PropModeReplace, + (unsigned char *)&context->time, + 1 + ); + return True; + } + if (target == targets) { + /* + * According to ICCCM, when requested for the `TARGETS` + * target, the selection owner should return a list of + * atoms representing the targets for which an attempt + * to convert the selection will (hopefully) succeed. + */ + nsupported = context->ntargets + 2; /* +2 for MULTIPLE + TIMESTAMP */ + if ((supported = calloc(nsupported, sizeof(*supported))) == NULL) + return False; + for (i = 0; i < context->ntargets; i++) { + supported[i] = context->targets[i].target; + } + supported[i++] = multiple; + supported[i++] = timestamp; + XChangeProperty( + context->display, + requestor, + property, + XA_ATOM, 32, + PropModeReplace, + (unsigned char *)supported, + nsupported + ); + free(supported); + return True; + } + for (i = 0; i < context->ntargets; i++) { + if (target == context->targets[i].target) + goto found; + } + return False; +found: + if (context->targets[i].bufsize > context->selmaxsize) { + XSelectInput( + context->display, + requestor, + StructureNotifyMask | PropertyChangeMask + ); + XChangeProperty( + context->display, + requestor, + property, + incr, + 32L, + PropModeReplace, + (unsigned char *)context->targets[i].buffer, + 1 + ); + newtransfer(context, &context->targets[i], requestor, property); + } else { + XChangeProperty( + context->display, + requestor, + property, + target, + context->targets[i].format, + PropModeReplace, + context->targets[i].buffer, + context->targets[i].nitems + ); + } + return True; +} + +static int +request(CtrlSelContext *context) +{ + Atom multiple, atom_pair; + Atom *pairs; + unsigned long i, size; + + for (i = 0; i < context->ntargets; i++) { + context->targets[i].nitems = 0; + context->targets[i].bufsize = 0; + context->targets[i].buffer = NULL; + } + if (context->ntargets == 1) { + (void)XConvertSelection( + context->display, + context->selection, + context->targets[0].target, + context->targets[0].target, + context->window, + context->time + ); + } else if (context->ntargets > 1) { + multiple = XInternAtom(context->display, MULTIPLE, False); + atom_pair = XInternAtom(context->display, ATOM_PAIR, False); + size = 2 * context->ntargets; + pairs = calloc(size, sizeof(*pairs)); + if (pairs == NULL) + return 0; + for (i = 0; i < context->ntargets; i++) { + pairs[i * 2 + 0] = context->targets[i].target; + pairs[i * 2 + 1] = context->targets[i].target; + } + (void)XChangeProperty( + context->display, + context->window, + multiple, + atom_pair, + 32, + PropModeReplace, + (unsigned char *)pairs, + size + ); + (void)XConvertSelection( + context->display, + context->selection, + multiple, + multiple, + context->window, + context->time + ); + free(pairs); + } + return 1; +} + +void +ctrlsel_filltarget( + Atom target, + Atom type, + int format, + unsigned char *buffer, + unsigned long size, + struct CtrlSelTarget *fill +) { + if (fill == NULL) + return; + if (format != 32 && format != 16) + format = 8; + *fill = (struct CtrlSelTarget){ + .target = target, + .type = type, + .action = None, + .format = format, + .nitems = size / nbytes(format), + .buffer = buffer, + .bufsize = size, + }; +} + +CtrlSelContext * +ctrlsel_request( + Display *display, + Window window, + Atom selection, + Time time, + struct CtrlSelTarget targets[], + unsigned long ntargets +) { + CtrlSelContext *context; + + if (!getservertime(display, &time)) + return NULL; + if ((context = malloc(sizeof(*context))) == NULL) + return NULL; + *context = (CtrlSelContext){ + .display = display, + .window = window, + .selection = selection, + .time = time, + .targets = targets, + .ntargets = ntargets, + .selmaxsize = getselmaxsize(display), + .ndone = 0, + .transfers = NULL, + .dndwindow = None, + .dndactions = 0x00, + .dndresult = 0x00, + }; + if (ntargets == 0) + return context; + if (request(context)) + return context; + free(context); + return NULL; +} + +CtrlSelContext * +ctrlsel_setowner( + Display *display, + Window window, + Atom selection, + Time time, + int ismanager, + struct CtrlSelTarget targets[], + unsigned long ntargets +) { + CtrlSelContext *context; + Window root; + + root = DefaultRootWindow(display); + if (!getservertime(display, &time)) + return NULL; + if ((context = malloc(sizeof(*context))) == NULL) + return NULL; + *context = (CtrlSelContext){ + .display = display, + .window = window, + .selection = selection, + .time = time, + .targets = targets, + .ntargets = ntargets, + .selmaxsize = getselmaxsize(display), + .ndone = 0, + .transfers = NULL, + .dndwindow = None, + .dndactions = 0x00, + .dndresult = 0x00, + }; + (void)XSetSelectionOwner(display, selection, window, time); + if (XGetSelectionOwner(display, selection) != window) { + free(context); + return NULL; + } + if (!ismanager) + return context; + + /* + * According to ICCCM, a manager client (that is, a client + * responsible for managing shared resources) should take + * ownership of an appropriate selection. + * + * Immediately after a manager successfully acquires ownership + * of a manager selection, it should announce its arrival by + * sending a `ClientMessage` event. (That is necessary for + * clients to be able to know when a specific manager has + * started: any client that wish to do so should select for + * `StructureNotify` on the root window and should watch for + * the appropriate `MANAGER` `ClientMessage`). + */ + (void)XSendEvent( + display, + root, + False, + StructureNotifyMask, + (XEvent *)&(XClientMessageEvent){ + .type = ClientMessage, + .window = root, + .message_type = XInternAtom(display, MANAGER, False), + .format = 32, + .data.l[0] = time, /* timestamp */ + .data.l[1] = selection, /* manager selection atom */ + .data.l[2] = window, /* window owning the selection */ + .data.l[3] = 0, /* manager-specific data */ + .data.l[4] = 0, /* manager-specific data */ + } + ); + return context; +} + +static int +receiveinit(CtrlSelContext *context, XEvent *xev) +{ + struct CtrlSelTarget *targetp; + XSelectionEvent *xselev; + Atom multiple, atom_pair; + Atom *pairs; + Atom pair[PAIR_LAST]; + unsigned long j, natoms; + unsigned long i; + int status, success; + + multiple = XInternAtom(context->display, MULTIPLE, False); + atom_pair = XInternAtom(context->display, ATOM_PAIR, False); + xselev = &xev->xselection; + if (xselev->selection != context->selection) + return CTRLSEL_NONE; + if (xselev->requestor != context->window) + return CTRLSEL_NONE; + if (xselev->property == None) + return CTRLSEL_ERROR; + if (xselev->target == multiple) { + natoms = getatomsprop( + xselev->display, + xselev->requestor, + xselev->property, + atom_pair, + &pairs + ); + if (natoms == 0 || pairs == NULL) { + free(pairs); + return CTRLSEL_ERROR; + } + } else { + pair[PAIR_TARGET] = xselev->target; + pair[PAIR_PROPERTY] = xselev->property; + pairs = pair; + natoms = 2; + } + success = 1; + for (j = 0; j < natoms; j += 2) { + targetp = NULL; + for (i = 0; i < context->ntargets; i++) { + if (pairs[j + PAIR_TARGET] == context->targets[i].target) { + targetp = &context->targets[i]; + break; + } + } + if (pairs[j + PAIR_PROPERTY] == None) + pairs[j + PAIR_PROPERTY] = pairs[j + PAIR_TARGET]; + if (targetp == NULL) { + success = 0; + continue; + } + status = getcontent( + targetp, + xselev->display, + xselev->requestor, + pairs[j + PAIR_PROPERTY] + ); + switch (status) { + case CONTENT_ERROR: + success = 0; + break; + case CONTENT_SUCCESS: + /* fallthrough */ + case CONTENT_ZERO: + context->ndone++; + break; + case CONTENT_INCR: + if (!newtransfer(context, targetp, xselev->requestor, pairs[j + PAIR_PROPERTY])) + success = 0; + break; + } + } + if (xselev->target == multiple) + free(pairs); + return success ? CTRLSEL_INTERNAL : CTRLSEL_ERROR; +} + +static int +receiveincr(CtrlSelContext *context, XEvent *xev) +{ + struct Transfer *transfer; + XPropertyEvent *xpropev; + int status; + + xpropev = &xev->xproperty; + if (xpropev->state != PropertyNewValue) + return CTRLSEL_NONE; + if (xpropev->window != context->window) + return CTRLSEL_NONE; + for (transfer = (struct Transfer *)context->transfers; transfer != NULL; transfer = transfer->next) + if (transfer->property == xpropev->atom) + goto found; + return CTRLSEL_NONE; +found: + status = getcontent( + transfer->target, + xpropev->display, + xpropev->window, + xpropev->atom + ); + switch (status) { + case CONTENT_ERROR: + case CONTENT_INCR: + return CTRLSEL_ERROR; + case CONTENT_SUCCESS: + return CTRLSEL_INTERNAL; + case CONTENT_ZERO: + context->ndone++; + deltransfer(context, transfer); + break; + } + return CTRLSEL_INTERNAL; +} + +int +ctrlsel_receive(CtrlSelContext *context, XEvent *xev) +{ + int status; + + if (xev->type == SelectionNotify) + status = receiveinit(context, xev); + else if (xev->type == PropertyNotify) + status = receiveincr(context, xev); + else + return CTRLSEL_NONE; + if (status == CTRLSEL_INTERNAL) { + if (context->ndone >= context->ntargets) { + status = CTRLSEL_RECEIVED; + goto done; + } + } else if (status == CTRLSEL_ERROR) { + freebuffers(context); + freetransferences(context); + } +done: + if (status == CTRLSEL_RECEIVED) + freetransferences(context); + return status; +} + +static int +sendinit(CtrlSelContext *context, XEvent *xev) +{ + XSelectionRequestEvent *xreqev; + XSelectionEvent xselev; + unsigned long natoms, i; + Atom *pairs; + Atom pair[PAIR_LAST]; + Atom multiple, atom_pair; + Bool success; + + xreqev = &xev->xselectionrequest; + if (xreqev->selection != context->selection) + return CTRLSEL_NONE; + multiple = XInternAtom(context->display, MULTIPLE, False); + atom_pair = XInternAtom(context->display, ATOM_PAIR, False); + xselev = (XSelectionEvent){ + .type = SelectionNotify, + .display = xreqev->display, + .requestor = xreqev->requestor, + .selection = xreqev->selection, + .time = xreqev->time, + .target = xreqev->target, + .property = None, + }; + if (xreqev->time != CurrentTime && xreqev->time < context->time) { + /* + * According to ICCCM, the selection owner + * should compare the timestamp with the period + * it has owned the selection and, if the time + * is outside, refuse the `SelectionRequest` by + * sending the requestor window a + * `SelectionNotify` event with the property set + * to `None` (by means of a `SendEvent` request + * with an empty event mask). + */ + goto done; + } + if (xreqev->target == multiple) { + if (xreqev->property == None) + goto done; + natoms = getatomsprop( + xreqev->display, + xreqev->requestor, + xreqev->property, + atom_pair, + &pairs + ); + } else { + pair[PAIR_TARGET] = xreqev->target; + pair[PAIR_PROPERTY] = xreqev->property; + pairs = pair; + natoms = 2; + } + success = True; + for (i = 0; i < natoms; i += 2) { + if (!convert(context, xreqev->requestor, + pairs[i + PAIR_TARGET], + pairs[i + PAIR_PROPERTY])) { + success = False; + pairs[i + PAIR_PROPERTY] = None; + } + } + if (xreqev->target == multiple) { + XChangeProperty( + xreqev->display, + xreqev->requestor, + xreqev->property, + atom_pair, + 32, PropModeReplace, + (unsigned char *)pairs, + natoms + ); + free(pairs); + } + if (success) { + if (xreqev->property == None) { + xselev.property = xreqev->target; + } else { + xselev.property = xreqev->property; + } + } +done: + XSendEvent( + xreqev->display, + xreqev->requestor, + False, + NoEventMask, + (XEvent *)&xselev + ); + return CTRLSEL_INTERNAL; +} + +static int +sendlost(CtrlSelContext *context, XEvent *xev) +{ + XSelectionClearEvent *xclearev; + + xclearev = &xev->xselectionclear; + if (xclearev->selection == context->selection && + xclearev->window == context->window) { + return CTRLSEL_LOST; + } + return CTRLSEL_NONE; +} + +static int +senddestroy(CtrlSelContext *context, XEvent *xev) +{ + struct Transfer *transfer; + XDestroyWindowEvent *xdestroyev; + + xdestroyev = &xev->xdestroywindow; + for (transfer = context->transfers; transfer != NULL; transfer = transfer->next) + if (transfer->requestor == xdestroyev->window) + deltransfer(context, transfer); + return CTRLSEL_NONE; +} + +static int +sendincr(CtrlSelContext *context, XEvent *xev) +{ + struct Transfer *transfer; + XPropertyEvent *xpropev; + unsigned long size; + + xpropev = &xev->xproperty; + if (xpropev->state != PropertyDelete) + return CTRLSEL_NONE; + for (transfer = context->transfers; transfer != NULL; transfer = transfer->next) + if (transfer->property == xpropev->atom && + transfer->requestor == xpropev->window) + goto found; + return CTRLSEL_NONE; +found: + if (transfer->size >= transfer->target->bufsize) + transfer->size = transfer->target->bufsize; + size = transfer->target->bufsize - transfer->size; + if (size > context->selmaxsize) + size = context->selmaxsize; + XChangeProperty( + xpropev->display, + xpropev->window, + xpropev->atom, + transfer->target->target, + transfer->target->format, + PropModeReplace, + transfer->target->buffer + transfer->size, + size / nbytes(transfer->target->format) + ); + if (transfer->size >= transfer->target->bufsize) { + deltransfer(context, transfer); + } else { + transfer->size += size; + } + return CTRLSEL_INTERNAL; +} + +int +ctrlsel_send(CtrlSelContext *context, XEvent *xev) +{ + int status; + + if (xev->type == SelectionRequest) + status = sendinit(context, xev); + else if (xev->type == SelectionClear) + status = sendlost(context, xev); + else if (xev->type == DestroyNotify) + status = senddestroy(context, xev); + else if (xev->type == PropertyNotify) + status = sendincr(context, xev); + else + return CTRLSEL_NONE; + if (status == CTRLSEL_LOST || status == CTRLSEL_ERROR) { + status = CTRLSEL_LOST; + freetransferences(context); + } + return status; +} + +void +ctrlsel_cancel(CtrlSelContext *context) +{ + if (context == NULL) + return; + freebuffers(context); + freetransferences(context); + free(context); +} + +void +ctrlsel_disown(CtrlSelContext *context) +{ + if (context == NULL) + return; + freetransferences(context); + free(context); +} + +static Bool +dndpred(Display *display, XEvent *event, XPointer p) +{ + struct PredArg *arg; + struct Transfer *transfer; + + arg = (struct PredArg *)p; + switch (event->type) { + case KeyPress: + case KeyRelease: + if (event->xkey.display == display && + event->xkey.window == arg->window) + return True; + break; + case ButtonPress: + case ButtonRelease: + if (event->xbutton.display == display && + event->xbutton.window == arg->window) + return True; + break; + case MotionNotify: + if (event->xmotion.display == display && + event->xmotion.window == arg->window) + return True; + break; + case DestroyNotify: + if (event->xdestroywindow.display == display && + event->xdestroywindow.window == arg->window) + return True; + break; + case UnmapNotify: + if (event->xunmap.display == display && + event->xunmap.window == arg->window) + return True; + break; + case SelectionClear: + if (event->xselectionclear.display == display && + event->xselectionclear.window == arg->window) + return True; + break; + case SelectionRequest: + if (event->xselectionrequest.display == display && + event->xselectionrequest.owner == arg->window) + return True; + break; + case ClientMessage: + if (event->xclient.display == display && + event->xclient.window == arg->window && + event->xclient.message_type == arg->message_type) + return True; + break; + case PropertyNotify: + if (event->xproperty.display != display || + event->xproperty.state != PropertyDelete) + return False; + for (transfer = arg->context->transfers; + transfer != NULL; + transfer = transfer->next) { + if (transfer->property == event->xproperty.atom && + transfer->requestor == event->xproperty.window) { + return True; + } + } + break; + default: + break; + } + return False; +} + +#define SOME(a, b, c) ((a) != None ? (a) : ((b) != None ? (b) : (c))) + +static Cursor +getcursor(Cursor cursors[CURSOR_LAST], int type) +{ + switch (type) { + case CURSOR_TARGET: + case CURSOR_DRAG: + return SOME(cursors[CURSOR_DRAG], cursors[CURSOR_TARGET], None); + case CURSOR_PIRATE: + case CURSOR_NODROP: + return SOME(cursors[CURSOR_NODROP], cursors[CURSOR_PIRATE], None); + case CURSOR_COPY: + return SOME(cursors[CURSOR_COPY], cursors[CURSOR_DRAG], cursors[CURSOR_TARGET]); + case CURSOR_MOVE: + return SOME(cursors[CURSOR_MOVE], cursors[CURSOR_DRAG], cursors[CURSOR_TARGET]); + case CURSOR_LINK: + return SOME(cursors[CURSOR_LINK], cursors[CURSOR_DRAG], cursors[CURSOR_TARGET]); + }; + return None; +} + +static void +initcursors(Display *display, Cursor cursors[CURSOR_LAST]) +{ + cursors[CURSOR_TARGET] = XCreateFontCursor(display, XC_target); + cursors[CURSOR_PIRATE] = XCreateFontCursor(display, XC_pirate); + cursors[CURSOR_DRAG] = XcursorLibraryLoadCursor(display, "dnd-none"); + cursors[CURSOR_COPY] = XcursorLibraryLoadCursor(display, "dnd-copy"); + cursors[CURSOR_MOVE] = XcursorLibraryLoadCursor(display, "dnd-move"); + cursors[CURSOR_LINK] = XcursorLibraryLoadCursor(display, "dnd-link"); + cursors[CURSOR_NODROP] = XcursorLibraryLoadCursor(display, "forbidden"); +} + +static void +freecursors(Display *display, Cursor cursors[CURSOR_LAST]) +{ + int i; + + for (i = 0; i < CURSOR_LAST; i++) { + if (cursors[i] != None) { + XFreeCursor(display, cursors[i]); + } + } +} + +static int +querypointer(Display *display, Window window, int *retx, int *rety, Window *retwin) +{ + Window root, child; + unsigned int mask; + int rootx, rooty; + int x, y; + int retval; + + retval = XQueryPointer( + display, + window, + &root, &child, + &rootx, &rooty, + &x, &y, + &mask + ); + if (retwin != NULL) + *retwin = child; + if (retx != NULL) + *retx = x; + if (rety != NULL) + *rety = y; + return retval; +} + +static Window +getdndwindowbelow(Display *display, Window root, Atom aware, Atom *version) +{ + Atom *p; + Window window; + + /* + * Query pointer location and return the window below it, + * and the version of the XDND protocol it uses. + */ + *version = None; + window = root; + p = NULL; + while (querypointer(display, window, NULL, NULL, &window)) { + if (window == None) + break; + p = NULL; + if (getatomsprop(display, window, aware, AnyPropertyType, &p) > 0) { + *version = *p; + XFree(p); + return window; + } + } + XFree(p); + return None; +} + +CtrlSelContext * +ctrlsel_dndwatch( + Display *display, + Window window, + unsigned int actions, + struct CtrlSelTarget targets[], + unsigned long ntargets +) { + CtrlSelContext *context; + Atom version = XDND_VERSION; /* yes, version is an Atom */ + Atom xdndaware, xdndselection; + + xdndaware = XInternAtom(display, atomnames[XDND_AWARE], False); + if (xdndaware == None) + return NULL; + xdndselection = XInternAtom(display, atomnames[XDND_SELECTION], False); + if (xdndselection == None) + return NULL; + if ((context = malloc(sizeof(*context))) == NULL) + return NULL; + *context = (CtrlSelContext){ + .display = display, + .window = window, + .selection = xdndselection, + .time = CurrentTime, + .targets = targets, + .ntargets = ntargets, + .selmaxsize = getselmaxsize(display), + .ndone = 0, + .transfers = NULL, + .dndwindow = None, + .dndactions = actions, + .dndresult = 0x00, + }; + (void)XChangeProperty( + display, + window, + xdndaware, + XA_ATOM, 32, + PropModeReplace, + (unsigned char *)&version, + 1 + ); + return context; +} + +static void +finishdrop(CtrlSelContext *context) +{ + long d[NCLIENTMSG_DATA]; + unsigned long i; + Atom finished; + + if (context->dndwindow == None) + return; + finished = XInternAtom(context->display, atomnames[XDND_FINISHED], False); + if (finished == None) + return; + for (i = 0; i < context->ntargets; i++) + context->targets[i].action = context->dndresult; + d[0] = context->window; + d[1] = d[2] = d[3] = d[4] = 0; + clientmsg(context->display, context->dndwindow, finished, d); + context->dndwindow = None; +} + +int +ctrlsel_dndreceive(CtrlSelContext *context, XEvent *event) +{ + Atom atoms[XDND_ATOM_LAST]; + Atom action; + long d[NCLIENTMSG_DATA]; + + if (!XInternAtoms(context->display, atomnames, XDND_ATOM_LAST, False, atoms)) + return CTRLSEL_NONE; + switch (ctrlsel_receive(context, event)) { + case CTRLSEL_RECEIVED: + finishdrop(context); + return CTRLSEL_RECEIVED; + case CTRLSEL_INTERNAL: + case CTRLSEL_ERROR: + return CTRLSEL_INTERNAL; + default: + break; + } + if (event->type != ClientMessage) + return CTRLSEL_NONE; + if (event->xclient.message_type == atoms[XDND_ENTER]) { + context->dndwindow = (Window)event->xclient.data.l[0]; + context->dndresult = 0x00; + } else if (event->xclient.message_type == atoms[XDND_LEAVE]) { + if ((Window)event->xclient.data.l[0] == None || + (Window)event->xclient.data.l[0] != context->dndwindow) + return CTRLSEL_NONE; + context->dndwindow = None; + } else if (event->xclient.message_type == atoms[XDND_DROP]) { + if ((Window)event->xclient.data.l[0] == None || + (Window)event->xclient.data.l[0] != context->dndwindow) + return CTRLSEL_NONE; + context->time = (Time)event->xclient.data.l[2]; + (void)request(context); + } else if (event->xclient.message_type == atoms[XDND_POSITION]) { + if ((Window)event->xclient.data.l[0] == None || + (Window)event->xclient.data.l[0] != context->dndwindow) + return CTRLSEL_NONE; + if (((Atom)event->xclient.data.l[4] == atoms[XDND_ACTION_COPY] && + context->dndactions & CTRLSEL_COPY) || + ((Atom)event->xclient.data.l[4] == atoms[XDND_ACTION_MOVE] && + context->dndactions & CTRLSEL_MOVE) || + ((Atom)event->xclient.data.l[4] == atoms[XDND_ACTION_LINK] && + context->dndactions & CTRLSEL_LINK) || + ((Atom)event->xclient.data.l[4] == atoms[XDND_ACTION_ASK] && + context->dndactions & CTRLSEL_ASK) || + ((Atom)event->xclient.data.l[4] == atoms[XDND_ACTION_PRIVATE] && + context->dndactions & CTRLSEL_PRIVATE)) { + action = (Atom)event->xclient.data.l[4]; + } else { + action = atoms[XDND_ACTION_COPY]; + } + d[0] = context->window; + d[1] = 0x1; + d[2] = 0; /* our rectangle is the entire screen */ + d[3] = 0xFFFFFFFF; /* so we do not get lots of messages */ + d[4] = action; + if (action == atoms[XDND_ACTION_PRIVATE]) + context->dndresult = CTRLSEL_PRIVATE; + else if (action == atoms[XDND_ACTION_ASK]) + context->dndresult = CTRLSEL_ASK; + else if (action == atoms[XDND_ACTION_LINK]) + context->dndresult = CTRLSEL_LINK; + else if (action == atoms[XDND_ACTION_MOVE]) + context->dndresult = CTRLSEL_MOVE; + else + context->dndresult = CTRLSEL_COPY; + clientmsg( + context->display, + (Window)event->xclient.data.l[0], + atoms[XDND_STATUS], + d + ); + } else { + return CTRLSEL_NONE; + } + return CTRLSEL_INTERNAL; +} + +void +ctrlsel_dndclose(CtrlSelContext *context) +{ + if (context == NULL) + return; + finishdrop(context); + freebuffers(context); + freetransferences(context); + free(context); +} + +void +ctrlsel_dnddisown(CtrlSelContext *context) +{ + ctrlsel_disown(context); +} + +int +ctrlsel_dndsend(CtrlSelContext *context, XEvent *event) +{ + Atom finished; + + finished = XInternAtom(context->display, atomnames[XDND_FINISHED], False); + if (event->type == ClientMessage && + event->xclient.message_type == finished && + (Window)event->xclient.data.l[0] == context->dndwindow) { + ctrlsel_dnddisown(context); + return CTRLSEL_SENT; + } + return ctrlsel_send(context, event); +} + +int +ctrlsel_dndown( + Display *display, + Window window, + Window miniature, + Time time, + struct CtrlSelTarget targets[], + unsigned long ntargets, + CtrlSelContext **context_ret +) { + CtrlSelContext *context; + struct PredArg arg; + XWindowAttributes wattr; + XEvent event; + Atom atoms[XDND_ATOM_LAST]; + Cursor cursors[CURSOR_LAST] = { None, None }; + Cursor cursor; + Window lastwin, winbelow; + Atom lastaction, action, version; + long d[NCLIENTMSG_DATA]; + int sendposition, retval, status, inside; + int x, y, w, h; + + *context_ret = NULL; + if (display == NULL || window == None) + return CTRLSEL_ERROR; + if (!XGetWindowAttributes(display, window, &wattr)) + return CTRLSEL_ERROR; + if ((wattr.your_event_mask & StructureNotifyMask) == 0x00) + return CTRLSEL_ERROR; + if (wattr.map_state != IsViewable) + return CTRLSEL_ERROR; + if (!XInternAtoms(display, atomnames, XDND_ATOM_LAST, False, atoms)) + return CTRLSEL_ERROR; + context = ctrlsel_setowner( + display, + window, + atoms[XDND_SELECTION], + time, + 0, + targets, + ntargets + ); + if (context == NULL) + return CTRLSEL_ERROR; + d[0] = window; + sendposition = 1; + x = y = w = h = 0; + retval = CTRLSEL_ERROR; + lastaction = action = None; + lastwin = None; + arg = (struct PredArg){ + .context = context, + .window = window, + .message_type = atoms[XDND_STATUS], + }; + initcursors(display, cursors); + status = XGrabPointer( + display, + window, + True, + ButtonPressMask | ButtonMotionMask | + ButtonReleaseMask | PointerMotionMask, + GrabModeAsync, + GrabModeAsync, + None, + None, + time + ); + if (status != GrabSuccess) + goto done; + status = XGrabKeyboard( + display, + window, + True, + GrabModeAsync, + GrabModeAsync, + time + ); + if (status != GrabSuccess) + goto done; + if (miniature != None) + XMapRaised(display, miniature); + cursor = getcursor(cursors, CURSOR_DRAG); + for (;;) { + (void)XIfEvent(display, &event, &dndpred, (XPointer)&arg); + switch (ctrlsel_send(context, &event)) { + case CTRLSEL_LOST: + retval = CTRLSEL_NONE; + goto done; + case CTRLSEL_INTERNAL: + continue; + default: + break; + } + switch (event.type) { + case KeyPress: + case KeyRelease: + if (event.xkey.keycode != 0 && + event.xkey.keycode == XKeysymToKeycode(display, XK_Escape)) { + retval = CTRLSEL_NONE; + goto done; + } + break; + case ButtonPress: + case ButtonRelease: + if (lastwin == None) { + retval = CTRLSEL_NONE; + } else if (lastwin == window) { + retval = CTRLSEL_DROPSELF; + } else { + retval = CTRLSEL_DROPOTHER; + d[1] = d[3] = d[4] = 0; + d[2] = event.xbutton.time; + clientmsg(display, lastwin, atoms[XDND_DROP], d); + context->dndwindow = lastwin; + } + goto done; + case MotionNotify: + if (event.xmotion.time - time < MOTION_TIME) + break; + if (miniature != None) { + XMoveWindow( + display, + miniature, + event.xmotion.x_root + DND_DISTANCE, + event.xmotion.y_root + DND_DISTANCE + ); + } + inside = between(event.xmotion.x, event.xmotion.y, x, y, w, h); + if ((lastaction != action || sendposition || !inside) + && lastwin != None) { + if (lastaction != None) + d[4] = lastaction; + else if (FLAG(event.xmotion.state, ControlMask|ShiftMask)) + d[4] = atoms[XDND_ACTION_LINK]; + else if (FLAG(event.xmotion.state, ShiftMask)) + d[4] = atoms[XDND_ACTION_MOVE]; + else if (FLAG(event.xmotion.state, ControlMask)) + d[4] = atoms[XDND_ACTION_COPY]; + else + d[4] = atoms[XDND_ACTION_ASK]; + d[1] = 0; + d[2] = event.xmotion.x_root << 16; + d[2] |= event.xmotion.y_root & 0xFFFF; + d[3] = event.xmotion.time; + clientmsg(display, lastwin, atoms[XDND_POSITION], d); + sendposition = 1; + } + time = event.xmotion.time; + lastaction = action; + winbelow = getdndwindowbelow(display, wattr.root, atoms[XDND_AWARE], &version); + if (winbelow == lastwin) + break; + sendposition = 1; + x = y = w = h = 0; + if (version > XDND_VERSION) + version = XDND_VERSION; + if (lastwin != None && lastwin != window) { + d[1] = d[2] = d[3] = d[4] = 0; + clientmsg(display, lastwin, atoms[XDND_LEAVE], d); + } + if (winbelow != None && winbelow != window) { + d[1] = version; + d[1] <<= 24; + d[2] = ntargets > 0 ? targets[0].target : None; + d[3] = ntargets > 1 ? targets[1].target : None; + d[4] = ntargets > 2 ? targets[2].target : None; + clientmsg(display, winbelow, atoms[XDND_ENTER], d); + } + if (winbelow == None) + cursor = getcursor(cursors, CURSOR_NODROP); + else if (FLAG(event.xmotion.state, ControlMask|ShiftMask)) + cursor = getcursor(cursors, CURSOR_LINK); + else if (FLAG(event.xmotion.state, ShiftMask)) + cursor = getcursor(cursors, CURSOR_MOVE); + else if (FLAG(event.xmotion.state, ControlMask)) + cursor = getcursor(cursors, CURSOR_COPY); + else + cursor = getcursor(cursors, CURSOR_DRAG); + XDefineCursor(display, window, cursor); + lastwin = winbelow; + lastaction = action = None; + break; + case ClientMessage: + if ((Window)event.xclient.data.l[0] != lastwin) + break; + sendposition = (event.xclient.data.l[1] & 0x02); + if (event.xclient.data.l[1] & 0x01) + XDefineCursor(display, window, cursor); + else + XDefineCursor(display, window, getcursor(cursors, CURSOR_NODROP)); + x = event.xclient.data.l[2] >> 16; + y = event.xclient.data.l[2] & 0xFFF; + w = event.xclient.data.l[3] >> 16; + h = event.xclient.data.l[3] & 0xFFF; + if ((Atom)event.xclient.data.l[4] != None) + action = (Atom)event.xclient.data.l[4]; + else + action = atoms[XDND_ACTION_COPY]; + break; + case DestroyNotify: + case UnmapNotify: + XPutBackEvent(display, &event); + retval = CTRLSEL_ERROR; + goto done; + default: + break; + } + } +done: + XUndefineCursor(display, window); + if (miniature != None) + XUnmapWindow(display, miniature); + XUngrabPointer(display, CurrentTime); + XUngrabKeyboard(display, CurrentTime); + freecursors(display, cursors); + if (retval != CTRLSEL_DROPOTHER) { + ctrlsel_dnddisown(context); + context = NULL; + } + *context_ret = context; + return retval; +} diff --git a/src/ctrlsel.h b/src/ctrlsel.h @@ -0,0 +1,131 @@ +/* + * MIT/X Consortium License + * + * © 2022-2023 Lucas de Sena <lucas at seninha dot org> + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +/* + * ctrlsel: X11 selection ownership and request helper functions + * + * Refer to the accompanying manual for a description of the interface. + */ +#ifndef _CTRLSEL_H_ +#define _CTRLSEL_H_ + +enum { + CTRLSEL_NONE, + CTRLSEL_INTERNAL, + CTRLSEL_RECEIVED, + CTRLSEL_SENT, + CTRLSEL_DROPSELF, + CTRLSEL_DROPOTHER, + CTRLSEL_ERROR, + CTRLSEL_LOST +}; + +enum { + CTRLSEL_COPY = 0x01, + CTRLSEL_MOVE = 0x02, + CTRLSEL_LINK = 0x04, + CTRLSEL_ASK = 0x08, + CTRLSEL_PRIVATE = 0x10, +}; + +typedef struct CtrlSelContext CtrlSelContext; + +struct CtrlSelTarget { + Atom target; + Atom type; + int format; + unsigned int action; + unsigned long nitems; + unsigned long bufsize; + unsigned char *buffer; +}; + +void +ctrlsel_filltarget( + Atom target, + Atom type, + int format, + unsigned char *buffer, + unsigned long size, + struct CtrlSelTarget *fill +); + +CtrlSelContext * +ctrlsel_request( + Display *display, + Window window, + Atom selection, + Time time, + struct CtrlSelTarget targets[], + unsigned long ntargets +); + +CtrlSelContext * +ctrlsel_setowner( + Display *display, + Window window, + Atom selection, + Time time, + int ismanager, + struct CtrlSelTarget targets[], + unsigned long ntargets +); + +int ctrlsel_receive(struct CtrlSelContext *context, XEvent *event); + +int ctrlsel_send(struct CtrlSelContext *context, XEvent *event); + +void ctrlsel_cancel(struct CtrlSelContext *context); + +void ctrlsel_disown(struct CtrlSelContext *context); + +CtrlSelContext * +ctrlsel_dndwatch( + Display *display, + Window window, + unsigned int actions, + struct CtrlSelTarget targets[], + unsigned long ntargets +); + +int ctrlsel_dndreceive(struct CtrlSelContext *context, XEvent *event); + +void ctrlsel_dndclose(struct CtrlSelContext *context); + +int +ctrlsel_dndown( + Display *display, + Window window, + Window miniature, + Time time, + struct CtrlSelTarget targets[], + unsigned long ntargets, + CtrlSelContext **context +); + +int ctrlsel_dndsend(struct CtrlSelContext *context, XEvent *event); + +void ctrlsel_dnddisown(struct CtrlSelContext *context); + +#endif /* _CTRLSEL_H_ */ diff --git a/src/entry.c b/src/entry.c @@ -67,11 +67,16 @@ static void cursor_left(ltk_entry *entry, ltk_key_event *event); static void cursor_right(ltk_entry *entry, ltk_key_event *event); static void expand_selection_left(ltk_entry *entry, ltk_key_event *event); static void expand_selection_right(ltk_entry *entry, ltk_key_event *event); +static void selection_to_primary(ltk_entry *entry, ltk_key_event *event); +static void selection_to_clipboard(ltk_entry *entry, ltk_key_event *event); +static void paste_primary(ltk_entry *entry, ltk_key_event *event); +static void paste_clipboard(ltk_entry *entry, ltk_key_event *event); static void select_all(ltk_entry *entry, ltk_key_event *event); static void delete_char_backwards(ltk_entry *entry, ltk_key_event *event); static void delete_char_forwards(ltk_entry *entry, ltk_key_event *event); static void recalc_ideal_size(ltk_entry *entry); static void ensure_cursor_shown(ltk_entry *entry); +static void insert_text(ltk_entry *entry, char *text, size_t len); struct key_cb { char *text; @@ -87,7 +92,11 @@ static struct key_cb cb_map[] = { {"delete-char-forwards", &delete_char_forwards}, {"expand-selection-left", &expand_selection_left}, {"expand-selection-right", &expand_selection_right}, + {"paste-clipboard", &paste_clipboard}, + {"paste-primary", &paste_primary}, {"select-all", &select_all}, + {"selection-to-clipboard", &selection_to_clipboard}, + {"selection-to-primary", &selection_to_primary}, }; struct keypress_cfg { @@ -360,6 +369,46 @@ expand_selection(ltk_entry *entry, int dir) { entry->pos = new; wipe_selection(entry); } + selection_to_primary(entry, NULL); +} + +/* FIXME: different programs have different behaviors when they set the selection */ +static void +selection_to_primary(ltk_entry *entry, ltk_key_event *event) { + (void)event; + if (entry->sel_end == entry->sel_start) + return; + txtbuf *primary = ltk_clipboard_get_primary_buffer(entry->widget.window->clipboard); + txtbuf_clear(primary); + txtbuf_appendn(primary, entry->text + entry->sel_start, entry->sel_end - entry->sel_start); + ltk_clipboard_set_primary_selection_owner(entry->widget.window->clipboard); +} + +static void +selection_to_clipboard(ltk_entry *entry, ltk_key_event *event) { + (void)event; + if (entry->sel_end == entry->sel_start) + return; + txtbuf *clip = ltk_clipboard_get_clipboard_buffer(entry->widget.window->clipboard); + txtbuf_clear(clip); + txtbuf_appendn(clip, entry->text + entry->sel_start, entry->sel_end - entry->sel_start); + ltk_clipboard_set_clipboard_selection_owner(entry->widget.window->clipboard); +} + +static void +paste_primary(ltk_entry *entry, ltk_key_event *event) { + (void)event; + txtbuf *buf = ltk_clipboard_get_primary_text(entry->widget.window->clipboard); + if (buf) + insert_text(entry, buf->text, buf->len); +} + +static void +paste_clipboard(ltk_entry *entry, ltk_key_event *event) { + (void)event; + txtbuf *buf = ltk_clipboard_get_clipboard_text(entry->widget.window->clipboard); + if (buf) + insert_text(entry, buf->text, buf->len); } static void @@ -414,6 +463,8 @@ static void select_all(ltk_entry *entry, ltk_key_event *event) { (void)event; set_selection(entry, 0, entry->len); + if (entry->len) + selection_to_primary(entry, NULL); entry->sel_side = 0; } @@ -454,8 +505,16 @@ ensure_cursor_shown(ltk_entry *entry) { /* FIXME: maybe make this a regular key binding with wildcard text like in ledit? */ static void insert_text(ltk_entry *entry, char *text, size_t len) { - /* FIXME: ignore newlines, etc. */ - size_t new_alloc = ideal_array_size(entry->alloc, entry->len + len + 1 - (entry->sel_end - entry->sel_start)); + size_t num = 0; + /* FIXME: this is ugly and there are probably a lot of other + cases that need to be handled */ + /* FIXME: Just ignoring newlines is weird, but what other option is there? */ + for (size_t i = 0; i < len; i++) { + if (text[i] == '\n' || text[i] == '\r') + num++; + } + size_t reallen = len - num; + size_t new_alloc = ideal_array_size(entry->alloc, entry->len + reallen + 1 - (entry->sel_end - entry->sel_start)); if (new_alloc != entry->alloc) { entry->text = ltk_realloc(entry->text, new_alloc); entry->alloc = new_alloc; @@ -463,15 +522,18 @@ insert_text(ltk_entry *entry, char *text, size_t len) { /* FIXME: also need to reset selecting status once mouse selections are supported */ if (entry->sel_start != entry->sel_end) { entry->pos = entry->sel_start; - memmove(entry->text + entry->pos + len, entry->text + entry->sel_end, entry->len - entry->sel_end); - entry->len = entry->len + len - (entry->sel_end - entry->sel_start); + memmove(entry->text + entry->pos + reallen, entry->text + entry->sel_end, entry->len - entry->sel_end); + entry->len = entry->len + reallen - (entry->sel_end - entry->sel_start); wipe_selection(entry); } else { - memmove(entry->text + entry->pos + len, entry->text + entry->pos, entry->len - entry->pos); - entry->len += len; + memmove(entry->text + entry->pos + reallen, entry->text + entry->pos, entry->len - entry->pos); + entry->len += reallen; + } + for (size_t i = 0, j = entry->pos; i < len; i++) { + if (text[i] != '\n' && text[i] != '\r') + entry->text[j++] = text[i]; } - memmove(entry->text + entry->pos, text, len); - entry->pos += len; + entry->pos += reallen; entry->text[entry->len] = '\0'; ltk_text_line_set_text(entry->tl, entry->text, 0); recalc_ideal_size(entry); @@ -489,7 +551,7 @@ ltk_entry_key_press(ltk_widget *self, ltk_key_event *event) { /* FIXME: change naming (rawtext, text, mapped...) */ /* FIXME: a bit weird to mask out shift, but if that isn't done, it would need to be included for all mappings with capital letters */ - if ((b.mods == event->modmask && b.sym == event->sym) || + if ((b.mods == event->modmask && b.sym != LTK_KEY_NONE && b.sym == event->sym) || (b.mods == (event->modmask & ~LTK_MOD_SHIFT) && ((b.text && event->mapped && !strcmp(b.text, event->mapped)) || (b.rawtext && event->text && !strcmp(b.rawtext, event->text))))) { @@ -499,7 +561,7 @@ ltk_entry_key_press(ltk_widget *self, ltk_key_event *event) { return 1; } } - if (event->text) { + if (event->text && (event->modmask & (LTK_MOD_CTRL | LTK_MOD_ALT | LTK_MOD_SUPER)) == 0) { /* FIXME: properly handle everything */ if (event->text[0] == '\n' || event->text[0] == '\r' || event->text[0] == 0x1b) return 0; diff --git a/src/entry.h b/src/entry.h @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 lumidify <nobody@lumidify.org> + * Copyright (c) 2022-2023 lumidify <nobody@lumidify.org> * * Permission to use, copy, modify, and/or distribute this software for any * purpose with or without fee is hereby granted, provided that the above diff --git a/src/event.h b/src/event.h @@ -58,10 +58,11 @@ typedef union { } ltk_event; #include "ltk.h" +#include "clipboard.h" void ltk_events_cleanup(void); /* WARNING: Text returned in key and keyboard events must be copied before calling this function again! */ -int ltk_next_event(ltk_renderdata *renderdata, size_t lang_index, ltk_event *event); +int ltk_next_event(ltk_renderdata *renderdata, ltk_clipboard *clip, size_t lang_index, ltk_event *event); void ltk_generate_keyboard_event(ltk_renderdata *renderdata, ltk_event *event); #endif /* LTK_EVENT_H */ diff --git a/src/event_xlib.c b/src/event_xlib.c @@ -8,6 +8,7 @@ #include "graphics.h" #include "xlib_shared.h" #include "config.h" +#include "clipboard_xlib.h" #define TEXT_INITIAL_SIZE 128 @@ -158,7 +159,7 @@ ltk_generate_keyboard_event(ltk_renderdata *renderdata, ltk_event *event) { 1 means no events pending, 2 means event discarded (need to call again) */ static int -next_event_base(ltk_renderdata *renderdata, size_t lang_index, ltk_event *event) { +next_event_base(ltk_renderdata *renderdata, ltk_clipboard *clip, size_t lang_index, ltk_event *event) { if (next_event_valid) { next_event_valid = 0; *event = (ltk_event){.button = next_event}; @@ -175,6 +176,8 @@ next_event_base(ltk_renderdata *renderdata, size_t lang_index, ltk_event *event) *event = (ltk_event){.type = LTK_UNKNOWN_EVENT}; if (XFilterEvent(&xevent, None)) return 2; + if (clip && ltk_clipboard_filter_event(clip, &xevent)) + return 2; int button = 0; switch (xevent.type) { case ButtonPress: @@ -335,9 +338,9 @@ next_event_base(ltk_renderdata *renderdata, size_t lang_index, ltk_event *event) } int -ltk_next_event(ltk_renderdata *renderdata, size_t lang_index, ltk_event *event) { +ltk_next_event(ltk_renderdata *renderdata, ltk_clipboard *clip, size_t lang_index, ltk_event *event) { int ret = 0; - while ((ret = next_event_base(renderdata, lang_index, event)) == 2) { + while ((ret = next_event_base(renderdata, clip, lang_index, event)) == 2) { /* NOP */ } return ret; diff --git a/src/ltk.h b/src/ltk.h @@ -45,12 +45,14 @@ typedef struct ltk_window_theme ltk_window_theme; #include "widget.h" #include "surface_cache.h" +#include "clipboard.h" #include "event.h" struct ltk_window { ltk_renderdata *renderdata; ltk_surface_cache *surface_cache; ltk_text_context *text_context; + ltk_clipboard *clipboard; ltk_surface *surface; ltk_widget *root_widget; ltk_widget *hover_widget; diff --git a/src/ltkd.c b/src/ltkd.c @@ -412,6 +412,7 @@ ltk_mainloop(ltk_window *window) { int clifd; struct timeval tv, tv_master; tv_master.tv_sec = 0; + /* FIXME: configure this number */ tv_master.tv_usec = 20000; FD_ZERO(&sock_state.rallfds); @@ -450,7 +451,7 @@ ltk_mainloop(ltk_window *window) { /* value of tv doesn't really matter anymore here because the necessary framerate-limiting delay is already done */ wretval = select(sock_state.maxfd + 1, NULL, &wfds, NULL, &tv); - while (!ltk_next_event(window->renderdata, window->cur_kbd, &event)) + while (!ltk_next_event(window->renderdata, window->clipboard, window->cur_kbd, &event)) ltk_handle_event(window, &event); if (rretval > 0 || (sock_write_available && wretval > 0)) { @@ -1040,7 +1041,6 @@ ltk_create_window(const char *title, int x, int y, unsigned int w, unsigned int window->popups_locked = 0; window->cur_kbd = 0; - ltk_renderdata *renderer_create_window(const char *title, int x, int y, unsigned int w, unsigned int h); window->renderdata = renderer_create_window(title, x, y, w, h); /* FIXME: search different directories for config */ char *config_path = ltk_strcat_useful(ltk_dir, "/ltk.cfg"); @@ -1085,12 +1085,14 @@ ltk_create_window(const char *title, int x, int y, unsigned int w, unsigned int window->surface = ltk_surface_from_window(window->renderdata, w, h); window->text_context = ltk_text_context_create(window->renderdata, window->theme->font); + window->clipboard = ltk_clipboard_create(window->renderdata); return window; } static void ltk_destroy_window(ltk_window *window) { + ltk_clipboard_destroy(window->clipboard); ltk_text_context_destroy(window->text_context); if (window->popups) ltk_free(window->popups); diff --git a/src/txtbuf.c b/src/txtbuf.c @@ -0,0 +1,145 @@ +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <stdarg.h> + +#include "util.h" +#include "memory.h" +#include "txtbuf.h" +#include "assert.h" + +txtbuf * +txtbuf_new(void) { + txtbuf *buf = ltk_malloc(sizeof(txtbuf)); + buf->text = NULL; + buf->cap = buf->len = 0; + return buf; +} + +txtbuf * +txtbuf_new_from_char(char *str) { + txtbuf *buf = ltk_malloc(sizeof(txtbuf)); + buf->text = ltk_strdup(str); + buf->len = strlen(str); + buf->cap = buf->len + 1; + return buf; +} + +txtbuf * +txtbuf_new_from_char_len(char *str, size_t len) { + txtbuf *buf = ltk_malloc(sizeof(txtbuf)); + buf->text = ltk_strndup(str, len); + buf->len = len; + buf->cap = len + 1; + return buf; +} + +void +txtbuf_fmt(txtbuf *buf, char *fmt, ...) { + va_list args; + va_start(args, fmt); + int len = vsnprintf(buf->text, buf->cap, fmt, args); + /* FIXME: len can never be negative, right? */ + /* FIXME: maybe also shrink here */ + if ((size_t)len >= buf->cap) { + va_end(args); + va_start(args, fmt); + txtbuf_resize(buf, len); + vsnprintf(buf->text, buf->cap, fmt, args); + } + buf->len = len; + va_end(args); +} + +void +txtbuf_set_text(txtbuf *buf, char *text) { + txtbuf_set_textn(buf, text, strlen(text)); +} + +void +txtbuf_set_textn(txtbuf *buf, char *text, size_t len) { + txtbuf_resize(buf, len); + buf->len = len; + memmove(buf->text, text, len); + buf->text[buf->len] = '\0'; +} + +void +txtbuf_append(txtbuf *buf, char *text) { + txtbuf_appendn(buf, text, strlen(text)); +} + +/* FIXME: some sort of append that does not resize until there's not enough + space so a buffer that will be filled up anyways doesn't have to be + constantly resized */ +void +txtbuf_appendn(txtbuf *buf, char *text, size_t len) { + /* FIXME: overflow protection here and everywhere else */ + txtbuf_resize(buf, buf->len + len); + memmove(buf->text + buf->len, text, len); + buf->len += len; + buf->text[buf->len] = '\0'; +} + +void +txtbuf_resize(txtbuf *buf, size_t sz) { + /* always leave room for extra \0 */ + size_t cap = ideal_array_size(buf->cap, sz + 1); + if (cap != buf->cap) { + buf->text = ltk_realloc(buf->text, cap); + buf->cap = cap; + } +} + +void +txtbuf_destroy(txtbuf *buf) { + if (!buf) + return; + free(buf->text); + free(buf); +} + +void +txtbuf_copy(txtbuf *dst, txtbuf *src) { + txtbuf_resize(dst, src->len); + if (src->text && dst->text) { + memcpy(dst->text, src->text, src->len); + dst->text[src->len] = '\0'; + } + dst->len = src->len; +} + +txtbuf * +txtbuf_dup(txtbuf *src) { + txtbuf *dst = txtbuf_new(); + txtbuf_copy(dst, src); + return dst; +} + +/* FIXME: proper "normalize" function to add nul-termination if needed */ +int +txtbuf_cmp(txtbuf *buf1, txtbuf *buf2) { + /* FIXME: I guess strcmp would be possible as well since it's nul-terminated now */ + /* FIXME: Test this because I was tired while writing it */ + int cmp = strncmp(buf1->text, buf2->text, buf1->len < buf2->len ? buf1->len : buf2->len); + if (cmp == 0) { + if (buf1->len < buf2->len) + return -1; + else if (buf1->len > buf2->len) + return 1; + } + return cmp; +} + +int +txtbuf_eql(txtbuf *buf1, txtbuf *buf2) { + return txtbuf_cmp(buf1, buf2) == 0; +} + +void +txtbuf_clear(txtbuf *buf) { + if (buf->len > 0) { + buf->len = 0; + buf->text[0] = '\0'; + } +} diff --git a/src/txtbuf.h b/src/txtbuf.h @@ -0,0 +1,99 @@ +#ifndef LTK_TXTBUF_H +#define LTK_TXTBUF_H + +#include <stddef.h> + +/* + * txtbuf is really just a string data type that is badly named. + * The stored text is always nul-terminated. + * FIXME: this data type is abused in some places and manually + * created so it isn't nul-terminated + */ + +typedef struct { + size_t len, cap; + char *text; +} txtbuf; + +/* + * Create an empty txtbuf. + */ +txtbuf *txtbuf_new(void); + +/* + * Create a new txtbuf, initializing it with the nul-terminated + * string 'str'. The input string is copied. + */ +txtbuf *txtbuf_new_from_char(char *str); + +/* + * Create a new txtbuf, initializing it with the string 'str' + * of length 'len'. The input string is copied. + */ +txtbuf *txtbuf_new_from_char_len(char *str, size_t len); + +/* + * Replace the stored text in 'buf' with the text generated by + * 'snprintf' when called with the given format string and args. + */ +void txtbuf_fmt(txtbuf *buf, char *fmt, ...); + +/* + * Replace the stored text in 'buf' with 'text'. + */ +void txtbuf_set_text(txtbuf *buf, char *text); + +/* + * Same as txtbuf_set_text, but with explicit length for 'text'. + */ +void txtbuf_set_textn(txtbuf *buf, char *text, size_t len); + +/* + * Append 'text' to the text stored in 'buf'. + */ +void txtbuf_append(txtbuf *buf, char *text); + +/* + * Same as txtbuf_append, but with explicit length for 'text'. + */ +void txtbuf_appendn(txtbuf *buf, char *text, size_t len); + +/* + * Compare the text of two txtbuf's like 'strcmp'. + */ +int txtbuf_cmp(txtbuf *buf1, txtbuf *buf2); + +/* + * Convenience function for calling 'txtbuf_cmp' and checking if the + * return value is 0, i.e. the strings are equal. + */ +int txtbuf_eql(txtbuf *buf1, txtbuf *buf2); + +/* + * Make sure the txtbuf has space for at least the given size, + * plus '\0' at the end. + */ +void txtbuf_resize(txtbuf *buf, size_t sz); + +/* + * Destroy a txtbuf. + */ +void txtbuf_destroy(txtbuf *buf); + +/* + * Copy txtbuf 'src' to txtbuf 'dst'. + */ +void txtbuf_copy(txtbuf *dst, txtbuf *src); + +/* + * Duplicate txtbuf 'src'. + */ +txtbuf *txtbuf_dup(txtbuf *src); + +/* + * Clear the text, but do not reduce the internal capacity + * (for efficiency if it will be filled up again anyways). + */ +void txtbuf_clear(txtbuf *buf); + +#endif /* LTK_TXTBUF */