Blob Blame History Raw
/* vim:set et sts=4 sw=4:
 *
 * ibus - The Input Bus
 *
 * Copyright(c) 2018 Peng Huang <shawn.p.huang@gmail.com>
 * Copyright(c) 2018 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
 * 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
 */

class Preedit : Gtk.Window {
    private Gtk.Label m_extension_preedit_text;
    private Gtk.Label m_extension_preedit_emoji;
    private IBus.Text? m_engine_preedit_text;
    private bool m_engine_preedit_text_show;
    private uint m_engine_preedit_cursor_pos;
    private string m_prefix = "@";
    private bool m_is_shown = true;


    public Preedit() {
        GLib.Object(
            name : "IBusPreedit",
            type: Gtk.WindowType.POPUP
        );
        m_extension_preedit_text  = new Gtk.Label("");
        m_extension_preedit_emoji  = new Gtk.Label("");
    }


    public new void hide() {
        reset();
        base.hide();
        m_is_shown = false;
    }


    public bool is_shown() {
        return m_is_shown;
    }


    public void reset() {
        set_emoji("");
        set_text("");
        resize(1, 1);
        m_is_shown = true;
    }

    public void append_text(string text) {
        if (text.length == 0)
            return;
        string total = m_extension_preedit_text.get_text();
        total += text;
        m_extension_preedit_text.set_text(total);
    }


    public string get_text() {
        return m_extension_preedit_text.get_text();
    }


    public void set_text(string text) {
        m_extension_preedit_text.set_text(text);
    }


    public string get_emoji() {
        return m_extension_preedit_emoji.get_text();
    }


    public void set_emoji(string text) {
        m_extension_preedit_emoji.set_text(text);
    }


    public bool backspace() {
        string total = m_extension_preedit_emoji.get_text();
        if (total.length > 0) {
            m_extension_preedit_emoji.set_text("");
            resize(1, 1);
            return false;
        }
        total = m_extension_preedit_text.get_text();
        int char_count = total.char_count();
        if (char_count == 0)
            return true;
        total = total[0:total.index_of_nth_char(char_count - 1)];
        resize(1, 1);
        m_extension_preedit_text.set_text(total);
        if (total.length == 0)
            resize(1, 1);
        return true;
    }


    private string get_extension_text () {
        string extension_text = m_extension_preedit_emoji.get_text();
        if (extension_text.length == 0)
            extension_text = m_extension_preedit_text.get_text();
        return m_prefix + extension_text;
    }


    private void set_preedit_color(IBus.Text text,
                                   uint start_index,
                                   uint end_index) {
        text.append_attribute(IBus.AttrType.UNDERLINE,
                              IBus.AttrUnderline.SINGLE,
                              start_index, (int)end_index);
    }


    public IBus.Text get_engine_preedit_text() {
        string extension_text = get_extension_text();
        uint char_count = extension_text.char_count();
        IBus.Text retval;
        if (m_engine_preedit_text == null || !m_engine_preedit_text_show) {
            retval = new IBus.Text.from_string(extension_text);
            set_preedit_color(retval, 0, char_count);
            return retval;
        }
        retval = new IBus.Text.from_string(
                extension_text + m_engine_preedit_text.get_text());
        set_preedit_color(retval, 0, char_count);

        unowned IBus.AttrList attrs = m_engine_preedit_text.get_attributes();

        if (attrs == null)
            return retval;

        int i = 0;
        while (true) {
            IBus.Attribute attr = attrs.get(i++);
            if (attr == null)
                break;
            long start_index = attr.start_index;
            long end_index = attr.end_index;
            if (start_index < 0)
                start_index = 0;
            if (end_index < 0)
                end_index = m_engine_preedit_text.get_length();
            retval.append_attribute(attr.type, attr.value,
                                    char_count + (uint)start_index,
                                    (int)char_count + (int)end_index);
        }
        return retval;
    }


    public void set_engine_preedit_text(IBus.Text? text) {
        m_engine_preedit_text = text;
    }


    public void show_engine_preedit_text() {
        m_engine_preedit_text_show = true;
    }


    public void hide_engine_preedit_text() {
        m_engine_preedit_text_show = false;
    }


