Blob Blame History Raw
/* vim:set et sts=4 sw=4:
 *
 * ibus - The Input Bus
 *
 * Copyright(c) 2011-2016 Peng Huang <shawn.p.huang@gmail.com>
 * Copyright(c) 2015-2017 Takao Fujiwara <takao.fujiwara1@gmail.com>
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301
 * USA
 */

class Switcher : Gtk.Window {
    private class IBusEngineButton : Gtk.Button {
        public IBusEngineButton(IBus.EngineDesc engine, Switcher switcher) {
            GLib.Object();

            var longname = engine.get_longname();
            var textdomain = engine.get_textdomain();
            this.transname = GLib.dgettext(textdomain, longname);

            var name = engine.get_name();

            if (name.length < 4 || name[0:4] != "xkb:") {
                IconWidget icon = new IconWidget(engine.get_icon(),
                                                 Gtk.IconSize.DIALOG);
                icon.set_halign(Gtk.Align.CENTER);
                icon.set_valign(Gtk.Align.CENTER);
                add(icon);
            } else {
                var language = switcher.get_xkb_language(engine);

                Gtk.Label label = new Gtk.Label(language);
                label.set_halign(Gtk.Align.CENTER);
                label.set_valign(Gtk.Align.CENTER);
                string language_font = "Monospace Bold 16";
                string markup = "<span font=\"%s\">%s</span>".
                        printf(language_font, language);

                label.set_markup(markup);

                int fixed_width, fixed_height;
                Gtk.icon_size_lookup(Gtk.IconSize.DIALOG,
                                     out fixed_width,
                                     out fixed_height);
                label.set_size_request(fixed_width, fixed_height);

                add(label);
            }
        }

        public string transname { get; set; }

        public override bool draw(Cairo.Context cr) {
            base.draw(cr);
            if (is_focus) {
                cr.save();
                cr.rectangle(
                        0, 0, get_allocated_width(), get_allocated_height());
                cr.set_source_rgba(0.0, 0.0, 1.0, 0.1);
                cr.fill();
                cr.restore();
            }
            return true;
        }
    }

    private Gtk.Box m_box;
    private Gtk.Label m_label;
    private IBusEngineButton[] m_buttons = {};
    private IBus.EngineDesc[] m_engines;
    private uint m_selected_engine;
    private uint m_keyval;
    private uint m_modifiers;
    private Gdk.ModifierType m_primary_modifier;
    private bool m_is_running = false;
    private string m_input_context_path = "";
    private GLib.MainLoop m_loop;
    private int m_result = -1;
    private IBus.EngineDesc? m_result_engine = null;
    private uint m_popup_delay_time = 0;
    private uint m_popup_delay_time_id = 0;
    private int m_root_x;
    private int m_root_y;
    private double m_mouse_init_x;
    private double m_mouse_init_y;
    private bool   m_mouse_moved;
    private GLib.HashTable<string, string> m_xkb_languages =
            new GLib.HashTable<string, string>(GLib.str_hash,
                                               GLib.str_equal);

    public Switcher() {
        GLib.Object(
            type : Gtk.WindowType.POPUP,
            events : Gdk.EventMask.KEY_PRESS_MASK | Gdk.EventMask.KEY_RELEASE_MASK,
            window_position : Gtk.WindowPosition.CENTER,
            accept_focus : true,
            decorated : false,
            modal : true,
            focus_visible : true
        );
        Gtk.Box vbox = new Gtk.Box(Gtk.Orientation.VERTICAL, 0);
        add(vbox);
        m_box = new Gtk.Box(Gtk.Orientation.HORIZONTAL, 0);
        m_box.set_halign(Gtk.Align.CENTER);
        m_box.set_valign(Gtk.Align.CENTER);
        vbox.pack_start(m_box, true, true, 0);
        m_label = new Gtk.Label("");

        /* Set the accessible role of the label to a status bar so it
         * will emit name changed events that can be used by screen
         * readers.
         */
        Atk.Object obj = m_label.get_accessible();
        obj.set_role (Atk.Role.STATUSBAR);

        /* Use Gtk.Widget.set_margin_start() and
         * Gtk.Widget.set_margin_top() since gtk 3.12 */
        m_label.set_padding(3, 3);
        vbox.pack_end(m_label, false, false, 0);

        grab_focus();
    }

