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
This commit is contained in:
Colin Walters 2009-11-29 17:45:30 -05:00
parent f5f92b2e79
commit b7646d18ae
10 changed files with 1163 additions and 566 deletions

View File

@ -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 {

View File

@ -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));
}
}

View File

@ -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);

View File

@ -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);

View File

@ -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");
}
};

View File

@ -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);

View File

@ -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,
PlaceSearchProvider.prototype = {
__proto__: Search.SearchProvider.prototype,
_init : function(placeInfo) {
GenericDisplay.GenericDisplayItem.prototype._init.call(this);
this._info = placeInfo;
this._setItemInfo(placeInfo.name, '');
_init: function() {
Search.SearchProvider.prototype._init.call(this, _("PLACES"));
},
//// Public method overrides ////
// Opens an application represented by this display item.
launch : 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) };
},
shellWorkspaceLaunch: function() {
this._info.launch();
activateResult: function(id) {
let placeInfo = Main.placesManager.lookupPlaceById(id);
placeInfo.launch();
},
//// Protected method overrides ////
// Returns an icon for the item.
_createIcon: function() {
return this._info.iconFactory(GenericDisplay.ITEM_DISPLAY_ICON_SIZE);
_compareResultMeta: function (idA, idB) {
let infoA = Main.placesManager.lookupPlaceById(idA);
let infoB = Main.placesManager.lookupPlaceById(idB);
return infoA.name.localeCompare(infoB.name);
},
// Returns a preview icon for the item.
_createPreviewIcon: function() {
return this._info.iconFactory(GenericDisplay.PREVIEW_ICON_SIZE);
_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);
}
multipleResults.sort(this._compareResultMeta);
prefixResults.sort(this._compareResultMeta);
substringResults.sort(this._compareResultMeta);
return multipleResults.concat(prefixResults.concat(substringResults));
},
getInitialResultSet: function(terms) {
let places = Main.placesManager.getAllPlaces();
return this._searchPlaces(places, terms);
},
getSubsearchResultSet: function(previousResults, terms) {
let places = previousResults.map(function (id) { return Main.placesManager.lookupPlaceById(id); });
return this._searchPlaces(places, terms);
}
};
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;
}));
},
//// 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;
}
this._stale = false;
return false;
},
// 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));
},
// 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);
}
};

272
js/ui/search.js Normal file
View File

@ -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);

View File

@ -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)
{

View File

@ -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__ */