    public uint get_engine_preedit_cursor_pos() {
        return get_extension_text().char_count() + m_engine_preedit_cursor_pos;
    }


    public void set_engine_preedit_cursor_pos(uint cursor_pos) {
        m_engine_preedit_cursor_pos = cursor_pos;
    }


    public IBus.Text get_commit_text() {
        string extension_text = m_extension_preedit_emoji.get_text();
        if (extension_text.length == 0)
            extension_text = m_extension_preedit_text.get_text();
        return new IBus.Text.from_string(extension_text);
    }


    public void set_extension_name(string extension_name) {
        if (extension_name.length == 0)
            m_prefix = "@";
        else
            m_prefix = extension_name[0:1];
    }
}


class PanelBinding : IBus.PanelService {
    private bool m_is_wayland;
    private bool m_wayland_lookup_table_is_visible;
    private IBus.Bus m_bus;
    private Gtk.Application m_application;
    private GLib.Settings m_settings_panel = null;
    private GLib.Settings m_settings_emoji = null;
    private string m_current_context_path = "";
    private string m_real_current_context_path = "";
    private IBusEmojier? m_emojier;
    private uint m_emojier_set_emoji_lang_id;
    private uint m_emojier_focus_commit_text_id;
    private string[] m_emojier_favorites = {};
    private Gtk.CssProvider m_css_provider;
    private const uint PRELOAD_ENGINES_DELAY_TIME = 30000;
    private bool m_load_emoji_at_startup;
    private bool m_loaded_emoji = false;
    private bool m_load_unicode_at_startup;
    private bool m_loaded_unicode = false;
    private bool m_enable_extension;
    private string m_extension_name = "";
    private Preedit m_preedit;
    private IBus.ProcessKeyEventData m_key_event_data =
            IBus.ProcessKeyEventData();

    public PanelBinding(IBus.Bus bus,
                        Gtk.Application application) {
        GLib.assert(bus.is_connected());
        // Chain up base class constructor
        GLib.Object(connection : bus.get_connection(),
                    object_path : IBus.PATH_PANEL_EXTENSION_EMOJI);

        Type instance_type = Gdk.Display.get_default().get_type();
        Type wayland_type = typeof(GdkWayland.Display);
        m_is_wayland = instance_type.is_a(wayland_type);

        m_bus = bus;
        m_application = application;

        init_settings();
        m_preedit = new Preedit();
    }


    private void init_settings() {
        m_settings_panel = new GLib.Settings("org.freedesktop.ibus.panel");
        m_settings_emoji = new GLib.Settings("org.freedesktop.ibus.panel.emoji");

        m_settings_panel.changed["custom-font"].connect((key) => {
                BindingCommon.set_custom_font(m_settings_panel,
                                              m_settings_emoji,
                                              ref m_css_provider);
        });

        m_settings_panel.changed["use-custom-font"].connect((key) => {
                BindingCommon.set_custom_font(m_settings_panel,
                                              m_settings_emoji,
                                              ref m_css_provider);
        });

        m_settings_emoji.changed["unicode-hotkey"].connect((key) => {
                set_emoji_hotkey();
        });

        m_settings_emoji.changed["font"].connect((key) => {
                BindingCommon.set_custom_font(m_settings_panel,
                                              m_settings_emoji,
                                              ref m_css_provider);
        });

        m_settings_emoji.changed["hotkey"].connect((key) => {
                set_emoji_hotkey();
        });

        m_settings_emoji.changed["favorites"].connect((key) => {
                set_emoji_favorites();
        });

        m_settings_emoji.changed["favorite-annotations"].connect((key) => {
                set_emoji_favorites();
        });

        m_settings_emoji.changed["lang"].connect((key) => {
                set_emoji_lang();
        });

        m_settings_emoji.changed["has-partial-match"].connect((key) => {
                set_emoji_partial_match();
        });

        m_settings_emoji.changed["partial-match-length"].connect((key) => {
                set_emoji_partial_match();
        });

        m_settings_emoji.changed["partial-match-condition"].connect((key) => {
                set_emoji_partial_match();
        });

        m_settings_emoji.changed["load-emoji-at-startup"].connect((key) => {
                set_load_emoji_at_startup();
        });

        m_settings_emoji.changed["load-unicode-at-startup"].connect((key) => {
                set_load_unicode_at_startup();
        });
    }


