fa3284232f
The documentation indicates that they are optional, so let us make the code behave accordingly. https://bugzilla.gnome.org/show_bug.cgi?id=719965
322 lines
11 KiB
JavaScript
322 lines
11 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 = null;
|
|
let icon = null;
|
|
|
|
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);
|
|
}
|
|
|
|
if (gicon)
|
|
icon = new St.Icon({ gicon: gicon,
|
|
icon_size: size });
|
|
return icon;
|
|
},
|
|
|
|
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());
|
|
}
|
|
});
|