From 9804c1f55c56cf07e4a87b88f68dd79bf585d64d Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Fri, 14 Aug 2026 22:29:14 +0900 Subject: [PATCH] fix(gtk): restore compose fallback and clean lifecycle --- gtk/main.c | 164 ++++++++++++++------ tests/gtk_live_test.c | 345 +++++++++++++++++++++--------------------- 2 files changed, 285 insertions(+), 224 deletions(-) diff --git a/gtk/main.c b/gtk/main.c index c5f552f..0fa2e5d 100644 --- a/gtk/main.c +++ b/gtk/main.c @@ -1,3 +1,4 @@ +#include #include #include #include @@ -12,12 +13,14 @@ typedef struct Im Im; struct Im { - GtkIMContext parent; + GtkIMContextSimple parent; int fd; int usepreedit; - int effective; int ext; int private; + int simpleactive; + int insimple; + int simpledone; char pre[Ipcfieldmax+1]; int prelen; GdkWindow *win; @@ -33,10 +36,12 @@ struct Im typedef struct ImClass ImClass; struct ImClass { - GtkIMContextClass parent; + GtkIMContextSimpleClass parent; }; static GType imtype; +static GObjectClass *parentobject; +static GtkIMContextClass *parentim; static void setpreedit(Im *im, const char *s, int n) @@ -60,15 +65,32 @@ setpreedit(Im *im, const char *s, int n) } static void -srvclose(Im *im) +srvdrop(Im *im, int notify) { if(im->fd >= 0) close(im->fd); im->fd = -1; im->ext = 0; - im->effective = 1; im->caretsent = 0; - setpreedit(im, "", 0); + if(notify) + setpreedit(im, "", 0); + else{ + im->pre[0] = '\0'; + im->prelen = 0; + } +} + +static void +srvclose(Im *im) +{ + srvdrop(im, 1); +} + +static int +validtext(const char *s, size_t n) +{ + return s[n] == '\0' && memchr(s, '\0', n) == NULL && + g_utf8_validate(s, n, NULL); } static int @@ -80,6 +102,11 @@ readresp(Im *im, int want, int update, char *commit, int ncommit, if(ipcreadresp(im->fd, want, commit, ncommit, pre, sizeof pre, resp) < 0) return -1; + if(!validtext(commit, resp->commitlen) || + (want && !validtext(pre, resp->preeditlen))){ + errno = EPROTO; + return -1; + } if(update) setpreedit(im, pre, resp->preeditlen); return 0; @@ -188,8 +215,7 @@ srvconnect(Im *im) return -1; } im->ext = resp.eaten != 0; - im->effective = im->ext ? im->usepreedit : 1; - if(!im->effective) + if(!im->usepreedit) setpreedit(im, "", 0); if(im->ext && sendcaret(im) < 0){ srvclose(im); @@ -228,6 +254,40 @@ mget(uint32_t state) return m; } +static void +simplecommit(GtkIMContext *ctx, const char *s, Im *im) +{ + (void)ctx; + (void)s; + if(im->insimple) + im->simpledone = 1; +} + +static void +simpleend(GtkIMContext *ctx, Im *im) +{ + (void)ctx; + if(im->insimple) + im->simpledone = 1; +} + +static gboolean +simplefilter(GtkIMContext *ctx, GdkEventKey *ev, int release) +{ + Im *im; + gboolean r; + + im = (Im*)ctx; + im->simpleactive = 1; + im->simpledone = 0; + im->insimple = 1; + r = parentim->filter_keypress(ctx, ev); + im->insimple = 0; + if((!r && !release) || im->simpledone) + im->simpleactive = 0; + return r; +} + static void sendreset(Im *im) { @@ -239,9 +299,9 @@ sendreset(Im *im) setpreedit(im, "", 0); return; } - ipcpackreset(buf, im->effective); + ipcpackreset(buf, im->usepreedit); if(ipcsend(im->fd, buf, sizeof buf) < 0 || - readresp(im, im->effective, 0, commit, + readresp(im, im->usepreedit, 0, commit, sizeof commit, &resp) < 0){ srvclose(im); return; @@ -249,22 +309,6 @@ sendreset(Im *im) setpreedit(im, "", 0); } -static gboolean -plaincommit(GtkIMContext *ctx, uint32_t key, uint32_t mod) -{ - char u[8]; - int n; - - if(mod & (Mctrl|Malt|Msuper)) - return FALSE; - if(key < 0x20 || key >= Kspec) - return FALSE; - n = g_unichar_to_utf8(key, u); - u[n] = '\0'; - g_signal_emit_by_name(ctx, "commit", u); - return TRUE; -} - static gboolean kpress(GtkIMContext *ctx, GdkEventKey *ev) { @@ -275,33 +319,36 @@ kpress(GtkIMContext *ctx, GdkEventKey *ev) Ipcresp resp; im = (Im*)ctx; - if(ev->type != GDK_KEY_PRESS) + if(ev->type != GDK_KEY_PRESS){ + if(im->simpleactive) + return simplefilter(ctx, ev, 1); return FALSE; + } + if(im->simpleactive) + return simplefilter(ctx, ev, 0); key = kget(ev->keyval); - if(key == 0) - return FALSE; mod = mget(ev->state); - if(im->private) - return plaincommit(ctx, key, mod); + if(im->private || key == 0) + return simplefilter(ctx, ev, 0); if(srvconnect(im) < 0) - return plaincommit(ctx, key, mod); + return simplefilter(ctx, ev, 0); /* A retained GDK window may have moved since the last cursor report. */ if(sendcaret(im) < 0){ srvclose(im); - return plaincommit(ctx, key, mod); + return simplefilter(ctx, ev, 0); } - ipcpackreq(buf, im->effective, mod, key); + ipcpackreq(buf, im->usepreedit, mod, key); if(ipcsend(im->fd, buf, sizeof buf) < 0 || - readresp(im, im->effective, im->effective, commit, + readresp(im, im->usepreedit, im->usepreedit, commit, sizeof commit, &resp) < 0){ srvclose(im); - return plaincommit(ctx, key, mod); + return simplefilter(ctx, ev, 0); } if(commit[0] != '\0') g_signal_emit_by_name(ctx, "commit", commit); if(resp.eaten) return TRUE; - return plaincommit(ctx, key, mod); + return simplefilter(ctx, ev, 0); } static void @@ -312,6 +359,10 @@ getpreedit(GtkIMContext *ctx, gchar **str, PangoAttrList **attrs, PangoAttribute *u; im = (Im*)ctx; + if(im->simpleactive){ + parentim->get_preedit_string(ctx, str, attrs, cursor_pos); + return; + } if(str) *str = g_strdup(im->pre); if(attrs){ @@ -330,6 +381,9 @@ getpreedit(GtkIMContext *ctx, gchar **str, PangoAttrList **attrs, static void reset(GtkIMContext *ctx) { + if(parentim->reset != NULL) + parentim->reset(ctx); + ((Im*)ctx)->simpleactive = 0; sendreset((Im*)ctx); } @@ -339,6 +393,9 @@ focusout(GtkIMContext *ctx) Im *im; im = (Im*)ctx; + if(parentim->focus_out != NULL) + parentim->focus_out(ctx); + im->simpleactive = 0; sendreset(im); /* Closing the context connection releases engine ownership. */ srvclose(im); @@ -354,13 +411,15 @@ setusepreedit(GtkIMContext *ctx, gboolean use) im = (Im*)ctx; use = use != FALSE; + if(parentim->set_use_preedit != NULL) + parentim->set_use_preedit(ctx, use); if(im->usepreedit == use) return; im->usepreedit = use; - if(im->fd < 0 || !im->ext){ - im->effective = im->ext ? use : 1; + if(!use) + setpreedit(im, "", 0); + if(im->fd < 0 || !im->ext) return; - } ipcpackcap(buf, use); if(ipcsend(im->fd, buf, sizeof buf) < 0 || readresp(im, use, use, commit, sizeof commit, &resp) < 0 || @@ -368,9 +427,6 @@ setusepreedit(GtkIMContext *ctx, gboolean use) srvclose(im); return; } - im->effective = use; - if(!use) - setpreedit(im, "", 0); } static void @@ -381,6 +437,8 @@ setclientwindow(GtkIMContext *ctx, GdkWindow *win) im = (Im*)ctx; if(win != NULL && gdk_window_is_destroyed(win)) win = NULL; + if(parentim->set_client_window != NULL) + parentim->set_client_window(ctx, win); if(im->win == win) return; if(win != NULL) @@ -397,6 +455,8 @@ setcursorlocation(GtkIMContext *ctx, GdkRectangle *area) Im *im; im = (Im*)ctx; + if(parentim->set_cursor_location != NULL) + parentim->set_cursor_location(ctx, area); if(im->win != NULL && gdk_window_is_destroyed(im->win)) dropwindow(im); if(area == NULL || im->win == NULL) @@ -432,18 +492,18 @@ purposechanged(GObject *obj, GParamSpec *pspec, gpointer data) return; im->private = private; if(private) - sendreset(im); + reset(GTK_IM_CONTEXT(im)); } static void -finalize(GObject *obj) +dispose(GObject *obj) { Im *im; im = (Im*)obj; - srvclose(im); + srvdrop(im, 0); dropwindow(im); - G_OBJECT_CLASS(g_type_class_peek_parent(G_OBJECT_GET_CLASS(obj)))->finalize(obj); + parentobject->dispose(obj); } static void @@ -453,12 +513,13 @@ init(Im *im) im->fd = -1; im->usepreedit = 1; - im->effective = 1; im->pre[0] = '\0'; g_object_get(im, "input-purpose", &purpose, NULL); im->private = isprivate(purpose); g_signal_connect(im, "notify::input-purpose", G_CALLBACK(purposechanged), NULL); + g_signal_connect(im, "commit", G_CALLBACK(simplecommit), im); + g_signal_connect(im, "preedit-end", G_CALLBACK(simpleend), im); } static void @@ -469,6 +530,8 @@ classinit(ImClass *klass) ic = GTK_IM_CONTEXT_CLASS(klass); oc = G_OBJECT_CLASS(klass); + parentim = g_type_class_peek_parent(klass); + parentobject = G_OBJECT_CLASS(parentim); ic->filter_keypress = kpress; ic->get_preedit_string = getpreedit; ic->reset = reset; @@ -476,7 +539,7 @@ classinit(ImClass *klass) ic->set_use_preedit = setusepreedit; ic->set_client_window = setclientwindow; ic->set_cursor_location = setcursorlocation; - oc->finalize = finalize; + oc->dispose = dispose; } static const GtkIMContextInfo info = { @@ -501,7 +564,8 @@ im_module_init(GTypeModule *mod) 0, (GInstanceInitFunc)init, }; - imtype = g_type_module_register_type(mod, GTK_TYPE_IM_CONTEXT, "strans-gtk", &ti, 0); + imtype = g_type_module_register_type(mod, GTK_TYPE_IM_CONTEXT_SIMPLE, + "strans-gtk", &ti, 0); } G_MODULE_EXPORT void diff --git a/tests/gtk_live_test.c b/tests/gtk_live_test.c index 4d5545f..f264089 100644 --- a/tests/gtk_live_test.c +++ b/tests/gtk_live_test.c @@ -34,6 +34,13 @@ enum Ereset, }; +enum +{ + Rnormal, + Rbadcommit, + Rbadpreedit, +}; + typedef struct Event Event; typedef struct Server Server; typedef struct Siglog Siglog; @@ -58,6 +65,9 @@ struct Server int wake[2]; int stop; int old; + int eaten; + int stall; + int response; int nclose; char pre[Ipcfieldmax+1]; Event ev[Maxevent]; @@ -128,16 +138,35 @@ closed(Server *s) } static int -sendresponse(Server *s, int fd, int eaten, int want) +sendresponse(Server *s, int fd, int eaten, int want, int key) { unsigned char buf[Ipcmaxresp]; + const char *commit; char pre[Ipcfieldmax+1]; - int n; + int n, response, stall; + size_t ncommit, npreedit; pthread_mutex_lock(&s->lock); memcpy(pre, s->pre, sizeof pre); + response = s->response; + stall = s->stall; pthread_mutex_unlock(&s->lock); - n = ipcpackresp(buf, sizeof buf, eaten, "", 0, pre, strlen(pre), want); + if(stall) + return 1; + commit = ""; + ncommit = 0; + npreedit = strlen(pre); + if(key && response == Rbadcommit){ + commit = "\xc3("; + ncommit = 2; + }else if(key && response == Rbadpreedit){ + pre[0] = 'x'; + pre[1] = 0; + pre[2] = 'y'; + npreedit = 3; + } + n = ipcpackresp(buf, sizeof buf, eaten, commit, ncommit, + pre, npreedit, want); return n >= 0 && ipcsend(fd, buf, n) == 0; } @@ -161,7 +190,7 @@ request(Server *s, int fd) pthread_mutex_lock(&s->lock); old = s->old; pthread_mutex_unlock(&s->lock); - return sendresponse(s, fd, !old, e.want); + return sendresponse(s, fd, !old, e.want, 0); case Ipccaret: if(ipcreadn(fd, buf + Ipcreqsz, Ipccaretsz - Ipcreqsz) < 0 || ipcunpackcaret(buf, &e.valid, &e.x, &e.y, &e.h) < 0) @@ -175,7 +204,11 @@ request(Server *s, int fd) e.key = key; e.type = ipcreqreset(buf) ? Ereset : Ekey; record(s, &e); - return sendresponse(s, fd, e.type == Ekey, want); + pthread_mutex_lock(&s->lock); + old = s->eaten; + pthread_mutex_unlock(&s->lock); + return sendresponse(s, fd, e.type == Ekey && old, want, + e.type == Ekey); } return 0; } @@ -230,6 +263,7 @@ startserver(Server *s, char *path) int cond; memset(s, 0, sizeof *s); + s->eaten = 1; s->listenfd = -1; s->wake[0] = s->wake[1] = -1; if(pthread_mutex_init(&s->lock, NULL) != 0) @@ -317,6 +351,16 @@ setold(Server *s, int old) pthread_mutex_unlock(&s->lock); } +static void +setreply(Server *s, int eaten, int response, int stall) +{ + pthread_mutex_lock(&s->lock); + s->eaten = eaten; + s->response = response; + s->stall = stall; + pthread_mutex_unlock(&s->lock); +} + static int eventcount(Server *s) { @@ -540,16 +584,6 @@ commit(GtkIMContext *ctx, gchar *s, Siglog *l) logsig(l, 'K'); } -static void -weakgone(void *v, GObject *obj) -{ - int *gone; - - (void)obj; - gone = v; - *gone = 1; -} - static void clearlog(Siglog *l) { @@ -571,21 +605,35 @@ newcontext(Siglog *l) } static int -key(GtkIMContext *ctx, GdkWindow *win, guint keyval) +keyevent(GtkIMContext *ctx, GdkWindow *win, GdkEventType type, + guint keyval, guint state) { GdkEvent *ev; int r; - ev = gdk_event_new(GDK_KEY_PRESS); + ev = gdk_event_new(type); ev->key.window = g_object_ref(win); ev->key.send_event = TRUE; ev->key.time = GDK_CURRENT_TIME; ev->key.keyval = keyval; + ev->key.state = state; r = gtk_im_context_filter_keypress(ctx, &ev->key); gdk_event_free(ev); return r; } +static int +keystate(GtkIMContext *ctx, GdkWindow *win, guint keyval, guint state) +{ + return keyevent(ctx, win, GDK_KEY_PRESS, keyval, state); +} + +static int +key(GtkIMContext *ctx, GdkWindow *win, guint keyval) +{ + return keystate(ctx, win, keyval, 0); +} + static GtkWidget* newwindow(int x, int y) { @@ -607,19 +655,25 @@ main(int argc, char **argv) Server srv; Siglog log; GtkIMContext *ctx, *oldctx, *badctx; - GtkWidget *top, *other; - GdkWindow *win, *win2; + GtkWidget *top; + GdkWindow *win; GdkRectangle rect; Event e; + static const GtkInputPurpose purposes[] = { + GTK_INPUT_PURPOSE_PASSWORD, + GTK_INPUT_PURPOSE_PIN, + }; + static const int badresponses[] = { Rbadcommit, Rbadpreedit }; 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; + int first, i, ox, oy, scale, closes, ok, rootmade, serverup; + int64_t stalledms, started; ctx = oldctx = badctx = NULL; - top = other = NULL; + top = NULL; xvfb = -1; rootmade = 0; serverup = 0; @@ -688,153 +742,60 @@ main(int argc, char **argv) Check(strcmp(log.event, "SC") == 0 && strcmp(log.pre, "pre") == 0, "initial preedit signals were %s (%s)", log.event, log.pre); + /* Unconsumed input uses GtkIMContextSimple, including its stateful forms. */ + setpre(&srv, ""); + setreply(&srv, 0, Rnormal, 0); 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); + Check(key(ctx, win, GDK_KEY_Multi_key) && + key(ctx, win, GDK_KEY_apostrophe) && key(ctx, win, GDK_KEY_e), + "Compose sequence was not consumed"); + Check(strcmp(log.commit, "é") == 0, "Compose committed %s", log.commit); 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); + Check(key(ctx, win, GDK_KEY_dead_acute) && key(ctx, win, GDK_KEY_e), + "dead-key sequence was not consumed"); + Check(strcmp(log.commit, "é") == 0, "dead key committed %s", log.commit); + clearlog(&log); + Check(keystate(ctx, win, GDK_KEY_U, GDK_CONTROL_MASK|GDK_SHIFT_MASK), + "Unicode prefix was not consumed"); + (void)keyevent(ctx, win, GDK_KEY_RELEASE, GDK_KEY_U, + GDK_CONTROL_MASK|GDK_SHIFT_MASK); + (void)keyevent(ctx, win, GDK_KEY_RELEASE, GDK_KEY_Shift_L, + GDK_CONTROL_MASK); + (void)keyevent(ctx, win, GDK_KEY_RELEASE, GDK_KEY_Control_L, 0); + Check(key(ctx, win, GDK_KEY_0), "Unicode digit 1 was not consumed"); + Check(key(ctx, win, GDK_KEY_0), "Unicode digit 2 was not consumed"); + Check(key(ctx, win, GDK_KEY_e), "Unicode digit 3 was not consumed"); + Check(key(ctx, win, GDK_KEY_9), "Unicode digit 4 was not consumed"); + (void)key(ctx, win, GDK_KEY_Return); + Check(strcmp(log.commit, "é") == 0, "Unicode entry committed %s", log.commit); + setreply(&srv, 1, Rnormal, 0); /* 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"); + for(i = 0; i < (int)(sizeof purposes / sizeof purposes[0]); i++){ + g_object_set(ctx, "input-purpose", GTK_INPUT_PURPOSE_FREE_FORM, NULL); + setpre(&srv, i == 0 ? "password" : "pin"); + clearlog(&log); + Check(key(ctx, win, GDK_KEY_c + i), "private setup key was not eaten"); + clearlog(&log); + first = eventcount(&srv); + g_object_set(ctx, "input-purpose", purposes[i], NULL); + Check(waitcount(&srv, first + 1), "private reset timed out"); + Check(getevent(&srv, first).type == Ereset && + strcmp(log.event, "CE") == 0, + "private transition did not reset and clear"); + clearlog(&log); + first = eventcount(&srv); + Check(key(ctx, win, GDK_KEY_x), "private fallback did not commit"); + Check(eventcount(&srv) == first && strcmp(log.commit, "x") == 0, + "private key reached daemon or did not commit"); + } 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"); + Check(key(ctx, win, GDK_KEY_e), "reset setup key was not eaten"); clearlog(&log); first = eventcount(&srv); gtk_im_context_reset(ctx); @@ -843,7 +804,7 @@ main(int argc, char **argv) "explicit reset did not clear preedit"); setpre(&srv, "focus"); clearlog(&log); - Check(key(ctx, win2, GDK_KEY_f), "focus setup key was not eaten"); + Check(key(ctx, win, GDK_KEY_f), "focus setup key was not eaten"); clearlog(&log); first = eventcount(&srv); closes = closecount(&srv); @@ -853,21 +814,58 @@ main(int argc, char **argv) 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); + /* Dispose is repeatable, closes the connection, and emits no preedit. */ + gtk_im_context_set_client_window(ctx, win); setpre(&srv, "final"); clearlog(&log); - Check(key(ctx, win2, GDK_KEY_g), "finalization setup key was not eaten"); + Check(key(ctx, win, GDK_KEY_g), "dispose setup key was not eaten"); + clearlog(&log); closes = closecount(&srv); + g_object_run_dispose(G_OBJECT(ctx)); + g_object_run_dispose(G_OBJECT(ctx)); g_object_unref(ctx); ctx = NULL; - Check(waitclose(&srv, closes + 1), "finalization did not close connection"); + Check(waitclose(&srv, closes + 1), "dispose did not close connection"); + Check(log.n == 0, "dispose emitted %s", log.event); - /* A new module talking to an old daemon keeps legacy inline preedit. */ + /* Malformed commit and preedit fields close the protocol connection. */ + for(i = 0; i < (int)(sizeof badresponses / sizeof badresponses[0]); i++){ + setreply(&srv, 1, badresponses[i], 0); + clearlog(&log); + badctx = newcontext(&log); + gtk_im_context_set_client_window(badctx, win); + closes = closecount(&srv); + Check(key(badctx, win, GDK_KEY_m + i), + "malformed response did not fall back"); + Check(waitclose(&srv, closes + 1), + "malformed response did not close connection"); + Check(log.commit[0] == 'm' + i && log.commit[1] == '\0', + "malformed response fallback committed %s", log.commit); + g_object_unref(badctx); + badctx = NULL; + } + + /* A peer which accepts a connection but never replies cannot freeze GTK. */ + setreply(&srv, 1, Rnormal, 1); + clearlog(&log); + badctx = newcontext(&log); + closes = closecount(&srv); + started = nowms(); + Check(key(badctx, win, GDK_KEY_t), "stalled peer did not fall back"); + stalledms = nowms() - started; + Check(stalledms < 4*Ipcwaitms, "stalled peer blocked GTK"); + Check(waitclose(&srv, closes + 1), "stalled connection was not closed"); + Check(strcmp(log.commit, "t") == 0, "stalled fallback committed %s", + log.commit); + g_object_unref(badctx); + badctx = NULL; + setreply(&srv, 1, Rnormal, 0); + + /* An old daemon still honors set_use_preedit(FALSE). */ setold(&srv, 1); clearlog(&log); oldctx = newcontext(&log); - gtk_im_context_set_client_window(oldctx, win2); + gtk_im_context_set_client_window(oldctx, win); rect.x = 2; rect.y = 3; rect.height = 10; @@ -875,29 +873,29 @@ main(int argc, char **argv) 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(key(oldctx, win, 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"); + Check(e.type == Ekey && e.want == 0, "old daemon forced inline 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); + Check(log.n == 0 && log.pre[0] == '\0', + "old daemon emitted inline preedit %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. */ + /* A failed first connection uses GtkIMContextSimple. */ 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(key(badctx, win, GDK_KEY_z), "connection failure did not fall back"); Check(strcmp(log.commit, "z") == 0 && log.pre[0] == '\0', "connection failure left stale preedit"); @@ -909,8 +907,6 @@ out: 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(); @@ -927,6 +923,7 @@ out: stopxvfb(&xvfb); free(module); if(ok) - printf("gtk_live_test: ok (scale %d)\n", scale); + printf("gtk_live_test: ok (scale %d, stalled peer %lld ms)\n", + scale, (long long)stalledms); return !ok; }