    // Returning unowned IBus.KeyEventData causes NULL with gcc optimization
    // and use m_key_event_data.
    private void parse_accelerator(string accelerator) {
        m_key_event_data = {};
        uint keysym = 0;
        IBus.ModifierType modifiers = 0;
        IBus.accelerator_parse(accelerator,
                out keysym, out modifiers);
        if (keysym == 0U && modifiers == 0) {
            warning("Failed to parse shortcut key '%s'".printf(accelerator));
            return;
        }
        if ((modifiers & IBus.ModifierType.SUPER_MASK) != 0) {
            modifiers ^= IBus.ModifierType.SUPER_MASK;
            modifiers |= IBus.ModifierType.MOD4_MASK;
        }
        m_key_event_data.keyval = keysym;
        m_key_event_data.state = modifiers;
    }


    private void set_emoji_hotkey() {
        IBus.ProcessKeyEventData[] emoji_keys = {};
        IBus.ProcessKeyEventData key;
        string[] accelerators = m_settings_emoji.get_strv("hotkey");
        foreach (var accelerator in accelerators) {
            parse_accelerator(accelerator);
            emoji_keys += m_key_event_data;
        }

        /* Since {} is not allocated, parse_accelerator() should be unowned. */
        key = {};
        emoji_keys += key;

        IBus.ProcessKeyEventData[] unicode_keys = {};
        accelerators = m_settings_emoji.get_strv("unicode-hotkey");
        foreach (var accelerator in accelerators) {
            parse_accelerator(accelerator);
            unicode_keys += m_key_event_data;
        }
        key = {};
        unicode_keys += key;

        panel_extension_register_keys("emoji", emoji_keys,
                                      "unicode", unicode_keys);
    }


    private void set_emoji_favorites() {
        m_emojier_favorites = m_settings_emoji.get_strv("favorites");
        IBusEmojier.set_favorites(
                m_emojier_favorites,
                m_settings_emoji.get_strv("favorite-annotations"));
    }


    private void set_emoji_lang() {
        if (m_emojier_set_emoji_lang_id > 0) {
            GLib.Source.remove(m_emojier_set_emoji_lang_id);
            m_emojier_set_emoji_lang_id = 0;
        }
        m_emojier_set_emoji_lang_id = GLib.Idle.add(() => {
            IBusEmojier.set_annotation_lang(
                    m_settings_emoji.get_string("lang"));
            m_emojier_set_emoji_lang_id = 0;
            m_loaded_emoji = true;
            if (m_load_unicode_at_startup && !m_loaded_unicode) {
                IBusEmojier.load_unicode_dict();
                m_loaded_unicode = true;
            }
            return false;
        });
    }


    private void set_emoji_partial_match() {
        IBusEmojier.set_partial_match(
                m_settings_emoji.get_boolean("has-partial-match"));
        IBusEmojier.set_partial_match_length(
                m_settings_emoji.get_int("partial-match-length"));
        IBusEmojier.set_partial_match_condition(
                m_settings_emoji.get_int("partial-match-condition"));
    }


    private void set_load_emoji_at_startup() {
        m_load_emoji_at_startup =
            m_settings_emoji.get_boolean("load-emoji-at-startup");
    }


    private void set_load_unicode_at_startup() {
        m_load_unicode_at_startup =
            m_settings_emoji.get_boolean("load-unicode-at-startup");
    }

    public void load_settings() {

        set_emoji_hotkey();
        set_load_emoji_at_startup();
        set_load_unicode_at_startup();
        BindingCommon.set_custom_font(m_settings_panel,
                                      m_settings_emoji,
                                      ref m_css_provider);
        set_emoji_favorites();
        if (m_load_emoji_at_startup && !m_loaded_emoji)
            set_emoji_lang();
        set_emoji_partial_match();
    }


