#include #include #include #include "meego/meego-im-proxy.h" #include "meego/qt-translate.h" #include "meego/meego-im-defs.h" #include "ximserver.h" #include "xmim.h" #include "mimclient.h" #include "context.h" typedef struct _Context { guint16 connect_id; guint16 icid; XIMStyle style; Window toplevel_window; Window client_window; Window focus_window; GHashTable *info; gboolean preediting; GString *preedit_str; } Context; static GHashTable *contexts; static guint16 last_icid; static guint16 focused_icid; static MeegoIMProxy *proxy; static char * utf8_to_compound_text(const char *string) { XTextProperty tp; Xutf8TextListToTextProperty(x_dpy, (char **)&string, 1, XCompoundTextStyle, &tp); return (char*) tp.value; } static GValue* context_info_value_new(GType type) { GValue *val = g_slice_new0(GValue); return g_value_init(val, type); } static void context_info_value_destroy(GValue *val) { g_value_unset(val); g_slice_free(GValue, val); } static GValue *context_info_lookup(Context *ctx, const char* name) { return g_hash_table_lookup(ctx->info, name); } static void context_info_unset(Context *ctx, const char *name) { g_hash_table_remove(ctx->info, name); } static void context_info_set_bool(Context *ctx, const char* name, gboolean value) { GValue *val = context_info_lookup(ctx, name); if (val) { g_value_set_boolean(val, value); } else { val = context_info_value_new(G_TYPE_BOOLEAN); g_value_set_boolean(val, value); g_hash_table_insert(ctx->info, (gpointer)name, val); } } static void context_info_set_int(Context *ctx, const char* name, gint value) { GValue *val = context_info_lookup(ctx, name); if (val) { g_value_set_int(val, value); } else { val = context_info_value_new(G_TYPE_INT); g_value_set_int(val, value); g_hash_table_insert(ctx->info, (gpointer)name, val); } } static void context_info_set_uint64(Context *ctx, const char* name, guint64 value) { GValue *val = context_info_lookup(ctx, name); if (val) { g_value_set_uint64(val, value); } else { val = context_info_value_new(G_TYPE_UINT64); g_value_set_uint64(val, value); g_hash_table_insert(ctx->info, (gpointer)name, val); } } static void context_info_set_rect(Context *ctx, const char* name, int x, int y, int w, int h) { GValue *val = context_info_lookup(ctx, name); if (val) { dbus_g_type_struct_set(val, 0, x, 1, y, 2, w, 3, h, G_MAXUINT); } else { GType type = dbus_g_type_get_struct("GValueArray", G_TYPE_INT, G_TYPE_INT, G_TYPE_INT, G_TYPE_INT, G_TYPE_INVALID); GValueArray *arr = (GValueArray*) dbus_g_type_specialized_construct(type); g_return_if_fail(arr); val = context_info_value_new(type); g_value_take_boxed(val, arr); if (!dbus_g_type_struct_set(val, 0, x, 1, y, 2, w, 3, h, G_MAXUINT)) { g_value_unset(val); g_return_if_reached(); } g_hash_table_insert(ctx->info, (gpointer)name, val); } } static Context* context_alloc() { Context* ctx = g_slice_new0(Context); g_return_val_if_fail(ctx, NULL); ctx->info = g_hash_table_new_full(g_str_hash, g_str_equal, NULL, (GDestroyNotify) context_info_value_destroy); ctx->preedit_str = g_string_new(NULL); // TODO Move this somewhere else context_info_set_int(ctx, "contentType", MeegoImFreeTextContentType); if (opt_translucent) { context_info_set_bool(ctx, MeegoImInfoTranslucent, TRUE); } return ctx; } static void context_free(Context *ctx) { g_string_free(ctx->preedit_str, TRUE); g_hash_table_destroy(ctx->info); g_slice_free(Context, ctx); } void contexts_init() { contexts = g_hash_table_new_full(g_direct_hash, g_direct_equal, NULL, (GDestroyNotify) context_free); last_icid = 0; focused_icid = 0; proxy = meego_im_proxy_get_singleton(); } void contexts_destroy() { g_hash_table_destroy(contexts); } guint contexts_focused_icid() { return focused_icid; } static Context* contexts_lookup(guint icid) { return g_hash_table_lookup(contexts, GUINT_TO_POINTER(icid)); } static Window find_toplevel_window(Window win) { Window root, parent; Window *children = NULL; unsigned int nchildren; if (XQueryTree(x_dpy, win, &root, &parent, &children, &nchildren)) { if (children) XFree(children); if (parent == root) { return win; } else { g_return_val_if_fail(win != parent, win); return find_toplevel_window(parent); } } g_warn_if_reached(); return win; } static gboolean context_update_style(Context *ctx, XIMStyle style) { if (ctx->style != style) { ctx->style = style; if (ctx->style & XIMPreeditNone) { // No preedit? Set the virtual keyboard in direct/raw mode. context_info_set_int(ctx, "inputMethodMode", MeegoImInputMethodModeDirect); } else { context_info_set_int(ctx, "inputMethodMode", MeegoImInputMethodModeNormal); } return TRUE; } else { return FALSE; } } static gboolean context_update_client_window(Context *ctx, Window w) { if (ctx->client_window != w) { ctx->client_window = w; if (opt_xephyr) { // Consider the passed window id the top level window id // Remember NOT to use it!, it is not accessible from our X11 server! ctx->toplevel_window = opt_xephyr; } else { ctx->toplevel_window = find_toplevel_window(w); } if (ctx->toplevel_window) { context_info_set_uint64(ctx, "winId", ctx->toplevel_window); } else { context_info_unset(ctx, "winId"); } return TRUE; } else { return FALSE; } } static gboolean context_update_spot_location(Context *ctx, const XPoint *p) { Window win = ctx->focus_window; Window root = DefaultRootWindow(x_dpy); int x, y; Window child; if (!win) { win = ctx->client_window; } g_warn_if_fail(win); if (win && XTranslateCoordinates(x_dpy, win, root, p->x, p->y, &x, &y, &child)) { g_debug("Root coordinates %d %d", x, y); context_info_set_rect(ctx, "cursorRectangle", x, y, 2, 20); } else { g_warn_if_reached(); context_info_unset(ctx, "cursorRectangle"); } return TRUE; } static gboolean context_parse_ic_values(Context *ctx, IMChangeICStruct *call_data) { XICAttribute *attr; int attr_num; gboolean changed = FALSE; ctx->connect_id = call_data->connect_id; attr = call_data->ic_attr; attr_num = call_data->ic_attr_num; for (int i = 0; i < attr_num; ++i, ++attr) { if (strcmp(XNInputStyle, attr->name) == 0) { changed |= context_update_style(ctx, *(CARD32 *) attr->value); } else if (strcmp(XNClientWindow, attr->name) == 0) { changed |= context_update_client_window(ctx, *(CARD32 *) attr->value); } else if (strcmp(XNFocusWindow, attr->name) == 0) { ctx->focus_window = *(CARD32 *) attr->value; } else { g_warning("Ignoring unknown ic attribute: %s", attr->name); } } attr = call_data->preedit_attr; attr_num = call_data->preedit_attr_num; for (int i = 0; i < attr_num; ++i, ++attr) { if (strcmp(XNSpotLocation, attr->name) == 0) { changed |= context_update_spot_location(ctx, (XPoint*) attr->value); } else { g_debug("Ignoring unknown ic preedit attribute: %s", attr->name); } } return changed; } static void context_start_preedit(Context *ctx) { g_return_if_fail(!(ctx->style & XIMPreeditNone)); if (!ctx->preediting) { IMPreeditCBStruct pcb = {0}; pcb.major_code = XIM_PREEDIT_START; pcb.connect_id = ctx->connect_id; pcb.icid = ctx->icid; xims_call_callback((XPointer)&pcb); ctx->preediting = TRUE; } } static void context_stop_preedit(Context *ctx, gboolean commit) { if (ctx->preediting) { IMPreeditCBStruct pcb = {0}; pcb.major_code = XIM_PREEDIT_DONE; pcb.connect_id = ctx->connect_id; pcb.icid = ctx->icid; xims_call_callback((XPointer)&pcb); ctx->preediting = FALSE; if (commit) { xims_commit(ctx->connect_id, ctx->icid, 0, ctx->preedit_str->str); } g_string_truncate(ctx->preedit_str, 0); } } static void context_info_print_value(gpointer key, gpointer value, gpointer user_data) { GValue *val = value; const gchar *k = key; gchar *v = g_strdup_value_contents(val); g_debug(" %s: %s", k, v); } static void context_info_print(Context *ctx) { g_hash_table_foreach(ctx->info, context_info_print_value, NULL); } gboolean context_create(IMChangeICStruct *call_data) { // Safeguard against being overrun with entries g_return_val_if_fail(g_hash_table_size(contexts) < G_MAXINT16, FALSE); Context *ctx = context_alloc(); g_return_val_if_fail(ctx, FALSE); guint icid = ++last_icid; while (g_hash_table_lookup(contexts, GUINT_TO_POINTER(icid))) { // icid collision! Try luck with another one icid = ++last_icid; } ctx->connect_id = call_data->connect_id; ctx->icid = icid; context_parse_ic_values(ctx, call_data); g_hash_table_insert(contexts, GUINT_TO_POINTER(icid), ctx); call_data->icid = icid; return TRUE; } gboolean context_destroy(IMChangeICStruct *call_data) { const guint icid = call_data->icid; Context *ctx = g_hash_table_lookup(contexts, GUINT_TO_POINTER(icid)); if (icid == focused_icid) { g_message("Destroying focused context"); focused_icid = 0; } if (ctx) { g_hash_table_remove(contexts, GUINT_TO_POINTER(icid)); return TRUE; } else { g_warn_if_reached(); return FALSE; } } gboolean context_set_values(IMChangeICStruct *call_data) { Context *ctx = contexts_lookup(call_data->icid); g_return_val_if_fail(ctx, FALSE); gboolean changed = context_parse_ic_values(ctx, call_data); if (focused_icid == ctx->icid && changed) { meego_im_proxy_update_widget_info(proxy, ctx->info, FALSE); } return TRUE; } gboolean context_get_values(IMChangeICStruct *call_data) { Context *ctx = contexts_lookup(call_data->icid); g_return_val_if_fail(ctx, FALSE); XICAttribute *ic_attr = call_data->ic_attr; for (int i = 0; i < (int)call_data->ic_attr_num; i++, ic_attr++) { if (strcmp(XNFilterEvents, ic_attr->name) == 0) { ic_attr->value_length = sizeof(CARD32); ic_attr->value = (void *)malloc(sizeof(CARD32)); if (ctx->style & XIMPreeditNone) { // No need to filter events in direct mode *(CARD32*)ic_attr->value = 0; } else { *(CARD32*)ic_attr->value = KeyPressMask | KeyReleaseMask; } } } return TRUE; } gboolean context_set_focus(IMChangeFocusStruct *call_data) { Context *ctx = contexts_lookup(call_data->icid); g_return_val_if_fail(ctx, FALSE); context_info_set_bool(ctx, "focusState", TRUE); focused_icid = call_data->icid; if (meego_im_proxy_activate_context(proxy)) { meego_im_proxy_app_orientation_changed(proxy, MeegoImAngle0); meego_im_proxy_update_widget_info(proxy, ctx->info, TRUE); if (!(ctx->style & XIMStatusNone)) { meego_im_proxy_show_input_method(proxy); } else g_debug("Not showing the keyboard because of XIMStatusNone"); return TRUE; } else { g_warning("Failed to activate a Meego input context"); return FALSE; } } gboolean context_unset_focus(IMChangeFocusStruct *call_data) { Context *ctx = contexts_lookup(call_data->icid); g_return_val_if_fail(ctx, FALSE); context_stop_preedit(ctx, TRUE); context_info_set_bool(ctx, "focusState", FALSE); if (call_data->icid == focused_icid) { meego_im_proxy_reset(proxy); meego_im_proxy_hide_input_method(proxy); focused_icid = 0; } return TRUE; } gboolean context_forward_event(IMForwardEventStruct *call_data) { Context *ctx = contexts_lookup(call_data->icid); g_return_val_if_fail(ctx, FALSE); if (!redirect_keys) { g_debug("Not forwarding to mim"); xims_forward_event(call_data); return TRUE; } if (call_data->event.type == KeyPress || call_data->event.type == KeyRelease) { gint type, code, modifiers; gchar *text; MeegoImKeyEvent e; char smallbuf[32+1]; gchar *buf = NULL; char *chars = smallbuf; KeySym keysym; Status status; int len = Xutf8LookupString(x_ic, &call_data->event.xkey, smallbuf, 32, &keysym, &status); if (status == XBufferOverflow) { buf = g_malloc(len + 1); len = Xutf8LookupString(x_ic, &call_data->event.xkey, buf, len, &keysym, &status); chars = buf; } e.release = call_data->event.type == KeyRelease; e.state = call_data->event.xkey.state; e.keysym = XLookupKeysym(&call_data->event.xkey, 0); switch (status) { case XLookupBoth: case XLookupChars: chars[len] = '\0'; e.text = chars; break; default: e.text = NULL; break; } meego_im_key_event_encode(&e, &type, &code, &modifiers, &text); g_debug("Forwarding to mim %d %d %d %s", type, code, modifiers, text); meego_im_proxy_process_key_event(proxy, type, code, modifiers, text, FALSE, 1, call_data->event.xkey.keycode, e.state, call_data->event.xkey.time); if (buf) g_free(buf); } return TRUE; } gboolean context_reset(IMResetICStruct *call_data) { Context *ctx = contexts_lookup(call_data->icid); g_return_val_if_fail(ctx, FALSE); call_data->commit_string = utf8_to_compound_text(ctx->preedit_str->str); call_data->length = strlen(call_data->commit_string); meego_im_proxy_reset(proxy); context_stop_preedit(ctx, FALSE); // Please note that I've patched IMdkit so that it will call XFree on // call_data->commit_string. // You probably haven't, so take care to avoid more leaks. return TRUE; } gboolean context_commit(guint icid, const char *string, int replace_start, int replace_length, int cursor_pos) { Context *ctx = contexts_lookup(icid); g_return_val_if_fail(ctx, FALSE); g_return_val_if_fail(string, FALSE); context_stop_preedit(ctx, FALSE); xims_commit(ctx->connect_id, icid, 0, string); return TRUE; } gboolean context_update_preedit(guint icid, const char *string, int preedit_format_count, const MeegoImPreeditFormat *preedit_formats, int replace_start, int replace_length, int cursor_pos) { Context *ctx = contexts_lookup(icid); g_return_val_if_fail(ctx, FALSE); g_return_val_if_fail(string, FALSE); if (!(ctx->style & XIMPreeditCallbacks)) { g_message("Got update_preedit msg but preedit callbacks not supported"); return TRUE; } glong string_len = g_utf8_strlen(string, -1); if (string_len > 0) { context_start_preedit(ctx); // Will start only if not already started } else { context_stop_preedit(ctx, FALSE); return TRUE; } IMPreeditCBStruct s = { 0 }; XIMText text = { 0 }; XIMFeedback feedback[string_len + 1]; for (int c = 0; c < string_len; c++) { feedback[c] = XIMUnderline; } #if 0 for (int i = 0; i < preedit_format_count; i++) { const MeegoImPreeditFormat * f = &preedit_formats[i]; for (int c = f->start; c < f->start + f->length; c++) { switch (f->face) { case MeegoImPreeditDefault: feedback[c] = XIMUnderline; break; default: feedback[c] = 0; break; } } } #endif feedback[string_len] = 0; char *ctext = utf8_to_compound_text(string); text.string.multi_byte = ctext; text.length = strlen(ctext); text.feedback = feedback; g_debug("Sending text '%s'(%u)", text.string.multi_byte, text.length); s.major_code = XIM_PREEDIT_DRAW; s.connect_id = ctx->connect_id; s.icid = icid; s.todo.draw.text = &text; if (replace_start == 0 && replace_length == 0) { // Special case: Replace all the string // TODO Is this true? Does start=0, len=0 mean that? replace_start = 0; replace_length = ctx->preedit_str->len; g_string_assign(ctx->preedit_str, string); } else { g_warn_if_fail(replace_start < 0); g_warn_if_fail(replace_length < 0); g_string_erase(ctx->preedit_str, replace_start, replace_length); g_string_insert(ctx->preedit_str, replace_start, string); } s.todo.draw.chg_first = replace_start; s.todo.draw.chg_length = replace_length; if (cursor_pos < 0) { // Assume we mean at the end of the preedit string cursor_pos = ctx->preedit_str->len; } s.todo.draw.caret = cursor_pos; g_debug(" chg %d %d caret %d", s.todo.draw.chg_first, s.todo.draw.chg_length, s.todo.draw.caret); xims_call_callback((XPointer)&s); XFree(ctext); return TRUE; } gboolean context_send_key(guint icid, const MeegoImKeyEvent* e) { Context *ctx = contexts_lookup(icid); g_return_val_if_fail(ctx, FALSE); XEvent xev = { 0 }; xev.xkey.type = e->release ? KeyRelease : KeyPress; xev.xkey.send_event = True; xev.xkey.display = x_dpy; xev.xkey.window = ctx->client_window; xev.xkey.root = DefaultRootWindow(x_dpy); xev.xkey.subwindow = ctx->focus_window; xev.xkey.time = CurrentTime; xev.xkey.state = e->state; xev.xkey.keycode = XKeysymToKeycode(x_dpy, e->keysym); xev.xkey.same_screen = True; xims_insert_event(ctx->connect_id, icid, &xev); return TRUE; }