spiegel-keyman/linux/ibus-keyman/tests/ibusimcontext.c
Eberhard Beilharz 0c2d8f0312
refactor(linux): changes to get integration tests working
This change contains various changes and refactorings which together
allows the integration tests to pass again.

One of the important changs is `DbusTestHelper` which listens to a DBus
signal for a sentinel message and then simulates the sentinel key press.
Getting this to work also requires processing sd-bus messages which hooks
into the glib event loop and is implemented in `EventSource`.

Fixes: #13204
2025-02-14 17:30:42 +01:00

1117 lines
35 KiB
C

/* -*- 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 <shawn.p.huang@gmail.com>
* Copyright (C) 2015-2022 Takao Fujiwara <takao.fujiwara1@gmail.com>
* Copyright (C) 2008-2022 Red Hat, Inc.
* Copyright (C) 2021-2022 SIL Global
*
* 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
// commit 506ac9993d5166196b7c4e9bfa9fb0f9d3792ffa.
// It simulates the GTK2 client, leaving out code for GTK3 and GTK4 and
// simplyfying the code a bit by replacing async calls with direct synchronous
// method calls.
#include <config.h>
#include "ibusimcontext.h"
#include <gdk/gdkkeysyms.h>
#include <gtk/gtk.h>
#include <ibus.h>
#include <string.h>
#ifdef GDK_WINDOWING_WAYLAND
#include <gdk/gdkwayland.h>
#endif
#ifdef DEBUG
#define IDEBUG g_debug
#else
#define IDEBUG(a...)
#endif
#define MAX_QUEUED_EVENTS 20
struct _IBusIMContext {
GtkIMContext parent;
/* instance members */
GdkWindow *client_window;
IBusInputContext *ibuscontext;
IBusInputContext *ibuscontext_needs_surrounding;
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;
};
static gboolean surrounding_text_supported = TRUE;
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;
/* 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_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_surrounding(GtkIMContext *context, const gchar *text, int len, int cursor_index);
static void ibus_im_context_set_surrounding_with_selection(
GtkIMContext *context,
const gchar *text,
int len,
int cursor_index,
int anchor_index);
/* static methods*/
static void _create_input_context(IBusIMContext *context);
static gboolean _set_cursor_location_internal(IBusIMContext *context);
static void _bus_connected_cb(IBusBus *bus, 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 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
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);
/* Avoid a loop with _ibus_context_forward_key_event_cb() */
event->state |= IBUS_HANDLED_MASK;
return TRUE;
}
return FALSE;
}
typedef struct {
GdkEvent *event;
IBusIMContext *ibusimcontext;
} 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)) {
gboolean return_value;
IDEBUG("requesting surrounding text");
return_value = _retrieve_surrounding(context);
if (!return_value) {
/* Engines can disable the surrounding text feature with
* the updated capabilities.
*/
if (context->caps & IBUS_CAP_SURROUNDING_TEXT) {
context->caps &= ~IBUS_CAP_SURROUNDING_TEXT;
ibus_input_context_set_capabilities(context->ibuscontext, context->caps);
}
// 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 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->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_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);
}
static void
ibus_im_context_init(GObject *obj) {
IDEBUG("%s", __FUNCTION__);
IBusIMContext *ibusimcontext = IBUS_IM_CONTEXT(obj);
ibusimcontext->client_window = NULL;
// 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->ibuscontext_needs_surrounding = NULL;
ibusimcontext->has_focus = FALSE;
ibusimcontext->time = GDK_CURRENT_TIME;
ibusimcontext->caps = IBUS_CAP_FOCUS | IBUS_CAP_AUXILIARY_TEXT | IBUS_CAP_PROPERTY;
if (surrounding_text_supported) {
ibusimcontext->caps |= IBUS_CAP_SURROUNDING_TEXT;
}
ibusimcontext->events_queue = g_queue_new();
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);
g_queue_free_full(ibusimcontext->events_queue, (GDestroyNotify)gdk_event_free);
G_OBJECT_CLASS(parent_class)->finalize(obj);
}
static gboolean
ibus_im_context_filter_keypress(GtkIMContext* context, GdkEventKey* event) {
IDEBUG("%s", __FUNCTION__);
IBusIMContext *ibusimcontext = IBUS_IM_CONTEXT(context);
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);
}
/* 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_input_context_focus_out(ibusimcontext->ibuscontext);
}
/* 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);
}
}
/* 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_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);
}
}
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);
}
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);
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);
}
}
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);
_request_surrounding_text(ibusimcontext);
_commit_text(ibusimcontext, ibus_text_get_text(text));
g_main_loop_quit(ibusimcontext->thread_loop);
}
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 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) {
for (guint val = nchars;
val <= ibusimcontext->text->len && (guint)ibusimcontext->text->str[ibusimcontext->text->len - val] >= 0x80;
val++) {
nchars = val;
}
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);
/* _create_gdk_event() will add 8 to keycode. */
if (keycode != 0)
keycode -= 8;
}
GdkEventKey *event = _create_gdk_event(ibusimcontext, keyval, keycode, state);
if (!surrounding_text_supported && keyval == IBUS_KEY_BackSpace && ibusimcontext->text->len > 0) {
int index = ibusimcontext->text->len - 1;
int len = 1;
do {
guchar c = (guchar)ibusimcontext->text->str[index];
// delete a single UTF-8 codepoint, code unit (byte) by code unit
if (c <= 0x7F) { // ASCII character
break;
} else if (c >= 0x80 && c <= 0xBF) { // Trailing byte of multi-byte character
index--;
len++;
} else if (c >= 0xC2 && c <= 0xF4) { // First byte of 2-/3-/4-byte character
break;
} else {
g_error("Illegal code unit value 0x%2x in %s", c, ibusimcontext->text->str);
g_test_fail();
}
} while (index >= 0);
g_string_erase(ibusimcontext->text, index, len);
} else {
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_require_surrounding_text_cb(IBusInputContext * ibuscontext, IBusIMContext * ibusimcontext) {
IDEBUG("%s", __FUNCTION__);
g_assert(ibusimcontext->ibuscontext == ibuscontext);
if (ibusimcontext->ibuscontext_needs_surrounding == ibuscontext) {
_request_surrounding_text(ibusimcontext);
ibusimcontext->ibuscontext_needs_surrounding = NULL;
}
}
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;
}
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(10 /*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) {
gchar *prgname = g_strdup(g_get_prgname());
gchar *client_name;
IDEBUG("%s", __FUNCTION__);
g_assert(ibusimcontext->ibuscontext == NULL);
if (!prgname)
prgname = g_strdup_printf("(%d)", getpid());
client_name = g_strdup_printf("%s:%s", "gtk-im", prgname);
g_free(prgname);
IBusInputContext *context = ibus_bus_create_input_context(_bus, client_name);
g_free(client_name);
if (context == NULL) {
g_warning("Create input context failed.");
} else {
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, "require-surrounding-text", G_CALLBACK(_ibus_context_require_surrounding_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 (ibus_input_context_needs_surrounding_text(ibusimcontext->ibuscontext)) {
_request_surrounding_text(ibusimcontext);
} else {
ibusimcontext->ibuscontext_needs_surrounding = ibusimcontext->ibuscontext;
}
}
if (!g_queue_is_empty(ibusimcontext->events_queue)) {
GdkEventKey *event;
while ((event = g_queue_pop_head(ibusimcontext->events_queue))) {
gboolean result = _process_key_event(context, event, ibusimcontext);
if (result) {
_run_main_loop_with_timeout(ibusimcontext);
}
gdk_event_free((GdkEvent *)event);
}
}
}
}
void
ibus_im_test_set_thread_loop(IBusIMContext *context, GMainLoop *loop) {
context->thread_loop = loop;
_run_main_loop_with_timeout(context);
}
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;
}
}
void ibus_im_test_set_surrounding_text_supported(gboolean supported) {
surrounding_text_supported = supported;
}