From cdee19e95328ed4fcf69daecf8c52ca73032e32b Mon Sep 17 00:00:00 2001 From: Eberhard Beilharz Date: Tue, 31 Aug 2021 08:25:38 +0200 Subject: [PATCH] chore(linux): Add integration tests for ibus-keyman This change adds integration/regression tests for ibus-keyman. It re-uses the unit tests that we have for core. Also modifies one test so that the context doesn't use characters used in the keyboard. This makes it easier to setup in ibus-keyman tests. --- .flake8 | 1 + common/core/desktop/.gitignore | 6 + .../unit/kmx/020 - deadkeys and backspace.kmn | 6 +- linux/ibus-keyman/Makefile.am | 1 + linux/ibus-keyman/configure.ac | 1 + linux/ibus-keyman/src/engine.c | 11 +- linux/ibus-keyman/tests/.gitignore | 2 + linux/ibus-keyman/tests/Makefile.am | 112 ++ linux/ibus-keyman/tests/ibusimcontext.c | 1487 +++++++++++++++++ linux/ibus-keyman/tests/ibusimcontext.h | 83 + linux/ibus-keyman/tests/run-tests.sh | 29 + linux/ibus-keyman/tests/testfixture.cpp | 352 ++++ linux/ibus-keyman/tests/testmodule.c | 64 + linux/ibus-keyman/tests/testmodule.h | 33 + 14 files changed, 2179 insertions(+), 9 deletions(-) create mode 100644 linux/ibus-keyman/tests/.gitignore create mode 100644 linux/ibus-keyman/tests/Makefile.am create mode 100644 linux/ibus-keyman/tests/ibusimcontext.c create mode 100644 linux/ibus-keyman/tests/ibusimcontext.h create mode 100755 linux/ibus-keyman/tests/run-tests.sh create mode 100644 linux/ibus-keyman/tests/testfixture.cpp create mode 100644 linux/ibus-keyman/tests/testmodule.c create mode 100644 linux/ibus-keyman/tests/testmodule.h 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__ */