summaryrefslogtreecommitdiffstats
path: root/js/ui/search.js
diff options
context:
space:
mode:
Diffstat (limited to 'js/ui/search.js')
-rw-r--r--js/ui/search.js945
1 files changed, 945 insertions, 0 deletions
diff --git a/js/ui/search.js b/js/ui/search.js
new file mode 100644
index 0000000..1029f31
--- /dev/null
+++ b/js/ui/search.js
@@ -0,0 +1,945 @@
+// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
+/* exported SearchResultsView */
+
+const { Clutter, Gio, GLib, GObject, Meta, Shell, St } = imports.gi;
+
+const AppDisplay = imports.ui.appDisplay;
+const IconGrid = imports.ui.iconGrid;
+const Main = imports.ui.main;
+const ParentalControlsManager = imports.misc.parentalControlsManager;
+const RemoteSearch = imports.ui.remoteSearch;
+const Util = imports.misc.util;
+
+const { Highlighter } = imports.misc.util;
+
+const SEARCH_PROVIDERS_SCHEMA = 'org.gnome.desktop.search-providers';
+
+var MAX_LIST_SEARCH_RESULTS_ROWS = 5;
+
+var MaxWidthBox = GObject.registerClass(
+class MaxWidthBox extends St.BoxLayout {
+ vfunc_allocate(box) {
+ let themeNode = this.get_theme_node();
+ let maxWidth = themeNode.get_max_width();
+ let availWidth = box.x2 - box.x1;
+ let adjustedBox = box;
+
+ if (availWidth > maxWidth) {
+ let excessWidth = availWidth - maxWidth;
+ adjustedBox.x1 += Math.floor(excessWidth / 2);
+ adjustedBox.x2 -= Math.floor(excessWidth / 2);
+ }
+
+ super.vfunc_allocate(adjustedBox);
+ }
+});
+
+var SearchResult = GObject.registerClass(
+class SearchResult extends St.Button {
+ _init(provider, metaInfo, resultsView) {
+ this.provider = provider;
+ this.metaInfo = metaInfo;
+ this._resultsView = resultsView;
+
+ super._init({
+ reactive: true,
+ can_focus: true,
+ track_hover: true,
+ });
+ }
+
+ vfunc_clicked() {
+ this.activate();
+ }
+
+ activate() {
+ this.provider.activateResult(this.metaInfo.id, this._resultsView.terms);
+
+ if (this.metaInfo.clipboardText) {
+ St.Clipboard.get_default().set_text(
+ St.ClipboardType.CLIPBOARD, this.metaInfo.clipboardText);
+ }
+ Main.overview.toggle();
+ }
+});
+
+var ListSearchResult = GObject.registerClass(
+class ListSearchResult extends SearchResult {
+ _init(provider, metaInfo, resultsView) {
+ super._init(provider, metaInfo, resultsView);
+
+ this.style_class = 'list-search-result';
+
+ let content = new St.BoxLayout({
+ style_class: 'list-search-result-content',
+ vertical: false,
+ x_align: Clutter.ActorAlign.START,
+ x_expand: true,
+ y_expand: true,
+ });
+ this.set_child(content);
+
+ let titleBox = new St.BoxLayout({
+ style_class: 'list-search-result-title',
+ y_align: Clutter.ActorAlign.CENTER,
+ });
+
+ content.add_child(titleBox);
+
+ // An icon for, or thumbnail of, content
+ let icon = this.metaInfo['createIcon'](this.ICON_SIZE);
+ if (icon)
+ titleBox.add(icon);
+
+ let title = new St.Label({
+ text: this.metaInfo['name'],
+ y_align: Clutter.ActorAlign.CENTER,
+ });
+ titleBox.add_child(title);
+
+ this.label_actor = title;
+
+ if (this.metaInfo['description']) {
+ this._descriptionLabel = new St.Label({
+ style_class: 'list-search-result-description',
+ y_align: Clutter.ActorAlign.CENTER,
+ });
+ content.add_child(this._descriptionLabel);
+
+ this._resultsView.connectObject(
+ 'terms-changed', this._highlightTerms.bind(this), this);
+
+ this._highlightTerms();
+ }
+ }
+
+ get ICON_SIZE() {
+ return 24;
+ }
+
+ _highlightTerms() {
+ let markup = this._resultsView.highlightTerms(this.metaInfo['description'].split('\n')[0]);
+ this._descriptionLabel.clutter_text.set_markup(markup);
+ }
+});
+
+var GridSearchResult = GObject.registerClass(
+class GridSearchResult extends SearchResult {
+ _init(provider, metaInfo, resultsView) {
+ super._init(provider, metaInfo, resultsView);
+
+ this.style_class = 'grid-search-result';
+
+ this.icon = new IconGrid.BaseIcon(this.metaInfo['name'],
+ { createIcon: this.metaInfo['createIcon'] });
+ let content = new St.Bin({
+ child: this.icon,
+ x_align: Clutter.ActorAlign.START,
+ x_expand: true,
+ y_expand: true,
+ });
+ this.set_child(content);
+ this.label_actor = this.icon.label;
+ }
+});
+
+var SearchResultsBase = GObject.registerClass({
+ GTypeFlags: GObject.TypeFlags.ABSTRACT,
+ Properties: {
+ 'focus-child': GObject.ParamSpec.object(
+ 'focus-child', 'focus-child', 'focus-child',
+ GObject.ParamFlags.READABLE,
+ Clutter.Actor.$gtype),
+ },
+}, class SearchResultsBase extends St.BoxLayout {
+ _init(provider, resultsView) {
+ super._init({ style_class: 'search-section', vertical: true });
+
+ this.provider = provider;
+ this._resultsView = resultsView;
+
+ this._terms = [];
+ this._focusChild = null;
+
+ this._resultDisplayBin = new St.Bin();
+ this.add_child(this._resultDisplayBin);
+
+ let separator = new St.Widget({ style_class: 'search-section-separator' });
+ this.add(separator);
+
+ this._resultDisplays = {};
+
+ this._cancellable = new Gio.Cancellable();
+
+ this.connect('destroy', this._onDestroy.bind(this));
+ }
+
+ _onDestroy() {
+ this._terms = [];
+ }
+
+ _createResultDisplay(meta) {
+ if (this.provider.createResultObject)
+ return this.provider.createResultObject(meta, this._resultsView);
+
+ return null;
+ }
+
+ clear() {
+ this._cancellable.cancel();
+ for (let resultId in this._resultDisplays)
+ this._resultDisplays[resultId].destroy();
+ this._resultDisplays = {};
+ this._clearResultDisplay();
+ this.hide();
+ }
+
+ get focusChild() {
+ return this._focusChild;
+ }
+
+ _keyFocusIn(actor) {
+ if (this._focusChild == actor)
+ return;
+ this._focusChild = actor;
+ this.notify('focus-child');
+ }
+
+ _setMoreCount(_count) {
+ }
+
+ async _ensureResultActors(results) {
+ let metasNeeded = results.filter(
+ resultId => this._resultDisplays[resultId] === undefined);
+
+ if (metasNeeded.length === 0)
+ return;
+
+ this._cancellable.cancel();
+ this._cancellable.reset();
+
+ const metas = await this.provider.getResultMetas(metasNeeded, this._cancellable);
+
+ if (this._cancellable.is_cancelled()) {
+ if (metas.length > 0)
+ throw new Error(`Search provider ${this.provider.id} returned results after the request was canceled`);
+ }
+
+ if (metas.length !== metasNeeded.length) {
+ throw new Error(`Wrong number of result metas returned by search provider ${this.provider.id}: ` +
+ `expected ${metasNeeded.length} but got ${metas.length}`);
+ }
+
+ if (metas.some(meta => !meta.name || !meta.id))
+ throw new Error(`Invalid result meta returned from search provider ${this.provider.id}`);
+
+ metasNeeded.forEach((resultId, i) => {
+ let meta = metas[i];
+ let display = this._createResultDisplay(meta);
+ display.connect('key-focus-in', this._keyFocusIn.bind(this));
+ this._resultDisplays[resultId] = display;
+ });
+ }
+
+ async updateSearch(providerResults, terms, callback) {
+ this._terms = terms;
+ if (providerResults.length == 0) {
+ this._clearResultDisplay();
+ this.hide();
+ callback();
+ } else {
+ let maxResults = this._getMaxDisplayedResults();
+ let results = maxResults > -1
+ ? this.provider.filterResults(providerResults, maxResults)
+ : providerResults;
+ let moreCount = Math.max(providerResults.length - results.length, 0);
+
+ try {
+ await this._ensureResultActors(results);
+
+ // To avoid CSS transitions causing flickering when
+ // the first search result stays the same, we hide the
+ // content while filling in the results.
+ this.hide();
+ this._clearResultDisplay();
+ results.forEach(
+ resultId => this._addItem(this._resultDisplays[resultId]));
+ this._setMoreCount(this.provider.canLaunchSearch ? moreCount : 0);
+ this.show();
+ callback();
+ } catch (e) {
+ this._clearResultDisplay();
+ callback();
+ }
+ }
+ }
+});
+
+var ListSearchResults = GObject.registerClass(
+class ListSearchResults extends SearchResultsBase {
+ _init(provider, resultsView) {
+ super._init(provider, resultsView);
+
+ this._container = new St.BoxLayout({ style_class: 'search-section-content' });
+ this.providerInfo = new ProviderInfo(provider);
+ this.providerInfo.connect('key-focus-in', this._keyFocusIn.bind(this));
+ this.providerInfo.connect('clicked', () => {
+ this.providerInfo.animateLaunch();
+ provider.launchSearch(this._terms);
+ Main.overview.toggle();
+ });
+
+ this._container.add_child(this.providerInfo);
+
+ this._content = new St.BoxLayout({
+ style_class: 'list-search-results',
+ vertical: true,
+ x_expand: true,
+ });
+ this._container.add_child(this._content);
+
+ this._resultDisplayBin.set_child(this._container);
+ }
+
+ _setMoreCount(count) {
+ this.providerInfo.setMoreCount(count);
+ }
+
+ _getMaxDisplayedResults() {
+ return MAX_LIST_SEARCH_RESULTS_ROWS;
+ }
+
+ _clearResultDisplay() {
+ this._content.remove_all_children();
+ }
+
+ _createResultDisplay(meta) {
+ return super._createResultDisplay(meta) ||
+ new ListSearchResult(this.provider, meta, this._resultsView);
+ }
+
+ _addItem(display) {
+ this._content.add_actor(display);
+ }
+
+ getFirstResult() {
+ if (this._content.get_n_children() > 0)
+ return this._content.get_child_at_index(0);
+ else
+ return null;
+ }
+});
+
+var GridSearchResultsLayout = GObject.registerClass({
+ Properties: {
+ 'spacing': GObject.ParamSpec.int('spacing', 'Spacing', 'Spacing',
+ GObject.ParamFlags.READWRITE, 0, GLib.MAXINT32, 0),
+ },
+}, class GridSearchResultsLayout extends Clutter.LayoutManager {
+ _init() {
+ super._init();
+ this._spacing = 0;
+ }
+
+ vfunc_set_container(container) {
+ this._container = container;
+ }
+
+ vfunc_get_preferred_width(container, forHeight) {
+ let minWidth = 0;
+ let natWidth = 0;
+ let first = true;
+
+ for (let child of container) {
+ if (!child.visible)
+ continue;
+
+ const [childMinWidth, childNatWidth] = child.get_preferred_width(forHeight);
+
+ minWidth = Math.max(minWidth, childMinWidth);
+ natWidth += childNatWidth;
+
+ if (first)
+ first = false;
+ else
+ natWidth += this._spacing;
+ }
+
+ return [minWidth, natWidth];
+ }
+
+ vfunc_get_preferred_height(container, forWidth) {
+ let minHeight = 0;
+ let natHeight = 0;
+
+ for (let child of container) {
+ if (!child.visible)
+ continue;
+
+ const [childMinHeight, childNatHeight] = child.get_preferred_height(forWidth);
+
+ minHeight = Math.max(minHeight, childMinHeight);
+ natHeight = Math.max(natHeight, childNatHeight);
+ }
+
+ return [minHeight, natHeight];
+ }
+
+ vfunc_allocate(container, box) {
+ const width = box.get_width();
+
+ const childBox = new Clutter.ActorBox();
+ childBox.x1 = 0;
+ childBox.y1 = 0;
+
+ let first = true;
+ for (let child of container) {
+ if (!child.visible)
+ continue;
+
+ if (first)
+ first = false;
+ else
+ childBox.x1 += this._spacing;
+
+ const [childWidth] = child.get_preferred_width(-1);
+ const [childHeight] = child.get_preferred_height(-1);
+
+ if (childBox.x1 + childWidth <= width)
+ childBox.set_size(childWidth, childHeight);
+ else
+ childBox.set_size(0, 0);
+
+ child.allocate(childBox);
+ child.can_focus = childBox.get_area() > 0;
+
+ childBox.x1 += childWidth;
+ }
+ }
+
+ columnsForWidth(width) {
+ if (!this._container)
+ return -1;
+
+ const [minWidth] = this.get_preferred_width(this._container, -1);
+
+ if (minWidth === 0)
+ return -1;
+
+ let nCols = 0;
+ while (width > minWidth) {
+ width -= minWidth;
+ if (nCols > 0)
+ width -= this._spacing;
+ nCols++;
+ }
+
+ return nCols;
+ }
+
+ get spacing() {
+ return this._spacing;
+ }
+
+ set spacing(v) {
+ if (this._spacing === v)
+ return;
+ this._spacing = v;
+ this.layout_changed();
+ }
+});
+
+var GridSearchResults = GObject.registerClass(
+class GridSearchResults extends SearchResultsBase {
+ _init(provider, resultsView) {
+ super._init(provider, resultsView);
+
+ this._grid = new St.Widget({ style_class: 'grid-search-results' });
+ this._grid.layout_manager = new GridSearchResultsLayout();
+
+ this._grid.connect('style-changed', () => {
+ const node = this._grid.get_theme_node();
+ this._grid.layout_manager.spacing = node.get_length('spacing');
+ });
+
+ this._resultDisplayBin.set_child(new St.Bin({
+ child: this._grid,
+ x_align: Clutter.ActorAlign.CENTER,
+ }));
+ }
+
+ _onDestroy() {
+ if (this._updateSearchLater) {
+ Meta.later_remove(this._updateSearchLater);
+ delete this._updateSearchLater;
+ }
+
+ super._onDestroy();
+ }
+
+ updateSearch(...args) {
+ if (this._notifyAllocationId)
+ this.disconnect(this._notifyAllocationId);
+ if (this._updateSearchLater) {
+ Meta.later_remove(this._updateSearchLater);
+ delete this._updateSearchLater;
+ }
+
+ // Make sure the maximum number of results calculated by
+ // _getMaxDisplayedResults() is updated after width changes.
+ this._notifyAllocationId = this.connect('notify::allocation', () => {
+ if (this._updateSearchLater)
+ return;
+ this._updateSearchLater = Meta.later_add(Meta.LaterType.BEFORE_REDRAW, () => {
+ delete this._updateSearchLater;
+ super.updateSearch(...args);
+ return GLib.SOURCE_REMOVE;
+ });
+ });
+
+ super.updateSearch(...args);
+ }
+
+ _getMaxDisplayedResults() {
+ let width = this.allocation.get_width();
+ if (width == 0)
+ return -1;
+
+ return this._grid.layout_manager.columnsForWidth(width);
+ }
+
+ _clearResultDisplay() {
+ this._grid.remove_all_children();
+ }
+
+ _createResultDisplay(meta) {
+ return super._createResultDisplay(meta) ||
+ new GridSearchResult(this.provider, meta, this._resultsView);
+ }
+
+ _addItem(display) {
+ this._grid.add_child(display);
+ }
+
+ getFirstResult() {
+ for (let child of this._grid) {
+ if (child.visible)
+ return child;
+ }
+ return null;
+ }
+});
+
+var SearchResultsView = GObject.registerClass({
+ Signals: { 'terms-changed': {} },
+}, class SearchResultsView extends St.BoxLayout {
+ _init() {
+ super._init({
+ name: 'searchResults',
+ vertical: true,
+ x_expand: true,
+ y_expand: true,
+ });
+
+ this._parentalControlsManager = ParentalControlsManager.getDefault();
+ this._parentalControlsManager.connect('app-filter-changed', this._reloadRemoteProviders.bind(this));
+
+ this._content = new MaxWidthBox({
+ name: 'searchResultsContent',
+ vertical: true,
+ x_expand: true,
+ });
+
+ this._scrollView = new St.ScrollView({
+ overlay_scrollbars: true,
+ style_class: 'search-display vfade',
+ x_expand: true,
+ y_expand: true,
+ });
+ this._scrollView.set_policy(St.PolicyType.NEVER, St.PolicyType.AUTOMATIC);
+ this._scrollView.add_actor(this._content);
+
+ let action = new Clutter.PanAction({ interpolate: true });
+ action.connect('pan', this._onPan.bind(this));
+ this._scrollView.add_action(action);
+
+ this.add_child(this._scrollView);
+
+ this._statusText = new St.Label({
+ style_class: 'search-statustext',
+ x_align: Clutter.ActorAlign.CENTER,
+ y_align: Clutter.ActorAlign.CENTER,
+ });
+ this._statusBin = new St.Bin({ y_expand: true });
+ this.add_child(this._statusBin);
+ this._statusBin.add_actor(this._statusText);
+
+ this._highlightDefault = false;
+ this._defaultResult = null;
+ this._startingSearch = false;
+
+ this._terms = [];
+ this._results = {};
+
+ this._providers = [];
+
+ this._highlighter = new Highlighter();
+
+ this._searchSettings = new Gio.Settings({ schema_id: SEARCH_PROVIDERS_SCHEMA });
+ this._searchSettings.connect('changed::disabled', this._reloadRemoteProviders.bind(this));
+ this._searchSettings.connect('changed::enabled', this._reloadRemoteProviders.bind(this));
+ this._searchSettings.connect('changed::disable-external', this._reloadRemoteProviders.bind(this));
+ this._searchSettings.connect('changed::sort-order', this._reloadRemoteProviders.bind(this));
+
+ this._searchTimeoutId = 0;
+ this._cancellable = new Gio.Cancellable();
+
+ this._registerProvider(new AppDisplay.AppSearchProvider());
+
+ let appSystem = Shell.AppSystem.get_default();
+ appSystem.connect('installed-changed', this._reloadRemoteProviders.bind(this));
+ this._reloadRemoteProviders();
+ }
+
+ get terms() {
+ return this._terms;
+ }
+
+ _reloadRemoteProviders() {
+ let remoteProviders = this._providers.filter(p => p.isRemoteProvider);
+ remoteProviders.forEach(provider => {
+ this._unregisterProvider(provider);
+ });
+
+ const providers = RemoteSearch.loadRemoteSearchProviders(this._searchSettings);
+ providers.forEach(this._registerProvider.bind(this));
+ }
+
+ _registerProvider(provider) {
+ provider.searchInProgress = false;
+
+ // Filter out unwanted providers.
+ if (provider.appInfo && !this._parentalControlsManager.shouldShowApp(provider.appInfo))
+ return;
+
+ this._providers.push(provider);
+ this._ensureProviderDisplay(provider);
+ }
+
+ _unregisterProvider(provider) {
+ let index = this._providers.indexOf(provider);
+ this._providers.splice(index, 1);
+
+ if (provider.display)
+ provider.display.destroy();
+ }
+
+ _clearSearchTimeout() {
+ if (this._searchTimeoutId > 0) {
+ GLib.source_remove(this._searchTimeoutId);
+ this._searchTimeoutId = 0;
+ }
+ }
+
+ _reset() {
+ this._terms = [];
+ this._results = {};
+ this._clearDisplay();
+ this._clearSearchTimeout();
+ this._defaultResult = null;
+ this._startingSearch = false;
+
+ this._updateSearchProgress();
+ }
+
+ async _doProviderSearch(provider, previousResults) {
+ provider.searchInProgress = true;
+
+ let results;
+ if (this._isSubSearch && previousResults) {
+ results = await provider.getSubsearchResultSet(
+ previousResults,
+ this._terms,
+ this._cancellable);
+ } else {
+ results = await provider.getInitialResultSet(
+ this._terms,
+ this._cancellable);
+ }
+
+ this._results[provider.id] = results;
+ this._updateResults(provider, results);
+ }
+
+ _doSearch() {
+ this._startingSearch = false;
+
+ let previousResults = this._results;
+ this._results = {};
+
+ this._providers.forEach(provider => {
+ let previousProviderResults = previousResults[provider.id];
+ this._doProviderSearch(provider, previousProviderResults);
+ });
+
+ this._updateSearchProgress();
+
+ this._clearSearchTimeout();
+ }
+
+ _onSearchTimeout() {
+ this._searchTimeoutId = 0;
+ this._doSearch();
+ return GLib.SOURCE_REMOVE;
+ }
+
+ setTerms(terms) {
+ // Check for the case of making a duplicate previous search before
+ // setting state of the current search or cancelling the search.
+ // This will prevent incorrect state being as a result of a duplicate
+ // search while the previous search is still active.
+ let searchString = terms.join(' ');
+ let previousSearchString = this._terms.join(' ');
+ if (searchString == previousSearchString)
+ return;
+
+ this._startingSearch = true;
+
+ this._cancellable.cancel();
+ this._cancellable.reset();
+
+ if (terms.length == 0) {
+ this._reset();
+ return;
+ }
+
+ let isSubSearch = false;
+ if (this._terms.length > 0)
+ isSubSearch = searchString.indexOf(previousSearchString) == 0;
+
+ this._terms = terms;
+ this._isSubSearch = isSubSearch;
+ this._updateSearchProgress();
+
+ if (this._searchTimeoutId == 0)
+ this._searchTimeoutId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 150, this._onSearchTimeout.bind(this));
+
+ this._highlighter = new Highlighter(this._terms);
+
+ this.emit('terms-changed');
+ }
+
+ _onPan(action) {
+ let [dist_, dx_, dy] = action.get_motion_delta(0);
+ let adjustment = this._scrollView.vscroll.adjustment;
+ adjustment.value -= (dy / this.height) * adjustment.page_size;
+ return false;
+ }
+
+ _focusChildChanged(provider) {
+ Util.ensureActorVisibleInScrollView(this._scrollView, provider.focusChild);
+ }
+
+ _ensureProviderDisplay(provider) {
+ if (provider.display)
+ return;
+
+ let providerDisplay;
+ if (provider.appInfo)
+ providerDisplay = new ListSearchResults(provider, this);
+ else
+ providerDisplay = new GridSearchResults(provider, this);
+
+ providerDisplay.connect('notify::focus-child', this._focusChildChanged.bind(this));
+ providerDisplay.hide();
+ this._content.add(providerDisplay);
+ provider.display = providerDisplay;
+ }
+
+ _clearDisplay() {
+ this._providers.forEach(provider => {
+ provider.display.clear();
+ });
+ }
+
+ _maybeSetInitialSelection() {
+ let newDefaultResult = null;
+
+ let providers = this._providers;
+ for (let i = 0; i < providers.length; i++) {
+ let provider = providers[i];
+ let display = provider.display;
+
+ if (!display.visible)
+ continue;
+
+ let firstResult = display.getFirstResult();
+ if (firstResult) {
+ newDefaultResult = firstResult;
+ break; // select this one!
+ }
+ }
+
+ if (newDefaultResult != this._defaultResult) {
+ this._setSelected(this._defaultResult, false);
+ this._setSelected(newDefaultResult, this._highlightDefault);
+
+ this._defaultResult = newDefaultResult;
+ }
+ }
+
+ get searchInProgress() {
+ if (this._startingSearch)
+ return true;
+
+ return this._providers.some(p => p.searchInProgress);
+ }
+
+ _updateSearchProgress() {
+ let haveResults = this._providers.some(provider => {
+ let display = provider.display;
+ return display.getFirstResult() != null;
+ });
+
+ this._scrollView.visible = haveResults;
+ this._statusBin.visible = !haveResults;
+
+ if (!haveResults) {
+ if (this.searchInProgress)
+ this._statusText.set_text(_("Searching…"));
+ else
+ this._statusText.set_text(_("No results."));
+ }
+ }
+
+ _updateResults(provider, results) {
+ let terms = this._terms;
+ let display = provider.display;
+
+ display.updateSearch(results, terms, () => {
+ provider.searchInProgress = false;
+
+ this._maybeSetInitialSelection();
+ this._updateSearchProgress();
+ });
+ }
+
+ activateDefault() {
+ // If we have a search queued up, force the search now.
+ if (this._searchTimeoutId > 0)
+ this._doSearch();
+
+ if (this._defaultResult)
+ this._defaultResult.activate();
+ }
+
+ highlightDefault(highlight) {
+ this._highlightDefault = highlight;
+ this._setSelected(this._defaultResult, highlight);
+ }
+
+ popupMenuDefault() {
+ // If we have a search queued up, force the search now.
+ if (this._searchTimeoutId > 0)
+ this._doSearch();
+
+ if (this._defaultResult)
+ this._defaultResult.popup_menu();
+ }
+
+ navigateFocus(direction) {
+ let rtl = this.get_text_direction() == Clutter.TextDirection.RTL;
+ if (direction == St.DirectionType.TAB_BACKWARD ||
+ direction == (rtl
+ ? St.DirectionType.RIGHT
+ : St.DirectionType.LEFT) ||
+ direction == St.DirectionType.UP) {
+ this.navigate_focus(null, direction, false);
+ return;
+ }
+
+ const from = this._defaultResult ?? null;
+ this.navigate_focus(from, direction, false);
+ }
+
+ _setSelected(result, selected) {
+ if (!result)
+ return;
+
+ if (selected) {
+ result.add_style_pseudo_class('selected');
+ Util.ensureActorVisibleInScrollView(this._scrollView, result);
+ } else {
+ result.remove_style_pseudo_class('selected');
+ }
+ }
+
+ highlightTerms(description) {
+ if (!description)
+ return '';
+
+ return this._highlighter.highlight(description);
+ }
+});
+
+var ProviderInfo = GObject.registerClass(
+class ProviderInfo extends St.Button {
+ _init(provider) {
+ this.provider = provider;
+ super._init({
+ style_class: 'search-provider-icon',
+ reactive: true,
+ can_focus: true,
+ accessible_name: provider.appInfo.get_name(),
+ track_hover: true,
+ y_align: Clutter.ActorAlign.START,
+ });
+
+ this._content = new St.BoxLayout({
+ vertical: false,
+ style_class: 'list-search-provider-content',
+ });
+ this.set_child(this._content);
+
+ const icon = new St.Icon({
+ icon_size: this.PROVIDER_ICON_SIZE,
+ gicon: provider.appInfo.get_icon(),
+ });
+
+ const detailsBox = new St.BoxLayout({
+ style_class: 'list-search-provider-details',
+ vertical: true,
+ x_expand: true,
+ });
+
+ const nameLabel = new St.Label({
+ text: provider.appInfo.get_name(),
+ x_align: Clutter.ActorAlign.START,
+ });
+
+ this._moreLabel = new St.Label({ x_align: Clutter.ActorAlign.START });
+
+ detailsBox.add_actor(nameLabel);
+ detailsBox.add_actor(this._moreLabel);
+
+
+ this._content.add_actor(icon);
+ this._content.add_actor(detailsBox);
+ }
+
+ get PROVIDER_ICON_SIZE() {
+ return 32;
+ }
+
+ animateLaunch() {
+ let appSys = Shell.AppSystem.get_default();
+ let app = appSys.lookup_app(this.provider.appInfo.get_id());
+ if (app.state == Shell.AppState.STOPPED)
+ IconGrid.zoomOutActor(this._content);
+ }
+
+ setMoreCount(count) {
+ this._moreLabel.text = ngettext("%d more", "%d more", count).format(count);
+ this._moreLabel.visible = count > 0;
+ }
+});