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. * */ 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={}) { 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(); 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; }, });