    /**
     * disconnect_signals:
     *
     * Call this API before m_panel = null so that the ref_count becomes 0
     */
    public void disconnect_signals() {
        if (m_emojier_set_emoji_lang_id > 0) {
            GLib.Source.remove(m_emojier_set_emoji_lang_id);
            m_emojier_set_emoji_lang_id = 0;
        }
        if (m_emojier != null) {
            m_application.remove_window(m_emojier);
            m_emojier = null;
        }
        m_application = null;
    }


    private void commit_text_update_favorites(IBus.Text text,
                                              bool      disable_extension) {
        commit_text(text);

        // If disable_extension is false, the extension event is already
        // sent before the focus-in is received.
        if (disable_extension) {
            IBus.ExtensionEvent event = new IBus.ExtensionEvent(
                    "name", m_extension_name,
                    "is-enabled", false,
                    "is-extension", true);
            panel_extension(event);
        }
        string committed_string = text.text;
        string preedit_string = m_preedit.get_text();
        m_preedit.hide();
        if (preedit_string == committed_string)
            return;
        bool has_favorite = false;
        foreach (unowned string favorite in m_emojier_favorites) {
            if (favorite == committed_string) {
                has_favorite = true;
                break;
            }
        }
        if (!has_favorite) {
            m_emojier_favorites += committed_string;
            m_settings_emoji.set_strv("favorites", m_emojier_favorites);
        }
    }


    private bool emojier_focus_commit_real() {
        if (m_emojier == null)
            return true;
        string selected_string = m_emojier.get_selected_string();
        string prev_context_path = m_emojier.get_input_context_path();
        if (selected_string != null &&
            prev_context_path != "" &&
            prev_context_path == m_current_context_path) {
            IBus.Text text = new IBus.Text.from_string(selected_string);
            commit_text_update_favorites(text, false);
            m_emojier.reset();
            return true;
        }

        return false;
    }


    private void emojier_focus_commit() {
        if (m_emojier == null)
            return;
        string selected_string = m_emojier.get_selected_string();
        string prev_context_path = m_emojier.get_input_context_path();
        if (selected_string == null &&
            prev_context_path != "") {
            var context = GLib.MainContext.default();
            if (m_emojier_focus_commit_text_id > 0 &&
                context.find_source_by_id(m_emojier_focus_commit_text_id)
                        != null) {
                GLib.Source.remove(m_emojier_focus_commit_text_id);
            }
            m_emojier_focus_commit_text_id = GLib.Timeout.add(100, () => {
                // focus_in is comming before switcher returns
                emojier_focus_commit_real();
                m_emojier_focus_commit_text_id = -1;
                return false;
            });
        } else {
            if (emojier_focus_commit_real()) {
                var context = GLib.MainContext.default();
                if (m_emojier_focus_commit_text_id > 0 &&
                    context.find_source_by_id(m_emojier_focus_commit_text_id)
                            != null) {
                    GLib.Source.remove(m_emojier_focus_commit_text_id);
                }
                m_emojier_focus_commit_text_id = -1;
            }
        }
    }


    private bool key_press_escape() {
        if (is_emoji_lookup_table()) {
            bool show_candidate = m_emojier.key_press_escape();
            convert_preedit_text();
            return show_candidate;
        }
        if (m_preedit.get_emoji() != "") {
            m_preedit.set_emoji("");
            string annotation = m_preedit.get_text();
            m_emojier.set_annotation(annotation);
            return false;
        }
        m_enable_extension = false;
        hide_emoji_lookup_table();
        m_preedit.hide();
        IBus.ExtensionEvent event = new IBus.ExtensionEvent(
                "name", m_extension_name,
                "is-enabled", false,
                "is-extension", true);
        panel_extension(event);
        return false;
    }


    private bool key_press_keyval(uint keyval) {
        unichar ch = IBus.keyval_to_unicode(keyval);
        if (ch.iscntrl())
                return false;
        string str = ch.to_string();
        m_preedit.append_text(str);
        string annotation = m_preedit.get_text();
        m_emojier.set_annotation(annotation);
        m_preedit.set_emoji("");
        return true;
    }


