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