gnome-shell/subprojects/extensions-app/js/main.js
Simon McVittie 7f4f328a7f Specify API versions for all public GIR APIs, except GLib
If one of these libraries breaks its GIR API in future, then upgrading
packages unrelated to gnome-shell might pull in the newer version,
causing gnome-shell to crash when it gets a newer GIR API that is
incompatible with its expectations. For example, this seems to be
happening in Debian testing at the moment, when GNOME Shell 41.4
imports GWeather and can get version 4.0 instead of the version 3.0 that
it expected.

Adding explicit API versions at the time the newer version is released
is too late, because that will still let the newer version of the GIR API
break pre-existing GNOME Shell packages. Prevent similar crashes in
future by making the desired versions explicit.

This is done for all third-party libraries except GLib, similar to the
common practice in Python code; if GLib breaks API, then that will be
a disruptive change to the whole GLib/GObject ecosystem, regardless.

Gvc, Meta, Shell, Shew, St are not included because they're private
(only exist in a non-default search path entry).

Clutter and Cogl *are* included, because we need to import the fork of
them that comes with Meta, as opposed to their deprecated standalone
versions.

Signed-off-by: Simon McVittie <smcv@debian.org>
Bug-Debian: https://bugs.debian.org/1008926
Part-of: <https://gitlab.gnome.org/GNOME/gnome-shell/-/merge_requests/2261>
2022-04-04 17:55:25 +01:00

547 lines
16 KiB
JavaScript

