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:
M | LICENSE | | | 3 | ++- |
A | LICENSE.ctrlsel | | | 23 | +++++++++++++++++++++++ |
M | Makefile | | | 13 | +++++++++---- |
M | clipboard.c | | | 265 | +++++++++++++++++++++---------------------------------------------------------- |
A | ctrlsel.c | | | 1621 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
A | ctrlsel.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_ */