From 6d02648d3a2ab9ee9751db8783f00b14071f57b4 Mon Sep 17 00:00:00 2001 From: duarm Date: Tue, 8 Nov 2022 15:18:45 -0300 Subject: [PATCH] input method (#1735) * input method draft * restoring relese event * using unused macro, removing debug code, handling disconnection * review fixes, new update_im_window_pos method * initializing variables correctly * initializing im pos queue correctly * ime window positioning * add widget_get_y_pos() position * [Build] Update makefile with imdkit * [CI] Add imdkit as dependency. * [XCB] rofi_view_paste don't throw warning, print debug. * [XCB] rofi_view_paste lower 'failed to convert selection' * [Build] Add minimum version check to imdkit * new macro XCB_IMDKIT_1_0_3_LOWER * [Build] Try to support old version of imdkit in meson/makefile. * [Build] Fix typo in meson.build * [XIM] Don't set use compound/set use utf8 when on old version. * [Build] Allow building without imdkit. * [Doc] Add imdkit to dependency list. Co-authored-by: Dave Davenport --- .github/actions/setup/action.yml | 1 + INSTALL.md | 1 + Makefile.am | 2 + configure.ac | 6 +- include/view.h | 10 +++ include/xcb-internal.h | 5 ++ include/xcb.h | 12 +++ meson.build | 20 ++++- source/rofi.c | 7 ++ source/view.c | 116 ++++++++++++++++++++++++ source/xcb.c | 149 +++++++++++++++++++++++++++---- 11 files changed, 308 insertions(+), 21 deletions(-) diff --git a/.github/actions/setup/action.yml b/.github/actions/setup/action.yml index 0f641a5f..14973be3 100644 --- a/.github/actions/setup/action.yml +++ b/.github/actions/setup/action.yml @@ -29,6 +29,7 @@ runs: libxcb-xkb-dev \ libxcb-xrm-dev \ libxcb-cursor-dev \ + libxcb-imdkit-dev \ libxkbcommon-dev \ libxkbcommon-dev \ libxkbcommon-x11-dev \ diff --git a/INSTALL.md b/INSTALL.md index f62ffbfc..dd7a5401 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -39,6 +39,7 @@ You can also use [Meson](https://mesonbuild.com/) as an alternative. * xcb-util * xcb-util-wm (sometimes split as libxcb-ewmh and libxcb-icccm) * xcb-util-cursor +* xcb-imdkit (optional, 1.0.3 or up preferred) On debian based systems, the developer packages are in the form of: `-dev` on rpm based `-devel`. diff --git a/Makefile.am b/Makefile.am index 24e5526a..4e316e74 100644 --- a/Makefile.am +++ b/Makefile.am @@ -145,6 +145,7 @@ rofi_CFLAGS=\ $(libsn_CFLAGS)\ $(cairo_CFLAGS)\ $(gdkpixbuf_CFLAGS)\ + $(imdclient_CFLAGS)\ -DMANPAGE_PATH="\"$(mandir)/\""\ -I$(top_srcdir)/include/\ -I$(top_builddir)/lexer/\ @@ -165,6 +166,7 @@ rofi_LDADD=\ $(pango_LIBS)\ $(cairo_LIBS)\ $(gdkpixbuf_LIBS)\ + $(imdclient_LIBS)\ $(LIBS) ## diff --git a/configure.ac b/configure.ac index de043d48..cdef673d 100644 --- a/configure.ac +++ b/configure.ac @@ -145,7 +145,11 @@ NK_INIT([bindings xdg-theme]) PKG_CHECK_MODULES([glib], [glib-2.0 >= ${glib_min_version} gio-unix-2.0 gmodule-2.0]) AC_DEFINE_UNQUOTED([GLIB_VERSION_MIN_REQUIRED], [(G_ENCODE_VERSION(${glib_min_major},${glib_min_minor}))], [The lower GLib version supported]) AC_DEFINE_UNQUOTED([GLIB_VERSION_MAX_ALLOWED], [(G_ENCODE_VERSION(${glib_min_major},${glib_min_minor}))], [The highest GLib version supported]) -GW_CHECK_XCB([xcb-aux xcb-xkb xkbcommon xkbcommon-x11 xcb-ewmh xcb-icccm xcb-cursor xcb-randr xcb-xinerama]) +GW_CHECK_XCB([xcb-aux xcb-xkb xkbcommon xkbcommon-x11 xcb-ewmh xcb-icccm xcb-cursor xcb-randr xcb-xinerama ]) +PKG_CHECK_MODULES([imdclient], [xcb-imdkit <= 1.0.2 ], + [AC_DEFINE([XCB_IMDKIT_1_0_3_LOWER], [1], [Indicate lower version of imdclient]) + AC_DEFINE([XCB_IMDKIT],[1], [IMD Kit missing])], + [PKG_CHECK_MODULES([imdclient], [xcb-imdkit >= 1.0.3],[AC_DEFINE([XCB_IMDKIT],[1], [IMD Kit missing])],[HAVE_IMDKIT=0])]) PKG_CHECK_MODULES([pango], [pango pangocairo]) PKG_CHECK_MODULES([cairo], [cairo cairo-xcb]) PKG_CHECK_MODULES([libsn], [libstartup-notification-1.0 ]) diff --git a/include/view.h b/include/view.h index 8fbe0159..9ca8bfbf 100644 --- a/include/view.h +++ b/include/view.h @@ -341,5 +341,15 @@ void rofi_view_set_window_title(const char *title); * set ellipsize mode to start. */ void rofi_view_ellipsize_start(RofiViewState *state); + +/** + * @param new_x New XIM window x pos + * @param new_y New XIM window y pos + * + * Updates the XIM window position to new_x and new_y, relative to the + * main_window + */ +gboolean rofi_set_im_window_pos(int new_x, int new_y); + /** @} */ #endif diff --git a/include/xcb-internal.h b/include/xcb-internal.h index f2d78ca1..c83ef585 100644 --- a/include/xcb-internal.h +++ b/include/xcb-internal.h @@ -32,6 +32,7 @@ #include #include +#include "xcb.h" #include #include #include @@ -45,6 +46,10 @@ struct _xcb_stuff { GMainLoop *main_loop; GWaterXcbSource *source; xcb_connection_t *connection; +#ifdef XCB_IMDKIT + xcb_xic_t ic; + xcb_xim_t *im; +#endif xcb_ewmh_connection_t ewmh; xcb_screen_t *screen; int screen_nbr; diff --git a/include/xcb.h b/include/xcb.h index afc9c5ec..2b847645 100644 --- a/include/xcb.h +++ b/include/xcb.h @@ -29,6 +29,10 @@ #define ROFI_XCB_H #include +#include +#ifdef XCB_IMDKIT +#include +#endif #include /** @@ -220,6 +224,7 @@ extern WindowManagerQuirk current_window_manager; * @returns NULL if window was not found, or unmapped, otherwise returns a * cairo_surface. */ + cairo_surface_t *x11_helper_get_screenshot_surface_window(xcb_window_t window, int size); @@ -233,4 +238,11 @@ cairo_surface_t *x11_helper_get_screenshot_surface_window(xcb_window_t window, void cairo_image_surface_blur(cairo_surface_t *surface, double radius, double deviation); +#ifdef XCB_IMDKIT +/** + * IME Forwarding + */ +void x11_event_handler_fowarding(xcb_xim_t *im, xcb_xic_t ic, + xcb_key_press_event_t *event, void *user_data); +#endif #endif diff --git a/meson.build b/meson.build index 98a56f10..2c8d476a 100644 --- a/meson.build +++ b/meson.build @@ -74,9 +74,27 @@ deps += [ dependency('libstartup-notification-1.0'), ] -check = dependency('check', version: '>= 0.11.0', required: get_option('check')) +imdkit_new = dependency('xcb-imdkit', version: '>= 1.0.3', required: false) +imdkit_old = dependency('xcb-imdkit', version: '<= 1.0.2', required: false) + +check = dependency('check', version: '>= 0.11.0', required: get_option('check')) header_conf = configuration_data() + +if imdkit_new.found() + deps += imdkit_new + header_conf.set('XCB_IMDKIT_1_0_3_LOWER', false) + header_conf.set('XCB_IMDKIT', true) +elif imdkit_old.found() + deps+= imdkit_old + header_conf.set('XCB_IMDKIT_1_0_3_LOWER', true) + header_conf.set('XCB_IMDKIT', true) +else + header_conf.set('XCB_IMDKIT_1_0_3_LOWER', false) + header_conf.set('XCB_IMDKIT', false) +endif + + header_conf.set_quoted('PACKAGE_NAME', meson.project_name()) header_conf.set_quoted('PACKAGE_VERSION', meson.project_version()) header_conf.set_quoted('VERSION', meson.project_version()) diff --git a/source/rofi.c b/source/rofi.c index 0bf4be81..d8d6a1f1 100644 --- a/source/rofi.c +++ b/source/rofi.c @@ -379,6 +379,13 @@ static void help(G_GNUC_UNUSED int argc, char **argv) { #else printf("\t• asan %sdisabled%s\n", is_term ? color_red : "", is_term ? color_reset : ""); +#endif +#ifdef XCB_IMDKIT + printf("\t• imdkit %senabled%s\n", is_term ? color_green : "", + is_term ? color_reset : ""); +#else + printf("\t• imdkit %sdisabled%s\n", is_term ? color_red : "", + is_term ? color_reset : ""); #endif printf("\n"); printf("For more information see: %sman rofi%s\n", is_term ? color_bold : "", diff --git a/source/view.c b/source/view.c index cfa96438..1c432891 100644 --- a/source/view.c +++ b/source/view.c @@ -38,6 +38,9 @@ #include #include #include +#ifdef XCB_IMDKIT +#include +#endif #include #include #include @@ -79,6 +82,20 @@ void rofi_view_update(RofiViewState *state, gboolean qr); static int rofi_view_calculate_height(RofiViewState *state); +#ifdef XCB_IMDKIT +static void xim_commit_string(xcb_xim_t *im, G_GNUC_UNUSED xcb_xic_t ic, + G_GNUC_UNUSED uint32_t flag, char *str, + uint32_t length, G_GNUC_UNUSED uint32_t *keysym, + G_GNUC_UNUSED size_t nKeySym, + G_GNUC_UNUSED void *user_data); +static void xim_disconnected(G_GNUC_UNUSED xcb_xim_t *im, + G_GNUC_UNUSED void *user_data); +xcb_xim_im_callback xim_callback = {.forward_event = + x11_event_handler_fowarding, + .commit_string = xim_commit_string, + .disconnected = xim_disconnected}; +#endif + /** Thread pool used for filtering */ GThreadPool *tpool = NULL; @@ -784,6 +801,86 @@ rofi_view_setup_fake_transparency(widget *win, TICK_N("Fake transparency"); } } + +#ifdef XCB_IMDKIT +static void xim_commit_string(xcb_xim_t *im, G_GNUC_UNUSED xcb_xic_t ic, + G_GNUC_UNUSED uint32_t flag, char *str, + uint32_t length, G_GNUC_UNUSED uint32_t *keysym, + G_GNUC_UNUSED size_t nKeySym, + G_GNUC_UNUSED void *user_data) { + RofiViewState *state = rofi_view_get_active(); + if (state == NULL) { + return; + } + +#ifndef XCB_IMDKIT_1_0_3_LOWER + if (xcb_xim_get_encoding(im) == XCB_XIM_UTF8_STRING) { + rofi_view_handle_text(state, str); + } else if (xcb_xim_get_encoding(im) == XCB_XIM_COMPOUND_TEXT) { + size_t newLength = 0; + char *utf8 = xcb_compound_text_to_utf8(str, length, &newLength); + if (utf8) { + rofi_view_handle_text(state, utf8); + } + } +#else + size_t newLength = 0; + char *utf8 = xcb_compound_text_to_utf8(str, length, &newLength); + if (utf8) { + rofi_view_handle_text(state, utf8); + } +#endif +} + +static void xim_disconnected(G_GNUC_UNUSED xcb_xim_t *im, + G_GNUC_UNUSED void *user_data) { + xcb->ic = 0; +} + +static void create_ic_callback(xcb_xim_t *im, xcb_xic_t new_ic, + G_GNUC_UNUSED void *user_data) { + xcb->ic = new_ic; + if (xcb->ic) { + xcb_xim_set_ic_focus(im, xcb->ic); + } +} + +gboolean rofi_set_im_window_pos(int new_x, int new_y) { + if (!xcb->ic) + return false; + + static xcb_point_t spot = {.x = 0, .y = 0}; + if (spot.x != new_x || spot.y != new_y) { + spot.x = new_x; + spot.y = new_y; + xcb_xim_nested_list nested = xcb_xim_create_nested_list( + xcb->im, XCB_XIM_XNSpotLocation, &spot, NULL); + xcb_xim_set_ic_values(xcb->im, xcb->ic, NULL, NULL, XCB_XIM_XNClientWindow, + &CacheState.main_window, XCB_XIM_XNFocusWindow, + &CacheState.main_window, XCB_XIM_XNPreeditAttributes, + &nested, NULL); + free(nested.data); + } + return true; +} +static void open_xim_callback(xcb_xim_t *im, G_GNUC_UNUSED void *user_data) { + RofiViewState *state = rofi_view_get_active(); + uint32_t input_style = XCB_IM_PreeditPosition | XCB_IM_StatusArea; + xcb_point_t spot; + spot.x += widget_get_x_pos(&state->text->widget) + + textbox_get_cursor_x_pos(state->text); + spot.y += widget_get_y_pos(&state->text->widget) + + widget_get_height(&state->text->widget); + xcb_xim_nested_list nested = + xcb_xim_create_nested_list(im, XCB_XIM_XNSpotLocation, &spot, NULL); + xcb_xim_create_ic( + im, create_ic_callback, NULL, XCB_XIM_XNInputStyle, &input_style, + XCB_XIM_XNClientWindow, &CacheState.main_window, XCB_XIM_XNFocusWindow, + &CacheState.main_window, XCB_XIM_XNPreeditAttributes, &nested, NULL); + free(nested.data); +} +#endif + void __create_window(MenuFlags menu_flags) { uint32_t selmask = XCB_CW_BACK_PIXMAP | XCB_CW_BORDER_PIXEL | XCB_CW_BIT_GRAVITY | XCB_CW_BACKING_STORE | @@ -799,6 +896,15 @@ void __create_window(MenuFlags menu_flags) { XCB_GRAVITY_STATIC, XCB_BACKING_STORE_NOT_USEFUL, xcb_event_masks, map}; +#ifdef XCB_IMDKIT + xcb_xim_set_im_callback(xcb->im, &xim_callback, NULL); +#endif + +// Open connection to XIM server. +#ifdef XCB_IMDKIT + xcb_xim_open(xcb->im, open_xim_callback, true, NULL); +#endif + xcb_window_t box_window = xcb_generate_id(xcb->connection); xcb_void_cookie_t cc = xcb_create_window_checked( xcb->connection, depth->depth, box_window, xcb_stuff_get_root_window(), 0, @@ -810,6 +916,7 @@ void __create_window(MenuFlags menu_flags) { g_error("xcb_create_window() failed error=0x%x\n", error->error_code); exit(EXIT_FAILURE); } + TICK_N("xcb create window"); CacheState.gc = xcb_generate_id(xcb->connection); xcb_create_gc(xcb->connection, CacheState.gc, box_window, 0, 0); @@ -1107,6 +1214,7 @@ static void update_callback(textbox *t, icon *ico, unsigned int index, textbox_set_pango_attributes(t, list); pango_attr_list_unref(list); } + g_list_free(add_list); g_free(text); } else { @@ -1149,6 +1257,14 @@ void rofi_view_update(RofiViewState *state, gboolean qr) { cairo_set_operator(d, CAIRO_OPERATOR_OVER); widget_draw(WIDGET(state->main_window), d); +#ifdef XCB_IMDKIT + int x = widget_get_x_pos(&state->text->widget) + + textbox_get_cursor_x_pos(state->text); + int y = widget_get_y_pos(&state->text->widget) + + widget_get_height(&state->text->widget); + rofi_set_im_window_pos(x, y); +#endif + TICK_N("widgets"); cairo_surface_flush(CacheState.edit_surf); if (qr) { diff --git a/source/xcb.c b/source/xcb.c index e7959b04..db9e344a 100644 --- a/source/xcb.c +++ b/source/xcb.c @@ -30,6 +30,9 @@ #define G_LOG_DOMAIN "X11Helper" #include "config.h" +#ifdef XCB_IMDKIT +#include +#endif #include #include #include @@ -62,6 +65,7 @@ #include "xcb-internal.h" #include "xcb.h" #include +#include #include "mode.h" #include "modes/window.h" @@ -84,6 +88,9 @@ WindowManagerQuirk current_window_manager = WM_EWHM; */ struct _xcb_stuff xcb_int = {.connection = NULL, .screen = NULL, +#ifdef XCB_IMDKIT + .im = NULL, +#endif .screen_nbr = -1, .sndisplay = NULL, .sncontext = NULL, @@ -1061,6 +1068,35 @@ int monitor_active(workarea *mon) { return FALSE; } +static bool get_atom_name(xcb_connection_t *conn, xcb_atom_t atom, char **out) { + xcb_get_atom_name_cookie_t cookie; + xcb_get_atom_name_reply_t *reply; + int length; + char *name; + + if (atom == 0) { + *out = NULL; + return true; + } + + cookie = xcb_get_atom_name(conn, atom); + reply = xcb_get_atom_name_reply(conn, cookie, NULL); + if (!reply) + return false; + + length = xcb_get_atom_name_name_length(reply); + name = xcb_get_atom_name_name(reply); + + (*out) = g_strndup(name, length); + if (!(*out)) { + free(reply); + return false; + } + + free(reply); + return true; +} + /** * @param state Internal state of the menu. * @param xse X selection event. @@ -1070,7 +1106,7 @@ int monitor_active(workarea *mon) { static void rofi_view_paste(RofiViewState *state, xcb_selection_notify_event_t *xse) { if (xse->property == XCB_ATOM_NONE) { - g_warning("Failed to convert selection"); + g_debug("Failed to convert selection"); } else if (xse->property == xcb->ewmh.UTF8_STRING) { gchar *text = window_get_text_prop(xse->requestor, xcb->ewmh.UTF8_STRING); if (text != NULL && text[0] != '\0') { @@ -1085,7 +1121,13 @@ static void rofi_view_paste(RofiViewState *state, } g_free(text); } else { - g_warning("Failed"); + char *out = NULL; + if (get_atom_name(xcb->connection, xse->property, &out)) { + g_debug("rofi_view_paste: Got unknown atom: %s", out); + g_free(out); + } else { + g_debug("rofi_view_paste: Got unknown, unnamed: %s", out); + } } } @@ -1142,6 +1184,32 @@ static gboolean x11_button_to_nk_bindings_scroll(guint32 x11_button, return TRUE; } +static void rofi_key_press_event_handler(xcb_key_press_event_t *xkpe, + RofiViewState *state) { + gchar *text; + + xcb->last_timestamp = xkpe->time; + if (config.xserver_i300_workaround) { + text = nk_bindings_seat_handle_key_with_modmask( + xcb->bindings_seat, NULL, xkpe->state, xkpe->detail, + NK_BINDINGS_KEY_STATE_PRESS); + } else { + text = nk_bindings_seat_handle_key(xcb->bindings_seat, NULL, xkpe->detail, + NK_BINDINGS_KEY_STATE_PRESS); + } + if (text != NULL) { + rofi_view_handle_text(state, text); + g_free(text); + } +} + +static void rofi_key_release_event_handler(xcb_key_release_event_t *xkre, + G_GNUC_UNUSED RofiViewState *state) { + xcb->last_timestamp = xkre->time; + nk_bindings_seat_handle_key(xcb->bindings_seat, NULL, xkre->detail, + NK_BINDINGS_KEY_STATE_RELEASE); +} + /** * Process X11 events in the main-loop (gui-thread) of the application. */ @@ -1301,28 +1369,26 @@ static void main_loop_x11_event_handler_view(xcb_generic_event_t *event) { } case XCB_KEY_PRESS: { xcb_key_press_event_t *xkpe = (xcb_key_press_event_t *)event; - gchar *text; - - xcb->last_timestamp = xkpe->time; - if (config.xserver_i300_workaround) { - text = nk_bindings_seat_handle_key_with_modmask( - xcb->bindings_seat, NULL, xkpe->state, xkpe->detail, - NK_BINDINGS_KEY_STATE_PRESS); - } else { - text = nk_bindings_seat_handle_key(xcb->bindings_seat, NULL, xkpe->detail, - NK_BINDINGS_KEY_STATE_PRESS); - } - if (text != NULL) { - rofi_view_handle_text(state, text); - g_free(text); +#ifdef XCB_IMDKIT + if (xcb->ic) { + xcb_xim_forward_event(xcb->im, xcb->ic, xkpe); + } else +#endif + { + rofi_key_press_event_handler(xkpe, state); } break; } case XCB_KEY_RELEASE: { xcb_key_release_event_t *xkre = (xcb_key_release_event_t *)event; - xcb->last_timestamp = xkre->time; - nk_bindings_seat_handle_key(xcb->bindings_seat, NULL, xkre->detail, - NK_BINDINGS_KEY_STATE_RELEASE); +#ifdef XCB_IMDKIT + if (xcb->ic) { + xcb_xim_forward_event(xcb->im, xcb->ic, xkre); + } else +#endif + { + rofi_key_release_event_handler(xkre, state); + } break; } default: @@ -1331,6 +1397,25 @@ static void main_loop_x11_event_handler_view(xcb_generic_event_t *event) { rofi_view_maybe_update(state); } +#ifdef XCB_IMDKIT +void x11_event_handler_fowarding(G_GNUC_UNUSED xcb_xim_t *im, + G_GNUC_UNUSED xcb_xic_t ic, + xcb_key_press_event_t *event, + G_GNUC_UNUSED void *user_data) { + RofiViewState *state = rofi_view_get_active(); + if (state == NULL) { + return; + } + uint8_t type = event->response_type & ~0x80; + if (type == XCB_KEY_PRESS) { + rofi_key_press_event_handler(event, state); + } else if (type == XCB_KEY_RELEASE) { + xcb_key_release_event_t *xkre = (xcb_key_release_event_t *)event; + rofi_key_release_event_handler(xkre, state); + } +} +#endif + static gboolean main_loop_x11_event_handler(xcb_generic_event_t *ev, G_GNUC_UNUSED gpointer user_data) { if (ev == NULL) { @@ -1346,6 +1431,13 @@ static gboolean main_loop_x11_event_handler(xcb_generic_event_t *ev, // status); return G_SOURCE_CONTINUE; } + +#ifdef XCB_IMDKIT + if (xcb->im && xcb_xim_filter_event(xcb->im, ev)) { + return G_SOURCE_CONTINUE; + } +#endif + uint8_t type = ev->response_type & ~0x80; if (type == xcb->xkb.first_event) { switch (ev->pad0) { @@ -1375,6 +1467,7 @@ static gboolean main_loop_x11_event_handler(xcb_generic_event_t *ev, if (xcb->sndisplay != NULL) { sn_xcb_display_process_event(xcb->sndisplay, ev); } + main_loop_x11_event_handler_view(ev); return G_SOURCE_CONTINUE; } @@ -1542,6 +1635,9 @@ gboolean display_setup(GMainLoop *main_loop, NkBindings *bindings) { find_arg_str("-display", &display_str); xcb->main_loop = main_loop; +#ifdef XCB_IMDKIT + xcb_compound_text_init(); +#endif xcb->source = g_water_xcb_source_new(g_main_loop_get_context(xcb->main_loop), display_str, &xcb->screen_nbr, main_loop_x11_event_handler, NULL, NULL); @@ -1550,6 +1646,16 @@ gboolean display_setup(GMainLoop *main_loop, NkBindings *bindings) { return FALSE; } xcb->connection = g_water_xcb_source_get_connection(xcb->source); +#ifdef XCB_IMDKIT + xcb->im = xcb_xim_create(xcb->connection, xcb->screen_nbr, NULL); +#endif + +#ifdef XCB_IMDKIT +#ifndef XCB_IMDKIT_1_0_3_LOWER + xcb_xim_set_use_compound_text(xcb->im, true); + xcb_xim_set_use_utf8_string(xcb->im, true); +#endif +#endif TICK_N("Open Display"); @@ -1819,6 +1925,11 @@ void display_cleanup(void) { xcb_ewmh_connection_wipe(&(xcb->ewmh)); xcb_flush(xcb->connection); xcb_aux_sync(xcb->connection); +#ifdef XCB_IMDKIT + xcb_xim_close(xcb->im); + xcb_xim_destroy(xcb->im); + xcb->im = NULL; +#endif g_water_xcb_source_free(xcb->source); xcb->source = NULL; xcb->connection = NULL;