summaryrefslogtreecommitdiff
path: root/overrides/endless_private/search_box.js
blob: d02462fe55cd3ca9ae3d6e48ea98ece65e91c4f4 (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
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
const Gdk = imports.gi.Gdk;
const GObject = imports.gi.GObject;
const Gtk = imports.gi.Gtk;
const Lang = imports.lang;
const Pango = imports.gi.Pango;

const BOX_WIDTH_CHARS = 25;
const CELL_PADDING_X = 8;
const CELL_PADDING_Y = 6;
const SCREEN_RES_WIDTH_TINY = 720;
const SCREEN_RES_WIDTH_SMALL = 800;
const SCREEN_RES_WIDTH_MEDIUM = 1024;
const SCREEN_RES_WIDTH_LARGE = 1366;
const SCREEN_RES_WIDTH_XL = 1920;
const TITLE_MAX_CHARS_TINY = 36;
const TITLE_MAX_CHARS_SMALL = 60;
const TITLE_MAX_CHARS_MEDIUM = 80;
const TITLE_MAX_CHARS_LARGE = 100;
const TITLE_MAX_CHARS_XL = 120;
const TITLE_MAX_CHARS_DEFAULT = 255;

/**
 * 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.
 *
 */
var 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={}) {
        if (['width_chars', 'width-chars', 'widthChars'].every(name =>
            typeof props[name] === 'undefined')) {
            props.width_chars = BOX_WIDTH_CHARS;
        }
        this.parent(props);

        this.primary_icon_name = 'edit-find-symbolic';

        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;
        cells[0].width_chars = this._get_title_max_chars();
        cells[0].ellipsize = Pango.EllipsizeMode.END;

        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()[0];
        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');
    },

    /*
     * This assumes that the search_box widget is centered in the topbar.
     * Aligning the topbar to any other position may cause this method to not
     * function as expected!
     *
     * The constants used here correspond to the responsive system breakpoints
     * defined by the Design team.
     */
    _get_title_max_chars: function () {
        let screen_width = Gdk.Screen.get_default().get_width();
        let title_max_chars = TITLE_MAX_CHARS_DEFAULT;

        if (screen_width <= SCREEN_RES_WIDTH_TINY) {
            title_max_chars = TITLE_MAX_CHARS_TINY;
        } else if (screen_width <= SCREEN_RES_WIDTH_SMALL) {
            title_max_chars = TITLE_MAX_CHARS_SMALL;
        } else if (screen_width <= SCREEN_RES_WIDTH_MEDIUM) {
            title_max_chars = TITLE_MAX_CHARS_MEDIUM;
        } else if (screen_width <= SCREEN_RES_WIDTH_LARGE) {
            title_max_chars = TITLE_MAX_CHARS_LARGE;
        } else if (screen_width <= SCREEN_RES_WIDTH_XL) {
            title_max_chars = TITLE_MAX_CHARS_XL;
        }

        return title_max_chars;
    },
});