/* exported main */
imports.gi.versions.Adw = '1';
imports.gi.versions.Gtk = '4.0';
const Gettext = imports.gettext;
const Package = imports.package;
const { Adw, GLib, Gio, GObject, Gtk, Shew } = imports.gi;
Package.initFormat();
const ExtensionUtils = imports.misc.extensionUtils;
const { ExtensionState, ExtensionType } = ExtensionUtils;
const GnomeShellIface = loadInterfaceXML('org.gnome.Shell.Extensions');
const GnomeShellProxy = Gio.DBusProxy.makeProxyWrapper(GnomeShellIface);
Gio._promisify(Gio.DBusConnection.prototype, 'call');
Gio._promisify(Shew.WindowExporter.prototype, 'export');
function loadInterfaceXML(iface) {
const uri = `resource:///org/gnome/Extensions/dbus-interfaces/${iface}.xml`;
const f = Gio.File.new_for_uri(uri);
try {
let [ok_, bytes] = f.load_contents(null);
return new TextDecoder().decode(bytes);
} catch (e) {
log(`Failed to load D-Bus interface ${iface}`);
}
return null;
}
function toggleState(action) {
let state = action.get_state();
action.change_state(new GLib.Variant('b', !state.get_boolean()));
}
var Application = GObject.registerClass(
class Application extends Adw.Application {
_init() {
GLib.set_prgname('gnome-extensions-app');
super._init({ application_id: Package.name });
this.connect('window-removed', (a, window) => window.run_dispose());
}
get shellProxy() {
return this._shellProxy;
}
vfunc_activate() {
this._shellProxy.CheckForUpdatesRemote();
this._window.present();
}
vfunc_startup() {
super.vfunc_startup();
const action = new Gio.SimpleAction({ name: 'quit' });
action.connect('activate', () => this._window.close());
this.add_action(action);
this.set_accels_for_action('app.quit', ['<Primary>q']);
this._shellProxy = new GnomeShellProxy(Gio.DBus.session,
'org.gnome.Shell.Extensions', '/org/gnome/Shell/Extensions');
this._window = new ExtensionsWindow({ application: this });
}
});
var ExtensionsWindow = GObject.registerClass({
GTypeName: 'ExtensionsWindow',
Template: 'resource:///org/gnome/Extensions/ui/extensions-window.ui',
InternalChildren: [
'userGroup',
'userList',
'systemGroup',
'systemList',
'mainStack',
'searchBar',
'searchButton',
'searchEntry',
'updatesBar',
'updatesLabel',
],
}, class ExtensionsWindow extends Gtk.ApplicationWindow {
_init(params) {
super._init(params);
this._updatesCheckId = 0;
this._exporter = new Shew.WindowExporter({ window: this });
this._exportedHandle = '';
let action;
action = new Gio.SimpleAction({ name: 'show-about' });
action.connect('activate', this._showAbout.bind(this));
this.add_action(action);
action = new Gio.SimpleAction({ name: 'logout' });
action.connect('activate', this._logout.bind(this));
this.add_action(action);
action = new Gio.SimpleAction({
name: 'user-extensions-enabled',
state: new GLib.Variant('b', false),
});
action.connect('activate', toggleState);
action.connect('change-state', (a, state) => {
this._shellProxy.UserExtensionsEnabled = state.get_boolean();
});
this.add_action(action);
this._searchTerms = [];
this._searchEntry.connect('search-changed', () => {
const { text } = this._searchEntry;
if (text === '')
this._searchTerms = [];
else
[this._searchTerms] = GLib.str_tokenize_and_fold(text, null);
this._userList.invalidate_filter();
this._systemList.invalidate_filter();
});
this._userList.set_sort_func(this._sortList.bind(this));
this._userList.set_filter_func(this._filterList.bind(this));
this._userList.set_placeholder(new Gtk.Label({
label: _('No Matches'),
margin_start: 12,
margin_end: 12,
margin_top: 12,
margin_bottom: 12,
}));
this._userList.connect('row-activated', (_list, row) => row.activate());
this._systemList.set_sort_func(this._sortList.bind(this));
this._systemList.set_filter_func(this._filterList.bind(this));
this._systemList.set_placeholder(new Gtk.Label({
label: _('No Matches'),
margin_start: 12,
margin_end: 12,
margin_top: 12,
margin_bottom: 12,
}));
this._systemList.connect('row-activated', (_list, row) => row.activate());
this._shellProxy.connectSignal('ExtensionStateChanged',
this._onExtensionStateChanged.bind(this));
this._shellProxy.connect('g-properties-changed',
this._onUserExtensionsEnabledChanged.bind(this));
this._onUserExtensionsEnabledChanged();
this._scanExtensions();
}
get _shellProxy() {
return this.application.shellProxy;
}
uninstall(uuid) {
let row = this._findExtensionRow(uuid);
let dialog = new Gtk.MessageDialog({
transient_for: this,
modal: true,
text: _('Remove “%s”?').format(row.name),
secondary_text: _('If you remove the extension, you need to return to download it if you want to enable it again'),
});
dialog.add_button(_('Cancel'), Gtk.ResponseType.CANCEL);
dialog.add_button(_('Remove'), Gtk.ResponseType.ACCEPT)
.get_style_context().add_class('destructive-action');
dialog.connect('response', (dlg, response) => {
if (response === Gtk.ResponseType.ACCEPT)
this._shellProxy.UninstallExtensionRemote(uuid);
dialog.destroy();
});
dialog.present();
}
async openPrefs(uuid) {
if (!this._exportedHandle) {
try {
this._exportedHandle = await this._exporter.export();
} catch (e) {
log(`Failed to export window: ${e.message}`);
}
}
this._shellProxy.OpenExtensionPrefsRemote(uuid,
this._exportedHandle,
{ modal: new GLib.Variant('b', true) });
}
_showAbout() {
let aboutDialog = new Gtk.AboutDialog({
authors: [
'Florian Müllner <fmuellner@gnome.org>',
'Jasper St. Pierre <jstpierre@mecheye.net>',
'Didier Roche <didrocks@ubuntu.com>',
'Romain Vigier <contact@romainvigier.fr>',
],
translator_credits: _('translator-credits'),
program_name: _('Extensions'),
comments: _('Manage your GNOME Extensions'),
license_type: Gtk.License.GPL_2_0,
logo_icon_name: Package.name,
version: Package.version,
transient_for: this,
modal: true,
});
aboutDialog.present();
}
_logout() {
this.application.get_dbus_connection().call(
'org.gnome.SessionManager',
'/org/gnome/SessionManager',
'org.gnome.SessionManager',
'Logout',
new GLib.Variant('(u)', [0]),
null,
Gio.DBusCallFlags.NONE,
-1,
null);
}
_sortList(row1, row2) {
return row1.name.localeCompare(row2.name);
}
_filterList(row) {
return this._searchTerms.every(
t => row.keywords.some(k => k.startsWith(t)));
}
_findExtensionRow(uuid) {
return [
...this._userList,
...this._systemList,
].find(c => c.uuid === uuid);
}
_onUserExtensionsEnabledChanged() {
let action = this.lookup_action('user-extensions-enabled');
action.set_state(
new GLib.Variant('b', this._shellProxy.UserExtensionsEnabled));
}
_onExtensionStateChanged(proxy, senderName, [uuid, newState]) {
let extension = ExtensionUtils.deserializeExtension(newState);
let row = this._findExtensionRow(uuid);
this._queueUpdatesCheck();
// the extension's type changed; remove the corresponding row
// and reset the variable to null so that we create a new row
// below and add it to the appropriate list
if (row && row.type !== extension.type) {
row.get_parent().remove(row);
row = null;
}
if (row) {
if (extension.state === ExtensionState.UNINSTALLED)
row.get_parent().remove(row);
} else {
this._addExtensionRow(extension);
}
this._syncListVisibility();
}
_scanExtensions() {
this._shellProxy.ListExtensionsRemote(([extensionsMap], e) => {
if (e) {
if (e instanceof Gio.DBusError) {
log(`Failed to connect to shell proxy: ${e}`);
this._mainStack.visible_child_name = 'noshell';
} else {
throw e;
}
return;
}
for (let uuid in extensionsMap) {
let extension = ExtensionUtils.deserializeExtension(extensionsMap[uuid]);
this._addExtensionRow(extension);
}
this._extensionsLoaded();
});
}
_addExtensionRow(extension) {
let row = new ExtensionRow(extension);
if (row.type === ExtensionType.PER_USER)
this._userList.append(row);
else
this._systemList.append(row);
}
_queueUpdatesCheck() {
if (this._updatesCheckId)
return;
this._updatesCheckId = GLib.timeout_add_seconds(
GLib.PRIORITY_DEFAULT, 1, () => {
this._checkUpdates();
this._updatesCheckId = 0;
return GLib.SOURCE_REMOVE;
});
}
_syncListVisibility() {
this._userGroup.visible = [...this._userList].length > 1;
this._systemGroup.visible = [...this._systemList].length > 1;
if (this._userGroup.visible || this._systemGroup.visible)
this._mainStack.visible_child_name = 'main';
else
this._mainStack.visible_child_name = 'placeholder';
}
_checkUpdates() {
let nUpdates = [...this._userList].filter(c => c.hasUpdate).length;
this._updatesLabel.label = Gettext.ngettext(
'%d extension will be updated on next login.',
'%d extensions will be updated on next login.',
nUpdates).format(nUpdates);
this._updatesBar.revealed = nUpdates > 0;
}
_extensionsLoaded() {
this._syncListVisibility();
this._checkUpdates();
}
});
var ExtensionRow = GObject.registerClass({
GTypeName: 'ExtensionRow',
Template: 'resource:///org/gnome/Extensions/ui/extension-row.ui',
InternalChildren: [
'nameLabel',
'descriptionLabel',
'versionLabel',
'errorLabel',
'errorIcon',
'updatesIcon',
'switch',
'actionsBox',
],
}, class ExtensionRow extends Gtk.ListBoxRow {
_init(extension) {
super._init();
this._app = Gio.Application.get_default();
this._extension = extension;
this._prefsModule = null;
[this._keywords] = GLib.str_tokenize_and_fold(this.name, null);
this._actionGroup = new Gio.SimpleActionGroup();
this.insert_action_group('row', this._actionGroup);
let action;
action = new Gio.SimpleAction({
name: 'show-prefs',
enabled: this.hasPrefs,
});
action.connect('activate', () => this.get_root().openPrefs(this.uuid));
this._actionGroup.add_action(action);
action = new Gio.SimpleAction({
name: 'show-url',
enabled: this.url !== '',
});
action.connect('activate', () => {
Gio.AppInfo.launch_default_for_uri(
this.url, this.get_display().get_app_launch_context());
});
this._actionGroup.add_action(action);
action = new Gio.SimpleAction({
name: 'uninstall',
enabled: this.type === ExtensionType.PER_USER,
});
action.connect('activate', () => this.get_root().uninstall(this.uuid));
this._actionGroup.add_action(action);
action = new Gio.SimpleAction({
name: 'enabled',
state: new GLib.Variant('b', false),
});
action.connect('activate', toggleState);
action.connect('change-state', (a, state) => {
if (state.get_boolean())
this._app.shellProxy.EnableExtensionRemote(this.uuid);
else
this._app.shellProxy.DisableExtensionRemote(this.uuid);
});
this._actionGroup.add_action(action);
this._nameLabel.label = this.name;
const desc = this._extension.metadata.description.split('\n')[0];
this._descriptionLabel.label = desc;
this._descriptionLabel.tooltip_text = desc;
this.connect('destroy', this._onDestroy.bind(this));
this._extensionStateChangedId = this._app.shellProxy.connectSignal(
'ExtensionStateChanged', (p, sender, [uuid, newState]) => {
if (this.uuid !== uuid)
return;
this._extension = ExtensionUtils.deserializeExtension(newState);
this._updateState();
});
this._updateState();
}
vfunc_activate() {
this._switch.mnemonic_activate(false);
}
get uuid() {
return this._extension.uuid;
}
get name() {
return this._extension.metadata.name;
}
get hasPrefs() {
return this._extension.hasPrefs;
}
get hasUpdate() {
return this._extension.hasUpdate || false;
}
get hasError() {
const { state } = this._extension;
return state === ExtensionState.OUT_OF_DATE ||
state === ExtensionState.ERROR;
}
get type() {
return this._extension.type;
}
get creator() {
return this._extension.metadata.creator || '';
}
get url() {
return this._extension.metadata.url || '';
}
get version() {
return this._extension.metadata.version || '';
}
get error() {
if (!this.hasError)
return '';
if (this._extension.state === ExtensionState.OUT_OF_DATE)
return _('The extension is incompatible with the current GNOME version');
return this._extension.error
? this._extension.error : _('The extension had an error');
}
get keywords() {
return this._keywords;
}
_updateState() {
let state = this._extension.state === ExtensionState.ENABLED;
let action = this._actionGroup.lookup('enabled');
action.set_state(new GLib.Variant('b', state));
action.enabled = this._canToggle();
if (!action.enabled)
this._switch.active = state;
this._updatesIcon.visible = this.hasUpdate;
this._errorIcon.visible = this.hasError;
this._descriptionLabel.visible = !this.hasError;
this._errorLabel.label = this.error;
this._errorLabel.visible = this.error !== '';
this._versionLabel.label = this.version.toString();
this._versionLabel.visible = this.version !== '';
}
_onDestroy() {
if (!this._app.shellProxy)
return;
if (this._extensionStateChangedId)
this._app.shellProxy.disconnectSignal(this._extensionStateChangedId);
this._extensionStateChangedId = 0;
}
_canToggle() {
return this._extension.canChange;
}
});
function initEnvironment() {
// Monkey-patch in a "global" object that fakes some Shell utilities
// that ExtensionUtils depends on.
globalThis.global = {
log(...args) {
print(args.join(', '));
},
logError(s) {
log(`ERROR: ${s}`);
},
userdatadir: GLib.build_filenamev([GLib.get_user_data_dir(), 'gnome-shell']),
};
}
function main(argv) {
initEnvironment();
Package.initGettext();
new Application().run(argv);
}