Blob Blame History Raw
/* vim:set et sts=4 sw=4:
 *
 * ibus - The Input Bus
 *
 * Copyright(c) 2011-2015 Peng Huang <shawn.p.huang@gmail.com>
 * Copyright(c) 2015-2018 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
 */

public class CandidatePanel : Gtk.Box{
    private bool m_vertical_panel_system = true;
    private bool m_vertical_writing;
    private Gtk.Window m_toplevel;
    private Gtk.Box m_vbox;

    private Gtk.Label m_preedit_label;
    private Gtk.Label m_aux_label;
    private CandidateArea m_candidate_area;
    private HSeparator m_hseparator;

    private Gdk.Rectangle m_cursor_location;

    public signal void cursor_up();
    public signal void cursor_down();
    public signal void page_up();
    public signal void page_down();
    public signal void candidate_clicked(uint index,
                                         uint button,
                                         uint state);

    public CandidatePanel() {
        // Call base class constructor
        GLib.Object(
            name : "IBusCandidate",
            orientation: Gtk.Orientation.HORIZONTAL,
            visible: true
        );

        m_toplevel = new Gtk.Window(Gtk.WindowType.POPUP);
        m_toplevel.add_events(Gdk.EventMask.BUTTON_PRESS_MASK);
        m_toplevel.button_press_event.connect((w, e) => {
            if (e.button != 1 || (e.state & Gdk.ModifierType.CONTROL_MASK) == 0)
                return false;
            set_vertical(!m_vertical_panel_system);
            return true;
        });
        m_toplevel.size_allocate.connect((w, a) => {
            adjust_window_position();
        });

        Handle handle = new Handle();
        handle.set_visible(true);
        pack_start(handle, false, false, 0);

        m_vbox = new Gtk.Box(Gtk.Orientation.VERTICAL, 0);
        m_vbox.set_visible(true);
        pack_start(m_vbox, false, false, 0);

        m_toplevel.add(this);

        create_ui();
    }

    public void set_vertical(bool vertical) {
        if (m_vertical_panel_system == vertical)
            return;
        m_vertical_panel_system = vertical;
        m_candidate_area.set_vertical(vertical);
    }

    private void set_orientation(IBus.Orientation orientation) {
        switch (orientation) {
        case IBus.Orientation.VERTICAL:
            m_candidate_area.set_vertical(true);
            break;
        case IBus.Orientation.HORIZONTAL:
            m_candidate_area.set_vertical(false);
            break;
        case IBus.Orientation.SYSTEM:
            m_candidate_area.set_vertical(m_vertical_panel_system);
            break;
        }
    }

    public void set_cursor_location(int x, int y, int width, int height) {
        Gdk.Rectangle location = Gdk.Rectangle(){
            x = x, y = y, width = width, height = height };
        if (m_cursor_location == location)
            return;
        m_cursor_location = location;

        /* Do not call adjust_window_position() here because
         * m_toplevel is not shown yet and
         * m_toplevel.get_allocation() returns height = width = 1 */
    }

    private void set_labels(IBus.Text[] labels) {
        m_candidate_area.set_labels(labels);
    }

    private void set_attributes(Gtk.Label label, IBus.Text text) {
        Pango.AttrList attrs = get_pango_attr_list_from_ibus_text(text);

        Gtk.StyleContext context = label.get_style_context();
        Gdk.RGBA color;

        if (context.lookup_color("placeholder_text_color", out color)) {
            Pango.Attribute pango_attr = Pango.attr_foreground_new(
                        (uint16)(color.red * uint16.MAX),
                        (uint16)(color.green * uint16.MAX),
                        (uint16)(color.blue * uint16.MAX));
            pango_attr.start_index = 0;
            pango_attr.end_index = label.get_text().length;
            attrs.insert((owned)pango_attr);
        }

        label.set_attributes(attrs);
    }

    public void set_preedit_text(IBus.Text? text, uint cursor) {
        if (text != null) {
            var str = text.get_text();

            if (str.length > 0) {
                m_preedit_label.set_text(str);
                m_preedit_label.show();
                set_attributes(m_preedit_label, text);
            } else {
                m_preedit_label.set_text("");
                m_preedit_label.hide();
            }
        } else {
            m_preedit_label.set_text("");
            m_preedit_label.hide();
        }
        update();
    }

    public void set_auxiliary_text(IBus.Text? text) {
        if (text != null) {
            m_aux_label.set_text(text.get_text());
            Pango.AttrList attrs = get_pango_attr_list_from_ibus_text(text);
            m_aux_label.set_attributes(attrs);
            m_aux_label.show();
        } else {
            m_aux_label.set_text("");
            m_aux_label.hide();
        }
        update();
    }

    public void set_lookup_table(IBus.LookupTable? table) {
        IBus.Text[] candidates = {};
        uint cursor_in_page = 0;
        bool show_cursor = true;
        IBus.Text[] labels = {};
        IBus.Orientation orientation = IBus.Orientation.SYSTEM;

        if (table != null) {
            uint page_size = table.get_page_size();
            uint ncandidates = table.get_number_of_candidates();
            uint cursor = table.get_cursor_pos();
            cursor_in_page = table.get_cursor_in_page();
            show_cursor = table.is_cursor_visible();

            uint page_start_pos = cursor / page_size * page_size;
            uint page_end_pos = uint.min(page_start_pos + page_size, ncandidates);
            for (uint i = page_start_pos; i < page_end_pos; i++)
                candidates += table.get_candidate(i);

            for (uint i = 0; i < page_size; i++) {
                IBus.Text? label = table.get_label(i);
                if (label != null)
                    labels += label;
            }

            orientation = (IBus.Orientation)table.get_orientation();
        }

        m_candidate_area.set_candidates(candidates, cursor_in_page, show_cursor);
        set_labels(labels);

        if (table != null) {
            // Do not change orientation if table is null to avoid recreate
            // candidates area.
            set_orientation(orientation);
        }

        if (candidates.length != 0)
            m_candidate_area.show_all();
        else
            m_candidate_area.hide();

        update();
    }

    public void set_content_type(uint purpose, uint hints) {
        m_vertical_writing =
                ((hints & IBus.InputHints.VERTICAL_WRITING) != 0);
    }

    private void update() {
        /* Do not call gtk_window_resize() in
         * GtkWidgetClass->get_preferred_width()
         * because the following warning is shown in GTK 3.20:
         * "Allocating size to GtkWindow %x without calling
         * gtk_widget_get_preferred_width/height(). How does the code
         * know the size to allocate?"
         * in gtk_widget_size_allocate_with_baseline() */
        m_toplevel.resize(1, 1);

        if (m_candidate_area.get_visible() ||
            m_preedit_label.get_visible() ||
            m_aux_label.get_visible())
            m_toplevel.show();
        else
            m_toplevel.hide();

        if (m_aux_label.get_visible() &&
            (m_candidate_area.get_visible() || m_preedit_label.get_visible()))
            m_hseparator.show();
        else
            m_hseparator.hide();
    }

    private void create_ui() {
        m_preedit_label = new Gtk.Label(null);
        m_preedit_label.set_size_request(20, -1);
        m_preedit_label.set_halign(Gtk.Align.START);
        m_preedit_label.set_valign(Gtk.Align.CENTER);
        /* Use Gtk.Widget.set_margin_start() since gtk 3.12 */
        m_preedit_label.set_padding(8, 0);
        m_preedit_label.set_no_show_all(true);

        m_aux_label = new Gtk.Label(null);
        m_aux_label.set_size_request(20, -1);
        m_aux_label.set_halign(Gtk.Align.START);
        m_aux_label.set_valign(Gtk.Align.CENTER);
        /* Use Gtk.Widget.set_margin_start() since gtk 3.12 */
        m_aux_label.set_padding(8, 0);
        m_aux_label.set_no_show_all(true);

        m_candidate_area = new CandidateArea(m_vertical_panel_system);
        m_candidate_area.candidate_clicked.connect(
                (w, i, b, s) => candidate_clicked(i, b, s));
        m_candidate_area.page_up.connect((c) => page_up());
        m_candidate_area.page_down.connect((c) => page_down());
        m_candidate_area.cursor_up.connect((c) => cursor_up());
        m_candidate_area.cursor_down.connect((c) => cursor_down());

        m_hseparator = new HSeparator();
        m_hseparator.set_visible(true);

        pack_all_widgets();
    }

