diff --git a/.flake8 b/.flake8 index c211bcb7f6..bfc9e2b850 100644 --- a/.flake8 +++ b/.flake8 @@ -1,5 +1,6 @@ [flake8] max-line-length = 120 +indent-size = 2 ignore = E402, # module level import not at top of file W504, # line break after binary operator diff --git a/common/core/desktop/.gitignore b/common/core/desktop/.gitignore index 90ca8c2bb1..e71a856da3 100644 --- a/common/core/desktop/.gitignore +++ b/common/core/desktop/.gitignore @@ -11,3 +11,9 @@ VERSION.md shellHelperFunctions.sh src/rust/keyman_keyboard_processor/target/ + +# The following files/subdirs are generated by autotools when building ibus-keyman. +# They can be removed after we convert ibus-keyman to use meson. +.deps/ +.dirstamp +*.o diff --git a/common/core/desktop/tests/unit/kmx/020 - deadkeys and backspace.kmn b/common/core/desktop/tests/unit/kmx/020 - deadkeys and backspace.kmn index b5d34a1624..0a3f97f461 100644 --- a/common/core/desktop/tests/unit/kmx/020 - deadkeys and backspace.kmn +++ b/common/core/desktop/tests/unit/kmx/020 - deadkeys and backspace.kmn @@ -6,14 +6,14 @@ c 4. Two deadkeys in a row in context dk(4a) dk(4b) + BKSP = nul c 5. One char and two deadkeys in context 'a' dk(5a) dk(5b) 'b' + BKSP = 'a' c 6. One char and two deadkeys and one char and two deadkeys in context 'a' dk(6a) dk(6b) 'b' dk(6c) dk(6d) + BKSP = 'a' c keys: [K_1][K_BKSP][K_2][K_BKSP][K_3][K_BKSP][K_4][K_BKSP][K_5][K_BKSP][K_6][K_BKSP] -c expected: 12aa -c context: 1234 +c expected: 78aa +c context: 7890 store(&VERSION) '9.0' begin unicode > use(main) -group(main) using keys +group(main) using keys + '1' > dk(1) + '2' > 'a' dk(2) diff --git a/linux/ibus-keyman/Makefile.am b/linux/ibus-keyman/Makefile.am index aee7e75b2f..3c7861def0 100644 --- a/linux/ibus-keyman/Makefile.am +++ b/linux/ibus-keyman/Makefile.am @@ -20,6 +20,7 @@ SUBDIRS = \ src \ + tests \ icons \ m4 \ po \ diff --git a/linux/ibus-keyman/configure.ac b/linux/ibus-keyman/configure.ac index f43279bd9b..e69817e3ef 100644 --- a/linux/ibus-keyman/configure.ac +++ b/linux/ibus-keyman/configure.ac @@ -100,6 +100,7 @@ Makefile src/Makefile src/keyman.xml.in src/keyman-version.h +tests/Makefile icons/Makefile m4/Makefile ]) diff --git a/linux/ibus-keyman/src/engine.c b/linux/ibus-keyman/src/engine.c index 930e8412f4..72c7a06ea0 100644 --- a/linux/ibus-keyman/src/engine.c +++ b/linux/ibus-keyman/src/engine.c @@ -162,13 +162,13 @@ static void ibus_keyman_engine_class_init (IBusKeymanEngineClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS (klass); - IBusObjectClass *ibus_object_class = IBUS_OBJECT_CLASS (klass); - IBusEngineClass *engine_class = IBUS_ENGINE_CLASS (klass); + IBusObjectClass *ibus_object_class = IBUS_OBJECT_CLASS (klass); + IBusEngineClass *engine_class = IBUS_ENGINE_CLASS (klass); - parent_class = (IBusEngineClass *) g_type_class_peek_parent (klass); + parent_class = (IBusEngineClass *) g_type_class_peek_parent (klass); object_class->constructor = ibus_keyman_engine_constructor; - ibus_object_class->destroy = (IBusObjectDestroyFunc) ibus_keyman_engine_destroy; + ibus_object_class->destroy = (IBusObjectDestroyFunc) ibus_keyman_engine_destroy; engine_class->process_key_event = ibus_keyman_engine_process_key_event; @@ -586,8 +586,7 @@ process_backspace_action( return FALSE; } else { g_message( - "DAR: ibus_keyman_engine_process_key_event - client_capabilities=%x, %x", engine->client_capabilities, - IBUS_CAP_SURROUNDING_TEXT); + "DAR: process_backspace_action - client_capabilities=%x, %x", engine->client_capabilities, IBUS_CAP_SURROUNDING_TEXT); if ((engine->client_capabilities & IBUS_CAP_SURROUNDING_TEXT) != 0) { g_message("deleting surrounding text 1 char"); diff --git a/linux/ibus-keyman/tests/.gitignore b/linux/ibus-keyman/tests/.gitignore new file mode 100644 index 0000000000..6d154ea019 --- /dev/null +++ b/linux/ibus-keyman/tests/.gitignore @@ -0,0 +1,2 @@ +.libs/ +ibus-keyman-tests diff --git a/linux/ibus-keyman/tests/Makefile.am b/linux/ibus-keyman/tests/Makefile.am new file mode 100644 index 0000000000..41a2bbbd3b --- /dev/null +++ b/linux/ibus-keyman/tests/Makefile.am @@ -0,0 +1,112 @@ +# vim:set noet ts=4: +# +# ibus-keyman unit tests +# +# Copyright (c) 2021 SIL International +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2, or (at your option) +# any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + +AUTOMAKE_OPTIONS = subdir-objects + +AM_CFLAGS = \ + @IBUS_CFLAGS@ \ + @GTK_CFLAGS@ \ + @JSON_GLIB_CFLAGS@ \ + @X11_CFLAGS@ \ + @KEYMAN_PROC_CFLAGS@ \ + -DPKGDATADIR=\"$(pkgdatadir)\" \ + $(NULL) + +AM_LDFLAGS = \ + @IBUS_LIBS@ \ + @GTK_LIBS@ \ + @X11_LIBS@ \ + @JSON_GLIB_LIBS@ \ + @KEYMAN_PROC_LIBS@ \ + $(NULL) + +libexec_PROGRAMS = ibus-keyman-tests + +ibus_keyman_tests_SOURCES = \ + ibusimcontext.c \ + ibusimcontext.h \ + testfixture.cpp \ + testmodule.c \ + testmodule.h \ + ../../../common/core/desktop/src/json.cpp \ + ../../../common/core/desktop/src/keyboard.cpp \ + ../../../common/core/desktop/src/kmx/kmx_actions.cpp \ + ../../../common/core/desktop/src/kmx/kmx_actions.h \ + ../../../common/core/desktop/src/kmx/kmx_base.h \ + ../../../common/core/desktop/src/kmx/kmx_capslock.cpp \ + ../../../common/core/desktop/src/kmx/kmx_consts.cpp \ + ../../../common/core/desktop/src/kmx/kmx_context.cpp \ + ../../../common/core/desktop/src/kmx/kmx_context.h \ + ../../../common/core/desktop/src/kmx/kmx_debug.cpp \ + ../../../common/core/desktop/src/kmx/kmx_debugger.cpp \ + ../../../common/core/desktop/src/kmx/kmx_debugger.h \ + ../../../common/core/desktop/src/kmx/kmx_environment.cpp \ + ../../../common/core/desktop/src/kmx/kmx_environment.h \ + ../../../common/core/desktop/src/kmx/kmx_file.cpp \ + ../../../common/core/desktop/src/kmx/kmx_file.h \ + ../../../common/core/desktop/src/kmx/kmx_modifiers.cpp \ + ../../../common/core/desktop/src/kmx/kmx_options.cpp \ + ../../../common/core/desktop/src/kmx/kmx_options.h \ + ../../../common/core/desktop/src/kmx/kmx_processevent.cpp \ + ../../../common/core/desktop/src/kmx/kmx_processevent.h \ + ../../../common/core/desktop/src/kmx/kmx_processor.cpp \ + ../../../common/core/desktop/src/kmx/kmx_processor.hpp \ + ../../../common/core/desktop/src/kmx/kmx_xstring.cpp \ + ../../../common/core/desktop/src/kmx/kmx_xstring.h \ + ../../../common/core/desktop/src/option.cpp \ + ../../../common/core/desktop/src/state.cpp \ + ../../../common/core/desktop/src/utfcodec.cpp \ + ../../../common/core/desktop/tests/unit/kmx/kmx_test_source.cpp \ + ../../../common/core/desktop/tests/unit/kmx/kmx_test_source.hpp \ + $(NULL) + +ibus_keyman_tests_CFLAGS = \ + @IBUS_CFLAGS@ \ + @GTK_CFLAGS@ \ + @X11_CFLAGS@ \ + @JSON_GLIB_CFLAGS@ \ + @KEYMAN_PROC_CFLAGS@ \ + -DPKGDATADIR=\"$(pkgdatadir)\" \ + -I../../../common/core/desktop/src \ + -I../../../common/core/desktop/src/kmx \ + -I../../../common/core/desktop/tests/unit/kmx \ + -I../src \ + $(NULL) + +ibus_keyman_tests_CPPFLAGS = \ + @IBUS_CFLAGS@ \ + @GTK_CFLAGS@ \ + @X11_CFLAGS@ \ + @JSON_GLIB_CFLAGS@ \ + @KEYMAN_PROC_CFLAGS@ \ + -DPKGDATADIR=\"$(pkgdatadir)\" \ + -I../../../common/core/desktop/src \ + -I../../../common/core/desktop/src/kmx \ + -I../../../common/core/desktop/tests/unit/kmx \ + -I../src \ + $(NULL) + +ibus_keyman_tests_LDFLAGS = \ + @GTK_LIBS@ \ + @IBUS_LIBS@ \ + @X11_LIBS@ \ + @JSON_GLIB_LIBS@ \ + @KEYMAN_PROC_LIBS@ \ + $(NULL) diff --git a/linux/ibus-keyman/tests/ibusimcontext.c b/linux/ibus-keyman/tests/ibusimcontext.c new file mode 100644 index 0000000000..5d3cf4a6bd --- /dev/null +++ b/linux/ibus-keyman/tests/ibusimcontext.c @@ -0,0 +1,1487 @@ +/* -*- mode: C; c-basic-offset: 4; indent-tabs-mode: nil; -*- */ +/* vim:set et sts=4: */ +/* ibus - The Input Bus + * Copyright (C) 2008-2013 Peng Huang + * Copyright (C) 2015-2021 Takao Fujiwara + * Copyright (C) 2008-2021 Red Hat, Inc. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + * USA + */ + +// This file is based on https://github.com/ibus/ibus/blob/master/client/gtk2/ibusimcontext.c + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "ibusimcontext.h" +#include +#include +#include +#include + +#ifdef GDK_WINDOWING_WAYLAND +#include +#endif + +#ifdef DEBUG +#define IDEBUG g_debug +#else +#define IDEBUG(a...) +#endif + +#define MAX_QUEUED_EVENTS 20 + +struct _IBusIMContext { + GtkIMContext parent; + + /* instance members */ + GtkIMContext *slave; + GdkWindow *client_window; + + IBusInputContext *ibuscontext; + + /* preedit status */ + gchar *preedit_string; + PangoAttrList *preedit_attrs; + gint preedit_cursor_pos; + gboolean preedit_visible; + guint preedit_mode; + + GdkRectangle cursor_area; + gboolean has_focus; + + guint32 time; + gint caps; + + /* cancellable */ + GCancellable *cancellable; + GQueue *events_queue; + + gboolean use_button_press_event; + + GMainLoop *thread_loop; + + // test properties + GString *text; +}; + +struct _IBusIMContextClass { + GtkIMContextClass parent; + /* class members */ +}; + +typedef struct _timeout_data { + GMainLoop *thread_loop; + gboolean timed_out; +} timeout_data; + +static GtkIMContext *_focus_im_context = NULL; +static IBusInputContext *_fake_context = NULL; +static GdkWindow *_input_window = NULL; +static GtkWidget *_input_widget = NULL; + +/* functions prototype */ +static void ibus_im_context_class_init(IBusIMContextClass *class); +static void ibus_im_context_class_fini(IBusIMContextClass *class); +static void ibus_im_context_init(GObject *obj); +static void ibus_im_context_notify(GObject *obj, GParamSpec *pspec); +static void ibus_im_context_finalize(GObject *obj); +static void ibus_im_context_reset(GtkIMContext *context); +static gboolean ibus_im_context_filter_keypress(GtkIMContext *context, GdkEventKey *key); +static void ibus_im_context_focus_in(GtkIMContext *context); +static void ibus_im_context_focus_out(GtkIMContext *context); +static void ibus_im_context_get_preedit_string(GtkIMContext *context, gchar **str, PangoAttrList **attrs, gint *cursor_pos); +static void ibus_im_context_set_client_window(GtkIMContext *context, GdkWindow *client); +static void ibus_im_context_set_cursor_location(GtkIMContext *context, GdkRectangle *area); +static void ibus_im_context_set_use_preedit(GtkIMContext *context, gboolean use_preedit); +static void ibus_im_context_set_surrounding(GtkIMContext *slave, const gchar *text, int len, int cursor_index); +static void ibus_im_context_set_surrounding_with_selection( + GtkIMContext *slave, + const gchar *text, + int len, + int cursor_index, + int anchor_index); + +/* static methods*/ +static void _ibus_context_update_preedit_text_cb( + IBusInputContext *ibuscontext, + IBusText *text, + gint cursor_pos, + gboolean visible, + guint mode, + IBusIMContext *ibusimcontext); +static void _create_input_context(IBusIMContext *context); +static gboolean _set_cursor_location_internal(IBusIMContext *context); + +static void _bus_connected_cb(IBusBus *bus, IBusIMContext *context); +/* callback functions for slave context */ +static void _slave_commit_cb(GtkIMContext *slave, gchar *string, IBusIMContext *context); +static void _slave_preedit_changed_cb(GtkIMContext *slave, IBusIMContext *context); +static void _slave_preedit_start_cb(GtkIMContext *slave, IBusIMContext *context); +static void _slave_preedit_end_cb(GtkIMContext *slave, IBusIMContext *context); +static gboolean _slave_retrieve_surrounding_cb(GtkIMContext *slave, IBusIMContext *context); +static gboolean _slave_delete_surrounding_cb(GtkIMContext *slave, gint offset_from_cursor, guint nchars, IBusIMContext *context); +static void _request_surrounding_text(IBusIMContext *context); +static gboolean _set_content_type(IBusIMContext *context); +static void _commit_text(IBusIMContext *context, const gchar *text); +static void _preedit_start(IBusIMContext *context); +static void _preedit_end(IBusIMContext *context); +static void _preedit_changed(IBusIMContext *context); +static gboolean _retrieve_surrounding(IBusIMContext *context); +static gboolean _delete_surrounding(IBusIMContext *context, gint offset_from_cursor, guint nchars); +static gboolean _timeout_callback(gpointer *user_data); +static void _run_main_loop_with_timeout(IBusIMContext *ibusimcontext); + +static GType _ibus_type_im_context = 0; +static GtkIMContextClass *parent_class = NULL; + +static IBusBus *_bus = NULL; +static guint _daemon_name_watch_id = 0; +static gboolean _daemon_is_running = FALSE; + +void +ibus_im_context_register_type(GTypeModule *type_module) { + IDEBUG("%s", __FUNCTION__); + + static const GTypeInfo ibus_im_context_info = { + sizeof(IBusIMContextClass), + (GBaseInitFunc)NULL, + (GBaseFinalizeFunc)NULL, + (GClassInitFunc)ibus_im_context_class_init, + (GClassFinalizeFunc)ibus_im_context_class_fini, + NULL, /* class data */ + sizeof(IBusIMContext), + 0, + (GInstanceInitFunc)ibus_im_context_init, + }; + + if (!_ibus_type_im_context) { + if (type_module) { + _ibus_type_im_context = + g_type_module_register_type(type_module, GTK_TYPE_IM_CONTEXT, "IBusIMContext", &ibus_im_context_info, (GTypeFlags)0); + } else { + _ibus_type_im_context = g_type_register_static(GTK_TYPE_IM_CONTEXT, "IBusIMContext", &ibus_im_context_info, (GTypeFlags)0); + } + } +} + +GType +ibus_im_context_get_type(void) { + IDEBUG("%s", __FUNCTION__); + + if (_ibus_type_im_context == 0) { + ibus_im_context_register_type(NULL); + } + + g_assert(_ibus_type_im_context != 0); + return _ibus_type_im_context; +} + +IBusIMContext * +ibus_im_context_new(void) { + IDEBUG("%s", __FUNCTION__); + + GObject *obj = g_object_new(IBUS_TYPE_IM_CONTEXT, NULL); + return IBUS_IM_CONTEXT(obj); +} + +static gboolean +_focus_in_cb(GtkWidget *widget, GdkEventFocus *event, gpointer user_data) { + if (_focus_im_context == NULL && _fake_context != NULL) { + ibus_input_context_focus_in(_fake_context); + } + return FALSE; +} + +static gboolean +_focus_out_cb(GtkWidget *widget, GdkEventFocus *event, gpointer user_data) { + if (_focus_im_context == NULL && _fake_context != NULL) { + ibus_input_context_focus_out(_fake_context); + } + return FALSE; +} + +static gboolean +ibus_im_context_commit_event(IBusIMContext *ibusimcontext, GdkEventKey *event) { + IDEBUG("%s", __FUNCTION__); + guint keyval = 0; + GdkModifierType state = 0; + int i; + GdkModifierType no_text_input_mask; + gunichar ch; + + if (event->type == GDK_KEY_RELEASE) + return FALSE; + keyval = event->keyval; + state = event->state; + + /* Ignore modifier key presses */ + for (i = 0; i < G_N_ELEMENTS(IBUS_COMPOSE_IGNORE_KEYLIST); i++) + if (keyval == IBUS_COMPOSE_IGNORE_KEYLIST[i]) + return FALSE; + no_text_input_mask = + gdk_keymap_get_modifier_mask(gdk_keymap_get_for_display(gdk_display_get_default()), GDK_MODIFIER_INTENT_NO_TEXT_INPUT); + if (state & no_text_input_mask || keyval == GDK_KEY_Return || keyval == GDK_KEY_ISO_Enter || keyval == GDK_KEY_KP_Enter) { + return FALSE; + } + ch = ibus_keyval_to_unicode(keyval); + if (ch != 0 && !g_unichar_iscntrl(ch)) { + IBusText *text = ibus_text_new_from_unichar(ch); + IDEBUG("%s: text=%p", __FUNCTION__, text); + _request_surrounding_text(ibusimcontext); + _commit_text(ibusimcontext, text->text); + return TRUE; + } + return FALSE; +} + +struct _ProcessKeyEventData { + GdkEvent *event; + IBusIMContext *ibusimcontext; +}; + +typedef struct _ProcessKeyEventData ProcessKeyEventData; + +static void +_process_key_event_done(GObject *object, GAsyncResult *res, gpointer user_data) { + IBusInputContext *context = (IBusInputContext *)object; + + ProcessKeyEventData *data = (ProcessKeyEventData *)user_data; + GdkEvent *event = data->event; + GError *error = NULL; + + g_slice_free(ProcessKeyEventData, data); + gboolean retval = ibus_input_context_process_key_event_async_finish(context, res, &error); + + if (error != NULL) { + g_warning("Process Key Event failed: %s.", error->message); + g_error_free(error); + } + + if (retval == FALSE) { + ((GdkEventKey *)event)->state |= IBUS_IGNORED_MASK; + gdk_event_put(event); + } + gdk_event_free(event); +} + +static gboolean +_process_key_event(IBusInputContext *context, GdkEventKey *event, IBusIMContext *ibusimcontext) { + guint state; + guint keyval = 0; + guint16 hardware_keycode = 0; + guint keycode = 0; + gboolean retval = FALSE; + + state = event->state; + if (event->type == GDK_KEY_RELEASE) + state |= IBUS_RELEASE_MASK; + keyval = event->keyval; + hardware_keycode = event->hardware_keycode; + keycode = hardware_keycode; + + ProcessKeyEventData *data = g_slice_new0(ProcessKeyEventData); + data->event = gdk_event_copy((GdkEvent *)event); + data->ibusimcontext = ibusimcontext; + ibus_input_context_process_key_event_async(context, keyval, keycode, state, -1, NULL, _process_key_event_done, data); + + retval = TRUE; + + if (retval) + event->state |= IBUS_HANDLED_MASK; + else + event->state |= IBUS_IGNORED_MASK; + + return retval; +} + +/* emit "retrieve-surrounding" glib signal of GtkIMContext, if + * context->caps has IBUS_CAP_SURROUNDING_TEXT and the current IBus + * engine needs surrounding-text. + */ +static void +_request_surrounding_text(IBusIMContext *context) { + if (context && (context->caps & IBUS_CAP_SURROUNDING_TEXT) != 0 && context->ibuscontext != NULL && + ibus_input_context_needs_surrounding_text(context->ibuscontext)) { + _retrieve_surrounding(context); + } else { + // g_debug("%s has no capability of surrounding-text feature", g_get_prgname()); + } +} + +static gboolean +_set_content_type(IBusIMContext *context) { + if (context->ibuscontext != NULL) { + GtkInputPurpose purpose; + GtkInputHints hints; + + g_object_get(G_OBJECT(context), "input-purpose", &purpose, "input-hints", &hints, NULL); + + ibus_input_context_set_content_type(context->ibuscontext, purpose, hints); + } + return TRUE; +} + +static gboolean +_get_boolean_env(const gchar *name, gboolean defval) { + const gchar *value = g_getenv(name); + + if (value == NULL) + return defval; + + if (g_strcmp0(value, "") == 0 || g_strcmp0(value, "0") == 0 || g_strcmp0(value, "false") == 0 || + g_strcmp0(value, "False") == 0 || g_strcmp0(value, "FALSE") == 0) + return FALSE; + + return TRUE; +} + +static void +daemon_name_appeared(GDBusConnection *connection, const gchar *name, const gchar *owner, gpointer data) { + if (!g_strcmp0(ibus_bus_get_service_name(_bus), IBUS_SERVICE_PORTAL)) { + _daemon_is_running = TRUE; + return; + } + /* If ibus-daemon is running and run ssh -X localhost, + * daemon_name_appeared() is called but ibus_get_address() == NULL + * because the hostname and display number are different between + * ibus-daemon and clients. So IBusBus would not be connected and + * ibusimcontext->ibuscontext == NULL and ibusimcontext->events_queue + * could go beyond MAX_QUEUED_EVENTS . */ + _daemon_is_running = (ibus_get_address() != NULL); +} + +static void +daemon_name_vanished(GDBusConnection *connection, const gchar *name, gpointer data) { + _daemon_is_running = FALSE; +} + +static void +ibus_im_context_class_init(IBusIMContextClass *class) { + IDEBUG("%s", __FUNCTION__); + + GtkIMContextClass *im_context_class = GTK_IM_CONTEXT_CLASS(class); + GObjectClass *gobject_class = G_OBJECT_CLASS(class); + + parent_class = (GtkIMContextClass *)g_type_class_peek_parent(class); + + im_context_class->reset = ibus_im_context_reset; + im_context_class->focus_in = ibus_im_context_focus_in; + im_context_class->focus_out = ibus_im_context_focus_out; + im_context_class->filter_keypress = ibus_im_context_filter_keypress; + im_context_class->get_preedit_string = ibus_im_context_get_preedit_string; + im_context_class->set_client_window = ibus_im_context_set_client_window; + im_context_class->set_cursor_location = ibus_im_context_set_cursor_location; + im_context_class->set_use_preedit = ibus_im_context_set_use_preedit; + im_context_class->set_surrounding = ibus_im_context_set_surrounding; + gobject_class->notify = ibus_im_context_notify; + gobject_class->finalize = ibus_im_context_finalize; + + /* init bus object */ + if (_bus == NULL) { + _bus = ibus_bus_new_async_client(); + + g_signal_connect(_bus, "connected", G_CALLBACK(_bus_connected_cb), NULL); + } + + _daemon_name_watch_id = g_bus_watch_name( + G_BUS_TYPE_SESSION, ibus_bus_get_service_name(_bus), G_BUS_NAME_WATCHER_FLAGS_NONE, daemon_name_appeared, + daemon_name_vanished, NULL, NULL); +} + +static void +ibus_im_context_class_fini(IBusIMContextClass *class) { + g_bus_unwatch_name(_daemon_name_watch_id); +} + +/* Copied from gtk+2.0-2.20.1/modules/input/imcedilla.c to fix crosbug.com/11421. + * Overwrite the original Gtk+'s compose table in gtk+-2.x.y/gtk/gtkimcontextsimple.c. */ + +/* The difference between this and the default input method is the handling + * of C+acute - this method produces C WITH CEDILLA rather than C WITH ACUTE. + * For languages that use CCedilla and not acute, this is the preferred mapping, + * and is particularly important for pt_BR, where the us-intl keyboard is + * used extensively. + */ +static guint16 cedilla_compose_seqs[] = { + GDK_KEY_dead_acute, GDK_KEY_C, 0, 0, 0, 0x00C7, /* LATIN_CAPITAL_LETTER_C_WITH_CEDILLA */ + GDK_KEY_dead_acute, GDK_KEY_c, 0, 0, 0, 0x00E7, /* LATIN_SMALL_LETTER_C_WITH_CEDILLA */ + GDK_KEY_Multi_key, GDK_KEY_apostrophe, GDK_KEY_C, 0, 0, 0x00C7, /* LATIN_CAPITAL_LETTER_C_WITH_CEDILLA */ + GDK_KEY_Multi_key, GDK_KEY_apostrophe, GDK_KEY_c, 0, 0, 0x00E7, /* LATIN_SMALL_LETTER_C_WITH_CEDILLA */ + GDK_KEY_Multi_key, GDK_KEY_C, GDK_KEY_apostrophe, 0, 0, 0x00C7, /* LATIN_CAPITAL_LETTER_C_WITH_CEDILLA */ + GDK_KEY_Multi_key, GDK_KEY_c, GDK_KEY_apostrophe, 0, 0, 0x00E7, /* LATIN_SMALL_LETTER_C_WITH_CEDILLA */ +}; + +static void +ibus_im_context_init(GObject *obj) { + IDEBUG("%s", __FUNCTION__); + + IBusIMContext *ibusimcontext = IBUS_IM_CONTEXT(obj); + + ibusimcontext->client_window = NULL; + + // Init preedit status + ibusimcontext->preedit_string = NULL; + ibusimcontext->preedit_attrs = NULL; + ibusimcontext->preedit_cursor_pos = 0; + ibusimcontext->preedit_visible = FALSE; + ibusimcontext->preedit_mode = IBUS_ENGINE_PREEDIT_CLEAR; + + // Init cursor area + ibusimcontext->cursor_area.x = -1; + ibusimcontext->cursor_area.y = -1; + ibusimcontext->cursor_area.width = 0; + ibusimcontext->cursor_area.height = 0; + + ibusimcontext->ibuscontext = NULL; + ibusimcontext->has_focus = FALSE; + ibusimcontext->time = GDK_CURRENT_TIME; + ibusimcontext->caps = IBUS_CAP_FOCUS | IBUS_CAP_SURROUNDING_TEXT | IBUS_CAP_AUXILIARY_TEXT | IBUS_CAP_PROPERTY; + + ibusimcontext->events_queue = g_queue_new(); + + // Create slave im context + ibusimcontext->slave = gtk_im_context_simple_new(); + gtk_im_context_simple_add_table( + GTK_IM_CONTEXT_SIMPLE(ibusimcontext->slave), cedilla_compose_seqs, 4, G_N_ELEMENTS(cedilla_compose_seqs) / (4 + 2)); + + // g_signal_connect(ibusimcontext->slave, + // "commit", + // G_CALLBACK (_slave_commit_cb), + // ibusimcontext); + // g_signal_connect (ibusimcontext->slave, + // "preedit-start", + // G_CALLBACK (_slave_preedit_start_cb), + // ibusimcontext); + // g_signal_connect (ibusimcontext->slave, + // "preedit-end", + // G_CALLBACK (_slave_preedit_end_cb), + // ibusimcontext); + // g_signal_connect (ibusimcontext->slave, + // "preedit-changed", + // G_CALLBACK (_slave_preedit_changed_cb), + // ibusimcontext); + // g_signal_connect (ibusimcontext->slave, + // "retrieve-surrounding", + // G_CALLBACK (_slave_retrieve_surrounding_cb), + // ibusimcontext); + // g_signal_connect (ibusimcontext->slave, + // "delete-surrounding", + // G_CALLBACK (_slave_delete_surrounding_cb), + // ibusimcontext); + // G_CALLBACK(_slave_commit_cb), + // ibusimcontext); + // g_signal_connect(ibusimcontext->slave, + // "preedit-start", + // G_CALLBACK(_slave_preedit_start_cb), + // ibusimcontext); + // g_signal_connect(ibusimcontext->slave, + // "preedit-end", + // G_CALLBACK(_slave_preedit_end_cb), + // ibusimcontext); + // g_signal_connect(ibusimcontext->slave, + // "preedit-changed", + // G_CALLBACK(_slave_preedit_changed_cb), + // ibusimcontext); + // g_signal_connect(ibusimcontext->slave, + // "retrieve-surrounding", + // G_CALLBACK(_slave_retrieve_surrounding_cb), + // ibusimcontext); + // g_signal_connect(ibusimcontext->slave, + // "delete-surrounding", + // G_CALLBACK(_slave_delete_surrounding_cb), + // ibusimcontext); + + if (ibus_bus_is_connected(_bus)) { + _create_input_context(ibusimcontext); + } + + g_signal_connect(_bus, "connected", G_CALLBACK(_bus_connected_cb), obj); + + _daemon_is_running = (ibus_get_address() != NULL); +} + +static void +ibus_im_context_notify(GObject *obj, GParamSpec *pspec) { + IDEBUG("%s", __FUNCTION__); + + if (g_strcmp0(pspec->name, "input-purpose") == 0 || g_strcmp0(pspec->name, "input-hints") == 0) { + _set_content_type(IBUS_IM_CONTEXT(obj)); + } +} + +static void +ibus_im_context_finalize(GObject *obj) { + IDEBUG("%s", __FUNCTION__); + + IBusIMContext *ibusimcontext = IBUS_IM_CONTEXT(obj); + + g_signal_handlers_disconnect_by_func(_bus, G_CALLBACK(_bus_connected_cb), obj); + + if (ibusimcontext->cancellable != NULL) { + /* Cancel any ongoing create input context request */ + g_cancellable_cancel(ibusimcontext->cancellable); + g_object_unref(ibusimcontext->cancellable); + ibusimcontext->cancellable = NULL; + } + + if (ibusimcontext->ibuscontext) { + ibus_proxy_destroy((IBusProxy *)ibusimcontext->ibuscontext); + } + + ibus_im_context_set_client_window((GtkIMContext *)ibusimcontext, NULL); + + if (ibusimcontext->slave) { + g_object_unref(ibusimcontext->slave); + ibusimcontext->slave = NULL; + } + + // release preedit + if (ibusimcontext->preedit_string) { + g_free(ibusimcontext->preedit_string); + } + if (ibusimcontext->preedit_attrs) { + pango_attr_list_unref(ibusimcontext->preedit_attrs); + } + + g_queue_free_full(ibusimcontext->events_queue, (GDestroyNotify)gdk_event_free); + + G_OBJECT_CLASS(parent_class)->finalize(obj); +} + +static void +ibus_im_context_clear_preedit_text(IBusIMContext *ibusimcontext) { + gchar *preedit_string = NULL; + g_assert(ibusimcontext->ibuscontext); + if (ibusimcontext->preedit_visible && ibusimcontext->preedit_mode == IBUS_ENGINE_PREEDIT_COMMIT) { + preedit_string = g_strdup(ibusimcontext->preedit_string); + } + + /* Clear the preedit_string but keep the preedit_cursor_pos and + * preedit_visible because a time lag could happen, firefox commit + * the preedit text before the preedit text is cleared and it cause + * a double commits of the Hangul preedit in firefox if the preedit + * would be located on the URL bar and click on anywhere of firefox + * out of the URL bar. + */ + _ibus_context_update_preedit_text_cb( + ibusimcontext->ibuscontext, ibus_text_new_from_string(""), ibusimcontext->preedit_cursor_pos, + ibusimcontext->preedit_visible, IBUS_ENGINE_PREEDIT_CLEAR, ibusimcontext); + if (preedit_string) { + _commit_text(ibusimcontext, preedit_string); + g_free(preedit_string); + _request_surrounding_text(ibusimcontext); + } +} + +static gboolean +ibus_im_context_filter_keypress(GtkIMContext *context, GdkEventKey *event) { + IDEBUG("%s", __FUNCTION__); + + IBusIMContext *ibusimcontext = IBUS_IM_CONTEXT(context); + + if (!_daemon_is_running) + return gtk_im_context_filter_keypress(ibusimcontext->slave, event); + + if (event->state & IBUS_HANDLED_MASK) + return TRUE; + + /* Do not call gtk_im_context_filter_keypress() because + * gtk_im_context_simple_filter_keypress() binds Ctrl-Shift-u + */ + if (event->state & IBUS_IGNORED_MASK) + return ibus_im_context_commit_event(ibusimcontext, event); + + /* XXX it is a workaround for some applications do not set client + * window. */ + if (ibusimcontext->client_window == NULL && event->window != NULL) + gtk_im_context_set_client_window((GtkIMContext *)ibusimcontext, event->window); + + _request_surrounding_text(ibusimcontext); + + ibusimcontext->time = event->time; + + if (ibusimcontext->ibuscontext) { + gboolean result = _process_key_event(ibusimcontext->ibuscontext, event, ibusimcontext); + if (result) { + _run_main_loop_with_timeout(ibusimcontext); + } + return result; + } + + /* At this point we _should_ be waiting for the IBus context to be + * created or the connection to IBus to be established. If that's + * the case we queue events to be processed when the IBus context + * is ready. */ + g_return_val_if_fail(ibusimcontext->cancellable != NULL || ibus_bus_is_connected(_bus) == FALSE, FALSE); + g_queue_push_tail(ibusimcontext->events_queue, gdk_event_copy((GdkEvent *)event)); + + if (g_queue_get_length(ibusimcontext->events_queue) > MAX_QUEUED_EVENTS) { + g_warning("Events queue growing too big, will start to drop."); + gdk_event_free((GdkEvent *)g_queue_pop_head(ibusimcontext->events_queue)); + } + + return TRUE; +} + +static void +ibus_im_context_focus_in(GtkIMContext *context) { + IBusIMContext *ibusimcontext = (IBusIMContext *)context; + GtkWidget *widget = NULL; + + IDEBUG("%s: context=%p", __FUNCTION__, context); + + if (ibusimcontext->has_focus) + return; + + /* don't set focus on password entry */ + if (ibusimcontext->client_window != NULL) { + gdk_window_get_user_data(ibusimcontext->client_window, (gpointer *)&widget); + } + + if (widget && GTK_IS_ENTRY(widget) && !gtk_entry_get_visibility(GTK_ENTRY(widget))) { + return; + } + /* Do not call gtk_im_context_focus_out() here. + * google-chrome's notification popup window (Pushbullet) + * takes the focus and the popup window disappears. + * So other applications lose the focus because + * ibusimcontext->has_focus is FALSE if + * gtk_im_context_focus_out() is called here when + * _focus_im_context != context. + */ + if (_focus_im_context == NULL) { + /* focus out fake context */ + if (_fake_context != NULL) { + ibus_input_context_focus_out(_fake_context); + } + } + + ibusimcontext->has_focus = TRUE; + if (ibusimcontext->ibuscontext) { + if (!_set_content_type(ibusimcontext)) { + ibusimcontext->has_focus = FALSE; + return; + } + ibus_input_context_focus_in(ibusimcontext->ibuscontext); + } + + gtk_im_context_focus_in(ibusimcontext->slave); + + /* set_cursor_location_internal() will get origin from X server, + * it blocks UI. So delay it to idle callback. */ + g_idle_add_full( + G_PRIORITY_DEFAULT_IDLE, (GSourceFunc)_set_cursor_location_internal, g_object_ref(ibusimcontext), + (GDestroyNotify)g_object_unref); + + /* retrieve the initial surrounding-text (regardless of whether + * the current IBus engine needs surrounding-text) */ + _request_surrounding_text(ibusimcontext); + + g_object_add_weak_pointer((GObject *)context, (gpointer *)&_focus_im_context); + _focus_im_context = context; +} + +static void +ibus_im_context_focus_out(GtkIMContext *context) { + IDEBUG("%s", __FUNCTION__); + IBusIMContext *ibusimcontext = (IBusIMContext *)context; + + if (ibusimcontext->has_focus == FALSE) { + return; + } + + if (_focus_im_context) { + g_object_remove_weak_pointer((GObject *)context, (gpointer *)&_focus_im_context); + _focus_im_context = NULL; + } + + ibusimcontext->has_focus = FALSE; + if (ibusimcontext->ibuscontext) { + ibus_im_context_clear_preedit_text(ibusimcontext); + ibus_input_context_focus_out(ibusimcontext->ibuscontext); + } + + gtk_im_context_focus_out(ibusimcontext->slave); + + /* focus in the fake ic */ + if (_fake_context != NULL) { + ibus_input_context_focus_in(_fake_context); + } +} + +static void +ibus_im_context_reset(GtkIMContext *context) { + IDEBUG("%s", __FUNCTION__); + + IBusIMContext *ibusimcontext = IBUS_IM_CONTEXT(context); + + if (ibusimcontext->ibuscontext) { + ibus_input_context_reset(ibusimcontext->ibuscontext); + } + gtk_im_context_reset(ibusimcontext->slave); +} + +static void +ibus_im_context_get_preedit_string(GtkIMContext *context, gchar **str, PangoAttrList **attrs, gint *cursor_pos) { + IDEBUG("%s", __FUNCTION__); + + IBusIMContext *ibusimcontext = IBUS_IM_CONTEXT(context); + + if (ibusimcontext->preedit_visible) { + if (str) { + *str = g_strdup(ibusimcontext->preedit_string ? ibusimcontext->preedit_string : ""); + } + + if (attrs) { + *attrs = ibusimcontext->preedit_attrs ? pango_attr_list_ref(ibusimcontext->preedit_attrs) : pango_attr_list_new(); + } + + if (cursor_pos) { + *cursor_pos = ibusimcontext->preedit_cursor_pos; + } + } else { + if (str) { + *str = g_strdup(""); + } + if (attrs) { + *attrs = pango_attr_list_new(); + } + if (cursor_pos) { + *cursor_pos = 0; + } + } + IDEBUG("str=%s", *str); +} + +/* Use the button-press-event signal until GtkIMContext always emits the reset + * signal. + * https://gitlab.gnome.org/GNOME/gtk/merge_requests/460 + */ +static gboolean +ibus_im_context_button_press_event_cb(GtkWidget *widget, GdkEventButton *event, IBusIMContext *ibusimcontext) { + IDEBUG("%s", __FUNCTION__); + if (event->button != 1) + return FALSE; + + if (ibusimcontext->ibuscontext) { + ibus_im_context_clear_preedit_text(ibusimcontext); + ibus_input_context_reset(ibusimcontext->ibuscontext); + } + return FALSE; +} + +static void +_connect_button_press_event(IBusIMContext *ibusimcontext, gboolean do_connect) { + GtkWidget *widget = NULL; + + g_assert(ibusimcontext->client_window); + gdk_window_get_user_data(ibusimcontext->client_window, (gpointer *)&widget); + /* firefox needs GtkWidget instead of GtkWindow */ + if (GTK_IS_WIDGET(widget)) { + if (do_connect) { + g_signal_connect(widget, "button-press-event", G_CALLBACK(ibus_im_context_button_press_event_cb), ibusimcontext); + ibusimcontext->use_button_press_event = TRUE; + } else { + g_signal_handlers_disconnect_by_func(widget, G_CALLBACK(ibus_im_context_button_press_event_cb), ibusimcontext); + ibusimcontext->use_button_press_event = FALSE; + } + } +} + +static void +ibus_im_context_set_client_window(GtkIMContext *context, GdkWindow *client) { + IBusIMContext *ibusimcontext; + + IDEBUG("%s", __FUNCTION__); + + ibusimcontext = IBUS_IM_CONTEXT(context); + + if (ibusimcontext->client_window) { + if (ibusimcontext->use_button_press_event) + _connect_button_press_event(ibusimcontext, FALSE); + g_object_unref(ibusimcontext->client_window); + ibusimcontext->client_window = NULL; + } + + if (client != NULL) { + ibusimcontext->client_window = g_object_ref(client); + if (!ibusimcontext->use_button_press_event) + _connect_button_press_event(ibusimcontext, TRUE); + } + if (ibusimcontext->slave) + gtk_im_context_set_client_window(ibusimcontext->slave, client); +} + +static void +_set_rect_scale_factor_with_window(GdkRectangle *area, GdkWindow *window) { + int scale_factor; + + g_assert(area); + g_assert(GDK_IS_WINDOW(window)); + + scale_factor = gdk_window_get_scale_factor(window); + area->x *= scale_factor; + area->y *= scale_factor; + area->width *= scale_factor; + area->height *= scale_factor; +} + +static gboolean +_set_cursor_location_internal(IBusIMContext *ibusimcontext) { + GdkRectangle area; + + if (ibusimcontext->client_window == NULL || ibusimcontext->ibuscontext == NULL) { + return FALSE; + } + + area = ibusimcontext->cursor_area; + + if (GDK_IS_WAYLAND_DISPLAY(gdk_display_get_default())) { + gdouble px, py; + GdkWindow *parent; + GdkWindow *window = ibusimcontext->client_window; + + while ((parent = gdk_window_get_effective_parent(window)) != NULL) { + gdk_window_coords_to_parent(window, area.x, area.y, &px, &py); + area.x = px; + area.y = py; + window = parent; + } + + _set_rect_scale_factor_with_window(&area, ibusimcontext->client_window); + ibus_input_context_set_cursor_location_relative(ibusimcontext->ibuscontext, area.x, area.y, area.width, area.height); + return FALSE; + } + + if (area.x == -1 && area.y == -1 && area.width == 0 && area.height == 0) { + area.x = 0; + area.y += gdk_window_get_height(ibusimcontext->client_window); + } + gdk_window_get_root_coords(ibusimcontext->client_window, area.x, area.y, &area.x, &area.y); + _set_rect_scale_factor_with_window(&area, ibusimcontext->client_window); + ibus_input_context_set_cursor_location(ibusimcontext->ibuscontext, area.x, area.y, area.width, area.height); + return FALSE; +} + +static void +ibus_im_context_set_cursor_location(GtkIMContext *context, GdkRectangle *area) { + IDEBUG("%s", __FUNCTION__); + + IBusIMContext *ibusimcontext = IBUS_IM_CONTEXT(context); + + /* The area is the relative coordinates and this has to get the absolute + * ones in _set_cursor_location_internal() since GTK 4.0. + */ + if (ibusimcontext->cursor_area.x == area->x && ibusimcontext->cursor_area.y == area->y && + ibusimcontext->cursor_area.width == area->width && ibusimcontext->cursor_area.height == area->height) { + return; + } + ibusimcontext->cursor_area = *area; + _set_cursor_location_internal(ibusimcontext); + gtk_im_context_set_cursor_location(ibusimcontext->slave, area); +} + +static void +ibus_im_context_set_use_preedit(GtkIMContext *context, gboolean use_preedit) { + IDEBUG("%s", __FUNCTION__); + + IBusIMContext *ibusimcontext = IBUS_IM_CONTEXT(context); + + if (use_preedit) { + ibusimcontext->caps |= IBUS_CAP_PREEDIT_TEXT; + } else { + ibusimcontext->caps &= ~IBUS_CAP_PREEDIT_TEXT; + } + if (ibusimcontext->ibuscontext) { + ibus_input_context_set_capabilities(ibusimcontext->ibuscontext, ibusimcontext->caps); + } + gtk_im_context_set_use_preedit(ibusimcontext->slave, use_preedit); +} + +static guint +get_selection_anchor_point(IBusIMContext *ibusimcontext, guint cursor_pos, guint surrounding_text_len) { + if (cursor_pos <= 0) { + return ibusimcontext->text->len; + } + return cursor_pos; +} + +static void +ibus_im_context_set_surrounding(GtkIMContext *context, const gchar *text, int len, int cursor_index) { + ibus_im_context_set_surrounding_with_selection(context, text, len, cursor_index, cursor_index); +} + +static void +ibus_im_context_set_surrounding_with_selection( + GtkIMContext *context, + const gchar *text, + int len, + int cursor_index, + int anchor_index) { + g_return_if_fail(context != NULL); + g_return_if_fail(IBUS_IS_IM_CONTEXT(context)); + g_return_if_fail(text != NULL); + g_return_if_fail(strlen(text) >= len); + g_return_if_fail(0 <= cursor_index && cursor_index <= len); + + IBusIMContext *ibusimcontext = IBUS_IM_CONTEXT(context); + + if (ibusimcontext->ibuscontext) { + IBusText *ibustext; + guint cursor_pos; + guint utf8_len; + gchar *p; + + p = g_strndup(text, len); + cursor_pos = g_utf8_strlen(p, cursor_index); + utf8_len = g_utf8_strlen(p, len); + ibustext = ibus_text_new_from_string(p); + // g_object_ref_sink(ibustext); + IDEBUG("%s: ibustext=%p", __FUNCTION__, ibustext); + g_free(p); + + gint anchor_pos = get_selection_anchor_point(ibusimcontext, cursor_pos, utf8_len); + ibus_input_context_set_surrounding_text(ibusimcontext->ibuscontext, ibustext, cursor_pos, anchor_pos); + // g_object_unref(ibustext); + } + gtk_im_context_set_surrounding(ibusimcontext->slave, text, len, cursor_index); +} + +static void +_bus_connected_cb(IBusBus *bus, IBusIMContext *ibusimcontext) { + IDEBUG("%s", __FUNCTION__); + if (ibusimcontext) + _create_input_context(ibusimcontext); +} + +static void +_ibus_context_commit_text_cb(IBusInputContext *ibuscontext, IBusText *text, IBusIMContext *ibusimcontext) { + IDEBUG("%s: text=%p", __FUNCTION__, text); + // g_object_ref_sink(text); + + _request_surrounding_text(ibusimcontext); + _commit_text(ibusimcontext, ibus_text_get_text(text)); + g_main_loop_quit(ibusimcontext->thread_loop); + // g_object_unref(text); +} + +static void +_commit_text(IBusIMContext *ibusimcontext, const gchar *text) { + // g_signal_emit(ibusimcontext, _signal_commit_id, 0, text->text); + ibus_im_test_set_text(ibusimcontext, text); +} + +static void +_preedit_start(IBusIMContext *ibusimcontext) { + // g_signal_emit(ibusimcontext, _signal_preedit_start_id, 0); + g_warning("%s not implemented", __FUNCTION__); +} + +static void +_preedit_end(IBusIMContext *ibusimcontext) { + // g_signal_emit(ibusimcontext, _signal_preedit_end_id, 0); + // g_warning("%s not implemented", __FUNCTION__); +} + +static void +_preedit_changed(IBusIMContext *ibusimcontext) { + // g_signal_emit(ibusimcontext, _signal_preedit_changed_id, 0); + // g_warning("%s not implemented", __FUNCTION__); +} + +static gboolean +_retrieve_surrounding(IBusIMContext *ibusimcontext) { + // No-op in our simulation since we store the text + return TRUE; +} + +static gboolean +_delete_surrounding(IBusIMContext *ibusimcontext, gint offset_from_cursor, guint nchars) { + // g_signal_emit(ibusimcontext, _signal_delete_surrounding_id, 0); + if (offset_from_cursor < 0) { + g_string_erase(ibusimcontext->text, ibusimcontext->text->len - nchars, nchars); + } else { + g_string_erase(ibusimcontext->text, offset_from_cursor, nchars); + } + return TRUE; +} + +static gboolean +_key_is_modifier(guint keyval) { + /* See gdkkeys-x11.c:_gdk_keymap_key_is_modifier() for how this + * really should be implemented */ + + switch (keyval) { + case GDK_KEY_Shift_L: + case GDK_KEY_Shift_R: + case GDK_KEY_Control_L: + case GDK_KEY_Control_R: + case GDK_KEY_Caps_Lock: + case GDK_KEY_Shift_Lock: + case GDK_KEY_Meta_L: + case GDK_KEY_Meta_R: + case GDK_KEY_Alt_L: + case GDK_KEY_Alt_R: + case GDK_KEY_Super_L: + case GDK_KEY_Super_R: + case GDK_KEY_Hyper_L: + case GDK_KEY_Hyper_R: + case GDK_KEY_ISO_Lock: + case GDK_KEY_ISO_Level2_Latch: + case GDK_KEY_ISO_Level3_Shift: + case GDK_KEY_ISO_Level3_Latch: + case GDK_KEY_ISO_Level3_Lock: + case GDK_KEY_ISO_Level5_Shift: + case GDK_KEY_ISO_Level5_Latch: + case GDK_KEY_ISO_Level5_Lock: + case GDK_KEY_ISO_Group_Shift: + case GDK_KEY_ISO_Group_Latch: + case GDK_KEY_ISO_Group_Lock: + return TRUE; + default: + return FALSE; + } +} + +/* Copy from gdk */ +static GdkEventKey * +_create_gdk_event(IBusIMContext *ibusimcontext, guint keyval, guint keycode, guint state) { + gunichar c = 0; + gchar buf[8]; + + GdkEventKey *event = (GdkEventKey *)gdk_event_new((state & IBUS_RELEASE_MASK) ? GDK_KEY_RELEASE : GDK_KEY_PRESS); + + if (ibusimcontext && ibusimcontext->client_window) + event->window = g_object_ref(ibusimcontext->client_window); + else if (_input_window) + event->window = g_object_ref(_input_window); + + /* The time is copied the latest value from the previous + * GdkKeyEvent in filter_keypress(). + * + * We understand the best way would be to pass the all time value + * to IBus functions process_key_event() and IBus DBus functions + * ProcessKeyEvent() in IM clients and IM engines so that the + * _create_gdk_event() could get the correct time values. + * However it would causes to change many functions and the time value + * would not provide the useful meanings for each IBus engines but just + * pass the original value to ForwardKeyEvent(). + * We use the saved value at the moment. + * + * Another idea might be to have the time implementation in X servers + * but some Xorg uses clock_gettime() and others use gettimeofday() + * and the values would be different in each implementation and + * locale/remote X server. So probably that idea would not work. */ + if (ibusimcontext) { + event->time = ibusimcontext->time; + } else { + event->time = GDK_CURRENT_TIME; + } + + event->send_event = FALSE; + event->state = state; + event->keyval = keyval; + event->string = NULL; + event->length = 0; + event->hardware_keycode = (keycode != 0) ? keycode + 8 : 0; + event->group = 0; + event->is_modifier = _key_is_modifier(keyval); + + if (keyval != GDK_KEY_VoidSymbol) + c = gdk_keyval_to_unicode(keyval); + + if (c) { + gsize bytes_written; + gint len; + + /* Apply the control key - Taken from Xlib + */ + if (event->state & GDK_CONTROL_MASK) { + if ((c >= '@' && c < '\177') || c == ' ') + c &= 0x1F; + else if (c == '2') { +#if GLIB_CHECK_VERSION(2, 68, 0) + event->string = g_memdup2("\0\0", 2); +#else + event->string = g_memdup("\0\0", 2); +#endif + event->length = 1; + buf[0] = '\0'; + goto out; + } else if (c >= '3' && c <= '7') + c -= ('3' - '\033'); + else if (c == '8') + c = '\177'; + else if (c == '/') + c = '_' & 0x1F; + } + + len = g_unichar_to_utf8(c, buf); + buf[len] = '\0'; + + event->string = g_locale_from_utf8(buf, len, NULL, &bytes_written, NULL); + if (event->string) + event->length = bytes_written; + } else if (keyval == GDK_KEY_Escape) { + event->length = 1; + event->string = g_strdup("\033"); + } else if (keyval == GDK_KEY_Return || keyval == GDK_KEY_KP_Enter) { + event->length = 1; + event->string = g_strdup("\r"); + } + + if (!event->string) { + event->length = 0; + event->string = g_strdup(""); + } +out: + return event; +} + +static void +_ibus_context_forward_key_event_cb( + IBusInputContext *ibuscontext, + guint keyval, + guint keycode, + guint state, + IBusIMContext *ibusimcontext) { + IDEBUG("%s", __FUNCTION__); + if (keycode == 0 && ibusimcontext->client_window) { + GdkDisplay *display = gdk_window_get_display(ibusimcontext->client_window); + GdkKeymap *keymap = gdk_keymap_get_for_display(display); + GdkKeymapKey *keys = NULL; + gint n_keys = 0; + if (gdk_keymap_get_entries_for_keyval(keymap, keyval, &keys, &n_keys)) + keycode = keys->keycode; + else + g_warning("Failed to parse keycode from keyval %x", keyval); + } + GdkEventKey *event = _create_gdk_event(ibusimcontext, keyval, keycode, state); + gdk_event_put((GdkEvent *)event); + gdk_event_free((GdkEvent *)event); +} + +static void +_ibus_context_delete_surrounding_text_cb( + IBusInputContext *ibuscontext, + gint offset_from_cursor, + guint nchars, + IBusIMContext *ibusimcontext) { + IDEBUG("%s: offset %d, nchar: %d", __FUNCTION__, offset_from_cursor, nchars); + _delete_surrounding(ibusimcontext, offset_from_cursor, nchars); +} + +static void +_ibus_context_update_preedit_text_cb( + IBusInputContext *ibuscontext, + IBusText *text, + gint cursor_pos, + gboolean visible, + guint mode, + IBusIMContext *ibusimcontext) { + IDEBUG("%s: text=%p", __FUNCTION__, text); + + // g_object_ref_sink(text); + + const gchar *str; + gboolean flag; + + if (ibusimcontext->preedit_string) { + g_free(ibusimcontext->preedit_string); + } + if (ibusimcontext->preedit_attrs) { + pango_attr_list_unref(ibusimcontext->preedit_attrs); + ibusimcontext->preedit_attrs = NULL; + } + + if (!ibusimcontext->use_button_press_event && mode == IBUS_ENGINE_PREEDIT_COMMIT) { + if (ibusimcontext->client_window) { + _connect_button_press_event(ibusimcontext, TRUE); + } + } + + str = text->text; + ibusimcontext->preedit_string = g_strdup(str); + if (text->attrs) { + guint i; + ibusimcontext->preedit_attrs = pango_attr_list_new(); + for (i = 0;; i++) { + IBusAttribute *attr = ibus_attr_list_get(text->attrs, i); + if (attr == NULL) { + break; + } + + PangoAttribute *pango_attr; + switch (attr->type) { + case IBUS_ATTR_TYPE_UNDERLINE: + pango_attr = pango_attr_underline_new(attr->value); + break; + case IBUS_ATTR_TYPE_FOREGROUND: + pango_attr = pango_attr_foreground_new( + ((attr->value & 0xff0000) >> 8) | 0xff, ((attr->value & 0x00ff00)) | 0xff, ((attr->value & 0x0000ff) << 8) | 0xff); + break; + case IBUS_ATTR_TYPE_BACKGROUND: + pango_attr = pango_attr_background_new( + ((attr->value & 0xff0000) >> 8) | 0xff, ((attr->value & 0x00ff00)) | 0xff, ((attr->value & 0x0000ff) << 8) | 0xff); + break; + default: + continue; + } + pango_attr->start_index = g_utf8_offset_to_pointer(str, attr->start_index) - str; + pango_attr->end_index = g_utf8_offset_to_pointer(str, attr->end_index) - str; + pango_attr_list_insert(ibusimcontext->preedit_attrs, pango_attr); + } + } + + ibusimcontext->preedit_cursor_pos = cursor_pos; + + flag = ibusimcontext->preedit_visible != visible; + ibusimcontext->preedit_visible = visible; + ibusimcontext->preedit_mode = mode; + + if (ibusimcontext->preedit_visible) { + if (flag) { + /* invisible => visible */ + _preedit_start(ibusimcontext); + } + _preedit_changed(ibusimcontext); + } else { + if (flag) { + /* visible => invisible */ + _preedit_changed(ibusimcontext); + _preedit_end(ibusimcontext); + } else { + /* still invisible */ + /* do nothing */ + } + } + // g_object_unref(text); +} + +static void +_ibus_context_show_preedit_text_cb(IBusInputContext *ibuscontext, IBusIMContext *ibusimcontext) { + IDEBUG("%s", __FUNCTION__); + + if (ibusimcontext->preedit_visible == TRUE) + return; + + ibusimcontext->preedit_visible = TRUE; + _preedit_start(ibusimcontext); + _preedit_changed(ibusimcontext); + + _request_surrounding_text(ibusimcontext); +} + +static void +_ibus_context_hide_preedit_text_cb(IBusInputContext *ibuscontext, IBusIMContext *ibusimcontext) { + IDEBUG("%s", __FUNCTION__); + + if (ibusimcontext->preedit_visible == FALSE) + return; + + ibusimcontext->preedit_visible = FALSE; + _preedit_changed(ibusimcontext); + _preedit_end(ibusimcontext); +} + +static void +_ibus_context_destroy_cb(IBusInputContext *ibuscontext, IBusIMContext *ibusimcontext) { + IDEBUG("%s", __FUNCTION__); + g_assert(ibusimcontext->ibuscontext == ibuscontext); + + g_object_unref(ibusimcontext->ibuscontext); + ibusimcontext->ibuscontext = NULL; + + /* clear preedit */ + ibusimcontext->preedit_visible = FALSE; + ibusimcontext->preedit_cursor_pos = 0; + g_free(ibusimcontext->preedit_string); + ibusimcontext->preedit_string = NULL; + + _preedit_changed(ibusimcontext); + _preedit_end(ibusimcontext); +} + +static gboolean +_timeout_callback(gpointer *user_data) { + timeout_data *data = (timeout_data *)user_data; + g_main_loop_quit(data->thread_loop); + data->timed_out = TRUE; + return FALSE; +} + +static void +_run_main_loop_with_timeout(IBusIMContext *ibusimcontext) { + timeout_data data; + data.thread_loop = ibusimcontext->thread_loop; + data.timed_out = FALSE; + int timeout_id = g_timeout_add(1000 /*ms*/, (GSourceFunc)_timeout_callback, &data); + g_main_loop_run(ibusimcontext->thread_loop); + if (!data.timed_out) { + g_source_remove(timeout_id); + } +} + +static void +_create_input_context(IBusIMContext *ibusimcontext) { + IDEBUG("%s", __FUNCTION__); + + g_assert(ibusimcontext->ibuscontext == NULL); + + IBusInputContext *context = ibus_bus_create_input_context(_bus, "gtk-im"); + if (context == NULL) { + g_warning("Create input context failed."); + } else { + ibus_input_context_set_client_commit_preedit(context, TRUE); + ibusimcontext->ibuscontext = context; + + g_signal_connect(ibusimcontext->ibuscontext, "commit-text", G_CALLBACK(_ibus_context_commit_text_cb), ibusimcontext); + g_signal_connect( + ibusimcontext->ibuscontext, "forward-key-event", G_CALLBACK(_ibus_context_forward_key_event_cb), ibusimcontext); + g_signal_connect( + ibusimcontext->ibuscontext, "delete-surrounding-text", G_CALLBACK(_ibus_context_delete_surrounding_text_cb), + ibusimcontext); + g_signal_connect( + ibusimcontext->ibuscontext, "update-preedit-text-with-mode", G_CALLBACK(_ibus_context_update_preedit_text_cb), + ibusimcontext); + g_signal_connect( + ibusimcontext->ibuscontext, "show-preedit-text", G_CALLBACK(_ibus_context_show_preedit_text_cb), ibusimcontext); + g_signal_connect( + ibusimcontext->ibuscontext, "hide-preedit-text", G_CALLBACK(_ibus_context_hide_preedit_text_cb), ibusimcontext); + g_signal_connect(ibusimcontext->ibuscontext, "destroy", G_CALLBACK(_ibus_context_destroy_cb), ibusimcontext); + + ibus_input_context_set_capabilities(ibusimcontext->ibuscontext, ibusimcontext->caps); + + if (ibusimcontext->has_focus) { + /* The time order is _create_input_context() -> + * ibus_im_context_notify() -> ibus_im_context_focus_in() -> + * _create_input_context_done() + * so _set_content_type() is called at the beginning here + * because ibusimcontext->ibuscontext == NULL before. */ + _set_content_type(ibusimcontext); + + ibus_input_context_focus_in(ibusimcontext->ibuscontext); + _set_cursor_location_internal(ibusimcontext); + } + + if (!g_queue_is_empty(ibusimcontext->events_queue)) { + GdkEventKey *event; + while ((event = g_queue_pop_head(ibusimcontext->events_queue))) { + _process_key_event(context, event, ibusimcontext); + gboolean result = _process_key_event(context, event, ibusimcontext); + if (result) { + _run_main_loop_with_timeout(ibusimcontext); + } + gdk_event_free((GdkEvent *)event); + } + } + } + + // g_object_unref(ibusimcontext); +} + +/* Callback functions for slave context */ +static void +_slave_commit_cb(GtkIMContext *slave, gchar *string, IBusIMContext *ibusimcontext) { + IDEBUG("%s", __FUNCTION__); + _commit_text(ibusimcontext, string); +} + +static void +_slave_preedit_changed_cb(GtkIMContext *slave, IBusIMContext *ibusimcontext) { + IDEBUG("%s", __FUNCTION__); + if (ibusimcontext->ibuscontext) { + return; + } + + _preedit_changed(ibusimcontext); +} + +static void +_slave_preedit_start_cb(GtkIMContext *slave, IBusIMContext *ibusimcontext) { + IDEBUG("%s", __FUNCTION__); + if (ibusimcontext->ibuscontext) { + return; + } + + _preedit_start(ibusimcontext); +} + +static void +_slave_preedit_end_cb(GtkIMContext *slave, IBusIMContext *ibusimcontext) { + IDEBUG("%s", __FUNCTION__); + if (ibusimcontext->ibuscontext) { + return; + } + _preedit_end(ibusimcontext); +} + +static gboolean +_slave_retrieve_surrounding_cb(GtkIMContext *slave, IBusIMContext *ibusimcontext) { + IDEBUG("%s", __FUNCTION__); + gboolean return_value; + + if (ibusimcontext->ibuscontext) { + return FALSE; + } + return _retrieve_surrounding(ibusimcontext); + ; +} + +static gboolean +_slave_delete_surrounding_cb(GtkIMContext *slave, gint offset_from_cursor, guint nchars, IBusIMContext *ibusimcontext) { + IDEBUG("%s", __FUNCTION__); + gboolean return_value; + + if (ibusimcontext->ibuscontext) { + return FALSE; + } + return _delete_surrounding(ibusimcontext, offset_from_cursor, nchars); +} + +void +ibus_im_test_set_thread_loop(IBusIMContext *context, GMainLoop *loop) { + context->thread_loop = loop; +} + +void +ibus_im_test_set_text(IBusIMContext *context, const gchar *text) { + if (!context->text) { + context->text = g_string_new(""); + } + + g_string_append(context->text, text); + ibus_im_context_set_surrounding_with_selection( + (GtkIMContext *)context, context->text->str, context->text->len, context->text->len, context->text->len); +} + +const gchar * +ibus_im_test_get_text(IBusIMContext *context) { + if (context->text) { + return context->text->str; + } + return NULL; +} + +void +ibus_im_test_clear_text(IBusIMContext *context) { + if (context->text) { + g_string_free(context->text, TRUE); + context->text = NULL; + } +} diff --git a/linux/ibus-keyman/tests/ibusimcontext.h b/linux/ibus-keyman/tests/ibusimcontext.h new file mode 100644 index 0000000000..5da7fa40f5 --- /dev/null +++ b/linux/ibus-keyman/tests/ibusimcontext.h @@ -0,0 +1,83 @@ +/* -*- mode: C; c-basic-offset: 4; indent-tabs-mode: nil; -*- */ +/* vim:set et ts=4: */ +/* ibus - The Input Bus + * Copyright (C) 2008-2010 Peng Huang + * Copyright (C) 2008-2010 Red Hat, Inc. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + * USA + */ +#ifndef __IBUSIMCONTEXT_H__ +#define __IBUSIMCONTEXT_H__ + +#include + +/* + * Type macros. + */ +#define IBUS_TYPE_IM_CONTEXT \ + (ibus_im_context_get_type ()) +#define IBUS_IM_CONTEXT(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST ((obj), IBUS_TYPE_IM_CONTEXT, IBusIMContext)) +#define IBUS_IM_CONTEXT_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_CAST ((klass), IBUS_TYPE_IM_CONTEXT, IBusIMContextClass)) +#define IBUS_IS_IM_CONTEXT(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE ((obj), IBUS_TYPE_IM_CONTEXT)) +#define IBUS_IS_IM_CONTEXT_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_TYPE ((klass), IBUS_TYPE_IM_CONTEXT)) +#define IBUS_IM_CONTEXT_GET_CLASS(obj) \ + (G_TYPE_CHECK_GET_CLASS ((obj), IBUS_TYPE_IM_CONTEXT, IBusIMContextClass)) + +G_BEGIN_DECLS +typedef struct _IBusIMContext IBusIMContext; +typedef struct _IBusIMContextClass IBusIMContextClass; +typedef struct _IBusIMContextPrivate IBusIMContextPrivate; + + +GType ibus_im_context_get_type (void); +IBusIMContext + *ibus_im_context_new (void); +void ibus_im_context_register_type + (GTypeModule *type_module); +void ibus_im_context_shutdown + (void); +const gchar + *ibus_im_context_get_ic (IBusIMContext *context); +void ibus_im_context_set_ic (IBusIMContext *context, + const gchar *ic); +void ibus_im_context_enable (IBusIMContext *context); +void ibus_im_context_disable (IBusIMContext *context); +void ibus_im_context_commit_string + (IBusIMContext *context, + const gchar *string); +void ibus_im_context_update_preedit + (IBusIMContext *context, + const gchar *string, + PangoAttrList *attrs, + gint cursor_pos, + gboolean visible); +void ibus_im_context_show_preedit + (IBusIMContext *context); +void ibus_im_context_hide_preedit + (IBusIMContext *context); + +void ibus_im_test_set_thread_loop(IBusIMContext *context, GMainLoop *loop); + +void ibus_im_test_set_text(IBusIMContext *context, const gchar *text); +const gchar *ibus_im_test_get_text(IBusIMContext *context); +void ibus_im_test_clear_text(IBusIMContext *context); +G_END_DECLS +#endif // __IBUSIMCONTEXT_H__ + diff --git a/linux/ibus-keyman/tests/run-tests.sh b/linux/ibus-keyman/tests/run-tests.sh new file mode 100755 index 0000000000..f455d2019f --- /dev/null +++ b/linux/ibus-keyman/tests/run-tests.sh @@ -0,0 +1,29 @@ +#!/bin/bash + +BASEDIR=$(dirname $0) +TESTDIR=${XDG_DATA_HOME:-$HOME/.local/share}/keyman/test_kmx + +if [ "$DISPLAY" == ":0" ]; then + echo "Please start xephyr and set the DISPLAY environment variable before running these tests." + exit 1 +fi + +if [ ! -d $TESTDIR ]; then + ln -sf $BASEDIR/../../../common/core/desktop/build/arch/debug/tests/unit/kmx $TESTDIR +fi + +if [ $# -gt 0 ]; then + TESTFILES=$@ +else + pushd $TESTDIR + TESTFILES=(*.kmx) + popd +fi + +export GSETTINGS_BACKEND=keyfile + +ibus-daemon --panel=disable --config=/usr/libexec/ibus-memconf > /tmp/ibus-daemon.log 2>&1 & +../src/ibus-engine-keyman > /tmp/ibus-engine-keyman.log 2>&1 & +sleep 1s + +./ibus-keyman-tests -k --tap --directory $TESTDIR "${TESTFILES[@]}" diff --git a/linux/ibus-keyman/tests/testfixture.cpp b/linux/ibus-keyman/tests/testfixture.cpp new file mode 100644 index 0000000000..6e7b538e28 --- /dev/null +++ b/linux/ibus-keyman/tests/testfixture.cpp @@ -0,0 +1,352 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "ibusimcontext.h" +#include "keycodes.h" +#include "kmx_test_source.hpp" +#include "testmodule.h" + +typedef struct { + IBusBus *bus; + GtkIMContext *context; + IBusIMContext *ibuscontext; +} IBusKeymanTestsFixture; + +static gboolean loaded = FALSE; +static GdkWindow *window = NULL; +static GMainLoop *thread_loop = NULL; +static GTypeModule *module = NULL; + +static void +module_register(GTypeModule *module) { + ibus_im_context_register_type(module); +} + +void +destroy(GtkWidget *widget, gpointer data) { + gtk_main_quit(); +} + +static void +ibus_keyman_tests_fixture_set_up(IBusKeymanTestsFixture *fixture, gconstpointer user_data) { + IBusIMContextClass *classClass; + + if (!window) { + GtkWidget *widget = gtk_window_new(GTK_WINDOW_TOPLEVEL); + g_signal_connect(widget, "destroy", G_CALLBACK(destroy), NULL); + window = gtk_widget_get_window(widget); + } + + if (!loaded) { + ibus_init(); + + module = test_module_new(module_register); + g_assert_nonnull(module); + + /* Not loaded until we call ref for the first time */ + classClass = static_cast(g_type_class_peek(IBUS_TYPE_IM_CONTEXT)); + g_assert_null(classClass); + + loaded = TRUE; + } + + fixture->bus = ibus_bus_new(); +} + +static void +ibus_keyman_tests_fixture_tear_down(IBusKeymanTestsFixture *fixture, gconstpointer user_data) { + if (thread_loop) { + g_main_loop_unref(thread_loop); + thread_loop = NULL; + } + + g_clear_object(&fixture->bus); + g_clear_object(&fixture->context); +} + +static void +switch_keyboard(IBusKeymanTestsFixture *fixture, const gchar *keyboard) { + ibus_bus_set_global_engine(fixture->bus, keyboard); + + IBusEngineDesc *desc = ibus_bus_get_global_engine(fixture->bus); + g_object_ref_sink(desc); + // g_debug("Switched to engine: '%s'", ibus_engine_desc_get_name(desc)); + g_assert_cmpstr(keyboard, ==, ibus_engine_desc_get_name(desc)); + g_clear_object(&desc); + + if (thread_loop) { + g_main_loop_unref(thread_loop); + thread_loop = NULL; + } + if (fixture->context) { + g_clear_object(&fixture->context); + } + fixture->ibuscontext = ibus_im_context_new(); + fixture->context = (GtkIMContext *)fixture->ibuscontext; + + thread_loop = g_main_loop_new(NULL, TRUE); + ibus_im_test_set_thread_loop(fixture->ibuscontext, thread_loop); +} + +template +std::string +string_format(const std::string &format, Args... args) { + // from https://stackoverflow.com/a/26221725/487503 + int size_s = std::snprintf(nullptr, 0, format.c_str(), args...) + 1; // Extra space for '\0' + if (size_s <= 0) { + throw std::runtime_error("Error during formatting."); + } + auto size = static_cast(size_s); + auto buf = std::make_unique(size); + std::snprintf(buf.get(), size, format.c_str(), args...); + return std::string(buf.get(), buf.get() + size - 1); // We don't want the '\0' inside +} + +static unsigned short vk_to_keycode(unsigned short vk) { + for (int i = 0; i < sizeof(keycode_to_vk); i++) { + if (keycode_to_vk[i] == vk) + return i; + } + return -1; +} + +#define KEYMAN_LCTRL 29 // 0x1D +#define KEYMAN_LALT 56 // 0x38 +#define KEYMAN_RCTRL 97 // 0x61 +#define KEYMAN_RALT 100 // 0x64 + +static guint +km_modifiers_to_ibus(km::tests::KmxTestSource& test_source, km::tests::key_event event) { + guint result = 0; + auto state = event.modifier_state | test_source.caps_lock_state(); + if (state & KM_KBP_MODIFIER_SHIFT) { + result |= IBUS_SHIFT_MASK; + } + if (state & KM_KBP_MODIFIER_ALT) { + result |= IBUS_MOD1_MASK; + } + if (state & KM_KBP_MODIFIER_RALT) { + if (event.vk == KEYMAN_RALT) + result |= IBUS_MOD1_MASK; + else + result |= IBUS_MOD5_MASK; + } + if (state & KM_KBP_MODIFIER_LALT) { + result |= IBUS_MOD1_MASK; + } + if (state & KM_KBP_MODIFIER_CTRL) { + result |= IBUS_CONTROL_MASK; + } + if (state & KM_KBP_MODIFIER_RCTRL) { + result |= IBUS_CONTROL_MASK; + } + if (state & KM_KBP_MODIFIER_LCTRL) { + result |= IBUS_CONTROL_MASK; + } + if (state & KM_KBP_MODIFIER_CAPS) { + result |= IBUS_LOCK_MASK; + } + return result; +} + +typedef struct { + guint modifiers_keydown; + guint modifiers_keyup; + guint keyval; + guint16 keycode; + guint is_modifier; +} gdk_key_event; + +static gdk_key_event +get_key_event_for_modifier(guint modifier, guint & prev_modifiers, guint keyval, guint keycode) { + gdk_key_event event; + event.modifiers_keydown = prev_modifiers; + event.modifiers_keyup = prev_modifiers | modifier | IBUS_RELEASE_MASK; + prev_modifiers |= modifier; + event.keyval = keyval; + event.keycode = keycode; + event.is_modifier = 1; + return event; +} + +static std::list +get_involved_keys(km::tests::KmxTestSource &test_source, km::tests::key_event test_event) { + guint modifiers = 0; + if (test_source.caps_lock_state()) { + modifiers = IBUS_LOCK_MASK; + } + + // process all modifiers + std::list result; + if (test_event.modifier_state & KM_KBP_MODIFIER_SHIFT) { + // we don't distinguish between R and L Shift + result.push_back(get_key_event_for_modifier(IBUS_SHIFT_MASK, modifiers, GDK_KEY_Shift_L, KEY_LEFTSHIFT)); + } + if (test_event.modifier_state & KM_KBP_MODIFIER_ALT || test_event.modifier_state & KM_KBP_MODIFIER_LALT) { + result.push_back(get_key_event_for_modifier(IBUS_MOD1_MASK, modifiers, GDK_KEY_Alt_L, KEY_LEFTALT)); + } + if (test_event.modifier_state & KM_KBP_MODIFIER_RALT) { + if (test_event.vk == KEYMAN_RALT) + result.push_back(get_key_event_for_modifier(IBUS_MOD1_MASK, modifiers, GDK_KEY_Alt_R, KEY_RIGHTALT)); + else + result.push_back(get_key_event_for_modifier(IBUS_MOD5_MASK, modifiers, GDK_KEY_Alt_R, KEY_RIGHTALT)); + } + if (test_event.modifier_state & KM_KBP_MODIFIER_CTRL || test_event.modifier_state & KM_KBP_MODIFIER_LCTRL) { + result.push_back(get_key_event_for_modifier(IBUS_CONTROL_MASK, modifiers, GDK_KEY_Control_L, KEY_LEFTCTRL)); + } + if (test_event.modifier_state & KM_KBP_MODIFIER_RCTRL) { + result.push_back(get_key_event_for_modifier(IBUS_CONTROL_MASK, modifiers, GDK_KEY_Control_R, KEY_RIGHTCTRL)); + } + + // process key + guint keyval = (test_event.modifier_state & KM_KBP_MODIFIER_SHIFT) ? gdk_keyval_to_upper(test_event.vk) + : gdk_keyval_to_lower(test_event.vk); + gdk_key_event event; + event.modifiers_keydown = modifiers; + event.modifiers_keyup = modifiers | IBUS_RELEASE_MASK; + event.keyval = keyval; + event.keycode = vk_to_keycode(test_event.vk); + event.is_modifier = 0; + result.push_back(event); + + return result; +} + +static std::list +get_context_keys(std::u16string context) { + std::list result; + for (auto c = context.begin(); c != context.end(); c++) { + gchar utf8[6]; + if (g_unichar_to_utf8((gunichar)*c, utf8) != 1 || utf8[0] < 32 || utf8[0] > 127) { + g_assertion_message_cmpstr( + G_LOG_DOMAIN, __FILE__, __LINE__, G_STRFUNC, "We can only deal with ASCII characters in the context", utf8, "", ""); + } + result.push_back( + {km::kbp::kmx::s_char_to_vkey[(int)utf8[0] - 32].vk, + (uint16_t)(km::kbp::kmx::s_char_to_vkey[(int)utf8[0] - 32].shifted ? KM_KBP_MODIFIER_SHIFT : 0)}); + } + return result; +} + +static void +press_keys(IBusKeymanTestsFixture *fixture, km::tests::KmxTestSource & test_source,km::tests::key_event key_event) { + auto involved_keys = get_involved_keys(test_source, key_event); + + // press and hold the individual modifier keys, finally the actual key + std::stack keys_to_release; + for (auto key = involved_keys.begin(); key != involved_keys.end(); key++) { + GdkEventKey keyEvent = { + .type = GDK_KEY_PRESS, + .window = window, + .send_event = 0, + .time = 0, + .state = key->modifiers_keydown, + .keyval = key->keyval, + .length = 0, + .string = NULL, + .hardware_keycode = key->keycode, + .group = 0, + .is_modifier = key->is_modifier}; + gtk_im_context_filter_keypress(fixture->context, &keyEvent); + + keyEvent.type = GDK_KEY_RELEASE; + keyEvent.state = key->modifiers_keyup; + keys_to_release.push(keyEvent); + } + + // then release the keys in reverse order + while (!keys_to_release.empty()) { + auto keyEvent = keys_to_release.top(); + gtk_im_context_filter_keypress(fixture->context, &keyEvent); + keys_to_release.pop(); + } +} + +static void test_source(IBusKeymanTestsFixture *fixture, gconstpointer user_data) { + auto sourcefile = string_format("%s.kmn", (char *)user_data); + auto kmxfile = string_format("und:%s.kmx", (char*)user_data); + + km::tests::KmxTestSource test_source; + std::string keys = ""; + std::u16string expected = u"", context = u""; + km::tests::kmx_options options; + bool expected_beep = false; + g_assert_cmpint(test_source.load_source(sourcefile.c_str(), keys, expected, context, options, expected_beep), ==, 0); + + switch_keyboard(fixture, kmxfile.c_str()); + + auto contextKeys = get_context_keys(context); + for (auto k = contextKeys.begin(); k != contextKeys.end(); k++) { + press_keys(fixture, test_source, *k); + } + + gtk_im_context_focus_in(fixture->context); + + for (auto p = test_source.next_key(keys); p.vk != 0; p = test_source.next_key(keys)) { + press_keys(fixture, test_source, p); + } + + auto expectedText = g_utf16_to_utf8((gunichar2*)expected.c_str(), expected.length(), NULL, NULL, NULL); + g_assert_cmpstr(ibus_im_test_get_text(fixture->ibuscontext), ==, expectedText); +} + +void +print_usage() { + printf("Usage: %s --directory test1 [test2 ...]", g_get_prgname()); +} + +int +main(int argc, char *argv[]) { + gtk_init(&argc, &argv); + g_test_init(&argc, &argv, NULL); + g_test_set_nonfatal_assertions(); + + if (argc < 4) { + print_usage(); + return 1; + } + + if (strcmp(argv[1], "--directory") != 0) { + print_usage(); + return 2; + } + + char *directory = argv[2]; + int nTests = argc - 3; + char **tests = &argv[3]; + + // Add tests + for (int i = 0; i < nTests; i++) + { + auto filename = tests[i]; + if (strstr(filename, ".kmx") || strstr(filename, ".kmn")) { + filename[strlen(filename) - 4] = '\0'; + } + auto file = g_file_new_for_commandline_arg(filename); + auto testfilebase = g_file_get_basename(file); + auto testname = g_string_new(NULL); + g_string_append_printf(testname, "/%s", testfilebase); + auto testfile = g_file_new_build_filename(directory, testfilebase, NULL); + g_test_add( + testname->str, IBusKeymanTestsFixture, g_file_get_parse_name(testfile), ibus_keyman_tests_fixture_set_up, test_source, + ibus_keyman_tests_fixture_tear_down); + g_object_unref(file); + g_object_unref(testfile); + g_string_free(testname, TRUE); + } + + // Run tests + int retVal = g_test_run(); + + test_module_unuse(module); + return retVal; +} diff --git a/linux/ibus-keyman/tests/testmodule.c b/linux/ibus-keyman/tests/testmodule.c new file mode 100644 index 0000000000..4e32573d94 --- /dev/null +++ b/linux/ibus-keyman/tests/testmodule.c @@ -0,0 +1,64 @@ +#include "ibusimcontext.h" +#include +#include +#include +#include +#include "testmodule.h" + +static gboolean +test_module_load(GTypeModule *module) { + TestModule *test_module = TEST_MODULE(module); + + test_module->register_func(module); + + return TRUE; +} + +static void +test_module_unload(GTypeModule *module) { +} + +static void +test_module_class_init(TestModuleClass *class) { + GTypeModuleClass *module_class = G_TYPE_MODULE_CLASS(class); + + module_class->load = test_module_load; + module_class->unload = test_module_unload; +} + +static GType +test_module_get_type(void) { + static GType object_type = 0; + + if (!object_type) { + static const GTypeInfo object_info = { + sizeof(TestModuleClass), (GBaseInitFunc)NULL, + (GBaseFinalizeFunc)NULL, (GClassInitFunc)test_module_class_init, + (GClassFinalizeFunc)NULL, NULL, + sizeof(TestModule), 0, + (GInstanceInitFunc)NULL, NULL, + }; + object_type = g_type_register_static(G_TYPE_TYPE_MODULE, "TestModule", &object_info, 0); + } + return object_type; +} + +GTypeModule * +test_module_new(TestModuleRegisterFunc register_func) { + TestModule *test_module = g_object_new(TEST_TYPE_MODULE, NULL); + GTypeModule *module = G_TYPE_MODULE(test_module); + + test_module->register_func = register_func; + + /* Register the types initially */ + g_type_module_set_name(module, "FOO"); + g_type_module_use(module); + //g_type_module_unuse(module); + + return G_TYPE_MODULE(module); +} + +void +test_module_unuse(GTypeModule *test_module) { + g_type_module_unuse(test_module); +} diff --git a/linux/ibus-keyman/tests/testmodule.h b/linux/ibus-keyman/tests/testmodule.h new file mode 100644 index 0000000000..8af60c86aa --- /dev/null +++ b/linux/ibus-keyman/tests/testmodule.h @@ -0,0 +1,33 @@ +#ifndef __TESTMODULE__ +#define __TESTMODULE__ + +G_BEGIN_DECLS + +typedef struct _TestModule TestModule; +typedef struct _TestModuleClass TestModuleClass; + +#define TEST_TYPE_MODULE (test_module_get_type()) +#define TEST_MODULE(module) (G_TYPE_CHECK_INSTANCE_CAST((module), TEST_TYPE_MODULE, TestModule)) +#define TEST_MODULE_CLASS(class) (G_TYPE_CHECK_CLASS_CAST((class), TEST_TYPE_MODULE, TestModuleClass)) +#define TEST_IS_MODULE(module) (G_TYPE_CHECK_INSTANCE_TYPE((module), TEST_TYPE_MODULE)) +#define TEST_IS_MODULE_CLASS(class) (G_TYPE_CHECK_CLASS_TYPE((class), TEST_TYPE_MODULE)) +#define TEST_MODULE_GET_CLASS(module) (G_TYPE_INSTANCE_GET_CLASS((module), TEST_TYPE_MODULE, TestModuleClass)) +typedef void (*TestModuleRegisterFunc)(GTypeModule* module); + +struct _TestModule { + GTypeModule parent_instance; + + TestModuleRegisterFunc register_func; +}; + +struct _TestModuleClass { + GTypeModuleClass parent_class; +}; + +static GType test_module_get_type(void); +GTypeModule* test_module_new(TestModuleRegisterFunc register_func); +void test_module_unuse(GTypeModule* test_module); + +G_END_DECLS + +#endif /* __TESTMODULE__ */