public inbox for git-commits@fedoraproject.org
help / color / mirror / Atom feed
From: Takao Fujiwara <tfujiwar@redhat.com>
To: git-commits@fedoraproject.org
Subject: [rpms/ibus] autotool: Implement IBusMessage and update compose feature
Date: Sun, 31 May 2026 02:08:55 GMT [thread overview]
Message-ID: <178019333565.1.5686319989303863717.rpms-ibus-8523ef50fd7b@fedoraproject.org> (raw)
A new commit has been pushed.
Repo : rpms/ibus
Branch : autotool
Commit : 8523ef50fd7be230ee0f3dced5b353a5ed2068c0
Author : Takao Fujiwara <tfujiwar@redhat.com>
Date : 2025-06-01T23:33:54+09:00
Stats : +5115/-44 in 3 file(s)
URL : https://src.fedoraproject.org/rpms/ibus/c/8523ef50fd7be230ee0f3dced5b353a5ed2068c0?branch=autotool
Log:
Implement IBusMessage and update compose feature
- Fix Exit and Restart menu items in Wayland input-method V2
- Update Plasma setup message
- Implement IBusMessage
- Improve BEPO compose sequence visuals
- Do not load en-US compose table by default
- Fix some memory leaks
---
diff --git a/ibus-1385349-segv-bus-proxy.patch b/ibus-1385349-segv-bus-proxy.patch
index b0bd4e1..612b654 100644
--- a/ibus-1385349-segv-bus-proxy.patch
+++ b/ibus-1385349-segv-bus-proxy.patch
@@ -545,7 +545,7 @@ diff --git a/ui/gtk3/switcher.vala b/ui/gtk3/switcher.vala
index 26bded99..21ede7be 100644
--- a/ui/gtk3/switcher.vala
+++ b/ui/gtk3/switcher.vala
-@@ -236,16 +236,18 @@ class Switcher : Gtk.Window {
+@@ -251,27 +251,37 @@ class Switcher : Gtk.Window {
null,
event,
null);
@@ -560,6 +560,12 @@ index 26bded99..21ede7be 100644
- null);
- if (status != Gdk.GrabStatus.SUCCESS)
- warning("Grab pointer failed! status = %d", status);
+-
+- // Probably we can delete m_popup_delay_time in 1.6
+- pointer.get_position_double(null,
+- out m_mouse_init_x,
+- out m_mouse_init_y);
+- m_mouse_moved = false;
+ } else {
+ status = seat.grab(get_window(),
+ Gdk.SeatCapabilities.POINTER,
@@ -570,45 +576,7 @@ index 26bded99..21ede7be 100644
+ if (status != Gdk.GrabStatus.SUCCESS)
+ warning("Grab pointer failed! status = %d", status);
+ }
- #else
- Gdk.Device device = event.get_device();
- if (device == null) {
-@@ -281,30 +283,41 @@ class Switcher : Gtk.Window {
- Gdk.EventMask.KEY_RELEASE_MASK,
- null,
- Gdk.CURRENT_TIME);
-- if (status != Gdk.GrabStatus.SUCCESS)
-+ if (status != Gdk.GrabStatus.SUCCESS) {
- warning("Grab keyboard failed! status = %d", status);
-- // Grab all pointer events
-- status = pointer.grab(get_window(),
-- Gdk.GrabOwnership.NONE,
-- true,
-- Gdk.EventMask.BUTTON_PRESS_MASK |
-- Gdk.EventMask.BUTTON_RELEASE_MASK,
-- null,
-- Gdk.CURRENT_TIME);
-- if (status != Gdk.GrabStatus.SUCCESS)
-- warning("Grab pointer failed! status = %d", status);
-+ } else {
-+ // Grab all pointer events
-+ status = pointer.grab(get_window(),
-+ Gdk.GrabOwnership.NONE,
-+ true,
-+ Gdk.EventMask.BUTTON_PRESS_MASK |
-+ Gdk.EventMask.BUTTON_RELEASE_MASK,
-+ null,
-+ Gdk.CURRENT_TIME);
-+ if (status != Gdk.GrabStatus.SUCCESS)
-+ warning("Grab pointer failed! status = %d", status);
-+ }
- #endif
-- // Probably we can delete m_popup_delay_time in 1.6
-- pointer.get_position_double(null,
-- out m_mouse_init_x,
-- out m_mouse_init_y);
-- m_mouse_moved = false;
+ /* Fix RHBZ #1771238 assert(m_loop == null)
+ * Grabbing keyboard can be failed when the second Super-e is typed
+ * before Switcher dialog is focused. And m_loop could not be released
@@ -623,7 +591,6 @@ index 26bded99..21ede7be 100644
+ out m_mouse_init_y);
+ m_mouse_moved = false;
-
- m_loop = new GLib.MainLoop();
- m_loop.run();
- m_loop = null;
@@ -632,8 +599,8 @@ index 26bded99..21ede7be 100644
+ m_loop = null;
+ }
- #if VALA_0_34
seat.ungrab();
+
--
2.45.0
diff --git a/ibus-HEAD.patch b/ibus-HEAD.patch
index e69de29..75e3b46 100644
--- a/ibus-HEAD.patch
+++ b/ibus-HEAD.patch
@@ -0,0 +1,5092 @@
+From d0ad4e6e519439f034d08fd6531403027bd7fbde Mon Sep 17 00:00:00 2001
+From: fujiwarat <takao.fujiwara1@gmail.com>
+Date: Fri, 18 Apr 2025 16:36:43 +0900
+Subject: [PATCH] ui/gtk3: Fix Exit and Restart menu items in Wayland input-method V2
+
+---
+ tools/main.vala | 1 +
+ ui/gtk3/panel.vala | 18 +++++++-----------
+ 2 files changed, 8 insertions(+), 11 deletions(-)
+
+diff --git a/tools/main.vala b/tools/main.vala
+index 4b5bcff2..a7c6d95d 100644
+--- a/tools/main.vala
++++ b/tools/main.vala
+@@ -277,6 +277,7 @@ bool start_daemon_in_wayland(bool restart,
+ "directly.\n");
+ }
+ bus = null;
++ Posix.sleep(3);
+ } else if (bus != null) {
+ stderr.printf("%s\n".printf(_("IBus is running.")));
+ Posix.exit(Posix.EXIT_FAILURE);
+diff --git a/ui/gtk3/panel.vala b/ui/gtk3/panel.vala
+index e42a166e..ab514bab 100644
+--- a/ui/gtk3/panel.vala
++++ b/ui/gtk3/panel.vala
+@@ -1524,25 +1524,21 @@ class Panel : IBus.PanelService {
+ string[] _args = {};
+ _args += binary;
+ _args += args;
+- if (args == "exit" || args == "restart")
+- _args += "--type=kde-wayland";
+- string? standard_out = null;
+- string? standard_error = null;
++ // FIXME: stdout & stderr causes a dead lock with `ibus restart`
++ // in Wayland input-method V2.
+ GLib.Process.spawn_sync(null,
+ _args,
+ GLib.Environ.get(),
+- 0,
++ GLib.SpawnFlags.SEARCH_PATH_FROM_ENVP,
++ null,
++ null,
+ null,
+- out standard_out,
+- out standard_error,
+ null);
+- if (standard_out != null)
+- print(standard_out);
+- if (standard_error != null)
+- warning("Execute %s failed! %s", binary, standard_error);
+ } catch (GLib.SpawnError e) {
+ warning("Execute %s failed! %s", binary, e.message);
+ }
++ IBus.quit();
++ Gtk.main_quit();
+ }
+
+ private void append_preferences_menu(Gtk.Menu menu) {
+--
+2.49.0
+
+From b4f51b69f03b18fadf4bbd82abc7b7e92fae44f0 Mon Sep 17 00:00:00 2001
+From: fujiwarat <takao.fujiwara1@gmail.com>
+Date: Sat, 3 May 2025 11:54:21 +0900
+Subject: [PATCH] bus/inputcontext: Free data in bus_input_context_forward_process_key_event
+
+Should free data in case of no focuses and engines.
+---
+ bus/inputcontext.c | 6 ++++--
+ 1 file changed, 4 insertions(+), 2 deletions(-)
+
+diff --git a/bus/inputcontext.c b/bus/inputcontext.c
+index c551fbc8..41b47540 100644
+--- a/bus/inputcontext.c
++++ b/bus/inputcontext.c
+@@ -2,8 +2,8 @@
+ /* vim:set et sts=4: */
+ /* ibus - The Input Bus
+ * Copyright (C) 2008-2014 Peng Huang <shawn.p.huang@gmail.com>
+- * Copyright (C) 2015-2024 Takao Fujiwara <takao.fujiwara1@gmail.com>
+- * Copyright (C) 2008-2024 Red Hat, Inc.
++ * Copyright (C) 2015-2025 Takao Fujiwara <takao.fujiwara1@gmail.com>
++ * Copyright (C) 2008-2025 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
+@@ -3564,5 +3564,7 @@ bus_input_context_forward_process_key_event (BusInputContext *context,
+ (GAsyncReadyCallback)
+ _forward_process_key_event_reply_cb,
+ data);
++ } else {
++ g_slice_free (ProcessKeyEventData, data);
+ }
+ }
+--
+2.49.0
+
+From e7222db9b72dfd08d7ef1a03d5b6baa046d025f0 Mon Sep 17 00:00:00 2001
+From: fujiwarat <takao.fujiwara1@gmail.com>
+Date: Thu, 22 May 2025 23:23:48 +0900
+Subject: [PATCH] ui/gtk3: Update Plasma setup message
+
+BUG=https://github.com/ibus/ibus/issues/2753
+---
+ ui/gtk3/panel.vala | 11 +++++------
+ 1 file changed, 5 insertions(+), 6 deletions(-)
+
+diff --git a/ui/gtk3/panel.vala b/ui/gtk3/panel.vala
+index ab514bab..363834e9 100644
+--- a/ui/gtk3/panel.vala
++++ b/ui/gtk3/panel.vala
+@@ -926,12 +926,11 @@ class Panel : IBus.PanelService {
+ var format =
+ _("IBus should be called from the desktop session in " +
+ "%s. For KDE, you can launch '%s' " +
+- "utility and go to \"Input Devices\" -> " +
+- "\"Virtual Keyboard\" section and select " +
++ "utility and go to \"Input & Output\" -> \"Keyboard\" " +
++ "-> \"Virtual Keyboard\" section and select " +
+ "\"%s\" icon and click \"Apply\" button to " +
+ "configure IBus in %s. For other desktop " +
+- "sessions, you can copy the 'Exec=' line in %s file " +
+- "to a configuration file of the session. " +
++ "sessions, you can run \"%s\" command. " +
+ "Please refer each document about the \"Wayland " +
+ "input method\" configuration. Before you configure " +
+ "the \"Wayland input method\", you should make sure " +
+@@ -939,10 +938,10 @@ class Panel : IBus.PanelService {
+ "variables are unset in the desktop session.");
+ message = format.printf(
+ "Wayland",
+- "systemsettings5",
++ "systemsettings",
+ "IBus Wayland",
+ "Wayland",
+- "org.freedesktop.IBus.Panel.Wayland.Gtk3.desktop");
++ "ibus start");
+ } else if (m_is_wayland && m_is_wayland_im && !is_gnome()) {
+ if (Environment.get_variable("QT_IM_MODULE") == "ibus") {
+ var format =
+--
+2.49.0
+
+From 7feb57a8330d0969f82c742a2c1c0aeb120971fc Mon Sep 17 00:00:00 2001
+From: matiwari <matiwari@redhat.com>
+Date: Tue, 29 Apr 2025 12:52:25 +0530
+Subject: [PATCH] src/ibusobservedpath: Free IBusObservedPath objects during traversal
+
+BUG=https://github.com/ibus/ibus/issues/2755
+---
+ src/ibusobservedpath.c | 9 ++++++++-
+ 1 file changed, 8 insertions(+), 1 deletion(-)
+
+diff --git a/src/ibusobservedpath.c b/src/ibusobservedpath.c
+index 42192431..224bfb8b 100644
+--- a/src/ibusobservedpath.c
++++ b/src/ibusobservedpath.c
+@@ -79,7 +79,12 @@ ibus_observed_path_init (IBusObservedPath *path)
+ static void
+ ibus_observed_path_destroy (IBusObservedPath *path)
+ {
++ IBusObservedPathPrivate *priv;
+ g_free (path->path);
++ if (IBUS_IS_OBSERVED_PATH (path)) {
++ priv = IBUS_OBSERVED_PATH_GET_PRIVATE (path);
++ g_clear_pointer (&priv->file_hash_list, g_free);
++ }
+ IBUS_OBJECT_CLASS (ibus_observed_path_parent_class)->destroy (IBUS_OBJECT (path));
+ }
+
+@@ -349,6 +354,8 @@ ibus_observed_path_traverse (IBusObservedPath *path,
+ ibus_observed_path_traverse (sub, dir_only));
+ } else if (sub->is_exist && !dir_only) {
+ paths = g_list_append (paths, sub);
++ } else {
++ g_object_unref (sub);
+ }
+ }
+ g_dir_close (dir);
+@@ -513,7 +520,7 @@ ibus_observed_path_new (const gchar *path,
+ priv->file_hash_list[i + 1] = 0;
+ ++i;
+ }
+- g_list_free_full (file_list, (GDestroyNotify)ibus_observed_path_destroy);
++ g_list_free_full (file_list, (GDestroyNotify)g_object_unref);
+
+ if (fill_stat)
+ ibus_observed_path_fill_stat (op);
+--
+2.49.0
+
+From c5d98f98ecc98f8c726b5d9631464305306c8fb1 Mon Sep 17 00:00:00 2001
+From: fujiwarat <takao.fujiwara1@gmail.com>
+Date: Sun, 25 May 2025 19:08:24 +0900
+Subject: [PATCH 1/3] ui/gtk3: Delete Vala deprecated APIs before 0.34
+
+Vala 0.34 has been available since 2018 and now the deprecated APIs
+can be deleted safely for 5 years.
+
+BUG=https://github.com/ibus/ibus/pull/2761
+---
+ ui/gtk3/bindingcommon.vala | 11 +-----
+ ui/gtk3/candidatepanel.vala | 7 ----
+ ui/gtk3/emojier.vala | 34 ----------------
+ ui/gtk3/propertypanel.vala | 61 -----------------------------
+ ui/gtk3/switcher.vala | 78 -------------------------------------
+ 5 files changed, 2 insertions(+), 189 deletions(-)
+
+diff --git a/ui/gtk3/bindingcommon.vala b/ui/gtk3/bindingcommon.vala
+index 8c332542..727e1257 100644
+--- a/ui/gtk3/bindingcommon.vala
++++ b/ui/gtk3/bindingcommon.vala
+@@ -76,17 +76,10 @@ class BindingCommon {
+ Gdk.ModifierType.HYPER_MASK |
+ Gdk.ModifierType.META_MASK);
+ if ((switch_modifiers & VIRTUAL_MODIFIERS) != 0) {
+- // workaround a bug in gdk vapi vala > 0.18
+- // https://bugzilla.gnome.org/show_bug.cgi?id=677559
+-#if VALA_0_18
++ // workaround a bug in gdk vapi vala > 0.18
++ // https://bugzilla.gnome.org/show_bug.cgi?id=677559
+ Gdk.Keymap.get_for_display(Gdk.Display.get_default()
+ ).map_virtual_modifiers(ref switch_modifiers);
+-#else
+- if ((switch_modifiers & Gdk.ModifierType.SUPER_MASK) != 0)
+- switch_modifiers |= Gdk.ModifierType.MOD4_MASK;
+- if ((switch_modifiers & Gdk.ModifierType.HYPER_MASK) != 0)
+- switch_modifiers |= Gdk.ModifierType.MOD4_MASK;
+-#endif
+ switch_modifiers &= ~VIRTUAL_MODIFIERS;
+ }
+
+diff --git a/ui/gtk3/candidatepanel.vala b/ui/gtk3/candidatepanel.vala
+index 5ba8cf36..aa1a34ce 100644
+--- a/ui/gtk3/candidatepanel.vala
++++ b/ui/gtk3/candidatepanel.vala
+@@ -508,17 +508,10 @@ public class CandidatePanel : Gtk.Box{
+ // Use get_monitor_geometry() instead of get_monitor_area().
+ // get_monitor_area() excludes docks, but the lookup window should be
+ // shown over them.
+-#if VALA_0_34
+ Gdk.Monitor monitor = window.get_display().get_monitor_at_point(
+ m_cursor_location.x,
+ m_cursor_location.y);
+ monitor_area = monitor.get_geometry();
+-#else
+- Gdk.Screen screen = Gdk.Screen.get_default();
+- int monitor_num = screen.get_monitor_at_point(m_cursor_location.x,
+- m_cursor_location.y);
+- screen.get_monitor_geometry(monitor_num, out monitor_area);
+-#endif
+ return monitor_area;
+ }
+
+diff --git a/ui/gtk3/emojier.vala b/ui/gtk3/emojier.vala
+index 519da3e2..1770de2f 100644
+--- a/ui/gtk3/emojier.vala
++++ b/ui/gtk3/emojier.vala
+@@ -29,11 +29,7 @@ public class IBusEmojier : Gtk.ApplicationWindow {
+ valign : Gtk.Align.FILL
+ );
+ this.motion_notify_event.connect((e) => {
+-#if VALA_0_24
+ Gdk.EventMotion pe = e;
+-#else
+- Gdk.EventMotion *pe = &e;
+-#endif
+ if (m_mouse_x == pe.x_root && m_mouse_y == pe.y_root)
+ return false;
+ m_mouse_x = pe.x_root;
+@@ -1516,11 +1512,7 @@ public class IBusEmojier : Gtk.ApplicationWindow {
+ return false;
+ if (m_lookup_table.get_cursor_pos() == index)
+ return false;
+-#if VALA_0_24
+ Gdk.EventMotion pe = e;
+-#else
+- Gdk.EventMotion *pe = &e;
+-#endif
+ if (m_mouse_x == pe.x_root && m_mouse_y == pe.y_root)
+ return false;
+ m_mouse_x = pe.x_root;
+@@ -1964,17 +1956,10 @@ public class IBusEmojier : Gtk.ApplicationWindow {
+ // Use get_monitor_geometry() instead of get_monitor_area().
+ // get_monitor_area() excludes docks, but the lookup window should be
+ // shown over them.
+-#if VALA_0_34
+ Gdk.Monitor monitor = Gdk.Display.get_default().get_monitor_at_point(
+ m_cursor_location.x,
+ m_cursor_location.y);
+ monitor_area = monitor.get_geometry();
+-#else
+- Gdk.Screen screen = Gdk.Screen.get_default();
+- int monitor_num = screen.get_monitor_at_point(m_cursor_location.x,
+- m_cursor_location.y);
+- screen.get_monitor_geometry(monitor_num, out monitor_area);
+-#endif
+ return monitor_area;
+ }
+
+@@ -2268,24 +2253,12 @@ public class IBusEmojier : Gtk.ApplicationWindow {
+ present_centralize(event);
+
+ Gdk.Device pointer;
+-#if VALA_0_34
+ Gdk.Seat seat = event.get_seat();
+ if (seat == null) {
+ var display = get_display();
+ seat = display.get_default_seat();
+ }
+ pointer = seat.get_pointer();
+-#else
+- Gdk.Device device = event.get_device();
+- if (device == null) {
+- var display = get_display();
+- device = display.list_devices().data;
+- }
+- if (device.get_source() == Gdk.InputSource.KEYBOARD)
+- pointer = device.get_associated_device();
+- else
+- pointer = device;
+-#endif
+ pointer.get_position_double(null,
+ out m_mouse_x,
+ out m_mouse_y);
+@@ -2521,17 +2494,10 @@ public class IBusEmojier : Gtk.ApplicationWindow {
+ get_allocation(out allocation);
+ Gdk.Rectangle monitor_area;
+ Gdk.Rectangle work_area;
+-#if VALA_0_34
+ Gdk.Display display = get_display();
+ Gdk.Monitor monitor = display.get_monitor_at_window(this.get_window());
+ monitor_area = monitor.get_geometry();
+ work_area = monitor.get_workarea();
+-#else
+- Gdk.Screen screen = Gdk.Screen.get_default();
+- int monitor_num = screen.get_monitor_at_window(this.get_window());
+- screen.get_monitor_geometry(monitor_num, out monitor_area);
+- work_area = screen.get_monitor_workarea(monitor_num);
+-#endif
+ int x = (monitor_area.x + monitor_area.width - allocation.width)/2;
+ int y = (monitor_area.y + monitor_area.height
+ - allocation.height)/2;
+diff --git a/ui/gtk3/propertypanel.vala b/ui/gtk3/propertypanel.vala
+index 20739b9c..f1ecd7f7 100644
+--- a/ui/gtk3/propertypanel.vala
++++ b/ui/gtk3/propertypanel.vala
+@@ -359,15 +359,10 @@ public class PropertyPanel : Gtk.Box {
+ m_toplevel.get_allocation(out allocation);
+
+ Gdk.Rectangle monitor_area;
+-#if VALA_0_34
+ // gdk_screen_get_monitor_workarea() no longer return the correct
+ // area from "_NET_WORKAREA" atom in GTK 3.22
+ Gdk.Monitor monitor = Gdk.Display.get_default().get_monitor(0);
+ monitor_area = monitor.get_workarea();
+-#else
+- Gdk.Screen screen = Gdk.Screen.get_default();
+- monitor_area = screen.get_monitor_workarea(0);
+-#endif
+ int monitor_right = monitor_area.x + monitor_area.width;
+ int monitor_bottom = monitor_area.y + monitor_area.height;
+ int x, y;
+@@ -524,15 +519,10 @@ public class PropMenu : Gtk.Menu, IPropToolItem {
+ public new void popup(uint button,
+ uint32 activate_time,
+ Gtk.Widget widget) {
+-#if VALA_0_34
+ base.popup_at_widget(widget,
+ Gdk.Gravity.SOUTH_WEST,
+ Gdk.Gravity.NORTH_WEST,
+ null);
+-#else
+- m_parent_button = widget;
+- base.popup(null, null, menu_position, button, activate_time);
+-#endif
+ }
+
+ public override void destroy() {
+@@ -590,57 +580,6 @@ public class PropMenu : Gtk.Menu, IPropToolItem {
+ }
+ }
+ }
+-
+-#if !VALA_0_34
+- private void menu_position(Gtk.Menu menu,
+- out int x,
+- out int y,
+- out bool push_in) {
+- var button = m_parent_button;
+- var screen = button.get_screen();
+- var monitor = screen.get_monitor_at_window(button.get_window());
+-
+- Gdk.Rectangle monitor_location;
+- screen.get_monitor_geometry(monitor, out monitor_location);
+-
+- button.get_window().get_origin(out x, out y);
+-
+- Gtk.Allocation button_allocation;
+- button.get_allocation(out button_allocation);
+-
+- x += button_allocation.x;
+- y += button_allocation.y;
+-
+- int menu_width;
+- int menu_height;
+- menu.get_size_request(out menu_width, out menu_height);
+-
+- if (x + menu_width >= monitor_location.width)
+- x -= menu_width - button_allocation.width;
+- else if (x - menu_width <= 0)
+- ;
+- else {
+- if (x <= monitor_location.width * 3 / 4)
+- ;
+- else
+- x -= menu_width - button_allocation.width;
+- }
+-
+- if (y + button_allocation.height + menu_width
+- >= monitor_location.height)
+- y -= menu_height;
+- else if (y - menu_height <= 0)
+- y += button_allocation.height;
+- else {
+- if (y <= monitor_location.height * 3 / 4)
+- y += button_allocation.height;
+- else
+- y -= menu_height;
+- }
+-
+- push_in = false;
+- }
+-#endif
+ }
+
+ public class PropToolButton : Gtk.ToolButton, IPropToolItem {
+diff --git a/ui/gtk3/switcher.vala b/ui/gtk3/switcher.vala
+index 99a2fa0b..d4a5fa11 100644
+--- a/ui/gtk3/switcher.vala
++++ b/ui/gtk3/switcher.vala
+@@ -235,7 +235,6 @@ class Switcher : Gtk.Window {
+ }
+
+ Gdk.Device pointer;
+-#if VALA_0_34
+ Gdk.Seat seat = event.get_seat();
+ if (seat == null) {
+ var display = get_display();
+@@ -262,54 +261,6 @@ class Switcher : Gtk.Window {
+ null);
+ if (status != Gdk.GrabStatus.SUCCESS)
+ warning("Grab pointer failed! status = %d", status);
+-#else
+- Gdk.Device device = event.get_device();
+- if (device == null) {
+- var display = get_display();
+- var device_manager = display.get_device_manager();
+-/* The macro VALA_X_Y supports even numbers.
+- * http://git.gnome.org/browse/vala/commit/?id=294b374af6
+- */
+-#if VALA_0_16
+- device = device_manager.list_devices(Gdk.DeviceType.MASTER).data;
+-#else
+- unowned GLib.List<Gdk.Device> devices =
+- device_manager.list_devices(Gdk.DeviceType.MASTER);
+- device = devices.data;
+-#endif
+- }
+-
+- Gdk.Device keyboard;
+- if (device.get_source() == Gdk.InputSource.KEYBOARD) {
+- keyboard = device;
+- pointer = device.get_associated_device();
+- } else {
+- pointer = device;
+- keyboard = device.get_associated_device();
+- }
+-
+- Gdk.GrabStatus status;
+- // Grab all keyboard events
+- status = keyboard.grab(get_window(),
+- Gdk.GrabOwnership.NONE,
+- true,
+- Gdk.EventMask.KEY_PRESS_MASK |
+- Gdk.EventMask.KEY_RELEASE_MASK,
+- null,
+- Gdk.CURRENT_TIME);
+- if (status != Gdk.GrabStatus.SUCCESS)
+- warning("Grab keyboard failed! status = %d", status);
+- // Grab all pointer events
+- status = pointer.grab(get_window(),
+- Gdk.GrabOwnership.NONE,
+- true,
+- Gdk.EventMask.BUTTON_PRESS_MASK |
+- Gdk.EventMask.BUTTON_RELEASE_MASK,
+- null,
+- Gdk.CURRENT_TIME);
+- if (status != Gdk.GrabStatus.SUCCESS)
+- warning("Grab pointer failed! status = %d", status);
+-#endif
+
+ // Probably we can delete m_popup_delay_time in 1.6
+ pointer.get_position_double(null,
+@@ -322,12 +273,7 @@ class Switcher : Gtk.Window {
+ m_loop.run();
+ m_loop = null;
+
+-#if VALA_0_34
+ seat.ungrab();
+-#else
+- keyboard.ungrab(Gdk.CURRENT_TIME);
+- pointer.ungrab(Gdk.CURRENT_TIME);
+-#endif
+
+ hide();
+ // Make sure the switcher is hidden before returning from this function.
+@@ -372,11 +318,7 @@ class Switcher : Gtk.Window {
+ return true;
+ });
+ button.motion_notify_event.connect((e) => {
+-#if VALA_0_24
+ Gdk.EventMotion pe = e;
+-#else
+- Gdk.EventMotion *pe = &e;
+-#endif
+ if (m_selected_engine == index)
+ return false;
+ if (!m_mouse_moved &&
+@@ -414,21 +356,12 @@ class Switcher : Gtk.Window {
+
+ Gdk.Display display = Gdk.Display.get_default();
+ int screen_width = 0;
+-#if VALA_0_34
+ // display.get_monitor_at_window() is null because of unrealized window
+ Gdk.Monitor monitor = display.get_primary_monitor();
+ if (monitor == null)
+ return;
+ Gdk.Rectangle area = monitor.get_geometry();
+ screen_width = area.width;
+-#else
+- Gdk.Screen screen = (display != null) ?
+- display.get_default_screen() : null;
+-
+- if (screen != null) {
+- screen_width = screen.get_width();
+- }
+-#endif
+
+ if (screen_width > 0 && max_label_width > (screen_width / 4)) {
+ max_label_width = screen_width / 4;
+@@ -503,14 +436,7 @@ class Switcher : Gtk.Window {
+ public override bool key_press_event(Gdk.EventKey e) {
+ bool retval = Gdk.EVENT_STOP;
+
+-/* Gdk.EventKey is changed to the pointer.
+- * https://git.gnome.org/browse/vala/commit/?id=598942f1
+- */
+-#if VALA_0_24
+ Gdk.EventKey pe = e;
+-#else
+- Gdk.EventKey *pe = &e;
+-#endif
+
+ if (m_popup_delay_time > 0) {
+ restore_window_position("pressed");
+@@ -557,11 +483,7 @@ class Switcher : Gtk.Window {
+ }
+
+ public override bool key_release_event(Gdk.EventKey e) {
+-#if VALA_0_24
+ Gdk.EventKey pe = e;
+-#else
+- Gdk.EventKey *pe = &e;
+-#endif
+
+ if (KeybindingManager.primary_modifier_still_pressed((Gdk.Event) pe,
+ m_primary_modifier)) {
+--
+2.49.0
+
+From cf6263e4742fa338fd9bbed78396bb29a70aeccb Mon Sep 17 00:00:00 2001
+From: fujiwarat <takao.fujiwara1@gmail.com>
+Date: Sun, 1 Jun 2025 20:24:42 +0900
+Subject: [PATCH 2/3] Implement IBusMessage
+
+IBusEngine and IBusPanel can send IBusMessage to IBusPanel.
+Currently GtkPopup takes input focus in Wayland, and the Panel can only
+display non-focused popups with the Wayland panel protocol.
+
+While GTK allows the IM modules to flash the focused window to alert
+users of thier typing errors, individual IBus engines lack this
+capability. This is because engines typically only receive keyboard
+input (keysyms/keycodes) and not the associated window information.
+The proposed solution is to redirect the error messages to the system
+Panel. The Panel, having access to window management in Wayland and
+Xorg, could then handle the display of non-focused popup notifications.
+
+GNOME-Shell needs to handle the IBusMessage. As a temporary solution in
+GNOME, the compose sequence will be flashed to alert users as a
+workaround until the full implementation is completed.
+
+Also refactor "Free data in bus_input_context_forward_process_key_event".
+
+Fixes: https://github.com/ibus/ibus/commit/b4f51b69
+BUG=https://github.com/ibus/ibus/issues/2722
+---
+ bindings/vala/IBus-1.0-custom.vala | 40 +-
+ bus/engineproxy.c | 40 +-
+ bus/ibusimpl.c | 67 ++--
+ bus/inputcontext.c | 34 +-
+ bus/panelproxy.c | 37 +-
+ bus/panelproxy.h | 7 +-
+ src/Makefile.am | 6 +-
+ src/ibusengine.c | 33 +-
+ src/ibusengine.h | 36 +-
+ src/ibusenginesimple.c | 63 +++-
+ src/ibusmessage.c | 562 +++++++++++++++++++++++++++++
+ src/ibusmessage.h | 205 +++++++++++
+ src/ibuspanelservice.c | 128 ++++++-
+ src/ibuspanelservice.h | 38 +-
+ src/ibustypes.h | 14 +
+ ui/gtk3/Makefile.am | 1 +
+ ui/gtk3/emojier.vala | 39 +-
+ ui/gtk3/message.vala | 353 ++++++++++++++++++
+ ui/gtk3/panel.vala | 137 +++++--
+ ui/gtk3/panelbinding.vala | 5 +-
+ 20 files changed, 1727 insertions(+), 118 deletions(-)
+ create mode 100644 src/ibusmessage.c
+ create mode 100644 src/ibusmessage.h
+ create mode 100644 ui/gtk3/message.vala
+
+diff --git a/bindings/vala/IBus-1.0-custom.vala b/bindings/vala/IBus-1.0-custom.vala
+index ec46fc90..8ced8a97 100644
+--- a/bindings/vala/IBus-1.0-custom.vala
++++ b/bindings/vala/IBus-1.0-custom.vala
+@@ -1,24 +1,38 @@
+ namespace IBus {
++ public class EmojiData : IBus.Serializable {
++ [CCode (cname = "ibus_emoji_data_new",
++ has_construct_function = true)]
++ public EmojiData (string first_property_name, ...);
++ }
++ public class ExtensionEvent : IBus.Serializable {
++ [CCode (cname = "ibus_extension_event_new",
++ has_construct_function = true)]
++ public ExtensionEvent (string first_property_name, ...);
++ }
++ public class Message : IBus.Serializable {
++ [CCode (cname = "ibus_message_new",
++ has_construct_function = true)]
++ public Message (uint domain,
++ uint code,
++ string? title,
++ string description,
++ ...);
++ }
++ public class PanelService : IBus.Service {
++ public void
++ panel_extension_register_keys(string first_property_name, ...);
++ }
+ // For some reason, ibus_text_new_from_static_string is hidden in GIR
+ // https://github.com/ibus/ibus/commit/37e6e587
+ [CCode (type_id = "ibus_text_get_type ()", cheader_filename = "ibus.h")]
+ public class Text : IBus.Serializable {
+- [CCode (cname = "ibus_text_new_from_static_string", has_construct_function = false)]
++ [CCode (cname = "ibus_text_new_from_static_string",
++ has_construct_function = false)]
+ public Text.from_static_string (string str);
+ }
+- public class ExtensionEvent : IBus.Serializable {
+- [CCode (cname = "ibus_extension_event_new", has_construct_function = true)]
+- public ExtensionEvent (string first_property_name, ...);
+- }
+ public class XEvent : IBus.Serializable {
+- [CCode (cname = "ibus_x_event_new", has_construct_function = true)]
++ [CCode (cname = "ibus_x_event_new",
++ has_construct_function = true)]
+ public XEvent (string first_property_name, ...);
+ }
+- public class PanelService : IBus.Service {
+- public void panel_extension_register_keys(string first_property_name, ...);
+- }
+- public class EmojiData : IBus.Serializable {
+- [CCode (cname = "ibus_emoji_data_new", has_construct_function = true)]
+- public EmojiData (string first_property_name, ...);
+- }
+ }
+diff --git a/bus/engineproxy.c b/bus/engineproxy.c
+index 8bd6cefd..98ec63fe 100644
+--- a/bus/engineproxy.c
++++ b/bus/engineproxy.c
+@@ -95,6 +95,7 @@ enum {
+ REGISTER_PROPERTIES,
+ UPDATE_PROPERTY,
+ PANEL_EXTENSION,
++ SEND_MESSAGE,
+ LAST_SIGNAL,
+ };
+
+@@ -458,6 +459,20 @@ bus_engine_proxy_class_init (BusEngineProxyClass *class)
+ G_TYPE_FROM_CLASS (class),
+ bus_marshal_VOID__OBJECTv);
+
++ engine_signals[SEND_MESSAGE] =
++ g_signal_new (I_("send-message"),
++ G_TYPE_FROM_CLASS (class),
++ G_SIGNAL_RUN_LAST,
++ 0,
++ NULL, NULL,
++ bus_marshal_VOID__VARIANT,
++ G_TYPE_NONE,
++ 1,
++ G_TYPE_VARIANT);
++ g_signal_set_va_marshaller (engine_signals[SEND_MESSAGE],
++ G_TYPE_FROM_CLASS (class),
++ bus_marshal_VOID__VARIANTv);
++
+ text_empty = ibus_text_new_from_static_string ("");
+ g_object_ref_sink (text_empty);
+
+@@ -594,7 +609,7 @@ bus_engine_proxy_g_signal (GDBusProxy *proxy,
+
+ gint i;
+ for (i = 0; i < G_N_ELEMENTS (signals); i++) {
+- if (g_strcmp0 (signal_name, signals[i].signal_name) == 0) {
++ if (!g_strcmp0 (signal_name, signals[i].signal_name)) {
+ g_signal_emit (engine, engine_signals[signals[i].signal_id], 0);
+ return;
+ }
+@@ -603,7 +618,7 @@ bus_engine_proxy_g_signal (GDBusProxy *proxy,
+ /* Handle D-Bus signals with parameters. Deserialize them and emit a glib
+ * signal.
+ */
+- if (g_strcmp0 (signal_name, "CommitText") == 0) {
++ if (!g_strcmp0 (signal_name, "CommitText")) {
+ GVariant *arg0 = NULL;
+ g_variant_get (parameters, "(v)", &arg0);
+ g_return_if_fail (arg0 != NULL);
+@@ -616,7 +631,7 @@ bus_engine_proxy_g_signal (GDBusProxy *proxy,
+ return;
+ }
+
+- if (g_strcmp0 (signal_name, "ForwardKeyEvent") == 0) {
++ if (!g_strcmp0 (signal_name, "ForwardKeyEvent")) {
+ guint32 keyval = 0;
+ guint32 keycode = 0;
+ guint32 states = 0;
+@@ -631,7 +646,7 @@ bus_engine_proxy_g_signal (GDBusProxy *proxy,
+ return;
+ }
+
+- if (g_strcmp0 (signal_name, "DeleteSurroundingText") == 0) {
++ if (!g_strcmp0 (signal_name, "DeleteSurroundingText")) {
+ gint offset_from_cursor = 0;
+ guint nchars = 0;
+ g_variant_get (parameters, "(iu)", &offset_from_cursor, &nchars);
+@@ -642,7 +657,7 @@ bus_engine_proxy_g_signal (GDBusProxy *proxy,
+ return;
+ }
+
+- if (g_strcmp0 (signal_name, "UpdatePreeditText") == 0) {
++ if (!g_strcmp0 (signal_name, "UpdatePreeditText")) {
+ GVariant *arg0 = NULL;
+ guint cursor_pos = 0;
+ gboolean visible = FALSE;
+@@ -664,7 +679,7 @@ bus_engine_proxy_g_signal (GDBusProxy *proxy,
+ return;
+ }
+
+- if (g_strcmp0 (signal_name, "UpdateAuxiliaryText") == 0) {
++ if (!g_strcmp0 (signal_name, "UpdateAuxiliaryText")) {
+ GVariant *arg0 = NULL;
+ gboolean visible = FALSE;
+
+@@ -684,7 +699,7 @@ bus_engine_proxy_g_signal (GDBusProxy *proxy,
+ return;
+ }
+
+- if (g_strcmp0 (signal_name, "UpdateLookupTable") == 0) {
++ if (!g_strcmp0 (signal_name, "UpdateLookupTable")) {
+ GVariant *arg0 = NULL;
+ gboolean visible = FALSE;
+
+@@ -705,7 +720,7 @@ bus_engine_proxy_g_signal (GDBusProxy *proxy,
+ return;
+ }
+
+- if (g_strcmp0 (signal_name, "RegisterProperties") == 0) {
++ if (!g_strcmp0 (signal_name, "RegisterProperties")) {
+ GVariant *arg0 = NULL;
+ g_variant_get (parameters, "(v)", &arg0);
+ g_return_if_fail (arg0 != NULL);
+@@ -723,7 +738,7 @@ bus_engine_proxy_g_signal (GDBusProxy *proxy,
+ return;
+ }
+
+- if (g_strcmp0 (signal_name, "UpdateProperty") == 0) {
++ if (!g_strcmp0 (signal_name, "UpdateProperty")) {
+ GVariant *arg0 = NULL;
+ g_variant_get (parameters, "(v)", &arg0);
+ g_return_if_fail (arg0 != NULL);
+@@ -738,7 +753,7 @@ bus_engine_proxy_g_signal (GDBusProxy *proxy,
+ return;
+ }
+
+- if (g_strcmp0 (signal_name, "PanelExtension") == 0) {
++ if (!g_strcmp0 (signal_name, "PanelExtension")) {
+ GVariant *arg0 = NULL;
+ g_variant_get (parameters, "(v)", &arg0);
+ g_return_if_fail (arg0 != NULL);
+@@ -752,6 +767,11 @@ bus_engine_proxy_g_signal (GDBusProxy *proxy,
+ return;
+ }
+
++ if (!g_strcmp0 (signal_name, "SendMessage")) {
++ g_signal_emit (engine, engine_signals[SEND_MESSAGE], 0, parameters);
++ return;
++ }
++
+ g_return_if_reached ();
+ }
+
+diff --git a/bus/ibusimpl.c b/bus/ibusimpl.c
+index 5c9c52ce..fe521f2d 100644
+--- a/bus/ibusimpl.c
++++ b/bus/ibusimpl.c
+@@ -470,6 +470,19 @@ _panel_forward_process_key_event_cb (BusPanelProxy *panel,
+ modifiers);
+ }
+
++static void
++_panel_send_message_cb (BusPanelProxy *panel,
++ GVariant *parameters,
++ BusIBusImpl *ibus)
++{
++ if (!ibus->panel) {
++ g_warning ("Panel is not running.");
++ return;
++ }
++ g_return_if_fail (BUS_IS_PANEL_PROXY (ibus->panel));
++ bus_panel_proxy_send_message_received (ibus->panel, parameters);
++}
++
+ static void
+ _registry_changed_cb (IBusRegistry *registry,
+ BusIBusImpl *ibus)
+@@ -556,26 +569,29 @@ _dbus_name_owner_changed_cb (BusDBusImpl *dbus,
+ G_CALLBACK (
+ _panel_panel_extension_register_keys_cb),
+ ibus);
+- g_signal_connect (
+- *panel,
+- "update-preedit-text-received",
+- G_CALLBACK (_panel_update_preedit_text_received_cb),
+- ibus);
+- g_signal_connect (
+- *panel,
+- "update-lookup-table-received",
+- G_CALLBACK (_panel_update_lookup_table_received_cb),
+- ibus);
+- g_signal_connect (
+- *panel,
+- "update-auxiliary-text-received",
+- G_CALLBACK (_panel_update_auxiliary_text_received_cb),
+- ibus);
+- g_signal_connect (
+- *panel,
+- "forward-process-key-event",
+- G_CALLBACK (_panel_forward_process_key_event_cb),
+- ibus);
++ g_signal_connect (*panel,
++ "update-preedit-text-received",
++ G_CALLBACK (
++ _panel_update_preedit_text_received_cb),
++ ibus);
++ g_signal_connect (*panel,
++ "update-lookup-table-received",
++ G_CALLBACK (
++ _panel_update_lookup_table_received_cb),
++ ibus);
++ g_signal_connect (*panel,
++ "update-auxiliary-text-received",
++ G_CALLBACK (
++ _panel_update_auxiliary_text_received_cb),
++ ibus);
++ g_signal_connect (*panel,
++ "forward-process-key-event",
++ G_CALLBACK (_panel_forward_process_key_event_cb),
++ ibus);
++ g_signal_connect (*panel,
++ "send-message",
++ G_CALLBACK (_panel_send_message_cb),
++ ibus);
+
+ if (ibus->focused_context != NULL) {
+ context = ibus->focused_context;
+@@ -849,11 +865,20 @@ _context_panel_extension_cb (BusInputContext *context,
+ bus_ibus_impl_set_panel_extension_mode (ibus, event);
+ }
+
++static void
++_context_send_message_cb (BusInputContext *context,
++ GVariant *parameters,
++ BusIBusImpl *ibus)
++{
++ _panel_send_message_cb (ibus->panel, parameters, ibus);
++}
++
+ const static struct {
+ const gchar *name;
+ GCallback callback;
+ } context_signals [] = {
+- { "panel-extension", G_CALLBACK (_context_panel_extension_cb) }
++ { "panel-extension", G_CALLBACK (_context_panel_extension_cb) },
++ { "send-message", G_CALLBACK (_context_send_message_cb) }
+ };
+
+ /**
+diff --git a/bus/inputcontext.c b/bus/inputcontext.c
+index 41b47540..ee4fa99b 100644
+--- a/bus/inputcontext.c
++++ b/bus/inputcontext.c
+@@ -165,6 +165,7 @@ enum {
+ REQUEST_ENGINE,
+ SET_CONTENT_TYPE,
+ PANEL_EXTENSION,
++ SEND_MESSAGE,
+ LAST_SIGNAL,
+ };
+
+@@ -767,6 +768,20 @@ bus_input_context_class_init (BusInputContextClass *class)
+ G_TYPE_FROM_CLASS (class),
+ bus_marshal_VOID__OBJECTv);
+
++ context_signals[SEND_MESSAGE] =
++ g_signal_new (I_("send-message"),
++ G_TYPE_FROM_CLASS (class),
++ G_SIGNAL_RUN_LAST,
++ 0,
++ NULL, NULL,
++ bus_marshal_VOID__VARIANT,
++ G_TYPE_NONE,
++ 1,
++ G_TYPE_VARIANT);
++ g_signal_set_va_marshaller (context_signals[SEND_MESSAGE],
++ G_TYPE_FROM_CLASS (class),
++ bus_marshal_VOID__VARIANTv);
++
+ text_empty = ibus_text_new_from_string ("");
+ g_object_ref_sink (text_empty);
+ lookup_table_empty = ibus_lookup_table_new (9 /* page size */,
+@@ -2789,6 +2804,20 @@ _engine_panel_extension_cb (BusEngineProxy *engine,
+ g_signal_emit (context, context_signals[PANEL_EXTENSION], 0, event);
+ }
+
++/**
++ * _engine_send_message_cb:
++ *
++ * A function to be called when "send-message" glib signal is sent
++ * from the engine object.
++ */
++static void
++_engine_send_message_cb (BusEngineProxy *engine,
++ GVariant *parameters,
++ BusInputContext *context)
++{
++ g_signal_emit (context, context_signals[SEND_MESSAGE], 0, parameters);
++}
++
+ static void
+ _engine_show_preedit_text_cb (BusEngineProxy *engine,
+ BusInputContext *context)
+@@ -2978,6 +3007,7 @@ const static struct {
+ { "register-properties", G_CALLBACK (_engine_register_properties_cb) },
+ { "update-property", G_CALLBACK (_engine_update_property_cb) },
+ { "panel-extension", G_CALLBACK (_engine_panel_extension_cb) },
++ { "send-message", G_CALLBACK (_engine_send_message_cb) },
+ { "destroy", G_CALLBACK (_engine_destroy_cb) }
+ };
+
+@@ -3549,9 +3579,9 @@ bus_input_context_forward_process_key_event (BusInputContext *context,
+ guint keycode,
+ guint modifiers)
+ {
+- ProcessKeyEventData *data = g_slice_new0 (ProcessKeyEventData);
+ g_assert (BUS_IS_INPUT_CONTEXT (context));
+ if (context->has_focus && context->engine && context->fake == FALSE) {
++ ProcessKeyEventData *data = g_slice_new0 (ProcessKeyEventData);
+ data->context = g_object_ref (context);
+ data->keyval = keyval;
+ data->keycode = keycode;
+@@ -3564,7 +3594,5 @@ bus_input_context_forward_process_key_event (BusInputContext *context,
+ (GAsyncReadyCallback)
+ _forward_process_key_event_reply_cb,
+ data);
+- } else {
+- g_slice_free (ProcessKeyEventData, data);
+ }
+ }
+diff --git a/bus/panelproxy.c b/bus/panelproxy.c
+index b76d6f76..997f5964 100644
+--- a/bus/panelproxy.c
++++ b/bus/panelproxy.c
+@@ -2,8 +2,8 @@
+ /* vim:set et sts=4: */
+ /* ibus - The Input Bus
+ * Copyright (C) 2008-2014 Peng Huang <shawn.p.huang@gmail.com>
+- * Copyright (C) 2017-2024 Takao Fujiwara <takao.fujiwara1@gmail.com>
+- * Copyright (C) 2008-2024 Red Hat, Inc.
++ * Copyright (C) 2017-2025 Takao Fujiwara <takao.fujiwara1@gmail.com>
++ * Copyright (C) 2008-2025 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
+@@ -57,6 +57,7 @@ enum {
+ UPDATE_LOOKUP_TABLE_RECEIVED,
+ UPDATE_AUXILIARY_TEXT_RECEIVED,
+ FORWARD_PROCESS_KEY_EVENT,
++ SEND_MESSAGE,
+ LAST_SIGNAL
+ };
+
+@@ -366,6 +367,19 @@ bus_panel_proxy_class_init (BusPanelProxyClass *class)
+ g_signal_set_va_marshaller (panel_signals[FORWARD_PROCESS_KEY_EVENT],
+ G_TYPE_FROM_CLASS (class),
+ bus_marshal_VOID__UINT_UINT_UINTv);
++
++ panel_signals[SEND_MESSAGE] =
++ g_signal_new (I_("send-message"),
++ G_TYPE_FROM_CLASS (class),
++ G_SIGNAL_RUN_LAST,
++ 0,
++ NULL, NULL,
++ bus_marshal_VOID__VARIANT,
++ G_TYPE_NONE, 1,
++ G_TYPE_VARIANT);
++ g_signal_set_va_marshaller (panel_signals[SEND_MESSAGE],
++ G_TYPE_FROM_CLASS (class),
++ bus_marshal_VOID__VARIANTv);
+ }
+
+ static void
+@@ -551,6 +565,11 @@ bus_panel_proxy_g_signal (GDBusProxy *proxy,
+ return;
+ }
+
++ if (g_strcmp0 ("SendMessage", signal_name) == 0) {
++ g_signal_emit (panel, panel_signals[SEND_MESSAGE], 0, parameters);
++ return;
++ }
++
+ /* shound not be reached */
+ g_return_if_reached ();
+ }
+@@ -1170,3 +1189,17 @@ bus_panel_proxy_candidate_clicked_lookup_table (BusPanelProxy *panel,
+ G_DBUS_CALL_FLAGS_NONE,
+ -1, NULL, NULL, NULL);
+ }
++
++void
++bus_panel_proxy_send_message_received (BusPanelProxy *panel,
++ GVariant *parameters)
++{
++ g_assert (BUS_IS_PANEL_PROXY (panel));
++ g_assert (parameters);
++
++ g_dbus_proxy_call ((GDBusProxy *)panel,
++ "SendMessageReceived",
++ g_variant_ref (parameters),
++ G_DBUS_CALL_FLAGS_NONE,
++ -1, NULL, NULL, NULL);
++}
+diff --git a/bus/panelproxy.h b/bus/panelproxy.h
+index 4d8afb98..ff205cf1 100644
+--- a/bus/panelproxy.h
++++ b/bus/panelproxy.h
+@@ -2,8 +2,8 @@
+ /* vim:set et sts=4: */
+ /* ibus - The Input Bus
+ * Copyright (C) 2008-2014 Peng Huang <shawn.p.huang@gmail.com>
+- * Copyright (C) 2017-2018 Takao Fujiwara <takao.fujiwara1@gmail.com>
+- * Copyright (C) 2008-2018 Red Hat, Inc.
++ * Copyright (C) 2017-2025 Takao Fujiwara <takao.fujiwara1@gmail.com>
++ * Copyright (C) 2008-2025 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
+@@ -155,6 +155,9 @@ void bus_panel_proxy_candidate_clicked_lookup_table
+ guint index,
+ guint button,
+ guint state);
++void bus_panel_proxy_send_message_received
++ (BusPanelProxy *panel,
++ GVariant *parameters);
+
+ G_END_DECLS
+ #endif
+diff --git a/src/Makefile.am b/src/Makefile.am
+index bef269cb..519fc64e 100644
+--- a/src/Makefile.am
++++ b/src/Makefile.am
+@@ -3,8 +3,8 @@
+ # ibus - The Input Bus
+ #
+ # Copyright (c) 2007-2015 Peng Huang <shawn.p.huang@gmail.com>
+-# Copyright (c) 2015-2024 Takao Fujiwara <takao.fujiwara1@gmail.com>
+-# Copyright (c) 2007-2017 Red Hat, Inc.
++# Copyright (c) 2015-2025 Takao Fujiwara <takao.fujiwara1@gmail.com>
++# Copyright (c) 2007-2025 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
+@@ -93,6 +93,7 @@ ibus_sources = \
+ ibuskeys.c \
+ ibuskeyuni.c \
+ ibuslookuptable.c \
++ ibusmessage.c \
+ ibusobject.c \
+ ibusobservedpath.c \
+ ibuspanelservice.c \
+@@ -151,6 +152,7 @@ ibus_headers = \
+ ibuskeysyms-compat.h \
+ ibuskeysyms.h \
+ ibuslookuptable.h \
++ ibusmessage.h \
+ ibusobject.h \
+ ibusobservedpath.h \
+ ibuspanelservice.h \
+diff --git a/src/ibusengine.c b/src/ibusengine.c
+index c7a0ac78..4e13b23d 100644
+--- a/src/ibusengine.c
++++ b/src/ibusengine.c
+@@ -3,7 +3,7 @@
+ /* ibus - The Input Bus
+ * Copyright (C) 2008-2013 Peng Huang <shawn.p.huang@gmail.com>
+ * Copyright (C) 2018-2025 Takao Fujiwara <takao.fujiwara1@gmail.com>
+- * Copyright (C) 2008-2021 Red Hat, Inc.
++ * Copyright (C) 2008-2025 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
+@@ -302,6 +302,13 @@ static const gchar introspection_xml[] =
+ " <signal name='PanelExtension'>"
+ " <arg type='v' name='data' />"
+ " </signal>"
++ " <signal name='SendMessage'>"
++ " <arg type='v' name='message' />"
++ " <annotation name='org.gtk.GDBus.Since'\n"
++ " value='1.5.33' />\n"
++ " <annotation name='org.gtk.GDBus.DocString'\n"
++ " value='Stability: Unstable' />\n"
++ " </signal>"
+ /* FIXME properties */
+ " <property name='ContentType' type='(uu)' access='write' />"
+ " <property name='FocusId' type='(b)' access='read' />"
+@@ -1812,12 +1819,17 @@ ibus_engine_emit_signal (IBusEngine *engine,
+ const gchar *signal_name,
+ GVariant *parameters)
+ {
++ GError *error = NULL;
+ ibus_service_emit_signal ((IBusService *)engine,
+ NULL,
+ IBUS_INTERFACE_ENGINE,
+ signal_name,
+ parameters,
+- NULL);
++ &error);
++ if (error) {
++ g_warning ("Failed to emit %s signal: %s", signal_name, error->message);
++ g_error_free (error);
++ }
+ }
+
+ static void
+@@ -2199,3 +2211,20 @@ ibus_engine_get_name (IBusEngine *engine)
+ g_return_val_if_fail (IBUS_IS_ENGINE (engine), NULL);
+ return engine->priv->engine_name;
+ }
++
++void
++ibus_engine_send_message (IBusEngine *engine,
++ IBusMessage *message)
++{
++ GVariant *variant;
++
++ g_return_if_fail (IBUS_IS_ENGINE (engine));
++ g_return_if_fail (IBUS_IS_MESSAGE (message));
++ variant = ibus_serializable_serialize ((IBusSerializable *)message);
++ ibus_engine_emit_signal (engine,
++ "SendMessage",
++ g_variant_new ("(v)", variant));
++ if (g_object_is_floating (message)) {
++ g_object_unref (message);
++ }
++}
+diff --git a/src/ibusengine.h b/src/ibusengine.h
+index 6af0e856..cdc96331 100644
+--- a/src/ibusengine.h
++++ b/src/ibusengine.h
+@@ -2,8 +2,8 @@
+ /* vim:set et sts=4: */
+ /* ibus - The Input Bus
+ * Copyright (C) 2008-2013 Peng Huang <shawn.p.huang@gmail.com>
+- * Copyright (C) 2012-2022 Takao Fujiwara <takao.fujiwara1@gmail.com>
+- * Copyright (C) 2008-2022 Red Hat, Inc.
++ * Copyright (C) 2012-2025 Takao Fujiwara <takao.fujiwara1@gmail.com>
++ * Copyright (C) 2008-2025 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
+@@ -43,6 +43,7 @@
+ #include "ibusservice.h"
+ #include "ibusattribute.h"
+ #include "ibuslookuptable.h"
++#include "ibusmessage.h"
+ #include "ibusproplist.h"
+
+ /*
+@@ -65,6 +66,24 @@
+
+ G_BEGIN_DECLS
+
++/**
++ * IBusEngineMsgCode:
++ * @IBUS_ENGINE_MSG_CODE_GENERAL: Generic message for Engine
++ * @IBUS_ENGINE_MSG_CODE_INVALID_COMPOSE_SEQUENCE: User's typing failure
++ * against the definition of the compose files.
++ *
++ * Message codes in the `IBusMessageDomain` domain for Engine
++ * See also #IBusMessage, ibus_engine_send_message()
++ *
++ * Since: 1.5.33
++ * Stability: Unstable
++ */
++typedef enum
++{
++ IBUS_ENGINE_MSG_CODE_GENERAL,
++ IBUS_ENGINE_MSG_CODE_INVALID_COMPOSE_SEQUENCE
++} IBusEngineMsgCode;
++
+ typedef struct _IBusEngine IBusEngine;
+ typedef struct _IBusEngineClass IBusEngineClass;
+ typedef struct _IBusEnginePrivate IBusEnginePrivate;
+@@ -469,5 +488,18 @@ void ibus_engine_get_content_type (IBusEngine *engine,
+ */
+ const gchar *ibus_engine_get_name (IBusEngine *engine);
+
++/**
++ * ibus_engine_send_message:
++ * @engine: An #IBusEngine.
++ * @message: An #IBusMessage.
++ *
++ * Send a message to the Engine for the focus-less notification popup.
++ * This is used for the user errors in Wayland mainly but in Xorg too.
++ *
++ * Since: 1.5.33
++ * Stability: Unstable
++ */
++void ibus_engine_send_message (IBusEngine *engine,
++ IBusMessage *message);
+ G_END_DECLS
+ #endif
+diff --git a/src/ibusenginesimple.c b/src/ibusenginesimple.c
+index e93c61f5..4e0ba40f 100644
+--- a/src/ibusenginesimple.c
++++ b/src/ibusenginesimple.c
+@@ -3,7 +3,7 @@
+ /* ibus - The Input Bus
+ * Copyright (C) 2014 Peng Huang <shawn.p.huang@gmail.com>
+ * Copyright (C) 2015-2025 Takao Fujiwara <takao.fujiwara1@gmail.com>
+- * Copyright (C) 2014-2017 Red Hat, Inc.
++ * Copyright (C) 2014-2025 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
+@@ -36,6 +36,7 @@
+
+ #include <memory.h>
+ #include <stdlib.h>
++#include <glib/gi18n-lib.h>
+
+ #define IBUS_ENGINE_SIMPLE_GET_PRIVATE(o) \
+ ((IBusEngineSimplePrivate *)ibus_engine_simple_get_instance_private (o))
+@@ -204,6 +205,27 @@ ibus_engine_simple_destroy (IBusEngineSimple *simple)
+ IBUS_OBJECT (simple));
+ }
+
++static void
++ibus_engine_simple_send_message_with_code (IBusEngineSimple *simple,
++ IBusEngineMsgCode code)
++{
++ IBusMessage *message;
++
++ g_return_if_fail (IBUS_IS_ENGINE_SIMPLE (simple));
++ message = ibus_message_new (
++ IBUS_MESSAGE_DOMAIN_ENGINE,
++ code,
++ _("Detect unregistered character in your compose sequence"),
++ _("The character you just input is not recognized as a valid " \
++ "part of the currently active compose sequence and the " \
++ "character was cancelled. Try inputting the correct character " \
++ "to the compose sequence again, or press Escape key to " \
++ "terminate whole the compose sequence."),
++ "timeout", 5,
++ NULL);
++ ibus_engine_send_message (IBUS_ENGINE (simple), message);
++}
++
+ static void
+ ibus_engine_simple_focus_in (IBusEngine *engine)
+ {
+@@ -677,7 +699,9 @@ no_sequence_matches (IBusEngineSimple *simple,
+ priv->compose_buffer[0] = 0;
+ if (n_compose > 1) {
+ /* Invalid sequence */
+- /* FIXME beep_window (event->window); */
++ ibus_engine_simple_send_message_with_code (
++ simple,
++ IBUS_ENGINE_MSG_CODE_INVALID_COMPOSE_SEQUENCE);
+ ibus_engine_simple_update_preedit_text (simple);
+ return TRUE;
+ }
+@@ -948,7 +972,9 @@ ibus_engine_simple_process_key_event (IBusEngine *engine,
+ priv->modifiers_dropped = TRUE;
+ } else {
+ /* invalid hex sequence */
+- /* FIXME beep_window (event->window); */
++ ibus_engine_simple_send_message_with_code (
++ simple,
++ IBUS_ENGINE_MSG_CODE_INVALID_COMPOSE_SEQUENCE);
+ g_string_set_size (priv->tentative_match, 0);
+ g_clear_pointer (&priv->tentative_emoji, g_free);
+ priv->in_hex_sequence = FALSE;
+@@ -971,7 +997,9 @@ ibus_engine_simple_process_key_event (IBusEngine *engine,
+ priv->modifiers_dropped = TRUE;
+ } else {
+ /* invalid hex sequence */
+- /* FIXME beep_window (event->window); */
++ ibus_engine_simple_send_message_with_code (
++ simple,
++ IBUS_ENGINE_MSG_CODE_INVALID_COMPOSE_SEQUENCE);
+ g_string_set_size (priv->tentative_match, 0);
+ g_clear_pointer (&priv->tentative_emoji, g_free);
+ priv->in_hex_sequence = FALSE;
+@@ -1020,7 +1048,9 @@ ibus_engine_simple_process_key_event (IBusEngine *engine,
+ }
+ else if (is_hex_end) {
+ /* invalid hex sequence */
+- /* FIXME beep_window (event->window); */
++ ibus_engine_simple_send_message_with_code (
++ simple,
++ IBUS_ENGINE_MSG_CODE_INVALID_COMPOSE_SEQUENCE);
+ g_string_set_size (priv->tentative_match, 0);
+ g_clear_pointer (&priv->tentative_emoji, g_free);
+ priv->in_hex_sequence = FALSE;
+@@ -1119,7 +1149,9 @@ ibus_engine_simple_process_key_event (IBusEngine *engine,
+ else {
+ /* invalid hex sequence */
+ if (n_compose > 0) {
+- /* FIXME beep_window (event->window); */
++ ibus_engine_simple_send_message_with_code (
++ simple,
++ IBUS_ENGINE_MSG_CODE_INVALID_COMPOSE_SEQUENCE);
+ g_string_set_size (priv->tentative_match, 0);
+ priv->in_hex_sequence = FALSE;
+ priv->compose_buffer[0] = 0;
+@@ -1184,7 +1216,9 @@ ibus_engine_simple_process_key_event (IBusEngine *engine,
+ return TRUE;
+ } else if (!is_hex_end) {
+ /* non-hex character in hex sequence */
+- /* FIXME beep_window (event->window); */
++ ibus_engine_simple_send_message_with_code (
++ simple,
++ IBUS_ENGINE_MSG_CODE_INVALID_COMPOSE_SEQUENCE);
+ return TRUE;
+ }
+ } else if (priv->in_emoji_sequence) {
+@@ -1243,15 +1277,18 @@ ibus_engine_simple_process_key_event (IBusEngine *engine,
+ return TRUE;
+ } else {
+ /* invalid hex sequence */
+- /* FIXME beep_window (event->window); */
++ ibus_engine_simple_send_message_with_code (
++ simple,
++ IBUS_ENGINE_MSG_CODE_INVALID_COMPOSE_SEQUENCE);
+ g_string_set_size (priv->tentative_match, 0);
+ priv->in_hex_sequence = FALSE;
+ priv->compose_buffer[0] = 0;
+ }
++ } else if (!check_hex (simple, n_compose)) {
++ ibus_engine_simple_send_message_with_code (
++ simple,
++ IBUS_ENGINE_MSG_CODE_INVALID_COMPOSE_SEQUENCE);
+ }
+- else if (!check_hex (simple, n_compose))
+- /* FIXME beep_window (event->window); */
+- ;
+ ibus_engine_simple_update_preedit_text (simple);
+
+ return TRUE;
+@@ -1343,7 +1380,9 @@ ibus_engine_simple_process_key_event (IBusEngine *engine,
+
+ n_compose = n_compose_prev;
+ g_assert (n_compose < (COMPOSE_BUFFER_SIZE + 1));
+- /* FIXME beep_window (event->window); */
++ ibus_engine_simple_send_message_with_code (
++ simple,
++ IBUS_ENGINE_MSG_CODE_INVALID_COMPOSE_SEQUENCE);
+ backup_char = priv->compose_buffer[n_compose];
+ priv->compose_buffer[n_compose] = 0;
+ if (ibus_engine_simple_check_all_compose_table (simple, n_compose))
+diff --git a/src/ibusmessage.c b/src/ibusmessage.c
+new file mode 100644
+index 00000000..89e31771
+--- /dev/null
++++ b/src/ibusmessage.c
+@@ -0,0 +1,562 @@
++/* -*- mode: C; c-basic-offset: 4; indent-tabs-mode: nil; -*- */
++/* vim:set et sts=4: */
++/* bus - The Input Bus
++ * Copyright (C) 2025 Takao Fujiwara <takao.fujiwara1@gmail.com>
++ * Copyright (C) 2025 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
++ */
++#include "ibusmessage.h"
++#include "ibusinternal.h"
++
++enum {
++ LAST_SIGNAL,
++};
++
++enum {
++ PROP_0 = 0,
++ PROP_DOMAIN,
++ PROP_CODE,
++ PROP_TITLE,
++ PROP_DESCRIPTION,
++ PROP_TIMEOUT,
++ PROP_PROGRESS,
++ PROP_SERIAL
++};
++
++
++/* IBusMessagePriv */
++struct _IBusMessagePrivate {
++ guchar domain;
++ guchar code;
++ gchar *title;
++ gchar *description;
++ int timeout;
++ int progress;
++ guint serial;
++};
++
++
++#define ibus_message_get_const_instance_private(self) \
++ G_STRUCT_MEMBER_P ((self), IBusMessage_private_offset)
++
++#define IBUS_MESSAGE_GET_PRIVATE(o) \
++ ((IBusMessagePrivate *)ibus_message_get_instance_private (o))
++#define IBUS_MESSAGE_GET_CONST_PRIVATE(o) \
++ ((const IBusMessagePrivate *)ibus_message_get_const_instance_private (o))
++
++#define DEFAULT_TIMEOUT -1
++#define DEFAULT_PROGRESS -1
++
++// static guint _signals[LAST_SIGNAL] = { 0 };
++
++/* functions prototype */
++static void ibus_message_set_property (IBusMessage *msg,
++ guint prop_id,
++ const GValue *value,
++ GParamSpec *pspec);
++static void ibus_message_get_property (IBusMessage *msg,
++ guint prop_id,
++ GValue *value,
++ GParamSpec *pspec);
++static void ibus_message_destroy (IBusMessage *msg);
++static gboolean ibus_message_serialize (IBusMessage *msg,
++ GVariantBuilder
++ *builder);
++static gint ibus_message_deserialize (IBusMessage *msg,
++ GVariant
++ *variant);
++static gboolean ibus_message_copy (IBusMessage *dest,
++ const IBusMessage *src);
++
++G_DEFINE_TYPE_WITH_PRIVATE (IBusMessage,
++ ibus_message,
++ IBUS_TYPE_SERIALIZABLE)
++
++
++static void
++ibus_message_class_init (IBusMessageClass *class)
++{
++ GObjectClass *gobject_class = G_OBJECT_CLASS (class);
++ IBusObjectClass *object_class = IBUS_OBJECT_CLASS (class);
++ IBusSerializableClass *serializable_class = IBUS_SERIALIZABLE_CLASS (class);
++
++ gobject_class->set_property =
++ (GObjectSetPropertyFunc)ibus_message_set_property;
++ gobject_class->get_property =
++ (GObjectGetPropertyFunc)ibus_message_get_property;
++ object_class->destroy = (IBusObjectDestroyFunc)ibus_message_destroy;
++
++ serializable_class->serialize =
++ (IBusSerializableSerializeFunc)ibus_message_serialize;
++ serializable_class->deserialize =
++ (IBusSerializableDeserializeFunc)ibus_message_deserialize;
++ serializable_class->copy =
++ (IBusSerializableCopyFunc)ibus_message_copy;
++
++ /* install properties */
++ /**
++ * IBusMessage:domain:
++ *
++ * The domain of message
++ */
++ g_object_class_install_property (gobject_class,
++ PROP_DOMAIN,
++ g_param_spec_uchar ("domain",
++ "message domain",
++ "The domain of message",
++ 0,
++ G_MAXUINT8,
++ 0,
++ G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY));
++
++ /**
++ * IBusMessage:code:
++ *
++ * The code of message
++ */
++ g_object_class_install_property (gobject_class,
++ PROP_CODE,
++ g_param_spec_uchar ("code",
++ "message code",
++ "The code of message",
++ 0,
++ G_MAXUINT8,
++ 0,
++ G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY));
++
++ /**
++ * IBusMessage:title:
++ *
++ * The title of message
++ */
++ g_object_class_install_property (gobject_class,
++ PROP_TITLE,
++ g_param_spec_string ("title",
++ "message title",
++ "The title of message",
++ NULL,
++ G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY));
++
++ /**
++ * IBusMessage:description:
++ *
++ * The description of message
++ */
++ g_object_class_install_property (gobject_class,
++ PROP_DESCRIPTION,
++ g_param_spec_string ("description",
++ "message description",
++ "The description of message",
++ "",
++ G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY));
++
++ /**
++ * IBusMessage:timeout:
++ *
++ * The timeout of message
++ */
++ g_object_class_install_property (gobject_class,
++ PROP_TIMEOUT,
++ g_param_spec_int ("timeout",
++ "message timeout",
++ "The timeout of message",
++ G_MININT,
++ G_MAXINT,
++ DEFAULT_TIMEOUT,
++ G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY));
++
++ /**
++ * IBusMessage:progress:
++ *
++ * The progress of message
++ */
++ g_object_class_install_property (gobject_class,
++ PROP_PROGRESS,
++ g_param_spec_int ("progress",
++ "message progress",
++ "The progress of message",
++ G_MININT,
++ G_MAXINT,
++ DEFAULT_PROGRESS,
++ G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY));
++
++ /**
++ * IBusMessage:serial:
++ *
++ * The serial of message
++ */
++ g_object_class_install_property (gobject_class,
++ PROP_SERIAL,
++ g_param_spec_uint ("serial",
++ "message serial",
++ "The serial of message",
++ 0,
++ G_MAXUINT,
++ 0,
++ G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY));
++}
++
++
++static void
++ibus_message_init (IBusMessage *msg)
++{
++ IBusMessagePrivate *priv = IBUS_MESSAGE_GET_PRIVATE (msg);
++ priv->timeout = DEFAULT_TIMEOUT;
++ priv->progress = DEFAULT_PROGRESS;
++}
++
++
++static void
++ibus_message_destroy (IBusMessage *msg)
++{
++ IBusMessagePrivate *priv = IBUS_MESSAGE_GET_PRIVATE (msg);
++ g_free (priv->title);
++ g_free (priv->description);
++
++ IBUS_OBJECT_CLASS (ibus_message_parent_class)->destroy (IBUS_OBJECT (msg));
++}
++
++
++static void
++ibus_message_set_property (IBusMessage *msg,
++ guint prop_id,
++ const GValue *value,
++ GParamSpec *pspec)
++{
++ IBusMessagePrivate *priv;
++
++ g_return_if_fail (IBUS_IS_MESSAGE (msg));
++ priv = IBUS_MESSAGE_GET_PRIVATE (msg);
++ switch (prop_id) {
++ case PROP_DOMAIN:
++ g_assert (priv->domain == 0);
++ priv->domain = g_value_get_uchar (value);
++ break;
++ case PROP_CODE:
++ g_assert (priv->code == 0);
++ priv->code = g_value_get_uchar (value);
++ break;
++ case PROP_TITLE:
++ g_assert (priv->title == NULL);
++ priv->title = g_value_dup_string (value);
++ break;
++ case PROP_DESCRIPTION:
++ g_assert (priv->description == NULL);
++ priv->description = g_value_dup_string (value);
++ break;
++ case PROP_TIMEOUT:
++ g_assert (priv->timeout == DEFAULT_TIMEOUT);
++ priv->timeout = g_value_get_int (value);
++ break;
++ case PROP_PROGRESS:
++ g_assert (priv->progress == DEFAULT_PROGRESS);
++ priv->progress = g_value_get_int (value);
++ break;
++ case PROP_SERIAL:
++ g_assert (priv->serial == 0);
++ priv->serial = g_value_get_uint (value);
++ break;
++ default:
++ G_OBJECT_WARN_INVALID_PROPERTY_ID (msg, prop_id, pspec);
++ }
++}
++
++
++static void
++ibus_message_get_property (IBusMessage *msg,
++ guint prop_id,
++ GValue *value,
++ GParamSpec *pspec)
++{
++ switch (prop_id) {
++ case PROP_DOMAIN: {
++ int domain = ibus_message_get_domain (msg);
++ g_assert (domain > 0 && domain <= G_MAXUINT8);
++ g_value_set_uchar (value, (guchar)domain);
++ }
++ break;
++ case PROP_CODE: {
++ int code = ibus_message_get_code (msg);
++ g_assert (code >= 0 && code <= G_MAXUINT8);
++ g_value_set_uchar (value, (guchar)code);
++ }
++ break;
++ case PROP_TITLE:
++ g_value_set_string (value, ibus_message_get_title (msg));
++ break;
++ case PROP_DESCRIPTION:
++ g_value_set_string (value, ibus_message_get_description (msg));
++ break;
++ case PROP_TIMEOUT:
++ g_value_set_int (value, ibus_message_get_timeout (msg));
++ break;
++ case PROP_PROGRESS:
++ g_value_set_int (value, ibus_message_get_progress (msg));
++ break;
++ case PROP_SERIAL:
++ g_value_set_uint (value, ibus_message_get_serial (msg));
++ break;
++ default:
++ G_OBJECT_WARN_INVALID_PROPERTY_ID (msg, prop_id, pspec);
++ }
++}
++
++
++static gboolean
++ibus_message_serialize (IBusMessage *msg,
++ GVariantBuilder *builder)
++{
++ gboolean retval;
++ IBusMessagePrivate *priv;
++
++ retval = IBUS_SERIALIZABLE_CLASS (ibus_message_parent_class)->
++ serialize ((IBusSerializable *)msg, builder);
++ g_return_val_if_fail (retval, FALSE);
++ /* End dict iter */
++
++ g_return_val_if_fail (IBUS_IS_MESSAGE (msg), FALSE);
++ priv = IBUS_MESSAGE_GET_PRIVATE (msg);
++
++#define NOTNULL(s) ((s) != NULL ? (s) : "")
++ /* If you will add a new property, you can append it at the end and
++ * you should not change the serialized order of domain, code,
++ * title, .... */
++ g_variant_builder_add (builder, "y", priv->domain);
++ g_variant_builder_add (builder, "y", priv->code);
++ g_variant_builder_add (builder, "s", NOTNULL (priv->title));
++ g_variant_builder_add (builder, "s", NOTNULL (priv->description));
++ g_variant_builder_add (builder, "i", priv->timeout);
++ g_variant_builder_add (builder, "i", priv->progress);
++ g_variant_builder_add (builder, "u", priv->serial);
++ /* The serialized order should be kept. */
++#undef NOTNULL
++
++ return TRUE;
++}
++
++
++static gint
++ibus_message_deserialize (IBusMessage *msg,
++ GVariant *variant)
++{
++ gint retval;
++ IBusMessagePrivate *priv;
++
++ retval = IBUS_SERIALIZABLE_CLASS (ibus_message_parent_class)->
++ deserialize ((IBusSerializable *)msg, variant);
++ g_return_val_if_fail (retval, 0);
++
++ g_return_val_if_fail (IBUS_IS_MESSAGE (msg), retval);
++ priv = IBUS_MESSAGE_GET_PRIVATE (msg);
++
++ /* If you will add a new property, you can append it at the end and
++ * you should not change the serialized order of domain, code,
++ * title, .... */
++ g_variant_get_child (variant, retval++, "y", &priv->domain);
++ g_variant_get_child (variant, retval++, "y", &priv->code);
++ ibus_g_variant_get_child_string (variant, retval++,
++ &priv->title);
++ ibus_g_variant_get_child_string (variant, retval++,
++ &priv->description);
++ g_variant_get_child (variant, retval++, "i", &priv->timeout);
++ g_variant_get_child (variant, retval++, "i", &priv->progress);
++ g_variant_get_child (variant, retval++, "u", &priv->serial);
++ /* The serialized order should be kept. */
++ return retval;
++}
++
++
++static gboolean
++ibus_message_copy (IBusMessage *dest,
++ const IBusMessage *src)
++{
++ gboolean retval;
++ IBusMessagePrivate *priv_dest;
++ const IBusMessagePrivate *priv_src;
++
++ retval = IBUS_SERIALIZABLE_CLASS (ibus_message_parent_class)->
++ copy ((IBusSerializable *)dest, (IBusSerializable *)src);
++ g_return_val_if_fail (retval, FALSE);
++
++ g_return_val_if_fail (IBUS_IS_MESSAGE (dest), FALSE);
++ g_return_val_if_fail (IBUS_IS_MESSAGE (src), FALSE);
++ priv_dest = IBUS_MESSAGE_GET_PRIVATE (dest);
++ priv_src = IBUS_MESSAGE_GET_CONST_PRIVATE (src);
++
++ priv_dest->domain = priv_src->domain;
++ priv_dest->code = priv_src->code;
++ priv_dest->title = g_strdup (priv_src->title);
++ priv_dest->description = g_strdup (priv_src->description);
++ priv_dest->timeout = priv_src->timeout;
++ priv_dest->progress = priv_src->progress;
++ priv_dest->serial = priv_src->serial;
++ return TRUE;
++}
++
++
++guint
++ibus_message_get_domain (IBusMessage *msg)
++{
++ IBusMessagePrivate *priv;
++ g_return_val_if_fail (IBUS_IS_MESSAGE (msg), 0);
++ priv = IBUS_MESSAGE_GET_PRIVATE (msg);
++ return (guint)priv->domain;
++}
++
++
++guint
++ibus_message_get_code (IBusMessage *msg)
++{
++ IBusMessagePrivate *priv;
++ g_return_val_if_fail (IBUS_IS_MESSAGE (msg), 0);
++ priv = IBUS_MESSAGE_GET_PRIVATE (msg);
++ return (guint)priv->code;
++}
++
++
++#define IBUS_MESSAGE_GET_PROPERTY(property, return_type, defval) \
++return_type \
++ibus_message_get_ ## property (IBusMessage *msg) \
++{ \
++ IBusMessagePrivate *priv; \
++ g_return_val_if_fail (IBUS_IS_MESSAGE (msg), (defval)); \
++ priv = IBUS_MESSAGE_GET_PRIVATE (msg); \
++ return priv->property; \
++}
++
++IBUS_MESSAGE_GET_PROPERTY (title, const gchar *, NULL)
++IBUS_MESSAGE_GET_PROPERTY (description, const gchar *, NULL)
++IBUS_MESSAGE_GET_PROPERTY (timeout, int, DEFAULT_TIMEOUT)
++IBUS_MESSAGE_GET_PROPERTY (progress, int, DEFAULT_PROGRESS)
++IBUS_MESSAGE_GET_PROPERTY (serial, guint32, 0)
++#undef IBUS_MESSAGE_GET_PROPERTY
++
++
++IBusMessage *
++ibus_message_new (guint domain,
++ guint code,
++ const gchar *title,
++ const gchar *description,
++ ...)
++{
++ va_list var_args;
++ gchar *prop;
++ gchar **names, **names_tmp;
++ GValue *values, *values_tmp;
++ guint n_properties = 4, i;
++ IBusMessage *msg;
++ IBusMessagePrivate *priv;
++
++ g_return_val_if_fail (domain > 0 && domain <= G_MAXUINT8, NULL);
++ g_return_val_if_fail (code <= G_MAXUINT8, NULL);
++ g_return_val_if_fail (description, NULL);
++ g_return_val_if_fail (*description != '\0', NULL);
++
++ if (!(names = g_new0 (gchar *, n_properties))) {
++ g_warning ("allocation error in %s", G_STRFUNC);
++ return NULL;
++ }
++ if (!(values = g_new0 (GValue, n_properties))) {
++ g_warning ("allocation error in %s", G_STRFUNC);
++ return NULL;
++ }
++
++ names[0] = "domain";
++ g_value_init (&values[0], G_TYPE_UCHAR);
++ g_value_set_uchar (&values[0], domain);
++ names[1] = "code";
++ g_value_init (&values[1], G_TYPE_UCHAR);
++ g_value_set_uchar (&values[1], code);
++ names[2] = "title";
++ g_value_init (&values[2], G_TYPE_STRING);
++ g_value_set_string (&values[2], title);
++ names[3] = "description";
++ g_value_init (&values[3], G_TYPE_STRING);
++ g_value_set_string (&values[3], description);
++
++#define IF_PROPERTY_IS_MATCHED(property, gtype, ctype) \
++ if (!g_strcmp0 (prop, #property)) { \
++ if (!(names_tmp = g_renew (gchar *, names, n_properties + 1))) { \
++ g_warning ("allocation error in %s", G_STRFUNC); \
++ va_end (var_args); \
++ g_free (names); \
++ for (i = 0; i <= n_properties; i++) \
++ g_value_unset (&values[i]); \
++ g_free (values); \
++ return NULL; \
++ } \
++ names = names_tmp; \
++ names[n_properties] = prop; \
++ if (!(values_tmp = g_renew (GValue, values, n_properties + 1))) { \
++ g_warning ("allocation error in %s", G_STRFUNC); \
++ va_end (var_args); \
++ g_free (names); \
++ for (i = 0; i < n_properties; i++) \
++ g_value_unset (&values[i]); \
++ g_free (values); \
++ return NULL; \
++ } \
++ values = values_tmp; \
++ memset (&values[n_properties], 0, sizeof (GValue)); \
++ g_value_init (&values[n_properties], (gtype)); \
++ g_value_set_## ctype (&values[n_properties], \
++ va_arg (var_args, g ## ctype)); \
++ }
++
++ va_start (var_args, description);
++ while ((prop = va_arg (var_args, gchar *))) {
++ IF_PROPERTY_IS_MATCHED (timeout, G_TYPE_INT, int)
++ else IF_PROPERTY_IS_MATCHED (progress, G_TYPE_INT, int)
++ else IF_PROPERTY_IS_MATCHED (serial, G_TYPE_UINT, uint)
++ else {
++ g_warning ("wrong parameter %s in %s", prop, G_STRFUNC);
++ for (i = 0; i < n_properties; i++)
++ g_value_unset (&values[i]);
++ g_free (values);
++ g_free (names);
++ va_end (var_args);
++ return NULL;
++ }
++ n_properties++;
++ }
++ va_end (var_args);
++
++#undef IF_PROPERTY_IS_MATCHED
++
++ msg = (IBusMessage *)g_object_new_with_properties (IBUS_TYPE_MESSAGE,
++ n_properties,
++ (const gchar **)names,
++ (const GValue*)values);
++
++
++ for (i = 0; i < n_properties; i++)
++ g_value_unset (&values[i]);
++ g_free (values);
++ g_free (names);
++ if (!IBUS_IS_MESSAGE (msg)) {
++ g_warning ("msg is not IBusMessage in %s", G_STRFUNC);
++ return NULL;
++ }
++ priv = IBUS_MESSAGE_GET_PRIVATE (msg);
++ /* name is required. Other properties are set in class_init by default. */
++ g_assert (priv->domain > 0 && priv->domain <= G_MAXUINT8);
++ g_assert (priv->code <= G_MAXUINT8);
++ g_assert (priv->description);
++ g_assert (*(priv->description) != '\0');
++
++ return msg;
++}
+diff --git a/src/ibusmessage.h b/src/ibusmessage.h
+new file mode 100644
+index 00000000..0ab58b80
+--- /dev/null
++++ b/src/ibusmessage.h
+@@ -0,0 +1,205 @@
++/* -*- mode: C; c-basic-offset: 4; indent-tabs-mode: nil; -*- */
++/* vim:set et sts=4: */
++/* bus - The Input Bus
++ * Copyright (C) 2025 Takao Fujiwara <takao.fujiwara1@gmail.com>
++ * Copyright (C) 2025 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
++ */
++
++#if !defined (__IBUS_H_INSIDE__) && !defined (IBUS_COMPILATION)
++#error "Only <ibus.h> can be included directly"
++#endif
++
++#ifndef __IBUS_MESSAGE_H_
++#define __IBUS_MESSAGE_H_
++
++/**
++ * SECTION: ibusmessage
++ * @short_description: Message data which is forwarded to IBus Panel.
++ * @title: IBusMessage
++ * @stability: Unstable
++ *
++ * An IBusMessage stores the message type, description, timeout.
++ * The message data can generated by ibus_message_new(),
++ *
++ * see_also: #IBusEngine, #IBuPanels
++ *
++ */
++
++#include "ibusserializable.h"
++
++/*
++ * Type macros.
++ */
++
++/* define GOBJECT macros */
++#define IBUS_TYPE_MESSAGE \
++ (ibus_message_get_type ())
++#define IBUS_MESSAGE(obj) \
++ (G_TYPE_CHECK_INSTANCE_CAST ((obj), IBUS_TYPE_MESSAGE, IBusMessage))
++#define IBUS_MESSAGE_CLASS(klass) \
++ (G_TYPE_CHECK_CLASS_CAST ((klass), IBUS_TYPE_MESSAGE, IBusMessageClass))
++#define IBUS_IS_MESSAGE(obj) \
++ (G_TYPE_CHECK_INSTANCE_TYPE ((obj), IBUS_TYPE_MESSAGE))
++#define IBUS_IS_MESSAGE_CLASS(klass) \
++ (G_TYPE_CHECK_CLASS_TYPE ((klass), IBUS_TYPE_MESSAGE))
++#define IBUS_MESSAGE_GET_CLASS(obj) \
++ (G_TYPE_INSTANCE_GET_CLASS ((obj), IBUS_TYPE_MESSAGE, IBusMessageClass))
++
++G_BEGIN_DECLS
++
++typedef struct _IBusMessage IBusMessage;
++typedef struct _IBusMessagePrivate IBusMessagePrivate;
++typedef struct _IBusMessageClass IBusMessageClass;
++
++/**
++ * IBusMessage:
++ *
++ * Message data which is forwarded to IBus Panel.
++ * You can get values with g_object_get_properties.
++ *
++ * Since: 1.5.33
++ */
++struct _IBusMessage {
++ IBusSerializable parent;
++ /* instance members */
++
++ /*< public >*/
++ /*< private >*/
++ IBusMessagePrivate *priv;
++};
++
++struct _IBusMessageClass {
++ IBusSerializableClass parent;
++ /* class members */
++
++ /*< private >*/
++ /* padding */
++ gpointer pdummy[5]; // We can add 5 pointers without breaking the ABI.
++};
++
++GType ibus_message_get_type (void);
++
++/**
++ * ibus_message_new:
++ * @domain: the message domain with @IBusMessageDomain likes #GError.
++ * @code: the message code with @IBusPanelServiceMsgCode likes #GError.
++ * @title: the message title.
++ * @description: the message description.
++ * @...: the NULL-terminated arguments of the properties and values.
++ *
++ * Creates a new #IBusMessage.
++ * You can give additional parameters to the API. E.g.
++ * ibus_message_new(IBUS_MESSAGE_DOMAIN_PANEL,
++ * IBUS_PANEL_SERVICE_MSG_CODE_LOADING_UNICODE, "IBus Emoji utility",
++ * "Loading a Unicode dictionary", "timeout", 10, "progress", 30, NULL)
++ *
++ * Returns: A newly allocated IBusMessage.
++ *
++ * Since: 1.5.33
++ */
++IBusMessage *ibus_message_new (guint domain,
++ guint code,
++ const gchar *title,
++ const gchar *description,
++ ...) G_GNUC_NULL_TERMINATED;
++
++/**
++ * ibus_message_get_domain:
++ * @msg: An #IBusMessage.
++ *
++ * Gets the domain property in #IBusMessage.
++ *
++ * Returns: domain property in #IBusMessage
++ *
++ * Since: 1.5.33
++ */
++guint ibus_message_get_domain (IBusMessage *msg);
++
++/**
++ * ibus_message_get_code:
++ * @msg: An #IBusMessage.
++ *
++ * Gets the code property in #IBusMessage.
++ *
++ * Returns: code property in #IBusMessage
++ *
++ * Since: 1.5.33
++ */
++guint ibus_message_get_code (IBusMessage *msg);
++
++/**
++ * ibus_message_get_title:
++ * @msg: An #IBuMessages.
++ *
++ * Gets the title property in #IBusMessage. It should not be freed.
++ *
++ * Returns: title property in #IBuaMessage
++ *
++ * Since: 1.5.33
++ */
++const gchar *ibus_message_get_title (IBusMessage *msg);
++
++/**
++ * ibus_message_get_description:
++ * @msg: An #IBuMessages.
++ *
++ * Gets the description property in #IBusMessage. It should not be freed.
++ *
++ * Returns: description property in #IBuaMessage
++ *
++ * Since: 1.5.33
++ */
++const gchar *ibus_message_get_description (IBusMessage *msg);
++
++/**
++ * ibus_message_get_timeout:
++ * @msg: An #IBusMessage.
++ *
++ * Gets the timeout property in #IBusMessage.
++ *
++ * Returns: timeout property in #IBusMessage
++ *
++ * Since: 1.5.33
++ */
++int ibus_message_get_timeout (IBusMessage *msg);
++
++/**
++ * ibus_message_get_progress:
++ * @msg: An #IBusMessage.
++ *
++ * Gets the progress property in #IBusMessage.
++ *
++ * Returns: progress property in #IBusMessage
++ *
++ * Since: 1.5.33
++ */
++int ibus_message_get_progress (IBusMessage *msg);
++
++/**
++ * ibus_message_get_serial:
++ * @msg: An #IBusMessage.
++ *
++ * Gets the serial property in #IBusMessage.
++ *
++ * Returns: serial property in #IBusMessage
++ *
++ * Since: 1.5.33
++ */
++guint ibus_message_get_serial (IBusMessage *msg);
++G_END_DECLS
++#endif
+diff --git a/src/ibuspanelservice.c b/src/ibuspanelservice.c
+index 80637ca2..9e3939d0 100644
+--- a/src/ibuspanelservice.c
++++ b/src/ibuspanelservice.c
+@@ -3,7 +3,7 @@
+ /* ibus - The Input Bus
+ * Copyright (c) 2009-2014 Google Inc. All rights reserved.
+ * Copyright (C) 2010-2014 Peng Huang <shawn.p.huang@gmail.com>
+- * Copyright (C) 2017-2024 Takao Fujiwara <takao.fujiwara1@gmail.com>
++ * Copyright (C) 2017-2025 Takao Fujiwara <takao.fujiwara1@gmail.com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+@@ -60,6 +60,7 @@ enum {
+ PROCESS_KEY_EVENT,
+ COMMIT_TEXT_RECEIVED,
+ CANDIDATE_CLICKED_LOOKUP_TABLE,
++ SEND_MESSAGE_RECEIVED,
+ LAST_SIGNAL,
+ };
+
+@@ -240,6 +241,13 @@ static const gchar introspection_xml[] =
+ " <method name='CommitTextReceived'>"
+ " <arg direction='in' type='v' name='text' />"
+ " </method>"
++ " <method name='SendMessageReceived'>"
++ " <arg direction='in' type='v' name='message' />"
++ " <annotation name='org.gtk.GDBus.Since'\n"
++ " value='1.5.33' />\n"
++ " <annotation name='org.gtk.GDBus.DocString'\n"
++ " value='Stability: Unstable' />\n"
++ " </method>"
+ /* Signals */
+ " <signal name='CursorUp' />"
+ " <signal name='CursorDown' />"
+@@ -266,9 +274,9 @@ static const gchar introspection_xml[] =
+ " <signal name='PanelExtension'>"
+ " <arg type='v' name='event' />"
+ " </signal>"
+- " <method name='PanelExtensionRegisterKeys'>"
++ " <signal name='PanelExtensionRegisterKeys'>"
+ " <arg type='v' name='data' />"
+- " </method>"
++ " </signal>"
+ " <signal name='UpdatePreeditTextReceived'>"
+ " <arg type='v' name='text' />"
+ " <arg type='u' name='cursor_pos' />"
+@@ -286,6 +294,17 @@ static const gchar introspection_xml[] =
+ " <arg type='u' name='keyval' />"
+ " <arg type='u' name='keycode' />"
+ " <arg type='u' name='state' />"
++ " <annotation name='org.gtk.GDBus.Since'\n"
++ " value='1.5.32' />\n"
++ " <annotation name='org.gtk.GDBus.DocString'\n"
++ " value='Stability: Unstable' />\n"
++ " </signal>"
++ " <signal name='SendMessage'>"
++ " <arg type='v' name='message' />"
++ " <annotation name='org.gtk.GDBus.Since'\n"
++ " value='1.5.33' />\n"
++ " <annotation name='org.gtk.GDBus.DocString'\n"
++ " value='Stability: Unstable' />\n"
+ " </signal>"
+ " </interface>"
+ "</node>";
+@@ -1006,7 +1025,7 @@ ibus_panel_service_class_init (IBusPanelServiceClass *class)
+ /**
+ * IBusPanelService::commit-text-received:
+ * @panel: An #IBusPanelService
+- * @text: A #IBusText
++ * @text: An #IBusText
+ *
+ * Emitted when the client application get the ::commit-text-received.
+ * Implement the member function
+@@ -1027,6 +1046,20 @@ ibus_panel_service_class_init (IBusPanelServiceClass *class)
+ 1,
+ IBUS_TYPE_TEXT);
+
++ /**
++ * IBusPanelService::candidate-clicked-lookup-table:
++ * @panel: An #IBusPanelService
++ * @text: An #IBusText
++ *
++ * Emitted when the client application get the
++ * ::candidate-clicked-lookup-table.
++ * Implement the member function
++ * IBusPanelServiceClass::candidate_cllicked_lookup_table in extended class
++ * to receive this signal.
++ *
++ * <note><para>Argument @user_data is ignored in this function.</para>
++ * </note>
++ */
+ panel_signals[CANDIDATE_CLICKED_LOOKUP_TABLE] =
+ g_signal_new (I_("candidate-clicked-lookup-table"),
+ G_TYPE_FROM_CLASS (gobject_class),
+@@ -1040,6 +1073,31 @@ ibus_panel_service_class_init (IBusPanelServiceClass *class)
+ G_TYPE_UINT,
+ G_TYPE_UINT,
+ G_TYPE_UINT);
++
++ /**
++ * IBusPanelService::send-message-received:
++ * @panel: An #IBusPanelService
++ * @message: An #IBusMessage
++ *
++ * Emitted when the client application get the ::send-meeeage-received.
++ * Implement the member function
++ *
++ * <note><para>Argument @user_data is ignored in this function.</para>
++ * </note>
++ *
++ * Since: 1.5.33
++ * Stability: Unstable
++ */
++ panel_signals[SEND_MESSAGE_RECEIVED] =
++ g_signal_new (I_("send-message-received"),
++ G_TYPE_FROM_CLASS (gobject_class),
++ G_SIGNAL_RUN_LAST,
++ 0,
++ NULL, NULL,
++ _ibus_marshal_VOID__OBJECT,
++ G_TYPE_NONE,
++ 1,
++ IBUS_TYPE_MESSAGE);
+ }
+
+ static void
+@@ -1310,6 +1368,26 @@ ibus_panel_service_service_method_call (IBusService *service,
+ index, button, state);
+ return;
+ }
++ if (g_strcmp0 (method_name, "SendMessageReceived") == 0) {
++ GVariant *arg0 = NULL;
++ IBusMessage *message = NULL;
++ g_variant_get (parameters, "(v)", &arg0);
++ if (arg0) {
++ message = (IBusMessage *)ibus_serializable_deserialize (arg0);
++ g_variant_unref (arg0);
++ }
++ if (!message) {
++ g_dbus_method_invocation_return_error (
++ invocation,
++ G_DBUS_ERROR,
++ G_DBUS_ERROR_FAILED,
++ "SendMessageReceived method gives NULL");
++ return;
++ }
++ g_signal_emit (panel, panel_signals[SEND_MESSAGE_RECEIVED], 0, message);
++ _g_object_unref_if_floating (message);
++ return;
++ }
+
+
+ const static struct {
+@@ -1510,7 +1588,7 @@ ibus_panel_service_candidate_clicked (IBusPanelService *panel,
+ guint state)
+ {
+ g_return_if_fail (IBUS_IS_PANEL_SERVICE (panel));
+- ibus_service_emit_signal ((IBusService *) panel,
++ ibus_service_emit_signal ((IBusService *)panel,
+ NULL,
+ IBUS_INTERFACE_PANEL,
+ "CandidateClicked",
+@@ -1524,7 +1602,7 @@ ibus_panel_service_property_activate (IBusPanelService *panel,
+ guint prop_state)
+ {
+ g_return_if_fail (IBUS_IS_PANEL_SERVICE (panel));
+- ibus_service_emit_signal ((IBusService *) panel,
++ ibus_service_emit_signal ((IBusService *)panel,
+ NULL,
+ IBUS_INTERFACE_PANEL,
+ "PropertyActivate",
+@@ -1537,7 +1615,7 @@ ibus_panel_service_property_show (IBusPanelService *panel,
+ const gchar *prop_name)
+ {
+ g_return_if_fail (IBUS_IS_PANEL_SERVICE (panel));
+- ibus_service_emit_signal ((IBusService *) panel,
++ ibus_service_emit_signal ((IBusService *)panel,
+ NULL,
+ IBUS_INTERFACE_PANEL,
+ "PropertyShow",
+@@ -1550,7 +1628,7 @@ ibus_panel_service_property_hide (IBusPanelService *panel,
+ const gchar *prop_name)
+ {
+ g_return_if_fail (IBUS_IS_PANEL_SERVICE (panel));
+- ibus_service_emit_signal ((IBusService *) panel,
++ ibus_service_emit_signal ((IBusService *)panel,
+ NULL,
+ IBUS_INTERFACE_PANEL,
+ "PropertyHide",
+@@ -1567,7 +1645,7 @@ ibus_panel_service_commit_text (IBusPanelService *panel,
+ g_return_if_fail (IBUS_IS_TEXT (text));
+
+ variant = ibus_serializable_serialize ((IBusSerializable *)text);
+- ibus_service_emit_signal ((IBusService *) panel,
++ ibus_service_emit_signal ((IBusService *)panel,
+ NULL,
+ IBUS_INTERFACE_PANEL,
+ "CommitText",
+@@ -1588,7 +1666,7 @@ ibus_panel_service_panel_extension (IBusPanelService *panel,
+ g_return_if_fail (IBUS_IS_EXTENSION_EVENT (event));
+
+ variant = ibus_serializable_serialize ((IBusSerializable *)event);
+- ibus_service_emit_signal ((IBusService *) panel,
++ ibus_service_emit_signal ((IBusService *)panel,
+ NULL,
+ IBUS_INTERFACE_PANEL,
+ "PanelExtension",
+@@ -1662,7 +1740,7 @@ ibus_panel_service_update_preedit_text_received (IBusPanelService *panel,
+
+ variant = ibus_serializable_serialize ((IBusSerializable *)text);
+ g_return_if_fail (variant);
+- ibus_service_emit_signal ((IBusService *) panel,
++ ibus_service_emit_signal ((IBusService *)panel,
+ NULL,
+ IBUS_INTERFACE_PANEL,
+ "UpdatePreeditTextReceived",
+@@ -1686,7 +1764,7 @@ ibus_panel_service_update_auxiliary_text_received (IBusPanelService *panel,
+
+ variant = ibus_serializable_serialize ((IBusSerializable *)text);
+ g_return_if_fail (variant);
+- ibus_service_emit_signal ((IBusService *) panel,
++ ibus_service_emit_signal ((IBusService *)panel,
+ NULL,
+ IBUS_INTERFACE_PANEL,
+ "UpdateAuxiliaryTextReceived",
+@@ -1711,7 +1789,7 @@ ibus_panel_service_update_lookup_table_received (IBusPanelService *panel,
+
+ variant = ibus_serializable_serialize ((IBusSerializable *)table);
+ g_return_if_fail (variant);
+- ibus_service_emit_signal ((IBusService *) panel,
++ ibus_service_emit_signal ((IBusService *)panel,
+ NULL,
+ IBUS_INTERFACE_PANEL,
+ "UpdateLookupTableReceived",
+@@ -1731,7 +1809,7 @@ ibus_panel_service_forward_process_key_event (IBusPanelService *panel,
+ guint32 state)
+ {
+ g_return_if_fail (IBUS_IS_PANEL_SERVICE (panel));
+- ibus_service_emit_signal ((IBusService *) panel,
++ ibus_service_emit_signal ((IBusService *)panel,
+ NULL,
+ IBUS_INTERFACE_PANEL,
+ "ForwardProcessKeyEvent",
+@@ -1740,6 +1818,28 @@ ibus_panel_service_forward_process_key_event (IBusPanelService *panel,
+ NULL);
+ }
+
++void
++ibus_panel_service_send_message (IBusPanelService *panel,
++ IBusMessage *message)
++{
++ GVariant *variant;
++ GError *error = NULL;
++
++ g_return_if_fail (IBUS_IS_PANEL_SERVICE (panel));
++ g_return_if_fail (IBUS_IS_MESSAGE (message));
++ variant = ibus_serializable_serialize ((IBusSerializable *)message);
++ ibus_service_emit_signal ((IBusService *)panel,
++ NULL,
++ IBUS_INTERFACE_PANEL,
++ "SendMessage",
++ g_variant_new ("(v)", variant),
++ &error);
++ if (error) {
++ g_warning ("Error in %s: %s", G_STRFUNC, error->message);
++ g_error_free (error);
++ }
++}
++
+ #define DEFINE_FUNC(name, Name) \
+ void \
+ ibus_panel_service_##name (IBusPanelService *panel) \
+diff --git a/src/ibuspanelservice.h b/src/ibuspanelservice.h
+index 64efca55..57045956 100644
+--- a/src/ibuspanelservice.h
++++ b/src/ibuspanelservice.h
+@@ -2,7 +2,7 @@
+ /* vim:set et sts=4: */
+ /* ibus - The Input Bus
+ * Copyright (c) 2009-2014 Google Inc. All rights reserved.
+- * Copyright (c) 2017-2024 Takao Fujiwara <takao.fujiwara1@gmail.com>
++ * Copyright (c) 2017-2025 Takao Fujiwara <takao.fujiwara1@gmail.com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+@@ -36,6 +36,7 @@
+ * Developers can "extend" this class for panel UI development.
+ */
+ #include "ibuslookuptable.h"
++#include "ibusmessage.h"
+ #include "ibusservice.h"
+ #include "ibusproplist.h"
+ #include "ibusxevent.h"
+@@ -60,6 +61,24 @@
+
+ G_BEGIN_DECLS
+
++/**
++ * IBusPanelServiceMsgCode:
++ * @IBUS_PANEL_SERVICE_MSG_CODE_GENERAL: Generic message for Panel
++ * @IBUS_PANEL_SERVICE_MSG_CODE_LOADING_UNICODE: Progress message when the
++ * Unicode data is loading.
++ *
++ * Message codes in the `IBusMessageDomain` domain for Panel.
++ *
++ * Since: 1.5.33
++ * Stability: Unstable
++ */
++typedef enum
++{
++ IBUS_PANEL_SERVICE_MSG_CODE_GENERAL,
++ IBUS_PANEL_SERVICE_MSG_CODE_LOADING_UNICODE
++} IBusPanelServiceMsgCode;
++
++
+ typedef struct _IBusPanelService IBusPanelService;
+ typedef struct _IBusPanelServiceClass IBusPanelServiceClass;
+
+@@ -372,11 +391,28 @@ void ibus_panel_service_update_lookup_table_received
+ *
+ * Forward key events when an IBus popup takes the focus and the events
+ * needs to be forwared to the target IBus engine.
++ *
++ * Since: 1.5.32
++ * Stability: Unstable
+ */
+ void ibus_panel_service_forward_process_key_event
+ (IBusPanelService *panel,
+ guint32 keyval,
+ guint32 keycode,
+ guint32 state);
++
++/**
++ * ibus_panel_service_send_message:
++ * @panel: An #IBusPanelService.
++ * @message: An #IBusMessage.
++ *
++ * Send a message to the Panel for the focus-less notification popup.
++ * This is used for the emoji component in Wayland mainly but in Xorg too.
++ *
++ * Since: 1.5.33
++ * Stability: Unstable
++ */
++void ibus_panel_service_send_message (IBusPanelService *panel,
++ IBusMessage *message);
+ G_END_DECLS
+ #endif
+diff --git a/src/ibustypes.h b/src/ibustypes.h
+index c8dfb89a..1243b2bc 100644
+--- a/src/ibustypes.h
++++ b/src/ibustypes.h
+@@ -358,5 +358,19 @@ typedef enum
+ IBUS_SUPER_MASK | \
+ IBUS_HYPER_MASK | \
+ IBUS_META_MASK))
++/**
++ * IBusMessageDomain:
++ * @IBUS_MESSAGE_ENGINE: The message domain for Engine messages
++ * @IBUS_MESSAGE_PANEL: The message domain for Panel messages
++ *
++ * Since: 1.5.33
++ * Stability: Unstable
++ */
++typedef enum
++{
++ IBUS_MESSAGE_DOMAIN_NONE,
++ IBUS_MESSAGE_DOMAIN_ENGINE,
++ IBUS_MESSAGE_DOMAIN_PANEL
++} IBusMessageDomain;
+
+ #endif
+diff --git a/ui/gtk3/Makefile.am b/ui/gtk3/Makefile.am
+index 4cc10780..d99d8e82 100644
+--- a/ui/gtk3/Makefile.am
++++ b/ui/gtk3/Makefile.am
+@@ -97,6 +97,7 @@ ibus_ui_gtk3_SOURCES = \
+ handle.vala \
+ iconwidget.vala \
+ keybindingmanager.vala \
++ message.vala \
+ panel.vala \
+ pango.vala \
+ property.vala \
+diff --git a/ui/gtk3/emojier.vala b/ui/gtk3/emojier.vala
+index 1770de2f..2d8754a4 100644
+--- a/ui/gtk3/emojier.vala
++++ b/ui/gtk3/emojier.vala
+@@ -2,7 +2,7 @@
+ *
+ * ibus - The Input Bus
+ *
+- * Copyright (c) 2017-2023 Takao Fujiwara <takao.fujiwara1@gmail.com>
++ * Copyright (c) 2017-2025 Takao Fujiwara <takao.fujiwara1@gmail.com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+@@ -312,6 +312,7 @@ public class IBusEmojier : Gtk.ApplicationWindow {
+ private uint m_unicode_progress_id;
+ private Gtk.Label m_unicode_percent_label;
+ private double m_unicode_percent;
++ private ulong m_unicode_deserialize_unicode_signal_id;
+ private Gdk.Rectangle m_cursor_location;
+ private bool m_is_up_side_down = false;
+ private uint m_redraw_window_id;
+@@ -321,6 +322,7 @@ public class IBusEmojier : Gtk.ApplicationWindow {
+ public signal void candidate_clicked(uint index, uint button, uint state);
+ public signal void commit_text(string text);
+ public signal void cancel();
++ public signal void send_message(IBus.Message message);
+
+ public IBusEmojier(bool is_wayland) {
+ GLib.Object(
+@@ -1178,9 +1180,13 @@ public class IBusEmojier : Gtk.ApplicationWindow {
+ hbox.pack_start(m_unicode_percent_label, false, true, 0);
+ hbox.show_all();
+
+- m_unicode_progress_object.deserialize_unicode.connect((i, n) => {
+- m_unicode_percent = (double)i / n;
+- });
++ if (m_unicode_deserialize_unicode_signal_id == 0) {
++ m_unicode_deserialize_unicode_signal_id =
++ m_unicode_progress_object.deserialize_unicode.connect(
++ (i, n) => {
++ m_unicode_percent = (double)i / n;
++ });
++ }
+ if (m_unicode_progress_id > 0) {
+ GLib.Source.remove(m_unicode_progress_id);
+ }
+@@ -1199,6 +1205,18 @@ public class IBusEmojier : Gtk.ApplicationWindow {
+ }
+
+
++ private void show_unicode_popup(int progress) {
++ var message = new IBus.Message(IBus.MessageDomain.PANEL,
++ IBus.PanelServiceMsgCode.LOADING_UNICODE,
++ _("IBus Emoji initialization"),
++ _("Loading a Unicode dictionary:"),
++ "timeout", 3,
++ "progress", progress,
++ "serial", 50001);
++ send_message(message);
++ }
++
++
+ private static string? check_unicode_point(string annotation) {
+ string unicode_point = null;
+ // Add "0x" because uint64.ascii_strtoull() is not accessible
+@@ -1232,7 +1250,7 @@ public class IBusEmojier : Gtk.ApplicationWindow {
+ }
+
+
+- private static GLib.SList<string>?
++ private GLib.SList<string>?
+ lookup_emojis_from_annotation(string annotation) {
+ GLib.SList<string>? total_emojis = null;
+ unowned GLib.SList<string>? sub_emojis = null;
+@@ -1310,6 +1328,17 @@ public class IBusEmojier : Gtk.ApplicationWindow {
+ }
+ }
+ }
++ if (!m_loaded_unicode && m_unicode_deserialize_unicode_signal_id == 0) {
++ m_unicode_deserialize_unicode_signal_id =
++ m_unicode_progress_object.deserialize_unicode.connect(
++ (i, n) => {
++ m_unicode_percent = (double)i / n;
++ show_unicode_popup(
++ (int)(m_unicode_percent * 100));
++ });
++ }
++ if (!m_loaded_unicode)
++ show_unicode_popup(0);
+ return total_emojis;
+ }
+
+diff --git a/ui/gtk3/message.vala b/ui/gtk3/message.vala
+new file mode 100644
+index 00000000..0c7dd478
+--- /dev/null
++++ b/ui/gtk3/message.vala
+@@ -0,0 +1,353 @@
++/* vim:set et sts=4 sw=4:
++ *
++ * ibus - The Input Bus
++ *
++ * Copyright(c) 2025 Takao Fujiwara <takao.fujiwara1@gmail.com>
++ *
++ * 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
++ */
++
++public class MessageDialog : Gtk.Box{
++ private Gtk.Window m_toplevel;
++ private Gtk.Box m_vbox;
++ private Gtk.Box m_hbox;
++ private Gtk.Label m_title_label;
++ private Gtk.Label m_description_label;
++ private Gtk.ProgressBar m_progress_bar;
++ private Gtk.Label m_progress_label;
++ private Gtk.Label m_timeout_label;
++ private Gtk.Button m_button;
++ private int m_timeout_time;
++ private int m_elapsed_time;
++ private uint m_timeout_id;
++
++ private Gdk.Rectangle m_cursor_location;
++
++ private bool m_is_wayland;
++ private bool m_no_wayland_panel;
++
++#if USE_GDK_WAYLAND
++ private bool m_hide_after_show;
++#endif
++
++ public signal void close();
++
++#if USE_GDK_WAYLAND
++ public signal void realize_surface(void *surface);
++#endif
++
++ public MessageDialog (bool is_wayland,
++ bool no_wayland_panel,
++ IBus.Message message) {
++ // Call base class constructor
++ GLib.Object(
++ name : "IBusMessageDialog",
++ orientation: Gtk.Orientation.HORIZONTAL,
++ visible: true
++ );
++
++ m_is_wayland = is_wayland;
++ m_no_wayland_panel = no_wayland_panel;
++ m_toplevel = new Gtk.Window(Gtk.WindowType.POPUP);
++ m_toplevel.add_events(Gdk.EventMask.BUTTON_PRESS_MASK);
++ m_toplevel.size_allocate.connect((w, a) => {
++ adjust_window_position(w);
++ });
++#if USE_GDK_WAYLAND
++ if (m_is_wayland) {
++ m_toplevel.realize.connect((w) => {
++ realize_window(true);
++ });
++ m_toplevel.show.connect((w) => {
++ if (m_hide_after_show)
++ realize_window(false);
++ m_hide_after_show = false;
++ });
++ m_toplevel.hide.connect((w) => {
++ m_hide_after_show = true;
++ });
++ }
++#endif
++
++ m_vbox = new Gtk.Box(Gtk.Orientation.VERTICAL, 0);
++ m_vbox.set_visible(true);
++ pack_start(m_vbox, false, false, 0);
++
++ m_toplevel.add(this);
++
++ create_ui(message);
++ }
++
++ public void set_cursor_location(int x, int y, int width, int height) {
++ Gdk.Rectangle location = Gdk.Rectangle(){
++ x = x, y = y, width = width, height = height };
++ if (m_cursor_location == location)
++ return;
++ m_cursor_location = location;
++
++ /* Do not call adjust_window_position() here because
++ * m_toplevel is not shown yet and
++ * m_toplevel.get_allocation() returns height = width = 1 */
++ }
++
++ private void create_ui(IBus.Message message) {
++ string? title = message.get_title();
++ string description = message.get_description();
++
++ if (title.length > 0) {
++ m_title_label = new Gtk.Label(title);
++ } else {
++ m_title_label = new Gtk.Label(null);
++ m_title_label.set_no_show_all(true);
++ }
++ m_title_label.set_halign(Gtk.Align.CENTER);
++ m_title_label.set_valign(Gtk.Align.CENTER);
++ m_title_label.set_margin_start(8);
++ m_title_label.set_margin_end(8);
++ m_title_label.set_line_wrap(true);
++ m_title_label.set_width_chars(50);
++
++ m_description_label = new Gtk.Label(description);
++ m_description_label.set_halign(Gtk.Align.START);
++ m_description_label.set_valign(Gtk.Align.CENTER);
++ m_description_label.set_margin_start(8);
++ m_description_label.set_margin_end(8);
++ m_description_label.set_line_wrap(true);
++ m_description_label.set_width_chars(50);
++
++ m_hbox = new Gtk.Box(Gtk.Orientation.HORIZONTAL, 5);
++ m_hbox.set_halign(Gtk.Align.CENTER);
++ m_hbox.set_valign(Gtk.Align.CENTER);
++ m_progress_bar = new Gtk.ProgressBar();
++ m_progress_bar.set_ellipsize(Pango.EllipsizeMode.MIDDLE);
++ m_progress_bar.set_halign(Gtk.Align.CENTER);
++ m_progress_bar.set_valign(Gtk.Align.CENTER);
++ m_progress_label = new Gtk.Label(null);
++
++ int progress = message.get_progress();
++ double progress_f = progress / 100.0;
++ if (progress_f <= 1.0 && progress >= 0) {
++ m_progress_bar.set_fraction(progress_f);
++ m_progress_bar.set_text(
++ "%2u%%\n".printf(progress));
++ m_progress_label.set_text(
++ "%2u%%\n".printf(progress));
++ } else {
++ if (progress_f > 1.0)
++ warning ("Progress should be below 100.");
++ m_hbox.set_no_show_all(true);
++ }
++
++ m_timeout_time = message.get_timeout();
++ if (m_timeout_time >= 0) {
++ m_timeout_label = new Gtk.Label(_("%5dSec").printf(m_timeout_time));
++ } else {
++ m_timeout_label = new Gtk.Label(null);
++ m_timeout_label.set_no_show_all(true);
++ }
++ m_timeout_label.set_size_request(20, -1);
++ m_timeout_label.set_halign(Gtk.Align.CENTER);
++ m_timeout_label.set_valign(Gtk.Align.CENTER);
++ m_timeout_label.set_margin_start(8);
++ m_timeout_label.set_margin_end(8);
++
++ if (message.get_serial() > 0 && m_timeout_time >= 0) {
++ m_button = new Gtk.Button();
++ m_button.set_no_show_all(true);
++ } else {
++ m_button = new Gtk.Button.with_label(_("Close"));
++ m_button.get_style_context().add_class("text-button");
++ m_button.set_can_default(true);
++ }
++ m_button.set_use_underline(true);
++ m_button.set_valign(Gtk.Align.BASELINE);
++ m_button.clicked.connect((w) => {
++ if (m_timeout_id > 0) {
++ GLib.Source.remove(m_timeout_id);
++ m_timeout_id = 0;
++ }
++ close();
++ });
++ if (m_timeout_time >= 0) {
++ m_elapsed_time = 1;
++ m_timeout_id = Timeout.add_seconds(1,
++ () => {
++ if (m_elapsed_time >= m_timeout_time) {
++ close();
++ m_timeout_id = 0;
++ return Source.REMOVE;
++ }
++ m_timeout_label.set_text(_("%5dSec").printf(
++ m_timeout_time - m_elapsed_time++));
++ return Source.CONTINUE;
++ });
++ }
++
++ m_vbox.pack_start(m_title_label, false, false, 4);
++ m_vbox.pack_start(m_description_label, false, false, 4);
++ m_vbox.add(m_hbox);
++ m_hbox.add(m_progress_bar);
++ m_hbox.add(m_progress_label);
++ m_vbox.pack_start(m_timeout_label, false, false, 4);
++ m_vbox.pack_start(m_button, false, false, 0);
++
++ m_toplevel.resize(1, 1);
++ }
++
++ public void update_message(IBus.Message message) {
++ string? title = message.get_title();
++ string description = message.get_description();
++
++ if (title.length > 0) {
++ m_title_label.set_text(title);
++ m_title_label.show();
++ } else {
++ m_title_label.hide();
++ }
++ m_description_label.set_text(description);
++
++ m_timeout_time = message.get_timeout();
++ if (m_timeout_time >= 0) {
++ m_timeout_label.set_text(_("%5dSec").printf(m_timeout_time));
++ m_timeout_label.show();
++ m_elapsed_time = 1;
++ } else {
++ m_timeout_label.hide();
++ }
++
++ int progress = message.get_progress();
++ double progress_f = progress / 100.0;
++ if (progress_f <= 1.0 && progress >= 0) {
++ m_progress_bar.set_fraction(progress_f);
++ m_progress_bar.set_text(
++ "%2u%%\n".printf(progress));
++ m_progress_label.set_text(
++ "%2u%%\n".printf(progress));
++ m_hbox.show();
++ } else {
++ if (progress_f > 1.0)
++ warning ("Progress should be below 100.");
++ m_hbox.hide();
++ }
++
++ m_toplevel.resize(1, 1);
++ this.show();
++ }
++
++ public new void show() {
++ m_toplevel.show_all();
++ }
++
++ public new void hide() {
++#if USE_GDK_WAYLAND
++ if (m_is_wayland)
++ realize_surface(null);
++#endif
++ m_toplevel.hide();
++ }
++
++ /**
++ * move:
++ * @x: left position of the #MessageDialog
++ * @y: top position of the #MessageDialog
++ */
++ private void move(int x, int y) {
++ m_toplevel.move(x, y);
++ }
++
++ private void adjust_window_position(Gtk.Widget window) {
++ adjust_window_position_horizontal(window);
++ }
++
++ /**
++ * adjust_window_position_horizontal:
++ * @window: A Gtk.Widget of the toplevel window.
++ *
++ * Horizontal writing mode but not the horizontal lookup table
++ * when the allocation is emmitted.
++ */
++ private void adjust_window_position_horizontal(Gtk.Widget window) {
++ Gdk.Point cursor_left_bottom = {
++ m_cursor_location.x,
++ m_cursor_location.y + m_cursor_location.height
++ };
++
++ Gtk.Allocation allocation;
++ m_toplevel.get_allocation(out allocation);
++ Gdk.Point window_right_bottom = {
++ cursor_left_bottom.x + allocation.width,
++ cursor_left_bottom.y + allocation.height
++ };
++
++ Gdk.Rectangle monitor_area = get_monitor_geometry(window);
++ int monitor_right = monitor_area.x + monitor_area.width;
++ int monitor_bottom = monitor_area.y + monitor_area.height;
++
++ int x, y;
++ if (window_right_bottom.x > monitor_right)
++ x = monitor_right - allocation.width;
++ else
++ x = cursor_left_bottom.x;
++ if (x < 0)
++ x = 0;
++
++ if (window_right_bottom.y > monitor_bottom)
++ y = m_cursor_location.y - allocation.height;
++ else
++ y = cursor_left_bottom.y;
++ if (y < 0)
++ y = 0;
++
++ move(x, y);
++ }
++
++ private Gdk.Rectangle get_monitor_geometry(Gtk.Widget window) {
++ Gdk.Rectangle monitor_area = { 0, };
++
++ // Use get_monitor_geometry() instead of get_monitor_area().
++ // get_monitor_area() excludes docks, but the lookup window should be
++ // shown over them.
++ Gdk.Monitor monitor = window.get_display().get_monitor_at_point(
++ m_cursor_location.x,
++ m_cursor_location.y);
++ monitor_area = monitor.get_geometry();
++ return monitor_area;
++ }
++
++#if USE_GDK_WAYLAND
++ private void realize_window(bool initial) {
++ // The custom surface can be used when the Wayland input-method
++ // is activated.
++ if (m_no_wayland_panel)
++ return;
++ var window = m_toplevel.get_window();
++ if (!window.ensure_native()) {
++ warning("No native window.");
++ return;
++ }
++ Type instance_type = window.get_type();
++ Type wayland_type = typeof(GdkWayland.Window);
++ if (!instance_type.is_a(wayland_type)) {
++ warning("Not GdkWindowWayland.");
++ return;
++ }
++ if (initial)
++ ((GdkWayland.Window)window).set_use_custom_surface();
++ var surface = ((GdkWayland.Window)window).get_wl_surface();
++ realize_surface(surface);
++ }
++#endif
++}
+diff --git a/ui/gtk3/panel.vala b/ui/gtk3/panel.vala
+index 363834e9..d65ffbb3 100644
+--- a/ui/gtk3/panel.vala
++++ b/ui/gtk3/panel.vala
+@@ -93,6 +93,10 @@ class Panel : IBus.PanelService {
+ private ulong m_registered_status_notifier_item_id;
+ private unowned FileStream m_log;
+ private bool m_verbose;
++ private GLib.HashTable<uint, MessageDialog> m_popup_dialogs =
++ new GLib.HashTable<uint, MessageDialog>(GLib.direct_hash,
++ GLib.direct_equal);
++ private uint m_popup_dialog_index;
+
+ private GLib.List<BindingCommon.Keybinding> m_keybindings =
+ new GLib.List<BindingCommon.Keybinding>();
+@@ -246,12 +250,12 @@ class Panel : IBus.PanelService {
+ m_settings_panel = new GLib.Settings("org.freedesktop.ibus.panel");
+
+ m_settings_general.changed["preload-engines"].connect((key) => {
+- update_engines(m_settings_general.get_strv(key),
+- null);
++ update_engines(m_settings_general.get_strv(key),
++ null);
+ });
+
+ m_settings_general.changed["switcher-delay-time"].connect((key) => {
+- set_switcher_delay_time();
++ set_switcher_delay_time();
+ });
+
+ m_settings_general.changed["use-system-keyboard-layout"].connect(
+@@ -260,72 +264,72 @@ class Panel : IBus.PanelService {
+ });
+
+ m_settings_general.changed["embed-preedit-text"].connect((key) => {
+- set_embed_preedit_text();
++ set_embed_preedit_text();
+ });
+
+ m_settings_general.changed["use-global-engine"].connect((key) => {
+- set_use_global_engine();
++ set_use_global_engine();
+ });
+
+ m_settings_general.changed["use-xmodmap"].connect((key) => {
+- set_use_xmodmap();
++ set_use_xmodmap();
+ });
+
+ m_settings_hotkey.changed["triggers"].connect((key) => {
+- BindingCommon.unbind_switch_shortcut(
+- BindingCommon.KeyEventFuncType.IME_SWITCHER,
+- m_keybindings);
+- m_keybindings = null;
+- bind_switch_shortcut();
++ BindingCommon.unbind_switch_shortcut(
++ BindingCommon.KeyEventFuncType.IME_SWITCHER,
++ m_keybindings);
++ m_keybindings = null;
++ bind_switch_shortcut();
+ });
+
+ m_settings_panel.changed["custom-font"].connect((key) => {
+- BindingCommon.set_custom_font(m_settings_panel,
+- null,
+- ref m_css_provider);
++ BindingCommon.set_custom_font(m_settings_panel,
++ null,
++ ref m_css_provider);
+ });
+
+ m_settings_panel.changed["use-custom-font"].connect((key) => {
+- BindingCommon.set_custom_font(m_settings_panel,
+- null,
+- ref m_css_provider);
++ BindingCommon.set_custom_font(m_settings_panel,
++ null,
++ ref m_css_provider);
+ });
+
+ m_settings_panel.changed["custom-theme"].connect((key) => {
+- BindingCommon.set_custom_theme(m_settings_panel);
++ BindingCommon.set_custom_theme(m_settings_panel);
+ });
+
+ m_settings_panel.changed["use-custom-theme"].connect((key) => {
+- BindingCommon.set_custom_theme(m_settings_panel);
++ BindingCommon.set_custom_theme(m_settings_panel);
+ });
+
+ m_settings_panel.changed["custom-icon"].connect((key) => {
+- BindingCommon.set_custom_icon(m_settings_panel);
++ BindingCommon.set_custom_icon(m_settings_panel);
+ });
+
+ m_settings_panel.changed["use-custom-icon"].connect((key) => {
+- BindingCommon.set_custom_icon(m_settings_panel);
++ BindingCommon.set_custom_icon(m_settings_panel);
+ });
+
+ m_settings_panel.changed["use-glyph-from-engine-lang"].connect((key) =>
+ {
+- set_use_glyph_from_engine_lang();
++ set_use_glyph_from_engine_lang();
+ });
+
+ m_settings_panel.changed["show-icon-on-systray"].connect((key) => {
+- set_show_icon_on_systray(true);
++ set_show_icon_on_systray(true);
+ });
+
+ m_settings_panel.changed["lookup-table-orientation"].connect((key) => {
+- set_lookup_table_orientation();
++ set_lookup_table_orientation();
+ });
+
+ m_settings_panel.changed["show"].connect((key) => {
+- set_show_property_panel();
++ set_show_property_panel();
+ });
+
+ m_settings_panel.changed["timeout"].connect((key) => {
+- set_timeout_property_panel();
++ set_timeout_property_panel();
+ });
+
+ m_settings_panel.changed["follow-input-cursor-when-always-shown"]
+@@ -334,11 +338,27 @@ class Panel : IBus.PanelService {
+ });
+
+ m_settings_panel.changed["xkb-icon-rgba"].connect((key) => {
+- set_xkb_icon_rgba();
++ set_xkb_icon_rgba();
+ });
+
+ m_settings_panel.changed["property-icon-delay-time"].connect((key) => {
+- set_property_icon_delay_time();
++ set_property_icon_delay_time();
++ });
++
++ this.send_message_received.connect((o, m) => {
++ uint domain = m.get_domain();
++ switch (domain) {
++ case IBus.MessageDomain.ENGINE:
++ set_message_engine(m);
++ break;
++ case IBus.MessageDomain.PANEL:
++ set_message_panel(m);
++ break;
++ default:
++ warning("SendMessageReceived does not support the domain %u",
++ domain);
++ break;
++ }
+ });
+ }
+
+@@ -855,6 +875,67 @@ class Panel : IBus.PanelService {
+ }
+
+
++ private void set_message_engine(IBus.Message message) {
++ uint code = message.get_code();
++ switch (code) {
++ case IBus.EngineMsgCode.INVALID_COMPOSE_SEQUENCE:
++ set_message_engine_invalid_compose(message);
++ break;
++ default:
++ set_message_general(message);
++ break;
++ }
++ }
++
++
++ private void set_message_engine_invalid_compose(IBus.Message message) {
++ var display = Gdk.Display.get_default();
++ display.beep();
++ }
++
++
++ private void set_message_panel(IBus.Message message) {
++ uint code = message.get_code();
++ switch (code) {
++ case IBus.PanelServiceMsgCode.LOADING_UNICODE:
++ set_message_general(message);
++ break;
++ }
++ }
++
++
++ private void set_message_general(IBus.Message message) {
++ MessageDialog popup = null;
++ uint serial = message.get_serial();
++ if (serial != 0)
++ popup = m_popup_dialogs.lookup(serial);
++ else
++ serial = m_popup_dialog_index++ + 20000;
++ if (popup != null) {
++ popup.update_message(message);
++ } else {
++ popup = new MessageDialog(m_is_wayland,
++#if USE_GDK_WAYLAND
++ m_wayland_object_path == null,
++#else
++ true,
++#endif
++ message);
++ popup.close.connect((w) => {
++ popup.hide();
++ m_popup_dialogs.remove(serial);
++ popup = null;
++ });
++#if USE_GDK_WAYLAND
++ popup.realize_surface.connect(
++ (w, s) => this.realize_surface(s));
++#endif
++ m_popup_dialogs.insert(serial, popup);
++ popup.show();
++ }
++ }
++
++
+ private int compare_versions(string version1, string version2) {
+ string[] version1_list = version1.split(".");
+ string[] version2_list = version2.split(".");
+diff --git a/ui/gtk3/panelbinding.vala b/ui/gtk3/panelbinding.vala
+index 10d09d75..f0827b3e 100644
+--- a/ui/gtk3/panelbinding.vala
++++ b/ui/gtk3/panelbinding.vala
+@@ -3,7 +3,7 @@
+ * ibus - The Input Bus
+ *
+ * Copyright(c) 2018 Peng Huang <shawn.p.huang@gmail.com>
+- * Copyright(c) 2018-2024 Takao Fujwiara <takao.fujiwara1@gmail.com>
++ * Copyright(c) 2018-2025 Takao Fujwiara <takao.fujiwara1@gmail.com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+@@ -902,6 +902,9 @@ class PanelBinding : IBus.PanelService {
+ "is-extension", true);
+ panel_extension(close_event);
+ });
++ m_emojier.send_message.connect((m) => {
++ send_message(m);
++ });
+ }
+ m_emojier.reset();
+ m_emojier.set_annotation("");
+--
+2.49.0
+
+From 40fcbebb8a4a60172d80482e35dcab2f39b80c2d Mon Sep 17 00:00:00 2001
+From: fujiwarat <takao.fujiwara1@gmail.com>
+Date: Sun, 1 Jun 2025 20:32:05 +0900
+Subject: [PATCH 3/3] Add workaround until GNOME handle IBusMessage
+
+The IBusEngine's focus-id feature is now always used in case the engine
+is IBusEngineSimple to make a workaround in GNOME and Wayland because
+GNOME-Shell does not implement the new D-Bus method
+"SendMessageReceived" yet and the Wayland input-method protocol cannot
+get the input surface for the Wayland xdg-system-bell protocol.
+
+The auxiliary text is shown with the candidate popup in GNOME as a
+workaround to notify the Unicode loading message until GNOME-Shell
+implements the D-Bus method "SendMessageReceived".
+
+Update the POT file.
+
+BUG=https://github.com/ibus/ibus/issues/2722
+---
+ po/POTFILES.in | 2 +
+ src/ibusengine.c | 18 ++-
+ src/ibusenginesimple.c | 134 ++++++++++++++++++++++-
+ ui/gtk3/candidatepanel.vala | 55 +++++++---
+ ui/gtk3/emojier.vala | 44 ++++++--
+ ui/gtk3/extension.vala | 3 +-
+ ui/gtk3/panelbinding.vala | 5 +-
+ 7 files changed, 338 insertions(+), 134 deletions(-)
+
+diff --git a/po/POTFILES.in b/po/POTFILES.in
+index c56408f7..d9178c85 100644
+--- a/po/POTFILES.in
++++ b/po/POTFILES.in
+@@ -40,9 +40,11 @@ setup/keyboardshortcut.py
+ setup/main.py
+ setup/setup.ui
+ src/ibusbus.c
++src/ibuscomposetable.c
+ src/ibusconfig.c
+ src/ibusemojigen.h
+ src/ibusengine.c
++src/ibusenginesimple.c
+ src/ibusfactory.c
+ src/ibushotkey.c
+ src/ibusinputcontext.c
+diff --git a/src/ibusengine.c b/src/ibusengine.c
+index 4e13b23d..1f12d253 100644
+--- a/src/ibusengine.c
++++ b/src/ibusengine.c
+@@ -34,6 +34,8 @@
+ #define IBUS_ENGINE_GET_PRIVATE(o) \
+ ((IBusEnginePrivate *)ibus_engine_get_instance_private (o))
+
++extern GType ibus_engine_simple_get_type (void);
++
+ enum {
+ PROCESS_KEY_EVENT,
+ FOCUS_IN,
+@@ -1544,7 +1546,21 @@ _ibus_engine_has_focus_id (IBusEngine *engine,
+ GDBusConnection *connection,
+ GError **error)
+ {
+- GVariant *retval = g_variant_new_boolean (engine->priv->has_focus_id);
++ GVariant *retval;
++
++#ifndef IBUS_TYPE_ENGINE_SIMPLE
++#define IBUS_TYPE_ENGINE_SIMPLE (ibus_engine_simple_get_type ())
++#define __IBUS_SET_LOCAL_ENGINE_SIMPLE
++#endif
++ /* Should not use IBUS_IS_ENGINE_SIMPLE() not to effect the inherited
++ * class.*/
++ if (G_OBJECT_TYPE (engine) == IBUS_TYPE_ENGINE_SIMPLE)
++ engine->priv->has_focus_id = TRUE;
++#ifdef __IBUS_SET_LOCAL_ENGINE_SIMPLE
++#undef __IBUS_SET_LOCAL_ENGINE_SIMPLE
++#undef IBUS_TYPE_ENGINE_SIMPLE
++#endif
++ retval = g_variant_new_boolean (engine->priv->has_focus_id);
+ g_assert (retval);
+ return retval;
+ }
+diff --git a/src/ibusenginesimple.c b/src/ibusenginesimple.c
+index 4e0ba40f..15c45c2e 100644
+--- a/src/ibusenginesimple.c
++++ b/src/ibusenginesimple.c
+@@ -75,6 +75,7 @@ struct _IBusEngineSimplePrivate {
+ GString *tentative_match;
+ int tentative_match_len;
+ char *tentative_emoji;
++ char *client;
+
+ guint hex_mode_enabled : 1;
+ guint in_hex_sequence : 1;
+@@ -85,6 +86,8 @@ struct _IBusEngineSimplePrivate {
+ IBusLookupTable *lookup_table;
+ gboolean lookup_table_visible;
+ IBusText *updated_preedit;
++ gboolean do_inform_user_error;
++ guint inform_user_error_timeout_id;
+ };
+
+ guint COMPOSE_BUFFER_SIZE = 20;
+@@ -96,7 +99,13 @@ static IBusComposeTableEx *en_compose_table;
+ /* functions prototype */
+ static void ibus_engine_simple_destroy (IBusEngineSimple *simple);
+ static void ibus_engine_simple_focus_in (IBusEngine *engine);
++static void ibus_engine_simple_focus_in_id (IBusEngine *engine,
++ const gchar
++ *object_path,
++ const gchar *client);
+ static void ibus_engine_simple_focus_out (IBusEngine *engine);
++static void ibus_engine_simple_focus_out_id (IBusEngine *engine,
++ const gchar *client);
+ static void ibus_engine_simple_reset (IBusEngine *engine);
+ static gboolean ibus_engine_simple_process_key_event
+ (IBusEngine *engine,
+@@ -135,7 +144,11 @@ ibus_engine_simple_class_init (IBusEngineSimpleClass *class)
+ (IBusObjectDestroyFunc) ibus_engine_simple_destroy;
+
+ engine_class->focus_in = ibus_engine_simple_focus_in;
++ engine_class->focus_in_id
++ = ibus_engine_simple_focus_in_id;
+ engine_class->focus_out = ibus_engine_simple_focus_out;
++ engine_class->focus_out_id
++ = ibus_engine_simple_focus_out_id;
+ engine_class->reset = ibus_engine_simple_reset;
+ engine_class->process_key_event
+ = ibus_engine_simple_process_key_event;
+@@ -158,6 +171,22 @@ ibus_engine_simple_class_init (IBusEngineSimpleClass *class)
+ en_compose_table = ibus_compose_table_deserialize (contents, length);
+ }
+
++
++static gboolean
++inform_user_error_timeout_cb (gpointer user_data)
++{
++ IBusEngineSimple *simple = (IBusEngineSimple *)user_data;
++ IBusEngineSimplePrivate *priv;
++
++ g_return_val_if_fail (IBUS_IS_ENGINE_SIMPLE (simple), G_SOURCE_REMOVE);
++ priv = simple->priv;
++ priv->do_inform_user_error = FALSE;
++ priv->inform_user_error_timeout_id = 0;
++ ibus_engine_show_preedit_text ((IBusEngine *)simple);
++ return G_SOURCE_REMOVE;
++}
++
++
+ static void
+ ibus_engine_simple_init (IBusEngineSimple *simple)
+ {
+@@ -205,13 +234,18 @@ ibus_engine_simple_destroy (IBusEngineSimple *simple)
+ IBUS_OBJECT (simple));
+ }
+
++
+ static void
+ ibus_engine_simple_send_message_with_code (IBusEngineSimple *simple,
+ IBusEngineMsgCode code)
+ {
++ IBusEngineSimplePrivate *priv;
+ IBusMessage *message;
+
+ g_return_if_fail (IBUS_IS_ENGINE_SIMPLE (simple));
++
++ priv = simple->priv;
++ priv->do_inform_user_error = TRUE;
+ message = ibus_message_new (
+ IBUS_MESSAGE_DOMAIN_ENGINE,
+ code,
+@@ -226,19 +260,57 @@ ibus_engine_simple_send_message_with_code (IBusEngineSimple *simple,
+ ibus_engine_send_message (IBUS_ENGINE (simple), message);
+ }
+
++
+ static void
+ ibus_engine_simple_focus_in (IBusEngine *engine)
+ {
++ ibus_engine_simple_focus_in_id (engine, NULL, NULL);
++}
++
++
++static void
++ibus_engine_simple_focus_in_id (IBusEngine *engine,
++ const gchar *object_path,
++ const gchar *client)
++{
++ /* Do not enable IBusEngine:has-focus-id in the IBusEngineSimple's
++ * constructor because the property should be enabled by the inherited
++ * engine. E.g. IBusEngineHangul disables it.
++ * The property is handled by ibusengine.c:_ibus_engine_has_focus_id();
++ */
++ if (IBUS_IS_ENGINE_SIMPLE (engine)) {
++ IBusEngineSimple *simple = IBUS_ENGINE_SIMPLE (engine);
++ g_free (simple->priv->client);
++ simple->priv->client = g_strdup (client);
++ } else {
++ g_warning ("IBUS_IS_ENGINE_SIMPLE(engine) in %s", G_STRFUNC);
++ }
+ IBUS_ENGINE_CLASS (ibus_engine_simple_parent_class)->focus_in (engine);
+ }
+
++
+ static void
+ ibus_engine_simple_focus_out (IBusEngine *engine)
+ {
+- ibus_engine_simple_reset (engine);
++ ibus_engine_simple_focus_out_id (engine, NULL);
++}
++
++
++static void
++ibus_engine_simple_focus_out_id (IBusEngine *engine,
++ const gchar *object_path)
++{
++ if (IBUS_IS_ENGINE_SIMPLE (engine)) {
++ IBusEngineSimple *simple = IBUS_ENGINE_SIMPLE (engine);
++ g_clear_pointer (&simple->priv->client, g_free);
++ ibus_engine_simple_reset (engine);
++ } else {
++ g_warning ("IBUS_IS_ENGINE_SIMPLE(engine) in %s", G_STRFUNC);
++ }
+ IBUS_ENGINE_CLASS (ibus_engine_simple_parent_class)->focus_out (engine);
+ }
+
++
+ static void
+ ibus_engine_simple_reset (IBusEngine *engine)
+ {
+@@ -259,11 +331,17 @@ ibus_engine_simple_reset (IBusEngine *engine)
+ priv->tentative_match_len = 0;
+ }
+ ibus_engine_hide_preedit_text ((IBusEngine *)simple);
++ priv->do_inform_user_error = FALSE;
++ if (priv->inform_user_error_timeout_id) {
++ g_source_remove (priv->inform_user_error_timeout_id);
++ priv->inform_user_error_timeout_id = 0;
++ }
+ g_object_unref (priv->updated_preedit);
+ priv->updated_preedit =
+ (IBusText *)g_object_ref_sink (updated_preedit_empty);
+ }
+
++
+ static void
+ ibus_engine_simple_commit_char (IBusEngineSimple *simple,
+ gunichar ch)
+@@ -326,6 +404,7 @@ ibus_engine_simple_commit_str (IBusEngineSimple *simple,
+ g_free (backup_str);
+ }
+
++
+ static void
+ ibus_engine_simple_update_preedit_text (IBusEngineSimple *simple)
+ {
+@@ -426,6 +505,11 @@ ibus_engine_simple_update_preedit_text (IBusEngineSimple *simple)
+ g_object_unref (priv->updated_preedit);
+ priv->updated_preedit =
+ (IBusText *)g_object_ref_sink (updated_preedit_empty);
++ priv->do_inform_user_error = FALSE;
++ if (priv->inform_user_error_timeout_id) {
++ g_source_remove (priv->inform_user_error_timeout_id);
++ priv->inform_user_error_timeout_id = 0;
++ }
+ }
+ } else if (s->len >= G_MAXINT) {
+ g_warning ("%s is too long compose length: %lu", s->str, s->len);
+@@ -433,9 +517,30 @@ ibus_engine_simple_update_preedit_text (IBusEngineSimple *simple)
+ guint len = (guint)g_utf8_strlen (s->str, -1);
+ IBusText *text = ibus_text_new_from_string (s->str);
+ ibus_text_append_attribute (text,
+- IBUS_ATTR_TYPE_UNDERLINE, IBUS_ATTR_UNDERLINE_SINGLE, 0, len);
++ IBUS_ATTR_TYPE_UNDERLINE,
++ IBUS_ATTR_UNDERLINE_SINGLE,
++ 0,
++ len);
+ g_object_ref_sink (text);
+- ibus_engine_update_preedit_text ((IBusEngine *)simple, text, len, TRUE);
++ /* gnome-shell does not handle "SendMessageReceived" D-Bus method yet.
++ * Seems other Wayland desktops do not implement xdg-system-bell
++ * Wayland protocol yet.
++ */
++ if (priv->do_inform_user_error && priv->client &&
++ (!g_ascii_strncasecmp (priv->client, "gnome-shell", 11) ||
++ !g_ascii_strncasecmp (priv->client, "wayland", 7))
++ ) {
++ ibus_engine_hide_preedit_text ((IBusEngine *)simple);
++ if (priv->inform_user_error_timeout_id)
++ g_source_remove (priv->inform_user_error_timeout_id);
++ priv->inform_user_error_timeout_id =
++ g_timeout_add (500,
++ inform_user_error_timeout_cb,
++ simple);
++ } else {
++ ibus_engine_update_preedit_text ((IBusEngine *)simple,
++ text, len, TRUE);
++ }
+ g_object_unref (priv->updated_preedit);
+ priv->updated_preedit = text;
+ }
+@@ -460,6 +565,7 @@ ibus_engine_simple_update_preedit_text (IBusEngineSimple *simple)
+ */
+ #define HEX_MOD_MASK (IBUS_CONTROL_MASK | IBUS_SHIFT_MASK)
+
++
+ static gboolean
+ check_hex (IBusEngineSimple *simple,
+ int n_compose)
+@@ -523,6 +629,7 @@ check_hex (IBusEngineSimple *simple,
+ return TRUE;
+ }
+
++
+ static IBusEngineDict *
+ load_emoji_dict (void)
+ {
+@@ -546,6 +653,7 @@ load_emoji_dict (void)
+ return emoji_dict;
+ }
+
++
+ static gboolean
+ check_emoji_table (IBusEngineSimple *simple,
+ int n_compose,
+@@ -727,6 +835,7 @@ no_sequence_matches (IBusEngineSimple *simple,
+ return FALSE;
+ }
+
++
+ static gboolean
+ is_hex_keyval (guint keyval)
+ {
+@@ -735,6 +844,7 @@ is_hex_keyval (guint keyval)
+ return g_unichar_isxdigit (ch);
+ }
+
++
+ static gboolean
+ is_graph_keyval (guint keyval)
+ {
+@@ -743,6 +853,7 @@ is_graph_keyval (guint keyval)
+ return g_unichar_isgraph (ch);
+ }
+
++
+ static void
+ ibus_engine_simple_update_lookup_and_aux_table (IBusEngineSimple *simple)
+ {
+@@ -768,6 +879,7 @@ ibus_engine_simple_update_lookup_and_aux_table (IBusEngineSimple *simple)
+ priv->lookup_table_visible);
+ }
+
++
+ static gboolean
+ ibus_engine_simple_if_in_range_of_lookup_table (IBusEngineSimple *simple,
+ guint keyval)
+@@ -794,6 +906,7 @@ ibus_engine_simple_if_in_range_of_lookup_table (IBusEngineSimple *simple,
+ return TRUE;
+ }
+
++
+ static void
+ ibus_engine_simple_set_number_on_lookup_table (IBusEngineSimple *simple,
+ guint keyval,
+@@ -886,8 +999,9 @@ ibus_engine_simple_check_all_compose_table (IBusEngineSimple *simple,
+ if (is_32bit) {
+ ibus_engine_simple_commit_str (simple, output->str);
+ } else {
+- ibus_engine_simple_commit_char (simple,
+- g_utf8_get_char (output->str));
++ ibus_engine_simple_commit_char (
++ simple,
++ g_utf8_get_char (output->str));
+ }
+ }
+ ibus_engine_simple_update_preedit_text (simple);
+@@ -951,8 +1065,10 @@ ibus_engine_simple_process_key_event (IBusEngine *engine,
+ guint printable_keyval;
+ int i;
+
+- while (n_compose <= COMPOSE_BUFFER_SIZE && priv->compose_buffer[n_compose] != 0)
++ while (n_compose <= COMPOSE_BUFFER_SIZE &&
++ priv->compose_buffer[n_compose] != 0) {
+ n_compose++;
++ }
+ if (n_compose > COMPOSE_BUFFER_SIZE) {
+ g_warning ("copmose table buffer is full.");
+ n_compose = COMPOSE_BUFFER_SIZE;
+@@ -1395,6 +1511,7 @@ ibus_engine_simple_process_key_event (IBusEngine *engine,
+ return no_sequence_matches (simple, n_compose, keyval, keycode, modifiers);
+ }
+
++
+ static void
+ ibus_engine_simple_page_down (IBusEngine *engine)
+ {
+@@ -1406,6 +1523,7 @@ ibus_engine_simple_page_down (IBusEngine *engine)
+ ibus_engine_simple_update_lookup_and_aux_table (simple);
+ }
+
++
+ static void
+ ibus_engine_simple_page_up (IBusEngine *engine)
+ {
+@@ -1417,6 +1535,7 @@ ibus_engine_simple_page_up (IBusEngine *engine)
+ ibus_engine_simple_update_lookup_and_aux_table (simple);
+ }
+
++
+ static void
+ ibus_engine_simple_candidate_clicked (IBusEngine *engine,
+ guint index,
+@@ -1440,6 +1559,7 @@ ibus_engine_simple_candidate_clicked (IBusEngine *engine,
+ ibus_engine_simple_set_number_on_lookup_table (simple, keyval, n_compose);
+ }
+
++
+ void
+ ibus_engine_simple_add_table (IBusEngineSimple *simple,
+ const guint16 *data,
+@@ -1454,6 +1574,7 @@ ibus_engine_simple_add_table (IBusEngineSimple *simple,
+ n_seqs);
+ }
+
++
+ gboolean
+ ibus_engine_simple_add_table_by_locale (IBusEngineSimple *simple,
+ const gchar *locale)
+@@ -1572,6 +1693,7 @@ ibus_engine_simple_add_table_by_locale (IBusEngineSimple *simple,
+ return retval;
+ }
+
++
+ gboolean
+ ibus_engine_simple_add_compose_file (IBusEngineSimple *simple,
+ const gchar *compose_file)
+diff --git a/ui/gtk3/candidatepanel.vala b/ui/gtk3/candidatepanel.vala
+index aa1a34ce..a4137f05 100644
+--- a/ui/gtk3/candidatepanel.vala
++++ b/ui/gtk3/candidatepanel.vala
+@@ -491,6 +491,11 @@ public class CandidatePanel : Gtk.Box{
+ m_toplevel.hide();
+ }
+
++ /**
++ * move:
++ * @x: left position of the #CandidatePanel
++ * @y: top position of the #CandidatePanel
++ */
+ private void move(int x, int y) {
+ m_toplevel.move(x, y);
+ }
+@@ -502,21 +507,15 @@ public class CandidatePanel : Gtk.Box{
+ adjust_window_position_vertical(window);
+ }
+
+- private Gdk.Rectangle get_monitor_geometry(Gtk.Widget window) {
+- Gdk.Rectangle monitor_area = { 0, };
+-
+- // Use get_monitor_geometry() instead of get_monitor_area().
+- // get_monitor_area() excludes docks, but the lookup window should be
+- // shown over them.
+- Gdk.Monitor monitor = window.get_display().get_monitor_at_point(
+- m_cursor_location.x,
+- m_cursor_location.y);
+- monitor_area = monitor.get_geometry();
+- return monitor_area;
+- }
+-
++ /**
++ * adjust_window_position_horizontal:
++ * @window: A Gtk.Widget of the toplevel window.
++ *
++ * Horizontal writing mode but not the horizontal lookup table
++ * when the allocation is emmitted.
++ */
+ private void adjust_window_position_horizontal(Gtk.Widget window) {
+- Gdk.Point cursor_right_bottom = {
++ Gdk.Point cursor_left_bottom = {
+ m_cursor_location.x,
+ m_cursor_location.y + m_cursor_location.height
+ };
+@@ -524,8 +523,8 @@ public class CandidatePanel : Gtk.Box{
+ Gtk.Allocation allocation;
+ m_toplevel.get_allocation(out allocation);
+ Gdk.Point window_right_bottom = {
+- cursor_right_bottom.x + allocation.width,
+- cursor_right_bottom.y + allocation.height
++ cursor_left_bottom.x + allocation.width,
++ cursor_left_bottom.y + allocation.height
+ };
+
+ Gdk.Rectangle monitor_area = get_monitor_geometry(window);
+@@ -536,20 +535,27 @@ public class CandidatePanel : Gtk.Box{
+ if (window_right_bottom.x > monitor_right)
+ x = monitor_right - allocation.width;
+ else
+- x = cursor_right_bottom.x;
++ x = cursor_left_bottom.x;
+ if (x < 0)
+ x = 0;
+
+ if (window_right_bottom.y > monitor_bottom)
+ y = m_cursor_location.y - allocation.height;
+ else
+- y = cursor_right_bottom.y;
++ y = cursor_left_bottom.y;
+ if (y < 0)
+ y = 0;
+
+ move(x, y);
+ }
+
++ /**
++ * adjust_window_position_vertical:
++ * @window: A Gtk.Widget of the toplevel window.
++ *
++ * Vertical writing mode but not the vertical lookup table
++ * when the allocation is emmitted.
++ */
+ private void adjust_window_position_vertical(Gtk.Widget window) {
+ /* Not sure in which top or left cursor appears
+ * in the vertical writing mode.
+@@ -611,6 +617,19 @@ public class CandidatePanel : Gtk.Box{
+ move(x, y);
+ }
+
++ private Gdk.Rectangle get_monitor_geometry(Gtk.Widget window) {
++ Gdk.Rectangle monitor_area = { 0, };
++
++ // Use get_monitor_geometry() instead of get_monitor_area().
++ // get_monitor_area() excludes docks, but the lookup window should be
++ // shown over them.
++ Gdk.Monitor monitor = window.get_display().get_monitor_at_point(
++ m_cursor_location.x,
++ m_cursor_location.y);
++ monitor_area = monitor.get_geometry();
++ return monitor_area;
++ }
++
+ #if USE_GDK_WAYLAND
+ private void realize_window(bool initial) {
+ // The custom surface can be used when the Wayland input-method
+diff --git a/ui/gtk3/emojier.vala b/ui/gtk3/emojier.vala
+index 2d8754a4..8c527eca 100644
+--- a/ui/gtk3/emojier.vala
++++ b/ui/gtk3/emojier.vala
+@@ -223,8 +223,6 @@ public class IBusEmojier : Gtk.ApplicationWindow {
+ BACKWARD,
+ }
+
+- public bool is_wayland { get; set; }
+-
+ public const uint BUTTON_CLOSE_BUTTON = 1000;
+
+ private const uint EMOJI_GRID_PAGE = 10;
+@@ -268,6 +266,8 @@ public class IBusEmojier : Gtk.ApplicationWindow {
+ private static bool m_loaded_unicode = false;
+ private static string m_warning_message = "";
+
++ private bool m_is_wayland;
++ private bool m_is_gnome = false;
+ private ThemedRGBA m_rgba;
+ private Gtk.Box m_vbox;
+ /* If emojier is emoji category list or Unicode category list,
+@@ -328,7 +328,8 @@ public class IBusEmojier : Gtk.ApplicationWindow {
+ GLib.Object(
+ type : Gtk.WindowType.POPUP
+ );
+- this.is_wayland = is_wayland;
++ m_is_wayland = is_wayland;
++ m_is_gnome = is_gnome();
+
+ // GLib.ActionEntry accepts const variables only.
+ var action = new GLib.SimpleAction.stateful(
+@@ -382,6 +383,23 @@ public class IBusEmojier : Gtk.ApplicationWindow {
+ }
+
+
++ private bool is_gnome() {
++ unowned string? desktop =
++ Environment.get_variable("XDG_CURRENT_DESKTOP");
++ if (desktop == "GNOME")
++ return true;
++ /* If ibus-dameon is launched from systemd, XDG_CURRENT_DESKTOP
++ * environment variable could be set after ibus-dameon would be
++ * launched and XDG_CURRENT_DESKTOP could be "(null)".
++ * But XDG_SESSION_DESKTOP can be set with systemd's PAM.
++ */
++ if (desktop == null || desktop == "(null)")
++ desktop = Environment.get_variable("XDG_SESSION_DESKTOP");
++ if (desktop == "gnome")
++ return true;
++ return false;
++ }
++
+ private static void reload_emoji_dict() {
+ init_emoji_dict();
+ make_emoji_dict("en");
+@@ -1807,7 +1825,7 @@ public class IBusEmojier : Gtk.ApplicationWindow {
+
+
+ private void start_rebuild_gui(bool initial_launching) {
+- if (!this.is_wayland)
++ if (!m_is_wayland)
+ return;
+ if (!initial_launching && !base.get_visible())
+ return;
+@@ -2189,17 +2207,23 @@ public class IBusEmojier : Gtk.ApplicationWindow {
+
+
+ public IBus.Text get_title_text() {
++ if (!m_loaded_unicode && m_is_gnome) {
++ unichar c = 0x26A0;
++ return new IBus.Text.from_string("%s%s %u%%".printf(
++ c.to_string(),
++ _("Loading a Unicode dictionary:"),
++ (uint)(m_unicode_percent * 100)));
++ }
+ var language = _(IBus.get_language_name(m_current_lang_id));
+ uint ncandidates = this.get_number_of_candidates();
+ string main_title = _("Emoji Choice");
+ if (m_show_unicode)
+ main_title = _("Unicode Choice");
+- var text = new IBus.Text.from_string(
+- "%s (%s) (%u / %u)".printf(
+- main_title,
+- language,
+- this.get_cursor_pos() + 1,
+- ncandidates));
++ var text = new IBus.Text.from_string("%s (%s) (%u / %u)".printf(
++ main_title,
++ language,
++ this.get_cursor_pos() + 1,
++ ncandidates));
+ int char_count = text.text.char_count();
+ int start_index = -1;
+ unowned string title = text.text;
+diff --git a/ui/gtk3/extension.vala b/ui/gtk3/extension.vala
+index a6f2e8e6..5cd1a6ad 100644
+--- a/ui/gtk3/extension.vala
++++ b/ui/gtk3/extension.vala
+@@ -3,7 +3,7 @@
+ * ibus - The Input Bus
+ *
+ * Copyright(c) 2018 Peng Huang <shawn.p.huang@gmail.com>
+- * Copyright(c) 2018-2020 Takao Fujiwara <takao.fujiwara1@gmail.com>
++ * Copyright(c) 2018-2025 Takao Fujiwara <takao.fujiwara1@gmail.com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+@@ -99,6 +99,7 @@ class ExtensionGtk : Gtk.Application {
+ private void bus_disconnected(IBus.Bus bus) {
+ debug("connection is lost.");
+ Gtk.main_quit();
++ this.quit();
+ }
+
+ private void bus_connected(IBus.Bus bus) {
+diff --git a/ui/gtk3/panelbinding.vala b/ui/gtk3/panelbinding.vala
+index f0827b3e..a2fd1441 100644
+--- a/ui/gtk3/panelbinding.vala
++++ b/ui/gtk3/panelbinding.vala
+@@ -632,6 +632,8 @@ class PanelBinding : IBus.PanelService {
+ m_emojier.key_press_cursor_horizontal(Gdk.Key.Right, 0);
+ } else {
+ m_emojier.set_annotation(annotation);
++ var text = m_emojier.get_title_text();
++ show_candidate = (text.text.get_char() == 0x26A0);
+ }
+ }
+ convert_preedit_text();
+@@ -738,10 +740,11 @@ class PanelBinding : IBus.PanelService {
+ private void show_wayland_lookup_table(IBus.Text text) {
+ m_wayland_lookup_table_is_visible = true;
+ var table = m_emojier.get_one_dimension_lookup_table();
++ bool do_show_auxiliary_text = (text.text.get_char() == 0x26A0);
+ uint ncandidates = table.get_number_of_candidates();
+ update_auxiliary_text_received(
+ text,
+- ncandidates > 0 ? true : false);
++ do_show_auxiliary_text ? true : ncandidates > 0 ? true : false);
+ update_lookup_table_received(
+ table,
+ ncandidates > 0 ? true : false);
+--
+2.49.0
+
+From c5ea35f14a65875a61dec76cf90c60988dfbfc27 Mon Sep 17 00:00:00 2001
+From: fujiwarat <takao.fujiwara1@gmail.com>
+Date: Sun, 1 Jun 2025 22:11:12 +0900
+Subject: [PATCH 1/2] src/ibusenginesimple: Improve BEPO compose sequence visuals
+
+The French (BEPO, AFNOR) keymap utilizes pseudo-deadkeys within
+U+FDD0 to U+FDD9 range, which lack readable characters but the
+symbol file, /usr/share/X11/xkb/symbols/fr, details the proposed
+expressions of the pseudo-deadkeys in the comment lines and IBus
+follows it.
+
+BUG=https://github.com/ibus/ibus/issues/2748
+BUG=https://gitlab.gnome.org/GNOME/gtk/-/merge_requests/8368
+---
+ src/ibuscomposetable.c | 28 ++++++++++++++++++----------
+ src/ibusenginesimple.c | 5 +++++
+ 2 files changed, 23 insertions(+), 10 deletions(-)
+
+diff --git a/src/ibuscomposetable.c b/src/ibuscomposetable.c
+index ffd40c75..8c58beb1 100644
+--- a/src/ibuscomposetable.c
++++ b/src/ibuscomposetable.c
+@@ -1982,6 +1982,11 @@ ibus_keysym_to_unicode (guint keysym,
+ return combined_unicode; \
+ else \
+ return isolated_unicode
++#define CASE_KEYSYM(keysym_val, unicode) \
++ case keysym_val: \
++ if (need_space) \
++ *need_space = FALSE; \
++ return unicode
+ switch (keysym) {
+ #ifdef IBUS_ENGLISH_DEAD_KEY
+ CASE (a, 0x0363, 1);
+@@ -2048,16 +2053,18 @@ ibus_keysym_to_unicode (guint keysym,
+ CASE (stroke, 0x0335, 1);
+ CASE_COMBINE (tilde, 0x0303, 0x007E, 0);
+ CASE_COMBINE (voiced_sound, 0x3099, 0x309B, 0);
+- case IBUS_KEY_Multi_key:
+- if (need_space)
+- *need_space = FALSE;
+- /* We only show the Compose key visibly when it is the
+- * only glyph in the preedit, or when it occurs in the
+- * middle of the sequence. Sadly, the official character,
+- * U+2384, COMPOSITION SYMBOL, is bit too distracting, so
+- * we use U+00B7, MIDDLE DOT.
+- */
+- return 0x00B7;
++ /* We only show the Compose key visibly when it is the
++ * only glyph in the preedit, or when it occurs in the
++ * middle of the sequence. Sadly, the official character,
++ * U+2384, COMPOSITION SYMBOL, is bit too distracting, so
++ * we use U+00B7, MIDDLE DOT.
++ */
++ CASE_KEYSYM(IBUS_KEY_Multi_key, 0x00B7);
++ /* Refer (BEPO, AFNOR) comments in /usr/share/X11/xkb/symbols/fr file. */
++ CASE_KEYSYM(0x0100FDD4, 0x00DF); /* ß */
++ CASE_KEYSYM(0x0100FDD5, 0x1D49); /* ᵉ */
++ CASE_KEYSYM(0x0100FDD7, 0x221E); /* ∞ */
++ CASE_KEYSYM(0x0100FDD8, 0x2015); /* ― */
+ default:;
+ if (need_space)
+ *need_space = FALSE;
+@@ -2065,4 +2072,5 @@ ibus_keysym_to_unicode (guint keysym,
+ return 0x0;
+ #undef CASE
+ #undef CASE_COMBINE
++#undef CASE_KEYSYM
+ }
+diff --git a/src/ibusenginesimple.c b/src/ibusenginesimple.c
+index 15c45c2e..784ed180 100644
+--- a/src/ibusenginesimple.c
++++ b/src/ibusenginesimple.c
+@@ -471,6 +471,11 @@ ibus_engine_simple_update_preedit_text (IBusEngineSimple *simple)
+ g_string_append_c (s, ' ');
+ g_string_append_unichar (s, ch);
+ }
++ } else if (keysym >= 0x0100fdd0 && keysym <= 0x0100fdd9) {
++ /* A BEPOs pseudo deadkey */
++ if (!(ch = ibus_keysym_to_unicode (keysym, FALSE, NULL)))
++ ch = 0x00B7;
++ g_string_append_unichar (s, ch);
+ } else {
+ ch = ibus_keyval_to_unicode (keysym);
+ if (ch) {
+--
+2.49.0
+
+From b9eaa72e4471b2415e59f47ded4a987ab9057c99 Mon Sep 17 00:00:00 2001
+From: fujiwarat <takao.fujiwara1@gmail.com>
+Date: Sun, 1 Jun 2025 22:14:00 +0900
+Subject: [PATCH 2/2] src: Do not load en-US compose table by default
+
+GTK4 has load en-US compose table in case of no user compose file.
+GTK4 now does not load the en-US compose table by default in case
+that the user compose file exists or el-GR locale is used since
+4.18.5 and new keyword "include %L" can loads the en-US compose table.
+If the old compose cache version 4 is found, the compose file
+will be rewritten to add the line of "include %L".
+IBus also follows GTK 4.18.5.
+
+Fixes: https://github.com/ibus/ibus/commit/28b357d
+Fixes: https://github.com/ibus/ibus/commit/09ea069
+BUG=https://github.com/ibus/ibus/issues/2706
+BUG=https://github.com/ibus/ibus/issues/2727
+---
+ src/gencomposetable.c | 9 +-
+ src/ibuscomposetable.c | 275 ++++++++++++++++++++++++++++------
+ src/ibuscomposetable.h | 24 ++-
+ src/ibusengine.h | 5 +-
+ src/ibusenginesimple.c | 139 ++++++++++++-----
+ src/ibusenginesimpleprivate.h | 19 ++-
+ src/tests/ibus-compose.c | 6 +-
+ 7 files changed, 414 insertions(+), 95 deletions(-)
+
+diff --git a/src/gencomposetable.c b/src/gencomposetable.c
+index 35d6ce83..161b89e0 100644
+--- a/src/gencomposetable.c
++++ b/src/gencomposetable.c
+@@ -1,8 +1,8 @@
+ /* -*- mode: C; c-basic-offset: 4; indent-tabs-mode: nil; -*- */
+ /* vim:set et sts=4: */
+ /* ibus - The Input Bus
+- * Copyright (C) 2023 Takao Fujiwara <takao.fujiwara1@gmail.com>
+- * Copyright (C) 2023 Red Hat, Inc.
++ * Copyright (C) 2023-2025 Takao Fujiwara <takao.fujiwara1@gmail.com>
++ * Copyright (C) 2023-2025 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
+@@ -76,6 +76,7 @@ main (int argc, char *argv[])
+ char * const *sys_lang = NULL;
+ char *path = NULL;
+ IBusComposeTableEx *compose_table;
++ guint16 saved_version = 0;
+ char *basename = NULL;
+
+ path = g_strdup ("./Compose");
+@@ -97,13 +98,15 @@ main (int argc, char *argv[])
+ g_debug ("Create a cache of %s", path);
+ }
+ g_setenv ("IBUS_COMPOSE_CACHE_DIR", ".", TRUE);
+- compose_table = ibus_compose_table_load_cache (path);
++ compose_table = ibus_compose_table_load_cache (path, &saved_version);
+ if (!compose_table &&
+ (compose_table = ibus_compose_table_new_with_file (path, NULL))
+ == NULL) {
+ g_warning ("Failed to generate the compose table.");
+ return 1;
+ }
++ if (saved_version > 0)
++ g_warning ("Old cache was updated.");
+ g_free (path);
+ basename = g_strdup_printf ("%08x.cache", compose_table->id);
+ g_debug ("Saving cache id %s", basename);
+diff --git a/src/ibuscomposetable.c b/src/ibuscomposetable.c
+index 8c58beb1..2f76ef61 100644
+--- a/src/ibuscomposetable.c
++++ b/src/ibuscomposetable.c
+@@ -24,6 +24,7 @@
+
+ #include <glib.h>
+ #include <glib/gstdio.h>
++#include <glib/gi18n-lib.h>
+ #include <stdlib.h>
+ #include <string.h>
+
+@@ -38,7 +39,7 @@
+
+
+ #define IBUS_COMPOSE_TABLE_MAGIC "IBusComposeTable"
+-#define IBUS_COMPOSE_TABLE_VERSION (4)
++#define IBUS_COMPOSE_TABLE_VERSION (5)
+ #define IBUS_MAX_COMPOSE_ALGORITHM_LEN 9
+
+ typedef struct {
+@@ -283,10 +284,15 @@ expand_include_path (const char *include_path) {
+ break;
+ }
+ case 'L': /* locale compose file */
+- g_warning ("while handling XCompose include target %s, found "
+- "redundant %%L include; the include has been "
+- "ignored", include_path);
+- goto fail;
++ out = g_strdup ("%L");
++ head = i + 2;
++ while (*head || *head == ' ' || *head == '\t') head++;
++ if (*head != '\0') {
++ g_warning ("\"%s\" after \"%%L\" is not supported in "
++ "XCompose include target.", head);
++ goto fail;
++ }
++ break;
+ case 'S': /* system compose dir */
+ o = out;
+ former = g_strndup (head, i - head);
+@@ -413,7 +419,8 @@ get_en_compose_file (void)
+
+ static GList *
+ ibus_compose_list_parse_file (const char *compose_file,
+- int *max_compose_len)
++ int *max_compose_len,
++ gboolean *can_load_en_us)
+ {
+ char *contents = NULL;
+ char **lines = NULL;
+@@ -423,6 +430,7 @@ ibus_compose_list_parse_file (const char *compose_file,
+ int i;
+
+ g_assert (max_compose_len);
++ g_assert (can_load_en_us);
+
+ if (!g_file_get_contents (compose_file, &contents, &length, &error)) {
+ g_error ("%s", error->message);
+@@ -438,6 +446,11 @@ ibus_compose_list_parse_file (const char *compose_file,
+ parse_compose_line (&compose_list, lines[i], &compose_len, &include);
+ if (*max_compose_len < compose_len)
+ *max_compose_len = compose_len;
++ if (!g_strcmp0 (include, "%L")) {
++ *can_load_en_us = TRUE;
++ g_clear_pointer (&include, g_free);
++ continue;
++ }
+ if (include && *include) {
+ GStatBuf buf_include = { 0, };
+ GStatBuf buf_parent = { 0, };
+@@ -486,7 +499,8 @@ ibus_compose_list_parse_file (const char *compose_file,
+ if (include && *include) {
+ GList *rest = ibus_compose_list_parse_file (
+ include,
+- max_compose_len);
++ max_compose_len,
++ can_load_en_us);
+ if (rest) {
+ compose_list = g_list_concat (compose_list, rest);
+ }
+@@ -504,13 +518,16 @@ ibus_compose_list_check_duplicated_with_en (GList *compose_list,
+ int max_compose_len,
+ GSList *compose_tables)
+ {
++ guint *keysyms;
++ GString *output;
+ GList *list;
+- static guint *keysyms;
+ GList *removed_list = NULL;
+ IBusComposeData *compose_data;
+- GString *output = g_string_new ("");
+
++ if (!compose_list)
++ return NULL;
+ keysyms = g_new (guint, max_compose_len + 1);
++ output = g_string_new ("");
+
+ for (list = compose_list; list != NULL; list = list->next) {
+ int i;
+@@ -694,6 +711,8 @@ ibus_compose_list_check_duplicated_with_own (GList *compose_list,
+ int i;
+ GList *removed_list = NULL;
+
++ if (!compose_list)
++ return NULL;
+ for (list = compose_list; list != NULL; list = list->next) {
+ gboolean is_different_value = FALSE;
+ if (!list->next)
+@@ -921,6 +940,7 @@ ibus_compose_table_serialize (IBusComposeTableEx *compose_table,
+ guint16 max_seq_len;
+ guint16 index_stride;
+ guint16 n_seqs;
++ guint8 compose_type = 0;
+ gsize n_seqs_32bit = 0;
+ gsize second_size = 0;
+ GVariant *variant_data = NULL;
+@@ -934,8 +954,9 @@ ibus_compose_table_serialize (IBusComposeTableEx *compose_table,
+ max_seq_len = compose_table->max_seq_len;
+ index_stride = max_seq_len + 2;
+ n_seqs = compose_table->n_seqs;
++ compose_type = compose_table->can_load_en_us ? 1 : 0;
+
+- g_return_val_if_fail (max_seq_len, NULL);
++ g_return_val_if_fail (max_seq_len || compose_type, NULL);
+
+ if (n_seqs) {
+ g_return_val_if_fail (compose_table->data, NULL);
+@@ -965,7 +986,7 @@ ibus_compose_table_serialize (IBusComposeTableEx *compose_table,
+ n_seqs_32bit = compose_table->priv->first_n_seqs;
+ second_size = compose_table->priv->second_size;
+ }
+- if (!n_seqs && !n_seqs_32bit) {
++ if (!n_seqs && !n_seqs_32bit && !compose_type) {
+ g_warning ("ComposeTable has not key sequences.");
+ goto out_serialize;
+ } else if (n_seqs_32bit && !second_size) {
+@@ -1028,7 +1049,7 @@ ibus_compose_table_serialize (IBusComposeTableEx *compose_table,
+ sizeof (guint32));
+ g_assert (variant_data_32bit_first && variant_data_32bit_second);
+ }
+- variant_table = g_variant_new ("(sqqqqqvvv)",
++ variant_table = g_variant_new ("(sqqqqqvvvy)",
+ header,
+ version,
+ max_seq_len,
+@@ -1037,7 +1058,8 @@ ibus_compose_table_serialize (IBusComposeTableEx *compose_table,
+ second_size,
+ variant_data,
+ variant_data_32bit_first,
+- variant_data_32bit_second);
++ variant_data_32bit_second,
++ compose_type);
+ return g_variant_ref_sink (variant_table);
+
+ out_serialize:
+@@ -1061,7 +1083,8 @@ ibus_compose_table_find (gconstpointer data1,
+
+ IBusComposeTableEx *
+ ibus_compose_table_deserialize (const char *contents,
+- gsize length)
++ gsize length,
++ guint16 *saved_version)
+ {
+ IBusComposeTableEx *retval = NULL;
+ GVariantType *type;
+@@ -1070,9 +1093,9 @@ ibus_compose_table_deserialize (const char *contents,
+ GVariant *variant_data_32bit_second = NULL;
+ GVariant *variant_table = NULL;
+ const char *header = NULL;
+- guint16 version = 0;
+ guint16 max_seq_len = 0;
+ guint16 n_seqs = 0;
++ guint8 compose_type = 0;
+ guint16 n_seqs_32bit = 0;
+ guint16 second_size = 0;
+ guint16 index_stride;
+@@ -1083,7 +1106,9 @@ ibus_compose_table_deserialize (const char *contents,
+
+ g_return_val_if_fail (contents != NULL, NULL);
+ g_return_val_if_fail (length > 0, NULL);
++ g_assert (saved_version);
+
++ *saved_version = 0;
+ /* Check the cache version at first before load whole the file content. */
+ type = g_variant_type_new ("(sq)");
+ variant_table = g_variant_new_from_data (type,
+@@ -1100,25 +1125,24 @@ ibus_compose_table_deserialize (const char *contents,
+ }
+
+ g_variant_ref_sink (variant_table);
+- g_variant_get (variant_table, "(&sq)", &header, &version);
++ g_variant_get (variant_table, "(&sq)", &header, saved_version);
+
+ if (g_strcmp0 (header, IBUS_COMPOSE_TABLE_MAGIC) != 0) {
+ g_warning ("cache is not IBusComposeTable.");
+ goto out_load_cache;
+ }
+
+- if (version != IBUS_COMPOSE_TABLE_VERSION) {
++ if (*saved_version != IBUS_COMPOSE_TABLE_VERSION) {
+ g_warning ("cache version is different: %u != %u",
+- version, IBUS_COMPOSE_TABLE_VERSION);
++ *saved_version, IBUS_COMPOSE_TABLE_VERSION);
+ goto out_load_cache;
+ }
+
+- version = 0;
+ header = NULL;
+ g_variant_unref (variant_table);
+ variant_table = NULL;
+
+- type = g_variant_type_new ("(sqqqqqvvv)");
++ type = g_variant_type_new ("(sqqqqqvvvy)");
+ variant_table = g_variant_new_from_data (type,
+ contents,
+ length,
+@@ -1133,7 +1157,7 @@ ibus_compose_table_deserialize (const char *contents,
+ }
+
+ g_variant_ref_sink (variant_table);
+- g_variant_get (variant_table, "(&sqqqqqvvv)",
++ g_variant_get (variant_table, "(&sqqqqqvvvy)",
+ NULL,
+ NULL,
+ &max_seq_len,
+@@ -1142,12 +1166,15 @@ ibus_compose_table_deserialize (const char *contents,
+ &second_size,
+ &variant_data,
+ &variant_data_32bit_first,
+- &variant_data_32bit_second);
++ &variant_data_32bit_second,
++ &compose_type);
+
+ if (max_seq_len == 0 || (n_seqs == 0 && n_seqs_32bit == 0)) {
+- g_warning ("cache size is not correct %d %d %d",
+- max_seq_len, n_seqs, n_seqs_32bit);
+- goto out_load_cache;
++ if (!compose_type) {
++ g_warning ("cache size is not correct %d %d %d",
++ max_seq_len, n_seqs, n_seqs_32bit);
++ goto out_load_cache;
++ }
+ }
+
+ if (n_seqs && variant_data) {
+@@ -1168,6 +1195,7 @@ ibus_compose_table_deserialize (const char *contents,
+ retval->data = data;
+ retval->max_seq_len = max_seq_len;
+ retval->n_seqs = n_seqs;
++ retval->can_load_en_us = compose_type ? TRUE : FALSE;
+
+ if (n_seqs_32bit && !second_size) {
+ g_warning ("32bit key sequences are loaded but the values " \
+@@ -1192,7 +1220,7 @@ ibus_compose_table_deserialize (const char *contents,
+ goto out_load_cache;
+ }
+ }
+- if (!data && !data_32bit_first) {
++ if (!data && !data_32bit_first && !compose_type) {
+ g_warning ("cache data is null.");
+ goto out_load_cache;
+ }
+@@ -1235,7 +1263,8 @@ out_load_cache:
+
+
+ IBusComposeTableEx *
+-ibus_compose_table_load_cache (const gchar *compose_file)
++ibus_compose_table_load_cache (const gchar *compose_file,
++ guint16 *saved_version)
+ {
+ IBusComposeTableEx *retval = NULL;
+ guint32 hash;
+@@ -1246,6 +1275,8 @@ ibus_compose_table_load_cache (const gchar *compose_file)
+ gsize length = 0;
+ GError *error = NULL;
+
++ g_assert (saved_version);
++ *saved_version = 0;
+ do {
+ hash = g_str_hash (compose_file);
+ if ((path = ibus_compose_hash_get_cache_path (hash)) == NULL)
+@@ -1270,7 +1301,9 @@ ibus_compose_table_load_cache (const gchar *compose_file)
+ break;
+ }
+
+- retval = ibus_compose_table_deserialize (contents, length);
++ retval = ibus_compose_table_deserialize (contents,
++ length,
++ saved_version);
+ if (retval == NULL) {
+ g_warning ("Failed to load the cache file: %s", path);
+ } else {
+@@ -1369,7 +1402,7 @@ ibus_compose_table_new_with_list (GList *compose_list,
+ IBusComposeData *compose_data = NULL;
+ IBusComposeTableEx *retval = NULL;
+
+- g_return_val_if_fail (compose_list != NULL, NULL);
++ g_return_val_if_fail (compose_list, NULL);
+
+ s_size_total = g_list_length (compose_list);
+ s_size_16bit = s_size_total;
+@@ -1498,6 +1531,7 @@ ibus_compose_table_new_with_list (GList *compose_list,
+ retval->n_seqs = s_size_16bit;
+ retval->id = hash;
+ retval->rawdata = rawdata;
++ retval->can_load_en_us = FALSE;
+ if (s_size_total > s_size_16bit) {
+ retval->priv = g_new0 (IBusComposeTablePrivate, 1);
+ retval->priv->data_first = ibus_compose_seqs_32bit_first;
+@@ -1515,6 +1549,8 @@ ibus_compose_table_new_with_file (const gchar *compose_file,
+ GSList *compose_tables)
+ {
+ GList *compose_list = NULL;
++ gboolean can_load_en_us = FALSE;
++ gboolean can_load_en_us_by_any = FALSE;
+ IBusComposeTableEx *compose_table;
+ int max_compose_len = 0;
+ int n_index_stride = 0;
+@@ -1522,13 +1558,29 @@ ibus_compose_table_new_with_file (const gchar *compose_file,
+ g_assert (compose_file != NULL);
+
+ compose_list = ibus_compose_list_parse_file (compose_file,
+- &max_compose_len);
+- if (compose_list == NULL)
++ &max_compose_len,
++ &can_load_en_us);
++ if (compose_list == NULL && !can_load_en_us)
+ return NULL;
+ n_index_stride = max_compose_len + 2;
+- compose_list = ibus_compose_list_check_duplicated_with_en (compose_list,
+- max_compose_len,
+- compose_tables);
++ can_load_en_us_by_any = can_load_en_us;
++ if (!can_load_en_us_by_any) {
++ GSList *l = compose_tables;
++ while (l) {
++ IBusComposeTableEx *table = l->data;
++ if (table->can_load_en_us) {
++ can_load_en_us_by_any = TRUE;
++ break;
++ }
++ l = l->next;
++ }
++ }
++ if (can_load_en_us_by_any) {
++ compose_list = ibus_compose_list_check_duplicated_with_en (
++ compose_list,
++ max_compose_len,
++ compose_tables);
++ }
+ compose_list = g_list_sort_with_data (
+ compose_list,
+ (GCompareDataFunc) ibus_compose_data_compare,
+@@ -1539,8 +1591,19 @@ ibus_compose_table_new_with_file (const gchar *compose_file,
+ max_compose_len);
+
+ if (compose_list == NULL) {
+- g_warning ("compose file %s does not include any keys besides keys "
+- "in en-us compose file", compose_file);
++ g_message ("compose file %s does not include any keys besides keys "
++ "in en-us compose file.\n", compose_file);
++ if (can_load_en_us) {
++ if (!(compose_table = g_new0 (IBusComposeTableEx, 1))) {
++ g_warning ("Failed to alloc IBusComposeTableEx for %s.",
++ compose_file);
++ } else {
++ compose_table->id = g_str_hash (compose_file);
++ compose_table->can_load_en_us = can_load_en_us;
++ return compose_table;
++ }
++ }
++
+ return NULL;
+ }
+
+@@ -1552,6 +1615,8 @@ ibus_compose_table_new_with_file (const gchar *compose_file,
+ max_compose_len,
+ n_index_stride,
+ g_str_hash (compose_file));
++ if (compose_table)
++ compose_table->can_load_en_us = can_load_en_us;
+
+ g_list_free_full (compose_list,
+ (GDestroyNotify) ibus_compose_list_element_free);
+@@ -1560,6 +1625,73 @@ ibus_compose_table_new_with_file (const gchar *compose_file,
+ }
+
+
++void
++ibus_compose_table_free (IBusComposeTableEx *compose_table)
++{
++ g_return_if_fail (compose_table);
++ g_clear_pointer (&compose_table->priv, g_free);
++ compose_table->data = NULL;
++ compose_table->max_seq_len = 0;
++ compose_table->n_seqs = 0;
++ compose_table->id = 0;
++ g_clear_pointer (&compose_table->rawdata, g_free);
++ compose_table->can_load_en_us = FALSE;
++ g_free (compose_table);
++
++}
++
++
++static gboolean
++rewrite_compose_file (const char *compose_file)
++{
++ static const char *prefix =
++ "# IBus has rewritten this file to add the line:\n"
++ "\n"
++ "include \"%L\"\n"
++ "\n"
++ "# This is necessary to add your own Compose sequences\n"
++ "# in addition to the builtin sequences of IBus. If this\n"
++ "# is not what you want, just remove that line.\n"
++ "#\n"
++ "# A backup of the previous file contents has been made.\n"
++ "\n"
++ "\n";
++
++ char *content = NULL;
++ gsize content_len;
++ GFile *file = NULL;
++ GOutputStream *stream = NULL;
++ gboolean ret = FALSE;
++
++ if (!g_file_get_contents (compose_file, &content, &content_len, NULL))
++ goto out;
++
++ file = g_file_new_for_path (compose_file);
++ stream = G_OUTPUT_STREAM (g_file_replace (file, NULL, TRUE, 0, NULL, NULL));
++
++ if (stream == NULL)
++ goto out;
++
++ if (!g_output_stream_write (stream, prefix, strlen (prefix), NULL, NULL))
++ goto out;
++
++ if (!g_output_stream_write (stream, content, content_len, NULL, NULL))
++ goto out;
++
++ if (!g_output_stream_close (stream, NULL, NULL))
++ goto out;
++
++ ret = TRUE;
++
++out:
++ g_clear_object (&stream);
++ g_clear_object (&file);
++ g_clear_pointer (&content, g_free);
++
++ return ret;
++}
++
++
+ /* if ibus_compose_seqs[N - 1] is an outputed compose character,
+ * ibus_compose_seqs[N * 2 - 1] is also an outputed compose character.
+ * and ibus_compose_seqs[0] to ibus_compose_seqs[0 + N - 3] are the
+@@ -1609,13 +1741,17 @@ ibus_compose_table_list_add_array (GSList *compose_tables,
+
+ GSList *
+ ibus_compose_table_list_add_file (GSList *compose_tables,
+- const gchar *compose_file)
++ const gchar *compose_file,
++ GError **error)
+ {
+ guint32 hash;
+ IBusComposeTableEx *compose_table;
++ guint16 saved_version = 0;
+
+- g_return_val_if_fail (compose_file != NULL, compose_tables);
++ g_return_val_if_fail (compose_file, compose_tables);
++ g_assert (error);
+
++ *error = NULL;
+ hash = g_str_hash (compose_file);
+ if (g_slist_find_custom (compose_tables,
+ GINT_TO_POINTER (hash),
+@@ -1623,15 +1759,61 @@ ibus_compose_table_list_add_file (GSList *compose_tables,
+ return compose_tables;
+ }
+
+- compose_table = ibus_compose_table_load_cache (compose_file);
++ compose_table = ibus_compose_table_load_cache (compose_file,
++ &saved_version);
+ if (compose_table != NULL)
+ return g_slist_prepend (compose_tables, compose_table);
+
+- if ((compose_table = ibus_compose_table_new_with_file (compose_file,
+- compose_tables))
++parse:
++ if ((compose_table = ibus_compose_table_new_with_file (compose_file,
++ compose_tables))
+ == NULL) {
+- return compose_tables;
+- }
++ return compose_tables;
++ }
++ if (saved_version > 0 && saved_version < 5 &&
++ !compose_table->can_load_en_us && compose_table->n_seqs < 100) {
++ if (rewrite_compose_file (compose_file)) {
++ g_assert ((*error) == NULL);
++ *error = g_error_new (
++ IBUS_COMPOSE_ERROR,
++ IBUS_ENGINE_MSG_CODE_UPDATE_COMPOSE_TABLE,
++ _("Since IBus 1.5.33, Compose files replace the "
++ "builtin\n"
++ "compose sequences. To keep them and add your own\n"
++ "sequences on top, the line:\n"
++ "\n"
++ " include \"%%L\"\n"
++ "\n"
++ "has been added to the Compose file:\n%s.\n"),
++ compose_file);
++ ibus_compose_table_free (compose_table);
++ goto parse;
++ } else {
++ gchar *error_message1;
++ gchar *error_message2;
++ if (*error) {
++ error_message1 = g_strdup_printf ("%s\n", (*error)->message);
++ g_error_free (*error);
++ } else {
++ error_message1 = g_strdup ("");
++ }
++ error_message2 = g_strdup_printf (
++ _("Since IBus 1.5.33, Compose files replace the "
++ "builtin\n"
++ "compose sequences. To keep them and add your own\n"
++ "sequences on top, you need to add the line:\n"
++ "\n"
++ " include \"%%L\"\n"
++ "\n"
++ "to the Compose file:\n%s."), compose_file);
++ *error = g_error_new (
++ IBUS_COMPOSE_ERROR,
++ IBUS_ENGINE_MSG_CODE_UPDATE_COMPOSE_TABLE,
++ "%s%s", error_message1, error_message2);
++ g_free (error_message1);
++ g_free (error_message2);
++ }
++ }
+
+ ibus_compose_table_save_cache (compose_table);
+ return g_slist_prepend (compose_tables, compose_table);
+@@ -2074,3 +2256,10 @@ ibus_keysym_to_unicode (guint keysym,
+ #undef CASE_COMBINE
+ #undef CASE_KEYSYM
+ }
++
++
++GQuark
++ibus_compose_error_quark (void)
++{
++ return g_quark_from_static_string ("ibus-compose-error-quark");
++}
+diff --git a/src/ibuscomposetable.h b/src/ibuscomposetable.h
+index 32e337f7..2068657d 100644
+--- a/src/ibuscomposetable.h
++++ b/src/ibuscomposetable.h
+@@ -2,7 +2,7 @@
+ /* vim:set et sts=4: */
+ /* ibus - The Input Bus
+ * Copyright (C) 2013-2014 Peng Huang <shawn.p.huang@gmail.com>
+- * Copyright (C) 2013-2023 Takao Fujiwara <takao.fujiwara1@gmail.com>
++ * Copyright (C) 2013-2025 Takao Fujiwara <takao.fujiwara1@gmail.com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+@@ -23,8 +23,19 @@
+ #ifndef __IBUS_COMPOSETABLE_H_
+ #define __IBUS_COMPOSETABLE_H_
+
+-#include <glib.h>
++/**
++ * SECTION: ibuscomposetable
++ * @short_description: Compose table handling based on libX11
++ * @title: IBusComposeTable
++ * @stability: Unstable
++ *
++ * Generate #IBusComposeTableEx with the compose file or array.
++ *
++ * see_also: #IBusEngineSimple
++ *
++ */
+
++#include <glib.h>
+
+ G_BEGIN_DECLS
+
+@@ -53,6 +64,7 @@ struct _IBusComposeTableEx
+ gint n_seqs;
+ guint32 id;
+ char *rawdata;
++ gboolean can_load_en_us;
+ };
+
+
+@@ -71,8 +83,11 @@ IBusComposeTableEx *
+ ibus_compose_table_new_with_file (const gchar *compose_file,
+ GSList
+ *compose_tables);
++void ibus_compose_table_free (IBusComposeTableEx
++ *compose_table);
+ IBusComposeTableEx *
+- ibus_compose_table_load_cache (const gchar *compose_file);
++ ibus_compose_table_load_cache (const gchar *compose_file,
++ guint16 *saved_version);
+ void ibus_compose_table_save_cache (IBusComposeTableEx
+ *compose_table);
+ GSList * ibus_compose_table_list_add_array
+@@ -84,7 +99,8 @@ GSList * ibus_compose_table_list_add_array
+ gint n_seqs);
+ GSList * ibus_compose_table_list_add_file (GSList
+ *compose_tables,
+- const gchar *compose_file);
++ const gchar *compose_file,
++ GError **error);
+ GSList * ibus_compose_table_list_add_table (GSList
+ *compose_tables,
+ IBusComposeTableEx
+diff --git a/src/ibusengine.h b/src/ibusengine.h
+index cdc96331..6d0c1d1f 100644
+--- a/src/ibusengine.h
++++ b/src/ibusengine.h
+@@ -71,6 +71,8 @@ G_BEGIN_DECLS
+ * @IBUS_ENGINE_MSG_CODE_GENERAL: Generic message for Engine
+ * @IBUS_ENGINE_MSG_CODE_INVALID_COMPOSE_SEQUENCE: User's typing failure
+ * against the definition of the compose files.
++ * @IBUS_ENGINE_MSG_CODE_UPDATE_COMPOSE_TABLE: Notification about new
++ * behaviors or attentions when the compose table version is changed.
+ *
+ * Message codes in the `IBusMessageDomain` domain for Engine
+ * See also #IBusMessage, ibus_engine_send_message()
+@@ -81,7 +83,8 @@ G_BEGIN_DECLS
+ typedef enum
+ {
+ IBUS_ENGINE_MSG_CODE_GENERAL,
+- IBUS_ENGINE_MSG_CODE_INVALID_COMPOSE_SEQUENCE
++ IBUS_ENGINE_MSG_CODE_INVALID_COMPOSE_SEQUENCE,
++ IBUS_ENGINE_MSG_CODE_UPDATE_COMPOSE_TABLE
+ } IBusEngineMsgCode;
+
+ typedef struct _IBusEngine IBusEngine;
+diff --git a/src/ibusenginesimple.c b/src/ibusenginesimple.c
+index 784ed180..f114da8c 100644
+--- a/src/ibusenginesimple.c
++++ b/src/ibusenginesimple.c
+@@ -139,6 +139,7 @@ ibus_engine_simple_class_init (IBusEngineSimpleClass *class)
+ GError *error = NULL;
+ const char *contents;
+ gsize length = 0;
++ guint16 saved_version = 0;
+
+ ibus_object_class->destroy =
+ (IBusObjectDestroyFunc) ibus_engine_simple_destroy;
+@@ -168,7 +169,14 @@ ibus_engine_simple_class_init (IBusEngineSimpleClass *class)
+ return;
+ }
+ contents = g_bytes_get_data (data, &length);
+- en_compose_table = ibus_compose_table_deserialize (contents, length);
++ en_compose_table = ibus_compose_table_deserialize (contents,
++ length,
++ &saved_version);
++ if (!en_compose_table && saved_version) {
++ g_warning ("Failed to parse the builtin compose due to the different "
++ "version %u. Please rebuild IBus resource files.",
++ saved_version);
++ }
+ }
+
+
+@@ -237,7 +245,8 @@ ibus_engine_simple_destroy (IBusEngineSimple *simple)
+
+ static void
+ ibus_engine_simple_send_message_with_code (IBusEngineSimple *simple,
+- IBusEngineMsgCode code)
++ IBusEngineMsgCode code,
++ GError *error)
+ {
+ IBusEngineSimplePrivate *priv;
+ IBusMessage *message;
+@@ -245,18 +254,33 @@ ibus_engine_simple_send_message_with_code (IBusEngineSimple *simple,
+ g_return_if_fail (IBUS_IS_ENGINE_SIMPLE (simple));
+
+ priv = simple->priv;
+- priv->do_inform_user_error = TRUE;
+- message = ibus_message_new (
+- IBUS_MESSAGE_DOMAIN_ENGINE,
+- code,
+- _("Detect unregistered character in your compose sequence"),
+- _("The character you just input is not recognized as a valid " \
+- "part of the currently active compose sequence and the " \
+- "character was cancelled. Try inputting the correct character " \
+- "to the compose sequence again, or press Escape key to " \
+- "terminate whole the compose sequence."),
+- "timeout", 5,
+- NULL);
++ switch (code) {
++ case IBUS_ENGINE_MSG_CODE_INVALID_COMPOSE_SEQUENCE:
++ priv->do_inform_user_error = TRUE;
++ message = ibus_message_new (
++ IBUS_MESSAGE_DOMAIN_ENGINE,
++ code,
++ _("Detect unregistered character in your compose sequence"),
++ _("The character you just input is not recognized as a valid " \
++ "part of the currently active compose sequence and the " \
++ "character was cancelled. Try inputting the correct " \
++ "character to the compose sequence again, or press Escape " \
++ "key to terminate whole the compose sequence."),
++ "timeout", 5,
++ NULL);
++ break;
++ case IBUS_ENGINE_MSG_CODE_UPDATE_COMPOSE_TABLE:
++ message = ibus_message_new (
++ IBUS_MESSAGE_DOMAIN_ENGINE,
++ code,
++ _("Compose file update"),
++ error ? error->message : "",
++ "timeout", 120,
++ NULL);
++ break;
++ default:
++ g_assert_not_reached ();
++ }
+ ibus_engine_send_message (IBUS_ENGINE (simple), message);
+ }
+
+@@ -814,7 +838,8 @@ no_sequence_matches (IBusEngineSimple *simple,
+ /* Invalid sequence */
+ ibus_engine_simple_send_message_with_code (
+ simple,
+- IBUS_ENGINE_MSG_CODE_INVALID_COMPOSE_SEQUENCE);
++ IBUS_ENGINE_MSG_CODE_INVALID_COMPOSE_SEQUENCE,
++ NULL);
+ ibus_engine_simple_update_preedit_text (simple);
+ return TRUE;
+ }
+@@ -958,6 +983,7 @@ ibus_engine_simple_check_all_compose_table (IBusEngineSimple *simple,
+ GString *output = g_string_new ("");
+ gboolean success = FALSE;
+ gboolean is_32bit = FALSE;
++ gboolean can_load_en_us = FALSE;
+ gunichar output_char = '\0';
+
+ /* GtkIMContextSimple output the first compose char in case of
+@@ -969,27 +995,40 @@ ibus_engine_simple_check_all_compose_table (IBusEngineSimple *simple,
+ G_LOCK (global_tables);
+ tmp_list = global_tables;
+ while (tmp_list) {
++ IBusComposeTableEx *compose_table = tmp_list->data;
++ if (compose_table->can_load_en_us)
++ can_load_en_us = TRUE;
++ /* If global_tables includes en_compose_table only, i.e. no user
++ * or locale compose tables, en_compose_table is used.
++ * If not, en_compose_table is used in case one of the other compose
++ * tables has can_load_en_us = %TRUE, i.e. the table file has
++ * the line of 'include "%L"'.
++ * en_compose_table is always appended to the last of global_tables.
++ */
++ if ((compose_table == en_compose_table) && global_tables->next != NULL
++ && !can_load_en_us) {
++ tmp_list = tmp_list->next;
++ continue;
++ }
+ is_32bit = FALSE;
+- if (ibus_compose_table_check (
+- (IBusComposeTableEx *)tmp_list->data,
+- priv->compose_buffer,
+- n_compose,
+- &compose_finish,
+- &compose_match,
+- output,
+- is_32bit)) {
++ if (ibus_compose_table_check (compose_table,
++ priv->compose_buffer,
++ n_compose,
++ &compose_finish,
++ &compose_match,
++ output,
++ is_32bit)) {
+ success = TRUE;
+ break;
+ }
+ is_32bit = TRUE;
+- if (ibus_compose_table_check (
+- (IBusComposeTableEx *)tmp_list->data,
+- priv->compose_buffer,
+- n_compose,
+- &compose_finish,
+- &compose_match,
+- output,
+- is_32bit)) {
++ if (ibus_compose_table_check (compose_table,
++ priv->compose_buffer,
++ n_compose,
++ &compose_finish,
++ &compose_match,
++ output,
++ is_32bit)) {
+ success = TRUE;
+ break;
+ }
+@@ -1095,7 +1134,8 @@ ibus_engine_simple_process_key_event (IBusEngine *engine,
+ /* invalid hex sequence */
+ ibus_engine_simple_send_message_with_code (
+ simple,
+- IBUS_ENGINE_MSG_CODE_INVALID_COMPOSE_SEQUENCE);
++ IBUS_ENGINE_MSG_CODE_INVALID_COMPOSE_SEQUENCE,
++ NULL);
+ g_string_set_size (priv->tentative_match, 0);
+ g_clear_pointer (&priv->tentative_emoji, g_free);
+ priv->in_hex_sequence = FALSE;
+@@ -1120,7 +1160,8 @@ ibus_engine_simple_process_key_event (IBusEngine *engine,
+ /* invalid hex sequence */
+ ibus_engine_simple_send_message_with_code (
+ simple,
+- IBUS_ENGINE_MSG_CODE_INVALID_COMPOSE_SEQUENCE);
++ IBUS_ENGINE_MSG_CODE_INVALID_COMPOSE_SEQUENCE,
++ NULL);
+ g_string_set_size (priv->tentative_match, 0);
+ g_clear_pointer (&priv->tentative_emoji, g_free);
+ priv->in_hex_sequence = FALSE;
+@@ -1171,7 +1212,8 @@ ibus_engine_simple_process_key_event (IBusEngine *engine,
+ /* invalid hex sequence */
+ ibus_engine_simple_send_message_with_code (
+ simple,
+- IBUS_ENGINE_MSG_CODE_INVALID_COMPOSE_SEQUENCE);
++ IBUS_ENGINE_MSG_CODE_INVALID_COMPOSE_SEQUENCE,
++ NULL);
+ g_string_set_size (priv->tentative_match, 0);
+ g_clear_pointer (&priv->tentative_emoji, g_free);
+ priv->in_hex_sequence = FALSE;
+@@ -1272,7 +1314,8 @@ ibus_engine_simple_process_key_event (IBusEngine *engine,
+ if (n_compose > 0) {
+ ibus_engine_simple_send_message_with_code (
+ simple,
+- IBUS_ENGINE_MSG_CODE_INVALID_COMPOSE_SEQUENCE);
++ IBUS_ENGINE_MSG_CODE_INVALID_COMPOSE_SEQUENCE,
++ NULL);
+ g_string_set_size (priv->tentative_match, 0);
+ priv->in_hex_sequence = FALSE;
+ priv->compose_buffer[0] = 0;
+@@ -1339,7 +1382,8 @@ ibus_engine_simple_process_key_event (IBusEngine *engine,
+ /* non-hex character in hex sequence */
+ ibus_engine_simple_send_message_with_code (
+ simple,
+- IBUS_ENGINE_MSG_CODE_INVALID_COMPOSE_SEQUENCE);
++ IBUS_ENGINE_MSG_CODE_INVALID_COMPOSE_SEQUENCE,
++ NULL);
+ return TRUE;
+ }
+ } else if (priv->in_emoji_sequence) {
+@@ -1400,7 +1444,8 @@ ibus_engine_simple_process_key_event (IBusEngine *engine,
+ /* invalid hex sequence */
+ ibus_engine_simple_send_message_with_code (
+ simple,
+- IBUS_ENGINE_MSG_CODE_INVALID_COMPOSE_SEQUENCE);
++ IBUS_ENGINE_MSG_CODE_INVALID_COMPOSE_SEQUENCE,
++ NULL);
+ g_string_set_size (priv->tentative_match, 0);
+ priv->in_hex_sequence = FALSE;
+ priv->compose_buffer[0] = 0;
+@@ -1408,7 +1453,8 @@ ibus_engine_simple_process_key_event (IBusEngine *engine,
+ } else if (!check_hex (simple, n_compose)) {
+ ibus_engine_simple_send_message_with_code (
+ simple,
+- IBUS_ENGINE_MSG_CODE_INVALID_COMPOSE_SEQUENCE);
++ IBUS_ENGINE_MSG_CODE_INVALID_COMPOSE_SEQUENCE,
++ NULL);
+ }
+ ibus_engine_simple_update_preedit_text (simple);
+
+@@ -1503,7 +1549,8 @@ ibus_engine_simple_process_key_event (IBusEngine *engine,
+ g_assert (n_compose < (COMPOSE_BUFFER_SIZE + 1));
+ ibus_engine_simple_send_message_with_code (
+ simple,
+- IBUS_ENGINE_MSG_CODE_INVALID_COMPOSE_SEQUENCE);
++ IBUS_ENGINE_MSG_CODE_INVALID_COMPOSE_SEQUENCE,
++ NULL);
+ backup_char = priv->compose_buffer[n_compose];
+ priv->compose_buffer[n_compose] = 0;
+ if (ibus_engine_simple_check_all_compose_table (simple, n_compose))
+@@ -1703,9 +1750,19 @@ gboolean
+ ibus_engine_simple_add_compose_file (IBusEngineSimple *simple,
+ const gchar *compose_file)
+ {
+- g_return_val_if_fail (IBUS_IS_ENGINE_SIMPLE (simple), TRUE);
++ GError *error = NULL;
++
++ g_return_val_if_fail (IBUS_IS_ENGINE_SIMPLE (simple), FALSE);
+
+ global_tables = ibus_compose_table_list_add_file (global_tables,
+- compose_file);
++ compose_file,
++ &error);
++ if (error) {
++ g_warning ("\n%s\n", error->message);
++ ibus_engine_simple_send_message_with_code (simple,
++ error->code,
++ error);
++ g_error_free (error);
++ }
+ return TRUE;
+ }
+diff --git a/src/ibusenginesimpleprivate.h b/src/ibusenginesimpleprivate.h
+index ec764fa7..9c58c03d 100644
+--- a/src/ibusenginesimpleprivate.h
++++ b/src/ibusenginesimpleprivate.h
+@@ -26,6 +26,15 @@
+
+ G_BEGIN_DECLS
+
++/**
++ * IBUS_COMPOSE_ERROR:
++ *
++ * The `GQuark` used for `IBusComposeTableEx` errors.
++ *
++ * Since: 1.5.33
++ */
++#define IBUS_COMPOSE_ERROR (ibus_compose_error_quark ())
++
+ /* Checks if a keysym is a dead key. Dead key keysym values are defined in
+ * ibuskeysyms.h and the first is GDK_KEY_dead_grave.
+ */
+@@ -42,6 +51,13 @@ struct _IBusComposeTablePrivate
+ };
+
+
++/**
++ * ibus_compose_error_quark:
++ *
++ * Since: 1.5.33
++ * Stability: Unstable
++ */
++GQuark ibus_compose_error_quark (void);
+ guint ibus_compose_key_flag (guint key);
+ gboolean ibus_check_algorithmically (const guint *compose_buffer,
+ int n_compose,
+@@ -54,7 +70,8 @@ GVariant *
+ IBusComposeTableEx *
+ ibus_compose_table_deserialize
+ (const char *contents,
+- gsize length);
++ gsize length,
++ guint16 *saved_version);
+ gboolean ibus_compose_table_check (const IBusComposeTableEx *table,
+ guint *compose_buffer,
+ int n_compose,
+diff --git a/src/tests/ibus-compose.c b/src/tests/ibus-compose.c
+index 326d3b90..c07c02c4 100644
+--- a/src/tests/ibus-compose.c
++++ b/src/tests/ibus-compose.c
+@@ -241,9 +241,13 @@ create_engine_cb (IBusFactory *factory,
+ else
+ compose_path = get_compose_path ();
+ if (compose_path != NULL) {
++ guint16 saved_version = 0;
+ ibus_engine_simple_add_compose_file (IBUS_ENGINE_SIMPLE (m_engine),
+ compose_path);
+- m_compose_table = ibus_compose_table_load_cache (compose_path);
++ m_compose_table = ibus_compose_table_load_cache (compose_path,
++ &saved_version);
++ if (m_compose_table)
++ g_assert (saved_version);
+ }
+ g_free (compose_path);
+ return m_engine;
+--
+2.49.0
+
diff --git a/ibus.spec b/ibus.spec
index 3be3c89..43f5404 100644
--- a/ibus.spec
+++ b/ibus.spec
@@ -63,7 +63,7 @@
Name: ibus
Version: 1.5.32
# https://github.com/fedora-infra/rpmautospec/issues/101
-Release: 1%{?dist}
+Release: 2%{?dist}
Summary: Intelligent Input Bus for Linux OS
License: LGPL-2.1-or-later
URL: https://github.com/ibus/%name/wiki
@@ -72,6 +72,7 @@ Source1: https://github.com/ibus/%name/releases/download/%{source_version
Source2: %{name}-xinput
Source3: %{name}.conf.5
# Patch0: %%{name}-HEAD.patch
+Patch0: %{name}-HEAD.patch
# Under testing #1349148 #1385349 #1350291 #1406699 #1432252 #1601577
Patch1: %{name}-1385349-segv-bus-proxy.patch
@@ -355,9 +356,10 @@ fi
%build
#autoreconf -f -i -v
-#make -C ui/gtk3 maintainer-clean-generic
-#make -C tools maintainer-clean-generic
+#make -C bindings/vala maintainer-clean-generic
#make -C src/compose maintainer-clean-generic
+#make -C tools maintainer-clean-generic
+#make -C ui/gtk3 maintainer-clean-generic
%configure \
--disable-static \
%if %{with gtk2}
@@ -384,6 +386,8 @@ fi
--enable-install-tests \
%{nil}
# for 1385349-segv-bus-proxy.patch
+make -C bindings/vala maintainer-clean-generic
+make -C tools maintainer-clean-generic
make -C ui/gtk3 maintainer-clean-generic
%make_build
@@ -637,6 +641,14 @@ dconf update || :
%{_datadir}/installed-tests/ibus
%changelog
+* Sun Jun 01 2025 Takao Fujiwara <tfujiwar@redhat.com> - 1.5.32-2
+- Fix Exit and Restart menu items in Wayland input-method V2
+- Update Plasma setup message
+- Implement IBusMessage
+- Improve BEPO compose sequence visuals
+- Do not load en-US compose table by default
+- Fix some memory leaks
+
* Tue Apr 08 2025 Takao Fujiwara <tfujiwar@redhat.com> - 1.5.32-1
- Bump to 1.5.32
reply other threads:[~2026-05-31 2:08 UTC|newest]
Thread overview: [no followups] expand[flat|nested] mbox.gz Atom feed
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=178019333565.1.5686319989303863717.rpms-ibus-8523ef50fd7b@fedoraproject.org \
--to=tfujiwar@redhat.com \
--cc=git-commits@fedoraproject.org \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox