955 lines
28 KiB
JavaScript
955 lines
28 KiB
JavaScript
// -*- 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();
|
|
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();
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
// 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;
|
|
});
|
|
});
|
|
|
|
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;
|
|
}
|
|
});
|
|
|
|
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() {
|
|
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) {
|
|
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');
|
|
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;
|
|
}
|
|
});
|