search: Make asynchronous providers more explicit
Currently, asynchronous search providers are expected to call startAsync() in getInitialResultSet()/getSubsearchResultSet(), which will trigger async mode until the search is canceled or updated. Switching between synchronous and asynchronous mode like this makes asynchronous search an implementation detail, but being transparent to the searchDisplay means that certain optimizations don't work as expected. Namely, updating asynchronous search results causes flickering, and the automatic selection never focuses asynchronous results. So change the API to require providers being either synchronous (with the current getInitialResultSet()/getSubsearchResultSet() methods) or asynchronous (with asynchronous variants), and handle asynchronous providers explicitly in searchDisplay. https://bugzilla.gnome.org/show_bug.cgi?id=663125
This commit is contained in:
parent
eb0d803617
commit
e2c66ce48a
109
js/ui/search.js
109
js/ui/search.js
@ -102,6 +102,11 @@ const SearchResultDisplay = new Lang.Class({
|
|||||||
* Subclass this object to add a new result type
|
* Subclass this object to add a new result type
|
||||||
* to the search system, then call registerProvider()
|
* to the search system, then call registerProvider()
|
||||||
* in SearchSystem with an instance.
|
* in SearchSystem with an instance.
|
||||||
|
* By default, search is synchronous and uses the
|
||||||
|
* getInitialResultSet()/getSubsearchResultSet() methods.
|
||||||
|
* For asynchronous search, set the async property to true
|
||||||
|
* and implement getInitialResultSetAsync()/getSubsearchResultSetAsync()
|
||||||
|
* instead.
|
||||||
*/
|
*/
|
||||||
const SearchProvider = new Lang.Class({
|
const SearchProvider = new Lang.Class({
|
||||||
Name: 'SearchProvider',
|
Name: 'SearchProvider',
|
||||||
@ -109,42 +114,7 @@ const SearchProvider = new Lang.Class({
|
|||||||
_init: function(title) {
|
_init: function(title) {
|
||||||
this.title = title;
|
this.title = title;
|
||||||
this.searchSystem = null;
|
this.searchSystem = null;
|
||||||
this.searchAsync = false;
|
this.async = false;
|
||||||
},
|
|
||||||
|
|
||||||
_asyncCancelled: function() {
|
|
||||||
},
|
|
||||||
|
|
||||||
startAsync: function() {
|
|
||||||
this.searchAsync = true;
|
|
||||||
},
|
|
||||||
|
|
||||||
tryCancelAsync: function() {
|
|
||||||
if (!this.searchAsync)
|
|
||||||
return;
|
|
||||||
this._asyncCancelled();
|
|
||||||
this.searchAsync = false;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* addItems:
|
|
||||||
* @items: an array of result identifier strings representing
|
|
||||||
* items which match the last given search terms.
|
|
||||||
*
|
|
||||||
* This should be used for something that requires a bit more
|
|
||||||
* logic; it's designed to be an asyncronous way to add a result
|
|
||||||
* to the current search.
|
|
||||||
*/
|
|
||||||
addItems: function(items) {
|
|
||||||
if (!this.searchSystem)
|
|
||||||
throw new Error('Search provider not registered');
|
|
||||||
|
|
||||||
if (!items.length)
|
|
||||||
return;
|
|
||||||
|
|
||||||
this.tryCancelAsync();
|
|
||||||
|
|
||||||
this.searchSystem.addProviderItems(this, items);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -172,6 +142,18 @@ const SearchProvider = new Lang.Class({
|
|||||||
throw new Error('Not implemented');
|
throw new Error('Not implemented');
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* getInitialResultSetAsync:
|
||||||
|
* @terms: Array of search terms, treated as logical AND
|
||||||
|
*
|
||||||
|
* Like getInitialResultSet(), but the method should return immediately
|
||||||
|
* without a return value - use SearchSystem.pushResults() when the
|
||||||
|
* corresponding results are ready.
|
||||||
|
*/
|
||||||
|
getInitialResultSetAsync: function(terms) {
|
||||||
|
throw new Error('Not implemented');
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* getSubsearchResultSet:
|
* getSubsearchResultSet:
|
||||||
* @previousResults: Array of item identifiers
|
* @previousResults: Array of item identifiers
|
||||||
@ -189,6 +171,19 @@ const SearchProvider = new Lang.Class({
|
|||||||
throw new Error('Not implemented');
|
throw new Error('Not implemented');
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* getSubsearchResultSetAsync:
|
||||||
|
* @previousResults: Array of item identifiers
|
||||||
|
* @newTerms: Updated search terms
|
||||||
|
*
|
||||||
|
* Like getSubsearchResultSet(), but the method should return immediately
|
||||||
|
* without a return value - use SearchSystem.pushResults() when the
|
||||||
|
* corresponding results are ready.
|
||||||
|
*/
|
||||||
|
getSubsearchResultSetAsync: function(previousResults, newTerms) {
|
||||||
|
throw new Error('Not implemented');
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* getResultMetas:
|
* getResultMetas:
|
||||||
* @ids: Result identifier strings
|
* @ids: Result identifier strings
|
||||||
@ -201,6 +196,19 @@ const SearchProvider = new Lang.Class({
|
|||||||
throw new Error('Not implemented');
|
throw new Error('Not implemented');
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* getResultMetasAsync:
|
||||||
|
* @ids: Result identifier strings
|
||||||
|
* @callback: callback to pass the results to when ready
|
||||||
|
*
|
||||||
|
* Like getResultMetas(), but the method should return immediately
|
||||||
|
* without a return value - pass the results to the provided @callback
|
||||||
|
* when ready.
|
||||||
|
*/
|
||||||
|
getResultMetasAsync: function(ids, callback) {
|
||||||
|
throw new Error('Not implemented');
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* createResultContainer:
|
* createResultContainer:
|
||||||
*
|
*
|
||||||
@ -369,8 +377,13 @@ const SearchSystem = new Lang.Class({
|
|||||||
this._previousResults = [];
|
this._previousResults = [];
|
||||||
},
|
},
|
||||||
|
|
||||||
addProviderItems: function(provider, items) {
|
pushResults: function(provider, results) {
|
||||||
this.emit('search-updated', provider, items);
|
let i = this._providers.indexOf(provider);
|
||||||
|
if (i == -1)
|
||||||
|
return;
|
||||||
|
|
||||||
|
this._previousResults[i] = [provider, results];
|
||||||
|
this.emit('search-updated', this._previousResults[i]);
|
||||||
},
|
},
|
||||||
|
|
||||||
updateSearch: function(searchString) {
|
updateSearch: function(searchString) {
|
||||||
@ -400,10 +413,14 @@ const SearchSystem = new Lang.Class({
|
|||||||
if (isSubSearch) {
|
if (isSubSearch) {
|
||||||
for (let i = 0; i < this._providers.length; i++) {
|
for (let i = 0; i < this._providers.length; i++) {
|
||||||
let [provider, previousResults] = this._previousResults[i];
|
let [provider, previousResults] = this._previousResults[i];
|
||||||
provider.tryCancelAsync();
|
|
||||||
try {
|
try {
|
||||||
let providerResults = provider.getSubsearchResultSet(previousResults, terms);
|
if (provider.async) {
|
||||||
results.push([provider, providerResults]);
|
provider.getSubsearchResultSetAsync(previousResults, terms);
|
||||||
|
results.push([provider, []]);
|
||||||
|
} else {
|
||||||
|
let providerResults = provider.getSubsearchResultSet(previousResults, terms);
|
||||||
|
results.push([provider, providerResults]);
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
global.log ('A ' + error.name + ' has occured in ' + provider.title + ': ' + error.message);
|
global.log ('A ' + error.name + ' has occured in ' + provider.title + ': ' + error.message);
|
||||||
}
|
}
|
||||||
@ -411,10 +428,14 @@ const SearchSystem = new Lang.Class({
|
|||||||
} else {
|
} else {
|
||||||
for (let i = 0; i < this._providers.length; i++) {
|
for (let i = 0; i < this._providers.length; i++) {
|
||||||
let provider = this._providers[i];
|
let provider = this._providers[i];
|
||||||
provider.tryCancelAsync();
|
|
||||||
try {
|
try {
|
||||||
let providerResults = provider.getInitialResultSet(terms);
|
if (provider.async) {
|
||||||
results.push([provider, providerResults]);
|
provider.getInitialResultSetAsync(terms);
|
||||||
|
results.push([provider, []]);
|
||||||
|
} else {
|
||||||
|
let providerResults = provider.getInitialResultSet(terms);
|
||||||
|
results.push([provider, providerResults]);
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
global.log ('A ' + error.name + ' has occured in ' + provider.title + ': ' + error.message);
|
global.log ('A ' + error.name + ' has occured in ' + provider.title + ': ' + error.message);
|
||||||
}
|
}
|
||||||
|
@ -119,17 +119,24 @@ const GridSearchResults = new Lang.Class({
|
|||||||
if (results.length == 0)
|
if (results.length == 0)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
let metas = provider.getResultMetas(results);
|
if (provider.async) {
|
||||||
this.renderResults(metas);
|
provider.getResultMetasAsync(results,
|
||||||
|
Lang.bind(this, this.renderResults));
|
||||||
|
} else {
|
||||||
|
let metas = provider.getResultMetas(results);
|
||||||
|
this.renderResults(metas);
|
||||||
|
}
|
||||||
}));
|
}));
|
||||||
}));
|
}));
|
||||||
this._notDisplayedResult = [];
|
this._notDisplayedResult = [];
|
||||||
this._terms = [];
|
this._terms = [];
|
||||||
|
this._pendingClear = false;
|
||||||
},
|
},
|
||||||
|
|
||||||
getResultsForDisplay: function() {
|
getResultsForDisplay: function() {
|
||||||
|
let alreadyVisible = this._pendingClear ? 0 : this._grid.visibleItemsCount();
|
||||||
let canDisplay = this._grid.childrenInRow(this._width) * MAX_SEARCH_RESULTS_ROWS
|
let canDisplay = this._grid.childrenInRow(this._width) * MAX_SEARCH_RESULTS_ROWS
|
||||||
- this._grid.visibleItemsCount();
|
- alreadyVisible;
|
||||||
|
|
||||||
let numResults = Math.min(this._notDisplayedResult.length, canDisplay);
|
let numResults = Math.min(this._notDisplayedResult.length, canDisplay);
|
||||||
|
|
||||||
@ -144,6 +151,7 @@ const GridSearchResults = new Lang.Class({
|
|||||||
// copy the lists
|
// copy the lists
|
||||||
this._notDisplayedResult = results.slice(0);
|
this._notDisplayedResult = results.slice(0);
|
||||||
this._terms = terms.slice(0);
|
this._terms = terms.slice(0);
|
||||||
|
this._pendingClear = true;
|
||||||
},
|
},
|
||||||
|
|
||||||
renderResults: function(metas) {
|
renderResults: function(metas) {
|
||||||
@ -154,10 +162,9 @@ const GridSearchResults = new Lang.Class({
|
|||||||
},
|
},
|
||||||
|
|
||||||
clear: function () {
|
clear: function () {
|
||||||
this._terms = [];
|
|
||||||
this._notDisplayedResult = [];
|
|
||||||
this._grid.removeAll();
|
this._grid.removeAll();
|
||||||
this.selectionIndex = -1;
|
this.selectionIndex = -1;
|
||||||
|
this._pendingClear = false;
|
||||||
},
|
},
|
||||||
|
|
||||||
selectIndex: function (index) {
|
selectIndex: function (index) {
|
||||||
@ -297,7 +304,8 @@ const SearchResults = new Lang.Class({
|
|||||||
|
|
||||||
this._providerMeta.push({ provider: provider,
|
this._providerMeta.push({ provider: provider,
|
||||||
actor: providerBox,
|
actor: providerBox,
|
||||||
resultDisplay: resultDisplay });
|
resultDisplay: resultDisplay,
|
||||||
|
hasPendingResults: false });
|
||||||
this._content.add(providerBox);
|
this._content.add(providerBox);
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -322,8 +330,8 @@ const SearchResults = new Lang.Class({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
_clearDisplayForProvider: function(index) {
|
_clearDisplayForProvider: function(provider) {
|
||||||
let meta = this._providerMeta[index];
|
let meta = this._metaForProvider(provider);
|
||||||
meta.resultDisplay.clear();
|
meta.resultDisplay.clear();
|
||||||
meta.actor.hide();
|
meta.actor.hide();
|
||||||
},
|
},
|
||||||
@ -350,15 +358,58 @@ const SearchResults = new Lang.Class({
|
|||||||
return this._providerMeta[this._providers.indexOf(provider)];
|
return this._providerMeta[this._providers.indexOf(provider)];
|
||||||
},
|
},
|
||||||
|
|
||||||
_updateCurrentResults: function(searchSystem, provider, results) {
|
_maybeSetInitialSelection: function() {
|
||||||
|
if (this._selectedOpenSearchButton > -1 || this._selectedProvider > -1)
|
||||||
|
return;
|
||||||
|
|
||||||
|
for (let i = 0; i < this._providerMeta.length; i++) {
|
||||||
|
let meta = this._providerMeta[i];
|
||||||
|
if (meta.hasPendingResults)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (meta.actor.visible)
|
||||||
|
break; // select this one!
|
||||||
|
}
|
||||||
|
|
||||||
|
this.selectDown(false);
|
||||||
|
this._initialSelectionSet = true;
|
||||||
|
},
|
||||||
|
|
||||||
|
_updateCurrentResults: function(searchSystem, results) {
|
||||||
let terms = searchSystem.getTerms();
|
let terms = searchSystem.getTerms();
|
||||||
|
let [provider, providerResults] = results;
|
||||||
let meta = this._metaForProvider(provider);
|
let meta = this._metaForProvider(provider);
|
||||||
meta.resultDisplay.clear();
|
meta.hasPendingResults = false;
|
||||||
meta.actor.show();
|
this._updateProviderResults(provider, providerResults, terms);
|
||||||
meta.resultDisplay.setResults(providerResults, terms);
|
},
|
||||||
let displayResults = meta.resultDisplay.getResultsForDisplay();
|
|
||||||
meta.resultDisplay.renderResults(provider.getResultMetas(displayResults));
|
_updateProviderResults: function(provider, providerResults, terms) {
|
||||||
return true;
|
let meta = this._metaForProvider(provider);
|
||||||
|
if (providerResults.length == 0) {
|
||||||
|
this._clearDisplayForProvider(provider);
|
||||||
|
meta.resultDisplay.setResults([], []);
|
||||||
|
} else {
|
||||||
|
this._providerMetaResults[provider.title] = providerResults;
|
||||||
|
meta.resultDisplay.setResults(providerResults, terms);
|
||||||
|
let results = meta.resultDisplay.getResultsForDisplay();
|
||||||
|
|
||||||
|
if (provider.async) {
|
||||||
|
provider.getResultMetasAsync(results, Lang.bind(this,
|
||||||
|
function(metas) {
|
||||||
|
this._clearDisplayForProvider(provider);
|
||||||
|
meta.actor.show();
|
||||||
|
this._content.hide();
|
||||||
|
meta.resultDisplay.renderResults(metas);
|
||||||
|
this._maybeSetInitialSelection();
|
||||||
|
this._content.show();
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
let metas = provider.getResultMetas(results);
|
||||||
|
this._clearDisplayForProvider(provider);
|
||||||
|
meta.actor.show();
|
||||||
|
meta.resultDisplay.renderResults(metas);
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
_updateResults: function(searchSystem, results) {
|
_updateResults: function(searchSystem, results) {
|
||||||
@ -368,6 +419,7 @@ const SearchResults = new Lang.Class({
|
|||||||
} else {
|
} else {
|
||||||
this._selectedOpenSearchButton = -1;
|
this._selectedOpenSearchButton = -1;
|
||||||
this._updateOpenSearchButtonState();
|
this._updateOpenSearchButtonState();
|
||||||
|
this._selectedProvider = -1;
|
||||||
this._statusText.hide();
|
this._statusText.hide();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -383,22 +435,13 @@ const SearchResults = new Lang.Class({
|
|||||||
|
|
||||||
for (let i = 0; i < results.length; i++) {
|
for (let i = 0; i < results.length; i++) {
|
||||||
let [provider, providerResults] = results[i];
|
let [provider, providerResults] = results[i];
|
||||||
if (providerResults.length == 0) {
|
let meta = this._metaForProvider(provider);
|
||||||
this._clearDisplayForProvider(i);
|
meta.hasPendingResults = provider.async;
|
||||||
} else {
|
if (!meta.hasPendingResults)
|
||||||
this._providerMetaResults[provider.title] = providerResults;
|
this._updateProviderResults(provider, providerResults, terms);
|
||||||
this._clearDisplayForProvider(i);
|
|
||||||
let meta = this._metaForProvider(provider);
|
|
||||||
meta.actor.show();
|
|
||||||
meta.resultDisplay.setResults(providerResults, terms);
|
|
||||||
let displayResults = meta.resultDisplay.getResultsForDisplay();
|
|
||||||
meta.resultDisplay.renderResults(provider.getResultMetas(displayResults));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this._selectedOpenSearchButton == -1)
|
this._maybeSetInitialSelection();
|
||||||
this.selectDown(false);
|
|
||||||
|
|
||||||
this._content.show();
|
this._content.show();
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
Loading…
Reference in New Issue
Block a user