Implement use-system-keyboard-layout
[platform/upstream/ibus.git] / ui / gtk3 / panel.vala
1 /* vim:set et sts=4 sw=4:
2  *
3  * ibus - The Input Bus
4  *
5  * Copyright(c) 2011 Peng Huang <shawn.p.huang@gmail.com>
6  *
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 of the License, or(at your option) any later version.
11  *
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
15  * GNU Lesser General Public License for more details.
16  *
17  * You should have received a copy of the GNU Lesser General Public
18  * License along with this program; if not, write to the
19  * Free Software Foundation, Inc., 59 Temple Place, Suite 330,
20  * Boston, MA  02111-1307  USA
21  */
22
23 class Panel : IBus.PanelService {
24     private class Keybinding {
25         public Keybinding(uint keysym,
26                           Gdk.ModifierType modifiers,
27                           bool reverse) {
28             this.keysym = keysym;
29             this.modifiers = modifiers;
30             this.reverse = reverse;
31         }
32
33         public uint keysym { get; set; }
34         public Gdk.ModifierType modifiers { get; set; }
35         public bool reverse { get; set; }
36     }
37
38     private IBus.Bus m_bus;
39     private IBus.Config m_config;
40     private Gtk.StatusIcon m_status_icon;
41     private Gtk.Menu m_ime_menu;
42     private Gtk.Menu m_sys_menu;
43     private IBus.EngineDesc[] m_engines = {};
44     private CandidatePanel m_candidate_panel;
45     private Switcher m_switcher;
46     private PropertyManager m_property_manager;
47     private GLib.Pid m_setup_pid = 0;
48     private Gtk.AboutDialog m_about_dialog;
49     private Gtk.CssProvider m_css_provider;
50     private int m_switcher_delay_time = 400;
51     private bool m_use_system_keyboard_layout = false;
52     private const string ACCELERATOR_SWITCH_IME_FOREWARD = "<Control>space";
53
54     private GLib.List<Keybinding> m_keybindings = new GLib.List<Keybinding>();
55
56     public Panel(IBus.Bus bus) {
57         GLib.assert(bus.is_connected());
58         // Chain up base class constructor
59         GLib.Object(connection : bus.get_connection(),
60                     object_path : "/org/freedesktop/IBus/Panel");
61
62         m_bus = bus;
63
64         // init ui
65         m_status_icon = new Gtk.StatusIcon();
66         m_status_icon.set_name("ibus-ui-gtk");
67         m_status_icon.set_title("IBus Panel");
68         m_status_icon.popup_menu.connect(status_icon_popup_menu_cb);
69         m_status_icon.activate.connect(status_icon_activate_cb);
70         m_status_icon.set_from_icon_name("ibus-keyboard");
71
72         m_candidate_panel = new CandidatePanel();
73         m_candidate_panel.page_up.connect((w) => this.page_up());
74         m_candidate_panel.page_down.connect((w) => this.page_down());
75
76         m_switcher = new Switcher();
77         // The initial shortcut is "<Control>space"
78         bind_switch_shortcut(null);
79
80         if (m_switcher_delay_time >= 0) {
81             m_switcher.set_popup_delay_time((uint) m_switcher_delay_time);
82         }
83
84         m_property_manager = new PropertyManager();
85         m_property_manager.property_activate.connect((k, s) => {
86             property_activate(k, s);
87         });
88
89         state_changed();
90     }
91
92     ~Panel() {
93         unbind_switch_shortcut();
94     }
95
96     private void keybinding_manager_bind(KeybindingManager keybinding_manager,
97                                          string?           accelerator) {
98         uint switch_keysym = 0;
99         Gdk.ModifierType switch_modifiers = 0;
100         Gdk.ModifierType reverse_modifier = Gdk.ModifierType.SHIFT_MASK;
101         Keybinding keybinding;
102
103         Gtk.accelerator_parse(accelerator,
104                 out switch_keysym, out switch_modifiers);
105
106         // Map virtual modifiers to (i.e. Mod2, Mod3, ...)
107         const Gdk.ModifierType VIRTUAL_MODIFIERS = (
108                 Gdk.ModifierType.SUPER_MASK |
109                 Gdk.ModifierType.HYPER_MASK |
110                 Gdk.ModifierType.META_MASK);
111         if ((switch_modifiers & VIRTUAL_MODIFIERS) != 0) {
112         // workaround a bug in gdk vapi vala > 0.18
113         // https://bugzilla.gnome.org/show_bug.cgi?id=677559
114 #if VALA_0_18
115             Gdk.Keymap.get_default().map_virtual_modifiers(
116                     ref switch_modifiers);
117 #else
118             if ((switch_modifiers & Gdk.ModifierType.SUPER_MASK) != 0)
119                 switch_modifiers |= Gdk.ModifierType.MOD4_MASK;
120             if ((switch_modifiers & Gdk.ModifierType.HYPER_MASK) != 0)
121                 switch_modifiers |= Gdk.ModifierType.MOD4_MASK;
122 #endif
123             switch_modifiers &= ~VIRTUAL_MODIFIERS;
124         }
125
126         if (switch_keysym == 0 && switch_modifiers == 0) {
127             warning("Parse accelerator '%s' failed!", accelerator);
128             return;
129         }
130
131         keybinding = new Keybinding(switch_keysym,
132                                     switch_modifiers,
133                                     false);
134         m_keybindings.append(keybinding);
135
136         keybinding_manager.bind(switch_keysym, switch_modifiers,
137                 (e) => handle_engine_switch(e, false));
138
139         // accelerator already has Shift mask
140         if ((switch_modifiers & reverse_modifier) != 0) {
141             return;
142         }
143
144         switch_modifiers |= reverse_modifier;
145
146         keybinding = new Keybinding(switch_keysym,
147                                     switch_modifiers,
148                                     true);
149         m_keybindings.append(keybinding);
150
151         keybinding_manager.bind(switch_keysym, switch_modifiers,
152                 (e) => handle_engine_switch(e, true));
153     }
154
155     private void bind_switch_shortcut(Variant? variant) {
156         string[] accelerators = {};
157         Variant var_trigger = variant;
158
159         if (var_trigger == null && m_config != null) {
160             var_trigger = m_config.get_value("general/hotkey",
161                                              "triggers");
162         }
163
164         if (var_trigger != null) {
165             accelerators = var_trigger.dup_strv();
166         } else {
167             accelerators += ACCELERATOR_SWITCH_IME_FOREWARD;
168         }
169
170         var keybinding_manager = KeybindingManager.get_instance();
171
172         foreach (var accelerator in accelerators) {
173             keybinding_manager_bind(keybinding_manager, accelerator);
174         }
175     }
176
177     private void unbind_switch_shortcut() {
178         var keybinding_manager = KeybindingManager.get_instance();
179
180         unowned GLib.List<Keybinding> keybindings = m_keybindings;
181
182         while (keybindings != null) {
183             Keybinding keybinding = keybindings.data;
184
185             keybinding_manager.unbind(keybinding.keysym,
186                                       keybinding.modifiers);
187             keybindings = keybindings.next;
188         }
189
190         m_keybindings = null;
191     }
192
193     private void set_custom_font() {
194         Gdk.Display display = Gdk.Display.get_default();
195         Gdk.Screen screen = (display != null) ?
196                 display.get_default_screen() : null;
197
198         if (screen == null) {
199             warning("Could not open display.");
200             return;
201         }
202
203         bool use_custom_font = false;
204         GLib.Variant var_use_custom_font = m_config.get_value("panel",
205                                                               "use_custom_font");
206
207         if (var_use_custom_font != null) {
208             use_custom_font = var_use_custom_font.get_boolean();
209         }
210
211         if (m_css_provider != null) {
212             Gtk.StyleContext.remove_provider_for_screen(screen,
213                                                         m_css_provider);
214             m_css_provider = null;
215         }
216
217         if (use_custom_font == false) {
218             return;
219         }
220
221         string font_name = null;
222         GLib.Variant var_custom_font = m_config.get_value("panel",
223                                                           "custom_font");
224         if (var_custom_font != null) {
225             font_name = var_custom_font.dup_string();
226         }
227
228         if (font_name == null) {
229             warning("No config panel:custom_font.");
230             return;
231         }
232
233         string data_format = "GtkLabel { font: %s; }";
234         string data = data_format.printf(font_name);
235         m_css_provider = new Gtk.CssProvider();
236
237         try {
238             m_css_provider.load_from_data(data, -1);
239         } catch (GLib.Error e) {
240             warning("Failed css_provider_from_data: %s: %s", font_name,
241                                                              e.message);
242             return;
243         }
244
245         Gtk.StyleContext.add_provider_for_screen(screen,
246                                                  m_css_provider,
247                                                  Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION);
248     }
249
250     private void set_switcher_delay_time(Variant? variant) {
251         Variant var_switcher_delay_time = variant;
252
253         if (var_switcher_delay_time == null) {
254             var_switcher_delay_time = m_config.get_value("general",
255                                                          "switcher-delay-time");
256         }
257
258         if (var_switcher_delay_time == null) {
259             return;
260         }
261
262         m_switcher_delay_time = var_switcher_delay_time.get_int32();
263
264         if (m_switcher_delay_time >= 0) {
265             m_switcher.set_popup_delay_time((uint) m_switcher_delay_time);
266         }
267     }
268
269     private void set_use_system_keyboard_layout(Variant? variant) {
270         Variant var_use_system_kbd_layout = variant;
271
272         if (var_use_system_kbd_layout == null) {
273             var_use_system_kbd_layout = m_config.get_value(
274                     "general",
275                     "use_system_keyboard_layout");
276         }
277
278         if (var_use_system_kbd_layout == null) {
279             return;
280         }
281
282         m_use_system_keyboard_layout = var_use_system_kbd_layout.get_boolean();
283     }
284
285     public void set_config(IBus.Config config) {
286         if (m_config != null) {
287             m_config.value_changed.disconnect(config_value_changed_cb);
288             m_config.watch(null, null);
289             m_config = null;
290         }
291
292         m_config = config;
293         if (m_config != null) {
294             m_config.value_changed.connect(config_value_changed_cb);
295             m_config.watch("general", "preload_engines");
296             m_config.watch("general", "engines_order");
297             m_config.watch("general", "switcher_delay_time");
298             m_config.watch("general", "use_system_keyboard_layout");
299             m_config.watch("general/hotkey", "triggers");
300             m_config.watch("panel", "custom_font");
301             m_config.watch("panel", "use_custom_font");
302             update_engines(m_config.get_value("general", "preload_engines"),
303                            m_config.get_value("general", "engines_order"));
304             unbind_switch_shortcut();
305             bind_switch_shortcut(null);
306             set_switcher_delay_time(null);
307             set_use_system_keyboard_layout(null);
308         } else {
309             update_engines(null, null);
310         }
311
312         set_custom_font();
313     }
314
315     private void exec_setxkbmap(IBus.EngineDesc engine) {
316         string layout = engine.get_layout();
317         string variant = engine.get_layout_variant();
318         string option = engine.get_layout_option();
319         string standard_error = null;
320         int exit_status = 0;
321         string[] args = { "setxkbmap" };
322
323         if (layout != null && layout != "" && layout != "default") {
324             args += "-layout";
325             args += layout;
326         }
327         if (variant != null && variant != "" && variant != "default") {
328             args += "-variant";
329             args += variant;
330         }
331         if (option != null && option != "" && option != "default") {
332             /*TODO: Need to get the session XKB options */
333             args += "-option";
334             args += "-option";
335             args += option;
336         }
337
338         if (args.length == 1) {
339             return;
340         }
341
342         try {
343             if (!GLib.Process.spawn_sync(null, args, null,
344                                          GLib.SpawnFlags.SEARCH_PATH,
345                                          null, null,
346                                          out standard_error,
347                                          out exit_status)) {
348                 warning("Switch xkb layout to %s failed.",
349                         engine.get_layout());
350             }
351         } catch (GLib.SpawnError e) {
352             warning("Execute setxkbmap failed: %s", e.message);
353         }
354
355         if (exit_status != 0) {
356             warning("Execute setxkbmap failed: %s", standard_error ?? "(null)");
357         }
358     }
359
360     private void switch_engine(int i, bool force = false) {
361         GLib.assert(i >= 0 && i < m_engines.length);
362
363         // Do not need switch
364         if (i == 0 && !force)
365             return;
366
367         IBus.EngineDesc engine = m_engines[i];
368
369         if (!m_bus.set_global_engine(engine.get_name())) {
370             warning("Switch engine to %s failed.", engine.get_name());
371             return;
372         }
373         // set xkb layout
374         if (!m_use_system_keyboard_layout) {
375             exec_setxkbmap(engine);
376         }
377     }
378
379     private void config_value_changed_cb(IBus.Config config,
380                                          string section,
381                                          string name,
382                                          Variant variant) {
383         if (section == "general" && name == "preload_engines") {
384             update_engines(variant, null);
385             return;
386         }
387
388         if (section == "general/hotkey" && name == "triggers") {
389             unbind_switch_shortcut();
390             bind_switch_shortcut(variant);
391             return;
392         }
393
394         if (section == "panel" && (name == "custom_font" ||
395                                    name == "use_custom_font")) {
396             set_custom_font();
397             return;
398         }
399
400         if (section == "general" && name == "switcher_delay_time") {
401             set_switcher_delay_time(variant);
402             return;
403         }
404
405         if (section == "general" && name == "use_system_keyboard_layout") {
406             set_use_system_keyboard_layout(variant);
407             return;
408         }
409     }
410
411     private void handle_engine_switch(Gdk.Event event, bool revert) {
412         // Do not need switch IME
413         if (m_engines.length <= 1)
414             return;
415
416         uint keyval = event.key.keyval;
417         uint modifiers = KeybindingManager.MODIFIER_FILTER & event.key.state;
418
419         uint primary_modifiers =
420             KeybindingManager.get_primary_modifier(event.key.state);
421
422         bool pressed = KeybindingManager.primary_modifier_still_pressed(
423                 event, primary_modifiers);
424
425         if (revert) {
426             modifiers &= ~Gdk.ModifierType.SHIFT_MASK;
427         }
428
429         if (pressed && m_switcher_delay_time >= 0) {
430             int i = revert ? m_engines.length - 1 : 1;
431             i = m_switcher.run(keyval, modifiers, event, m_engines, i);
432             if (i < 0) {
433                 debug("switch cancelled");
434             } else {
435                 GLib.assert(i < m_engines.length);
436                 switch_engine(i);
437             }
438         } else {
439             int i = revert ? m_engines.length - 1 : 1;
440             switch_engine(i);
441         }
442     }
443
444     private void update_engines(GLib.Variant? var_engines,
445                                 GLib.Variant? var_order) {
446         string[] engine_names = null;
447
448         if (var_engines != null)
449             engine_names = var_engines.dup_strv();
450         if (engine_names == null || engine_names.length == 0)
451             engine_names = {"xkb:us::eng"};
452
453         string[] order_names =
454             (var_order != null) ? var_order.dup_strv() : null;
455
456         string[] names = {};
457
458         foreach (var name in order_names) {
459             if (name in engine_names)
460                 names += name;
461         }
462
463         foreach (var name in engine_names) {
464             if (name in names)
465                 continue;
466             names += name;
467         }
468
469         var engines = m_bus.get_engines_by_names(names);
470
471         if (m_engines.length == 0) {
472             m_engines = engines;
473             switch_engine(0, true);
474         } else {
475             var current_engine = m_engines[0];
476             m_engines = engines;
477             int i;
478             for (i = 0; i < m_engines.length; i++) {
479                 if (current_engine.get_name() == engines[i].get_name()) {
480                     switch_engine(i);
481                     return;
482                 }
483             }
484             switch_engine(0, true);
485         }
486
487     }
488
489     private void show_setup_dialog() {
490         if (m_setup_pid != 0) {
491             if (Posix.kill(m_setup_pid, Posix.SIGUSR1) == 0)
492                 return;
493             m_setup_pid = 0;
494         }
495
496         string binary = GLib.Path.build_filename(Config.BINDIR, "ibus-setup");
497         try {
498             GLib.Process.spawn_async(null,
499                                      {binary, "ibus-setup"},
500                                      null,
501                                      GLib.SpawnFlags.DO_NOT_REAP_CHILD,
502                                      null,
503                                      out m_setup_pid);
504         } catch (GLib.SpawnError e) {
505             warning("Execute %s failed! %s", binary, e.message);
506             m_setup_pid = 0;
507         }
508
509         GLib.ChildWatch.add(m_setup_pid, (pid, state) => {
510             if (pid != m_setup_pid)
511                 return;
512             m_setup_pid = 0;
513             GLib.Process.close_pid(pid);
514         });
515     }
516
517     private void show_about_dialog() {
518         if (m_about_dialog == null) {
519             m_about_dialog = new Gtk.AboutDialog();
520             m_about_dialog.set_program_name("IBus");
521             m_about_dialog.set_version(Config.PACKAGE_VERSION);
522
523             string copyright = _(
524                 "Copyright (c) 2007-2012 Peng Huang\n" +
525                 "Copyright (c) 2007-2010 Red Hat, Inc.\n");
526
527             m_about_dialog.set_copyright(copyright);
528             m_about_dialog.set_license("LGPL");
529             m_about_dialog.set_comments(_("IBus is an intelligent input bus for Linux/Unix."));
530             m_about_dialog.set_website("http://code.google.com/p/ibus");
531             m_about_dialog.set_authors({"Peng Huang <shawn.p.huang@gmail.com>"});
532             m_about_dialog.set_documenters({"Peng Huang <shawn.p.huang@gmail.com>"});
533             m_about_dialog.set_translator_credits(_("translator-credits"));
534             m_about_dialog.set_logo_icon_name("ibus");
535             m_about_dialog.set_icon_name("ibus");
536         }
537
538         if (!m_about_dialog.get_visible()) {
539             m_about_dialog.run();
540             m_about_dialog.hide();
541         } else {
542             m_about_dialog.present();
543         }
544     }
545
546     private void status_icon_popup_menu_cb(Gtk.StatusIcon status_icon,
547                                            uint button,
548                                            uint activate_time) {
549         // Show system menu
550         if (m_sys_menu == null) {
551             Gtk.ImageMenuItem item;
552             m_sys_menu = new Gtk.Menu();
553
554             item = new Gtk.ImageMenuItem.from_stock(Gtk.Stock.PREFERENCES, null);
555             item.activate.connect((i) => show_setup_dialog());
556             m_sys_menu.append(item);
557
558             item = new Gtk.ImageMenuItem.from_stock(Gtk.Stock.ABOUT, null);
559             item.activate.connect((i) => show_about_dialog());
560             m_sys_menu.append(item);
561
562             m_sys_menu.append(new Gtk.SeparatorMenuItem());
563
564             item = new Gtk.ImageMenuItem.from_stock(Gtk.Stock.REFRESH, null);
565             item.set_label(_("Restart"));
566             item.activate.connect((i) => m_bus.exit(true));
567             m_sys_menu.append(item);
568
569             item = new Gtk.ImageMenuItem.from_stock(Gtk.Stock.QUIT, null);
570             item.activate.connect((i) => m_bus.exit(false));
571             m_sys_menu.append(item);
572
573             m_sys_menu.show_all();
574         }
575
576         m_sys_menu.popup(null,
577                          null,
578                          m_status_icon.position_menu,
579                          0,
580                          Gtk.get_current_event_time());
581     }
582
583     private void status_icon_activate_cb(Gtk.StatusIcon status_icon) {
584         m_ime_menu = new Gtk.Menu();
585
586         // Show properties and IME switching menu
587         m_property_manager.create_menu_items(m_ime_menu);
588
589         m_ime_menu.append(new Gtk.SeparatorMenuItem());
590
591         // Append IMEs
592         foreach (var engine in m_engines) {
593             var language = engine.get_language();
594             var longname = engine.get_longname();
595             var item = new Gtk.ImageMenuItem.with_label(
596                 "%s - %s".printf (IBus.get_language_name(language), longname));
597             if (engine.get_icon() != "") {
598                 var icon = new IconWidget(engine.get_icon(), Gtk.IconSize.MENU);
599                  item.set_image(icon);
600             }
601             // Make a copy of engine to workaround a bug in vala.
602             // https://bugzilla.gnome.org/show_bug.cgi?id=628336
603             var e = engine;
604             item.activate.connect((item) => {
605                 for (int i = 0; i < m_engines.length; i++) {
606                     if (e == m_engines[i]) {
607                         switch_engine(i);
608                         break;
609                     }
610                 }
611             });
612             m_ime_menu.add(item);
613         }
614
615         m_ime_menu.show_all();
616
617         // Do not take focuse to avoid some focus related issues.
618         m_ime_menu.set_take_focus(false);
619         m_ime_menu.popup(null,
620                          null,
621                          m_status_icon.position_menu,
622                          0,
623                          Gtk.get_current_event_time());
624     }
625
626     /* override virtual functions */
627     public override void set_cursor_location(int x, int y,
628                                              int width, int height) {
629         m_candidate_panel.set_cursor_location(x, y, width, height);
630     }
631
632     public override void focus_in(string input_context_path) {
633     }
634
635     public override void focus_out(string input_context_path) {
636     }
637
638     public override void register_properties(IBus.PropList props) {
639         m_property_manager.set_properties(props);
640     }
641
642     public override void update_property(IBus.Property prop) {
643         m_property_manager.update_property(prop);
644     }
645
646     public override void update_preedit_text(IBus.Text text,
647                                              uint cursor_pos,
648                                              bool visible) {
649         if (visible)
650             m_candidate_panel.set_preedit_text(text, cursor_pos);
651         else
652             m_candidate_panel.set_preedit_text(null, 0);
653     }
654
655     public override void hide_preedit_text() {
656         m_candidate_panel.set_preedit_text(null, 0);
657     }
658
659     public override void update_auxiliary_text(IBus.Text text,
660                                                bool visible) {
661         m_candidate_panel.set_auxiliary_text(visible ? text : null);
662     }
663
664     public override void hide_auxiliary_text() {
665         m_candidate_panel.set_auxiliary_text(null);
666     }
667
668     public override void update_lookup_table(IBus.LookupTable table,
669                                              bool visible) {
670         m_candidate_panel.set_lookup_table(visible ? table : null);
671     }
672
673     public override void hide_lookup_table() {
674         m_candidate_panel.set_lookup_table(null);
675     }
676
677     public override void state_changed() {
678         var icon_name = "ibus-keyboard";
679
680         var engine = m_bus.get_global_engine();
681         if (engine != null)
682             icon_name = engine.get_icon();
683
684         if (icon_name[0] == '/')
685             m_status_icon.set_from_file(icon_name);
686         else {
687             var theme = Gtk.IconTheme.get_default();
688             if (theme.lookup_icon(icon_name, 48, 0) != null) {
689                 m_status_icon.set_from_icon_name(icon_name);
690             } else {
691                 m_status_icon.set_from_icon_name("ibus-engine");
692             }
693         }
694
695         if (engine == null)
696             return;
697
698         int i;
699         for (i = 0; i < m_engines.length; i++) {
700             if (m_engines[i].get_name() == engine.get_name())
701                 break;
702         }
703
704         // engine is first engine in m_engines.
705         if (i == 0)
706             return;
707
708         // engine is not in m_engines.
709         if (i >= m_engines.length)
710             return;
711
712         for (int j = i; j > 0; j--) {
713             m_engines[j] = m_engines[j - 1];
714         }
715         m_engines[0] = engine;
716
717         string[] names = {};
718         foreach(var desc in m_engines) {
719             names += desc.get_name();
720         }
721         if (m_config != null)
722             m_config.set_value("general",
723                                "engines_order",
724                                new GLib.Variant.strv(names));
725     }
726 }