3cbdf4f9a5
Some apps may be configured as NoDisplay=true, but still install a search provider. Like we do elsewhere, exclude them from showing up in the UI. https://gitlab.gnome.org/GNOME/gnome-shell/merge_requests/568
336 lines
11 KiB
JavaScript
336 lines
11 KiB
JavaScript
// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
|
|
|
|
const { GdkPixbuf, Gio, GLib, Shell, St } = imports.gi;
|
|
|
|
const FileUtils = imports.misc.fileUtils;
|
|
|
|
const KEY_FILE_GROUP = 'Shell Search Provider';
|
|
|
|
const SearchProviderIface = `
|
|
<node>
|
|
<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>
|
|
</node>`;
|
|
|
|
const SearchProvider2Iface = `
|
|
<node>
|
|
<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>
|
|
</node>`;
|
|
|
|
var SearchProviderProxyInfo = Gio.DBusInterfaceInfo.new_for_xml(SearchProviderIface);
|
|
var SearchProvider2ProxyInfo = Gio.DBusInterfaceInfo.new_for_xml(SearchProvider2Iface);
|
|
|
|
function loadRemoteSearchProviders(searchSettings, callback) {
|
|
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);
|
|
if (!appInfo.should_show())
|
|
return;
|
|
} catch (e) {
|
|
log('Ignoring search provider ' + path + ': missing DesktopId');
|
|
return;
|
|
}
|
|
|
|
let autoStart = true;
|
|
try {
|
|
autoStart = keyfile.get_boolean(group, 'AutoStart');
|
|
} catch(e) {
|
|
// ignore error
|
|
}
|
|
|
|
let version = '1';
|
|
try {
|
|
version = keyfile.get_string(group, 'Version');
|
|
} catch (e) {
|
|
// ignore error
|
|
}
|
|
|
|
if (version >= 2)
|
|
remoteProvider = new RemoteSearchProvider2(appInfo, busName, objectPath, autoStart);
|
|
else
|
|
remoteProvider = new RemoteSearchProvider(appInfo, busName, objectPath, autoStart);
|
|
|
|
remoteProvider.defaultEnabled = true;
|
|
try {
|
|
remoteProvider.defaultEnabled = !keyfile.get_boolean(group, 'DefaultDisabled');
|
|
} catch(e) {
|
|
// ignore error
|
|
}
|
|
|
|
objectPaths[objectPath] = remoteProvider;
|
|
loadedProviders.push(remoteProvider);
|
|
} catch(e) {
|
|
log('Failed to add search provider %s: %s'.format(path, e.toString()));
|
|
}
|
|
}
|
|
|
|
if (searchSettings.get_boolean('disable-external')) {
|
|
callback([]);
|
|
return;
|
|
}
|
|
|
|
FileUtils.collectFromDatadirs('search-providers', false, loadRemoteSearchProvider);
|
|
|
|
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 = loadedProviders.filter(provider => {
|
|
let appId = provider.appInfo.get_id();
|
|
|
|
if (provider.defaultEnabled) {
|
|
let disabled = searchSettings.get_strv('disabled');
|
|
return disabled.indexOf(appId) == -1;
|
|
} else {
|
|
let enabled = searchSettings.get_strv('enabled');
|
|
return enabled.indexOf(appId) != -1;
|
|
}
|
|
});
|
|
|
|
loadedProviders.sort((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);
|
|
});
|
|
|
|
callback(loadedProviders);
|
|
}
|
|
|
|
var RemoteSearchProvider = class {
|
|
constructor(appInfo, dbusName, dbusPath, autoStart, proxyInfo) {
|
|
if (!proxyInfo)
|
|
proxyInfo = SearchProviderProxyInfo;
|
|
|
|
let g_flags = Gio.DBusProxyFlags.DO_NOT_LOAD_PROPERTIES;
|
|
if (autoStart)
|
|
g_flags |= Gio.DBusProxyFlags.DO_NOT_AUTO_START_AT_CONSTRUCTION;
|
|
else
|
|
g_flags |= Gio.DBusProxyFlags.DO_NOT_AUTO_START;
|
|
|
|
this.proxy = new Gio.DBusProxy({ g_bus_type: Gio.BusType.SESSION,
|
|
g_name: dbusName,
|
|
g_object_path: dbusPath,
|
|
g_interface_info: proxyInfo,
|
|
g_interface_name: proxyInfo.name,
|
|
g_flags });
|
|
this.proxy.init_async(GLib.PRIORITY_DEFAULT, null, null);
|
|
|
|
this.appInfo = appInfo;
|
|
this.id = appInfo.get_id();
|
|
this.isRemoteProvider = true;
|
|
this.canLaunchSearch = false;
|
|
}
|
|
|
|
createIcon(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(results, maxNumber) {
|
|
if (results.length <= maxNumber)
|
|
return results;
|
|
|
|
let regularResults = results.filter(r => !r.startsWith('special:'));
|
|
let specialResults = results.filter(r => r.startsWith('special:'));
|
|
|
|
return regularResults.slice(0, maxNumber).concat(specialResults.slice(0, maxNumber));
|
|
}
|
|
|
|
_getResultsFinished(results, error, callback) {
|
|
if (error) {
|
|
if (error.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
|
|
return;
|
|
|
|
log('Received error from DBus search provider %s: %s'.format(this.id, String(error)));
|
|
callback([]);
|
|
return;
|
|
}
|
|
|
|
callback(results[0]);
|
|
}
|
|
|
|
getInitialResultSet(terms, callback, cancellable) {
|
|
this.proxy.GetInitialResultSetRemote(terms,
|
|
(results, error) => {
|
|
this._getResultsFinished(results, error, callback);
|
|
},
|
|
cancellable);
|
|
}
|
|
|
|
getSubsearchResultSet(previousResults, newTerms, callback, cancellable) {
|
|
this.proxy.GetSubsearchResultSetRemote(previousResults, newTerms,
|
|
(results, error) => {
|
|
this._getResultsFinished(results, error, callback);
|
|
},
|
|
cancellable);
|
|
}
|
|
|
|
_getResultMetasFinished(results, error, callback) {
|
|
if (error) {
|
|
if (!error.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
|
|
log('Received error from DBus search provider %s during GetResultMetas: %s'.format(this.id, String(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: size => {
|
|
return this.createIcon(size, metas[i]);
|
|
},
|
|
clipboardText: metas[i]['clipboardText'] });
|
|
}
|
|
callback(resultMetas);
|
|
}
|
|
|
|
getResultMetas(ids, callback, cancellable) {
|
|
this.proxy.GetResultMetasRemote(ids,
|
|
(results, error) => {
|
|
this._getResultMetasFinished(results, error, callback);
|
|
},
|
|
cancellable);
|
|
}
|
|
|
|
activateResult(id) {
|
|
this.proxy.ActivateResultRemote(id);
|
|
}
|
|
|
|
launchSearch(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(0, -1));
|
|
}
|
|
};
|
|
|
|
var RemoteSearchProvider2 = class extends RemoteSearchProvider {
|
|
constructor(appInfo, dbusName, dbusPath, autoStart) {
|
|
super(appInfo, dbusName, dbusPath, autoStart, SearchProvider2ProxyInfo);
|
|
|
|
this.canLaunchSearch = true;
|
|
}
|
|
|
|
activateResult(id, terms) {
|
|
this.proxy.ActivateResultRemote(id, terms, global.get_current_time());
|
|
}
|
|
|
|
launchSearch(terms) {
|
|
this.proxy.LaunchSearchRemote(terms, global.get_current_time());
|
|
}
|
|
};
|