gnome-shell/js/ui/remoteSearch.js
Felipe Borges 0be20aba88 search: Stop special-casing GNOME Settings on search
See https://gitlab.gnome.org/GNOME/gnome-control-center/-/issues/1282#note_1617502

From the chat:
<aday> i think it's fine to allow people to disable settings search.
<aday> the issue i see here is that the shell doesn't follow the search
results order. settings is always at the top of the results list
<aday> if settings search is to appear in the list, then that should
work as you'd expect

We are also removing the special-casing in the GNOME Settings side,
see https://gitlab.gnome.org/GNOME/gnome-control-center/-/merge_requests/1555

Part-of: <https://gitlab.gnome.org/GNOME/gnome-shell/-/merge_requests/2584>
2022-12-15 19:02:02 +00:00

331 lines
10 KiB
JavaScript

// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
/* exported loadRemoteSearchProviders */
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);
/**
* loadRemoteSearchProviders:
*
* @param {Gio.Settings} searchSettings - search settings
* @returns {RemoteSearchProvider[]} - the list of remote providers
*/
function loadRemoteSearchProviders(searchSettings) {
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 ${path}: ${e}`);
}
}
if (searchSettings.get_boolean('disable-external'))
return [];
for (const {dir} of FileUtils.collectFromDatadirs('search-providers', false))
loadRemoteSearchProvider(dir);
let sortOrder = searchSettings.get_strv('sort-order');
const disabled = searchSettings.get_strv('disabled');
const enabled = searchSettings.get_strv('enabled');
loadedProviders = loadedProviders.filter(provider => {
let appId = provider.appInfo.get_id();
if (provider.defaultEnabled)
return !disabled.includes(appId);
else
return enabled.includes(appId);
});
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;
});
return loadedProviders;
}
var RemoteSearchProvider = class {
constructor(appInfo, dbusName, dbusPath, autoStart, proxyInfo) {
if (!proxyInfo)
proxyInfo = SearchProviderProxyInfo;
let gFlags = Gio.DBusProxyFlags.DO_NOT_LOAD_PROPERTIES;
if (autoStart)
gFlags |= Gio.DBusProxyFlags.DO_NOT_AUTO_START_AT_CONSTRUCTION;
else
gFlags |= 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,
gFlags,
});
this.proxy.init_async(GLib.PRIORITY_DEFAULT, 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']) {
const [
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, 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));
}
async getInitialResultSet(terms, cancellable) {
try {
const [results] = await this.proxy.GetInitialResultSetAsync(terms, cancellable);
return results;
} catch (error) {
if (!error.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
log(`Received error from D-Bus search provider ${this.id}: ${error}`);
return [];
}
}
async getSubsearchResultSet(previousResults, newTerms, cancellable) {
try {
const [results] = await this.proxy.GetSubsearchResultSetAsync(previousResults, newTerms, cancellable);
return results;
} catch (error) {
if (!error.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
log(`Received error from D-Bus search provider ${this.id}: ${error}`);
return [];
}
}
async getResultMetas(ids, cancellable) {
let metas;
try {
[metas] = await this.proxy.GetResultMetasAsync(ids, cancellable);
} catch (error) {
if (!error.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
log(`Received error from D-Bus search provider ${this.id} during GetResultMetas: ${error}`);
return [];
}
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].deepUnpack();
}
resultMetas.push({
id: metas[i]['id'],
name: metas[i]['name'],
description: metas[i]['description'],
createIcon: size => this.createIcon(size, metas[i]),
clipboardText: metas[i]['clipboardText'],
});
}
return resultMetas;
}
activateResult(id) {
this.proxy.ActivateResultAsync(id).catch(logError);
}
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.ActivateResultAsync(
id, terms, global.get_current_time()).catch(logError);
}
launchSearch(terms) {
this.proxy.LaunchSearchAsync(
terms, global.get_current_time()).catch(logError);
}
};