public inbox for git-commits@fedoraproject.org
help / color / mirror / Atom feed
* [rpms/ibus] autotool: Implement IBusMessage and update compose feature
@ 2026-05-31  2:08 Takao Fujiwara
  0 siblings, 0 replies; only message in thread
From: Takao Fujiwara @ 2026-05-31  2:08 UTC (permalink / raw)
  To: git-commits

            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
 

^ permalink raw reply related	[flat|nested] only message in thread

only message in thread, other threads:[~2026-05-31  2:08 UTC | newest]

Thread overview: (only message) (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-05-31  2:08 [rpms/ibus] autotool: Implement IBusMessage and update compose feature Takao Fujiwara

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox