summaryrefslogtreecommitdiff
path: root/overrides/endless_private/search_box.js
blob: f076962e1ad20d71bbbe1e80a40f5fe594cc8bad (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
const Gdk = imports.gi.Gdk;
const GObject = imports.gi.GObject;
const Gtk = imports.gi.Gtk;
const Lang = imports.lang;

const BOX_WIDTH_CHARS = 25;
const CELL_PADDING_X = 8;
const CELL_PADDING_Y = 6;

/**
 * Class: SearchBox
 *
 * This is a Search Box with autocompletion functionality.
 * The primary icon is a magnifying glass and the cursor turns into a hand when
 * hovering over the icon.
 *
 * NOTE: Due to a limitation in GTK, the cursor change will not work if the
 * search box's alignment is set to Gtk.Align.FILL in either direction.
 *
 */
const SearchBox = new Lang.Class({
    Name: 'SearchBox',
    GTypeName: 'EosSearchBox',
    Extends: Gtk.Entry,
    Signals: {
        /**
         * Event: menu-item-selected
         *
         * This event is triggered when an item is selected from the autocomplete menu.
         */
        'menu-item-selected': {
            param_types: [GObject.TYPE_STRING]
        },
        /**
         * Event: text-changed
         *
         * This event is triggered when the text in the search entry is changed by the user.
         */
        'text-changed': {
            param_types: [GObject.TYPE_STRING]
        }
    },

    _init: function(props) {
        this.parent(props);

        this.primary_icon_name = 'edit-find-symbolic';
        this.set_width_chars(BOX_WIDTH_CHARS);

        this._auto_complete = new Gtk.EntryCompletion();

        this._list_store = new Gtk.ListStore();
        this._list_store.set_column_types([GObject.TYPE_STRING]);

        this._auto_complete.set_model(this._list_store);
        this._auto_complete.set_text_column(0);

        let cells = this._auto_complete.get_cells();
        cells[0].xpad = CELL_PADDING_X;
        cells[0].ypad = CELL_PADDING_Y;

        this._auto_complete.set_match_func(function () { return true; });
        this.completion = this._auto_complete;

        this.connect('icon-press', Lang.bind(this, function () {
            this.emit('activate');
        }));
        this.completion.connect('match-selected', this._onMatchSelected.bind(this));
        this.connect('changed', Lang.bind(this, function () {
            if (!this._entry_changed_by_widget) {
                // If there is entry text, need to add the 'go' icon
                this.secondary_icon_name = (this.text.length > 0)? 'go-next-symbolic' : null;
                this.emit('text-changed', this.text);
            }
            this._entry_changed_by_widget = false;
        }));
        this.connect('enter-notify-event', this._on_motion.bind(this));
        this.connect('motion-notify-event', this._on_motion.bind(this));
        this.connect('leave-notify-event', this._on_leave.bind(this));

        this.get_style_context().add_class('endless-search-box');
    },

    // Returns true if x, y is on icon at icon_pos.
    // Throws a string error starting with 'STOP' if the search box was not
    // realized or not added to a toplevel window.
    _cursor_is_on_icon: function (x, y, icon_pos) {
        let rect = this.get_icon_area(icon_pos);
        let top = this.get_toplevel();
        if (!top.is_toplevel())
            throw 'STOP: Search box is not contained in a toplevel.';
        let [realized, icon_x, icon_y] = this.translate_coordinates(top,
            rect.x, rect.y);
        if (!realized)
            throw 'STOP: Search box is not realized.';

        return (x >= icon_x && x <= icon_x + rect.width &&
            y >= icon_y && y <= icon_y + rect.height);
    },

    _on_motion: function (widget, event) {
        let [has_coords, x, y] = event.get_root_coords();
        if (!has_coords)
            return;
        let should_show_cursor;
        try {
            let on_primary = this._cursor_is_on_icon(x, y,
                Gtk.EntryIconPosition.PRIMARY);
            let has_secondary = this.secondary_icon_name !== null;
            let on_secondary = has_secondary && this._cursor_is_on_icon(x, y,
                Gtk.EntryIconPosition.SECONDARY);
            should_show_cursor = on_primary || on_secondary;
        } catch (e) {
            if (typeof e === 'string' && e.startsWith('STOP'))
                return;
            throw e;
        }

        if (should_show_cursor) {
            if (this._has_hand_cursor)
                return;
            let cursor = Gdk.Cursor.new_for_display(Gdk.Display.get_default(),
                Gdk.CursorType.HAND1);
            this.window.set_cursor(cursor);
            this._has_hand_cursor = true;
        } else {
            this._on_leave(widget);
        }
    },

    _on_leave: function (widget) {
        if (!this._has_hand_cursor)
            return;
        this.window.set_cursor(null);
        this._has_hand_cursor = false;
    },

    _onMatchSelected: function (widget, model, iter) {
        let index = model.get_path(iter).get_indices();
        this.emit('menu-item-selected', this._items[index]['id']);
        return Gdk.EVENT_STOP;
    },

    /* Set the entry text without triggering the text-changed signal.
    */
    set_text_programmatically: function (text) {
        if (this.text === text)
            return;
        this._entry_changed_by_widget = true;
        this.text = text;
        this.set_position(-1);
    },

    /* Set the menu items by providing an array of item objects:
        [
            {
                'title': 'Frango',
                'id': 'http://www.myfrango.com'
            }
        ]

        'title' must be a string but 'id' can be any type and is used to
        identify the data that was selected.
    */
    set_menu_items: function (items) {
        this._items = items;
        let model = this._auto_complete.get_model();
        model.clear();
        for (let i = 0; i < this._items.length; i++) {
            model.set(model.append(), [0], [this._items[i]['title']]);
        }
        this._entry_changed_by_widget = true;
        this.emit('changed');
    }
});