407 lines
12 KiB
JavaScript
407 lines
12 KiB
JavaScript
|
/**
|
||
|
* V-Shell (Vertical Workspaces)
|
||
|
* extensionsSearchProvider.js
|
||
|
*
|
||
|
* @author GdH <G-dH@github.com>
|
||
|
* @copyright 2022 - 2023
|
||
|
* @license GPL-3.0
|
||
|
*/
|
||
|
|
||
|
'use strict';
|
||
|
|
||
|
import GLib from 'gi://GLib';
|
||
|
import St from 'gi://St';
|
||
|
import Gio from 'gi://Gio';
|
||
|
import Shell from 'gi://Shell';
|
||
|
import GObject from 'gi://GObject';
|
||
|
import Clutter from 'gi://Clutter';
|
||
|
|
||
|
import * as Main from 'resource:///org/gnome/shell/ui/main.js';
|
||
|
|
||
|
const ExtensionState = {
|
||
|
1: 'ENABLED',
|
||
|
2: 'DISABLED',
|
||
|
3: 'ERROR',
|
||
|
4: 'INCOMPATIBLE',
|
||
|
5: 'DOWNLOADING',
|
||
|
6: 'INITIALIZED',
|
||
|
7: 'DISABLING',
|
||
|
8: 'ENABLING',
|
||
|
};
|
||
|
|
||
|
let Me;
|
||
|
let opt;
|
||
|
// gettext
|
||
|
let _;
|
||
|
let _toggleTimeout;
|
||
|
|
||
|
// prefix helps to eliminate results from other search providers
|
||
|
// so it needs to be something less common
|
||
|
// needs to be accessible from vw module
|
||
|
export const PREFIX = 'eq//';
|
||
|
|
||
|
export class ExtensionsSearchProviderModule {
|
||
|
// export for other modules
|
||
|
static _PREFIX = PREFIX;
|
||
|
constructor(me) {
|
||
|
Me = me;
|
||
|
opt = Me.opt;
|
||
|
_ = Me.gettext;
|
||
|
|
||
|
this._firstActivation = true;
|
||
|
this.moduleEnabled = false;
|
||
|
this._extensionsSearchProvider = null;
|
||
|
this._enableTimeoutId = 0;
|
||
|
}
|
||
|
|
||
|
cleanGlobals() {
|
||
|
Me = null;
|
||
|
opt = null;
|
||
|
_ = null;
|
||
|
}
|
||
|
|
||
|
update(reset) {
|
||
|
if (_toggleTimeout)
|
||
|
GLib.source_remove(_toggleTimeout);
|
||
|
|
||
|
this.moduleEnabled = opt.get('extensionsSearchProviderModule');
|
||
|
|
||
|
reset = reset || !this.moduleEnabled;
|
||
|
|
||
|
if (reset && !this._firstActivation) {
|
||
|
this._disableModule();
|
||
|
} else if (!reset) {
|
||
|
this._firstActivation = false;
|
||
|
this._activateModule();
|
||
|
}
|
||
|
if (reset && this._firstActivation)
|
||
|
console.debug(' ExtensionsSearchProviderModule - Keeping untouched');
|
||
|
}
|
||
|
|
||
|
_activateModule() {
|
||
|
// delay because Fedora had problem to register a new provider soon after Shell restarts
|
||
|
this._enableTimeoutId = GLib.timeout_add(
|
||
|
GLib.PRIORITY_DEFAULT,
|
||
|
2000,
|
||
|
() => {
|
||
|
if (!this._extensionsSearchProvider) {
|
||
|
this._extensionsSearchProvider = new extensionsSearchProvider(opt);
|
||
|
this._getOverviewSearchResult()._registerProvider(this._extensionsSearchProvider);
|
||
|
}
|
||
|
this._enableTimeoutId = 0;
|
||
|
return GLib.SOURCE_REMOVE;
|
||
|
}
|
||
|
);
|
||
|
console.debug(' ExtensionsSearchProviderModule - Activated');
|
||
|
}
|
||
|
|
||
|
_disableModule() {
|
||
|
if (this._enableTimeoutId) {
|
||
|
GLib.source_remove(this._enableTimeoutId);
|
||
|
this._enableTimeoutId = 0;
|
||
|
}
|
||
|
|
||
|
if (this._extensionsSearchProvider) {
|
||
|
this._getOverviewSearchResult()._unregisterProvider(this._extensionsSearchProvider);
|
||
|
this._extensionsSearchProvider = null;
|
||
|
}
|
||
|
|
||
|
|
||
|
console.debug(' ExtensionsSearchProviderModule - Disabled');
|
||
|
}
|
||
|
|
||
|
_getOverviewSearchResult() {
|
||
|
return Main.overview._overview.controls._searchController._searchResults;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
class extensionsSearchProvider {
|
||
|
constructor() {
|
||
|
this.id = 'extensions';
|
||
|
const appSystem = Shell.AppSystem.get_default();
|
||
|
let appInfo = appSystem.lookup_app('com.matjakeman.ExtensionManager.desktop')?.get_app_info();
|
||
|
if (!appInfo)
|
||
|
appInfo = appSystem.lookup_app('org.gnome.Extensions.desktop')?.get_app_info();
|
||
|
if (!appInfo)
|
||
|
appInfo = Gio.AppInfo.create_from_commandline('/usr/bin/gnome-extensions-app', 'Extensions', null);
|
||
|
appInfo.get_description = () => _('Search extensions');
|
||
|
appInfo.get_name = () => _('Extensions');
|
||
|
appInfo.get_id = () => 'org.gnome.Extensions.desktop';
|
||
|
appInfo.get_icon = () => Gio.icon_new_for_string('application-x-addon');
|
||
|
appInfo.should_show = () => true;
|
||
|
|
||
|
this.appInfo = appInfo;
|
||
|
this.canLaunchSearch = true;
|
||
|
this.isRemoteProvider = false;
|
||
|
}
|
||
|
|
||
|
getInitialResultSet(terms/* , callback*/) {
|
||
|
const extensions = {};
|
||
|
Main.extensionManager._extensions.forEach(
|
||
|
e => {
|
||
|
extensions[e.uuid] = e;
|
||
|
}
|
||
|
);
|
||
|
this.extensions = extensions;
|
||
|
|
||
|
return new Promise(resolve => resolve(this._getResultSet(terms)));
|
||
|
}
|
||
|
|
||
|
_getResultSet(terms) {
|
||
|
// do not modify original terms
|
||
|
let termsCopy = [...terms];
|
||
|
// search for terms without prefix
|
||
|
termsCopy[0] = termsCopy[0].replace(PREFIX, '');
|
||
|
|
||
|
const candidates = this.extensions;
|
||
|
const _terms = [].concat(termsCopy);
|
||
|
|
||
|
const term = _terms.join(' ');
|
||
|
|
||
|
const results = [];
|
||
|
let m;
|
||
|
for (let id in candidates) {
|
||
|
const extension = this.extensions[id];
|
||
|
const text = extension.metadata.name + (extension.state === 1 ? 'enabled' : '') + ([6, 2].includes(extension.state) ? 'disabled' : '');
|
||
|
if (opt.SEARCH_FUZZY)
|
||
|
m = Me.Util.fuzzyMatch(term, text);
|
||
|
else
|
||
|
m = Me.Util.strictMatch(term, text);
|
||
|
|
||
|
if (m !== -1)
|
||
|
results.push({ weight: m, id });
|
||
|
}
|
||
|
|
||
|
// sort alphabetically
|
||
|
results.sort((a, b) => this.extensions[a.id].metadata.name.localeCompare(this.extensions[b.id].metadata.name));
|
||
|
// enabled first
|
||
|
// results.sort((a, b) => this.extensions[a.id].state !== 1 && this.extensions[b.id].state === 1);
|
||
|
// incompatible last
|
||
|
results.sort((a, b) => this.extensions[a.id].state === 4 && this.extensions[b.id].state !== 4);
|
||
|
|
||
|
const resultIds = results.map(item => item.id);
|
||
|
return resultIds;
|
||
|
}
|
||
|
|
||
|
getResultMetas(resultIds/* , callback = null*/) {
|
||
|
const metas = resultIds.map(id => this.getResultMeta(id));
|
||
|
return new Promise(resolve => resolve(metas));
|
||
|
}
|
||
|
|
||
|
getResultMeta(resultId) {
|
||
|
const result = this.extensions[resultId];
|
||
|
|
||
|
const versionName = result.metadata['version-name'] ?? '';
|
||
|
let version = result.metadata['version'] ?? '';
|
||
|
version = versionName && version ? `/${version}` : version;
|
||
|
const versionStr = `${versionName}${version}`;
|
||
|
|
||
|
return {
|
||
|
'id': resultId,
|
||
|
'name': `${result.metadata.name}`,
|
||
|
'version': versionStr,
|
||
|
'description': versionStr, // description will be updated in result object
|
||
|
'createIcon': size => {
|
||
|
let icon = this.getIcon(result, size);
|
||
|
return icon;
|
||
|
},
|
||
|
};
|
||
|
}
|
||
|
|
||
|
getIcon(extension, size) {
|
||
|
let opacity = 0;
|
||
|
let iconName = 'process-stop-symbolic';
|
||
|
|
||
|
switch (extension.state) {
|
||
|
case 1:
|
||
|
if (extension.hasUpdate)
|
||
|
iconName = 'software-update-available'; // 'software-update-available-symbolic';
|
||
|
else
|
||
|
iconName = 'object-select-symbolic';// 'object-select-symbolic';
|
||
|
|
||
|
opacity = 255;
|
||
|
break;
|
||
|
case 3:
|
||
|
if (Main.extensionManager._enabledExtensions.includes(extension.uuid))
|
||
|
iconName = 'emblem-ok-symbolic';
|
||
|
else
|
||
|
iconName = 'dialog-error';
|
||
|
opacity = 100;
|
||
|
break;
|
||
|
case 4:
|
||
|
iconName = 'software-update-urgent'; // 'software-update-urgent-symbolic';
|
||
|
opacity = 100;
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
if (extension.hasUpdate) {
|
||
|
iconName = 'software-update-available'; // 'software-update-available-symbolic';
|
||
|
opacity = 100;
|
||
|
}
|
||
|
|
||
|
const icon = new St.Icon({ icon_name: iconName, icon_size: size });
|
||
|
icon.set({
|
||
|
opacity,
|
||
|
});
|
||
|
|
||
|
return icon;
|
||
|
}
|
||
|
|
||
|
createResultObject(meta) {
|
||
|
return new ListSearchResult(this, meta, this.extensions[meta.id]);
|
||
|
}
|
||
|
|
||
|
launchSearch(terms, timeStamp) {
|
||
|
this.appInfo.launch([], global.create_app_launch_context(timeStamp, -1), null);
|
||
|
}
|
||
|
|
||
|
activateResult(resultId/* terms, timeStamp*/) {
|
||
|
const extension = this.extensions[resultId];
|
||
|
if (Me.Util.isShiftPressed())
|
||
|
this._toggleExtension(extension);
|
||
|
else if (extension.hasPrefs)
|
||
|
Me.Util.openPreferences(extension.metadata);
|
||
|
}
|
||
|
|
||
|
filterResults(results /* , maxResults*/) {
|
||
|
// return results.slice(0, maxResults);
|
||
|
return results;
|
||
|
}
|
||
|
|
||
|
getSubsearchResultSet(previousResults, terms/* , callback*/) {
|
||
|
return this.getInitialResultSet(terms);
|
||
|
}
|
||
|
|
||
|
getSubsearchResultSet42(terms, callback) {
|
||
|
callback(this._getResultSet(terms));
|
||
|
}
|
||
|
}
|
||
|
|
||
|
const ListSearchResult = GObject.registerClass(
|
||
|
class ListSearchResult extends St.Button {
|
||
|
_init(provider, metaInfo, extension) {
|
||
|
this.provider = provider;
|
||
|
this.metaInfo = metaInfo;
|
||
|
this.extension = extension;
|
||
|
|
||
|
super._init({
|
||
|
reactive: true,
|
||
|
can_focus: true,
|
||
|
track_hover: true,
|
||
|
});
|
||
|
|
||
|
this.style_class = 'list-search-result';
|
||
|
|
||
|
let content = new St.BoxLayout({
|
||
|
style_class: 'list-search-result-content',
|
||
|
vertical: false,
|
||
|
x_align: Clutter.ActorAlign.START,
|
||
|
x_expand: true,
|
||
|
y_expand: true,
|
||
|
});
|
||
|
this.set_child(content);
|
||
|
|
||
|
let titleBox = new St.BoxLayout({
|
||
|
style_class: 'list-search-result-title',
|
||
|
y_align: Clutter.ActorAlign.CENTER,
|
||
|
});
|
||
|
|
||
|
content.add_child(titleBox);
|
||
|
|
||
|
// An icon for, or thumbnail of, content
|
||
|
let icon = this.metaInfo['createIcon'](this.ICON_SIZE);
|
||
|
let iconBox = new St.Button();
|
||
|
iconBox.set_child(icon);
|
||
|
titleBox.add(iconBox);
|
||
|
iconBox.set_style('border: 1px solid rgba(200,200,200,0.2); padding: 2px; border-radius: 8px;');
|
||
|
this._iconBox = iconBox;
|
||
|
this.icon = icon;
|
||
|
|
||
|
iconBox.connect('clicked', () => {
|
||
|
this._toggleExtension();
|
||
|
return Clutter.EVENT_STOP;
|
||
|
});
|
||
|
|
||
|
let title = new St.Label({
|
||
|
text: this.metaInfo['name'],
|
||
|
y_align: Clutter.ActorAlign.CENTER,
|
||
|
opacity: extension.hasPrefs ? 255 : 150,
|
||
|
});
|
||
|
titleBox.add_child(title);
|
||
|
|
||
|
this.label_actor = title;
|
||
|
|
||
|
this._descriptionLabel = new St.Label({
|
||
|
style_class: 'list-search-result-description',
|
||
|
y_align: Clutter.ActorAlign.CENTER,
|
||
|
});
|
||
|
content.add_child(this._descriptionLabel);
|
||
|
|
||
|
this._highlightTerms();
|
||
|
|
||
|
this.connect('destroy', () => {
|
||
|
if (_toggleTimeout) {
|
||
|
GLib.source_remove(_toggleTimeout);
|
||
|
_toggleTimeout = 0;
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
|
||
|
_toggleExtension() {
|
||
|
const state = this.extension.state;
|
||
|
if (![1, 2, 6, 3].includes(state) || this.extension.metadata.name.includes('vertical-workspaces'))
|
||
|
return;
|
||
|
|
||
|
if ([2, 6].includes(state))
|
||
|
Main.extensionManager.enableExtension(this.extension.uuid);
|
||
|
else if ([1, 3].includes(state))
|
||
|
Main.extensionManager.disableExtension(this.extension.uuid);
|
||
|
|
||
|
if (_toggleTimeout)
|
||
|
GLib.source_remove(_toggleTimeout);
|
||
|
|
||
|
_toggleTimeout = GLib.timeout_add(GLib.PRIORITY_LOW, 200,
|
||
|
() => {
|
||
|
if ([7, 8].includes(this.extension.state))
|
||
|
return GLib.SOURCE_CONTINUE;
|
||
|
|
||
|
this.icon?.destroy();
|
||
|
this.icon = this.metaInfo['createIcon'](this.ICON_SIZE);
|
||
|
this._iconBox.set_child(this.icon);
|
||
|
this._highlightTerms();
|
||
|
|
||
|
_toggleTimeout = 0;
|
||
|
return GLib.SOURCE_REMOVE;
|
||
|
}
|
||
|
);
|
||
|
}
|
||
|
|
||
|
get ICON_SIZE() {
|
||
|
return 24;
|
||
|
}
|
||
|
|
||
|
_highlightTerms() {
|
||
|
const extension = this.extension;
|
||
|
const state = extension.state === 4 ? ExtensionState[this.extension.state] : '';
|
||
|
const error = extension.state === 3 ? ` ERROR: ${this.extension.error}` : '';
|
||
|
const update = extension.hasUpdate ? ' | UPDATE PENDING' : '';
|
||
|
const text = `${this.metaInfo.version} ${state}${error}${update}`;
|
||
|
let markup = text;// this.metaInfo['description'].split('\n')[0];
|
||
|
this._descriptionLabel.clutter_text.set_markup(markup);
|
||
|
}
|
||
|
|
||
|
vfunc_clicked() {
|
||
|
this.activate();
|
||
|
}
|
||
|
|
||
|
activate() {
|
||
|
this.provider.activateResult(this.metaInfo.id);
|
||
|
|
||
|
if (this.metaInfo.clipboardText) {
|
||
|
St.Clipboard.get_default().set_text(
|
||
|
St.ClipboardType.CLIPBOARD, this.metaInfo.clipboardText);
|
||
|
}
|
||
|
Main.overview.toggle();
|
||
|
}
|
||
|
});
|