From 76f9e1a21f24078c4775370680c1d7280d2eebb5 Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Fri, 14 Aug 2026 19:12:53 +0900 Subject: [PATCH] gtk: honor preedit policy and cursor placement --- .gitignore | 1 + Dockerfile | 1 + gtk/main.c | 336 ++++++++++++--- tests/Makefile | 9 +- tests/gtk_live_test.c | 928 ++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 1223 insertions(+), 52 deletions(-) create mode 100644 tests/gtk_live_test.c diff --git a/.gitignore b/.gitignore index ffd1480..86f6050 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ /tests/*.o /tests/unit_test /tests/ibus_live_test +/tests/gtk_live_test /tests/ipc_live_test /tests/daemon_collision_test /tests/daemon_failure_test diff --git a/Dockerfile b/Dockerfile index 83f873f..a4dc2a3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -18,6 +18,7 @@ RUN pacman -Syu --noconfirm --needed \ ttf-dejavu \ ttf-jigmo \ which \ + xorg-server-xvfb \ xcb-imdkit \ dbus \ && pacman -Scc --noconfirm diff --git a/gtk/main.c b/gtk/main.c index 987faa6..43739ec 100644 --- a/gtk/main.c +++ b/gtk/main.c @@ -1,8 +1,12 @@ +#include #include #include #include #include #include +#ifdef GDK_WINDOWING_X11 +#include +#endif #include "ipc.h" typedef struct Im Im; @@ -10,8 +14,20 @@ struct Im { GtkIMContext parent; int fd; + int usepreedit; + int effective; + int ext; + int private; char pre[Ipcfieldmax+1]; int prelen; + GdkWindow *win; + GdkRectangle cursor; + int cursorvalid; + int caretsent; + int sentvalid; + int32_t sentx; + int32_t senty; + int32_t senth; }; typedef struct ImClass ImClass; @@ -23,48 +39,163 @@ struct ImClass static GType imtype; static void -srvconnect(Im *im) +setpreedit(Im *im, const char *s, int n) { - if(im->fd >= 0) + int was; + + if(n < 0 || n > Ipcfieldmax) + n = 0; + if(n == im->prelen && memcmp(im->pre, s, n) == 0) return; - im->fd = ipcconnect(); + was = im->prelen; + if(n > 0) + memcpy(im->pre, s, n); + im->pre[n] = '\0'; + im->prelen = n; + if(was == 0 && n > 0) + g_signal_emit_by_name(im, "preedit-start"); + g_signal_emit_by_name(im, "preedit-changed"); + if(was > 0 && n == 0) + g_signal_emit_by_name(im, "preedit-end"); } static void srvclose(Im *im) { - int was; - if(im->fd >= 0) close(im->fd); im->fd = -1; - was = im->prelen; - im->pre[0] = '\0'; - im->prelen = 0; - if(was > 0){ - g_signal_emit_by_name(im, "preedit-changed"); - g_signal_emit_by_name(im, "preedit-end"); - } + im->ext = 0; + im->effective = 1; + im->caretsent = 0; + setpreedit(im, "", 0); } static int -readresp(Im *im, char *buf, int bufsz) +readresp(Im *im, int want, int update, char *commit, int ncommit, + Ipcresp *resp) { - Ipcresp resp; - int was; + char pre[Ipcfieldmax+1]; - was = im->prelen; - if(ipcreadresp(im->fd, 1, buf, bufsz, im->pre, - sizeof(im->pre), &resp) < 0) + if(ipcreadresp(im->fd, want, commit, ncommit, pre, sizeof pre, + resp) < 0) return -1; - im->prelen = resp.npreedit; - if(was == 0 && im->prelen > 0) - g_signal_emit_by_name(im, "preedit-start"); - if(was != 0 || im->prelen != 0) - g_signal_emit_by_name(im, "preedit-changed"); - if(was > 0 && im->prelen == 0) - g_signal_emit_by_name(im, "preedit-end"); - return resp.eaten; + if(update) + setpreedit(im, pre, resp->npreedit); + return 0; +} + +static void +dropwindow(Im *im) +{ + if(im->win != NULL) + g_object_unref(im->win); + im->win = NULL; + im->cursorvalid = 0; +} + +static int +caretget(Im *im, int *valid, int32_t *x, int32_t *y, int32_t *h) +{ + gint rx, ry, scale; + int64_t sx, sy, sh; + + *valid = 0; + *x = *y = *h = 0; + if(im->win != NULL && gdk_window_is_destroyed(im->win)) + dropwindow(im); + if(!im->cursorvalid || im->win == NULL){ + im->cursorvalid = 0; + return 0; + } +#ifdef GDK_WINDOWING_X11 + if(!GDK_IS_X11_WINDOW(im->win)){ + im->cursorvalid = 0; + return 0; + } +#else + im->cursorvalid = 0; + return 0; +#endif + if(im->cursor.height < 0){ + im->cursorvalid = 0; + return 0; + } + scale = gdk_window_get_scale_factor(im->win); + if(scale <= 0){ + im->cursorvalid = 0; + return 0; + } + /* GDK translates the client-relative point before device scaling. */ + gdk_window_get_root_coords(im->win, 0, 0, &rx, &ry); + sx = ((int64_t)rx + im->cursor.x) * scale; + sy = ((int64_t)ry + im->cursor.y) * scale; + sh = (int64_t)im->cursor.height * scale; + if(sx < INT32_MIN || sx > INT32_MAX || + sy < INT32_MIN || sy > INT32_MAX || sh > INT32_MAX){ + im->cursorvalid = 0; + return 0; + } + *valid = 1; + *x = sx; + *y = sy; + *h = sh; + return 0; +} + +static int +sendcaret(Im *im) +{ + unsigned char buf[Ipccaretsz]; + int valid; + int32_t x, y, h; + + if(im->fd < 0 || !im->ext) + return 0; + caretget(im, &valid, &x, &y, &h); + if(im->caretsent && valid == im->sentvalid && + (!valid || (x == im->sentx && y == im->senty && h == im->senth))) + return 0; + if(ipcpackcaret(buf, valid, x, y, h) < 0 || + ipcsend(im->fd, buf, sizeof buf) < 0) + return -1; + im->caretsent = 1; + im->sentvalid = valid; + im->sentx = x; + im->senty = y; + im->senth = h; + return 0; +} + +static int +srvconnect(Im *im) +{ + unsigned char buf[Ipcreqsz]; + char commit[Ipcfieldmax+1]; + Ipcresp resp; + + if(im->fd >= 0) + return 0; + im->fd = ipcconnect(); + if(im->fd < 0) + return -1; + /* Marker zero is an old daemon's harmless key-zero response. */ + ipcpackcap(buf, im->usepreedit); + if(ipcsend(im->fd, buf, sizeof buf) < 0 || + readresp(im, im->usepreedit, im->usepreedit, commit, + sizeof commit, &resp) < 0){ + srvclose(im); + return -1; + } + im->ext = resp.eaten != 0; + im->effective = im->ext ? im->usepreedit : 1; + if(!im->effective) + setpreedit(im, "", 0); + if(im->ext && sendcaret(im) < 0){ + srvclose(im); + return -1; + } + return 0; } static uint32_t @@ -101,16 +232,21 @@ static void sendreset(Im *im) { unsigned char buf[Ipcreqsz]; - char resp[Ipcfieldmax+1]; + char commit[Ipcfieldmax+1]; + Ipcresp resp; if(im->fd < 0){ + setpreedit(im, "", 0); + return; + } + ipcpackreset(buf, im->effective); + if(ipcsend(im->fd, buf, sizeof buf) < 0 || + readresp(im, im->effective, 0, commit, + sizeof commit, &resp) < 0){ srvclose(im); return; } - ipcpackreset(buf, 1); - if(ipcsend(im->fd, buf, sizeof buf) < 0 || - readresp(im, resp, sizeof(resp)) < 0) - srvclose(im); + setpreedit(im, "", 0); } static gboolean @@ -134,10 +270,9 @@ kpress(GtkIMContext *ctx, GdkEventKey *ev) { Im *im; unsigned char buf[Ipcreqsz]; - char resp[Ipcfieldmax+1]; + char commit[Ipcfieldmax+1]; uint32_t key, mod; - int r; - GtkInputPurpose purpose; + Ipcresp resp; im = (Im*)ctx; if(ev->type != GDK_KEY_PRESS) @@ -146,27 +281,25 @@ kpress(GtkIMContext *ctx, GdkEventKey *ev) if(key == 0) return FALSE; mod = mget(ev->state); - g_object_get(ctx, "input-purpose", &purpose, NULL); - if(purpose == GTK_INPUT_PURPOSE_PASSWORD || purpose == GTK_INPUT_PURPOSE_PIN) + if(im->private) return plaincommit(ctx, key, mod); - srvconnect(im); - if(im->fd < 0){ + if(srvconnect(im) < 0) + return plaincommit(ctx, key, mod); + /* A retained GDK window may have moved since the last cursor report. */ + if(sendcaret(im) < 0){ srvclose(im); return plaincommit(ctx, key, mod); } - ipcpackreq(buf, 1, mod, key); - if(ipcsend(im->fd, buf, sizeof buf) < 0){ + ipcpackreq(buf, im->effective, mod, key); + if(ipcsend(im->fd, buf, sizeof buf) < 0 || + readresp(im, im->effective, im->effective, commit, + sizeof commit, &resp) < 0){ srvclose(im); return plaincommit(ctx, key, mod); } - r = readresp(im, resp, sizeof(resp)); - if(r < 0){ - srvclose(im); - return plaincommit(ctx, key, mod); - } - if(resp[0] != '\0') - g_signal_emit_by_name(ctx, "commit", resp); - if(r != 0) + if(commit[0] != '\0') + g_signal_emit_by_name(ctx, "commit", commit); + if(resp.eaten) return TRUE; return plaincommit(ctx, key, mod); } @@ -211,23 +344,121 @@ focusout(GtkIMContext *ctx) srvclose(im); } +static void +setusepreedit(GtkIMContext *ctx, gboolean use) +{ + Im *im; + unsigned char buf[Ipcreqsz]; + char commit[Ipcfieldmax+1]; + Ipcresp resp; + + im = (Im*)ctx; + use = use != FALSE; + if(im->usepreedit == use) + return; + im->usepreedit = use; + if(im->fd < 0 || !im->ext){ + im->effective = im->ext ? use : 1; + return; + } + ipcpackcap(buf, use); + if(ipcsend(im->fd, buf, sizeof buf) < 0 || + readresp(im, use, use, commit, sizeof commit, &resp) < 0 || + !resp.eaten){ + srvclose(im); + return; + } + im->effective = use; + if(!use) + setpreedit(im, "", 0); +} + +static void +setclientwindow(GtkIMContext *ctx, GdkWindow *win) +{ + Im *im; + + im = (Im*)ctx; + if(win != NULL && gdk_window_is_destroyed(win)) + win = NULL; + if(im->win == win) + return; + if(win != NULL) + g_object_ref(win); + dropwindow(im); + im->win = win; + if(sendcaret(im) < 0) + srvclose(im); +} + +static void +setcursorlocation(GtkIMContext *ctx, GdkRectangle *area) +{ + Im *im; + + im = (Im*)ctx; + if(im->win != NULL && gdk_window_is_destroyed(im->win)) + dropwindow(im); + if(area == NULL || im->win == NULL) + im->cursorvalid = 0; + else{ + im->cursor = *area; + im->cursorvalid = 1; + } + if(sendcaret(im) < 0) + srvclose(im); +} + +static int +isprivate(GtkInputPurpose purpose) +{ + return purpose == GTK_INPUT_PURPOSE_PASSWORD || + purpose == GTK_INPUT_PURPOSE_PIN; +} + +static void +purposechanged(GObject *obj, GParamSpec *pspec, gpointer data) +{ + Im *im; + GtkInputPurpose purpose; + int private; + + (void)pspec; + (void)data; + im = (Im*)obj; + g_object_get(obj, "input-purpose", &purpose, NULL); + private = isprivate(purpose); + if(private == im->private) + return; + im->private = private; + if(private) + sendreset(im); +} + static void finalize(GObject *obj) { Im *im; im = (Im*)obj; - if(im->fd >= 0) - close(im->fd); + srvclose(im); + dropwindow(im); G_OBJECT_CLASS(g_type_class_peek_parent(G_OBJECT_GET_CLASS(obj)))->finalize(obj); } static void init(Im *im) { + GtkInputPurpose purpose; + im->fd = -1; + im->usepreedit = 1; + im->effective = 1; im->pre[0] = '\0'; - im->prelen = 0; + g_object_get(im, "input-purpose", &purpose, NULL); + im->private = isprivate(purpose); + g_signal_connect(im, "notify::input-purpose", + G_CALLBACK(purposechanged), NULL); } static void @@ -242,6 +473,9 @@ classinit(ImClass *klass) ic->get_preedit_string = getpreedit; ic->reset = reset; ic->focus_out = focusout; + ic->set_use_preedit = setusepreedit; + ic->set_client_window = setclientwindow; + ic->set_cursor_location = setcursorlocation; oc->finalize = finalize; } diff --git a/tests/Makefile b/tests/Makefile index d79bfff..7fb0b74 100644 --- a/tests/Makefile +++ b/tests/Makefile @@ -6,6 +6,8 @@ TEXT_CFLAGS = $(shell pkg-config --cflags pangocairo cairo) TEXT_LIBS = $(shell pkg-config --libs pangocairo cairo) DBUS_CFLAGS = $(shell pkg-config --cflags dbus-1) DBUS_LIBS = $(shell pkg-config --libs dbus-1) +GTK_CFLAGS = $(shell pkg-config --cflags gtk+-3.0) +GTK_LIBS = $(shell pkg-config --libs gtk+-3.0) IBUS_CFLAGS = $(shell pkg-config --cflags dbus-1 xkbcommon) IBUS_LIBS = $(shell pkg-config --libs dbus-1 xkbcommon) XIM_CFLAGS = $(shell pkg-config --cflags xcb-imdkit xkbcommon) @@ -13,7 +15,7 @@ XIM_LIBS = $(shell pkg-config --libs xcb-imdkit xkbcommon) LIBS = -lthread -lbio $(TEXT_LIBS) $(IBUS_LIBS) $(XIM_LIBS) PROG = unit_test -LIVE = ibus_live_test ipc_live_test daemon_collision_test daemon_failure_test \ +LIVE = ibus_live_test gtk_live_test ipc_live_test daemon_collision_test daemon_failure_test \ daemon_restart_test TESTSRC = unit_test.c test_util.c str_test.c hash_test.c trie_test.c \ ko_test.c vi_test.c engine_test.c dict_test.c ipc_test.c \ @@ -30,6 +32,7 @@ all: $(PROG) $(LIVE) check test: $(PROG) $(LIVE) ./$(PROG) $(TESTARGS) ./ibus_live_test ../strans ../map + ./gtk_live_test ../gtk/im-strans.so ./ipc_live_test ../strans ../map ./daemon_collision_test ../strans ../map ./daemon_failure_test ../strans @@ -41,6 +44,10 @@ $(PROG): $(OBJS) ibus_live_test: ibus_live_test.c $(HOSTCC) -std=c99 -Wall -Wextra -O2 -g $(DBUS_CFLAGS) -o $@ $< $(DBUS_LIBS) +gtk_live_test: gtk_live_test.c ../ipc.c ../ipc.h + $(HOSTCC) -std=c99 -Wall -Wextra -O2 -g -I.. $(GTK_CFLAGS) -o $@ \ + gtk_live_test.c ../ipc.c $(GTK_LIBS) -pthread + ipc_live_test: ipc_live_test.c ../ipc.c ../ipc.h $(HOSTCC) -std=c99 -Wall -Wextra -O2 -g -I.. -o $@ ipc_live_test.c ../ipc.c diff --git a/tests/gtk_live_test.c b/tests/gtk_live_test.c new file mode 100644 index 0000000..3b824b8 --- /dev/null +++ b/tests/gtk_live_test.c @@ -0,0 +1,928 @@ +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "ipc.h" + +enum +{ + Maxevent = 256, + Timeout = 4000, +}; + +enum +{ + Ecap, + Ecaret, + Ekey, + Ereset, +}; + +typedef struct Event Event; +typedef struct Server Server; +typedef struct Siglog Siglog; + +struct Event +{ + int type; + int want; + int valid; + uint32_t key; + int32_t x; + int32_t y; + int32_t h; +}; + +struct Server +{ + pthread_t thread; + pthread_mutex_t lock; + pthread_cond_t changed; + int listenfd; + int wake[2]; + int stop; + int old; + int nclose; + char pre[Ipcfieldmax+1]; + Event ev[Maxevent]; + int nev; +}; + +struct Siglog +{ + char event[128]; + int n; + char pre[Ipcfieldmax+1]; + char commit[Ipcfieldmax+1]; +}; + +static int +fail(char *fmt, ...) +{ + va_list ap; + + fprintf(stderr, "gtk_live_test: "); + va_start(ap, fmt); + vfprintf(stderr, fmt, ap); + va_end(ap); + fputc('\n', stderr); + return 0; +} + +static int64_t +nowms(void) +{ + struct timespec ts; + + if(clock_gettime(CLOCK_MONOTONIC, &ts) < 0) + return 0; + return (int64_t)ts.tv_sec * 1000 + ts.tv_nsec / 1000000; +} + +static int +leftms(int64_t deadline) +{ + int64_t n; + + n = deadline - nowms(); + if(n <= 0) + return 0; + if(n > 0x7fffffff) + return 0x7fffffff; + return n; +} + +static void +record(Server *s, Event *e) +{ + pthread_mutex_lock(&s->lock); + if(s->nev < Maxevent) + s->ev[s->nev++] = *e; + pthread_cond_broadcast(&s->changed); + pthread_mutex_unlock(&s->lock); +} + +static void +closed(Server *s) +{ + pthread_mutex_lock(&s->lock); + s->nclose++; + pthread_cond_broadcast(&s->changed); + pthread_mutex_unlock(&s->lock); +} + +static int +sendresponse(Server *s, int fd, int eaten, int want) +{ + unsigned char buf[Ipcmaxresp]; + char pre[Ipcfieldmax+1]; + int n; + + pthread_mutex_lock(&s->lock); + memcpy(pre, s->pre, sizeof pre); + pthread_mutex_unlock(&s->lock); + n = ipcpackresp(buf, sizeof buf, eaten, "", 0, pre, strlen(pre), want); + return n >= 0 && ipcsend(fd, buf, n) == 0; +} + +static int +request(Server *s, int fd) +{ + unsigned char buf[Ipccaretsz]; + uint32_t mod, key; + Event e; + int old, type, want; + + if(ipcreadn(fd, buf, Ipcreqsz) < 0) + return 0; + type = ipcreqtype(buf); + memset(&e, 0, sizeof e); + switch(type){ + case Ipccap: + e.type = Ecap; + ipcunpackreq(buf, &e.want, &mod, &key); + record(s, &e); + pthread_mutex_lock(&s->lock); + old = s->old; + pthread_mutex_unlock(&s->lock); + return sendresponse(s, fd, !old, e.want); + case Ipccaret: + if(ipcreadn(fd, buf + Ipcreqsz, Ipccaretsz - Ipcreqsz) < 0 || + ipcunpackcaret(buf, &e.valid, &e.x, &e.y, &e.h) < 0) + return 0; + e.type = Ecaret; + record(s, &e); + return 1; + case Ipclegacy: + ipcunpackreq(buf, &want, &mod, &key); + e.want = want; + e.key = key; + e.type = ipcreqreset(buf) ? Ereset : Ekey; + record(s, &e); + return sendresponse(s, fd, e.type == Ekey, want); + } + return 0; +} + +static void* +serverthread(void *v) +{ + Server *s; + struct pollfd pfd[3]; + int fd, n; + char c; + + s = v; + fd = -1; + for(;;){ + pfd[0].fd = s->wake[0]; + pfd[0].events = POLLIN; + pfd[1].fd = s->listenfd; + pfd[1].events = fd < 0 ? POLLIN : 0; + pfd[2].fd = fd; + pfd[2].events = fd >= 0 ? POLLIN : 0; + n = poll(pfd, 3, -1); + if(n < 0 && errno == EINTR) + continue; + if(n < 0 || (pfd[0].revents & POLLIN)) + break; + if(pfd[1].revents & POLLIN){ + fd = accept(s->listenfd, NULL, NULL); + continue; + } + if(fd >= 0 && pfd[2].revents != 0){ + if((pfd[2].revents & POLLIN) && request(s, fd)) + continue; + close(fd); + fd = -1; + closed(s); + } + } + if(fd >= 0){ + close(fd); + closed(s); + } + while(read(s->wake[0], &c, 1) < 0 && errno == EINTR) + ; + return NULL; +} + +static int +startserver(Server *s, char *path) +{ + struct sockaddr_un addr; + int cond; + + memset(s, 0, sizeof *s); + s->listenfd = -1; + s->wake[0] = s->wake[1] = -1; + if(pthread_mutex_init(&s->lock, NULL) != 0) + return fail("initialize server lock"); + cond = pthread_cond_init(&s->changed, NULL) == 0; + if(!cond){ + fail("initialize server condition"); + goto bad; + } + if(pipe(s->wake) < 0){ + fail("pipe: %s", strerror(errno)); + goto bad; + } + s->listenfd = socket(AF_UNIX, SOCK_STREAM, 0); + if(s->listenfd < 0){ + fail("socket: %s", strerror(errno)); + goto bad; + } + memset(&addr, 0, sizeof addr); + addr.sun_family = AF_UNIX; + if(strlen(path) >= sizeof addr.sun_path){ + fail("socket path is too long"); + goto bad; + } + strcpy(addr.sun_path, path); + if(bind(s->listenfd, (struct sockaddr*)&addr, sizeof addr) < 0 || + listen(s->listenfd, 4) < 0){ + fail("listen %s: %s", path, strerror(errno)); + goto bad; + } + if(pthread_create(&s->thread, NULL, serverthread, s) != 0){ + fail("create server thread"); + goto bad; + } + return 1; +bad: + if(s->listenfd >= 0) + close(s->listenfd); + if(s->wake[0] >= 0) + close(s->wake[0]); + if(s->wake[1] >= 0) + close(s->wake[1]); + if(cond) + pthread_cond_destroy(&s->changed); + pthread_mutex_destroy(&s->lock); + s->listenfd = s->wake[0] = s->wake[1] = -1; + return 0; +} + +static void +stopserver(Server *s) +{ + char c; + + if(s->wake[1] >= 0){ + c = 0; + while(write(s->wake[1], &c, 1) < 0 && errno == EINTR) + ; + pthread_join(s->thread, NULL); + } + if(s->listenfd >= 0) + close(s->listenfd); + if(s->wake[0] >= 0) + close(s->wake[0]); + if(s->wake[1] >= 0) + close(s->wake[1]); + pthread_cond_destroy(&s->changed); + pthread_mutex_destroy(&s->lock); + s->listenfd = s->wake[0] = s->wake[1] = -1; +} + +static void +setpre(Server *s, char *pre) +{ + pthread_mutex_lock(&s->lock); + snprintf(s->pre, sizeof s->pre, "%s", pre); + pthread_mutex_unlock(&s->lock); +} + +static void +setold(Server *s, int old) +{ + pthread_mutex_lock(&s->lock); + s->old = old; + pthread_mutex_unlock(&s->lock); +} + +static int +eventcount(Server *s) +{ + int n; + + pthread_mutex_lock(&s->lock); + n = s->nev; + pthread_mutex_unlock(&s->lock); + return n; +} + +static int +closecount(Server *s) +{ + int n; + + pthread_mutex_lock(&s->lock); + n = s->nclose; + pthread_mutex_unlock(&s->lock); + return n; +} + +static int +waitcount(Server *s, int n) +{ + struct timespec ts; + int r; + + clock_gettime(CLOCK_REALTIME, &ts); + ts.tv_sec += Timeout / 1000; + ts.tv_nsec += (Timeout % 1000) * 1000000; + if(ts.tv_nsec >= 1000000000){ + ts.tv_sec++; + ts.tv_nsec -= 1000000000; + } + pthread_mutex_lock(&s->lock); + while(s->nev < n){ + r = pthread_cond_timedwait(&s->changed, &s->lock, &ts); + if(r == ETIMEDOUT) + break; + } + r = s->nev >= n; + pthread_mutex_unlock(&s->lock); + return r; +} + +static int +waitclose(Server *s, int n) +{ + struct timespec ts; + int r; + + clock_gettime(CLOCK_REALTIME, &ts); + ts.tv_sec += Timeout / 1000; + pthread_mutex_lock(&s->lock); + while(s->nclose < n){ + r = pthread_cond_timedwait(&s->changed, &s->lock, &ts); + if(r == ETIMEDOUT) + break; + } + r = s->nclose >= n; + pthread_mutex_unlock(&s->lock); + return r; +} + +static Event +getevent(Server *s, int n) +{ + Event e; + + memset(&e, 0, sizeof e); + pthread_mutex_lock(&s->lock); + if(n >= 0 && n < s->nev) + e = s->ev[n]; + pthread_mutex_unlock(&s->lock); + return e; +} + +static int +runquery(char *module, char *cache) +{ + int fd, status; + pid_t pid; + + fd = open(cache, O_WRONLY|O_CREAT|O_TRUNC, 0600); + if(fd < 0) + return fail("open module cache: %s", strerror(errno)); + pid = fork(); + if(pid < 0){ + close(fd); + return fail("fork gtk-query-immodules: %s", strerror(errno)); + } + if(pid == 0){ + dup2(fd, STDOUT_FILENO); + close(fd); + execlp("gtk-query-immodules-3.0", "gtk-query-immodules-3.0", + module, (char*)NULL); + _exit(127); + } + close(fd); + if(waitpid(pid, &status, 0) != pid || !WIFEXITED(status) || + WEXITSTATUS(status) != 0) + return fail("gtk-query-immodules-3.0 failed"); + return 1; +} + +static int +startxvfb(pid_t *pidp, char *display, size_t ndisplay) +{ + struct pollfd pfd; + char fdarg[32], line[32]; + int p[2], n, status; + pid_t pid; + + if(pipe(p) < 0) + return fail("Xvfb display pipe: %s", strerror(errno)); + pid = fork(); + if(pid < 0){ + close(p[0]); + close(p[1]); + return fail("fork Xvfb: %s", strerror(errno)); + } + if(pid == 0){ + close(p[0]); + snprintf(fdarg, sizeof fdarg, "%d", p[1]); + execlp("Xvfb", "Xvfb", "-displayfd", fdarg, "-screen", "0", + "1024x768x24", "-nolisten", "tcp", "-noreset", (char*)NULL); + _exit(127); + } + close(p[1]); + pfd.fd = p[0]; + pfd.events = POLLIN; + if(poll(&pfd, 1, Timeout) <= 0){ + kill(pid, SIGTERM); + waitpid(pid, NULL, 0); + close(p[0]); + return fail("Xvfb did not publish a display"); + } + n = read(p[0], line, sizeof line - 1); + close(p[0]); + if(n <= 0){ + waitpid(pid, &status, 0); + return fail("read Xvfb display number"); + } + line[n] = '\0'; + line[strcspn(line, "\r\n")] = '\0'; + if(snprintf(display, ndisplay, ":%s", line) >= (int)ndisplay){ + kill(pid, SIGTERM); + waitpid(pid, NULL, 0); + return fail("Xvfb display number is too long"); + } + *pidp = pid; + return 1; +} + +static void +stopxvfb(pid_t *pidp) +{ + int status; + int64_t deadline; + + if(*pidp <= 0) + return; + kill(*pidp, SIGTERM); + deadline = nowms() + Timeout; + while(waitpid(*pidp, &status, WNOHANG) == 0 && leftms(deadline) > 0) + g_usleep(10000); + if(waitpid(*pidp, &status, WNOHANG) == 0){ + kill(*pidp, SIGKILL); + waitpid(*pidp, &status, 0); + } + *pidp = -1; +} + +static void +pump(void) +{ + while(gtk_events_pending()) + gtk_main_iteration(); +} + +static void +logsig(Siglog *l, char c) +{ + if(l->n + 1 < (int)sizeof l->event){ + l->event[l->n++] = c; + l->event[l->n] = '\0'; + } +} + +static void +prestart(GtkIMContext *ctx, Siglog *l) +{ + (void)ctx; + logsig(l, 'S'); +} + +static void +prechange(GtkIMContext *ctx, Siglog *l) +{ + gchar *s; + + gtk_im_context_get_preedit_string(ctx, &s, NULL, NULL); + snprintf(l->pre, sizeof l->pre, "%s", s); + g_free(s); + logsig(l, 'C'); +} + +static void +preend(GtkIMContext *ctx, Siglog *l) +{ + (void)ctx; + logsig(l, 'E'); +} + +static void +commit(GtkIMContext *ctx, gchar *s, Siglog *l) +{ + (void)ctx; + snprintf(l->commit, sizeof l->commit, "%s", s); + logsig(l, 'K'); +} + +static void +weakgone(void *v, GObject *obj) +{ + int *gone; + + (void)obj; + gone = v; + *gone = 1; +} + +static void +clearlog(Siglog *l) +{ + memset(l, 0, sizeof *l); +} + +static GtkIMContext* +newcontext(Siglog *l) +{ + GtkIMContext *ctx; + + ctx = gtk_im_multicontext_new(); + gtk_im_multicontext_set_context_id(GTK_IM_MULTICONTEXT(ctx), "strans"); + g_signal_connect(ctx, "preedit-start", G_CALLBACK(prestart), l); + g_signal_connect(ctx, "preedit-changed", G_CALLBACK(prechange), l); + g_signal_connect(ctx, "preedit-end", G_CALLBACK(preend), l); + g_signal_connect(ctx, "commit", G_CALLBACK(commit), l); + return ctx; +} + +static int +key(GtkIMContext *ctx, GdkWindow *win, guint keyval) +{ + GdkEvent *ev; + int r; + + ev = gdk_event_new(GDK_KEY_PRESS); + ev->key.window = g_object_ref(win); + ev->key.send_event = TRUE; + ev->key.time = GDK_CURRENT_TIME; + ev->key.keyval = keyval; + r = gtk_im_context_filter_keypress(ctx, &ev->key); + gdk_event_free(ev); + return r; +} + +static GtkWidget* +newwindow(int x, int y) +{ + GtkWidget *w; + + w = gtk_window_new(GTK_WINDOW_TOPLEVEL); + gtk_window_set_default_size(GTK_WINDOW(w), 240, 120); + gtk_window_move(GTK_WINDOW(w), x, y); + gtk_widget_show(w); + pump(); + return w; +} + +#define Check(c, ...) do{ if(!(c)){ fail(__VA_ARGS__); goto out; } }while(0) + +int +main(int argc, char **argv) +{ + Server srv; + Siglog log; + GtkIMContext *ctx, *oldctx, *badctx; + GtkWidget *top, *other; + GdkWindow *win, *win2; + GdkRectangle rect; + Event e; + char root[] = "/tmp/strans-gtk.XXXXXX"; + char runtime[320] = "", socket[384] = "", cache[384] = ""; + char display[32]; + char *module; + pid_t xvfb; + int first, i, ox, oy, scale, closes, gone, ok, rootmade, serverup; + + ctx = oldctx = badctx = NULL; + top = other = NULL; + xvfb = -1; + rootmade = 0; + serverup = 0; + ok = 0; + if(argc != 2) + return fail("usage: gtk_live_test /path/to/im-strans.so"); + module = realpath(argv[1], NULL); + if(module == NULL) + return fail("realpath %s: %s", argv[1], strerror(errno)); + Check(mkdtemp(root) != NULL, "mkdtemp: %s", strerror(errno)); + rootmade = 1; + Check(snprintf(runtime, sizeof runtime, "%s/runtime", root) < + (int)sizeof runtime, "runtime path is too long"); + Check(snprintf(socket, sizeof socket, "%s/strans.sock", runtime) < + (int)sizeof socket, "socket path is too long"); + Check(snprintf(cache, sizeof cache, "%s/immodules.cache", root) < + (int)sizeof cache, "cache path is too long"); + Check(mkdir(runtime, 0700) == 0, "mkdir runtime: %s", strerror(errno)); + Check(startxvfb(&xvfb, display, sizeof display), "start private Xvfb"); + Check(setenv("DISPLAY", display, 1) == 0 && + setenv("GDK_BACKEND", "x11", 1) == 0 && + setenv("GDK_SCALE", "2", 1) == 0 && + setenv("XDG_RUNTIME_DIR", runtime, 1) == 0, + "set private environment: %s", strerror(errno)); + Check(runquery(module, cache), "generate GTK module cache"); + Check(setenv("GTK_IM_MODULE_FILE", cache, 1) == 0 && + setenv("GTK_IM_MODULE", "strans", 1) == 0, + "set GTK module environment: %s", strerror(errno)); + Check(startserver(&srv, socket), "start fake daemon"); + serverup = 1; + gtk_init(&argc, &argv); + + clearlog(&log); + ctx = newcontext(&log); + top = newwindow(73, 41); + win = gtk_widget_get_window(top); + gtk_im_context_set_client_window(ctx, win); + rect.x = 11; + rect.y = 17; + rect.width = 3; + rect.height = 19; + gtk_im_context_set_cursor_location(ctx, &rect); + Check(eventcount(&srv) == 0, "cursor reporting opened the socket"); + setpre(&srv, "pre"); + Check(key(ctx, win, GDK_KEY_a), "initial key was not eaten"); + Check(waitcount(&srv, 3), "initial protocol frames timed out"); + e = getevent(&srv, 0); + Check(e.type == Ecap && e.want == 1, "default preedit capability missing"); + e = getevent(&srv, 1); + Check(e.type == Ecaret && e.valid, "cached caret was not negotiated"); + gdk_window_get_origin(win, &ox, &oy); + Check(ox != 0 && oy != 0, "test window did not get a nonzero root origin"); + scale = gdk_window_get_scale_factor(win); + Check(scale == 2, "private Xvfb did not honor GDK_SCALE=2"); + Check(e.x == (ox + rect.x) * scale && + e.y == (oy + rect.y) * scale && e.h == rect.height * scale, + "caret conversion got %d,%d,%d want %d,%d,%d", + e.x, e.y, e.h, (ox + rect.x) * scale, + (oy + rect.y) * scale, rect.height * scale); + e = getevent(&srv, 2); + Check(e.type == Ekey && e.want == 1, "initial key framing changed"); + Check(strcmp(log.event, "SC") == 0 && strcmp(log.pre, "pre") == 0, + "initial preedit signals were %s (%s)", log.event, log.pre); + + clearlog(&log); + first = eventcount(&srv); + gtk_im_context_set_use_preedit(ctx, FALSE); + Check(waitcount(&srv, first + 1), "disable capability timed out"); + e = getevent(&srv, first); + Check(e.type == Ecap && e.want == 0, "disable capability frame missing"); + Check(strcmp(log.event, "CE") == 0 && log.pre[0] == '\0', + "disable signal order was %s (%s)", log.event, log.pre); + clearlog(&log); + gtk_im_context_set_use_preedit(ctx, FALSE); + Check(log.n == 0, "repeated disable emitted %s", log.event); + first = eventcount(&srv); + gtk_im_context_set_use_preedit(ctx, TRUE); + Check(waitcount(&srv, first + 1), "enable capability timed out"); + e = getevent(&srv, first); + Check(e.type == Ecap && e.want == 1, "enable capability frame missing"); + Check(strcmp(log.event, "SC") == 0 && strcmp(log.pre, "pre") == 0, + "enable signal order was %s (%s)", log.event, log.pre); + + /* The retained relative rectangle follows a moved window before a key. */ + gtk_window_move(GTK_WINDOW(top), 137, 89); + pump(); + first = eventcount(&srv); + setpre(&srv, "pre2"); + Check(key(ctx, win, GDK_KEY_b), "key after move was not eaten"); + Check(waitcount(&srv, first + 2), "moved caret/key timed out"); + e = getevent(&srv, first); + Check(e.type == Ecaret && e.valid, "window move did not refresh caret"); + gdk_window_get_origin(win, &ox, &oy); + Check(e.x == (ox + rect.x) * scale && e.y == (oy + rect.y) * scale, + "moved caret root conversion is wrong"); + Check(getevent(&srv, first + 1).type == Ekey, + "caret did not precede moved-window key"); + + other = newwindow(301, 157); + win2 = gtk_widget_get_window(other); + first = eventcount(&srv); + gtk_im_context_set_client_window(ctx, win2); + Check(waitcount(&srv, first + 1), "window replacement invalidation timed out"); + e = getevent(&srv, first); + Check(e.type == Ecaret && !e.valid, "window replacement retained old caret"); + rect.x = 7; + rect.y = 9; + rect.height = 13; + first = eventcount(&srv); + gtk_im_context_set_cursor_location(ctx, &rect); + Check(waitcount(&srv, first + 1), "replacement caret timed out"); + e = getevent(&srv, first); + gdk_window_get_origin(win2, &ox, &oy); + scale = gdk_window_get_scale_factor(win2); + Check(e.type == Ecaret && e.valid && e.x == (ox + rect.x) * scale && + e.y == (oy + rect.y) * scale && e.h == rect.height * scale, + "replacement window caret is wrong"); + first = eventcount(&srv); + gtk_im_context_set_client_window(ctx, NULL); + Check(waitcount(&srv, first + 1), "NULL window invalidation timed out"); + e = getevent(&srv, first); + Check(e.type == Ecaret && !e.valid, "NULL window did not invalidate caret"); + gtk_im_context_set_client_window(ctx, win2); + rect.x = 1; + rect.y = 1; + rect.height = 1; + first = eventcount(&srv); + gtk_im_context_set_cursor_location(ctx, &rect); + Check(waitcount(&srv, first + 1), "pre-overflow valid caret timed out"); + Check(getevent(&srv, first).type == Ecaret && + getevent(&srv, first).valid, "pre-overflow caret is not valid"); + rect.x = G_MAXINT; + rect.y = 0; + rect.height = 1; + first = eventcount(&srv); + gtk_im_context_set_cursor_location(ctx, &rect); + Check(waitcount(&srv, first + 1), "overflow invalidation timed out"); + e = getevent(&srv, first); + Check(e.type == Ecaret && !e.valid, "overflow caret was accepted"); + rect.x = 4; + rect.y = 5; + rect.height = -1; + first = eventcount(&srv); + gtk_im_context_set_cursor_location(ctx, &rect); + if(eventcount(&srv) > first){ + e = getevent(&srv, first); + Check(e.type == Ecaret && !e.valid, "negative caret height was accepted"); + } + + /* A destroyed retained window invalidates placement on the next key. */ + rect.x = 4; + rect.y = 5; + rect.height = 10; + first = eventcount(&srv); + gtk_im_context_set_cursor_location(ctx, &rect); + Check(waitcount(&srv, first + 1) && getevent(&srv, first).valid, + "pre-destruction caret is not valid"); + gone = 0; + g_object_weak_ref(G_OBJECT(win2), weakgone, &gone); + gtk_widget_destroy(other); + other = NULL; + Check(!gone, "module did not retain its client window"); + first = eventcount(&srv); + setpre(&srv, "destroyed"); + Check(key(ctx, win2, GDK_KEY_q), "destroyed-window key was not eaten"); + Check(waitcount(&srv, first + 2), "destroyed-window frames timed out"); + e = getevent(&srv, first); + Check(e.type == Ecaret && !e.valid && + getevent(&srv, first + 1).type == Ekey, + "destroyed window did not invalidate before the key"); + gtk_im_context_set_client_window(ctx, win); + pump(); + if(!gone) + g_object_weak_unref(G_OBJECT(win2), weakgone, &gone); + win2 = win; + gtk_im_context_set_cursor_location(ctx, &rect); + + /* PASSWORD and PIN transitions reset an existing composition. */ + gtk_im_context_set_client_window(ctx, win2); + rect.height = 12; + gtk_im_context_set_cursor_location(ctx, &rect); + setpre(&srv, "secret"); + clearlog(&log); + Check(key(ctx, win2, GDK_KEY_c), "password setup key was not eaten"); + Check(strcmp(log.pre, "secret") == 0, "password setup preedit missing"); + clearlog(&log); + first = eventcount(&srv); + g_object_set(ctx, "input-purpose", GTK_INPUT_PURPOSE_PASSWORD, NULL); + Check(waitcount(&srv, first + 1), "password reset timed out"); + Check(getevent(&srv, first).type == Ereset, "password did not reset owner"); + Check(strcmp(log.event, "CE") == 0, "password clear order was %s", log.event); + clearlog(&log); + first = eventcount(&srv); + Check(key(ctx, win2, GDK_KEY_x), "password fallback did not commit"); + Check(eventcount(&srv) == first && strcmp(log.commit, "x") == 0, + "password key reached daemon or did not commit"); + g_object_set(ctx, "input-purpose", GTK_INPUT_PURPOSE_FREE_FORM, NULL); + setpre(&srv, "pin"); + clearlog(&log); + Check(key(ctx, win2, GDK_KEY_d), "PIN setup key was not eaten"); + clearlog(&log); + first = eventcount(&srv); + g_object_set(ctx, "input-purpose", GTK_INPUT_PURPOSE_PIN, NULL); + Check(waitcount(&srv, first + 1), "PIN reset timed out"); + Check(getevent(&srv, first).type == Ereset && strcmp(log.event, "CE") == 0, + "PIN transition did not reset and clear"); + + g_object_set(ctx, "input-purpose", GTK_INPUT_PURPOSE_FREE_FORM, NULL); + setpre(&srv, "reset"); + clearlog(&log); + Check(key(ctx, win2, GDK_KEY_e), "reset setup key was not eaten"); + clearlog(&log); + first = eventcount(&srv); + gtk_im_context_reset(ctx); + Check(waitcount(&srv, first + 1), "explicit reset timed out"); + Check(getevent(&srv, first).type == Ereset && strcmp(log.event, "CE") == 0, + "explicit reset did not clear preedit"); + setpre(&srv, "focus"); + clearlog(&log); + Check(key(ctx, win2, GDK_KEY_f), "focus setup key was not eaten"); + clearlog(&log); + first = eventcount(&srv); + closes = closecount(&srv); + gtk_im_context_focus_out(ctx); + Check(waitcount(&srv, first + 1) && getevent(&srv, first).type == Ereset, + "focus-out reset missing"); + Check(waitclose(&srv, closes + 1), "focus-out did not close connection"); + Check(strcmp(log.event, "CE") == 0, "focus-out clear order was %s", log.event); + + /* Finalization closes its connection and drops the retained GdkWindow. */ + gtk_im_context_set_client_window(ctx, win2); + setpre(&srv, "final"); + clearlog(&log); + Check(key(ctx, win2, GDK_KEY_g), "finalization setup key was not eaten"); + closes = closecount(&srv); + g_object_unref(ctx); + ctx = NULL; + Check(waitclose(&srv, closes + 1), "finalization did not close connection"); + + /* A new module talking to an old daemon keeps legacy inline preedit. */ + setold(&srv, 1); + clearlog(&log); + oldctx = newcontext(&log); + gtk_im_context_set_client_window(oldctx, win2); + rect.x = 2; + rect.y = 3; + rect.height = 10; + gtk_im_context_set_cursor_location(oldctx, &rect); + gtk_im_context_set_use_preedit(oldctx, FALSE); + setpre(&srv, "legacy"); + first = eventcount(&srv); + Check(key(oldctx, win2, GDK_KEY_h), "old-daemon key was not eaten"); + Check(waitcount(&srv, first + 2), "old-daemon exchange timed out"); + e = getevent(&srv, first); + Check(e.type == Ecap && e.want == 0, "old-daemon probe framing is wrong"); + e = getevent(&srv, first + 1); + Check(e.type == Ekey && e.want == 1, "old fallback did not request preedit"); + for(i = first; i < eventcount(&srv); i++) + Check(getevent(&srv, i).type != Ecaret, + "caret extension sent to old daemon"); + Check(strcmp(log.event, "SC") == 0 && strcmp(log.pre, "legacy") == 0, + "old fallback preedit was %s (%s)", log.event, log.pre); + closes = closecount(&srv); + g_object_unref(oldctx); + oldctx = NULL; + Check(waitclose(&srv, closes + 1), "old-daemon context did not close"); + + /* A failed first connection retains the existing plain-key fallback. */ + stopserver(&srv); + serverup = 0; + unlink(socket); + clearlog(&log); + badctx = newcontext(&log); + Check(key(badctx, win2, GDK_KEY_z), "connection failure did not fall back"); + Check(strcmp(log.commit, "z") == 0 && log.pre[0] == '\0', + "connection failure left stale preedit"); + + ok = 1; +out: + if(badctx != NULL) + g_object_unref(badctx); + if(oldctx != NULL) + g_object_unref(oldctx); + if(ctx != NULL) + g_object_unref(ctx); + if(other != NULL) + gtk_widget_destroy(other); + if(top != NULL) + gtk_widget_destroy(top); + pump(); + if(serverup) + stopserver(&srv); + if(socket[0] != '\0') + unlink(socket); + if(cache[0] != '\0') + unlink(cache); + if(runtime[0] != '\0') + rmdir(runtime); + if(rootmade) + rmdir(root); + stopxvfb(&xvfb); + free(module); + if(ok) + printf("gtk_live_test: ok (scale %d)\n", scale); + return !ok; +}