[gnome-shell] Add search.js, rebase search system on top
- From: Colin Walters <walters src gnome org>
- To: svn-commits-list gnome org
- Cc:
- Subject: [gnome-shell] Add search.js, rebase search system on top
- Date: Fri, 18 Dec 2009 15:06:10 +0000 (UTC)
commit b7646d18ae34a38dd3103421cfbff053e907cfa1
Author: Colin Walters <walters verbum org>
Date: Sun Nov 29 17:45:30 2009 -0500
Add search.js, rebase search system on top
The high level goal is to separate the concern of searching for
things with display of those things; for example in newer mockups,
applications are displayed exactly the same as they look in the
AppWell.
Another goal was optimizing for speed; for example,
application search was pushed mostly down into C, and we avoid
lowercasing and normalizing every item over and over.
https://bugzilla.gnome.org/show_bug.cgi?id=603523
data/theme/gnome-shell.css | 34 ++-
js/misc/docInfo.js | 57 +++++-
js/ui/appDisplay.js | 92 +++++++-
js/ui/dash.js | 521 ++++++++++++++++++++++++--------------------
js/ui/docDisplay.js | 50 ++++-
js/ui/overview.js | 1 +
js/ui/placeDisplay.js | 228 +++++++++----------
js/ui/search.js | 272 +++++++++++++++++++++++
src/shell-app-system.c | 443 ++++++++++++++++++++++++--------------
src/shell-app-system.h | 23 +-
10 files changed, 1159 insertions(+), 562 deletions(-)
---
diff --git a/data/theme/gnome-shell.css b/data/theme/gnome-shell.css
index 8b42630..7fc91ff 100644
--- a/data/theme/gnome-shell.css
+++ b/data/theme/gnome-shell.css
@@ -152,17 +152,6 @@ StTooltip {
spacing: 12px;
}
-.dash-search-section-header {
- padding: 6px 0px;
- spacing: 4px;
- font-size: 12px;
- color: #bbbbbb;
-}
-
-.dash-search-section-title, dash-search-section-count {
- font-weight: bold;
-}
-
#searchEntry {
padding: 4px;
border-bottom: 1px solid #262626;
@@ -237,6 +226,29 @@ StTooltip {
height: 16px;
}
+.dash-search-section-header {
+ padding: 6px 0px;
+ spacing: 4px;
+}
+
+.dash-search-section-results {
+ color: #ffffff;
+ padding-left: 4px;
+}
+
+.dash-search-section-list-results {
+ spacing: 4px;
+}
+
+.dash-search-result-content {
+ padding: 2px;
+}
+
+.dash-search-result-content:selected {
+ padding: 1px;
+ border: 1px solid #262626;
+}
+
/* GenericDisplay */
.generic-display-container {
diff --git a/js/misc/docInfo.js b/js/misc/docInfo.js
index e60d7ca..559e9ec 100644
--- a/js/misc/docInfo.js
+++ b/js/misc/docInfo.js
@@ -7,6 +7,7 @@ const Shell = imports.gi.Shell;
const Lang = imports.lang;
const Signals = imports.signals;
+const Search = imports.ui.search;
const Main = imports.ui.main;
const THUMBNAIL_ICON_MARGIN = 2;
@@ -23,6 +24,7 @@ DocInfo.prototype = {
// correctly. See http://bugzilla.gnome.org/show_bug.cgi?id=567094
this.timestamp = recentInfo.get_modified().getTime() / 1000;
this.name = recentInfo.get_display_name();
+ this._lowerName = this.name.toLowerCase();
this.uri = recentInfo.get_uri();
this.mimeType = recentInfo.get_mime_type();
},
@@ -35,8 +37,24 @@ DocInfo.prototype = {
Shell.DocSystem.get_default().open(this.recentInfo);
},
- exists : function() {
- return this.recentInfo.exists();
+ matchTerms: function(terms) {
+ let mtype = Search.MatchType.NONE;
+ for (let i = 0; i < terms.length; i++) {
+ let term = terms[i];
+ let idx = this._lowerName.indexOf(term);
+ if (idx == 0) {
+ if (mtype != Search.MatchType.NONE)
+ return Search.MatchType.MULTIPLE;
+ mtype = Search.MatchType.PREFIX;
+ } else if (idx > 0) {
+ if (mtype != Search.MatchType.NONE)
+ return Search.MatchType.MULTIPLE;
+ mtype = Search.MatchType.SUBSTRING;
+ } else {
+ continue;
+ }
+ }
+ return mtype;
}
};
@@ -93,6 +111,41 @@ DocManager.prototype = {
queueExistenceCheck: function(count) {
return this._docSystem.queue_existence_check(count);
+ },
+
+ initialSearch: function(terms) {
+ let multipleMatches = [];
+ let prefixMatches = [];
+ let substringMatches = [];
+ for (let i = 0; i < this._infosByTimestamp.length; i++) {
+ let item = this._infosByTimestamp[i];
+ let mtype = item.matchTerms(terms);
+ if (mtype == Search.MatchType.MULTIPLE)
+ multipleMatches.push(item.uri);
+ else if (mtype == Search.MatchType.PREFIX)
+ prefixMatches.push(item.uri);
+ else if (mtype == Search.MatchType.SUBSTRING)
+ substringMatches.push(item.uri);
+ }
+ return multipleMatches.concat(prefixMatches.concat(substringMatches));
+ },
+
+ subsearch: function(previousResults, terms) {
+ let multipleMatches = [];
+ let prefixMatches = [];
+ let substringMatches = [];
+ for (let i = 0; i < previousResults.length; i++) {
+ let uri = previousResults[i];
+ let item = this._infosByUri[uri];
+ let mtype = item.matchTerms(terms);
+ if (mtype == Search.MatchType.MULTIPLE)
+ multipleMatches.push(uri);
+ else if (mtype == Search.MatchType.PREFIX)
+ prefixMatches.push(uri);
+ else if (mtype == Search.MatchType.SUBSTRING)
+ substringMatches.push(uri);
+ }
+ return multipleMatches.concat(prefixMatches.concat(substringMatches));
}
}
diff --git a/js/ui/appDisplay.js b/js/ui/appDisplay.js
index 651ab3a..e7c372a 100644
--- a/js/ui/appDisplay.js
+++ b/js/ui/appDisplay.js
@@ -18,6 +18,7 @@ const AppFavorites = imports.ui.appFavorites;
const DND = imports.ui.dnd;
const GenericDisplay = imports.ui.genericDisplay;
const Main = imports.ui.main;
+const Search = imports.ui.search;
const Workspaces = imports.ui.workspaces;
const APPICON_SIZE = 48;
@@ -134,17 +135,10 @@ AppDisplay.prototype = {
this._addApp(app);
}
} else {
- // Loop over the toplevel menu items, load the set of desktop file ids
- // associated with each one.
- let allMenus = this._appSystem.get_menus();
- for (let i = 0; i < allMenus.length; i++) {
- let menu = allMenus[i];
- let menuApps = this._appSystem.get_applications_for_menu(menu.id);
-
- for (let j = 0; j < menuApps.length; j++) {
- let app = menuApps[j];
- this._addApp(app);
- }
+ let apps = this._appSystem.get_flattened_apps();
+ for (let i = 0; i < apps.length; i++) {
+ let app = apps[i];
+ this._addApp(app);
}
}
@@ -220,6 +214,82 @@ AppDisplay.prototype = {
Signals.addSignalMethods(AppDisplay.prototype);
+function BaseAppSearchProvider() {
+ this._init();
+}
+
+BaseAppSearchProvider.prototype = {
+ __proto__: Search.SearchProvider.prototype,
+
+ _init: function(name) {
+ Search.SearchProvider.prototype._init.call(this, name);
+ this._appSys = Shell.AppSystem.get_default();
+ },
+
+ getResultMeta: function(resultId) {
+ let app = this._appSys.get_app(resultId);
+ if (!app)
+ return null;
+ return { 'id': resultId,
+ 'name': app.get_name(),
+ 'icon': app.create_icon_texture(Search.RESULT_ICON_SIZE)};
+ },
+
+ activateResult: function(id) {
+ let app = this._appSys.get_app(id);
+ app.launch();
+ }
+};
+
+function AppSearchProvider() {
+ this._init();
+}
+
+AppSearchProvider.prototype = {
+ __proto__: BaseAppSearchProvider.prototype,
+
+ _init: function() {
+ BaseAppSearchProvider.prototype._init.call(this, _("APPLICATIONS"));
+ },
+
+ getInitialResultSet: function(terms) {
+ return this._appSys.initial_search(false, terms);
+ },
+
+ getSubsearchResultSet: function(previousResults, terms) {
+ return this._appSys.subsearch(false, previousResults, terms);
+ },
+
+ expandSearch: function(terms) {
+ log("TODO expand search");
+ }
+}
+
+function PrefsSearchProvider() {
+ this._init();
+}
+
+PrefsSearchProvider.prototype = {
+ __proto__: BaseAppSearchProvider.prototype,
+
+ _init: function() {
+ BaseAppSearchProvider.prototype._init.call(this, _("PREFERENCES"));
+ },
+
+ getInitialResultSet: function(terms) {
+ return this._appSys.initial_search(true, terms);
+ },
+
+ getSubsearchResultSet: function(previousResults, terms) {
+ return this._appSys.subsearch(true, previousResults, terms);
+ },
+
+ expandSearch: function(terms) {
+ let controlCenter = this._appSys.load_from_desktop_file('gnomecc.desktop');
+ controlCenter.launch();
+ Main.overview.hide();
+ }
+}
function AppIcon(app) {
this._init(app);
diff --git a/js/ui/dash.js b/js/ui/dash.js
index 1fc2184..3fe7e95 100644
--- a/js/ui/dash.js
+++ b/js/ui/dash.js
@@ -17,6 +17,10 @@ const DocDisplay = imports.ui.docDisplay;
const PlaceDisplay = imports.ui.placeDisplay;
const GenericDisplay = imports.ui.genericDisplay;
const Main = imports.ui.main;
+const Search = imports.ui.search;
+
+// 25 search results (per result type) should be enough for everyone
+const MAX_RENDERED_SEARCH_RESULTS = 25;
const DEFAULT_PADDING = 4;
const DEFAULT_SPACING = 4;
@@ -332,6 +336,254 @@ SearchEntry.prototype = {
};
Signals.addSignalMethods(SearchEntry.prototype);
+function SearchResult(provider, metaInfo, terms) {
+ this._init(provider, metaInfo, terms);
+}
+
+SearchResult.prototype = {
+ _init: function(provider, metaInfo, terms) {
+ this.provider = provider;
+ this.metaInfo = metaInfo;
+ this.actor = new St.Clickable({ style_class: 'dash-search-result',
+ reactive: true,
+ x_align: St.Align.START,
+ x_fill: true,
+ y_fill: true });
+ this.actor._delegate = this;
+
+ let content = provider.createResultActor(metaInfo, terms);
+ if (content == null) {
+ content = new St.BoxLayout({ style_class: 'dash-search-result-content' });
+ let title = new St.Label({ text: this.metaInfo['name'] });
+ let icon = this.metaInfo['icon'];
+ content.add(icon, { y_fill: false });
+ content.add(title, { expand: true, y_fill: false });
+ }
+ this._content = content;
+ this.actor.set_child(content);
+
+ this.actor.connect('clicked', Lang.bind(this, this._onResultClicked));
+ },
+
+ setSelected: function(selected) {
+ this._content.set_style_pseudo_class(selected ? 'selected' : null);
+ },
+
+ activate: function() {
+ this.provider.activateResult(this.metaInfo.id);
+ Main.overview.toggle();
+ },
+
+ _onResultClicked: function(actor, event) {
+ this.activate();
+ }
+}
+
+function OverflowSearchResults(provider) {
+ this._init(provider);
+}
+
+OverflowSearchResults.prototype = {
+ __proto__: Search.SearchResultDisplay.prototype,
+
+ _init: function(provider) {
+ Search.SearchResultDisplay.prototype._init.call(this, provider);
+ this.actor = new St.OverflowBox({ style_class: 'dash-search-section-list-results' });
+ },
+
+ renderResults: function(results, terms) {
+ for (let i = 0; i < results.length && i < MAX_RENDERED_SEARCH_RESULTS; i++) {
+ let result = results[i];
+ let meta = this.provider.getResultMeta(result);
+ let display = new SearchResult(this.provider, meta, terms);
+ this.actor.add_actor(display.actor);
+ }
+ },
+
+ getVisibleCount: function() {
+ return this.actor.get_n_visible();
+ },
+
+ selectIndex: function(index) {
+ let nVisible = this.actor.get_n_visible();
+ let children = this.actor.get_children();
+ if (this.selectionIndex >= 0) {
+ let prevActor = children[this.selectionIndex];
+ prevActor._delegate.setSelected(false);
+ }
+ this.selectionIndex = -1;
+ if (index >= nVisible)
+ return false;
+ else if (index < 0)
+ return false;
+ let targetActor = children[index];
+ targetActor._delegate.setSelected(true);
+ this.selectionIndex = index;
+ return true;
+ }
+}
+
+function SearchResults(searchSystem) {
+ this._init(searchSystem);
+}
+
+SearchResults.prototype = {
+ _init: function(searchSystem) {
+ this._searchSystem = searchSystem;
+
+ this.actor = new St.BoxLayout({ name: 'dashSearchResults',
+ vertical: true });
+ this._searchingNotice = new St.Label({ style_class: 'dash-search-starting',
+ text: _("Searching...") });
+ this.actor.add(this._searchingNotice);
+ this._selectedProvider = -1;
+ this._providers = this._searchSystem.getProviders();
+ this._providerMeta = [];
+ for (let i = 0; i < this._providers.length; i++) {
+ let provider = this._providers[i];
+ let providerBox = new St.BoxLayout({ style_class: 'dash-search-section',
+ vertical: true });
+ let titleButton = new St.Button({ style_class: 'dash-search-section-header',
+ reactive: true,
+ x_fill: true,
+ y_fill: true });
+ titleButton.connect('clicked', Lang.bind(this, function () { this._onHeaderClicked(provider); }));
+ providerBox.add(titleButton);
+ let titleBox = new St.BoxLayout();
+ titleButton.set_child(titleBox);
+ let title = new St.Label({ text: provider.title });
+ let count = new St.Label();
+ titleBox.add(title, { expand: true });
+ titleBox.add(count);
+
+ let resultDisplayBin = new St.Bin({ style_class: 'dash-search-section-results',
+ x_fill: true,
+ y_fill: true });
+ providerBox.add(resultDisplayBin, { expand: true });
+ let resultDisplay = provider.createResultContainerActor();
+ if (resultDisplay == null) {
+ resultDisplay = new OverflowSearchResults(provider);
+ }
+ resultDisplayBin.set_child(resultDisplay.actor);
+
+ this._providerMeta.push({ actor: providerBox,
+ resultDisplay: resultDisplay,
+ count: count });
+ this.actor.add(providerBox);
+ }
+ },
+
+ _clearDisplay: function() {
+ this._selectedProvider = -1;
+ this._visibleResultsCount = 0;
+ for (let i = 0; i < this._providerMeta.length; i++) {
+ let meta = this._providerMeta[i];
+ meta.resultDisplay.clear();
+ meta.actor.hide();
+ }
+ },
+
+ reset: function() {
+ this._searchSystem.reset();
+ this._searchingNotice.hide();
+ this._clearDisplay();
+ },
+
+ startingSearch: function() {
+ this.reset();
+ this._searchingNotice.show();
+ },
+
+ _metaForProvider: function(provider) {
+ return this._providerMeta[this._providers.indexOf(provider)];
+ },
+
+ updateSearch: function (searchString) {
+ let results = this._searchSystem.updateSearch(searchString);
+
+ this._searchingNotice.hide();
+ this._clearDisplay();
+
+ let terms = this._searchSystem.getTerms();
+
+ for (let i = 0; i < results.length; i++) {
+ let [provider, providerResults] = results[i];
+ let meta = this._metaForProvider(provider);
+ meta.actor.show();
+ meta.resultDisplay.renderResults(providerResults, terms);
+ meta.count.set_text(""+providerResults.length);
+ }
+
+ this.selectDown();
+
+ return true;
+ },
+
+ _onHeaderClicked: function(provider) {
+ provider.expandSearch(this._searchSystem.getTerms());
+ },
+
+ _modifyActorSelection: function(resultDisplay, up) {
+ let success;
+ let index = resultDisplay.getSelectionIndex();
+ if (up && index == -1)
+ index = resultDisplay.getVisibleCount() - 1;
+ else if (up)
+ index = index - 1;
+ else
+ index = index + 1;
+ return resultDisplay.selectIndex(index);
+ },
+
+ selectUp: function() {
+ for (let i = this._selectedProvider; i >= 0; i--) {
+ let meta = this._providerMeta[i];
+ if (!meta.actor.visible)
+ continue;
+ let success = this._modifyActorSelection(meta.resultDisplay, true);
+ if (success) {
+ this._selectedProvider = i;
+ return;
+ }
+ }
+ if (this._providerMeta.length > 0) {
+ this._selectedProvider = this._providerMeta.length - 1;
+ this.selectUp();
+ }
+ },
+
+ selectDown: function() {
+ let current = this._selectedProvider;
+ if (current == -1)
+ current = 0;
+ for (let i = current; i < this._providerMeta.length; i++) {
+ let meta = this._providerMeta[i];
+ if (!meta.actor.visible)
+ continue;
+ let success = this._modifyActorSelection(meta.resultDisplay, false);
+ if (success) {
+ this._selectedProvider = i;
+ return;
+ }
+ }
+ if (this._providerMeta.length > 0) {
+ this._selectedProvider = 0;
+ this.selectDown();
+ }
+ },
+
+ activateSelected: function() {
+ let current = this._selectedProvider;
+ if (current < 0)
+ return;
+ let meta = this._providerMeta[current];
+ let resultDisplay = meta.resultDisplay;
+ let children = resultDisplay.actor.get_children();
+ let targetActor = children[resultDisplay.getSelectionIndex()];
+ targetActor._delegate.activate();
+ }
+}
+
function MoreLink() {
this._init();
}
@@ -500,9 +752,9 @@ Dash.prototype = {
vertical: true,
reactive: true });
- // Size for this one explicitly set from overlay.js
- this.searchArea = new Big.Box({ y_align: Big.BoxAlignment.CENTER });
-
+ // The searchArea just holds the entry
+ this.searchArea = new St.BoxLayout({ name: "dashSearchArea",
+ vertical: true });
this.sectionArea = new St.BoxLayout({ name: "dashSections",
vertical: true });
@@ -517,16 +769,35 @@ Dash.prototype = {
this._searchActive = false;
this._searchPending = false;
this._searchEntry = new SearchEntry();
- this.searchArea.append(this._searchEntry.actor, Big.BoxPackFlags.EXPAND);
+ this.searchArea.add(this._searchEntry.actor, { y_fill: false, expand: true });
+
+ this._searchSystem = new Search.SearchSystem();
+ this._searchSystem.registerProvider(new AppDisplay.AppSearchProvider());
+ this._searchSystem.registerProvider(new AppDisplay.PrefsSearchProvider());
+ this._searchSystem.registerProvider(new PlaceDisplay.PlaceSearchProvider());
+ this._searchSystem.registerProvider(new DocDisplay.DocSearchProvider());
+
+ this.searchResults = new SearchResults(this._searchSystem);
+ this.actor.add(this.searchResults.actor);
+ this.searchResults.actor.hide();
this._searchTimeoutId = 0;
this._searchEntry.entry.connect('text-changed', Lang.bind(this, function (se, prop) {
let text = this._searchEntry.getText();
- text = text.replace(/^\s+/g, "").replace(/\s+$/g, "")
+ text = text.replace(/^\s+/g, "").replace(/\s+$/g, "");
let searchPreviouslyActive = this._searchActive;
this._searchActive = text != '';
this._searchPending = this._searchActive && !searchPreviouslyActive;
- this._updateDashActors();
+ if (this._searchPending) {
+ this.searchResults.startingSearch();
+ }
+ if (this._searchActive) {
+ this.searchResults.actor.show();
+ this.sectionArea.hide();
+ } else {
+ this.searchResults.actor.hide();
+ this.sectionArea.show();
+ }
if (!this._searchActive) {
if (this._searchTimeoutId > 0) {
Mainloop.source_remove(this._searchTimeoutId);
@@ -543,24 +814,15 @@ Dash.prototype = {
Mainloop.source_remove(this._searchTimeoutId);
this._doSearch();
}
- // Only one of the displays will have an item selected, so it's ok to
- // call activateSelected() on all of them.
- for (var i = 0; i < this._searchSections.length; i++) {
- let section = this._searchSections[i];
- section.resultArea.display.activateSelected();
- }
+ this.searchResults.activateSelected();
return true;
}));
this._searchEntry.entry.connect('key-press-event', Lang.bind(this, function (se, e) {
- let text = this._searchEntry.getText();
let symbol = e.get_key_symbol();
if (symbol == Clutter.Escape) {
// Escape will keep clearing things back to the desktop.
- // If we are showing a particular section of search, go back to all sections.
- if (this._searchResultsSingleShownSection != null)
- this._showAllSearchSections();
// If we have an active search, we remove it.
- else if (this._searchActive)
+ if (this._searchActive)
this._searchEntry.reset();
// Next, if we're in one of the "more" modes or showing the details pane, close them
else if (this._activePane != null)
@@ -572,44 +834,14 @@ Dash.prototype = {
} else if (symbol == Clutter.Up) {
if (!this._searchActive)
return true;
- // selectUp and selectDown wrap around in their respective displays
- // too, but there doesn't seem to be any flickering if we first select
- // something in one display, but then unset the selection, and move
- // it to the other display, so it's ok to do that.
- for (var i = 0; i < this._searchSections.length; i++) {
- let section = this._searchSections[i];
- if (section.resultArea.display.hasSelected() && !section.resultArea.display.selectUp()) {
- if (this._searchResultsSingleShownSection != section.type) {
- // We need to move the selection to the next section above this section that has items,
- // wrapping around at the bottom, if necessary.
- let newSectionIndex = this._findAnotherSectionWithItems(i, -1);
- if (newSectionIndex >= 0) {
- this._searchSections[newSectionIndex].resultArea.display.selectLastItem();
- section.resultArea.display.unsetSelected();
- }
- }
- break;
- }
- }
+ this.searchResults.selectUp();
+
return true;
} else if (symbol == Clutter.Down) {
if (!this._searchActive)
return true;
- for (var i = 0; i < this._searchSections.length; i++) {
- let section = this._searchSections[i];
- if (section.resultArea.display.hasSelected() && !section.resultArea.display.selectDown()) {
- if (this._searchResultsSingleShownSection != section.type) {
- // We need to move the selection to the next section below this section that has items,
- // wrapping around at the top, if necessary.
- let newSectionIndex = this._findAnotherSectionWithItems(i, 1);
- if (newSectionIndex >= 0) {
- this._searchSections[newSectionIndex].resultArea.display.selectFirstItem();
- section.resultArea.display.unsetSelected();
- }
- }
- break;
- }
- }
+
+ this.searchResults.selectDown();
return true;
}
return false;
@@ -666,102 +898,12 @@ Dash.prototype = {
this._docDisplay.emit('changed');
this.sectionArea.add(this._docsSection.actor, { expand: true });
-
- /***** Search Results *****/
-
- this._searchResultsSection = new Section(_("SEARCH RESULTS"), true);
-
- this._searchResultsSingleShownSection = null;
-
- this._searchResultsSection.header.connect('back-link-activated', Lang.bind(this, function () {
- this._showAllSearchSections();
- }));
-
- this._searchSections = [
- { type: APPS,
- title: _("APPLICATIONS"),
- header: null,
- resultArea: null
- },
- { type: PREFS,
- title: _("PREFERENCES"),
- header: null,
- resultArea: null
- },
- { type: DOCS,
- title: _("RECENT DOCUMENTS"),
- header: null,
- resultArea: null
- },
- { type: PLACES,
- title: _("PLACES"),
- header: null,
- resultArea: null
- }
- ];
-
- for (var i = 0; i < this._searchSections.length; i++) {
- let section = this._searchSections[i];
- section.header = new SearchSectionHeader(section.title,
- Lang.bind(this,
- function () {
- this._showSingleSearchSection(section.type);
- }));
- this._searchResultsSection.content.add(section.header.actor);
- section.resultArea = new ResultArea(section.type, GenericDisplay.GenericDisplayFlags.DISABLE_VSCROLLING);
- this._searchResultsSection.content.add(section.resultArea.actor, { expand: true });
- createPaneForDetails(this, section.resultArea.display);
- }
-
- this.sectionArea.add(this._searchResultsSection.actor, { expand: true });
- this._searchResultsSection.actor.hide();
},
_doSearch: function () {
this._searchTimeoutId = 0;
let text = this._searchEntry.getText();
- text = text.replace(/^\s+/g, "").replace(/\s+$/g, "");
-
- let selectionSet = false;
-
- for (var i = 0; i < this._searchSections.length; i++) {
- let section = this._searchSections[i];
- section.resultArea.display.setSearch(text);
- let itemCount = section.resultArea.display.getMatchedItemsCount();
- let itemCountText = itemCount + "";
- section.header.countText.text = itemCountText;
-
- if (this._searchResultsSingleShownSection == section.type) {
- this._searchResultsSection.header.setCountText(itemCountText);
- if (itemCount == 0) {
- section.resultArea.actor.hide();
- } else {
- section.resultArea.actor.show();
- }
- } else if (this._searchResultsSingleShownSection == null) {
- // Don't show the section if it has no results
- if (itemCount == 0) {
- section.header.actor.hide();
- section.resultArea.actor.hide();
- } else {
- section.header.actor.show();
- section.resultArea.actor.show();
- }
- }
-
- // Refresh the selection when a new search is applied.
- section.resultArea.display.unsetSelected();
- if (!selectionSet && section.resultArea.display.hasItems() &&
- (this._searchResultsSingleShownSection == null || this._searchResultsSingleShownSection == section.type)) {
- section.resultArea.display.selectFirstItem();
- selectionSet = true;
- }
- }
-
- // Here work around a bug that I never quite tracked down
- // the root cause of; it appeared that the search results
- // section was getting a 0 height allocation.
- this._searchResultsSection.content.queue_relayout();
+ this.searchResults.updateSearch(text);
return false;
},
@@ -794,101 +936,6 @@ Dash.prototype = {
}
}));
Main.overview.addPane(pane);
- },
-
- _updateDashActors: function() {
- if (this._searchPending) {
- this._searchResultsSection.actor.show();
- // We initially hide all sections when we start a search. When the search timeout
- // first runs, the sections that have matching results are shown. As the search
- // is refined, only the sections that have matching results will be shown.
- for (let i = 0; i < this._searchSections.length; i++) {
- let section = this._searchSections[i];
- section.header.actor.hide();
- section.resultArea.actor.hide();
- }
- this._appsSection.actor.hide();
- this._placesSection.actor.hide();
- this._docsSection.actor.hide();
- } else if (!this._searchActive) {
- this._showAllSearchSections();
- this._searchResultsSection.actor.hide();
- this._appsSection.actor.show();
- this._placesSection.actor.show();
- this._docsSection.actor.show();
- }
- },
-
- _showSingleSearchSection: function(type) {
- // We currently don't allow going from showing one section to showing another section.
- if (this._searchResultsSingleShownSection != null) {
- throw new Error("We were already showing a single search section: '" + this._searchResultsSingleShownSection
- + "' when _showSingleSearchSection() was called for '" + type + "'");
- }
- for (var i = 0; i < this._searchSections.length; i++) {
- let section = this._searchSections[i];
- if (section.type == type) {
- // This will be the only section shown.
- section.resultArea.display.selectFirstItem();
- let itemCount = section.resultArea.display.getMatchedItemsCount();
- let itemCountText = itemCount + "";
- section.header.actor.hide();
- this._searchResultsSection.header.setTitle(section.title);
- this._searchResultsSection.header.setBackLinkVisible(true);
- this._searchResultsSection.header.setCountText(itemCountText);
- } else {
- // We need to hide this section.
- section.header.actor.hide();
- section.resultArea.actor.hide();
- section.resultArea.display.unsetSelected();
- }
- }
- this._searchResultsSingleShownSection = type;
- },
-
- _showAllSearchSections: function() {
- if (this._searchResultsSingleShownSection != null) {
- let selectionSet = false;
- for (var i = 0; i < this._searchSections.length; i++) {
- let section = this._searchSections[i];
- if (section.type == this._searchResultsSingleShownSection) {
- // This will no longer be the only section shown.
- let itemCount = section.resultArea.display.getMatchedItemsCount();
- if (itemCount != 0) {
- section.header.actor.show();
- section.resultArea.display.selectFirstItem();
- selectionSet = true;
- }
- this._searchResultsSection.header.setTitle(_("SEARCH RESULTS"));
- this._searchResultsSection.header.setBackLinkVisible(false);
- this._searchResultsSection.header.setCountText("");
- } else {
- // We need to restore this section.
- let itemCount = section.resultArea.display.getMatchedItemsCount();
- if (itemCount != 0) {
- section.header.actor.show();
- section.resultArea.actor.show();
- // This ensures that some other section will have the selection if the
- // single section that was being displayed did not have any items.
- if (!selectionSet) {
- section.resultArea.display.selectFirstItem();
- selectionSet = true;
- }
- }
- }
- }
- this._searchResultsSingleShownSection = null;
- }
- },
-
- _findAnotherSectionWithItems: function(index, increment) {
- let pos = _getIndexWrapped(index, increment, this._searchSections.length);
- while (pos != index) {
- if (this._searchSections[pos].resultArea.display.hasItems())
- return pos;
- pos = _getIndexWrapped(pos, increment, this._searchSections.length);
- }
- return -1;
}
};
Signals.addSignalMethods(Dash.prototype);
diff --git a/js/ui/docDisplay.js b/js/ui/docDisplay.js
index 9ba6c4c..ee4fa7e 100644
--- a/js/ui/docDisplay.js
+++ b/js/ui/docDisplay.js
@@ -10,11 +10,14 @@ const Shell = imports.gi.Shell;
const Signals = imports.signals;
const St = imports.gi.St;
const Mainloop = imports.mainloop;
+const Gettext = imports.gettext.domain('gnome-shell');
+const _ = Gettext.gettext;
const DocInfo = imports.misc.docInfo;
const DND = imports.ui.dnd;
const GenericDisplay = imports.ui.genericDisplay;
const Main = imports.ui.main;
+const Search = imports.ui.search;
const MAX_DASH_DOCS = 50;
const DASH_DOCS_ICON_SIZE = 16;
@@ -179,13 +182,8 @@ DocDisplay.prototype = {
this._matchedItemKeys = [];
let docIdsToRemove = [];
for (docId in this._allItems) {
- // this._allItems[docId].exists() checks if the resource still exists
- if (this._allItems[docId].exists()) {
- this._matchedItems[docId] = 1;
- this._matchedItemKeys.push(docId);
- } else {
- docIdsToRemove.push(docId);
- }
+ this._matchedItems[docId] = 1;
+ this._matchedItemKeys.push(docId);
}
for (docId in docIdsToRemove) {
@@ -479,3 +477,41 @@ DashDocDisplay.prototype = {
Signals.addSignalMethods(DashDocDisplay.prototype);
+function DocSearchProvider() {
+ this._init();
+}
+
+DocSearchProvider.prototype = {
+ __proto__: Search.SearchProvider.prototype,
+
+ _init: function(name) {
+ Search.SearchProvider.prototype._init.call(this, _("DOCUMENTS"));
+ this._docManager = DocInfo.getDocManager();
+ },
+
+ getResultMeta: function(resultId) {
+ let docInfo = this._docManager.lookupByUri(resultId);
+ if (!docInfo)
+ return null;
+ return { 'id': resultId,
+ 'name': docInfo.name,
+ 'icon': docInfo.createIcon(Search.RESULT_ICON_SIZE)};
+ },
+
+ activateResult: function(id) {
+ let docInfo = this._docManager.lookupByUri(id);
+ docInfo.launch();
+ },
+
+ getInitialResultSet: function(terms) {
+ return this._docManager.initialSearch(terms);
+ },
+
+ getSubsearchResultSet: function(previousResults, terms) {
+ return this._docManager.subsearch(previousResults, terms);
+ },
+
+ expandSearch: function(terms) {
+ log("TODO expand docs search");
+ }
+};
diff --git a/js/ui/overview.js b/js/ui/overview.js
index 74859a7..a9697ff 100644
--- a/js/ui/overview.js
+++ b/js/ui/overview.js
@@ -184,6 +184,7 @@ Overview.prototype = {
this._dash.actor.set_size(displayGridColumnWidth, contentHeight);
this._dash.searchArea.height = this._workspacesY - contentY;
this._dash.sectionArea.height = this._workspacesHeight;
+ this._dash.searchResults.actor.height = this._workspacesHeight;
// place the 'Add Workspace' button in the bottom row of the grid
addRemoveButtonSize = Math.floor(displayGridRowHeight * 3/5);
diff --git a/js/ui/placeDisplay.js b/js/ui/placeDisplay.js
index 82a3674..868ad11 100644
--- a/js/ui/placeDisplay.js
+++ b/js/ui/placeDisplay.js
@@ -15,7 +15,7 @@ const _ = Gettext.gettext;
const DND = imports.ui.dnd;
const Main = imports.ui.main;
-const GenericDisplay = imports.ui.genericDisplay;
+const Search = imports.ui.search;
const NAUTILUS_PREFS_DIR = '/apps/nautilus/preferences';
const DESKTOP_IS_HOME_KEY = NAUTILUS_PREFS_DIR + '/desktop_is_home_dir';
@@ -30,16 +30,30 @@ const PLACES_ICON_SIZE = 16;
* @iconFactory: A JavaScript callback which will create an icon texture given a size parameter
* @launch: A JavaScript callback to launch the entry
*/
-function PlaceInfo(name, iconFactory, launch) {
- this._init(name, iconFactory, launch);
+function PlaceInfo(id, name, iconFactory, launch) {
+ this._init(id, name, iconFactory, launch);
}
PlaceInfo.prototype = {
- _init: function(name, iconFactory, launch) {
+ _init: function(id, name, iconFactory, launch) {
+ this.id = id;
this.name = name;
+ this._lowerName = name.toLowerCase();
this.iconFactory = iconFactory;
this.launch = launch;
- this.id = null;
+ },
+
+ matchTerms: function(terms) {
+ let mtype = Search.MatchType.NONE;
+ for (let i = 0; i < terms.length; i++) {
+ let term = terms[i];
+ let idx = this._lowerName.indexOf(term);
+ if (idx == 0)
+ return Search.MatchType.PREFIX;
+ else if (idx > 0)
+ mtype = Search.MatchType.SUBSTRING;
+ }
+ return mtype;
}
}
@@ -52,6 +66,7 @@ PlacesManager.prototype = {
let gconf = Shell.GConf.get_default();
gconf.watch_directory(NAUTILUS_PREFS_DIR);
+ this._defaultPlaces = [];
this._mounts = [];
this._bookmarks = [];
this._isDesktopHome = false;
@@ -60,7 +75,7 @@ PlacesManager.prototype = {
let homeUri = homeFile.get_uri();
let homeLabel = Shell.util_get_label_for_uri (homeUri);
let homeIcon = Shell.util_get_icon_for_uri (homeUri);
- this._home = new PlaceInfo(homeLabel,
+ this._home = new PlaceInfo('special:home', homeLabel,
function(size) {
return Shell.TextureCache.get_default().load_gicon(homeIcon, size);
},
@@ -73,7 +88,7 @@ PlacesManager.prototype = {
let desktopUri = desktopFile.get_uri();
let desktopLabel = Shell.util_get_label_for_uri (desktopUri);
let desktopIcon = Shell.util_get_icon_for_uri (desktopUri);
- this._desktopMenu = new PlaceInfo(desktopLabel,
+ this._desktopMenu = new PlaceInfo('special:desktop', desktopLabel,
function(size) {
return Shell.TextureCache.get_default().load_gicon(desktopIcon, size);
},
@@ -81,7 +96,7 @@ PlacesManager.prototype = {
Gio.app_info_launch_default_for_uri(desktopUri, global.create_app_launch_context());
});
- this._connect = new PlaceInfo(_("Connect to..."),
+ this._connect = new PlaceInfo('special:connect', _("Connect to..."),
function (size) {
return Shell.TextureCache.get_default().load_icon_name("applications-internet", size);
},
@@ -101,7 +116,7 @@ PlacesManager.prototype = {
}
if (networkApp != null) {
- this._network = new PlaceInfo(networkApp.get_name(),
+ this._network = new PlaceInfo('special:network', networkApp.get_name(),
function(size) {
return networkApp.create_icon_texture(size);
},
@@ -110,6 +125,16 @@ PlacesManager.prototype = {
});
}
+ this._defaultPlaces.push(this._home);
+
+ if (!this._isDesktopHome)
+ this._defaultPlaces.push(this._desktopMenu);
+
+ if (this._network)
+ this._defaultPlaces.push(this._network);
+
+ this._defaultPlaces.push(this._connect);
+
/*
* Show devices, code more or less ported from nautilus-places-sidebar.c
*/
@@ -238,7 +263,7 @@ PlacesManager.prototype = {
continue;
let icon = Shell.util_get_icon_for_uri(bookmark);
- let item = new PlaceInfo(label,
+ let item = new PlaceInfo('bookmark:' + bookmark, label,
function(size) {
return Shell.TextureCache.get_default().load_gicon(icon, size);
},
@@ -267,7 +292,8 @@ PlacesManager.prototype = {
let mountIcon = mount.get_icon();
let root = mount.get_root();
let mountUri = root.get_uri();
- let devItem = new PlaceInfo(mountLabel,
+ let devItem = new PlaceInfo('mount:' + mountUri,
+ mountLabel,
function(size) {
return Shell.TextureCache.get_default().load_gicon(mountIcon, size);
},
@@ -282,16 +308,7 @@ PlacesManager.prototype = {
},
getDefaultPlaces: function () {
- let places = [this._home];
-
- if (!this._isDesktopHome)
- places.push(this._desktopMenu);
-
- if (this._network)
- places.push(this._network);
-
- places.push(this._connect);
- return places;
+ return this._defaultPlaces;
},
getBookmarks: function () {
@@ -300,6 +317,28 @@ PlacesManager.prototype = {
getMounts: function () {
return this._mounts;
+ },
+
+ _lookupById: function(sourceArray, id) {
+ for (let i = 0; i < sourceArray.length; i++) {
+ let place = sourceArray[i];
+ if (place.id == id)
+ return place;
+ }
+ return null;
+ },
+
+ lookupPlaceById: function(id) {
+ let colonIdx = id.indexOf(':');
+ let type = id.substring(0, colonIdx);
+ let sourceArray = null;
+ if (type == 'special')
+ sourceArray = this._defaultPlaces;
+ else if (type == 'mount')
+ sourceArray = this._mounts;
+ else if (type == 'bookmark')
+ sourceArray = this._bookmarks;
+ return this._lookupById(sourceArray, id);
}
};
@@ -421,120 +460,67 @@ DashPlaceDisplay.prototype = {
Signals.addSignalMethods(DashPlaceDisplay.prototype);
-
-function PlaceDisplayItem(placeInfo) {
- this._init(placeInfo);
+function PlaceSearchProvider() {
+ this._init();
}
-PlaceDisplayItem.prototype = {
- __proto__: GenericDisplay.GenericDisplayItem.prototype,
-
- _init : function(placeInfo) {
- GenericDisplay.GenericDisplayItem.prototype._init.call(this);
- this._info = placeInfo;
-
- this._setItemInfo(placeInfo.name, '');
- },
+PlaceSearchProvider.prototype = {
+ __proto__: Search.SearchProvider.prototype,
- //// Public method overrides ////
-
- // Opens an application represented by this display item.
- launch : function() {
- this._info.launch();
+ _init: function() {
+ Search.SearchProvider.prototype._init.call(this, _("PLACES"));
},
- shellWorkspaceLaunch: function() {
- this._info.launch();
+ getResultMeta: function(resultId) {
+ let placeInfo = Main.placesManager.lookupPlaceById(resultId);
+ if (!placeInfo)
+ return null;
+ return { 'id': resultId,
+ 'name': placeInfo.name,
+ 'icon': placeInfo.iconFactory(Search.RESULT_ICON_SIZE) };
},
- //// Protected method overrides ////
-
- // Returns an icon for the item.
- _createIcon: function() {
- return this._info.iconFactory(GenericDisplay.ITEM_DISPLAY_ICON_SIZE);
+ activateResult: function(id) {
+ let placeInfo = Main.placesManager.lookupPlaceById(id);
+ placeInfo.launch();
},
- // Returns a preview icon for the item.
- _createPreviewIcon: function() {
- return this._info.iconFactory(GenericDisplay.PREVIEW_ICON_SIZE);
- }
-
-};
-
-function PlaceDisplay(flags) {
- this._init(flags);
-}
-
-PlaceDisplay.prototype = {
- __proto__: GenericDisplay.GenericDisplay.prototype,
-
- _init: function(flags) {
- GenericDisplay.GenericDisplay.prototype._init.call(this, flags);
- this._stale = true;
- Main.placesManager.connect('places-updated', Lang.bind(this, function (e) {
- this._stale = true;
- }));
+ _compareResultMeta: function (idA, idB) {
+ let infoA = Main.placesManager.lookupPlaceById(idA);
+ let infoB = Main.placesManager.lookupPlaceById(idB);
+ return infoA.name.localeCompare(infoB.name);
},
- //// Protected method overrides ////
- _refreshCache: function () {
- if (!this._stale)
- return true;
- this._allItems = {};
- let array = Main.placesManager.getAllPlaces();
- for (let i = 0; i < array.length; i ++) {
- // We are using an array id as placeInfo id because placeInfo doesn't have any
- // other information piece that can be used as a unique id. There are different
- // types of placeInfo, such as devices and directories that would result in differently
- // structured ids. Also the home directory can show up in both the default places and in
- // bookmarks which means its URI can't be used as a unique id. (This does mean it can
- // appear twice in search results, though that doesn't happen at the moment because we
- // name it "Home Folder" in default places and it's named with the user's system name
- // if it appears as a bookmark.)
- let placeInfo = array[i];
- placeInfo.id = i;
- this._allItems[i] = placeInfo;
+ _searchPlaces: function(places, terms) {
+ let multipleResults = [];
+ let prefixResults = [];
+ let substringResults = [];
+
+ terms = terms.map(String.toLowerCase);
+
+ for (let i = 0; i < places.length; i++) {
+ let place = places[i];
+ let mtype = place.matchTerms(terms);
+ if (mtype == Search.MatchType.MULTIPLE)
+ multipleResults.push(place.id);
+ else if (mtype == Search.MatchType.PREFIX)
+ prefixResults.push(place.id);
+ else if (mtype == Search.MatchType.SUBSTRING)
+ substringResults.push(place.id);
}
- this._stale = false;
- return false;
+ multipleResults.sort(this._compareResultMeta);
+ prefixResults.sort(this._compareResultMeta);
+ substringResults.sort(this._compareResultMeta);
+ return multipleResults.concat(prefixResults.concat(substringResults));
},
- // Sets the list of the displayed items.
- _setDefaultList: function() {
- this._matchedItems = {};
- this._matchedItemKeys = [];
- for (id in this._allItems) {
- this._matchedItems[id] = 1;
- this._matchedItemKeys.push(id);
- }
- this._matchedItemKeys.sort(Lang.bind(this, this._compareItems));
+ getInitialResultSet: function(terms) {
+ let places = Main.placesManager.getAllPlaces();
+ return this._searchPlaces(places, terms);
},
- // Checks if the item info can be a match for the search string by checking
- // the name of the place. Item info is expected to be PlaceInfo.
- // Returns a boolean flag indicating if itemInfo is a match.
- _isInfoMatching: function(itemInfo, search) {
- if (search == null || search == '')
- return true;
-
- let name = itemInfo.name.toLowerCase();
- if (name.indexOf(search) >= 0)
- return true;
-
- return false;
- },
-
- // Compares items associated with the item ids based on the alphabetical order
- // of the item names.
- // Returns an integer value indicating the result of the comparison.
- _compareItems: function(itemIdA, itemIdB) {
- let placeA = this._allItems[itemIdA];
- let placeB = this._allItems[itemIdB];
- return placeA.name.localeCompare(placeB.name);
- },
-
- // Creates a PlaceDisplayItem based on itemInfo, which is expected to be a PlaceInfo object.
- _createDisplayItem: function(itemInfo) {
- return new PlaceDisplayItem(itemInfo);
+ getSubsearchResultSet: function(previousResults, terms) {
+ let places = previousResults.map(function (id) { return Main.placesManager.lookupPlaceById(id); });
+ return this._searchPlaces(places, terms);
}
-};
+}
diff --git a/js/ui/search.js b/js/ui/search.js
new file mode 100644
index 0000000..2b73852
--- /dev/null
+++ b/js/ui/search.js
@@ -0,0 +1,272 @@
+/* -*- mode: js2; js2-basic-offset: 4; indent-tabs-mode: nil -*- */
+
+const Signals = imports.signals;
+const St = imports.gi.St;
+
+const RESULT_ICON_SIZE = 24;
+
+// Not currently referenced by the search API, but
+// this enumeration can be useful for provider
+// implementations.
+const MatchType = {
+ NONE: 0,
+ MULTIPLE: 1,
+ PREFIX: 2,
+ SUBSTRING: 3
+};
+
+function SearchResultDisplay(provider) {
+ this._init(provider);
+}
+
+SearchResultDisplay.prototype = {
+ _init: function(provider) {
+ this.provider = provider;
+ this.actor = null;
+ this.selectionIndex = -1;
+ },
+
+ /**
+ * renderResults:
+ * @results: List of identifier strings
+ * @terms: List of search term strings
+ *
+ * Display the given search matches which resulted
+ * from the given terms. It's expected that not
+ * all results will fit in the space for the container
+ * actor; in this case, show as many as makes sense
+ * for your result type.
+ *
+ * The terms are useful for search match highlighting.
+ */
+ renderResults: function(results, terms) {
+ throw new Error("not implemented");
+ },
+
+ /**
+ * clear:
+ * Remove all results from this display and reset the selection index.
+ */
+ clear: function() {
+ this.actor.get_children().forEach(function (actor) { actor.destroy(); });
+ this.selectionIndex = -1;
+ },
+
+ /**
+ * getSelectionIndex:
+ *
+ * Returns the index of the selected actor, or -1 if none.
+ */
+ getSelectionIndex: function() {
+ return this.selectionIndex;
+ },
+
+ /**
+ * getVisibleResultCount:
+ *
+ * Returns: The number of actors visible.
+ */
+ getVisibleResultCount: function() {
+ throw new Error("not implemented");
+ },
+
+ /**
+ * selectIndex:
+ * @index: Integer index
+ *
+ * Move selection to the given index.
+ * Return true if successful, false if no more results
+ * available.
+ */
+ selectIndex: function() {
+ throw new Error("not implemented");
+ }
+};
+
+/**
+ * SearchProvider:
+ *
+ * Subclass this object to add a new result type
+ * to the search system, then call registerProvider()
+ * in SearchSystem with an instance.
+ */
+function SearchProvider(title) {
+ this._init(title);
+}
+
+SearchProvider.prototype = {
+ _init: function(title) {
+ this.title = title;
+ },
+
+ /**
+ * getInitialResultSet:
+ * @terms: Array of search terms, treated as logical OR
+ *
+ * Called when the user first begins a search (most likely
+ * therefore a single term of length one or two), or when
+ * a new term is added.
+ *
+ * Should return an array of result identifier strings representing
+ * items which match the given search terms. This
+ * is expected to be a substring match on the metadata for a given
+ * item. Ordering of returned results is up to the discretion of the provider,
+ * but you should follow these heruistics:
+ *
+ * * Put items which match multiple search terms before single matches
+ * * Put items which match on a prefix before non-prefix substring matches
+ *
+ * This function should be fast; do not perform unindexed full-text searches
+ * or network queries.
+ */
+ getInitialResultSet: function(terms) {
+ throw new Error("not implemented");
+ },
+
+ /**
+ * getSubsearchResultSet:
+ * @previousResults: Array of item identifiers
+ * @newTerms: Updated search terms
+ *
+ * Called when a search is performed which is a "subsearch" of
+ * the previous search; i.e. when every search term has exactly
+ * one corresponding term in the previous search which is a prefix
+ * of the new term.
+ *
+ * This allows search providers to only search through the previous
+ * result set, rather than possibly performing a full re-query.
+ */
+ getSubsearchResultSet: function(previousResults, newTerms) {
+ throw new Error("not implemented");
+ },
+
+ /**
+ * getResultInfo:
+ * @id: Result identifier string
+ *
+ * Return an object with 'id', 'name', (both strings) and 'icon' (Clutter.Texture)
+ * properties which describe the given search result.
+ */
+ getResultMeta: function(id) {
+ throw new Error("not implemented");
+ },
+
+ /**
+ * createResultContainer:
+ *
+ * Search providers may optionally override this to render their
+ * results in a custom fashion. The default implementation
+ * will create a vertical list.
+ *
+ * Returns: An instance of SearchResultDisplay.
+ */
+ createResultContainerActor: function() {
+ return null;
+ },
+
+ /**
+ * createResultActor:
+ * @resultMeta: Object with result metadata
+ * @terms: Array of search terms, should be used for highlighting
+ *
+ * Search providers may optionally override this to render a
+ * particular serch result in a custom fashion. The default
+ * implementation will show the icon next to the name.
+ *
+ * The actor should be an instance of St.Widget, with the style class
+ * 'dash-search-result-content'.
+ */
+ createResultActor: function(resultMeta, terms) {
+ return null;
+ },
+
+ /**
+ * activateResult:
+ * @id: Result identifier string
+ *
+ * Called when the user chooses a given result.
+ */
+ activateResult: function(id) {
+ throw new Error("not implemented");
+ },
+
+ /**
+ * expandSearch:
+ *
+ * Called when the user clicks on the header for this
+ * search section. Should typically launch an external program
+ * displaying search results for that item type.
+ */
+ expandSearch: function(terms) {
+ throw new Error("not implemented");
+ }
+}
+Signals.addSignalMethods(SearchProvider.prototype);
+
+function SearchSystem() {
+ this._init();
+}
+
+SearchSystem.prototype = {
+ _init: function() {
+ this._providers = [];
+ this.reset();
+ },
+
+ registerProvider: function (provider) {
+ this._providers.push(provider);
+ },
+
+ getProviders: function() {
+ return this._providers;
+ },
+
+ getTerms: function() {
+ return this._previousTerms;
+ },
+
+ reset: function() {
+ this._previousTerms = [];
+ this._previousResults = [];
+ },
+
+ updateSearch: function(searchString) {
+ searchString = searchString.replace(/^\s+/g, "").replace(/\s+$/g, "");
+ if (searchString == '')
+ return null;
+
+ let terms = searchString.split(/\s+/);
+ let isSubSearch = terms.length == this._previousTerms.length;
+ if (isSubSearch) {
+ for (let i = 0; i < terms.length; i++) {
+ if (terms[i].indexOf(this._previousTerms[i]) != 0) {
+ isSubSearch = false;
+ break;
+ }
+ }
+ }
+
+ let results = [];
+ if (isSubSearch) {
+ for (let i = 0; i < this._previousResults.length; i++) {
+ let [provider, previousResults] = this._previousResults[i];
+ let providerResults = provider.getSubsearchResultSet(previousResults, terms);
+ if (providerResults.length > 0)
+ results.push([provider, providerResults]);
+ }
+ } else {
+ for (let i = 0; i < this._providers.length; i++) {
+ let provider = this._providers[i];
+ let providerResults = provider.getInitialResultSet(terms);
+ if (providerResults.length > 0)
+ results.push([provider, providerResults]);
+ }
+ }
+
+ this._previousTerms = terms;
+ this._previousResults = results;
+
+ return results;
+ }
+}
+Signals.addSignalMethods(SearchSystem.prototype);
diff --git a/src/shell-app-system.c b/src/shell-app-system.c
index 0f61576..ffe3ccc 100644
--- a/src/shell-app-system.c
+++ b/src/shell-app-system.c
@@ -48,9 +48,7 @@ struct _ShellAppSystemPrivate {
GHashTable *app_id_to_info;
GHashTable *app_id_to_app;
- GHashTable *cached_menu_contents; /* <char *id, GSList<ShellAppInfo*>> */
- GSList *cached_app_menus; /* ShellAppMenuEntry */
-
+ GSList *cached_flattened_apps; /* ShellAppInfo */
GSList *cached_settings; /* ShellAppInfo */
gint app_monitor_id;
@@ -58,7 +56,6 @@ struct _ShellAppSystemPrivate {
guint app_change_timeout_id;
};
-static void free_appinfo_gslist (gpointer list);
static void shell_app_system_finalize (GObject *object);
static gboolean on_tree_changed (gpointer user_data);
static void on_tree_changed_cb (GMenuTree *tree, gpointer user_data);
@@ -83,6 +80,10 @@ struct _ShellAppInfo {
*/
guint refcount;
+ char *casefolded_name;
+ char *name_collation_key;
+ char *casefolded_description;
+
GMenuTreeItem *entry;
GKeyFile *keyfile;
@@ -104,6 +105,11 @@ shell_app_info_unref (ShellAppInfo *info)
{
if (--info->refcount > 0)
return;
+
+ g_free (info->casefolded_name);
+ g_free (info->name_collation_key);
+ g_free (info->casefolded_description);
+
switch (info->type)
{
case SHELL_APP_INFO_TYPE_ENTRY:
@@ -129,7 +135,7 @@ shell_app_info_new_from_tree_item (GMenuTreeItem *item)
if (!item)
return NULL;
- info = g_slice_alloc (sizeof (ShellAppInfo));
+ info = g_slice_alloc0 (sizeof (ShellAppInfo));
info->type = SHELL_APP_INFO_TYPE_ENTRY;
info->refcount = 1;
info->entry = gmenu_tree_item_ref (item);
@@ -141,7 +147,7 @@ shell_app_info_new_from_window (MetaWindow *window)
{
ShellAppInfo *info;
- info = g_slice_alloc (sizeof (ShellAppInfo));
+ info = g_slice_alloc0 (sizeof (ShellAppInfo));
info->type = SHELL_APP_INFO_TYPE_WINDOW;
info->refcount = 1;
info->window = g_object_ref (window);
@@ -159,7 +165,7 @@ shell_app_info_new_from_keyfile_take_ownership (GKeyFile *keyfile,
{
ShellAppInfo *info;
- info = g_slice_alloc (sizeof (ShellAppInfo));
+ info = g_slice_alloc0 (sizeof (ShellAppInfo));
info->type = SHELL_APP_INFO_TYPE_DESKTOP_FILE;
info->refcount = 1;
info->keyfile = keyfile;
@@ -167,29 +173,6 @@ shell_app_info_new_from_keyfile_take_ownership (GKeyFile *keyfile,
return info;
}
-static gpointer
-shell_app_menu_entry_copy (gpointer entryp)
-{
- ShellAppMenuEntry *entry;
- ShellAppMenuEntry *copy;
- entry = entryp;
- copy = g_new0 (ShellAppMenuEntry, 1);
- copy->name = g_strdup (entry->name);
- copy->id = g_strdup (entry->id);
- copy->icon = g_strdup (entry->icon);
- return copy;
-}
-
-static void
-shell_app_menu_entry_free (gpointer entryp)
-{
- ShellAppMenuEntry *entry = entryp;
- g_free (entry->name);
- g_free (entry->id);
- g_free (entry->icon);
- g_free (entry);
-}
-
static void shell_app_system_class_init(ShellAppSystemClass *klass)
{
GObjectClass *gobject_class = (GObjectClass *)klass;
@@ -225,9 +208,6 @@ shell_app_system_init (ShellAppSystem *self)
/* Key is owned by info */
priv->app_id_to_app = g_hash_table_new (g_str_hash, g_str_equal);
- priv->cached_menu_contents = g_hash_table_new_full (g_str_hash, g_str_equal,
- g_free, free_appinfo_gslist);
-
/* For now, we want to pick up Evince, Nautilus, etc. We'll
* handle NODISPLAY semantics at a higher level or investigate them
* case by case.
@@ -257,15 +237,12 @@ shell_app_system_finalize (GObject *object)
gmenu_tree_unref (priv->apps_tree);
gmenu_tree_unref (priv->settings_tree);
- g_hash_table_destroy (priv->cached_menu_contents);
-
g_hash_table_destroy (priv->app_id_to_info);
g_hash_table_destroy (priv->app_id_to_app);
- g_slist_foreach (priv->cached_app_menus, (GFunc)shell_app_menu_entry_free, NULL);
- g_slist_free (priv->cached_app_menus);
- priv->cached_app_menus = NULL;
-
+ g_slist_foreach (priv->cached_flattened_apps, (GFunc)shell_app_info_unref, NULL);
+ g_slist_free (priv->cached_flattened_apps);
+ priv->cached_flattened_apps = NULL;
g_slist_foreach (priv->cached_settings, (GFunc)shell_app_info_unref, NULL);
g_slist_free (priv->cached_settings);
priv->cached_settings = NULL;
@@ -273,60 +250,10 @@ shell_app_system_finalize (GObject *object)
G_OBJECT_CLASS (shell_app_system_parent_class)->finalize(object);
}
-static void
-free_appinfo_gslist (gpointer listp)
-{
- GSList *list = listp;
- g_slist_foreach (list, (GFunc) shell_app_info_unref, NULL);
- g_slist_free (list);
-}
-
-static void
-reread_directories (ShellAppSystem *self, GSList **cache, GMenuTree *tree)
-{
- GMenuTreeDirectory *trunk;
- GSList *entries;
- GSList *iter;
-
- trunk = gmenu_tree_get_root_directory (tree);
- entries = gmenu_tree_directory_get_contents (trunk);
-
- g_slist_foreach (*cache, (GFunc)shell_app_menu_entry_free, NULL);
- g_slist_free (*cache);
- *cache = NULL;
-
- for (iter = entries; iter; iter = iter->next)
- {
- GMenuTreeItem *item = iter->data;
-
- switch (gmenu_tree_item_get_type (item))
- {
- case GMENU_TREE_ITEM_DIRECTORY:
- {
- GMenuTreeDirectory *dir = iter->data;
- ShellAppMenuEntry *shell_entry = g_new0 (ShellAppMenuEntry, 1);
- shell_entry->name = g_strdup (gmenu_tree_directory_get_name (dir));
- shell_entry->id = g_strdup (gmenu_tree_directory_get_menu_id (dir));
- shell_entry->icon = g_strdup (gmenu_tree_directory_get_icon (dir));
-
- *cache = g_slist_prepend (*cache, shell_entry);
- }
- break;
- default:
- break;
- }
-
- gmenu_tree_item_unref (item);
- }
- *cache = g_slist_reverse (*cache);
-
- g_slist_free (entries);
- gmenu_tree_item_unref (trunk);
-}
-
static GSList *
gather_entries_recurse (ShellAppSystem *monitor,
GSList *apps,
+ GHashTable *unique,
GMenuTreeDirectory *root)
{
GSList *contents;
@@ -342,13 +269,17 @@ gather_entries_recurse (ShellAppSystem *monitor,
case GMENU_TREE_ITEM_ENTRY:
{
ShellAppInfo *app = shell_app_info_new_from_tree_item (item);
- apps = g_slist_prepend (apps, app);
+ if (!g_hash_table_lookup (unique, shell_app_info_get_id (app)))
+ {
+ apps = g_slist_prepend (apps, app);
+ g_hash_table_insert (unique, (char*)shell_app_info_get_id (app), app);
+ }
}
break;
case GMENU_TREE_ITEM_DIRECTORY:
{
GMenuTreeDirectory *dir = (GMenuTreeDirectory*)item;
- apps = gather_entries_recurse (monitor, apps, dir);
+ apps = gather_entries_recurse (monitor, apps, unique, dir);
}
break;
default:
@@ -365,6 +296,7 @@ gather_entries_recurse (ShellAppSystem *monitor,
static void
reread_entries (ShellAppSystem *self,
GSList **cache,
+ GHashTable *unique,
GMenuTree *tree)
{
GMenuTreeDirectory *trunk;
@@ -375,46 +307,40 @@ reread_entries (ShellAppSystem *self,
g_slist_free (*cache);
*cache = NULL;
- *cache = gather_entries_recurse (self, *cache, trunk);
+ *cache = gather_entries_recurse (self, *cache, unique, trunk);
gmenu_tree_item_unref (trunk);
}
static void
-cache_by_id (ShellAppSystem *self, GSList *apps, gboolean ref)
+cache_by_id (ShellAppSystem *self, GSList *apps)
{
GSList *iter;
for (iter = apps; iter; iter = iter->next)
{
ShellAppInfo *info = iter->data;
- if (ref)
- shell_app_info_ref (info);
+ shell_app_info_ref (info);
/* the name is owned by the info itself */
- g_hash_table_insert (self->priv->app_id_to_info, (char*)shell_app_info_get_id (info),
- info);
+ g_hash_table_replace (self->priv->app_id_to_info, (char*)shell_app_info_get_id (info),
+ info);
}
}
static void
reread_menus (ShellAppSystem *self)
{
- GSList *apps;
- GMenuTreeDirectory *trunk;
+ GHashTable *unique = g_hash_table_new (g_str_hash, g_str_equal);
- reread_directories (self, &(self->priv->cached_app_menus), self->priv->apps_tree);
+ reread_entries (self, &(self->priv->cached_flattened_apps), unique, self->priv->apps_tree);
+ g_hash_table_remove_all (unique);
+ reread_entries (self, &(self->priv->cached_settings), unique, self->priv->settings_tree);
+ g_hash_table_destroy (unique);
- reread_entries (self, &(self->priv->cached_settings), self->priv->settings_tree);
-
- /* Now loop over applications.menu and settings.menu, inserting each by desktop file
- * ID into a hash */
g_hash_table_remove_all (self->priv->app_id_to_info);
- trunk = gmenu_tree_get_root_directory (self->priv->apps_tree);
- apps = gather_entries_recurse (self, NULL, trunk);
- gmenu_tree_item_unref (trunk);
- cache_by_id (self, apps, FALSE);
- g_slist_free (apps);
- cache_by_id (self, self->priv->cached_settings, TRUE);
+
+ cache_by_id (self, self->priv->cached_flattened_apps);
+ cache_by_id (self, self->priv->cached_settings);
}
static gboolean
@@ -423,7 +349,6 @@ on_tree_changed (gpointer user_data)
ShellAppSystem *self = SHELL_APP_SYSTEM (user_data);
reread_menus (self);
- g_hash_table_remove_all (self->priv->cached_menu_contents);
g_signal_emit (self, signals[INSTALLED_CHANGED], 0);
@@ -469,21 +394,8 @@ shell_app_info_get_type (void)
return gtype;
}
-GType
-shell_app_menu_entry_get_type (void)
-{
- static GType gtype = G_TYPE_INVALID;
- if (gtype == G_TYPE_INVALID)
- {
- gtype = g_boxed_type_register_static ("ShellAppMenuEntry",
- shell_app_menu_entry_copy,
- shell_app_menu_entry_free);
- }
- return gtype;
-}
-
/**
- * shell_app_system_get_applications_for_menu:
+ * shell_app_system_get_flattened_apps:
*
* Traverses a toplevel menu, and returns all items under it. Nested items
* are flattened. This value is computed on initial call and cached thereafter
@@ -492,41 +404,9 @@ shell_app_menu_entry_get_type (void)
* Return value: (transfer none) (element-type ShellAppInfo): List of applications
*/
GSList *
-shell_app_system_get_applications_for_menu (ShellAppSystem *self,
- const char *menu)
+shell_app_system_get_flattened_apps (ShellAppSystem *self)
{
- GSList *apps;
-
- apps = g_hash_table_lookup (self->priv->cached_menu_contents, menu);
- if (!apps)
- {
- char *path;
- GMenuTreeDirectory *menu_entry;
- path = g_strdup_printf ("/%s", menu);
- menu_entry = gmenu_tree_get_directory_from_path (self->priv->apps_tree, path);
- g_free (path);
- g_assert (menu_entry != NULL);
-
- apps = gather_entries_recurse (self, NULL, menu_entry);
- g_hash_table_insert (self->priv->cached_menu_contents, g_strdup (menu), apps);
-
- gmenu_tree_item_unref (menu_entry);
- }
-
- return apps;
-}
-
-/**
- * shell_app_system_get_menus:
- *
- * Returns a list of toplevel #ShellAppMenuEntry items
- *
- * Return value: (transfer none) (element-type AppMenuEntry): List of toplevel menus
- */
-GSList *
-shell_app_system_get_menus (ShellAppSystem *monitor)
-{
- return monitor->priv->cached_app_menus;
+ return self->priv->cached_flattened_apps;
}
/**
@@ -711,6 +591,249 @@ shell_app_system_lookup_heuristic_basename (ShellAppSystem *system,
return NULL;
}
+typedef enum {
+ MATCH_NONE,
+ MATCH_MULTIPLE, /* Matches multiple terms */
+ MATCH_PREFIX, /* Strict prefix */
+ MATCH_SUBSTRING /* Not prefix, substring */
+} ShellAppInfoSearchMatch;
+
+static char *
+normalize_and_casefold (const char *str)
+{
+ char *normalized, *result;
+
+ if (str == NULL)
+ return NULL;
+
+ normalized = g_utf8_normalize (str, -1, G_NORMALIZE_ALL);
+ result = g_utf8_casefold (normalized, -1);
+ g_free (normalized);
+ return result;
+}
+
+static void
+shell_app_info_init_search_data (ShellAppInfo *info)
+{
+ const char *name;
+ const char *comment;
+
+ g_assert (info->type == SHELL_APP_INFO_TYPE_ENTRY);
+
+ name = gmenu_tree_entry_get_name ((GMenuTreeEntry*)info->entry);
+ info->casefolded_name = normalize_and_casefold (name);
+ info->name_collation_key = g_utf8_collate_key (name, -1);
+
+ comment = gmenu_tree_entry_get_comment ((GMenuTreeEntry*)info->entry);
+ info->casefolded_description = normalize_and_casefold (comment);
+}
+
+static ShellAppInfoSearchMatch
+shell_app_info_match_terms (ShellAppInfo *info,
+ GSList *terms)
+{
+ GSList *iter;
+ ShellAppInfoSearchMatch match;
+
+ if (G_UNLIKELY(!info->casefolded_name))
+ shell_app_info_init_search_data (info);
+
+ match = MATCH_NONE;
+ for (iter = terms; iter; iter = iter->next)
+ {
+ const char *term = iter->data;
+ const char *p;
+
+ p = strstr (info->casefolded_name, term);
+ if (p == info->casefolded_name)
+ {
+ if (match != MATCH_NONE)
+ return MATCH_MULTIPLE;
+ else
+ match = MATCH_PREFIX;
+ }
+ else if (p != NULL)
+ match = MATCH_SUBSTRING;
+
+ if (!info->casefolded_description)
+ continue;
+ p = strstr (info->casefolded_description, term);
+ if (p != NULL)
+ match = MATCH_SUBSTRING;
+ }
+ return match;
+}
+
+static gint
+shell_app_info_compare (gconstpointer a,
+ gconstpointer b,
+ gpointer data)
+{
+ ShellAppSystem *system = data;
+ const char *id_a = a;
+ const char *id_b = b;
+ ShellAppInfo *info_a = g_hash_table_lookup (system->priv->app_id_to_info, id_a);
+ ShellAppInfo *info_b = g_hash_table_lookup (system->priv->app_id_to_info, id_b);
+
+ return strcmp (info_a->name_collation_key, info_b->name_collation_key);
+}
+
+static GSList *
+sort_and_concat_results (ShellAppSystem *system,
+ GSList *multiple_matches,
+ GSList *prefix_matches,
+ GSList *substring_matches)
+{
+ multiple_matches = g_slist_sort_with_data (multiple_matches, shell_app_info_compare, system);
+ prefix_matches = g_slist_sort_with_data (prefix_matches, shell_app_info_compare, system);
+ substring_matches = g_slist_sort_with_data (substring_matches, shell_app_info_compare, system);
+ return g_slist_concat (multiple_matches, g_slist_concat (prefix_matches, substring_matches));
+}
+
+/**
+ * normalize_terms:
+ * @terms: (element-type utf8): Input search terms
+ *
+ * Returns: (element-type utf8) (transfer full): Unicode-normalized and lowercased terms
+ */
+static GSList *
+normalize_terms (GSList *terms)
+{
+ GSList *normalized_terms = NULL;
+ GSList *iter;
+ for (iter = terms; iter; iter = iter->next)
+ {
+ const char *term = iter->data;
+ normalized_terms = g_slist_prepend (normalized_terms, normalize_and_casefold (term));
+ }
+ return normalized_terms;
+}
+
+static inline void
+shell_app_system_do_match (ShellAppSystem *system,
+ ShellAppInfo *info,
+ GSList *terms,
+ GSList **multiple_results,
+ GSList **prefix_results,
+ GSList **substring_results)
+{
+ const char *id = shell_app_info_get_id (info);
+ ShellAppInfoSearchMatch match;
+
+ if (shell_app_info_get_is_nodisplay (info))
+ return;
+
+ match = shell_app_info_match_terms (info, terms);
+ switch (match)
+ {
+ case MATCH_NONE:
+ break;
+ case MATCH_MULTIPLE:
+ *multiple_results = g_slist_prepend (*multiple_results, (char *) id);
+ break;
+ case MATCH_PREFIX:
+ *prefix_results = g_slist_prepend (*prefix_results, (char *) id);
+ break;
+ case MATCH_SUBSTRING:
+ *substring_results = g_slist_prepend (*substring_results, (char *) id);
+ break;
+ }
+}
+
+static GSList *
+shell_app_system_initial_search_internal (ShellAppSystem *self,
+ GSList *terms,
+ GSList *source)
+{
+ GSList *multiple_results = NULL;
+ GSList *prefix_results = NULL;
+ GSList *substring_results = NULL;
+ GSList *iter;
+ GSList *normalized_terms = normalize_terms (terms);
+
+ for (iter = source; iter; iter = iter->next)
+ {
+ ShellAppInfo *info = iter->data;
+
+ shell_app_system_do_match (self, info, normalized_terms, &multiple_results, &prefix_results, &substring_results);
+ }
+ g_slist_foreach (normalized_terms, (GFunc)g_free, NULL);
+ g_slist_free (normalized_terms);
+
+ return sort_and_concat_results (self, multiple_results, prefix_results, substring_results);
+}
+
+/**
+ * shell_app_system_initial_search:
+ * @self: A #ShellAppSystem
+ * @prefs: %TRUE iff we should search preferences instead of apps
+ * @terms: (element-type utf8): List of terms, logical OR
+ *
+ * Search through applications for the given search terms. Note that returned
+ * strings are only valid until a return to the main loop.
+ *
+ * Returns: (transfer container) (element-type utf8): List of application identifiers
+ */
+GSList *
+shell_app_system_initial_search (ShellAppSystem *self,
+ gboolean prefs,
+ GSList *terms)
+{
+ return shell_app_system_initial_search_internal (self, terms,
+ prefs ? self->priv->cached_settings : self->priv->cached_flattened_apps);
+}
+
+/**
+ * shell_app_system_subsearch:
+ * @self: A #ShellAppSystem
+ * @prefs: %TRUE iff we should search preferences instead of apps
+ * @previous_results: (element-type utf8): List of previous results
+ * @terms: (element-type utf8): List of terms, logical OR
+ *
+ * Search through a previous result set; for more information, see
+ * js/ui/search.js. Note the value of @prefs must be
+ * the same as passed to shell_app_system_initial_search(). Note that returned
+ * strings are only valid until a return to the main loop.
+ *
+ * Returns: (transfer container) (element-type utf8): List of application identifiers
+ */
+GSList *
+shell_app_system_subsearch (ShellAppSystem *system,
+ gboolean prefs,
+ GSList *previous_results,
+ GSList *terms)
+{
+ GSList *iter;
+ GSList *multiple_results = NULL;
+ GSList *prefix_results = NULL;
+ GSList *substring_results = NULL;
+ GSList *normalized_terms = normalize_terms (terms);
+
+ /* Note prefs is deliberately ignored; both apps and prefs are in app_id_to_app,
+ * but we have the parameter for consistency and in case in the future
+ * they're not in the same data structure.
+ */
+
+ for (iter = previous_results; iter; iter = iter->next)
+ {
+ const char *id = iter->data;
+ ShellAppInfo *info;
+
+ info = g_hash_table_lookup (system->priv->app_id_to_info, id);
+ if (!info)
+ continue;
+
+ shell_app_system_do_match (system, info, normalized_terms, &multiple_results, &prefix_results, &substring_results);
+ }
+ g_slist_foreach (normalized_terms, (GFunc)g_free, NULL);
+ g_slist_free (normalized_terms);
+
+ /* Note that a shorter term might have matched as a prefix, but
+ when extended only as a substring, so we have to redo the
+ sort rather than reusing the existing ordering */
+ return sort_and_concat_results (system, multiple_results, prefix_results, substring_results);
+}
+
const char *
shell_app_info_get_id (ShellAppInfo *info)
{
diff --git a/src/shell-app-system.h b/src/shell-app-system.h
index 3e3bee1..d6c7581 100644
--- a/src/shell-app-system.h
+++ b/src/shell-app-system.h
@@ -37,18 +37,6 @@ struct _ShellAppSystemClass
GType shell_app_system_get_type (void) G_GNUC_CONST;
ShellAppSystem* shell_app_system_get_default(void);
-GSList *shell_app_system_get_applications_for_menu (ShellAppSystem *system, const char *menu);
-
-typedef struct _ShellAppMenuEntry ShellAppMenuEntry;
-
-struct _ShellAppMenuEntry {
- char *name;
- char *id;
- char *icon;
-};
-
-GType shell_app_menu_entry_get_type (void);
-
typedef struct _ShellAppInfo ShellAppInfo;
#define SHELL_TYPE_APP_INFO (shell_app_info_get_type ())
@@ -85,8 +73,17 @@ ShellApp *shell_app_system_lookup_heuristic_basename (ShellAppSystem *system, co
ShellAppInfo *shell_app_system_create_from_window (ShellAppSystem *system, MetaWindow *window);
-GSList *shell_app_system_get_menus (ShellAppSystem *system);
+GSList *shell_app_system_get_flattened_apps (ShellAppSystem *system);
GSList *shell_app_system_get_all_settings (ShellAppSystem *system);
+GSList *shell_app_system_initial_search (ShellAppSystem *system,
+ gboolean prefs,
+ GSList *terms);
+
+GSList *shell_app_system_subsearch (ShellAppSystem *system,
+ gboolean prefs,
+ GSList *previous_results,
+ GSList *terms);
+
#endif /* __SHELL_APP_SYSTEM_H__ */
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]