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.
556 lines
12 KiB
C
556 lines
12 KiB
C
#include "dat.h"
|
|
#include "fn.h"
|
|
|
|
#include <errno.h>
|
|
#include <poll.h>
|
|
#include <xcb/xcb.h>
|
|
#include <xcb/xkb.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 uint8_t xkbevent;
|
|
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);
|
|
}
|
|
|
|
/* Reads the server's keymap; XKB-aware clients get XKB events, not MappingNotify. */
|
|
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, &xkbevent, 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 void
|
|
kwatch(void)
|
|
{
|
|
u16int events, parts;
|
|
|
|
events = XCB_XKB_EVENT_TYPE_NEW_KEYBOARD_NOTIFY |
|
|
XCB_XKB_EVENT_TYPE_MAP_NOTIFY;
|
|
parts = XCB_XKB_MAP_PART_KEY_TYPES | XCB_XKB_MAP_PART_KEY_SYMS |
|
|
XCB_XKB_MAP_PART_MODIFIER_MAP |
|
|
XCB_XKB_MAP_PART_EXPLICIT_COMPONENTS |
|
|
XCB_XKB_MAP_PART_KEY_ACTIONS | XCB_XKB_MAP_PART_VIRTUAL_MODS |
|
|
XCB_XKB_MAP_PART_VIRTUAL_MOD_MAP;
|
|
xcb_xkb_select_events(conn, XCB_XKB_ID_USE_CORE_KBD, events, 0,
|
|
events, parts, parts, nil);
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
/* Losing the engine commits the pending text; the release hands it back. */
|
|
static void
|
|
release(Ic *state)
|
|
{
|
|
Keyres res;
|
|
char buf[Maxutf];
|
|
int n;
|
|
|
|
if(!state->engaged){
|
|
clearpreedit(state);
|
|
return;
|
|
}
|
|
sendrequest(state, Keyrelease, 0, 0, &res);
|
|
state->engaged = 0;
|
|
n = stoutf(&res.commit, buf, sizeof buf);
|
|
commit(state, buf, n);
|
|
clearpreedit(state);
|
|
xcb_flush(conn);
|
|
}
|
|
|
|
/* XIM reset hands the pending text back to the client as committed text. */
|
|
static void
|
|
resetic(Ic *state, xcb_im_reset_ic_reply_fr_t *reply)
|
|
{
|
|
Keyres res;
|
|
char utf[Maxutf], *wire;
|
|
size_t nwire;
|
|
int nbyte;
|
|
|
|
if(!state->engaged){
|
|
clearpreedit(state);
|
|
return;
|
|
}
|
|
sendrequest(state, Keyreset, 0, 0, &res);
|
|
clearpreedit(state);
|
|
if(reply == nil || res.commit.n == 0)
|
|
return;
|
|
nbyte = stoutf(&res.commit, 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;
|
|
}
|
|
kwatch();
|
|
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 == xkbevent){
|
|
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");
|
|
}
|