    private void pack_all_widgets() {
        m_vbox.pack_start(m_preedit_label, false, false, 4);
        m_vbox.pack_start(m_aux_label, false, false, 4);
        m_vbox.pack_start(m_hseparator, false, false, 0);
        m_vbox.pack_start(m_candidate_area, false, false, 0);
    }

    public new void show() {
        m_toplevel.show_all();
    }

    public new void hide() {
        m_toplevel.hide();
    }

    private void move(int x, int y) {
        m_toplevel.move(x, y);
    }

    private void adjust_window_position() {
        if (!m_vertical_writing)
            adjust_window_position_horizontal();
        else
            adjust_window_position_vertical();
    }

    private Gdk.Rectangle get_monitor_geometry() {
        Gdk.Rectangle monitor_area = { 0, };

        // Use get_monitor_geometry() instead of get_monitor_area().
        // get_monitor_area() excludes docks, but the lookup window should be
        // shown over them.
#if VALA_0_34
        Gdk.Monitor monitor = Gdk.Display.get_default().get_monitor_at_point(
                m_cursor_location.x,
                m_cursor_location.y);
        monitor_area = monitor.get_geometry();
#else
        Gdk.Screen screen = Gdk.Screen.get_default();
        int monitor_num = screen.get_monitor_at_point(m_cursor_location.x,
                                                      m_cursor_location.y);
        screen.get_monitor_geometry(monitor_num, out monitor_area);
#endif
        return monitor_area;
    }

    private void adjust_window_position_horizontal() {
        Gdk.Point cursor_right_bottom = {
                m_cursor_location.x,
                m_cursor_location.y + m_cursor_location.height
        };

        Gtk.Allocation allocation;
        m_toplevel.get_allocation(out allocation);
        Gdk.Point window_right_bottom = {
            cursor_right_bottom.x + allocation.width,
            cursor_right_bottom.y + allocation.height
        };

        Gdk.Rectangle monitor_area = get_monitor_geometry();
        int monitor_right = monitor_area.x + monitor_area.width;
        int monitor_bottom = monitor_area.y + monitor_area.height;

        int x, y;
        if (window_right_bottom.x > monitor_right)
            x = monitor_right - allocation.width;
        else
            x = cursor_right_bottom.x;
        if (x < 0)
            x = 0;

        if (window_right_bottom.y > monitor_bottom)
            y = m_cursor_location.y - allocation.height;
        else
            y = cursor_right_bottom.y;
        if (y < 0)
            y = 0;

        move(x, y);
    }

    private void adjust_window_position_vertical() {
        /* Not sure in which top or left cursor appears
         * in the vertical writing mode.
         * Max (m_cursor_location.width, m_cursor_location.height)
         * can be considered as a char size.
         */
        int char_size = int.max(m_cursor_location.width,
                                m_cursor_location.height);
        Gdk.Point cursor_right_bottom = {
                m_cursor_location.x + char_size,
                m_cursor_location.y + char_size
        };

        Gtk.Allocation allocation;
        m_toplevel.get_allocation(out allocation);
        Gdk.Point hwindow_right_bottom = {
            m_cursor_location.x + allocation.width,
            cursor_right_bottom.y + allocation.height
        };
        Gdk.Point vwindow_left_bottom = {
            m_cursor_location.x - allocation.width,
            m_cursor_location.y + allocation.height
        };

        Gdk.Rectangle monitor_area = get_monitor_geometry();
        int monitor_right = monitor_area.x + monitor_area.width;
        int monitor_bottom = monitor_area.y + monitor_area.height;

        int x, y;
        if (!m_candidate_area.get_vertical()) {
            if (hwindow_right_bottom.x > monitor_right)
                x = monitor_right - allocation.width;
            else
                x = m_cursor_location.x;

            if (hwindow_right_bottom.y > monitor_bottom)
                y = m_cursor_location.y - allocation.height;
            else
                y = cursor_right_bottom.y;
        } else {
            if (vwindow_left_bottom.x > monitor_right)
                x = monitor_right - allocation.width;
            else if (vwindow_left_bottom.x < 0)
                x = cursor_right_bottom.x;
            else
                x = vwindow_left_bottom.x;

            if (vwindow_left_bottom.y > monitor_bottom)
                y = monitor_bottom - allocation.height;
            else
                y = m_cursor_location.y;
        }

        if (x < 0)
            x = 0;
        if (y < 0)
            y = 0;

        move(x, y);
    }
}