    private bool key_press_enter() {
        if (m_extension_name != "unicode" && is_emoji_lookup_table()) {
            // Check if variats exist
            if (m_emojier.key_press_enter(false)) {
                convert_preedit_text();
                return true;
            }
        }
        IBus.Text text = m_preedit.get_commit_text();
        commit_text_update_favorites(text, true);
        return false;
    }


    private void convert_preedit_text() {
        if (m_emojier.get_number_of_candidates() > 0)
            m_preedit.set_emoji(m_emojier.get_current_candidate());
        else
            m_preedit.set_emoji("");
    }


    private bool key_press_space() {
        bool show_candidate = false;
        if (m_preedit.get_emoji() != "") {
            m_emojier.key_press_cursor_horizontal(Gdk.Key.Right, 0);
            show_candidate = true;
        } else {
            string annotation = m_preedit.get_text();
            if (annotation.length == 0) {
                show_candidate = true;
                if (is_emoji_lookup_table())
                    m_emojier.key_press_cursor_horizontal(Gdk.Key.Right, 0);
            } else {
                m_emojier.set_annotation(annotation);
            }
        }
        convert_preedit_text();
        return show_candidate;
    }


    private bool key_press_cursor_horizontal(uint keyval,
                                             uint modifiers) {
        if (is_emoji_lookup_table()) {
            m_emojier.key_press_cursor_horizontal(keyval, modifiers);
            convert_preedit_text();
            return true;
        }
        return false;
    }


    private bool key_press_cursor_vertical(uint keyval,
                                           uint modifiers) {
        if (is_emoji_lookup_table()) {
            m_emojier.key_press_cursor_vertical(keyval, modifiers);
            convert_preedit_text();
            return true;
        }
        return false;
    }


    private bool key_press_cursor_home_end(uint keyval,
                                           uint modifiers) {
        if (is_emoji_lookup_table()) {
            m_emojier.key_press_cursor_home_end(keyval, modifiers);
            convert_preedit_text();
            return true;
        }
        return false;
    }


