diff --git a/README.md b/README.md index 0087762..6537e5e 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,8 @@ SDL_VIDEODRIVER=dummy build/examples/2048 # omit env var for a visible window The bundle covers a 2048 game, a paint demo, Conway's Game of Life, an analog clock, an interactive Mandelbrot viewer, a single-runner Processing-style -showcase, and the upstream X.Org `x11perf` benchmark. See +showcase, an SDL-backed clipboard `TARGETS` probe, and the upstream X.Org +`x11perf` benchmark. See [`docs/EXAMPLES.md`](docs/EXAMPLES.md) for the API each example exercises. diff --git a/docs/EXAMPLES.md b/docs/EXAMPLES.md index c297157..3fa2ca7 100644 --- a/docs/EXAMPLES.md +++ b/docs/EXAMPLES.md @@ -18,6 +18,7 @@ SDL_VIDEODRIVER=dummy build/examples/2048 # omit env var for a visible window | `clock` | [`examples/clock.c`](../examples/clock.c) | Analog clock (MIT, in-tree). `XDrawArc` / `XFillArc`, multiple GCs with `XSetLineAttributes`, one-second redraw cadence driven by `gettimeofday`. | | `mandel` | [`examples/mandel.c`](../examples/mandel.c) | Interactive Mandelbrot viewer (MIT, in-tree). `XCreateImage` + `XPutImage` with a 32-bit ZPixmap raster (buffer ownership transferred to the `XImage`), `ButtonPress` dispatch. | | `processing` | [`examples/processing.c`](../examples/processing.c) | Standalone Processing-like showcase (MIT, in-tree). Exercises Xlib polygons, arcs, lines, strings, pointer input, key input, and timed redraws. | +| `clipboard` | [`examples/clipboard.c`](../examples/clipboard.c) | Interactive selection conversion probe. Lets the user seed SDL clipboard text, request `TARGETS`, read `UTF8_STRING`, and quit from a small Xlib window. Pass `--once` for a headless `TARGETS` printout. | | `x11perf` | [`examples/x11perf/`](../examples/x11perf/README.md) | Upstream X.Org `x11perf` benchmark. Imported from `0c3597b6` with only local build glue (a `config.h` shim and one cosmetic output tweak). Self-contained performance harness for the SDL2-backed paths. | The short regression loop used during performance work: diff --git a/examples/clipboard.c b/examples/clipboard.c new file mode 100644 index 0000000..9036005 --- /dev/null +++ b/examples/clipboard.c @@ -0,0 +1,302 @@ +/* + * clipboard - interactive SDL clipboard bridge probe. + * + * Press C to seed SDL's clipboard, T to query CLIPBOARD TARGETS, V to read + * UTF8_STRING text, and Esc to quit. Pass --once to print the TARGETS list + * without opening an interactive loop. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static volatile bool quitRequested = false; + +/* Handler only sets the flag; everything else (SDL, stdio, malloc) is not + * async-signal-safe. The main loop checks the flag on the next event, so + * Ctrl-C followed by any keystroke or WM event runs cleanup cleanly. */ +static void onSignal(int signo) +{ + (void) signo; + quitRequested = true; +} + +static Bool isSelectionNotify(Display *display, XEvent *event, XPointer arg) +{ + (void) display; + (void) arg; + return event->type == SelectionNotify; +} + +#define WIN_W 560 +#define WIN_H 220 + +typedef struct { + Display *display; + Window window; + GC gc; + Atom clipboard; + Atom targets; + Atom utf8; + Atom property; + const char *status; + char details[256]; +} App; + +static void convertSelection(App *app, Atom target) +{ + XConvertSelection(app->display, app->clipboard, target, app->property, + app->window, CurrentTime); +} + +static int readProperty(App *app, + Atom reqType, + Atom *actualType, + int *actualFormat, + unsigned long *nItems, + unsigned char **data) +{ + unsigned long bytesAfter = 0; + *data = NULL; + /* long_length is in 32-bit units. LONG_MAX / 4 covers any plausible + * clipboard payload in a single request (the X11 wire protocol caps a + * request at ~16 MiB anyway), so bytesAfter==0 below stays a valid + * "fully read" check without truncating a large UTF8_STRING. */ + int status = XGetWindowProperty(app->display, app->window, app->property, 0, + LONG_MAX / 4, False, reqType, actualType, + actualFormat, nItems, &bytesAfter, data); + /* XGetWindowProperty allocates *data even when the helper's secondary + * checks (success + bytesAfter==0 + non-NULL data) reject the result. */ + if (status != Success || bytesAfter != 0 || !*data) { + if (*data) { + XFree(*data); + *data = NULL; + } + return 0; + } + return 1; +} + +static void render(App *app) +{ + XClearWindow(app->display, app->window); + XDrawString(app->display, app->window, app->gc, 20, 32, + "libx11-compat clipboard", 23); + XDrawString(app->display, app->window, app->gc, 20, 62, + "C: copy sample text T: query TARGETS V: read text", 56); + XDrawString(app->display, app->window, app->gc, 20, 92, + "Esc: quit", 9); + XDrawString(app->display, app->window, app->gc, 20, 132, app->status, + (int) strlen(app->status)); + XDrawString(app->display, app->window, app->gc, 20, 162, app->details, + (int) strlen(app->details)); + XFlush(app->display); +} + +static void showTargets(App *app) +{ + convertSelection(app, app->targets); + + XEvent event; + XIfEvent(app->display, &event, isSelectionNotify, NULL); + if (event.xselection.property == None) { + app->status = "TARGETS conversion failed"; + app->details[0] = '\0'; + return; + } + + Atom actualType = None; + int actualFormat = 0; + unsigned long nItems = 0; + unsigned char *data = NULL; + if (!readProperty(app, XA_ATOM, &actualType, &actualFormat, &nItems, + &data)) { + app->status = "TARGETS property read failed"; + app->details[0] = '\0'; + return; + } + if (actualType != XA_ATOM || actualFormat != 32) { + XFree(data); + app->status = "TARGETS property read failed"; + app->details[0] = '\0'; + return; + } + + Atom *atoms = (Atom *) data; + app->status = "CLIPBOARD target atoms:"; + app->details[0] = '\0'; + for (unsigned long i = 0; i < nItems; i++) { + char *name = XGetAtomName(app->display, atoms[i]); + size_t used = strlen(app->details); + snprintf(app->details + used, sizeof(app->details) - used, "%s%s", + used ? ", " : "", name ? name : ""); + XFree(name); + } + XFree(data); +} + +static void showClipboardText(App *app) +{ + convertSelection(app, app->utf8); + + XEvent event; + XIfEvent(app->display, &event, isSelectionNotify, NULL); + if (event.xselection.property == None) { + app->status = "UTF8_STRING conversion failed"; + app->details[0] = '\0'; + return; + } + + Atom actualType = None; + int actualFormat = 0; + unsigned long nItems = 0; + unsigned char *data = NULL; + if (!readProperty(app, app->utf8, &actualType, &actualFormat, &nItems, + &data)) { + app->status = "UTF8_STRING property read failed"; + app->details[0] = '\0'; + return; + } + if (actualType != app->utf8 || actualFormat != 8) { + XFree(data); + app->status = "UTF8_STRING property read failed"; + app->details[0] = '\0'; + return; + } + + app->status = "CLIPBOARD text:"; + snprintf(app->details, sizeof(app->details), "%.*s", (int) nItems, + (char *) data); + XFree(data); +} + +static int runOnce(App *app) +{ + XSetSelectionOwner(app->display, app->clipboard, None, CurrentTime); + SDL_SetClipboardText("clipboard text from SDL"); + showTargets(app); + if (app->details[0] == '\0') { + fprintf(stderr, "%s\n", app->status); + return 1; + } + printf("SDL clipboard text is available through XConvertSelection.\n"); + printf("Supported CLIPBOARD target atoms: %s\n", app->details); + return 0; +} + +static void seedClipboard(App *app) +{ + XSetSelectionOwner(app->display, app->clipboard, None, CurrentTime); + SDL_SetClipboardText("clipboard text from SDL"); + app->status = "Copied sample text into SDL clipboard"; + snprintf(app->details, sizeof(app->details), "%s", + "Press T to inspect targets or V to read the text back."); +} + +static void cleanup(App *app) +{ + if (app->gc) + XFreeGC(app->display, app->gc); + if (app->window) + XDestroyWindow(app->display, app->window); + if (app->display) + XCloseDisplay(app->display); +} + +int main(int argc, char **argv) +{ + App app; + memset(&app, 0, sizeof(app)); + app.status = "Press C to seed the SDL clipboard"; + + signal(SIGINT, onSignal); + signal(SIGTERM, onSignal); + + app.display = XOpenDisplay(NULL); + if (!app.display) { + fprintf(stderr, "XOpenDisplay failed\n"); + return 1; + } + + int rc = 1; + int screen = DefaultScreen(app.display); + Window root = RootWindow(app.display, screen); + app.window = XCreateSimpleWindow(app.display, root, 0, 0, WIN_W, WIN_H, 1, + BlackPixel(app.display, screen), + WhitePixel(app.display, screen)); + if (app.window == None) { + fprintf(stderr, "XCreateSimpleWindow failed\n"); + goto done; + } + app.gc = XCreateGC(app.display, app.window, 0, NULL); + if (!app.gc) { + fprintf(stderr, "XCreateGC failed\n"); + goto done; + } + app.clipboard = XInternAtom(app.display, "CLIPBOARD", False); + app.targets = XInternAtom(app.display, "TARGETS", False); + app.utf8 = XInternAtom(app.display, "UTF8_STRING", False); + app.property = XInternAtom(app.display, "CLIPBOARD_EXAMPLE", False); + if (app.clipboard == None || app.targets == None || app.utf8 == None || + app.property == None) { + fprintf(stderr, "XInternAtom failed\n"); + goto done; + } + + if (argc > 1 && strcmp(argv[1], "--once") == 0) { + rc = runOnce(&app); + goto done; + } + + XStoreName(app.display, app.window, "clipboard"); + Atom wmDelete = XInternAtom(app.display, "WM_DELETE_WINDOW", False); + XSetWMProtocols(app.display, app.window, &wmDelete, 1); + XSelectInput(app.display, app.window, ExposureMask | KeyPressMask); + XMapWindow(app.display, app.window); + + XEvent event; + while (!quitRequested) { + XNextEvent(app.display, &event); + if (quitRequested) + break; + switch (event.type) { + case Expose: + if (event.xexpose.count == 0) + render(&app); + break; + case KeyPress: { + KeySym key = XLookupKeysym(&event.xkey, 0); + if (key == XK_Escape) { + rc = 0; + goto done; + } else if (key == XK_c || key == XK_C) { + seedClipboard(&app); + } else if (key == XK_t || key == XK_T) { + showTargets(&app); + } else if (key == XK_v || key == XK_V) { + showClipboardText(&app); + } + render(&app); + break; + } + case ClientMessage: + if ((Atom) event.xclient.data.l[0] == wmDelete) { + rc = 0; + goto done; + } + break; + } + } + rc = 0; + +done: + cleanup(&app); + return rc; +} diff --git a/mk/examples.mk b/mk/examples.mk index 0700b21..1a376bb 100644 --- a/mk/examples.mk +++ b/mk/examples.mk @@ -1,4 +1,4 @@ -EXAMPLE_NAMES := 2048 paint life clock mandel processing +EXAMPLE_NAMES := 2048 paint life clock mandel processing clipboard EXAMPLE_BINS := $(addprefix $(OUT)/examples/,$(EXAMPLE_NAMES)) X11PERF_DIR := examples/x11perf X11PERF_SRCS := \ diff --git a/src/display.c b/src/display.c index 36fae0e..1efbb42 100644 --- a/src/display.c +++ b/src/display.c @@ -65,6 +65,7 @@ int XCloseDisplay(Display *display) freeImageStorage(); destroyScreenWindow(display); freeAtomStorage(); + resetSelectionAtomCache(); freeFontStorage(); freeColorStorage(); freeVisuals(); diff --git a/src/events.c b/src/events.c index d829030..4093736 100644 --- a/src/events.c +++ b/src/events.c @@ -2196,6 +2196,21 @@ Bool XCheckIfEvent(register Display *display, return checkIfEvent(display, event, predicate, arg); } +int XIfEvent(register Display *display, + register XEvent *event, + Bool (*predicate)(Display * /* display */, + XEvent * /* event */, + char * /* arg */ + ), + char *arg) +{ + while (!checkIfEvent(display, event, predicate, arg)) { + pumpEventsSafe(); + SDL_Delay(1); + } + return 0; +} + Bool XCheckTypedEvent(Display *display, int type, XEvent *event) { return checkTypedEvent(display, 0, type, 0, event, &checkTypedPredicate); diff --git a/src/missing.c b/src/missing.c index 4b17491..c83f66c 100644 --- a/src/missing.c +++ b/src/missing.c @@ -2771,17 +2771,6 @@ Status XcmsCIExyYToCIEXYZ(XcmsCCC ccc, XcmsColorSpace XcmsCIELabColorSpace = {}; XcmsColorSpace XcmsCIEXYZColorSpace = {}; -int XIfEvent(register Display *dpy, - register XEvent *event, - Bool (*predicate)(Display * /* display */, - XEvent * /* event */, - char * /* arg */ - ), /* function to call */ - char *arg) -{ - return 0; -} - int XPeekIfEvent(register Display *dpy, register XEvent *event, Bool (*predicate)(Display * /* display */, diff --git a/src/selection.c b/src/selection.c index bb59474..984a1ef 100644 --- a/src/selection.c +++ b/src/selection.c @@ -109,20 +109,41 @@ void freeSelectionStorage(Display *display) free(t); } +/* Atom IDs come from the global atom table in atoms.c. That table is + * destroyed by freeAtomStorage() when the last Display closes, so any + * cached ID becomes stale across XCloseDisplay+XOpenDisplay cycles. + * resetSelectionAtomCache() is invoked from XCloseDisplay alongside + * freeAtomStorage() to keep these in sync. */ +static Atom cachedClipboardAtom = None; +static Atom cachedUtf8StringAtom = None; +static Atom cachedTargetsAtom = None; + static Atom clipboardAtom(Display *display) { - static Atom cached = None; - if (cached == None && display) - cached = XInternAtom(display, "CLIPBOARD", False); - return cached; + if (cachedClipboardAtom == None && display) + cachedClipboardAtom = XInternAtom(display, "CLIPBOARD", False); + return cachedClipboardAtom; } static Atom utf8StringAtom(Display *display) { - static Atom cached = None; - if (cached == None && display) - cached = XInternAtom(display, "UTF8_STRING", False); - return cached; + if (cachedUtf8StringAtom == None && display) + cachedUtf8StringAtom = XInternAtom(display, "UTF8_STRING", False); + return cachedUtf8StringAtom; +} + +static Atom targetsAtom(Display *display) +{ + if (cachedTargetsAtom == None && display) + cachedTargetsAtom = XInternAtom(display, "TARGETS", False); + return cachedTargetsAtom; +} + +void resetSelectionAtomCache(void) +{ + cachedClipboardAtom = None; + cachedUtf8StringAtom = None; + cachedTargetsAtom = None; } static Bool postSelectionClear(Display *display, @@ -244,6 +265,23 @@ int XConvertSelection(Display *display, * configurations. */ char *text = SDL_GetClipboardText(); Atom utf8 = utf8StringAtom(display); + Atom targets = targetsAtom(display); + /* Only advertise TARGETS once we know SDL actually has retrievable + * text. SDL_HasClipboardText() above can disagree with a follow-up + * SDL_GetClipboardText() returning NULL (race, OOM), and a TARGETS + * reply listing UTF8_STRING/STRING when no text exists would lie + * to the requestor. Fall through to the no-data path instead. */ + if (target == targets && text) { + Atom supportedTargets[] = {targets, utf8, XA_STRING}; + XChangeProperty( + display, requestor, effectiveProperty, XA_ATOM, 32, + PropModeReplace, (unsigned char *) supportedTargets, + (int) (sizeof(supportedTargets) / sizeof(supportedTargets[0]))); + postSelectionNotify(display, requestor, selection, target, + effectiveProperty, time); + SDL_free(text); + return 1; + } if (text && (target == XA_STRING || target == utf8)) { XChangeProperty(display, requestor, effectiveProperty, target, 8, PropModeReplace, (unsigned char *) text, diff --git a/src/selection.h b/src/selection.h index 9e07846..1844133 100644 --- a/src/selection.h +++ b/src/selection.h @@ -4,5 +4,6 @@ #include void freeSelectionStorage(Display *display); +void resetSelectionAtomCache(void); #endif /* SELECTION_H */ diff --git a/src/window.c b/src/window.c index dc81418..ab2edb3 100644 --- a/src/window.c +++ b/src/window.c @@ -775,6 +775,10 @@ int XChangeProperty(Display *display, handleError(0, display, property, 0, BadAtom, 0); return 0; } + if (!isValidAtom(type)) { + handleError(0, display, type, 0, BadAtom, 0); + return 0; + } WindowStruct *windowStruct = GET_WINDOW_STRUCT(window); WindowProperty *windowProperty = findProperty(&windowStruct->properties, property, NULL); @@ -788,10 +792,6 @@ int XChangeProperty(Display *display, if (!propertyIsNew) { previousDataLength = windowProperty->dataLength; previousData = windowProperty->data; - if (windowProperty->type != type) { - handleError(0, display, type, 0, BadMatch, 0); - return 0; - } } else { windowProperty = malloc(sizeof(WindowProperty)); if (!windowProperty) { diff --git a/tests/check.c b/tests/check.c index dddfa8c..6381a43 100644 --- a/tests/check.c +++ b/tests/check.c @@ -3057,6 +3057,7 @@ static int test_selection(Display *display) PropertyChangeMask | SubstructureNotifyMask | NoEventMask); Atom clipboard = XInternAtom(display, "CLIPBOARD", False); Atom utf8 = XInternAtom(display, "UTF8_STRING", False); + Atom targets = XInternAtom(display, "TARGETS", False); Atom prop = XInternAtom(display, "SDL2X11_SEL_TEST", False); /* No owner: XConvertSelection emits SelectionNotify with property=None. */ @@ -3098,6 +3099,29 @@ static int test_selection(Display *display) CHECK(ev.xselection.property == utf8, "property=None conversion did not default to target atom"); + XConvertSelection(display, clipboard, targets, prop, window, CurrentTime); + CHECK(XCheckTypedEvent(display, SelectionNotify, &ev), + "missing SelectionNotify for SDL-backed TARGETS conversion"); + CHECK(ev.xselection.property == prop, + "SDL-backed TARGETS SelectionNotify wrong property"); + Atom actualType = None; + int actualFormat = 0; + unsigned long nItems = 0; + unsigned long bytesAfter = 0; + unsigned char *data = NULL; + CHECK(XGetWindowProperty(display, window, prop, 0, 3, False, XA_ATOM, + &actualType, &actualFormat, &nItems, &bytesAfter, + &data) == Success, + "XGetWindowProperty for SDL-backed TARGETS failed"); + CHECK(actualType == XA_ATOM && actualFormat == 32 && nItems == 3 && + bytesAfter == 0, + "SDL-backed TARGETS property had wrong shape"); + CHECK(data != NULL, "SDL-backed TARGETS property data missing"); + Atom *atoms = (Atom *) data; + CHECK(atoms[0] == targets && atoms[1] == utf8 && atoms[2] == XA_STRING, + "SDL-backed TARGETS atom list was incorrect"); + XFree(data); + XDestroyWindow(display, owner1); XDestroyWindow(display, owner2); XDestroyWindow(display, window);