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.
531 lines
10 KiB
C
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;
|
|
}
|