    public int run(uint              keyval,
                   uint              state,
                   Gdk.Event         event,
                   IBus.EngineDesc[] engines,
                   int               index,
                   string            input_context_path) {
        assert (m_loop == null);
        assert (index < engines.length);

        m_is_running = true;
        m_keyval = keyval;
        m_modifiers = state;
        m_primary_modifier =
            KeybindingManager.get_primary_modifier(
                state & KeybindingManager.MODIFIER_FILTER);
        m_selected_engine = m_result = index;
        m_input_context_path = input_context_path;
        m_result_engine = null;

        update_engines(engines);
        /* Let gtk recalculate the window size. */
        resize(1, 1);

        m_label.set_text(m_buttons[index].transname);
        m_buttons[index].grab_focus();

        // Avoid regressions.
        if (m_popup_delay_time > 0) {
            get_position(out m_root_x, out m_root_y);
            // Pull the window from the screen so that the window gets
            // the key press and release events but mouse does not select
            // an IME unexpectedly.
            move(-1000, -1000);
        }

        show_all();

        if (m_popup_delay_time > 0) {
            // Restore the window position after m_popup_delay_time
            m_popup_delay_time_id = GLib.Timeout.add(m_popup_delay_time,
                                                     () => {
                restore_window_position("timeout");
                return false;
            });
        }

        Gdk.Device pointer;
#if VALA_0_34
        Gdk.Seat seat = event.get_seat();
        if (seat == null) {
            var display = get_display();
            seat = display.get_default_seat();
        }
        //keyboard = seat.get_keyboard();
        pointer = seat.get_pointer();

        Gdk.GrabStatus status;
        // Grab all keyboard events
        status = seat.grab(get_window(),
                           Gdk.SeatCapabilities.KEYBOARD,
                           true,
                           null,
                           event,
                           null);
        if (status != Gdk.GrabStatus.SUCCESS)
            warning("Grab keyboard failed! status = %d", status);
        status = seat.grab(get_window(),
                           Gdk.SeatCapabilities.POINTER,
                           true,
                           null,
                           event,
                           null);
        if (status != Gdk.GrabStatus.SUCCESS)
            warning("Grab pointer failed! status = %d", status);
#else
        Gdk.Device device = event.get_device();
        if (device == null) {
            var display = get_display();
            var device_manager = display.get_device_manager();
/* The macro VALA_X_Y supports even numbers.
 * http://git.gnome.org/browse/vala/commit/?id=294b374af6
 */
#if VALA_0_16
            device = device_manager.list_devices(Gdk.DeviceType.MASTER).data;
#else
            unowned GLib.List<Gdk.Device> devices =
                    device_manager.list_devices(Gdk.DeviceType.MASTER);
            device = devices.data;
#endif
        }

        Gdk.Device keyboard;
        if (device.get_source() == Gdk.InputSource.KEYBOARD) {
            keyboard = device;
            pointer = device.get_associated_device();
        } else {
            pointer = device;
            keyboard = device.get_associated_device();
        }

        Gdk.GrabStatus status;
        // Grab all keyboard events
        status = keyboard.grab(get_window(),
                               Gdk.GrabOwnership.NONE,
                               true,
                               Gdk.EventMask.KEY_PRESS_MASK |
                               Gdk.EventMask.KEY_RELEASE_MASK,
                               null,
                               Gdk.CURRENT_TIME);
        if (status != Gdk.GrabStatus.SUCCESS)
            warning("Grab keyboard failed! status = %d", status);
        // Grab all pointer events
        status = pointer.grab(get_window(),
                              Gdk.GrabOwnership.NONE,
                              true,
                              Gdk.EventMask.BUTTON_PRESS_MASK |
                              Gdk.EventMask.BUTTON_RELEASE_MASK,
                              null,
                              Gdk.CURRENT_TIME);
        if (status != Gdk.GrabStatus.SUCCESS)
            warning("Grab pointer failed! status = %d", status);
#endif

        // Probably we can delete m_popup_delay_time in 1.6
        pointer.get_position_double(null,
                                    out m_mouse_init_x,
                                    out m_mouse_init_y);
        m_mouse_moved = false;


        m_loop = new GLib.MainLoop();
        m_loop.run();
        m_loop = null;

#if VALA_0_34
        seat.ungrab();
#else
        keyboard.ungrab(Gdk.CURRENT_TIME);
        pointer.ungrab(Gdk.CURRENT_TIME);
#endif

        hide();
        // Make sure the switcher is hidden before returning from this function.
        while (Gtk.events_pending())
            Gtk.main_iteration ();

        GLib.assert(m_result < m_engines.length);
        m_result_engine = m_engines[m_result];
        m_is_running = false;

        return m_result;
    }