    private bool key_press_control_keyval(uint keyval,
                                          uint modifiers) {
        bool show_candidate = false;
        switch(keyval) {
        case Gdk.Key.f:
            show_candidate = key_press_cursor_horizontal(Gdk.Key.Right,
                                                         modifiers);
            break;
        case Gdk.Key.b:
            show_candidate = key_press_cursor_horizontal(Gdk.Key.Left,
                                                         modifiers);
            break;
        case Gdk.Key.n:
        case Gdk.Key.N:
            show_candidate = key_press_cursor_vertical(Gdk.Key.Down, modifiers);
            break;
        case Gdk.Key.p:
        case Gdk.Key.P:
            show_candidate = key_press_cursor_vertical(Gdk.Key.Up, modifiers);
            break;
        case Gdk.Key.h:
            show_candidate = key_press_cursor_home_end(Gdk.Key.Home, modifiers);
            break;
        case Gdk.Key.e:
            show_candidate = key_press_cursor_home_end(Gdk.Key.End, modifiers);
            break;
        case Gdk.Key.u:
            m_preedit.reset();
            m_emojier.set_annotation("");
            hide_emoji_lookup_table();
            break;
        case Gdk.Key.C:
        case Gdk.Key.c:
            if ((modifiers & Gdk.ModifierType.SHIFT_MASK) != 0) {
                if (!m_is_wayland && m_emojier != null &&
                    m_emojier.get_number_of_candidates() > 0) {
                    var text = m_emojier.get_current_candidate();
                    Gtk.Clipboard clipboard =
                            Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD);
                    clipboard.set_text(text, -1);
                    clipboard.store();
                }
                show_candidate = is_emoji_lookup_table();
            }
            break;
        default:
            show_candidate = is_emoji_lookup_table();
            break;
        }
        return show_candidate;
    }


    private void hide_wayland_lookup_table() {
        m_wayland_lookup_table_is_visible = false;
        var text = new IBus.Text.from_string("");
        update_auxiliary_text_received(text, false);
        update_lookup_table_received(
                new IBus.LookupTable(1, 0, false, true),
                false);
    }


    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();
        uint ncandidates = table.get_number_of_candidates();
        update_auxiliary_text_received(
                text,
                ncandidates > 0 ? true : false);
        update_lookup_table_received(
                table,
                ncandidates > 0 ? true : false);
    }


    private void hide_emoji_lookup_table() {
        if (m_emojier == null)
            return;
        if (m_wayland_lookup_table_is_visible)
            hide_wayland_lookup_table();
        else
            m_emojier.hide();
    }


    private void show_emoji_lookup_table() {
        /* Emojier category_list is shown in both Xorg and Wayland
         * because the annotation information is useful but the Wayland lookup
         * window is alway one dimension. So the category_list is shown
         * when the user annotation is null.
         */
        if (m_is_wayland && m_preedit.get_text() != "") {
            var text = m_emojier.get_title_text();
            show_wayland_lookup_table(text);
        } else {
            // POPUP window takes the focus in Wayland.
            if (m_is_wayland)
                m_emojier.set_input_context_path(m_real_current_context_path);
            m_emojier.show_all();
        }
    }


    private bool is_emoji_lookup_table() {
        if (m_is_wayland)
            return m_wayland_lookup_table_is_visible;
        else
            return m_emojier.get_visible();
    }


    private void show_preedit_and_candidate(bool show_candidate) {
        uint cursor_pos = 0;
        if (!show_candidate)
            cursor_pos = m_preedit.get_engine_preedit_cursor_pos();
        update_preedit_text_received(
                m_preedit.get_engine_preedit_text(),
                cursor_pos,
                true);
        if (!show_candidate) {
            hide_emoji_lookup_table();
            return;
        }
        if (m_emojier == null)
            return;
        /* Wayland gives the focus on Emojir which is a GTK popup window
         * and move the focus fom the current input context to Emojier.
         * This forwards the lookup table to gnome-shell's lookup table
         * but it enables one dimension lookup table only.
         */
        show_emoji_lookup_table();
    }


    public override void focus_in(string input_context_path) {
        m_current_context_path = input_context_path;

        /* 'fake' input context is named as 
         * '/org/freedesktop/IBus/InputContext_1' and always send in
         * focus-out events by ibus-daemon for the global engine mode.
         * Now ibus-daemon assumes to always use the global engine.
         * But this event should not be used for modal dialogs
         * such as Switcher.
         */
        if (!input_context_path.has_suffix("InputContext_1")) {
            m_real_current_context_path = m_current_context_path;
            if (m_is_wayland)
                this.emojier_focus_commit();
        }
    }


    public override void focus_out(string input_context_path) {
        m_current_context_path = "";
    }


    public override void panel_extension_received(IBus.ExtensionEvent event) {
        m_extension_name = event.get_name();
        if (m_extension_name != "emoji" && m_extension_name != "unicode") {
            string format = "The name %s is not implemented in PanelExtension";
            warning (format.printf(m_extension_name));
            m_extension_name = "";
            return;
        }
        m_enable_extension = event.is_enabled;
        if (!m_enable_extension) {
            hide_emoji_lookup_table();
            return;
        }
        if (!m_loaded_emoji)
            set_emoji_lang();
        if (!m_loaded_unicode && m_loaded_emoji) {
            IBusEmojier.load_unicode_dict();
            m_loaded_unicode = true;
        }
        if (m_emojier == null) {
            m_emojier = new IBusEmojier();
            // For title handling in gnome-shell
            m_application.add_window(m_emojier);
            m_emojier.candidate_clicked.connect((i, b, s) => {
                candidate_clicked_lookup_table_real(i, b, s, true);
            });
            m_emojier.commit_text.connect((s) => {
                if (!m_is_wayland)
                    return;
                // Currently emojier has a focus but the text input focus
                // does not and commit the text later.
                IBus.ExtensionEvent close_event = new IBus.ExtensionEvent(
                        "name", m_extension_name,
                        "is-enabled", false,
                        "is-extension", true);
                panel_extension(close_event);
            });
        }
        m_emojier.reset();
        m_emojier.set_annotation("");
        m_preedit.set_extension_name(m_extension_name);
        m_preedit.reset();
        update_preedit_text_received(
                m_preedit.get_engine_preedit_text(),
                m_preedit.get_engine_preedit_cursor_pos(),
                true);
        string params = event.get_params();
        if (params == "category-list") {
            key_press_space();
            show_preedit_and_candidate(true);
        }
    }


    public override void set_cursor_location(int x,
                                             int y,
                                             int width,
                                             int height) {
        if (m_emojier != null)
            m_emojier.set_cursor_location(x, y, width, height);
    }


    public override void update_preedit_text(IBus.Text text,
                                             uint      cursor_pos,
                                             bool      visible) {
        m_preedit.set_engine_preedit_text(text);
        if (visible)
            m_preedit.show_engine_preedit_text();
        else
            m_preedit.hide_engine_preedit_text();
        m_preedit.set_engine_preedit_cursor_pos(cursor_pos);
        update_preedit_text_received(m_preedit.get_engine_preedit_text(),
                                     m_preedit.get_engine_preedit_cursor_pos(),
                                     visible);
    }


    public override void show_preedit_text() {
        m_preedit.show_engine_preedit_text();
        show_preedit_and_candidate(false);
    }


    public override void hide_preedit_text() {
        m_preedit.hide_engine_preedit_text();
        show_preedit_and_candidate(false);
    }


    public override bool process_key_event(uint keyval,
                                           uint keycode,
                                           uint state) {
        if ((state & IBus.ModifierType.RELEASE_MASK) != 0)
            return false;
        uint modifiers = state;
        bool show_candidate = false;
        switch(keyval) {
        case Gdk.Key.Escape:
            show_candidate = key_press_escape();
            if (!m_preedit.is_shown())
                return true;
            break;
        case Gdk.Key.Return:
        case Gdk.Key.KP_Enter:
            if (m_extension_name == "unicode")
                key_press_space();
            show_candidate = key_press_enter();
            if (!m_preedit.is_shown()) {
                hide_emoji_lookup_table();
                return true;
            }
            break;
        case Gdk.Key.BackSpace:
            m_preedit.backspace();
            string annotation = m_preedit.get_text();
            if (annotation == "" && m_extension_name == "unicode") {
                key_press_escape();
                return true;
            }
            m_emojier.set_annotation(annotation);
            break;
        case Gdk.Key.space:
        case Gdk.Key.KP_Space:
            if ((modifiers & Gdk.ModifierType.SHIFT_MASK) != 0) {
                if (!key_press_keyval(keyval))
                    return true;
                show_candidate = is_emoji_lookup_table();
                break;
            }
            show_candidate = key_press_space();
            if (m_extension_name == "unicode") {
                key_press_enter();
                return true;
            }
            break;
        case Gdk.Key.Right:
        case Gdk.Key.KP_Right:
            /* one dimension in Wayland, two dimensions in X11 */
            if (m_is_wayland) {
                show_candidate = key_press_cursor_vertical(Gdk.Key.Down,
                                                           modifiers);
            } else {
                show_candidate = key_press_cursor_horizontal(Gdk.Key.Right,
                                                             modifiers);
            }
            break;
        case Gdk.Key.Left:
        case Gdk.Key.KP_Left:
            if (m_is_wayland) {
                show_candidate = key_press_cursor_vertical(Gdk.Key.Up,
                                                           modifiers);
            } else {
                show_candidate = key_press_cursor_horizontal(Gdk.Key.Left,
                                                             modifiers);
            }
            break;
        case Gdk.Key.Down:
        case Gdk.Key.KP_Down:
            if (m_is_wayland) {
                show_candidate = key_press_cursor_horizontal(Gdk.Key.Right,
                                                             modifiers);
            } else {
                show_candidate = key_press_cursor_vertical(Gdk.Key.Down,
                                                           modifiers);
            }
            break;
        case Gdk.Key.Up:
        case Gdk.Key.KP_Up:
            if (m_is_wayland) {
                show_candidate = key_press_cursor_horizontal(Gdk.Key.Left,
                                                             modifiers);
            } else {
                show_candidate = key_press_cursor_vertical(Gdk.Key.Up,
                                                           modifiers);
            }
            break;
        case Gdk.Key.Page_Down:
        case Gdk.Key.KP_Page_Down:
            if (m_is_wayland) {
                show_candidate = key_press_cursor_vertical(Gdk.Key.Down,
                                                           modifiers);
            } else {
                show_candidate = key_press_cursor_vertical(Gdk.Key.Page_Down,
                                                           modifiers);
            }
            break;
        case Gdk.Key.Page_Up:
        case Gdk.Key.KP_Page_Up:
            if (m_is_wayland) {
                show_candidate = key_press_cursor_vertical(Gdk.Key.Up,
                                                           modifiers);
            } else {
                show_candidate = key_press_cursor_vertical(Gdk.Key.Page_Up,
                                                           modifiers);
            }
            break;
        case Gdk.Key.Home:
        case Gdk.Key.KP_Home:
            show_candidate = key_press_cursor_home_end(Gdk.Key.Home, modifiers);
            break;
        case Gdk.Key.End:
        case Gdk.Key.KP_End:
            show_candidate = key_press_cursor_home_end(Gdk.Key.End, modifiers);
            break;
        default:
            if ((modifiers & Gdk.ModifierType.CONTROL_MASK) != 0) {
                show_candidate = key_press_control_keyval(keyval, modifiers);
                break;
            }
            if (!key_press_keyval(keyval))
                return true;
            show_candidate = is_emoji_lookup_table();
            break;
        }
        show_preedit_and_candidate(show_candidate);
        return true;
    }

    public override void commit_text_received(IBus.Text text) {
        unowned string? str = text.text;
        if (str == null)
            return;
        /* Do not call convert_preedit_text() because it depends on
         * each IME whether process_key_event() receives Shift-space or not.
         */
        m_preedit.append_text(str);
        m_preedit.set_emoji("");
        string annotation = m_preedit.get_text();
        m_emojier.set_annotation(annotation);
        show_preedit_and_candidate(false);
    }

    public override void page_up_lookup_table() {
        bool show_candidate = key_press_cursor_vertical(Gdk.Key.Up, 0);
        show_preedit_and_candidate(show_candidate);
    }

    public override void page_down_lookup_table() {
        bool show_candidate = key_press_cursor_vertical(Gdk.Key.Down, 0);
        show_preedit_and_candidate(show_candidate);
    }

    public override void cursor_up_lookup_table() {
        bool show_candidate = key_press_cursor_horizontal(Gdk.Key.Left, 0);
        show_preedit_and_candidate(show_candidate);
    }

    public override void cursor_down_lookup_table() {
        bool show_candidate = key_press_cursor_horizontal(Gdk.Key.Right, 0);
        show_preedit_and_candidate(show_candidate);
    }

    private void candidate_clicked_lookup_table_real(uint index,
                                                     uint button,
                                                     uint state,
                                                     bool is_emojier) {
        if (button == IBusEmojier.BUTTON_CLOSE_BUTTON) {
            m_enable_extension = false;
            hide_emoji_lookup_table();
            m_preedit.hide();
            IBus.ExtensionEvent event = new IBus.ExtensionEvent(
                    "name", m_extension_name,
                    "is-enabled", false,
                    "is-extension", true);
            panel_extension(event);
            return;
        }
        if (m_emojier == null)
            return;
        bool show_candidate = false;
        uint ncandidates = m_emojier.get_number_of_candidates();
        if (ncandidates > 0 && ncandidates >= index) {
            m_emojier.set_cursor_pos(index);
            bool need_commit_signal = m_is_wayland && is_emojier;
            show_candidate = m_emojier.has_variants(index, need_commit_signal);
            if (!m_is_wayland)
                m_preedit.set_emoji(m_emojier.get_current_candidate());
        } else {
            return;
        }
        if (!show_candidate) {
            IBus.Text text = m_preedit.get_commit_text();
            hide_emoji_lookup_table();
            if (!is_emojier || !m_is_wayland)
                commit_text_update_favorites(text, true);
            return;
        }
        show_preedit_and_candidate(show_candidate);
    }

    public override void candidate_clicked_lookup_table(uint index,
                                                        uint button,
                                                        uint state) {
        candidate_clicked_lookup_table_real(index, button, state, false);
    }
}