gnome-shell/subprojects/extensions-app/js/main.js
Florian Müllner 39e6375aff extensions-app: Move Extensions app to new subproject
The Extensions app code is now independent enough from the rest of the code
base to move it to its own subprojects, like we did for the extensions-tool.

This allows for stand-alone builds of the app, which we are about to use
for distributing it as flatpak.

https://gitlab.gnome.org/GNOME/gnome-shell/-/merge_requests/1133
2020-03-27 23:33:30 +00:00

501 lines
15 KiB
JavaScript

/* exported main */
imports.gi.versions.Gdk = '3.0';
imports.gi.versions.Gtk = '3.0';
const Gettext = imports.gettext;
const Package = imports.package;
const { Gdk, 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(Shew.WindowExporter.prototype, 'export', 'export_finish');
function loadInterfaceXML(iface) {
const uri = 'resource:///org/gnome/Extensions/dbus-interfaces/%s.xml'.format(iface);
const f = Gio.File.new_for_uri(uri);
try {
let [ok_, bytes] = f.load_contents(null);
return imports.byteArray.toString(bytes);
} catch (e) {
log('Failed to load D-Bus interface %s'.format(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 Gtk.Application {
_init() {
GLib.set_prgname('gnome-shell-extension-prefs');
super._init({ application_id: 'org.gnome.Extensions' });
}
get shellProxy() {
return this._shellProxy;
}
vfunc_activate() {
this._shellProxy.CheckForUpdatesRemote();
this._window.present();
}
vfunc_startup() {
super.vfunc_startup();
let provider = new Gtk.CssProvider();
let uri = 'resource:///org/gnome/Extensions/css/application.css';
try {
provider.load_from_file(Gio.File.new_for_uri(uri));
} catch (e) {
logError(e, 'Failed to add application style');
}
Gtk.StyleContext.add_provider_for_screen(Gdk.Screen.get_default(),
provider,
Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION);
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: [
'userList',
'systemList',
'mainBox',
'mainStack',
'scrolledWindow',
'updatesBar',
'updatesLabel',
],
}, class ExtensionsWindow extends Gtk.ApplicationWindow {
_init(params) {
super._init(params);
this._updatesCheckId = 0;
this._exporter = new Shew.WindowExporter({ window: this });
this._exportedHandle = '';
this._mainBox.set_focus_vadjustment(this._scrolledWindow.vadjustment);
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._userList.set_sort_func(this._sortList.bind(this));
this._userList.set_header_func(this._updateHeader.bind(this));
this._systemList.set_sort_func(this._sortList.bind(this));
this._systemList.set_header_func(this._updateHeader.bind(this));
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: %s'.format(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>',
],
translator_credits: _('translator-credits'),
program_name: _('Extensions'),
comments: _('Manage your GNOME Extensions'),
license_type: Gtk.License.GPL_2_0,
logo_icon_name: 'org.gnome.Extensions',
version: imports.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,
(o, res) => {
o.call_finish(res);
});
}
_sortList(row1, row2) {
return row1.name.localeCompare(row2.name);
}
_updateHeader(row, before) {
if (!before || row.get_header())
return;
let sep = new Gtk.Separator({ orientation: Gtk.Orientation.HORIZONTAL });
row.set_header(sep);
}
_findExtensionRow(uuid) {
return [
...this._userList.get_children(),
...this._systemList.get_children(),
].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.destroy();
row = null;
}
if (row) {
if (extension.state === ExtensionState.UNINSTALLED)
row.destroy();
} 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: %s'.format(e.toString()));
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);
row.show_all();
if (row.type === ExtensionType.PER_USER)
this._userList.add(row);
else
this._systemList.add(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._userList.visible = this._userList.get_children().length > 0;
this._systemList.visible = this._systemList.get_children().length > 0;
if (this._userList.visible || this._systemList.visible)
this._mainStack.visible_child_name = 'main';
else
this._mainStack.visible_child_name = 'placeholder';
}
_checkUpdates() {
let nUpdates = this._userList.get_children().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.visible = 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',
'authorLabel',
'updatesIcon',
'revealButton',
'revealer',
],
}, class ExtensionRow extends Gtk.ListBoxRow {
_init(extension) {
super._init();
this._app = Gio.Application.get_default();
this._extension = extension;
this._prefsModule = 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_toplevel().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_toplevel().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;
let desc = this._extension.metadata.description.split('\n')[0];
this._descriptionLabel.label = desc;
this._revealButton.connect('clicked', () => {
this._revealer.reveal_child = !this._revealer.reveal_child;
});
this._revealer.connect('notify::reveal-child', () => {
if (this._revealer.reveal_child)
this._revealButton.get_style_context().add_class('expanded');
else
this._revealButton.get_style_context().remove_class('expanded');
});
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();
}
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 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 || '';
}
_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();
this._updatesIcon.visible = this.hasUpdate;
this._versionLabel.label = this.version.toString();
this._versionLabel.visible = this.version !== '';
this._authorLabel.label = this.creator.toString();
this._authorLabel.visible = this.creator !== '';
}
_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.
window.global = {
log(...args) {
print(args.join(', '));
},
logError(s) {
log('ERROR: %s'.format(s));
},
userdatadir: GLib.build_filenamev([GLib.get_user_data_dir(), 'gnome-shell']),
};
}
function main(argv) {
initEnvironment();
Package.initGettext();
new Application().run(argv);
}