    /* Based on metacity/src/ui/tabpopup.c:meta_ui_tab_popup_new */
    private void update_engines(IBus.EngineDesc[] engines) {
        foreach (var button in m_buttons) {
            button.destroy();
        }
        m_buttons = {};

        if (engines == null) {
            m_engines = {};
            return;
        }

        m_engines = engines;
        int max_label_width = 0;

        for (int i = 0; i < m_engines.length; i++) {
            var index = i;
            var engine = m_engines[i];
            var button = new IBusEngineButton(engine, this);
            var longname = engine.get_longname();
            var textdomain = engine.get_textdomain();
            var transname = GLib.dgettext(textdomain, longname);
            button.set_relief(Gtk.ReliefStyle.NONE);
            button.add_events(Gdk.EventMask.POINTER_MOTION_MASK);
            button.show();

            button.enter_notify_event.connect((e) => {
                // avoid gtk_button_update_state()
                return true;
            });
            button.motion_notify_event.connect((e) => {
#if VALA_0_24
                Gdk.EventMotion pe = e;
#else
                Gdk.EventMotion *pe = &e;
#endif
                if (m_selected_engine == index)
                    return false;
                if (!m_mouse_moved &&
                    m_mouse_init_x == pe.x_root &&
                    m_mouse_init_y == pe.y_root) {
                    return false;
                }
                m_mouse_moved = true;
                button.grab_focus();
                m_selected_engine = index;
                return false;
            });

            button.button_press_event.connect((e) => {
                m_selected_engine = index;
                m_result = (int)m_selected_engine;
                m_loop.quit();
                return true;
            });

            button.transname = transname;
            m_label.set_label(transname);

            int width;
            m_label.get_preferred_width(null, out width);
            max_label_width = int.max(max_label_width, width);

            m_box.pack_start(button, true, true);
            m_buttons += button;
        }

        m_label.set_text(m_buttons[0].transname);
        m_label.set_ellipsize(Pango.EllipsizeMode.END);

        Gdk.Display display = Gdk.Display.get_default();
        int screen_width = 0;
#if VALA_0_34
        // display.get_monitor_at_window() is null because of unrealized window
        Gdk.Monitor monitor = display.get_primary_monitor();
        Gdk.Rectangle area = monitor.get_geometry();
        screen_width = area.width;
#else
        Gdk.Screen screen = (display != null) ?
                display.get_default_screen() : null;

        if (screen != null) {
            screen_width = screen.get_width();
        }
#endif

        if (screen_width > 0 && max_label_width > (screen_width / 4)) {
            max_label_width = screen_width / 4;
        }

        /* add random padding */
        max_label_width += 20;
        set_default_size(max_label_width, -1);
    }

    private void next_engine() {
        if (m_selected_engine == m_engines.length - 1)
            m_selected_engine = 0;
        else
            m_selected_engine ++;
        m_label.set_text(m_buttons[m_selected_engine].transname);
        set_focus(m_buttons[m_selected_engine]);
    }

