Files
strans/gtk/main.c
Hojun-Cho 11bf379ad3 fix(engine): a reset or a lost focus commits the pending text
Clicking elsewhere, changing focus, or any client reset dropped the
composition in the GTK module and IBus (only XIM ResetIC handed it
back), so typing 안녕 and clicking Send lost 녕. The engine now returns
the pending text — a moved-to candidate first, as Enter would — on
Keyreset and Keyrelease; the GTK module asks for it before closing on
focus-out and commits it, IBus commits it on FocusOut, Reset and a
switch to a password field, and XIM commits it on focus loss and hands
it back on ResetIC without a separate capability probe.
2026-08-16 21:05:31 +09:00

531 lines
10 KiB
C

#include <errno.h>
#include <string.h>
#include <unistd.h>
#include <gtk/gtk.h>
#ifdef GDK_WINDOWING_X11
#include <gdk/gdkx.h>
#endif
#include "ipc.h"
typedef struct Im Im;
struct Im
{
GtkIMContextSimple parent;
int fd;
int usepreedit;
int ext;
int private;
int simpleactive;
int simpledone;
char pre[Ipcfieldmax+1];
int prelen;
GdkWindow *win;
GdkRectangle cursor;
int cursorvalid;
int caretsent;
unsigned char sent[Ipccaretsz];
};
typedef struct ImClass ImClass;
struct ImClass
{
GtkIMContextSimpleClass parent;
};
static GType imtype;
static GObjectClass *parentobject;
static GtkIMContextClass *parentim;
static void
setpreedit(Im *im, const char *s, int n)
{
int was;
if(n < 0 || n > Ipcfieldmax)
n = 0;
if(n == 0 && im->prelen == 0)
return;
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
srvdrop(Im *im, int notify)
{
if(im->fd >= 0)
close(im->fd);
im->fd = -1;
im->ext = 0;
im->caretsent = 0;
if(notify)
setpreedit(im, "", 0);
else{
im->pre[0] = '\0';
im->prelen = 0;
}
}
static void
srvclose(Im *im)
{
srvdrop(im, 1);
}
/* Both buffers are char[Ipcfieldmax+1]. */
static int
readresp(Im *im, int want, char *commit, char *pre, Ipcresp *resp)
{
if(ipcreadresp(im->fd, want, commit, pre, resp) < 0)
return -1;
if(!g_utf8_validate(commit, resp->commitlen, NULL) ||
(want && !g_utf8_validate(pre, resp->preeditlen, NULL))){
errno = EPROTO;
return -1;
}
return 0;
}
static void
dropwindow(Im *im)
{
if(im->win != NULL)
g_object_unref(im->win);
im->win = NULL;
im->cursorvalid = 0;
}
/* Root-relative caret in device pixels; returns whether it is known. */
static int
caretget(Im *im, int32_t *x, int32_t *y, int32_t *h)
{
gint rx, ry, scale;
int64_t sx, sy, sh;
*x = *y = *h = 0;
if(im->win != NULL && gdk_window_is_destroyed(im->win))
dropwindow(im);
if(!im->cursorvalid || im->win == NULL || im->cursor.height < 0)
return 0;
#ifdef GDK_WINDOWING_X11
if(!GDK_IS_X11_WINDOW(im->win))
return 0;
#else
return 0;
#endif
scale = gdk_window_get_scale_factor(im->win);
if(scale <= 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)
return 0;
*x = sx;
*y = sy;
*h = sh;
return 1;
}
static int
sendcaret(Im *im)
{
unsigned char buf[Ipccaretsz];
int32_t x, y, h;
int valid;
if(im->fd < 0 || !im->ext)
return 0;
valid = caretget(im, &x, &y, &h);
ipcpackcaret(buf, valid, x, y, h);
if(im->caretsent && memcmp(buf, im->sent, sizeof buf) == 0)
return 0;
if(ipcsend(im->fd, buf, sizeof buf) < 0)
return -1;
memcpy(im->sent, buf, sizeof buf);
im->caretsent = 1;
return 0;
}
static int
srvconnect(Im *im)
{
unsigned char buf[Ipcreqsz];
char commit[Ipcfieldmax+1];
char pre[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, commit, pre, &resp) < 0){
srvclose(im);
return -1;
}
im->ext = resp.eaten != 0;
if(sendcaret(im) < 0){
srvclose(im);
return -1;
}
return 0;
}
static void
simplecommit(GtkIMContext *ctx, const char *s, Im *im)
{
(void)ctx;
(void)s;
im->simpledone = 1;
}
static void
simpleend(GtkIMContext *ctx, Im *im)
{
(void)ctx;
im->simpledone = 1;
}
/*
* GtkIMContextSimple composes dead keys and Compose sequences; it keeps
* the key stream until it commits, ends its preedit, or rejects a press.
*/
static gboolean
simplefilter(GtkIMContext *ctx, GdkEventKey *ev, int release)
{
Im *im;
gboolean r;
im = (Im*)ctx;
im->simpleactive = 1;
im->simpledone = 0;
r = parentim->filter_keypress(ctx, ev);
if((!r && !release) || im->simpledone)
im->simpleactive = 0;
return r;
}
/* The daemon hands the pending text back; it becomes committed text. */
static void
sendreset(Im *im)
{
unsigned char buf[Ipcreqsz];
char commit[Ipcfieldmax+1];
char pre[Ipcfieldmax+1];
Ipcresp resp;
if(im->fd < 0){
setpreedit(im, "", 0);
return;
}
ipcpackreset(buf, im->usepreedit);
if(ipcsend(im->fd, buf, sizeof buf) < 0 ||
readresp(im, im->usepreedit, commit, pre, &resp) < 0){
srvclose(im);
return;
}
setpreedit(im, "", 0);
if(commit[0] != '\0')
g_signal_emit_by_name(im, "commit", commit);
}
static gboolean
kpress(GtkIMContext *ctx, GdkEventKey *ev)
{
Im *im;
unsigned char buf[Ipcreqsz];
char commit[Ipcfieldmax+1];
char pre[Ipcfieldmax+1];
uint32_t key, mod;
Ipcresp resp;
im = (Im*)ctx;
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 = ipckeysym(ev->keyval, gdk_keyval_to_unicode(ev->keyval));
mod = ipcmod(ev->state);
if(im->private || key == 0)
return simplefilter(ctx, ev, 0);
if(srvconnect(im) < 0)
return simplefilter(ctx, ev, 0);
/* A retained GDK window may have moved since the last cursor report. */
if(sendcaret(im) < 0){
srvclose(im);
return simplefilter(ctx, ev, 0);
}
ipcpackreq(buf, im->usepreedit, mod, key);
if(ipcsend(im->fd, buf, sizeof buf) < 0 ||
readresp(im, im->usepreedit, commit, pre, &resp) < 0){
srvclose(im);
return simplefilter(ctx, ev, 0);
}
if(im->usepreedit && commit[0] != '\0' && im->prelen > 0)
setpreedit(im, "", 0);
if(commit[0] != '\0')
g_signal_emit_by_name(ctx, "commit", commit);
if(im->usepreedit)
setpreedit(im, pre, resp.preeditlen);
if(resp.eaten)
return TRUE;
return simplefilter(ctx, ev, 0);
}
static void
getpreedit(GtkIMContext *ctx, gchar **str, PangoAttrList **attrs,
gint *cursor_pos)
{
Im *im;
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){
*attrs = pango_attr_list_new();
if(im->prelen > 0){
u = pango_attr_underline_new(PANGO_UNDERLINE_SINGLE);
u->start_index = 0;
u->end_index = im->prelen;
pango_attr_list_insert(*attrs, u);
}
}
if(cursor_pos)
*cursor_pos = g_utf8_strlen(im->pre, -1);
}
static void
reset(GtkIMContext *ctx)
{
if(parentim->reset != NULL)
parentim->reset(ctx);
((Im*)ctx)->simpleactive = 0;
sendreset((Im*)ctx);
}
static void
focusout(GtkIMContext *ctx)
{
Im *im;
im = (Im*)ctx;
if(parentim->focus_out != NULL)
parentim->focus_out(ctx);
im->simpleactive = 0;
/* Closing the connection releases the engine. */
sendreset(im);
srvclose(im);
}
static void
setusepreedit(GtkIMContext *ctx, gboolean use)
{
Im *im;
unsigned char buf[Ipcreqsz];
char commit[Ipcfieldmax+1];
char pre[Ipcfieldmax+1];
Ipcresp resp;
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(!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, commit, pre, &resp) < 0 ||
!resp.eaten){
srvclose(im);
return;
}
if(use)
setpreedit(im, pre, resp.preeditlen);
}
static void
setclientwindow(GtkIMContext *ctx, GdkWindow *win)
{
Im *im;
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)
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(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)
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)
reset(GTK_IM_CONTEXT(im));
}
static void
dispose(GObject *obj)
{
Im *im;
im = (Im*)obj;
srvdrop(im, 0);
dropwindow(im);
parentobject->dispose(obj);
}
static void
init(Im *im)
{
GtkInputPurpose purpose;
im->fd = -1;
im->usepreedit = 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
classinit(ImClass *klass)
{
GtkIMContextClass *ic;
GObjectClass *oc;
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;
ic->focus_out = focusout;
ic->set_use_preedit = setusepreedit;
ic->set_client_window = setclientwindow;
ic->set_cursor_location = setcursorlocation;
oc->dispose = dispose;
}
static const GtkIMContextInfo info = {
"strans",
"strans",
"strans",
"",
"*",
};
static const GtkIMContextInfo *infolist[] = { &info };
G_MODULE_EXPORT void
im_module_init(GTypeModule *mod)
{
static const GTypeInfo ti = {
sizeof(ImClass),
NULL, NULL,
(GClassInitFunc)classinit,
NULL, NULL,
sizeof(Im),
0,
(GInstanceInitFunc)init,
};
imtype = g_type_module_register_type(mod, GTK_TYPE_IM_CONTEXT_SIMPLE,
"strans-gtk", &ti, 0);
}
G_MODULE_EXPORT void
im_module_exit(void)
{
}
G_MODULE_EXPORT void
im_module_list(const GtkIMContextInfo ***contexts, int *n)
{
*contexts = infolist;
*n = 1;
}
G_MODULE_EXPORT GtkIMContext*
im_module_create(const char *id)
{
if(strcmp(id, "strans") == 0)
return g_object_new(imtype, NULL);
return NULL;
}