The engine can reach a Hanja reading back into the text the client already holds, but only if the frontend hands that text over and can take some of it away again. GTK 3 has both: retrieve-surrounding brings the text around the cursor and delete-surrounding removes runes before it, and a widget that answers neither leaves the text empty, so nothing is ever reached into or taken from it. The wire grows a control frame for the text, sent like the caret only when it changes, and one byte in every response for the runes to take back. That byte moves the length fields along, so the version goes to 2: an old daemon and a new module, either way round, fail the handshake and the module falls through to GtkIMContextSimple rather than misread a frame. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
610 lines
12 KiB
C
610 lines
12 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];
|
|
char surround[Ipcfieldmax];
|
|
int nsurround;
|
|
int surroundsent;
|
|
};
|
|
|
|
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;
|
|
im->surroundsent = 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;
|
|
}
|
|
|
|
/*
|
|
* The text just before the cursor, as much of it as a reading can use,
|
|
* cut on a rune boundary. A widget that keeps none leaves it empty, and
|
|
* then the daemon never asks for any of it back.
|
|
*/
|
|
static void
|
|
readsurround(Im *im, GtkIMContext *ctx)
|
|
{
|
|
char *text, *p;
|
|
gint cursor;
|
|
int start;
|
|
|
|
im->nsurround = 0;
|
|
if(!gtk_im_context_get_surrounding(ctx, &text, &cursor))
|
|
return;
|
|
if(cursor < 0 || cursor > (gint)strlen(text)){
|
|
g_free(text);
|
|
return;
|
|
}
|
|
start = cursor > Ipcfieldmax ? cursor - Ipcfieldmax : 0;
|
|
for(p = text + start; p < text + cursor && (*p & 0xc0) == 0x80; p++)
|
|
;
|
|
im->nsurround = text + cursor - p;
|
|
memcpy(im->surround, p, im->nsurround);
|
|
g_free(text);
|
|
}
|
|
|
|
/* Sent only when it changes, as the caret is. */
|
|
static int
|
|
sendsurround(Im *im, GtkIMContext *ctx)
|
|
{
|
|
unsigned char hdr[Ipcreqsz];
|
|
char was[Ipcfieldmax];
|
|
int nwas;
|
|
|
|
if(im->fd < 0 || !im->ext)
|
|
return 0;
|
|
nwas = im->nsurround;
|
|
memcpy(was, im->surround, nwas);
|
|
readsurround(im, ctx);
|
|
if(im->surroundsent && nwas == im->nsurround &&
|
|
memcmp(was, im->surround, nwas) == 0)
|
|
return 0;
|
|
ipcpacksurround(hdr, im->nsurround);
|
|
if(ipcsend(im->fd, hdr, sizeof hdr) < 0 ||
|
|
(im->nsurround > 0 &&
|
|
ipcsend(im->fd, im->surround, im->nsurround) < 0))
|
|
return -1;
|
|
im->surroundsent = 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);
|
|
}
|
|
|
|
/* A password entry hidden the older way sets no purpose and signals nothing. */
|
|
static int
|
|
ishidden(Im *im)
|
|
{
|
|
GtkWidget *w;
|
|
|
|
if(im->win == NULL)
|
|
return 0;
|
|
gdk_window_get_user_data(im->win, (gpointer*)&w);
|
|
return GTK_IS_ENTRY(w) && !gtk_entry_get_visibility(GTK_ENTRY(w));
|
|
}
|
|
|
|
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;
|
|
gboolean r;
|
|
|
|
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 || ishidden(im) || key == 0){
|
|
r = simplefilter(ctx, ev, 0);
|
|
/* A dead key starts a compose: pending text was typed first. */
|
|
if(im->simpleactive)
|
|
sendreset(im);
|
|
return r;
|
|
}
|
|
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 || sendsurround(im, ctx) < 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);
|
|
/* The reading reached into the widget's own text: give it back. */
|
|
if(resp.del > 0){
|
|
gtk_im_context_delete_surrounding(ctx, -resp.del, resp.del);
|
|
im->surroundsent = 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;
|
|
}
|