ledit

Text editor (WIP)
git clone git://lumidify.org/ledit.git (fast, but not encrypted)
git clone https://lumidify.org/git/ledit.git (encrypted, but very slow)
Log | Files | Refs | README | LICENSE

commit 2685622dcc0ab5293da9b82e9d651d3b9696e1ec
parent 60f311ec4a85a62f84f76d5a1a05432f08417287
Author: lumidify <nobody@lumidify.org>
Date:   Sun, 20 Aug 2023 22:15:59 +0200

Replace broken clipboard handling with ctrlsel

Diffstat:
MLICENSE | 3++-
ALICENSE.ctrlsel | 23+++++++++++++++++++++++
MMakefile | 13+++++++++----
Mclipboard.c | 265+++++++++++++++++++++----------------------------------------------------------
Actrlsel.c | 1621+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Actrlsel.h | 107+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
6 files changed, 1832 insertions(+), 200 deletions(-)

diff --git a/LICENSE b/LICENSE @@ -2,10 +2,11 @@ Note 1: Some stuff is stolen from st (https://st.suckless.org) Note 2: Some stuff is stolen from OpenBSD (https://openbsd.org) Note 3: pango-compat.{c,h} contains a bit of code copied from Pango in order to be compatible with older versions. +Note 4: See LICENSE.ctrlsel for ctrlsel.{c,h} ISC License -Copyright (c) 2021, 2022 lumidify <nobody@lumidify.org> +Copyright (c) 2021-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/LICENSE.ctrlsel b/LICENSE.ctrlsel @@ -0,0 +1,23 @@ +License for ctrlsel.{c,h}: + +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. diff --git a/Makefile b/Makefile @@ -32,7 +32,8 @@ OBJ = \ draw_util.o \ window.o \ clipboard.o \ - pango-compat.o + pango-compat.o \ + ctrlsel.o SRC = ${OBJ:.o=.c} @@ -57,7 +58,8 @@ HDR = \ macros.h \ pango-compat.h \ clipboard.h \ - uglycrap.h + uglycrap.h \ + ctrlsel.h CONFIGHDR = \ config.h \ @@ -69,8 +71,11 @@ EXTRA_LDFLAGS_DEBUG0 = ${LDFLAGS} EXTRA_CFLAGS_DEBUG1 = -DLEDIT_DEBUG -g EXTRA_FLAGS_SANITIZE1 = -fsanitize=address -CFLAGS_LEDIT = ${EXTRA_FLAGS_SANITIZE${SANITIZE}} ${EXTRA_CFLAGS_DEBUG${DEBUG}} -Wall -Wextra -pedantic -D_POSIX_C_SOURCE=200809L -std=c99 `pkg-config --cflags x11 xkbfile pangoxft xext` -LDFLAGS_LEDIT = ${EXTRA_FLAGS_SANITIZE${SANITIZE}} ${EXTRA_LDFLAGS_DEBUG${DEBUG}} `pkg-config --libs x11 xkbfile pangoxft xext` -lm +# Xcursor isn't actually needed right now since I'm not using the drag 'n drop functionality +# of ctrlsel yet, but since it's moderately likely that I will use that in the future, I +# decided to just leave it in. +CFLAGS_LEDIT = ${EXTRA_FLAGS_SANITIZE${SANITIZE}} ${EXTRA_CFLAGS_DEBUG${DEBUG}} -Wall -Wextra -pedantic -D_POSIX_C_SOURCE=200809L -std=c99 `pkg-config --cflags x11 xkbfile pangoxft xext xcursor` +LDFLAGS_LEDIT = ${EXTRA_FLAGS_SANITIZE${SANITIZE}} ${EXTRA_LDFLAGS_DEBUG${DEBUG}} `pkg-config --libs x11 xkbfile pangoxft xext xcursor` -lm all: ${BIN} diff --git a/clipboard.c b/clipboard.c @@ -12,27 +12,25 @@ #include "clipboard.h" #include "macros.h" #include "config.h" +#include "ctrlsel.h" -/* clipboard handling largely stolen from st (https://st.suckless.org), - with some *inspiration* taken from SDL (https://libsdl.org), mainly - the idea to create a separate window just for clipboard handling */ +/* 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(ledit_clipboard *clip); static Bool check_window(Display *dpy, XEvent *event, XPointer arg); static txtbuf *get_text(ledit_clipboard *clip, int primary); -static int clipboard_propnotify(ledit_clipboard *clip, XEvent *e, txtbuf *buf); -static int clipboard_selnotify(ledit_clipboard *clip, XEvent *e, txtbuf *buf); -static void clipboard_selrequest(ledit_clipboard *clip, XEvent *e); - -#define MODBIT(x, set, bit) ((set) ? ((x) |= (bit)) : ((x) &= ~(bit))) struct ledit_clipboard { txtbuf *primary; txtbuf *clipboard; + txtbuf *rbuf; ledit_common *common; Window window; + struct CtrlSelTarget starget; + struct CtrlSelTarget rtarget; + CtrlSelContext *scontext; Atom xtarget; - XSetWindowAttributes wattrs; }; ledit_clipboard * @@ -40,15 +38,16 @@ clipboard_create(ledit_common *common) { ledit_clipboard *clip = ledit_malloc(sizeof(ledit_clipboard)); clip->primary = txtbuf_new(); clip->clipboard = txtbuf_new(); + clip->rbuf = txtbuf_new(); clip->common = common; clip->window = None; clip->xtarget = None; #ifdef X_HAVE_UTF8_STRING clip->xtarget = XInternAtom(common->dpy, "UTF8_STRING", False); + #else + clip->xtarget = XA_STRING; #endif - if (clip->xtarget == None) - clip->xtarget = XA_STRING; - clip->wattrs.event_mask = 0; + clip->scontext = NULL; return clip; } @@ -56,6 +55,9 @@ void clipboard_destroy(ledit_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->common->dpy, clip->window); free(clip); @@ -66,7 +68,7 @@ get_clipboard_window(ledit_clipboard *clip) { if (clip->window == None) { clip->window = XCreateWindow( clip->common->dpy, DefaultRootWindow(clip->common->dpy), - -10, -10, 1, 1, 0, CopyFromParent, InputOnly, CopyFromParent, 0, &clip->wattrs + -10, -10, 1, 1, 0, CopyFromParent, InputOnly, CopyFromParent, 0, NULL ); XFlush(clip->common->dpy); } @@ -75,9 +77,8 @@ get_clipboard_window(ledit_clipboard *clip) { void clipboard_set_primary_text(ledit_clipboard *clip, char *text) { - Window window = get_clipboard_window(clip); txtbuf_set_text(clip->primary, text); - XSetSelectionOwner(clip->common->dpy, XA_PRIMARY, window, CurrentTime); + clipboard_set_primary_selection_owner(clip); } txtbuf * @@ -88,16 +89,21 @@ clipboard_get_primary_buffer(ledit_clipboard *clip) { void clipboard_set_primary_selection_owner(ledit_clipboard *clip) { Window window = get_clipboard_window(clip); - XSetSelectionOwner(clip->common->dpy, XA_PRIMARY, window, CurrentTime); + 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->common->dpy, window, XA_PRIMARY, CurrentTime, 0, &clip->starget, 1); + if (!clip->scontext) + fprintf(stderr, "WARNING: Could not own primary selection.\n"); } void clipboard_set_clipboard_text(ledit_clipboard *clip, char *text) { - Atom clip_atom; - Window window = get_clipboard_window(clip); - clip_atom = XInternAtom(clip->common->dpy, "CLIPBOARD", False); txtbuf_set_text(clip->clipboard, text); - XSetSelectionOwner(clip->common->dpy, clip_atom, window, CurrentTime); + clipboard_set_clipboard_selection_owner(clip); } txtbuf * @@ -110,25 +116,30 @@ clipboard_set_clipboard_selection_owner(ledit_clipboard *clip) { Atom clip_atom; Window window = get_clipboard_window(clip); clip_atom = XInternAtom(clip->common->dpy, "CLIPBOARD", False); - XSetSelectionOwner(clip->common->dpy, clip_atom, window, CurrentTime); + 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->common->dpy, window, clip_atom, CurrentTime, 0, &clip->starget, 1); + if (!clip->scontext) + fprintf(stderr, "WARNING: Could not own clipboard selection.\n"); } void clipboard_primary_to_clipboard(ledit_clipboard *clip) { - Atom clip_atom; if (clip->primary->len > 0) { - Window window = get_clipboard_window(clip); txtbuf_copy(clip->clipboard, clip->primary); - clip_atom = XInternAtom(clip->common->dpy, "CLIPBOARD", False); - XSetSelectionOwner(clip->common->dpy, clip_atom, window, CurrentTime); + clipboard_set_clipboard_selection_owner(clip); } } int clipboard_filter_event(ledit_clipboard *clip, XEvent *e) { if (clip->window != None && e->xany.window == clip->window) { - if (e->type == SelectionRequest) - clipboard_selrequest(clip, e); + if (clip->scontext) + ctrlsel_send(clip->scontext, e); /* other events are discarded since there was no request to get the clipboard text */ return 1; @@ -145,9 +156,18 @@ check_window(Display *dpy, XEvent *event, XPointer arg) { /* WARNING: The returned txtbuf needs to be copied before further processing! */ static txtbuf * get_text(ledit_clipboard *clip, int primary) { - txtbuf *buf = primary ? clip->primary : clip->clipboard; - txtbuf_clear(buf); + 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->common->dpy, "CLIPBOARD", False); + /* FIXME: use proper time here */ + context = ctrlsel_request(clip->common->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); @@ -156,24 +176,22 @@ get_text(ledit_clipboard *clip, int primary) { while (1) { /* FIXME: I have no idea how inefficient this is */ if (XCheckIfEvent(clip->common->dpy, &event, &check_window, (XPointer)&window)) { - switch (event.type) { - case SelectionNotify: - if (!clipboard_selnotify(clip, &event, buf)) - return buf; - break; - case PropertyNotify: - if (!clipboard_propnotify(clip, &event, buf)) - return buf; - break; - case SelectionRequest: - clipboard_selrequest(clip, &event); - break; - default: - break; + 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); ledit_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) clipboard_set_primary_text(clip, ""); @@ -189,6 +207,15 @@ get_text(ledit_clipboard *clip, int primary) { 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 * @@ -202,7 +229,6 @@ clipboard_get_clipboard_text(ledit_clipboard *clip) { } else if (owner == window) { return clip->clipboard; } else { - XConvertSelection(clip->common->dpy, clip_atom, clip->xtarget, clip_atom, window, CurrentTime); return get_text(clip, 0); } } @@ -216,157 +242,6 @@ clipboard_get_primary_text(ledit_clipboard *clip) { } else if (owner == window) { return clip->primary; } else { - XConvertSelection(clip->common->dpy, XA_PRIMARY, clip->xtarget, XA_PRIMARY, window, CurrentTime); return get_text(clip, 1); } } - -/* 0 means the transfer is done, 1 means there might be more */ -static int -clipboard_propnotify(ledit_clipboard *clip, XEvent *e, txtbuf *buf) { - XPropertyEvent *xpev; - Atom clipboard = XInternAtom(clip->common->dpy, "CLIPBOARD", False); - xpev = &e->xproperty; - if (xpev->state == PropertyNewValue && (xpev->atom == XA_PRIMARY || xpev->atom == clipboard)) { - return clipboard_selnotify(clip, e, buf); - } - return 1; -} - -/* FIXME: test this properly because I don't really understand all of it */ -/* 0 means the transfer is done, 1 means there might be more */ -static int -clipboard_selnotify(ledit_clipboard *clip, XEvent *e, txtbuf *buf) { - unsigned long nitems, ofs, rem; - int format; - unsigned char *data; - Atom type, incratom, property = None; - - incratom = XInternAtom(clip->common->dpy, "INCR", 0); - - ofs = 0; - if (e->type == SelectionNotify) { - property = e->xselection.property; - } else if (e->type == PropertyNotify) { - property = e->xproperty.atom; - } - - if (property == None) - return 1; - - Window window = get_clipboard_window(clip); - do { - /* FIXME: proper error logging */ - /* FIXME: show error message in window */ - if (XGetWindowProperty( - clip->common->dpy, window, property, ofs, BUFSIZ/4, False, - AnyPropertyType, &type, &format, &nitems, &rem, &data)) { - fprintf(stderr, "Clipboard allocation failed\n"); - return 0; - } - - if (e->type == PropertyNotify && nitems == 0 && rem == 0) { - /* - * If there is some PropertyNotify with no data, then - * this is the signal of the selection owner that all - * data has been transferred. We won't need to receive - * PropertyNotify events anymore. - */ - MODBIT(clip->wattrs.event_mask, 0, PropertyChangeMask); - XChangeWindowAttributes(clip->common->dpy, window, CWEventMask, &clip->wattrs); - return 0; - } - - if (type == incratom) { - /* - * Activate the PropertyNotify events so we receive - * when the selection owner sends us the next - * chunk of data. - */ - MODBIT(clip->wattrs.event_mask, 1, PropertyChangeMask); - XChangeWindowAttributes(clip->common->dpy, window, CWEventMask, &clip->wattrs); - - /* - * Deleting the property is the transfer start signal. - */ - XDeleteProperty(clip->common->dpy, window, (int)property); - continue; - } - - /* FIXME: XGetWindowProperty takes data as unsigned char, so it is casted here. Why? */ - txtbuf_appendn(buf, (char *)data, (size_t)(nitems * format / 8)); - XFree(data); - if (!(clip->wattrs.event_mask & PropertyChangeMask)) - return 0; - /* number of 32-bit chunks returned */ - ofs += nitems * format / 32; - } while (rem > 0); - - /* - * Deleting the property again tells the selection owner to send the - * next data chunk in the property. - */ - XDeleteProperty(clip->common->dpy, window, (int)property); - return 1; -} - -static void -clipboard_selrequest(ledit_clipboard *clip, XEvent *e) -{ - XSelectionRequestEvent *xsre; - XSelectionEvent xev; - Atom xa_targets, string, clip_atom; - char *seltext; - - xsre = (XSelectionRequestEvent *) e; - xev.type = SelectionNotify; - xev.requestor = xsre->requestor; - xev.selection = xsre->selection; - xev.target = xsre->target; - xev.time = xsre->time; - if (xsre->property == None) - xsre->property = xsre->target; - - /* reject */ - xev.property = None; - - xa_targets = XInternAtom(clip->common->dpy, "TARGETS", 0); - if (xsre->target == xa_targets) { - /* respond with the supported type */ - string = clip->xtarget; - XChangeProperty( - xsre->display, xsre->requestor, xsre->property, - XA_ATOM, 32, PropModeReplace, (unsigned char *) &string, 1 - ); - xev.property = xsre->property; - } else if (xsre->target == clip->xtarget || xsre->target == XA_STRING) { - /* - * xith XA_STRING non ascii characters may be incorrect in the - * requestor. It is not our problem, use utf8. - */ - clip_atom = XInternAtom(clip->common->dpy, "CLIPBOARD", 0); - if (xsre->selection == XA_PRIMARY) { - seltext = clip->primary->text; - } else if (xsre->selection == clip_atom) { - seltext = clip->clipboard->text; - } else { - fprintf( - stderr, - "Unhandled clipboard selection 0x%lx\n", xsre->selection - ); - return; - } - /* FIXME: Should this handle sending data in multiple chunks? */ - if (seltext != NULL) { - XChangeProperty( - xsre->display, xsre->requestor, xsre->property, xsre->target, - 8, PropModeReplace, (unsigned char *)seltext, strlen(seltext) - ); - xev.property = xsre->property; - } - } - - /* all done, send a notification to the listener */ - if (!XSendEvent(xsre->display, xsre->requestor, 1, 0, (XEvent *)&xev)) - fprintf(stderr, "Error sending SelectionNotify event\n"); -} diff --git a/ctrlsel.c b/ctrlsel.c @@ -0,0 +1,1621 @@ +#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/ctrlsel.h b/ctrlsel.h @@ -0,0 +1,107 @@ +/* + * 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_ */