XIM text always went out through imdkit's COMPOUND_TEXT converter, which already wraps every UTF-8 string in ESC%G; the UTF8_STRING negotiation, the per-client encoding list, ximtext.c's fallback and its wrapper never changed a byte on the wire. The spot location is honoured for every style now (GTK's XIM module sends it with PreeditCallbacks) and an unset focus window means the client window, as the spec says, so those clients get the popup at the caret. readattrs/place kept four transient fields to pass values between them; ximclose tore down state right before die(); the OOM passthrough context was a third policy for one small calloc where emalloc dies like everything else. Both frontends now poll for engine-owner loss only while a preedit shows instead of XIM waking on a pipe the engine had to know about; ibus stops waking five times a second when idle. keymeaningful() is the engine's own predicate. The standalone xim_test moved into the unit suite, so xim/Makefile is gone.
529 lines
12 KiB
C
529 lines
12 KiB
C
#include "dat.h"
|
|
#include "fn.h"
|
|
|
|
#include <errno.h>
|
|
#include <poll.h>
|
|
#include <xcb/xcb.h>
|
|
#include <xkbcommon/xkbcommon.h>
|
|
#include <xkbcommon/xkbcommon-x11.h>
|
|
#include <xcb-imdkit/imdkit.h>
|
|
#include <xcb-imdkit/encoding.h>
|
|
|
|
u32int keymaplookup(struct xkb_state*, uint8_t, u16int);
|
|
|
|
/*
|
|
* One XIM input context. Only PreeditCallbacks clients draw the preedit
|
|
* themselves (cap); the popup shows it for the others. engaged means the
|
|
* engine has seen a real key from this context and owes it a release.
|
|
*/
|
|
typedef struct Ic Ic;
|
|
struct Ic
|
|
{
|
|
Ic *next;
|
|
xcb_im_input_context_t *xic;
|
|
xcb_im_client_t *client;
|
|
int engaged;
|
|
int cap;
|
|
int prestarted;
|
|
int nprerune;
|
|
Caret caret;
|
|
};
|
|
|
|
enum
|
|
{
|
|
Ownerpoll = 200, /* ms between owner checks while a preedit shows */
|
|
};
|
|
|
|
static xcb_connection_t *conn;
|
|
static xcb_im_t *xim;
|
|
static struct xkb_state *kstate;
|
|
static xcb_window_t rootwin;
|
|
static Ic *ics;
|
|
static Ic *preowner;
|
|
static Channel *replyc;
|
|
|
|
static char *encs[] = {"COMPOUND_TEXT"};
|
|
static u32int styles[] = {
|
|
XCB_IM_PreeditPosition | XCB_IM_StatusNothing,
|
|
XCB_IM_PreeditCallbacks | XCB_IM_StatusNothing,
|
|
XCB_IM_PreeditNothing | XCB_IM_StatusNothing,
|
|
};
|
|
|
|
static void
|
|
ximlog(char *msg)
|
|
{
|
|
fprint(2, "strans: xim: %s\n", msg);
|
|
}
|
|
|
|
static int
|
|
kinit(void)
|
|
{
|
|
struct xkb_context *context;
|
|
struct xkb_keymap *keymap;
|
|
struct xkb_state *state;
|
|
int32_t device;
|
|
int ok;
|
|
|
|
keymap = nil;
|
|
state = nil;
|
|
ok = -1;
|
|
context = xkb_context_new(XKB_CONTEXT_NO_FLAGS);
|
|
if(context == nil || !xkb_x11_setup_xkb_extension(conn,
|
|
XKB_X11_MIN_MAJOR_XKB_VERSION, XKB_X11_MIN_MINOR_XKB_VERSION,
|
|
XKB_X11_SETUP_XKB_EXTENSION_NO_FLAGS, nil, nil, nil, nil))
|
|
goto out;
|
|
device = xkb_x11_get_core_keyboard_device_id(conn);
|
|
if(device < 0)
|
|
goto out;
|
|
keymap = xkb_x11_keymap_new_from_device(context, conn, device,
|
|
XKB_KEYMAP_COMPILE_NO_FLAGS);
|
|
if(keymap == nil)
|
|
goto out;
|
|
state = xkb_state_new(keymap);
|
|
if(state == nil)
|
|
goto out;
|
|
xkb_state_unref(kstate);
|
|
kstate = state;
|
|
state = nil;
|
|
ok = 0;
|
|
out:
|
|
xkb_state_unref(state);
|
|
xkb_keymap_unref(keymap);
|
|
xkb_context_unref(context);
|
|
return ok;
|
|
}
|
|
|
|
static xcb_screen_t*
|
|
getscreen(int scr)
|
|
{
|
|
xcb_screen_iterator_t iter;
|
|
|
|
iter = xcb_setup_roots_iterator(xcb_get_setup(conn));
|
|
for(; iter.rem; scr--, xcb_screen_next(&iter))
|
|
if(scr == 0)
|
|
return iter.data;
|
|
return nil;
|
|
}
|
|
|
|
static int
|
|
translate(xcb_window_t win, int x, int y, Caret *caret)
|
|
{
|
|
xcb_translate_coordinates_cookie_t cookie;
|
|
xcb_translate_coordinates_reply_t *reply;
|
|
|
|
if(win == XCB_NONE)
|
|
return 0;
|
|
cookie = xcb_translate_coordinates(conn, win, rootwin, x, y);
|
|
reply = xcb_translate_coordinates_reply(conn, cookie, nil);
|
|
if(reply == nil || !reply->same_screen){
|
|
free(reply);
|
|
return 0;
|
|
}
|
|
caret->valid = 1;
|
|
caret->x = reply->dst_x;
|
|
caret->y = reply->dst_y;
|
|
caret->h = 0;
|
|
free(reply);
|
|
return 1;
|
|
}
|
|
|
|
static int
|
|
placebottom(Ic *state, xcb_window_t win)
|
|
{
|
|
xcb_get_geometry_cookie_t gcookie;
|
|
xcb_get_geometry_reply_t *geometry;
|
|
int height;
|
|
|
|
if(win == XCB_NONE)
|
|
return 0;
|
|
gcookie = xcb_get_geometry(conn, win);
|
|
geometry = xcb_get_geometry_reply(conn, gcookie, nil);
|
|
if(geometry == nil)
|
|
return 0;
|
|
height = geometry->height;
|
|
free(geometry);
|
|
if(!translate(win, 0, 0, &state->caret))
|
|
return 0;
|
|
state->caret.y += height;
|
|
return 1;
|
|
}
|
|
|
|
/*
|
|
* The popup goes at the client's spot location when it sends one, else
|
|
* under the focus window, else under the client window. An unset focus
|
|
* window means the client window, as the XIM spec says.
|
|
*/
|
|
static void
|
|
place(Ic *state)
|
|
{
|
|
const xcb_im_preedit_attr_t *attr;
|
|
xcb_window_t clientwin, focuswin;
|
|
|
|
memset(&state->caret, 0, sizeof state->caret);
|
|
clientwin = xcb_im_input_context_get_client_window(state->xic);
|
|
focuswin = xcb_im_input_context_get_focus_window(state->xic);
|
|
if(focuswin == XCB_NONE)
|
|
focuswin = clientwin;
|
|
if(xcb_im_input_context_get_preedit_attr_mask(state->xic) &
|
|
XCB_XIM_XNSpotLocation_MASK){
|
|
attr = xcb_im_input_context_get_preedit_attr(state->xic);
|
|
if(translate(focuswin, attr->spot_location.x,
|
|
attr->spot_location.y, &state->caret))
|
|
return;
|
|
}
|
|
if(placebottom(state, focuswin))
|
|
return;
|
|
if(clientwin != focuswin)
|
|
placebottom(state, clientwin);
|
|
}
|
|
|
|
/* XIM text goes out as COMPOUND_TEXT; imdkit wraps UTF-8 in ESC%G. */
|
|
static void
|
|
commit(Ic *state, char *s, int nbyte)
|
|
{
|
|
char *wire;
|
|
size_t nwire;
|
|
|
|
if(nbyte == 0)
|
|
return;
|
|
wire = xcb_utf8_to_compound_text(s, nbyte, &nwire);
|
|
if(wire == nil)
|
|
return;
|
|
xcb_im_commit_string(xim, state->xic, XCB_XIM_LOOKUP_CHARS,
|
|
wire, (u32int)nwire, 0);
|
|
free(wire);
|
|
}
|
|
|
|
static void
|
|
clearpreedit(Ic *state)
|
|
{
|
|
xcb_im_preedit_draw_fr_t frame;
|
|
|
|
if(!state->prestarted)
|
|
return;
|
|
memset(&frame, 0, sizeof frame);
|
|
frame.chg_length = state->nprerune;
|
|
frame.status = 1;
|
|
xcb_im_preedit_draw_callback(xim, state->xic, &frame);
|
|
xcb_im_preedit_done_callback(xim, state->xic);
|
|
state->prestarted = 0;
|
|
state->nprerune = 0;
|
|
if(preowner == state)
|
|
preowner = nil;
|
|
xcb_flush(conn);
|
|
}
|
|
|
|
static void
|
|
updatepreedit(Ic *state, Str *pre)
|
|
{
|
|
xcb_im_preedit_draw_fr_t frame;
|
|
u32int feedback[Maxrunes];
|
|
char utf[Maxutf], *wire;
|
|
size_t nwire;
|
|
int i, nbyte;
|
|
|
|
if(!(state->cap & Cclientpreedit))
|
|
return;
|
|
if(pre->n == 0){
|
|
clearpreedit(state);
|
|
return;
|
|
}
|
|
nbyte = stoutf(pre, utf, sizeof utf);
|
|
wire = xcb_utf8_to_compound_text(utf, nbyte, &nwire);
|
|
if(wire == nil)
|
|
return;
|
|
for(i = 0; i < pre->n; i++)
|
|
feedback[i] = XCB_XIM_UNDERLINE;
|
|
if(!state->prestarted){
|
|
xcb_im_preedit_start_callback(xim, state->xic);
|
|
state->prestarted = 1;
|
|
}
|
|
memset(&frame, 0, sizeof frame);
|
|
frame.caret = pre->n;
|
|
frame.chg_length = state->nprerune;
|
|
frame.length_of_preedit_string = (u16int)nwire;
|
|
frame.preedit_string = (uchar*)wire;
|
|
frame.feedback_array.size = pre->n;
|
|
frame.feedback_array.items = feedback;
|
|
xcb_im_preedit_draw_callback(xim, state->xic, &frame);
|
|
state->nprerune = pre->n;
|
|
preowner = state;
|
|
free(wire);
|
|
}
|
|
|
|
static void
|
|
sendrequest(Ic *state, int op, u32int key, u32int mod, Keyres *res)
|
|
{
|
|
Keyreq kr;
|
|
|
|
memset(&kr, 0, sizeof kr);
|
|
kr.owner = state;
|
|
kr.cap = state->cap;
|
|
kr.op = op;
|
|
kr.ks = key;
|
|
kr.mod = ipcmod(mod);
|
|
kr.caret = state->caret;
|
|
kr.reply = replyc;
|
|
chansend(keyc, &kr);
|
|
chanrecv(replyc, res);
|
|
}
|
|
|
|
/* Another frontend may have taken the engine; then our preedit is stale. */
|
|
static void
|
|
checkpreowner(void)
|
|
{
|
|
Keyres res;
|
|
|
|
if(preowner == nil)
|
|
return;
|
|
sendrequest(preowner, Keycap, 0, 0, &res);
|
|
if(!res.eaten)
|
|
clearpreedit(preowner);
|
|
}
|
|
|
|
static void
|
|
keypress(Ic *state, u32int key, u32int mod, Keyres *res)
|
|
{
|
|
if(keymeaningful(key)){
|
|
if(preowner != nil && preowner != state)
|
|
clearpreedit(preowner);
|
|
state->engaged = 1;
|
|
}
|
|
sendrequest(state, Keypress, key, mod, res);
|
|
}
|
|
|
|
static void
|
|
release(Ic *state)
|
|
{
|
|
Keyres res;
|
|
|
|
clearpreedit(state);
|
|
if(!state->engaged)
|
|
return;
|
|
sendrequest(state, Keyrelease, 0, 0, &res);
|
|
state->engaged = 0;
|
|
}
|
|
|
|
/* XIM reset hands the pending preedit back to the client as committed text. */
|
|
static void
|
|
resetic(Ic *state, xcb_im_reset_ic_reply_fr_t *reply)
|
|
{
|
|
Keyres pending, res;
|
|
char utf[Maxutf], *wire;
|
|
size_t nwire;
|
|
int nbyte;
|
|
|
|
if(!state->engaged){
|
|
clearpreedit(state);
|
|
return;
|
|
}
|
|
sendrequest(state, Keycap, 0, 0, &pending);
|
|
sendrequest(state, Keyreset, 0, 0, &res);
|
|
clearpreedit(state);
|
|
if(reply == nil || !pending.eaten || pending.preedit.n == 0)
|
|
return;
|
|
nbyte = stoutf(&pending.preedit, utf, sizeof utf);
|
|
wire = xcb_utf8_to_compound_text(utf, nbyte, &nwire);
|
|
if(wire == nil)
|
|
return;
|
|
reply->committed_string = (uchar*)wire;
|
|
reply->byte_length_of_committed_string = nwire;
|
|
}
|
|
|
|
static void
|
|
kpress(Ic *state, xcb_key_press_event_t *ev)
|
|
{
|
|
Keyres res;
|
|
char buf[Maxutf];
|
|
u32int key;
|
|
int n, wasvalid;
|
|
|
|
key = keymaplookup(kstate, ev->detail, ev->state);
|
|
key = ipckeysym(key, xkb_keysym_to_utf32(key));
|
|
if(keymeaningful(key)){
|
|
wasvalid = state->caret.valid;
|
|
place(state);
|
|
if(state->engaged && wasvalid && !state->caret.valid)
|
|
sendrequest(state, Keycaret, 0, 0, &res);
|
|
}
|
|
keypress(state, key, ev->state, &res);
|
|
n = stoutf(&res.commit, buf, sizeof buf);
|
|
commit(state, buf, n);
|
|
updatepreedit(state, &res.preedit);
|
|
if(!res.eaten)
|
|
xcb_im_forward_event(xim, state->xic, ev);
|
|
xcb_flush(conn);
|
|
}
|
|
|
|
static void
|
|
icunlink(Ic *state)
|
|
{
|
|
Ic **p;
|
|
|
|
for(p = &ics; *p != nil; p = &(*p)->next)
|
|
if(*p == state){
|
|
*p = state->next;
|
|
return;
|
|
}
|
|
}
|
|
|
|
static void
|
|
icfree(void *p)
|
|
{
|
|
Ic *state;
|
|
|
|
state = p;
|
|
/* The owner remains valid until imthread acknowledges its release. */
|
|
release(state);
|
|
icunlink(state);
|
|
free(state);
|
|
}
|
|
|
|
static void
|
|
iccreate(xcb_im_client_t *client, xcb_im_input_context_t *ic)
|
|
{
|
|
Ic *state;
|
|
|
|
state = emalloc(sizeof(Ic));
|
|
state->xic = ic;
|
|
state->client = client;
|
|
if(xcb_im_input_context_get_input_style(ic) ==
|
|
(XCB_IM_PreeditCallbacks | XCB_IM_StatusNothing))
|
|
state->cap = Cclientpreedit;
|
|
state->next = ics;
|
|
ics = state;
|
|
xcb_im_input_context_set_data(ic, state, icfree);
|
|
}
|
|
|
|
static void
|
|
callback(xcb_im_t *im, xcb_im_client_t *client, xcb_im_input_context_t *ic,
|
|
const xcb_im_packet_header_fr_t *hdr, void *frame, void *arg, void *user)
|
|
{
|
|
xcb_key_press_event_t *ev;
|
|
Keyres res;
|
|
Ic *state;
|
|
|
|
USED(im);
|
|
USED(frame);
|
|
USED(user);
|
|
if(hdr->major_opcode == XCB_XIM_DISCONNECT ||
|
|
hdr->major_opcode == XCB_XIM_CLOSE){
|
|
for(state = ics; state != nil; state = state->next)
|
|
if(state->client == client)
|
|
release(state);
|
|
return;
|
|
}
|
|
if(hdr->major_opcode == XCB_XIM_CREATE_IC){
|
|
iccreate(client, ic);
|
|
return;
|
|
}
|
|
if(ic == nil)
|
|
return;
|
|
state = xcb_im_input_context_get_data(ic);
|
|
switch(hdr->major_opcode){
|
|
case XCB_XIM_FORWARD_EVENT:
|
|
ev = arg;
|
|
if(ev != nil && (ev->response_type & ~0x80) == XCB_KEY_PRESS)
|
|
kpress(state, ev);
|
|
break;
|
|
case XCB_XIM_SET_IC_VALUES:
|
|
case XCB_XIM_SET_IC_FOCUS:
|
|
place(state);
|
|
if(state->engaged)
|
|
sendrequest(state, Keycaret, 0, 0, &res);
|
|
break;
|
|
case XCB_XIM_DESTROY_IC:
|
|
case XCB_XIM_UNSET_IC_FOCUS:
|
|
release(state);
|
|
break;
|
|
case XCB_XIM_RESET_IC:
|
|
resetic(state, arg);
|
|
break;
|
|
}
|
|
}
|
|
|
|
static int
|
|
ximinit(void)
|
|
{
|
|
xcb_screen_t *screen;
|
|
xcb_window_t win;
|
|
xcb_im_styles_t st;
|
|
xcb_im_encodings_t enc;
|
|
int scr;
|
|
|
|
replyc = chancreate(sizeof(Keyres), 0);
|
|
st.nStyles = nelem(styles);
|
|
st.styles = styles;
|
|
enc.nEncodings = nelem(encs);
|
|
enc.encodings = encs;
|
|
xcb_compound_text_init();
|
|
conn = xcb_connect(nil, &scr);
|
|
if(xcb_connection_has_error(conn)){
|
|
ximlog("cannot connect to X server");
|
|
return -1;
|
|
}
|
|
screen = getscreen(scr);
|
|
if(screen == nil){
|
|
ximlog("cannot find X screen");
|
|
return -1;
|
|
}
|
|
rootwin = screen->root;
|
|
if(kinit() < 0){
|
|
ximlog("cannot read keyboard mapping");
|
|
return -1;
|
|
}
|
|
win = xcb_generate_id(conn);
|
|
xcb_create_window(conn, XCB_COPY_FROM_PARENT, win, screen->root,
|
|
0, 0, 1, 1, 0, XCB_WINDOW_CLASS_INPUT_OUTPUT,
|
|
screen->root_visual, 0, nil);
|
|
xim = xcb_im_create(conn, scr, win, "strans",
|
|
XCB_IM_ALL_LOCALES, &st, nil, nil, &enc,
|
|
XCB_EVENT_MASK_KEY_PRESS, callback, nil);
|
|
if(xim == nil){
|
|
ximlog("cannot create XIM server");
|
|
return -1;
|
|
}
|
|
if(!xcb_im_open_im(xim)){
|
|
ximlog("cannot claim XIM selection");
|
|
return -1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
void
|
|
ximthread(void *arg)
|
|
{
|
|
struct pollfd pfd;
|
|
xcb_generic_event_t *ev;
|
|
uint8_t type;
|
|
int n;
|
|
|
|
USED(arg);
|
|
threadsetname("xim");
|
|
if(ximinit() < 0)
|
|
die("xim: initialization failed");
|
|
pfd.fd = xcb_get_file_descriptor(conn);
|
|
pfd.events = POLLIN;
|
|
for(;;){
|
|
pfd.revents = 0;
|
|
n = poll(&pfd, 1, preowner != nil ? Ownerpoll : -1);
|
|
if(n < 0 && errno != EINTR)
|
|
break;
|
|
while((ev = xcb_poll_for_event(conn)) != nil){
|
|
type = ev->response_type & ~0x80;
|
|
if(type == XCB_MAPPING_NOTIFY){
|
|
if(kinit() < 0)
|
|
ximlog("cannot refresh keyboard mapping");
|
|
}else
|
|
xcb_im_filter_event(xim, ev);
|
|
free(ev);
|
|
}
|
|
checkpreowner();
|
|
if(pfd.revents & (POLLERR|POLLHUP|POLLNVAL))
|
|
break;
|
|
}
|
|
if(xcb_connection_has_error(conn))
|
|
die("xim: X server disconnected");
|
|
die("xim: frontend stopped");
|
|
}
|