002afda503
Allow the prefix 'special:' applied to result IDs to mark results that should be always shown, even when they would overflow the maximum results cap. This will be used by epiphany for the special "Search the Web" result. https://bugzilla.gnome.org/show_bug.cgi?id=707055
318 lines
10 KiB
JavaScript
318 lines
10 KiB
JavaScript
// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
|
|
|
|
const GdkPixbuf = imports.gi.GdkPixbuf;
|
|
const Gio = imports.gi.Gio;
|
|
const GLib = imports.gi.GLib;
|
|
const Lang = imports.lang;
|
|
const St = imports.gi.St;
|
|
const Shell = imports.gi.Shell;
|
|
|
|
const Search = imports.ui.search;
|
|
|
|
const KEY_FILE_GROUP = 'Shell Search Provider';
|
|
|
|
const SearchProviderIface = <interface name="org.gnome.Shell.SearchProvider">
|
|
<method name="GetInitialResultSet">
|
|
<arg type="as" direction="in" />
|
|
<arg type="as" direction="out" />
|
|
</method>
|
|
<method name="GetSubsearchResultSet">
|
|
<arg type="as" direction="in" />
|
|
<arg type="as" direction="in" />
|
|
<arg type="as" direction="out" />
|
|
</method>
|
|
<method name="GetResultMetas">
|
|
<arg type="as" direction="in" />
|
|
<arg type="aa{sv}" direction="out" />
|
|
</method>
|
|
<method name="ActivateResult">
|
|
<arg type="s" direction="in" />
|
|
</method>
|
|
</interface>;
|
|
|
|
const SearchProvider2Iface = <interface name="org.gnome.Shell.SearchProvider2">
|
|
<method name="GetInitialResultSet">
|
|
<arg type="as" direction="in" />
|
|
<arg type="as" direction="out" />
|
|
</method>
|
|
<method name="GetSubsearchResultSet">
|
|
<arg type="as" direction="in" />
|
|
<arg type="as" direction="in" />
|
|
<arg type="as" direction="out" />
|
|
</method>
|
|
<method name="GetResultMetas">
|
|
<arg type="as" direction="in" />
|
|
<arg type="aa{sv}" direction="out" />
|
|
</method>
|
|
<method name="ActivateResult">
|
|
<arg type="s" direction="in" />
|
|
<arg type="as" direction="in" />
|
|
<arg type="u" direction="in" />
|
|
</method>
|
|
<method name="LaunchSearch">
|
|
<arg type="as" direction="in" />
|
|
<arg type="u" direction="in" />
|
|
</method>
|
|
</interface>;
|
|
|
|
var SearchProviderProxy = Gio.DBusProxy.makeProxyWrapper(SearchProviderIface);
|
|
var SearchProvider2Proxy = Gio.DBusProxy.makeProxyWrapper(SearchProvider2Iface);
|
|
|
|
function loadRemoteSearchProviders(addProviderCallback) {
|
|
let objectPaths = {};
|
|
let loadedProviders = [];
|
|
|
|
function loadRemoteSearchProvider(file) {
|
|
let keyfile = new GLib.KeyFile();
|
|
let path = file.get_path();
|
|
|
|
try {
|
|
keyfile.load_from_file(path, 0);
|
|
} catch(e) {
|
|
return;
|
|
}
|
|
|
|
if (!keyfile.has_group(KEY_FILE_GROUP))
|
|
return;
|
|
|
|
let remoteProvider;
|
|
try {
|
|
let group = KEY_FILE_GROUP;
|
|
let busName = keyfile.get_string(group, 'BusName');
|
|
let objectPath = keyfile.get_string(group, 'ObjectPath');
|
|
|
|
if (objectPaths[objectPath])
|
|
return;
|
|
|
|
let appInfo = null;
|
|
try {
|
|
let desktopId = keyfile.get_string(group, 'DesktopId');
|
|
appInfo = Gio.DesktopAppInfo.new(desktopId);
|
|
} catch (e) {
|
|
log('Ignoring search provider ' + path + ': missing DesktopId');
|
|
return;
|
|
}
|
|
|
|
let version = '1';
|
|
try {
|
|
version = keyfile.get_string(group, 'Version');
|
|
} catch (e) {
|
|
// ignore error
|
|
}
|
|
|
|
if (version >= 2)
|
|
remoteProvider = new RemoteSearchProvider2(appInfo, busName, objectPath);
|
|
else
|
|
remoteProvider = new RemoteSearchProvider(appInfo, busName, objectPath);
|
|
|
|
objectPaths[objectPath] = remoteProvider;
|
|
loadedProviders.push(remoteProvider);
|
|
} catch(e) {
|
|
log('Failed to add search provider %s: %s'.format(path, e.toString()));
|
|
}
|
|
}
|
|
|
|
let dataDirs = GLib.get_system_data_dirs();
|
|
dataDirs.forEach(function(dataDir) {
|
|
let path = GLib.build_filenamev([dataDir, 'gnome-shell', 'search-providers']);
|
|
let dir = Gio.File.new_for_path(path);
|
|
let fileEnum;
|
|
try {
|
|
fileEnum = dir.enumerate_children('standard::name,standard::type',
|
|
Gio.FileQueryInfoFlags.NONE, null);
|
|
} catch (e) {
|
|
fileEnum = null;
|
|
}
|
|
if (fileEnum != null) {
|
|
let info;
|
|
while ((info = fileEnum.next_file(null)))
|
|
loadRemoteSearchProvider(fileEnum.get_child(info));
|
|
}
|
|
});
|
|
|
|
let searchSettings = new Gio.Settings({ schema: Search.SEARCH_PROVIDERS_SCHEMA });
|
|
let sortOrder = searchSettings.get_strv('sort-order');
|
|
|
|
// Special case gnome-control-center to be always active and always first
|
|
sortOrder.unshift('gnome-control-center.desktop');
|
|
|
|
loadedProviders.sort(function(providerA, providerB) {
|
|
let idxA, idxB;
|
|
let appIdA, appIdB;
|
|
|
|
appIdA = providerA.appInfo.get_id();
|
|
appIdB = providerB.appInfo.get_id();
|
|
|
|
idxA = sortOrder.indexOf(appIdA);
|
|
idxB = sortOrder.indexOf(appIdB);
|
|
|
|
// if no provider is found in the order, use alphabetical order
|
|
if ((idxA == -1) && (idxB == -1)) {
|
|
let nameA = providerA.appInfo.get_name();
|
|
let nameB = providerB.appInfo.get_name();
|
|
|
|
return GLib.utf8_collate(nameA, nameB);
|
|
}
|
|
|
|
// if providerA isn't found, it's sorted after providerB
|
|
if (idxA == -1)
|
|
return 1;
|
|
|
|
// if providerB isn't found, it's sorted after providerA
|
|
if (idxB == -1)
|
|
return -1;
|
|
|
|
// finally, if both providers are found, return their order in the list
|
|
return (idxA - idxB);
|
|
});
|
|
|
|
loadedProviders.forEach(addProviderCallback);
|
|
}
|
|
|
|
const RemoteSearchProvider = new Lang.Class({
|
|
Name: 'RemoteSearchProvider',
|
|
|
|
_init: function(appInfo, dbusName, dbusPath, proxyType) {
|
|
if (!proxyType)
|
|
proxyType = SearchProviderProxy;
|
|
|
|
this.proxy = new proxyType(Gio.DBus.session,
|
|
dbusName, dbusPath, Lang.bind(this, this._onProxyConstructed));
|
|
|
|
this.appInfo = appInfo;
|
|
this.id = appInfo.get_id();
|
|
this.isRemoteProvider = true;
|
|
|
|
this._cancellable = new Gio.Cancellable();
|
|
},
|
|
|
|
_onProxyConstructed: function(proxy) {
|
|
// Do nothing
|
|
},
|
|
|
|
createIcon: function(size, meta) {
|
|
let gicon;
|
|
if (meta['icon']) {
|
|
gicon = Gio.icon_deserialize(meta['icon']);
|
|
} else if (meta['gicon']) {
|
|
gicon = Gio.icon_new_for_string(meta['gicon']);
|
|
} else if (meta['icon-data']) {
|
|
let [width, height, rowStride, hasAlpha,
|
|
bitsPerSample, nChannels, data] = meta['icon-data'];
|
|
gicon = Shell.util_create_pixbuf_from_data(data, GdkPixbuf.Colorspace.RGB, hasAlpha,
|
|
bitsPerSample, width, height, rowStride);
|
|
}
|
|
|
|
return new St.Icon({ gicon: gicon,
|
|
icon_size: size });
|
|
},
|
|
|
|
filterResults: function(results, maxNumber) {
|
|
if (results.length <= maxNumber)
|
|
return results;
|
|
|
|
let regularResults = results.filter(function(r) { return !r.startsWith('special:'); });
|
|
let specialResults = results.filter(function(r) { return r.startsWith('special:'); });
|
|
|
|
return regularResults.slice(0, maxNumber).concat(specialResults.slice(0, maxNumber));
|
|
},
|
|
|
|
_getResultsFinished: function(results, error) {
|
|
if (error)
|
|
return;
|
|
this.searchSystem.setResults(this, results[0]);
|
|
},
|
|
|
|
getInitialResultSet: function(terms) {
|
|
this._cancellable.cancel();
|
|
this._cancellable.reset();
|
|
try {
|
|
this.proxy.GetInitialResultSetRemote(terms,
|
|
Lang.bind(this, this._getResultsFinished),
|
|
this._cancellable);
|
|
} catch(e) {
|
|
log('Error calling GetInitialResultSet for provider %s: %s'.format(this.id, e.toString()));
|
|
this.searchSystem.setResults(this, []);
|
|
}
|
|
},
|
|
|
|
getSubsearchResultSet: function(previousResults, newTerms) {
|
|
this._cancellable.cancel();
|
|
this._cancellable.reset();
|
|
try {
|
|
this.proxy.GetSubsearchResultSetRemote(previousResults, newTerms,
|
|
Lang.bind(this, this._getResultsFinished),
|
|
this._cancellable);
|
|
} catch(e) {
|
|
log('Error calling GetSubsearchResultSet for provider %s: %s'.format(this.id, e.toString()));
|
|
this.searchSystem.setResults(this, []);
|
|
}
|
|
},
|
|
|
|
_getResultMetasFinished: function(results, error, callback) {
|
|
if (error) {
|
|
callback([]);
|
|
return;
|
|
}
|
|
let metas = results[0];
|
|
let resultMetas = [];
|
|
for (let i = 0; i < metas.length; i++) {
|
|
for (let prop in metas[i]) {
|
|
// we can use the serialized icon variant directly
|
|
if (prop != 'icon')
|
|
metas[i][prop] = metas[i][prop].deep_unpack();
|
|
}
|
|
|
|
resultMetas.push({ id: metas[i]['id'],
|
|
name: metas[i]['name'],
|
|
description: metas[i]['description'],
|
|
createIcon: Lang.bind(this,
|
|
this.createIcon, metas[i]) });
|
|
}
|
|
callback(resultMetas);
|
|
},
|
|
|
|
getResultMetas: function(ids, callback) {
|
|
this._cancellable.cancel();
|
|
this._cancellable.reset();
|
|
try {
|
|
this.proxy.GetResultMetasRemote(ids,
|
|
Lang.bind(this, this._getResultMetasFinished, callback),
|
|
this._cancellable);
|
|
} catch(e) {
|
|
log('Error calling GetResultMetas for provider %s: %s'.format(this.id, e.toString()));
|
|
callback([]);
|
|
}
|
|
},
|
|
|
|
activateResult: function(id) {
|
|
this.proxy.ActivateResultRemote(id);
|
|
},
|
|
|
|
launchSearch: function(terms) {
|
|
// the provider is not compatible with the new version of the interface, launch
|
|
// the app itself but warn so we can catch the error in logs
|
|
log('Search provider ' + this.appInfo.get_id() + ' does not implement LaunchSearch');
|
|
this.appInfo.launch([], global.create_app_launch_context());
|
|
}
|
|
});
|
|
|
|
const RemoteSearchProvider2 = new Lang.Class({
|
|
Name: 'RemoteSearchProvider2',
|
|
Extends: RemoteSearchProvider,
|
|
|
|
_init: function(appInfo, dbusName, dbusPath) {
|
|
this.parent(appInfo, dbusName, dbusPath, SearchProvider2Proxy);
|
|
|
|
this.canLaunchSearch = true;
|
|
},
|
|
|
|
activateResult: function(id, terms) {
|
|
this.proxy.ActivateResultRemote(id, terms, global.get_current_time());
|
|
},
|
|
|
|
launchSearch: function(terms) {
|
|
this.proxy.LaunchSearchRemote(terms, global.get_current_time());
|
|
}
|
|
});
|