gnome-shell/js/ui/search.js

955 lines
28 KiB
JavaScript
Raw Normal View History

// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
import Clutter from 'gi://Clutter';
import Gio from 'gi://Gio';
import GLib from 'gi://GLib';
import GObject from 'gi://GObject';
import Meta from 'gi://Meta';
import Shell from 'gi://Shell';
import St from 'gi://St';
import * as AppDisplay from './appDisplay.js';
import * as IconGrid from './iconGrid.js';
import * as Main from './main.js';
import * as ParentalControlsManager from '../misc/parentalControlsManager.js';
import * as RemoteSearch from './remoteSearch.js';
import {ensureActorVisibleInScrollView} from '../misc/animationUtils.js';
import {Highlighter} from '../misc/util.js';
const SEARCH_PROVIDERS_SCHEMA = 'org.gnome.desktop.search-providers';
const MAX_LIST_SEARCH_RESULTS_ROWS = 5;
const 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);
}
});
export const 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();
}
});
export const 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);
}
});
export const 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;
}
});
export const 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();
search: Don't use max number of results if allocation width is 0 Since commit 1a27ff6130e03e64d9e426ece72a12e4e2a80f50 we use the allocation width of the GridSearchResults actor to calculate the max number of results that can be shown for this search provider. On the first run of the search, when no previous (cached) allocation is available for the actor of GridSearchResults, the allocation width used in `_getMaxDisplayedResults` will be 0, which in turn will make `updateSearch` filter out all results returned by the search provider. Now if this search provider is the only search provider that's enabled, after calling `updateSearch`, the `SearchResults` class will call `_updateSearchProgress` to check if any results are visible, assume nothing was found and therefore hide the scrollView. This in turn causes the GridSearchResults actor to not get a new allocation, which prevents our code to fixup the max number of results on `notify::allocation` from working: The number will continue to be 0 and we'll never show any results. To fix this regression, return -1 in `_getMaxDisplayedResults` if the allocation width is 0 to inform `updateSearch` that we can't calculate the maximum number yet and interpret a return value of -1 as "show all results" in `updateSearch`. The same problem would probably also appear if the allocation width is anything between 0 and the width of the iconGrid with one icon in it, although this might as well be a valid width in case a very small screen is used or with very large icons. So let's only check for a width of 0 and hope the GridSearchResults actor won't get weird temporary allocations in that range. https://gitlab.gnome.org/GNOME/gnome-shell/merge_requests/672
2019-08-07 14:42:17 +00:00
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();
}
}
}
});
export const 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;
}
});
const 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();
}
});
export const 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) {
const laters = global.compositor.get_laters();
laters.remove(this._updateSearchLater);
delete this._updateSearchLater;
}
super._onDestroy();
}
search: Fix calculation of max number of displayed results for grid The calculation of how many results can be shown in GridSearchResults is broken: The width of the parent container (resultsView.actor) we're using as the maximum width right now is the width of the scrollView of SearchResults (which always expands to the whole screen size). This width will only be correct if the scrollView (ie. the whole screen) is smaller than the max width of searchResultsContent, which only is the case for screens smaller than 1000px. To fix the calculation, use the width of our own actor and don't get it using clutter_actor_get_width(), but using the last allocation of the actor. This way we don't get the preferred width if the actor is not allocated at this point (it's hidden by _ensureProviderDisplay() when starting a new search). Then, when the allocation of the actor changes, rebuild the grid search results by calling updateSearch() with the old arguments to ensure the number of visible results is correct. The fact that we're only listening for allocation changes here is the reason why we never want to use the preferred width of the actor inside _getMaxDisplayedResults(): While the actor is hidden clutter_actor_get_width() would return the preferred width, which we'd then use the as the maximum width. But if the actor had a correct allocation before, no notify::allocation signal will be emitted when the actor is shown again because the allocation is still the same, and we'll end up using the preferred width as maximium width forever. https://gitlab.gnome.org/GNOME/gnome-shell/merge_requests/110
2018-11-24 23:57:00 +00:00
updateSearch(...args) {
if (this._notifyAllocationId)
this.disconnect(this._notifyAllocationId);
if (this._updateSearchLater) {
const laters = global.compositor.get_laters();
laters.remove(this._updateSearchLater);
delete this._updateSearchLater;
}
search: Fix calculation of max number of displayed results for grid The calculation of how many results can be shown in GridSearchResults is broken: The width of the parent container (resultsView.actor) we're using as the maximum width right now is the width of the scrollView of SearchResults (which always expands to the whole screen size). This width will only be correct if the scrollView (ie. the whole screen) is smaller than the max width of searchResultsContent, which only is the case for screens smaller than 1000px. To fix the calculation, use the width of our own actor and don't get it using clutter_actor_get_width(), but using the last allocation of the actor. This way we don't get the preferred width if the actor is not allocated at this point (it's hidden by _ensureProviderDisplay() when starting a new search). Then, when the allocation of the actor changes, rebuild the grid search results by calling updateSearch() with the old arguments to ensure the number of visible results is correct. The fact that we're only listening for allocation changes here is the reason why we never want to use the preferred width of the actor inside _getMaxDisplayedResults(): While the actor is hidden clutter_actor_get_width() would return the preferred width, which we'd then use the as the maximum width. But if the actor had a correct allocation before, no notify::allocation signal will be emitted when the actor is shown again because the allocation is still the same, and we'll end up using the preferred width as maximium width forever. https://gitlab.gnome.org/GNOME/gnome-shell/merge_requests/110
2018-11-24 23:57:00 +00:00
// 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;
const laters = global.compositor.get_laters();
this._updateSearchLater = laters.add(Meta.LaterType.BEFORE_REDRAW, () => {
delete this._updateSearchLater;
super.updateSearch(...args);
return GLib.SOURCE_REMOVE;
});
search: Fix calculation of max number of displayed results for grid The calculation of how many results can be shown in GridSearchResults is broken: The width of the parent container (resultsView.actor) we're using as the maximum width right now is the width of the scrollView of SearchResults (which always expands to the whole screen size). This width will only be correct if the scrollView (ie. the whole screen) is smaller than the max width of searchResultsContent, which only is the case for screens smaller than 1000px. To fix the calculation, use the width of our own actor and don't get it using clutter_actor_get_width(), but using the last allocation of the actor. This way we don't get the preferred width if the actor is not allocated at this point (it's hidden by _ensureProviderDisplay() when starting a new search). Then, when the allocation of the actor changes, rebuild the grid search results by calling updateSearch() with the old arguments to ensure the number of visible results is correct. The fact that we're only listening for allocation changes here is the reason why we never want to use the preferred width of the actor inside _getMaxDisplayedResults(): While the actor is hidden clutter_actor_get_width() would return the preferred width, which we'd then use the as the maximum width. But if the actor had a correct allocation before, no notify::allocation signal will be emitted when the actor is shown again because the allocation is still the same, and we'll end up using the preferred width as maximium width forever. https://gitlab.gnome.org/GNOME/gnome-shell/merge_requests/110
2018-11-24 23:57:00 +00:00
});
super.updateSearch(...args);
}
_getMaxDisplayedResults() {
let width = this.allocation.get_width();
if (width === 0)
search: Don't use max number of results if allocation width is 0 Since commit 1a27ff6130e03e64d9e426ece72a12e4e2a80f50 we use the allocation width of the GridSearchResults actor to calculate the max number of results that can be shown for this search provider. On the first run of the search, when no previous (cached) allocation is available for the actor of GridSearchResults, the allocation width used in `_getMaxDisplayedResults` will be 0, which in turn will make `updateSearch` filter out all results returned by the search provider. Now if this search provider is the only search provider that's enabled, after calling `updateSearch`, the `SearchResults` class will call `_updateSearchProgress` to check if any results are visible, assume nothing was found and therefore hide the scrollView. This in turn causes the GridSearchResults actor to not get a new allocation, which prevents our code to fixup the max number of results on `notify::allocation` from working: The number will continue to be 0 and we'll never show any results. To fix this regression, return -1 in `_getMaxDisplayedResults` if the allocation width is 0 to inform `updateSearch` that we can't calculate the maximum number yet and interpret a return value of -1 as "show all results" in `updateSearch`. The same problem would probably also appear if the allocation width is anything between 0 and the width of the iconGrid with one icon in it, although this might as well be a valid width in case a very small screen is used or with very large icons. So let's only check for a width of 0 and hope the GridSearchResults actor won't get weird temporary allocations in that range. https://gitlab.gnome.org/GNOME/gnome-shell/merge_requests/672
2019-08-07 14:42:17 +00:00
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;
}
});
export const 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() {
2014-09-30 06:19:28 +00:00
if (this._searchTimeoutId > 0) {
GLib.source_remove(this._searchTimeoutId);
this._searchTimeoutId = 0;
}
}
2014-09-30 06:19:28 +00:00
_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();
2014-09-30 06:19:28 +00:00
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) {
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;
2014-09-11 23:14:54 +00:00
if (!haveResults) {
if (this.searchInProgress)
this._statusText.set_text(_('Searching…'));
else
this._statusText.set_text(_('No results.'));
2014-09-11 23:14:54 +00:00
}
}
_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');
ensureActorVisibleInScrollView(this._scrollView, result);
} else {
result.remove_style_pseudo_class('selected');
}
}
highlightTerms(description) {
if (!description)
return '';
return this._highlighter.highlight(description);
}
});
const 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;
}
});