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:
parent
f5f92b2e79
commit
b7646d18ae
@ -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 {
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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,19 +135,12 @@ 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];
|
||||
let apps = this._appSystem.get_flattened_apps();
|
||||
for (let i = 0; i < apps.length; i++) {
|
||||
let app = apps[i];
|
||||
this._addApp(app);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this._appsStale = false;
|
||||
return false;
|
||||
@ -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);
|
||||
|
521
js/ui/dash.js
521
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);
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
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");
|
||||
}
|
||||
};
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
},
|
||||
|
||||
function PlaceDisplay(flags) {
|
||||
this._init(flags);
|
||||
getSubsearchResultSet: function(previousResults, terms) {
|
||||
let places = previousResults.map(function (id) { return Main.placesManager.lookupPlaceById(id); });
|
||||
return this._searchPlaces(places, terms);
|
||||
}
|
||||
}
|
||||
|
||||
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
272
js/ui/search.js
Normal 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);
|
@ -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);
|
||||
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,23 +307,22 @@ 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);
|
||||
/* 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),
|
||||
g_hash_table_replace (self->priv->app_id_to_info, (char*)shell_app_info_get_id (info),
|
||||
info);
|
||||
}
|
||||
}
|
||||
@ -399,22 +330,17 @@ cache_by_id (ShellAppSystem *self, GSList *apps, gboolean ref)
|
||||
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)
|
||||
{
|
||||
|
@ -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__ */
|
||||
|
Loading…
Reference in New Issue
Block a user