    private void previous_engine() {
        if (m_selected_engine == 0)
            m_selected_engine = m_engines.length - 1;
        else
            m_selected_engine --;
        m_label.set_text(m_buttons[m_selected_engine].transname);
        set_focus(m_buttons[m_selected_engine]);
    }

    private void restore_window_position(string debug_str) {
        debug("restore_window_position %s: (%ld, %ld)\n",
                debug_str, m_root_x, m_root_y);

        if (m_popup_delay_time_id == 0) {
            return;
        }

        GLib.Source.remove(m_popup_delay_time_id);
        m_popup_delay_time_id = 0;
        move(m_root_x, m_root_y);
    }

    /* override virtual functions */
    public override void show() {
        base.show();
        set_focus_visible(true);
    }

    public override bool key_press_event(Gdk.EventKey e) {
        bool retval = true;

/* Gdk.EventKey is changed to the pointer.
 * https://git.gnome.org/browse/vala/commit/?id=598942f1
 */
#if VALA_0_24
        Gdk.EventKey pe = e;
#else
        Gdk.EventKey *pe = &e;
#endif

        if (m_popup_delay_time > 0) {
            restore_window_position("pressed");
        }

        do {
            uint modifiers = KeybindingManager.MODIFIER_FILTER & pe.state;

            if ((modifiers != m_modifiers) &&
                (modifiers != (m_modifiers | Gdk.ModifierType.SHIFT_MASK))) {
                break;
            }

            if (pe.keyval == m_keyval) {
                if (modifiers == m_modifiers)
                    next_engine();
                else // modififers == m_modifiers | SHIFT_MASK
                    previous_engine();
                break;
            }

            switch (pe.keyval) {
                case 0x08fb: /* leftarrow */
                case 0xff51: /* Left */
                    previous_engine();
                    break;
                case 0x08fc: /* uparrow */
                case 0xff52: /* Up */
                    break;
                case 0x08fd: /* rightarrow */
                case 0xff53: /* Right */
                    next_engine();
                    break;
                case 0x08fe: /* downarrow */
                case 0xff54: /* Down */
                    break;
                default:
                    debug("0x%04x", pe.keyval);
                    break;
            }
        } while (false);
        return retval;
    }

    public override bool key_release_event(Gdk.EventKey e) {
#if VALA_0_24
        Gdk.EventKey pe = e;
#else
        Gdk.EventKey *pe = &e;
#endif

        if (KeybindingManager.primary_modifier_still_pressed((Gdk.Event) pe,
            m_primary_modifier)) {
            return true;
        }

        // if e.type == Gdk.EventType.KEY_RELEASE, m_loop is already null.
        if (m_loop == null) {
            return false;
        }

        if (m_popup_delay_time > 0) {
            if (m_popup_delay_time_id != 0) {
                GLib.Source.remove(m_popup_delay_time_id);
                m_popup_delay_time_id = 0;
            }
        }

        m_loop.quit();
        m_result = (int)m_selected_engine;
        return true;
    }

    public void set_popup_delay_time(uint popup_delay_time) {
        m_popup_delay_time = popup_delay_time;
    }

    public string get_xkb_language(IBus.EngineDesc engine) {
        var name = engine.get_name();

        assert(name[0:4] == "xkb:");

        var language = m_xkb_languages[name];

        if (language != null)
            return language;

        language = engine.get_language();
        int length = language.length;

        /* Maybe invalid layout */
        if (length < 2)
            return language;

        language = language.up();

        int index = 0;

        foreach (var saved_language in m_xkb_languages.get_values()) {
            if (language == saved_language[0:length])
                index++;
        }

        if (index > 0) {
            unichar u = 0x2081 + index;
            language = "%s%s".printf(language, u.to_string());
        }

        m_xkb_languages.insert(name, language);
        return language;
    }

    public bool is_running() {
        return m_is_running;
    }

    public string get_input_context_path() {
        return m_input_context_path;
    }

    public IBus.EngineDesc? get_selected_engine() {
        return m_result_engine;
    }

    public void reset() {
        m_input_context_path = "";
        m_result = -1;
        m_result_engine = null;
    }
}