1 /* vim:set et sts=4 sw=4:
5 * Copyright(c) 2011-2013 Peng Huang <shawn.p.huang@gmail.com>
7 * This library is free software; you can redistribute it and/or
8 * modify it under the terms of the GNU Lesser General Public
9 * License as published by the Free Software Foundation; either
10 * version 2.1 of the License, or (at your option) any later version.
12 * This library is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
15 * Lesser General Public License for more details.
17 * You should have received a copy of the GNU Lesser General Public
18 * License along with this library; if not, write to the Free Software
19 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
23 class Panel : IBus.PanelService {
24 private class Keybinding {
25 public Keybinding(uint keysym,
26 Gdk.ModifierType modifiers,
29 this.modifiers = modifiers;
30 this.reverse = reverse;
33 public uint keysym { get; set; }
34 public Gdk.ModifierType modifiers { get; set; }
35 public bool reverse { get; set; }
38 private IBus.Bus m_bus;
39 private GLib.Settings m_settings_general = null;
40 private GLib.Settings m_settings_hotkey = null;
41 private GLib.Settings m_settings_panel = null;
42 private Gtk.StatusIcon m_status_icon;
43 private Gtk.Menu m_ime_menu;
44 private Gtk.Menu m_sys_menu;
45 private IBus.EngineDesc[] m_engines = {};
46 private GLib.HashTable<string, IBus.EngineDesc> m_engine_contexts =
47 new GLib.HashTable<string, IBus.EngineDesc>(GLib.str_hash,
49 private string m_current_context_path = "";
50 private bool m_use_global_engine = true;
51 private CandidatePanel m_candidate_panel;
52 private Switcher m_switcher;
53 private bool m_switcher_is_running = false;
54 private PropertyManager m_property_manager;
55 private GLib.Pid m_setup_pid = 0;
56 private Gtk.AboutDialog m_about_dialog;
57 private Gtk.CssProvider m_css_provider;
58 private int m_switcher_delay_time = 400;
59 private bool m_use_system_keyboard_layout = false;
61 private GLib.List<Keybinding> m_keybindings = new GLib.List<Keybinding>();
63 public Panel(IBus.Bus bus) {
64 GLib.assert(bus.is_connected());
65 // Chain up base class constructor
66 GLib.Object(connection : bus.get_connection(),
67 object_path : "/org/freedesktop/IBus/Panel");
74 m_status_icon = new Gtk.StatusIcon();
75 m_status_icon.set_name("ibus-ui-gtk");
76 m_status_icon.set_title("IBus Panel");
77 m_status_icon.popup_menu.connect(status_icon_popup_menu_cb);
78 m_status_icon.activate.connect(status_icon_activate_cb);
79 m_status_icon.set_from_icon_name("ibus-keyboard");
81 m_candidate_panel = new CandidatePanel();
82 m_candidate_panel.page_up.connect((w) => this.page_up());
83 m_candidate_panel.page_down.connect((w) => this.page_down());
85 m_switcher = new Switcher();
86 // The initial shortcut is "<Super>space"
87 bind_switch_shortcut();
89 if (m_switcher_delay_time >= 0) {
90 m_switcher.set_popup_delay_time((uint) m_switcher_delay_time);
93 m_property_manager = new PropertyManager();
94 m_property_manager.property_activate.connect((k, s) => {
95 property_activate(k, s);
102 unbind_switch_shortcut();
105 private void init_settings() {
106 m_settings_general = new GLib.Settings("org.freedesktop.ibus.general");
108 new GLib.Settings("org.freedesktop.ibus.general.hotkey");
109 m_settings_panel = new GLib.Settings("org.freedesktop.ibus.panel");
111 m_settings_general.changed["preload-engines"].connect((key) => {
112 update_engines(m_settings_general.get_strv(key),
116 m_settings_general.changed["switcher-delay-time"].connect((key) => {
117 set_switcher_delay_time();
120 m_settings_general.changed["use-system-keyboard-layout"].connect(
122 set_use_system_keyboard_layout();
125 m_settings_general.changed["embed-preedit-text"].connect((key) => {
126 set_embed_preedit_text();
129 m_settings_general.changed["use-global-engine"].connect((key) => {
130 set_use_global_engine();
133 m_settings_hotkey.changed["triggers"].connect((key) => {
134 unbind_switch_shortcut();
135 bind_switch_shortcut();
138 m_settings_panel.changed["custom-font"].connect((key) => {
142 m_settings_panel.changed["use-custom-font"].connect((key) => {
147 private void keybinding_manager_bind(KeybindingManager keybinding_manager,
148 string? accelerator) {
149 uint switch_keysym = 0;
150 Gdk.ModifierType switch_modifiers = 0;
151 Gdk.ModifierType reverse_modifier = Gdk.ModifierType.SHIFT_MASK;
152 Keybinding keybinding;
154 Gtk.accelerator_parse(accelerator,
155 out switch_keysym, out switch_modifiers);
157 // Map virtual modifiers to (i.e. Mod2, Mod3, ...)
158 const Gdk.ModifierType VIRTUAL_MODIFIERS = (
159 Gdk.ModifierType.SUPER_MASK |
160 Gdk.ModifierType.HYPER_MASK |
161 Gdk.ModifierType.META_MASK);
162 if ((switch_modifiers & VIRTUAL_MODIFIERS) != 0) {
163 // workaround a bug in gdk vapi vala > 0.18
164 // https://bugzilla.gnome.org/show_bug.cgi?id=677559
166 Gdk.Keymap.get_default().map_virtual_modifiers(
167 ref switch_modifiers);
169 if ((switch_modifiers & Gdk.ModifierType.SUPER_MASK) != 0)
170 switch_modifiers |= Gdk.ModifierType.MOD4_MASK;
171 if ((switch_modifiers & Gdk.ModifierType.HYPER_MASK) != 0)
172 switch_modifiers |= Gdk.ModifierType.MOD4_MASK;
174 switch_modifiers &= ~VIRTUAL_MODIFIERS;
177 if (switch_keysym == 0 && switch_modifiers == 0) {
178 warning("Parse accelerator '%s' failed!", accelerator);
182 keybinding = new Keybinding(switch_keysym,
185 m_keybindings.append(keybinding);
187 keybinding_manager.bind(switch_keysym, switch_modifiers,
188 (e) => handle_engine_switch(e, false));
190 // accelerator already has Shift mask
191 if ((switch_modifiers & reverse_modifier) != 0) {
195 switch_modifiers |= reverse_modifier;
197 keybinding = new Keybinding(switch_keysym,
200 m_keybindings.append(keybinding);
202 keybinding_manager.bind(switch_keysym, switch_modifiers,
203 (e) => handle_engine_switch(e, true));
206 private void bind_switch_shortcut() {
207 string[] accelerators = m_settings_hotkey.get_strv("triggers");
209 var keybinding_manager = KeybindingManager.get_instance();
211 foreach (var accelerator in accelerators) {
212 keybinding_manager_bind(keybinding_manager, accelerator);
216 private void unbind_switch_shortcut() {
217 var keybinding_manager = KeybindingManager.get_instance();
219 unowned GLib.List<Keybinding> keybindings = m_keybindings;
221 while (keybindings != null) {
222 Keybinding keybinding = keybindings.data;
224 keybinding_manager.unbind(keybinding.keysym,
225 keybinding.modifiers);
226 keybindings = keybindings.next;
229 m_keybindings = null;
232 private void set_custom_font() {
233 Gdk.Display display = Gdk.Display.get_default();
234 Gdk.Screen screen = (display != null) ?
235 display.get_default_screen() : null;
237 if (screen == null) {
238 warning("Could not open display.");
242 bool use_custom_font = m_settings_panel.get_boolean("use-custom-font");
244 if (m_css_provider != null) {
245 Gtk.StyleContext.remove_provider_for_screen(screen,
247 m_css_provider = null;
250 if (use_custom_font == false) {
254 string font_name = m_settings_panel.get_string("custom-font");
256 if (font_name == null) {
257 warning("No config panel:custom-font.");
261 string data_format = "GtkLabel { font: %s; }";
262 string data = data_format.printf(font_name);
263 m_css_provider = new Gtk.CssProvider();
266 m_css_provider.load_from_data(data, -1);
267 } catch (GLib.Error e) {
268 warning("Failed css_provider_from_data: %s: %s", font_name,
273 Gtk.StyleContext.add_provider_for_screen(screen,
275 Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION);
278 private void set_switcher_delay_time() {
279 m_switcher_delay_time =
280 m_settings_general.get_int("switcher-delay-time");
282 if (m_switcher_delay_time >= 0) {
283 m_switcher.set_popup_delay_time((uint) m_switcher_delay_time);
287 private void set_use_system_keyboard_layout() {
288 m_use_system_keyboard_layout =
289 m_settings_general.get_boolean("use-system-keyboard-layout");
292 private void set_embed_preedit_text() {
294 m_settings_general.get_value("embed-preedit-text");
296 if (variant == null) {
300 m_bus.set_ibus_property("EmbedPreeditText", variant);
303 private void set_use_global_engine() {
304 m_use_global_engine =
305 m_settings_general.get_boolean("use-global-engine");
308 private int compare_versions(string version1, string version2) {
309 string[] version1_list = version1.split(".");
310 string[] version2_list = version2.split(".");
311 int major1, minor1, micro1, major2, minor2, micro2;
313 if (version1 == version2) {
317 // The initial dconf value of "version" is "".
318 if (version1 == "") {
321 if (version2 == "") {
325 assert(version1_list.length >= 3);
326 assert(version2_list.length >= 3);
328 major1 = int.parse(version1_list[0]);
329 minor1 = int.parse(version1_list[1]);
330 micro1 = int.parse(version1_list[2]);
332 major2 = int.parse(version2_list[0]);
333 minor2 = int.parse(version2_list[1]);
334 micro2 = int.parse(version2_list[2]);
336 if (major1 == minor1 && minor1 == minor2 && micro1 == micro2) {
339 if ((major1 > major2) ||
340 (major1 == major2 && minor1 > minor2) ||
341 (major1 == major2 && minor1 == minor2 &&
348 private void update_version_1_5_3() {
350 if (!Notify.is_initted()) {
351 Notify.init ("ibus");
354 var notification = new Notify.Notification(
356 _("Super+space is now the default hotkey."),
358 notification.set_timeout(30 * 1000);
359 notification.set_category("hotkey");
363 } catch (GLib.Error e){
364 warning ("Notification is failed for IBus 1.5.3: %s", e.message);
367 warning(_("Super+space is now the default hotkey."));
371 private void set_version() {
372 string prev_version = m_settings_general.get_string("version");
373 string current_version = null;
375 if (compare_versions(prev_version, "1.5.3") < 0) {
376 update_version_1_5_3();
379 current_version = "%d.%d.%d".printf(IBus.MAJOR_VERSION,
383 if (prev_version == current_version) {
387 m_settings_general.set_string("version", current_version);
390 public void load_settings() {
391 // Update m_use_system_keyboard_layout before update_engines()
393 set_use_system_keyboard_layout();
394 set_use_global_engine();
395 update_engines(m_settings_general.get_strv("preload-engines"),
396 m_settings_general.get_strv("engines-order"));
397 unbind_switch_shortcut();
398 bind_switch_shortcut();
399 set_switcher_delay_time();
400 set_embed_preedit_text();
406 private void exec_setxkbmap(IBus.EngineDesc engine) {
407 string layout = engine.get_layout();
408 string variant = engine.get_layout_variant();
409 string option = engine.get_layout_option();
410 string standard_error = null;
412 string[] args = { "setxkbmap" };
414 if (layout != null && layout != "" && layout != "default") {
418 if (variant != null && variant != "" && variant != "default") {
422 if (option != null && option != "" && option != "default") {
423 /*TODO: Need to get the session XKB options */
429 if (args.length == 1) {
434 if (!GLib.Process.spawn_sync(null, args, null,
435 GLib.SpawnFlags.SEARCH_PATH,
439 warning("Switch xkb layout to %s failed.",
440 engine.get_layout());
442 } catch (GLib.SpawnError e) {
443 warning("Execute setxkbmap failed: %s", e.message);
446 if (exit_status != 0) {
447 warning("Execute setxkbmap failed: %s", standard_error ?? "(null)");
451 private void engine_contexts_insert(IBus.EngineDesc engine) {
452 if (m_use_global_engine)
455 if (m_engine_contexts.size() >= 200) {
456 warning ("Contexts by windows are too much counted!");
457 m_engine_contexts.remove_all();
460 m_engine_contexts.replace(m_current_context_path, engine);
463 private void set_engine(IBus.EngineDesc engine) {
464 if (!m_bus.set_global_engine(engine.get_name())) {
465 warning("Switch engine to %s failed.", engine.get_name());
470 if (!m_use_system_keyboard_layout)
471 exec_setxkbmap(engine);
473 engine_contexts_insert(engine);
476 private void switch_engine(int i, bool force = false) {
477 GLib.assert(i >= 0 && i < m_engines.length);
479 // Do not need switch
480 if (i == 0 && !force)
483 IBus.EngineDesc engine = m_engines[i];
488 private void handle_engine_switch(Gdk.Event event, bool revert) {
489 // Do not need switch IME
490 if (m_engines.length <= 1)
493 uint keyval = event.key.keyval;
494 uint modifiers = KeybindingManager.MODIFIER_FILTER & event.key.state;
496 uint primary_modifiers =
497 KeybindingManager.get_primary_modifier(event.key.state);
499 bool pressed = KeybindingManager.primary_modifier_still_pressed(
500 event, primary_modifiers);
503 modifiers &= ~Gdk.ModifierType.SHIFT_MASK;
506 if (pressed && m_switcher_delay_time >= 0) {
507 int i = revert ? m_engines.length - 1 : 1;
509 /* The flag of m_switcher_is_running avoids the following problem:
511 * When an IME is chosen on m_switcher, focus_in() is called
512 * for the root window. If an engine is set in focus_in()
513 * during running m_switcher when m_use_global_engine is false,
514 * state_changed() is also called and m_engines[] is modified
515 * in state_changed() and m_switcher.run() returns the index
516 * for m_engines[] but m_engines[] was modified by state_changed()
517 * and the index is not correct. */
518 m_switcher_is_running = true;
519 i = m_switcher.run(keyval, modifiers, event, m_engines, i);
520 m_switcher_is_running = false;
523 debug("switch cancelled");
525 GLib.assert(i < m_engines.length);
529 int i = revert ? m_engines.length - 1 : 1;
534 private void run_preload_engines(IBus.EngineDesc[] engines, int index) {
537 if (engines.length <= index) {
541 names += engines[index].get_name();
542 m_bus.preload_engines_async(names, -1, null);
545 private void update_engines(string[]? unowned_engine_names,
546 string[]? order_names) {
547 string[]? engine_names = unowned_engine_names;
549 if (engine_names == null || engine_names.length == 0)
550 engine_names = {"xkb:us::eng"};
554 foreach (var name in order_names) {
555 if (name in engine_names)
559 foreach (var name in engine_names) {
565 var engines = m_bus.get_engines_by_names(names);
567 if (m_engines.length == 0) {
569 switch_engine(0, true);
570 run_preload_engines(engines, 1);
572 var current_engine = m_engines[0];
575 for (i = 0; i < m_engines.length; i++) {
576 if (current_engine.get_name() == engines[i].get_name()) {
579 run_preload_engines(engines, 0);
581 run_preload_engines(engines, 1);
586 switch_engine(0, true);
587 run_preload_engines(engines, 1);
592 private void show_setup_dialog() {
593 if (m_setup_pid != 0) {
594 if (Posix.kill(m_setup_pid, Posix.SIGUSR1) == 0)
599 string binary = GLib.Path.build_filename(Config.BINDIR, "ibus-setup");
601 GLib.Process.spawn_async(null,
602 {binary, "ibus-setup"},
604 GLib.SpawnFlags.DO_NOT_REAP_CHILD,
607 } catch (GLib.SpawnError e) {
608 warning("Execute %s failed! %s", binary, e.message);
612 GLib.ChildWatch.add(m_setup_pid, (pid, state) => {
613 if (pid != m_setup_pid)
616 GLib.Process.close_pid(pid);
620 private void show_about_dialog() {
621 if (m_about_dialog == null) {
622 m_about_dialog = new Gtk.AboutDialog();
623 m_about_dialog.set_program_name("IBus");
624 m_about_dialog.set_version(Config.PACKAGE_VERSION);
626 string copyright = _(
627 "Copyright (c) 2007-2012 Peng Huang\n" +
628 "Copyright (c) 2007-2010 Red Hat, Inc.\n");
630 m_about_dialog.set_copyright(copyright);
631 m_about_dialog.set_license("LGPL");
632 m_about_dialog.set_comments(_("IBus is an intelligent input bus for Linux/Unix."));
633 m_about_dialog.set_website("http://code.google.com/p/ibus");
634 m_about_dialog.set_authors({"Peng Huang <shawn.p.huang@gmail.com>"});
635 m_about_dialog.set_documenters({"Peng Huang <shawn.p.huang@gmail.com>"});
636 m_about_dialog.set_translator_credits(_("translator-credits"));
637 m_about_dialog.set_logo_icon_name("ibus");
638 m_about_dialog.set_icon_name("ibus");
641 if (!m_about_dialog.get_visible()) {
642 m_about_dialog.run();
643 m_about_dialog.hide();
645 m_about_dialog.present();
649 private void status_icon_popup_menu_cb(Gtk.StatusIcon status_icon,
651 uint activate_time) {
653 if (m_sys_menu == null) {
654 Gtk.ImageMenuItem item;
655 m_sys_menu = new Gtk.Menu();
657 item = new Gtk.ImageMenuItem.from_stock(Gtk.Stock.PREFERENCES, null);
658 item.activate.connect((i) => show_setup_dialog());
659 m_sys_menu.append(item);
661 item = new Gtk.ImageMenuItem.from_stock(Gtk.Stock.ABOUT, null);
662 item.activate.connect((i) => show_about_dialog());
663 m_sys_menu.append(item);
665 m_sys_menu.append(new Gtk.SeparatorMenuItem());
667 item = new Gtk.ImageMenuItem.from_stock(Gtk.Stock.REFRESH, null);
668 item.set_label(_("Restart"));
669 item.activate.connect((i) => m_bus.exit(true));
670 m_sys_menu.append(item);
672 item = new Gtk.ImageMenuItem.from_stock(Gtk.Stock.QUIT, null);
673 item.activate.connect((i) => m_bus.exit(false));
674 m_sys_menu.append(item);
676 m_sys_menu.show_all();
679 m_sys_menu.popup(null,
681 m_status_icon.position_menu,
683 Gtk.get_current_event_time());
686 private void status_icon_activate_cb(Gtk.StatusIcon status_icon) {
687 m_ime_menu = new Gtk.Menu();
689 // Show properties and IME switching menu
690 m_property_manager.create_menu_items(m_ime_menu);
692 m_ime_menu.append(new Gtk.SeparatorMenuItem());
695 foreach (var engine in m_engines) {
696 var language = engine.get_language();
697 var longname = engine.get_longname();
698 var item = new Gtk.ImageMenuItem.with_label(
699 "%s - %s".printf (IBus.get_language_name(language), longname));
700 if (engine.get_icon() != "") {
701 var icon = new IconWidget(engine.get_icon(), Gtk.IconSize.MENU);
702 item.set_image(icon);
704 // Make a copy of engine to workaround a bug in vala.
705 // https://bugzilla.gnome.org/show_bug.cgi?id=628336
707 item.activate.connect((item) => {
708 for (int i = 0; i < m_engines.length; i++) {
709 if (e == m_engines[i]) {
715 m_ime_menu.add(item);
718 m_ime_menu.show_all();
720 // Do not take focuse to avoid some focus related issues.
721 m_ime_menu.set_take_focus(false);
722 m_ime_menu.popup(null,
724 m_status_icon.position_menu,
726 Gtk.get_current_event_time());
729 /* override virtual functions */
730 public override void set_cursor_location(int x, int y,
731 int width, int height) {
732 m_candidate_panel.set_cursor_location(x, y, width, height);
735 public override void focus_in(string input_context_path) {
736 if (m_use_global_engine)
739 /* Do not change the order of m_engines during running switcher. */
740 if (m_switcher_is_running)
743 m_current_context_path = input_context_path;
745 var engine = m_engine_contexts[m_current_context_path];
747 if (engine == null) {
748 /* If engine == null, need to call set_engine(m_engines[0])
749 * here and update m_engine_contexts[] to avoid the
752 * If context1 is focused and does not set an engine and
753 * return here, the current engine1 is used for context1.
754 * When context2 is focused and switch engine1 to engine2,
755 * the current engine becomes engine2.
756 * And when context1 is focused again, context1 still
757 * does not set an engine and return here,
758 * engine2 is used for context2 instead of engine1. */
759 engine = m_engines.length > 0 ? m_engines[0] : null;
764 bool in_engines = false;
766 foreach (var e in m_engines) {
767 if (engine.get_name() == e.get_name()) {
773 /* The engine is deleted by ibus-setup before focus_in()
782 public override void focus_out(string input_context_path) {
783 if (m_use_global_engine)
786 /* Do not change the order of m_engines during running switcher. */
787 if (m_switcher_is_running)
790 m_current_context_path = "";
793 public override void destroy_context(string input_context_path) {
794 if (m_use_global_engine)
797 m_engine_contexts.remove(input_context_path);
800 public override void register_properties(IBus.PropList props) {
801 m_property_manager.set_properties(props);
804 public override void update_property(IBus.Property prop) {
805 m_property_manager.update_property(prop);
808 public override void update_preedit_text(IBus.Text text,
812 m_candidate_panel.set_preedit_text(text, cursor_pos);
814 m_candidate_panel.set_preedit_text(null, 0);
817 public override void hide_preedit_text() {
818 m_candidate_panel.set_preedit_text(null, 0);
821 public override void update_auxiliary_text(IBus.Text text,
823 m_candidate_panel.set_auxiliary_text(visible ? text : null);
826 public override void hide_auxiliary_text() {
827 m_candidate_panel.set_auxiliary_text(null);
830 public override void update_lookup_table(IBus.LookupTable table,
832 m_candidate_panel.set_lookup_table(visible ? table : null);
835 public override void hide_lookup_table() {
836 m_candidate_panel.set_lookup_table(null);
839 public override void state_changed() {
840 /* Do not change the order of m_engines during running switcher. */
841 if (m_switcher_is_running)
844 var icon_name = "ibus-keyboard";
846 var engine = m_bus.get_global_engine();
848 icon_name = engine.get_icon();
850 if (icon_name[0] == '/')
851 m_status_icon.set_from_file(icon_name);
853 var theme = Gtk.IconTheme.get_default();
854 if (theme.lookup_icon(icon_name, 48, 0) != null) {
855 m_status_icon.set_from_icon_name(icon_name);
857 m_status_icon.set_from_icon_name("ibus-engine");
865 for (i = 0; i < m_engines.length; i++) {
866 if (m_engines[i].get_name() == engine.get_name())
870 // engine is first engine in m_engines.
874 // engine is not in m_engines.
875 if (i >= m_engines.length)
878 for (int j = i; j > 0; j--) {
879 m_engines[j] = m_engines[j - 1];
881 m_engines[0] = engine;
884 foreach(var desc in m_engines) {
885 names += desc.get_name();
887 m_settings_general.set_strv("engines-order", names);