/* vim:set et sts=4 sw=4: * * ibus - The Input Bus * * Copyright(c) 2011-2014 Peng Huang * Copyright(c) 2015-2018 Takao Fujwiara * * 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 Panel : IBus.PanelService { private enum IconType { STATUS_ICON, INDICATOR, } private IBus.Bus m_bus; private GLib.Settings m_settings_general = null; private GLib.Settings m_settings_hotkey = null; private GLib.Settings m_settings_panel = null; private IconType m_icon_type = IconType.STATUS_ICON; private Indicator m_indicator; #if INDICATOR private GLib.DBusConnection m_session_bus_connection; #endif private Gtk.StatusIcon m_status_icon; private Gtk.Menu m_ime_menu; private Gtk.Menu m_sys_menu; private IBus.EngineDesc[] m_engines = {}; private GLib.HashTable m_engine_contexts = new GLib.HashTable(GLib.str_hash, GLib.str_equal); private string m_current_context_path = ""; private string m_real_current_context_path = ""; private bool m_use_global_engine = true; private CandidatePanel m_candidate_panel; private Switcher m_switcher; private uint m_switcher_focus_set_engine_id; private PropertyManager m_property_manager; private PropertyPanel m_property_panel; private GLib.Pid m_setup_pid = 0; private Gtk.AboutDialog m_about_dialog; private Gtk.CssProvider m_css_provider; private int m_switcher_delay_time = 400; private bool m_use_system_keyboard_layout = false; private GLib.HashTable m_xkb_icon_pixbufs = new GLib.HashTable(GLib.str_hash, GLib.str_equal); private GLib.HashTable m_xkb_icon_image = new GLib.HashTable(GLib.str_hash, GLib.str_equal); private Gdk.RGBA m_xkb_icon_rgba = Gdk.RGBA(){ red = 0.0, green = 0.0, blue = 0.0, alpha = 1.0 }; private XKBLayout m_xkblayout = new XKBLayout(); private bool inited_engines_order = true; private uint m_preload_engines_id; private const uint PRELOAD_ENGINES_DELAY_TIME = 30000; private string m_icon_prop_key = ""; private int m_property_icon_delay_time = 500; private uint m_property_icon_delay_time_id; #if INDICATOR private bool m_is_kde = is_kde(); #else private bool m_is_kde = false; #endif private ulong m_popup_menu_id; private ulong m_activate_id; private ulong m_registered_status_notifier_item_id; private GLib.List m_keybindings = new GLib.List(); public Panel(IBus.Bus bus) { GLib.assert(bus.is_connected()); // Chain up base class constructor GLib.Object(connection : bus.get_connection(), object_path : "/org/freedesktop/IBus/Panel"); m_bus = bus; init_settings(); // init ui #if INDICATOR if (m_is_kde) { init_indicator(); } else { init_status_icon(); } #else init_status_icon(); #endif m_candidate_panel = new CandidatePanel(); m_candidate_panel.page_up.connect((w) => this.page_up()); m_candidate_panel.page_down.connect((w) => this.page_down()); m_candidate_panel.cursor_up.connect((w) => this.cursor_up()); m_candidate_panel.cursor_down.connect((w) => this.cursor_down()); m_candidate_panel.candidate_clicked.connect( (w, i, b, s) => this.candidate_clicked(i, b, s)); m_switcher = new Switcher(); // The initial shortcut is "space" bind_switch_shortcut(); if (m_switcher_delay_time >= 0) { m_switcher.set_popup_delay_time((uint) m_switcher_delay_time); } m_property_manager = new PropertyManager(); m_property_manager.property_activate.connect((w, k, s) => { property_activate(k, s); }); m_property_panel = new PropertyPanel(); m_property_panel.property_activate.connect((w, k, s) => { property_activate(k, s); }); state_changed(); } ~Panel() { #if INDICATOR // unref m_indicator if (m_indicator != null) m_indicator.unregister_connection(); #endif BindingCommon.unbind_switch_shortcut( BindingCommon.KeyEventFuncType.ANY, m_keybindings); m_keybindings = null; } private void init_settings() { m_settings_general = new GLib.Settings("org.freedesktop.ibus.general"); m_settings_hotkey = new GLib.Settings("org.freedesktop.ibus.general.hotkey"); 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); }); m_settings_general.changed["switcher-delay-time"].connect((key) => { set_switcher_delay_time(); }); m_settings_general.changed["use-system-keyboard-layout"].connect( (key) => { set_use_system_keyboard_layout(); }); m_settings_general.changed["embed-preedit-text"].connect((key) => { set_embed_preedit_text(); }); m_settings_general.changed["use-global-engine"].connect((key) => { set_use_global_engine(); }); m_settings_general.changed["use-xmodmap"].connect((key) => { 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(); }); m_settings_panel.changed["custom-font"].connect((key) => { 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); }); m_settings_panel.changed["show-icon-on-systray"].connect((key) => { set_show_icon_on_systray(); }); m_settings_panel.changed["lookup-table-orientation"].connect((key) => { set_lookup_table_orientation(); }); m_settings_panel.changed["show"].connect((key) => { set_show_property_panel(); }); m_settings_panel.changed["timeout"].connect((key) => { set_timeout_property_panel(); }); m_settings_panel.changed["follow-input-cursor-when-always-shown"] .connect((key) => { set_follow_input_cursor_when_always_shown_property_panel(); }); m_settings_panel.changed["xkb-icon-rgba"].connect((key) => { set_xkb_icon_rgba(); }); m_settings_panel.changed["property-icon-delay-time"].connect((key) => { set_property_icon_delay_time(); }); } private void popup_menu_at_area_window(Gtk.Menu menu, Gdk.Rectangle area, Gdk.Window? window, Gtk.MenuPositionFunc? func) { #if VALA_0_34 Gdk.Gravity rect_anchor = Gdk.Gravity.SOUTH_WEST; Gdk.Gravity menu_anchor = Gdk.Gravity.NORTH_WEST; // Gtk.Menu.popup() is now deprecated but // Gtk.Menu.popup_at_rect() requires a Gdk.Window and // Gtk.Menu.popup_at_rect() outputs a warning of // "no trigger event for menu popup" // for the foreigner QT window which is generated by // Gdk.X11.Window.foreign_for_display. // https://git.gnome.org/browse/gtk+/tree/gtk/gtkmenu.c?h=gtk-3-22#n2251 menu.popup_at_rect(window, area, rect_anchor, menu_anchor, null); #else menu.popup(null, null, func, 0, Gtk.get_current_event_time()); #endif } #if INDICATOR private static bool is_kde() { if (Environment.get_variable("XDG_CURRENT_DESKTOP") == "KDE") return true; warning ("If you launch KDE5 on xterm, " + "export XDG_CURRENT_DESKTOP=KDE before launch KDE5."); return false; } private void popup_menu_at_pointer_window(Gtk.Menu menu, int x, int y, Gdk.Window? window, Gtk.MenuPositionFunc? func) { int win_x = 0; int win_y = 0; window.get_origin(out win_x, out win_y); Gdk.Rectangle area = { x - win_x, y - win_y, 1, 1 }; // window is a bottom wide panel instead of status icon popup_menu_at_area_window(menu, area, window, func); } private void init_indicator() { m_icon_type = IconType.INDICATOR; GLib.Bus.get.begin(GLib.BusType.SESSION, null, (obj, res) => { try { m_session_bus_connection = GLib.Bus.get.end(res); m_indicator = new Indicator("ibus-ui-gtk3", m_session_bus_connection, Indicator.Category.APPLICATION_STATUS); m_indicator.title = _("IBus Panel"); m_registered_status_notifier_item_id = m_indicator.registered_status_notifier_item.connect( () => { m_indicator.set_status(Indicator.Status.ACTIVE); state_changed(); }); m_popup_menu_id = m_indicator.context_menu.connect((x, y, w, b, t) => { popup_menu_at_pointer_window( create_context_menu(), x, y, w, m_indicator.position_context_menu); }); m_activate_id = m_indicator.activate.connect((x, y, w) => { popup_menu_at_pointer_window( create_activate_menu(), x, y, w, m_indicator.position_activate_menu); }); } catch (GLib.IOError e) { warning("Failed to get the session bus: %s", e.message); } }); } #endif private void init_status_icon() { m_status_icon = new Gtk.StatusIcon(); m_status_icon.set_name("ibus-ui-gtk"); m_status_icon.set_title(_("IBus Panel")); // Gdk.Window.get_width() is needed for the menu position if (m_status_icon.get_size() > 0) init_status_icon_menu(); else m_status_icon.notify["size"].connect(init_status_icon_menu); } private void init_status_icon_menu() { Gdk.Rectangle area = { 0, 0, 0, 0 }; Gdk.Window? window = null; Gtk.MenuPositionFunc? func = null; #if VALA_0_34 window = Gdk.X11.Window.lookup_for_display( Gdk.Display.get_default() as Gdk.X11.Display, m_status_icon.get_x11_window_id()) as Gdk.Window; if (window == null) { warning("StatusIcon does not have GdkWindow"); return; } Gtk.Orientation orient; m_status_icon.get_geometry(null, out area, out orient); int win_x = 0; int win_y = 0; window.get_origin(out win_x, out win_y); // The (x, y) is converted by gdk_window_get_root_coords() // in gdk_window_impl_move_to_rect() area.x -= win_x; area.y -= win_y; #else func = m_status_icon.position_menu; #endif m_popup_menu_id = m_status_icon.popup_menu.connect((b, t) => { popup_menu_at_area_window( create_context_menu(), area, window, func); }); m_activate_id = m_status_icon.activate.connect(() => { popup_menu_at_area_window( create_activate_menu(), area, window, func); }); m_status_icon.set_from_icon_name("ibus-keyboard"); } private void bind_switch_shortcut() { string[] accelerators = m_settings_hotkey.get_strv("triggers"); var keybinding_manager = KeybindingManager.get_instance(); foreach (var accelerator in accelerators) { BindingCommon.keybinding_manager_bind( keybinding_manager, ref m_keybindings, accelerator, BindingCommon.KeyEventFuncType.IME_SWITCHER, handle_engine_switch_normal, handle_engine_switch_reverse); } } /* public static void unbind_switch_shortcut(KeyEventFuncType ftype, GLib.List keybindings) { var keybinding_manager = KeybindingManager.get_instance(); while (keybindings != null) { Keybinding keybinding = keybindings.data; if (ftype == KeyEventFuncType.ANY || ftype == keybinding.ftype) { keybinding_manager.unbind(keybinding.keysym, keybinding.modifiers); } keybindings = keybindings.next; } } */ /** * panel_get_engines_from_xkb: * @self: #Panel class * @engines: all engines from ibus_bus_list_engines() * @returns: ibus xkb engines * * Made ibus engines from the current XKB keymaps. * This returns only XKB engines whose name start with "xkb:". */ private GLib.List get_engines_from_xkb(GLib.List engines) { string layouts; string variants; string option; XKBLayout.get_layout(out layouts, out variants, out option); GLib.List xkb_engines = new GLib.List(); IBus.EngineDesc us_engine = new IBus.EngineDesc("xkb:us::eng", "", "", "", "", "", "", ""); string[] layout_array = layouts.split(","); string[] variant_array = variants.split(","); for (int i = 0; i < layout_array.length; i++) { string layout = layout_array[i]; string variant = null; IBus.EngineDesc current_engine = null; if (i < variant_array.length) variant = variant_array[i]; /* If variants == "", variants.split(",") is { null }. * To meet engine.get_layout_variant(), convert null to "" * here. */ if (variant == null) variant = ""; foreach (unowned IBus.EngineDesc engine in engines) { string name = engine.get_name(); if (!name.has_prefix("xkb:")) continue; if (engine.get_layout() == layout && engine.get_layout_variant() == variant) { current_engine = engine; break; } } if (current_engine != null) { xkb_engines.append(current_engine); } else if (xkb_engines.find(us_engine) == null) { warning("Fallback %s(%s) to us layout.", layout, variant); xkb_engines.append(us_engine); } } if (xkb_engines.length() == 0) warning("Not found IBus XKB engines from the session."); return xkb_engines; } /** * panel_get_engines_from_locale: * @self: #Panel class * @engines: all engines from ibus_bus_list_engines() * @returns: ibus im engines * * Made ibus engines from the current locale and IBus.EngineDesc.lang . * This returns non-XKB engines whose name does not start "xkb:". */ private GLib.List get_engines_from_locale(GLib.List engines) { string locale = Intl.setlocale(LocaleCategory.CTYPE, null); if (locale == null) locale = "C"; string lang = locale.split(".")[0]; GLib.List im_engines = new GLib.List(); foreach (unowned IBus.EngineDesc engine in engines) { string name = engine.get_name(); if (name.has_prefix("xkb:")) continue; if (engine.get_language() == lang && engine.get_rank() > 0) im_engines.append(engine); } if (im_engines.length() == 0) { lang = lang.split("_")[0]; foreach (unowned IBus.EngineDesc engine in engines) { string name = engine.get_name(); if (name.has_prefix("xkb:")) continue; if (engine.get_language() == lang && engine.get_rank() > 0) im_engines.append(engine); } } if (im_engines.length() == 0) return im_engines; im_engines.sort((a, b) => { return (int) b.get_rank() - (int) a.get_rank(); }); return im_engines; } private void init_engines_order() { m_xkblayout.set_latin_layouts( m_settings_general.get_strv("xkb-latin-layouts")); if (inited_engines_order) return; if (m_settings_general.get_strv("preload-engines").length > 0) return; GLib.List engines = m_bus.list_engines(); GLib.List xkb_engines = get_engines_from_xkb(engines); GLib.List im_engines = get_engines_from_locale(engines); string[] names = {}; foreach (unowned IBus.EngineDesc engine in xkb_engines) names += engine.get_name(); foreach (unowned IBus.EngineDesc engine in im_engines) names += engine.get_name(); m_settings_general.set_strv("preload-engines", names); } private void set_switcher_delay_time() { m_switcher_delay_time = m_settings_general.get_int("switcher-delay-time"); if (m_switcher != null && m_switcher_delay_time >= 0) { m_switcher.set_popup_delay_time((uint) m_switcher_delay_time); } } private void set_use_system_keyboard_layout() { m_use_system_keyboard_layout = m_settings_general.get_boolean("use-system-keyboard-layout"); } private void set_embed_preedit_text() { Variant variant = m_settings_general.get_value("embed-preedit-text"); if (variant == null) { return; } m_bus.set_ibus_property("EmbedPreeditText", variant); } private void set_use_global_engine() { m_use_global_engine = m_settings_general.get_boolean("use-global-engine"); } private void set_use_xmodmap() { m_xkblayout.set_use_xmodmap( m_settings_general.get_boolean("use-xmodmap")); } private void set_show_icon_on_systray() { if (m_icon_type == IconType.STATUS_ICON) { if (m_status_icon == null) return; m_status_icon.set_visible( m_settings_panel.get_boolean("show-icon-on-systray")); } else if (m_icon_type == IconType.INDICATOR) { if (m_indicator == null) return; if (m_settings_panel.get_boolean("show-icon-on-systray")) { m_indicator.set_status(Indicator.Status.ACTIVE); } else { m_indicator.set_status(Indicator.Status.PASSIVE); } } } private void set_lookup_table_orientation() { if (m_candidate_panel == null) return; m_candidate_panel.set_vertical( m_settings_panel.get_int("lookup-table-orientation") == IBus.Orientation.VERTICAL); } private void set_show_property_panel() { if (m_property_panel == null) return; m_property_panel.set_show(m_settings_panel.get_int("show")); } private void set_timeout_property_panel() { if (m_property_panel == null) return; m_property_panel.set_auto_hide_timeout( (uint) m_settings_panel.get_int("auto-hide-timeout")); } private void set_follow_input_cursor_when_always_shown_property_panel() { if (m_property_panel == null) return; m_property_panel.set_follow_input_cursor_when_always_shown( m_settings_panel.get_boolean( "follow-input-cursor-when-always-shown")); } private void set_xkb_icon_rgba() { string spec = m_settings_panel.get_string("xkb-icon-rgba"); Gdk.RGBA rgba = { 0, }; if (!rgba.parse(spec)) { warning("invalid format of xkb-icon-rgba: %s", spec); m_xkb_icon_rgba = Gdk.RGBA(){ red = 0.0, green = 0.0, blue = 0.0, alpha = 1.0 }; } else m_xkb_icon_rgba = rgba; if (m_icon_type == IconType.STATUS_ICON) { if (m_xkb_icon_pixbufs.size() > 0) { m_xkb_icon_pixbufs.remove_all(); if (m_status_icon != null && m_switcher != null) state_changed(); } } else if (m_icon_type == IconType.INDICATOR) { if (m_xkb_icon_image.size() > 0) { m_xkb_icon_image.remove_all(); if (m_indicator != null && m_switcher != null) state_changed(); } } } private void set_property_icon_delay_time() { m_property_icon_delay_time = m_settings_panel.get_int("property-icon-delay-time"); } private int compare_versions(string version1, string version2) { string[] version1_list = version1.split("."); string[] version2_list = version2.split("."); int major1, minor1, micro1, major2, minor2, micro2; if (version1 == version2) { return 0; } // The initial dconf value of "version" is "". if (version1 == "") { return -1; } if (version2 == "") { return 1; } assert(version1_list.length >= 3); assert(version2_list.length >= 3); major1 = int.parse(version1_list[0]); minor1 = int.parse(version1_list[1]); micro1 = int.parse(version1_list[2]); major2 = int.parse(version2_list[0]); minor2 = int.parse(version2_list[1]); micro2 = int.parse(version2_list[2]); if (major1 == minor1 && minor1 == minor2 && micro1 == micro2) { return 0; } if ((major1 > major2) || (major1 == major2 && minor1 > minor2) || (major1 == major2 && minor1 == minor2 && micro1 > micro2)) { return 1; } return -1; } private void update_version_1_5_3() { #if ENABLE_LIBNOTIFY if (!Notify.is_initted()) { Notify.init ("ibus"); } var notification = new Notify.Notification( _("IBus Update"), _("Super+space is now the default hotkey."), "ibus"); notification.set_timeout(30 * 1000); notification.set_category("hotkey"); try { notification.show(); } catch (GLib.Error e){ warning ("Notification is failed for IBus 1.5.3: %s", e.message); } #else warning(_("Super+space is now the default hotkey.")); #endif } private void update_version_1_5_8() { inited_engines_order = false; } private void set_version() { string prev_version = m_settings_general.get_string("version"); string current_version = null; if (compare_versions(prev_version, "1.5.3") < 0) update_version_1_5_3(); if (compare_versions(prev_version, "1.5.8") < 0) update_version_1_5_8(); current_version = "%d.%d.%d".printf(IBus.MAJOR_VERSION, IBus.MINOR_VERSION, IBus.MICRO_VERSION); if (prev_version == current_version) { return; } m_settings_general.set_string("version", current_version); } public void load_settings() { set_version(); init_engines_order(); // Update m_use_system_keyboard_layout before update_engines() // is called. set_use_system_keyboard_layout(); set_use_global_engine(); set_use_xmodmap(); update_engines(m_settings_general.get_strv("preload-engines"), m_settings_general.get_strv("engines-order")); BindingCommon.unbind_switch_shortcut( BindingCommon.KeyEventFuncType.ANY, m_keybindings); m_keybindings = null; bind_switch_shortcut(); set_switcher_delay_time(); set_embed_preedit_text(); BindingCommon.set_custom_font(m_settings_panel, null, ref m_css_provider); set_show_icon_on_systray(); set_lookup_table_orientation(); set_show_property_panel(); set_timeout_property_panel(); set_follow_input_cursor_when_always_shown_property_panel(); set_xkb_icon_rgba(); set_property_icon_delay_time(); } /** * disconnect_signals: * * Call this API before m_panel = null so that the ref_count becomes 0 */ public void disconnect_signals() { unowned GLib.Object object = m_status_icon; #if INDICATOR if (m_is_kde) object = m_indicator; #endif if (m_popup_menu_id > 0) { // No name functions refer m_panel in m_status_icon if (GLib.SignalHandler.is_connected(object, m_popup_menu_id)) object.disconnect(m_popup_menu_id); m_popup_menu_id = 0; } if (m_activate_id > 0) { if (GLib.SignalHandler.is_connected(object, m_activate_id)) object.disconnect(m_activate_id); m_activate_id = 0; } if (m_registered_status_notifier_item_id > 0) { if (GLib.SignalHandler.is_connected( object, m_registered_status_notifier_item_id)) { object.disconnect(m_registered_status_notifier_item_id); } m_registered_status_notifier_item_id = 0; } if (m_preload_engines_id > 0) { GLib.Source.remove(m_preload_engines_id); m_preload_engines_id = 0; } } private void engine_contexts_insert(IBus.EngineDesc engine) { if (m_use_global_engine) return; if (m_engine_contexts.size() >= 200) { warning ("Contexts by windows are too much counted!"); m_engine_contexts.remove_all(); } m_engine_contexts.replace(m_current_context_path, engine); } private void set_engine(IBus.EngineDesc engine) { if (m_property_icon_delay_time_id > 0) { GLib.Source.remove(m_property_icon_delay_time_id); m_property_icon_delay_time_id = 0; } if (!m_bus.set_global_engine(engine.get_name())) { warning("Switch engine to %s failed.", engine.get_name()); return; } /* Panel.update_property() will be called with a time lag * by another engine because of DBus delay so need to * clear m_icon_prop_key here to avoid wrong panel icon in * disabled m_use_global_engine. */ m_icon_prop_key = ""; // set xkb layout if (!m_use_system_keyboard_layout) m_xkblayout.set_layout(engine); engine_contexts_insert(engine); } private void switch_engine(int i, bool force = false) { GLib.assert(i >= 0 && i < m_engines.length); // Do not need switch if (i == 0 && !force) return; IBus.EngineDesc engine = m_engines[i]; set_engine(engine); } private void handle_engine_switch_normal(Gdk.Event event) { handle_engine_switch(event, false); } private void handle_engine_switch_reverse(Gdk.Event event) { handle_engine_switch(event, true); } private void handle_engine_switch(Gdk.Event event, bool reverse) { // Do not need switch IME if (m_engines.length <= 1) return; uint keyval = event.key.keyval; uint modifiers = KeybindingManager.MODIFIER_FILTER & event.key.state; uint primary_modifiers = KeybindingManager.get_primary_modifier(event.key.state); bool pressed = KeybindingManager.primary_modifier_still_pressed( event, primary_modifiers); if (reverse) { modifiers &= ~Gdk.ModifierType.SHIFT_MASK; } if (pressed && m_switcher_delay_time >= 0) { int i = reverse ? m_engines.length - 1 : 1; /* The flag of m_switcher.is_running avoids the following problem: * * When an IME is chosen on m_switcher, focus_in() is called * for the fake input context. If an engine is set in focus_in() * during running m_switcher when m_use_global_engine is false, * state_changed() is also called and m_engines[] is modified * in state_changed() and m_switcher.run() returns the index * for m_engines[] but m_engines[] was modified by state_changed() * and the index is not correct. */ i = m_switcher.run(keyval, modifiers, event, m_engines, i, m_real_current_context_path); if (i < 0) { debug("switch cancelled"); } else if (i == 0) { debug("do not have to switch"); } else { this.switcher_focus_set_engine(); } } else { int i = reverse ? m_engines.length - 1 : 1; switch_engine(i); } } private void run_preload_engines(IBus.EngineDesc[] engines, int index) { string[] names = {}; if (engines.length <= index) { return; } if (m_preload_engines_id != 0) { GLib.Source.remove(m_preload_engines_id); m_preload_engines_id = 0; } names += engines[index].get_name(); m_preload_engines_id = Timeout.add( PRELOAD_ENGINES_DELAY_TIME, () => { if (!m_bus.is_connected()) return false; m_bus.preload_engines_async.begin(names, -1, null); return false; }); } private void update_engines(string[]? unowned_engine_names, string[]? order_names) { string[]? engine_names = unowned_engine_names; if (engine_names == null || engine_names.length == 0) engine_names = {"xkb:us::eng"}; string[] names = {}; foreach (var name in order_names) { if (name in engine_names) names += name; } foreach (var name in engine_names) { if (name in names) continue; names += name; } var engines = m_bus.get_engines_by_names(names); /* Fedora internal patch could save engines not in simple.xml * likes 'xkb:cn::chi'. */ if (engines.length == 0) { names = {"xkb:us::eng"}; m_settings_general.set_strv("preload-engines", names); engines = m_bus.get_engines_by_names(names); } if (m_engines.length == 0) { m_engines = engines; switch_engine(0, true); run_preload_engines(engines, 1); } else { var current_engine = m_engines[0]; m_engines = engines; int i; for (i = 0; i < m_engines.length; i++) { if (current_engine.get_name() == engines[i].get_name()) { switch_engine(i); if (i != 0) { run_preload_engines(engines, 0); } else { run_preload_engines(engines, 1); } return; } } switch_engine(0, true); run_preload_engines(engines, 1); } } private void context_render_string(Cairo.Context cr, string symbol, int image_width, int image_height) { int lwidth = 0; int lheight = 0; var desc = Pango.FontDescription.from_string("Monospace Bold 22"); var layout = Pango.cairo_create_layout(cr); if (symbol.length >= 3) desc = Pango.FontDescription.from_string("Monospace Bold 18"); layout.set_font_description(desc); layout.set_text(symbol, -1); layout.get_size(out lwidth, out lheight); cr.move_to((image_width - lwidth / Pango.SCALE) / 2, (image_height - lheight / Pango.SCALE) / 2); cr.set_source_rgba(m_xkb_icon_rgba.red, m_xkb_icon_rgba.green, m_xkb_icon_rgba.blue, m_xkb_icon_rgba.alpha); Pango.cairo_show_layout(cr, layout); } private Cairo.ImageSurface create_cairo_image_surface_with_string(string symbol, bool cache) { Cairo.ImageSurface image = null; if (cache) { image = m_xkb_icon_image[symbol]; if (image != null) return image; } image = new Cairo.ImageSurface(Cairo.Format.ARGB32, 48, 48); var cr = new Cairo.Context(image); int width = image.get_width(); int height = image.get_height(); int stride = image.get_stride(); cr.set_source_rgba(0.0, 0.0, 0.0, 0.0); cr.set_operator(Cairo.Operator.SOURCE); cr.paint(); cr.set_operator(Cairo.Operator.OVER); context_render_string(cr, symbol, width, height); image.flush(); if (m_icon_type == IconType.INDICATOR) { if (GLib.BYTE_ORDER == GLib.ByteOrder.LITTLE_ENDIAN) { unowned uint[] data = (uint[]) image.get_data(); int length = stride * height / (int) sizeof(uint); for (int i = 0; i < length; i++) data[i] = data[i].to_big_endian(); } } if (cache) m_xkb_icon_image.insert(symbol, image); return image; } private Gdk.Pixbuf create_icon_pixbuf_with_string(string symbol) { Gdk.Pixbuf pixbuf = m_xkb_icon_pixbufs[symbol]; if (pixbuf != null) return pixbuf; var image = create_cairo_image_surface_with_string(symbol, false); int width = image.get_width(); int height = image.get_height(); pixbuf = Gdk.pixbuf_get_from_surface(image, 0, 0, width, height); m_xkb_icon_pixbufs.insert(symbol, pixbuf); return pixbuf; } private void show_setup_dialog() { if (m_setup_pid != 0) { if (Posix.kill(m_setup_pid, Posix.SIGUSR1) == 0) return; m_setup_pid = 0; } string binary = GLib.Path.build_filename(Config.BINDIR, "ibus-setup"); try { GLib.Process.spawn_async(null, {binary, "ibus-setup"}, null, GLib.SpawnFlags.DO_NOT_REAP_CHILD, null, out m_setup_pid); } catch (GLib.SpawnError e) { warning("Execute %s failed! %s", binary, e.message); m_setup_pid = 0; } GLib.ChildWatch.add(m_setup_pid, (pid, state) => { if (pid != m_setup_pid) return; m_setup_pid = 0; GLib.Process.close_pid(pid); }); } private void show_about_dialog() { if (m_about_dialog == null) { m_about_dialog = new Gtk.AboutDialog(); m_about_dialog.set_program_name("IBus"); m_about_dialog.set_version(Config.PACKAGE_VERSION); string copyright = "Copyright © 2007-2015 Peng Huang\n" + "Copyright © 2015 Takao Fujiwara\n" + "Copyright © 2007-2015 Red Hat, Inc.\n"; m_about_dialog.set_copyright(copyright); m_about_dialog.set_license("LGPL"); m_about_dialog.set_comments(_("IBus is an intelligent input bus for Linux/Unix.")); m_about_dialog.set_website("https://github.com/ibus/ibus/wiki"); m_about_dialog.set_authors({"Peng Huang "}); m_about_dialog.set_documenters({"Peng Huang "}); m_about_dialog.set_translator_credits(_("translator-credits")); m_about_dialog.set_logo_icon_name("ibus"); m_about_dialog.set_icon_name("ibus"); } if (!m_about_dialog.get_visible()) { m_about_dialog.run(); m_about_dialog.hide(); } else { m_about_dialog.present(); } } private Gtk.Menu create_context_menu() { // Show system menu if (m_sys_menu == null) { Gtk.MenuItem item; m_sys_menu = new Gtk.Menu(); item = new Gtk.MenuItem.with_label(_("Preferences")); item.activate.connect((i) => show_setup_dialog()); m_sys_menu.append(item); #if EMOJI_DICT item = new Gtk.MenuItem.with_label(_("Emoji Choice")); item.activate.connect((i) => { IBus.ExtensionEvent event = new IBus.ExtensionEvent( "name", "emoji", "is-enabled", true, "params", "category-list"); /* new GLib.Variant("(sv)", "emoji", event.serialize_object()) * will call g_variant_unref() for the child variant by vala. * I have no idea not to unref the object so integrated * the purpose to IBus.ExtensionEvent above. */ panel_extension(event); }); m_sys_menu.append(item); #endif item = new Gtk.MenuItem.with_label(_("About")); item.activate.connect((i) => show_about_dialog()); m_sys_menu.append(item); m_sys_menu.append(new Gtk.SeparatorMenuItem()); item = new Gtk.MenuItem.with_label(_("Restart")); item.activate.connect((i) => m_bus.exit(true)); m_sys_menu.append(item); item = new Gtk.MenuItem.with_label(_("Quit")); item.activate.connect((i) => m_bus.exit(false)); m_sys_menu.append(item); m_sys_menu.show_all(); } return m_sys_menu; } private Gtk.Menu create_activate_menu() { m_ime_menu = new Gtk.Menu(); // Show properties and IME switching menu m_property_manager.create_menu_items(m_ime_menu); m_ime_menu.append(new Gtk.SeparatorMenuItem()); // Append IMEs foreach (var engine in m_engines) { var language = engine.get_language(); var longname = engine.get_longname(); var textdomain = engine.get_textdomain(); var transname = GLib.dgettext(textdomain, longname); var item = new Gtk.MenuItem.with_label( "%s - %s".printf (IBus.get_language_name(language), transname)); // Make a copy of engine to workaround a bug in vala. // https://bugzilla.gnome.org/show_bug.cgi?id=628336 var e = engine; item.activate.connect((item) => { for (int i = 0; i < m_engines.length; i++) { if (e == m_engines[i]) { switch_engine(i); break; } } }); m_ime_menu.add(item); } m_ime_menu.show_all(); // Do not take focuse to avoid some focus related issues. m_ime_menu.set_take_focus(false); return m_ime_menu; } private void set_properties(IBus.PropList props) { int i = 0; while (true) { IBus.Property prop = props.get(i); if (prop == null) break; set_property(props.get(i), true); i++; } } private new void set_property(IBus.Property prop, bool all_update) { string symbol = prop.get_symbol().get_text(); if (m_icon_prop_key != "" && prop.get_key() == m_icon_prop_key && symbol != "") animate_icon(symbol, all_update); } private void animate_icon(string symbol, bool all_update) { if (m_property_icon_delay_time < 0) return; uint timeout = 0; if (all_update) timeout = (uint) m_property_icon_delay_time; if (m_property_icon_delay_time_id > 0) { GLib.Source.remove(m_property_icon_delay_time_id); m_property_icon_delay_time_id = 0; } m_property_icon_delay_time_id = GLib.Timeout.add(timeout, () => { m_property_icon_delay_time_id = 0; if (m_icon_type == IconType.STATUS_ICON) { Gdk.Pixbuf pixbuf = create_icon_pixbuf_with_string(symbol); m_status_icon.set_from_pixbuf(pixbuf); } else if (m_icon_type == IconType.INDICATOR) { Cairo.ImageSurface image = create_cairo_image_surface_with_string(symbol, true); m_indicator.set_cairo_image_surface_full(image, ""); } return false; }); } /* override virtual functions */ public override void set_cursor_location(int x, int y, int width, int height) { m_candidate_panel.set_cursor_location(x, y, width, height); m_property_panel.set_cursor_location(x, y, width, height); } private bool switcher_focus_set_engine_real() { IBus.EngineDesc? selected_engine = m_switcher.get_selected_engine(); string prev_context_path = m_switcher.get_input_context_path(); if (selected_engine != null && prev_context_path != "" && prev_context_path == m_current_context_path) { set_engine(selected_engine); m_switcher.reset(); return true; } return false; } private void switcher_focus_set_engine() { IBus.EngineDesc? selected_engine = m_switcher.get_selected_engine(); string prev_context_path = m_switcher.get_input_context_path(); if (selected_engine == null && prev_context_path != "" && m_switcher.is_running()) { var context = GLib.MainContext.default(); if (m_switcher_focus_set_engine_id > 0 && context.find_source_by_id(m_switcher_focus_set_engine_id) != null) { GLib.Source.remove(m_switcher_focus_set_engine_id); } m_switcher_focus_set_engine_id = GLib.Timeout.add(100, () => { // focus_in is comming before switcher returns switcher_focus_set_engine_real(); m_switcher_focus_set_engine_id = -1; return false; }); } else { if (switcher_focus_set_engine_real()) { var context = GLib.MainContext.default(); if (m_switcher_focus_set_engine_id > 0 && context.find_source_by_id(m_switcher_focus_set_engine_id) != null) { GLib.Source.remove(m_switcher_focus_set_engine_id); } m_switcher_focus_set_engine_id = -1; } } } 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; m_property_panel.focus_in(); this.switcher_focus_set_engine(); } if (m_use_global_engine) return; var engine = m_engine_contexts[input_context_path]; if (engine == null) { /* If engine == null, need to call set_engine(m_engines[0]) * here and update m_engine_contexts[] to avoid the * following problem: * * If context1 is focused and does not set an engine and * return here, the current engine1 is used for context1. * When context2 is focused and switch engine1 to engine2, * the current engine becomes engine2. * And when context1 is focused again, context1 still * does not set an engine and return here, * engine2 is used for context2 instead of engine1. */ engine = m_engines.length > 0 ? m_engines[0] : null; if (engine == null) return; } else { bool in_engines = false; foreach (var e in m_engines) { if (engine.get_name() == e.get_name()) { in_engines = true; break; } } /* The engine is deleted by ibus-setup before focus_in() * is called. */ if (!in_engines) return; } set_engine(engine); } public override void focus_out(string input_context_path) { m_current_context_path = ""; } public override void destroy_context(string input_context_path) { if (m_use_global_engine) return; m_engine_contexts.remove(input_context_path); } public override void register_properties(IBus.PropList props) { m_property_manager.set_properties(props); m_property_panel.set_properties(props); set_properties(props); } public override void update_property(IBus.Property prop) { m_property_manager.update_property(prop); m_property_panel.update_property(prop); set_property(prop, false); } public override void update_preedit_text(IBus.Text text, uint cursor_pos, bool visible) { if (visible) { m_candidate_panel.set_preedit_text(text, cursor_pos); m_property_panel.set_preedit_text(text, cursor_pos); } else { m_candidate_panel.set_preedit_text(null, 0); m_property_panel.set_preedit_text(null, 0); } } public override void hide_preedit_text() { m_candidate_panel.set_preedit_text(null, 0); } public override void update_auxiliary_text(IBus.Text text, bool visible) { m_candidate_panel.set_auxiliary_text(visible ? text : null); m_property_panel.set_auxiliary_text(visible ? text : null); } public override void hide_auxiliary_text() { m_candidate_panel.set_auxiliary_text(null); } public override void update_lookup_table(IBus.LookupTable table, bool visible) { m_candidate_panel.set_lookup_table(visible ? table : null); m_property_panel.set_lookup_table(visible ? table : null); } public override void hide_lookup_table() { m_candidate_panel.set_lookup_table(null); } public override void set_content_type(uint purpose, uint hints) { m_candidate_panel.set_content_type(purpose, hints); } public override void state_changed() { /* Do not change the order of m_engines during running switcher. */ if (m_switcher.is_running()) return; if (m_icon_type == IconType.INDICATOR) { // Wait for the callback of the session bus. if (m_indicator == null) return; } var icon_name = "ibus-keyboard"; var engine = m_bus.get_global_engine(); if (engine != null) { icon_name = engine.get_icon(); m_icon_prop_key = engine.get_icon_prop_key(); } else { m_icon_prop_key = ""; } if (icon_name[0] == '/') { if (m_icon_type == IconType.STATUS_ICON) { m_status_icon.set_from_file(icon_name); } else if (m_icon_type == IconType.INDICATOR) { #if INDICATOR_ENGINE_ICON m_indicator.set_icon_full(icon_name, ""); #else warning("plasma-workspace 5.2 or later is required to " + "show the absolute path icon %s. Currently check " + "qtbase 5.4 since there is no way to check " + "the version of plasma-workspace.", icon_name); m_indicator.set_icon_full("ibus-engine", ""); #endif } } else { string language = null; if (engine != null) { var name = engine.get_name(); if (name.length >= 4 && name[0:4] == "xkb:") language = m_switcher.get_xkb_language(engine); } if (language != null) { if (m_icon_type == IconType.STATUS_ICON) { Gdk.Pixbuf pixbuf = create_icon_pixbuf_with_string(language); m_status_icon.set_from_pixbuf(pixbuf); } else if (m_icon_type == IconType.INDICATOR) { Cairo.ImageSurface image = create_cairo_image_surface_with_string(language, true); m_indicator.set_cairo_image_surface_full(image, ""); } } else { var theme = Gtk.IconTheme.get_default(); if (theme.lookup_icon(icon_name, 48, 0) != null) { if (m_icon_type == IconType.STATUS_ICON) { m_status_icon.set_from_icon_name(icon_name); } else if (m_icon_type == IconType.INDICATOR) { m_indicator.set_icon_full(icon_name, ""); } } else { if (m_icon_type == IconType.STATUS_ICON) { m_status_icon.set_from_icon_name("ibus-engine"); } else if (m_icon_type == IconType.INDICATOR) { m_indicator.set_icon_full("ibus-engine", ""); } } } } if (engine == null) return; int i; for (i = 0; i < m_engines.length; i++) { if (m_engines[i].get_name() == engine.get_name()) break; } // engine is first engine in m_engines. if (i == 0) return; // engine is not in m_engines. if (i >= m_engines.length) return; for (int j = i; j > 0; j--) { m_engines[j] = m_engines[j - 1]; } m_engines[0] = engine; string[] names = {}; foreach(var desc in m_engines) { names += desc.get_name(); } m_settings_general.set_strv("engines-order", names); } }