diff options
Diffstat (limited to '')
-rw-r--r-- | js/dbusServices/extensions/css/application.css | 9 | ||||
-rw-r--r-- | js/dbusServices/extensions/extensionPrefsDialog.js | 178 | ||||
-rw-r--r-- | js/dbusServices/extensions/extensionsService.js | 161 | ||||
-rw-r--r-- | js/dbusServices/extensions/main.js | 23 | ||||
-rw-r--r-- | js/dbusServices/extensions/ui/extension-error-page.ui | 112 |
5 files changed, 483 insertions, 0 deletions
diff --git a/js/dbusServices/extensions/css/application.css b/js/dbusServices/extensions/css/application.css new file mode 100644 index 0000000..4ef4066 --- /dev/null +++ b/js/dbusServices/extensions/css/application.css @@ -0,0 +1,9 @@ +.error-page preferencespage { margin: 30px; } + +.expander { padding: 12px; } +.expander.expanded { border: 0 solid @borders; border-bottom-width: 1px; } +.expander-toolbar { + border: 0 solid @borders; + border-top-width: 1px; + padding: 3px; +} diff --git a/js/dbusServices/extensions/extensionPrefsDialog.js b/js/dbusServices/extensions/extensionPrefsDialog.js new file mode 100644 index 0000000..7155c1a --- /dev/null +++ b/js/dbusServices/extensions/extensionPrefsDialog.js @@ -0,0 +1,178 @@ +// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*- +/* exported ExtensionPrefsDialog */ + +const { Adw, Gdk, Gio, GLib, GObject, Gtk } = imports.gi; + +const ExtensionUtils = imports.misc.extensionUtils; + +var ExtensionPrefsDialog = GObject.registerClass({ + GTypeName: 'ExtensionPrefsDialog', +}, class ExtensionPrefsDialog extends Adw.PreferencesWindow { + _init(extension) { + super._init({ + title: extension.metadata.name, + search_enabled: false, + }); + + try { + ExtensionUtils.installImporter(extension); + + // give extension prefs access to their own extension object + ExtensionUtils.setCurrentExtension(extension); + + const prefsModule = extension.imports.prefs; + prefsModule.init(extension.metadata); + + if (prefsModule.fillPreferencesWindow) { + prefsModule.fillPreferencesWindow(this); + + if (!this.visible_page) + throw new Error('Extension did not provide any UI'); + } else { + const widget = prefsModule.buildPrefsWidget(); + const page = this._wrapWidget(widget); + this.add(page); + } + } catch (e) { + this._showErrorPage(e); + logError(e, 'Failed to open preferences'); + } + } + + set titlebar(w) { + this.set_titlebar(w); + } + + // eslint-disable-next-line camelcase + set_titlebar() { + // intercept fatal libadwaita error, show error page instead + GLib.idle_add(GLib.PRIORITY_DEFAULT, () => { + this._showErrorPage( + new Error('set_titlebar() is not supported for Adw.Window')); + return GLib.SOURCE_REMOVE; + }); + } + + _showErrorPage(e) { + while (this.visible_page) + this.remove(this.visible_page); + + const extension = ExtensionUtils.getCurrentExtension(); + this.add(new ExtensionPrefsErrorPage(extension, e)); + } + + _wrapWidget(widget) { + if (widget instanceof Adw.PreferencesPage) + return widget; + + const page = new Adw.PreferencesPage(); + if (widget instanceof Adw.PreferencesGroup) { + page.add(widget); + return page; + } + + const group = new Adw.PreferencesGroup(); + group.add(widget); + page.add(group); + + return page; + } +}); + +const ExtensionPrefsErrorPage = GObject.registerClass({ + GTypeName: 'ExtensionPrefsErrorPage', + Template: 'resource:///org/gnome/Shell/Extensions/ui/extension-error-page.ui', + InternalChildren: [ + 'expander', + 'expanderArrow', + 'revealer', + 'errorView', + ], +}, class ExtensionPrefsErrorPage extends Adw.PreferencesPage { + static _classInit(klass) { + super._classInit(klass); + + klass.install_action('page.copy-error', + null, + self => { + const clipboard = self.get_display().get_clipboard(); + clipboard.set(self._errorMarkdown); + }); + klass.install_action('page.show-url', + null, + self => Gtk.show_uri(self.get_root(), self._url, Gdk.CURRENT_TIME)); + + return klass; + } + + _init(extension, error) { + super._init(); + + this._addCustomStylesheet(); + + this._uuid = extension.uuid; + this._url = extension.metadata.url || ''; + + this.action_set_enabled('page.show-url', this._url !== ''); + + this._gesture = new Gtk.GestureClick({ + button: 0, + exclusive: true, + }); + this._expander.add_controller(this._gesture); + + this._gesture.connect('released', (gesture, nPress) => { + if (nPress === 1) + this._revealer.reveal_child = !this._revealer.reveal_child; + }); + + this._revealer.connect('notify::reveal-child', () => { + this._expanderArrow.icon_name = this._revealer.reveal_child + ? 'pan-down-symbolic' + : 'pan-end-symbolic'; + this._syncExpandedStyle(); + }); + this._revealer.connect('notify::child-revealed', + () => this._syncExpandedStyle()); + + this._errorView.buffer.text = `${error}\n\nStack trace:\n`; + // Indent stack trace. + this._errorView.buffer.text += + error.stack.split('\n').map(line => ` ${line}`).join('\n'); + + // markdown for pasting in gitlab issues + let lines = [ + `The settings of extension ${this._uuid} had an error:`, + '```', + `${error}`, + '```', + '', + 'Stack trace:', + '```', + error.stack.replace(/\n$/, ''), // stack without trailing newline + '```', + '', + ]; + this._errorMarkdown = lines.join('\n'); + } + + _syncExpandedStyle() { + if (this._revealer.reveal_child) + this._expander.add_css_class('expanded'); + else if (!this._revealer.child_revealed) + this._expander.remove_css_class('expanded'); + } + + _addCustomStylesheet() { + let provider = new Gtk.CssProvider(); + let uri = 'resource:///org/gnome/Shell/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_display(Gdk.Display.get_default(), + provider, + Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION); + } +}); diff --git a/js/dbusServices/extensions/extensionsService.js b/js/dbusServices/extensions/extensionsService.js new file mode 100644 index 0000000..d8234d2 --- /dev/null +++ b/js/dbusServices/extensions/extensionsService.js @@ -0,0 +1,161 @@ +// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*- +/* exported ExtensionsService */ + +const { Gio, GLib, Shew } = imports.gi; + +const ExtensionUtils = imports.misc.extensionUtils; + +const { loadInterfaceXML } = imports.misc.dbusUtils; +const { ExtensionPrefsDialog } = imports.extensionPrefsDialog; +const { ServiceImplementation } = imports.dbusService; + +const ExtensionsIface = loadInterfaceXML('org.gnome.Shell.Extensions'); +const ExtensionsProxy = Gio.DBusProxy.makeProxyWrapper(ExtensionsIface); + +var ExtensionsService = class extends ServiceImplementation { + constructor() { + super(ExtensionsIface, '/org/gnome/Shell/Extensions'); + + this._proxy = new ExtensionsProxy(Gio.DBus.session, + 'org.gnome.Shell', '/org/gnome/Shell'); + + this._proxy.connectSignal('ExtensionStateChanged', + (proxy, sender, params) => { + this._dbusImpl.emit_signal('ExtensionStateChanged', + new GLib.Variant('(sa{sv})', params)); + }); + + this._proxy.connect('g-properties-changed', () => { + this._dbusImpl.emit_property_changed('UserExtensionsEnabled', + new GLib.Variant('b', this._proxy.UserExtensionsEnabled)); + }); + } + + get ShellVersion() { + return this._proxy.ShellVersion; + } + + get UserExtensionsEnabled() { + return this._proxy.UserExtensionsEnabled; + } + + set UserExtensionsEnabled(enable) { + this._proxy.UserExtensionsEnabled = enable; + } + + async ListExtensionsAsync(params, invocation) { + try { + const res = await this._proxy.ListExtensionsAsync(...params); + invocation.return_value(new GLib.Variant('(a{sa{sv}})', res)); + } catch (error) { + this._handleError(invocation, error); + } + } + + async GetExtensionInfoAsync(params, invocation) { + try { + const res = await this._proxy.GetExtensionInfoAsync(...params); + invocation.return_value(new GLib.Variant('(a{sv})', res)); + } catch (error) { + this._handleError(invocation, error); + } + } + + async GetExtensionErrorsAsync(params, invocation) { + try { + const res = await this._proxy.GetExtensionErrorsAsync(...params); + invocation.return_value(new GLib.Variant('(as)', res)); + } catch (error) { + this._handleError(invocation, error); + } + } + + async InstallRemoteExtensionAsync(params, invocation) { + try { + const res = await this._proxy.InstallRemoteExtensionAsync(...params); + invocation.return_value(new GLib.Variant('(s)', res)); + } catch (error) { + this._handleError(invocation, error); + } + } + + async UninstallExtensionAsync(params, invocation) { + try { + const res = await this._proxy.UninstallExtensionAsync(...params); + invocation.return_value(new GLib.Variant('(b)', res)); + } catch (error) { + this._handleError(invocation, error); + } + } + + async EnableExtensionAsync(params, invocation) { + try { + const res = await this._proxy.EnableExtensionAsync(...params); + invocation.return_value(new GLib.Variant('(b)', res)); + } catch (error) { + this._handleError(invocation, error); + } + } + + async DisableExtensionAsync(params, invocation) { + try { + const res = await this._proxy.DisableExtensionAsync(...params); + invocation.return_value(new GLib.Variant('(b)', res)); + } catch (error) { + this._handleError(invocation, error); + } + } + + LaunchExtensionPrefsAsync([uuid], invocation) { + this.OpenExtensionPrefsAsync([uuid, '', {}], invocation); + } + + async OpenExtensionPrefsAsync(params, invocation) { + const [uuid, parentWindow, options] = params; + + try { + const [serialized] = await this._proxy.GetExtensionInfoAsync(uuid); + + if (this._prefsDialog) + throw new Error('Already showing a prefs dialog'); + + const extension = ExtensionUtils.deserializeExtension(serialized); + + this._prefsDialog = new ExtensionPrefsDialog(extension); + this._prefsDialog.connect('realize', () => { + let externalWindow = null; + + if (parentWindow) + externalWindow = Shew.ExternalWindow.new_from_handle(parentWindow); + + if (externalWindow) + externalWindow.set_parent_of(this._prefsDialog.get_surface()); + }); + + if (options.modal) + this._prefsDialog.modal = options.modal.get_boolean(); + + this._prefsDialog.connect('close-request', () => { + delete this._prefsDialog; + this.release(); + return false; + }); + this.hold(); + + this._prefsDialog.show(); + + invocation.return_value(null); + } catch (error) { + this._handleError(invocation, error); + } + } + + async CheckForUpdatesAsync(params, invocation) { + try { + await this._proxy.CheckForUpdatesAsync(...params); + invocation.return_value(null); + } catch (error) { + this._handleError(invocation, error); + } + } +}; diff --git a/js/dbusServices/extensions/main.js b/js/dbusServices/extensions/main.js new file mode 100644 index 0000000..306fe36 --- /dev/null +++ b/js/dbusServices/extensions/main.js @@ -0,0 +1,23 @@ +/* exported main */ + +imports.gi.versions.Adw = '1'; +imports.gi.versions.Gdk = '4.0'; +imports.gi.versions.Gtk = '4.0'; + +const { Adw, GObject } = imports.gi; +const pkg = imports.package; + +const { DBusService } = imports.dbusService; +const { ExtensionsService } = imports.extensionsService; + +function main() { + Adw.init(); + pkg.initFormat(); + + GObject.gtypeNameBasedOnJSPath = true; + + const service = new DBusService( + 'org.gnome.Shell.Extensions', + new ExtensionsService()); + service.run(); +} diff --git a/js/dbusServices/extensions/ui/extension-error-page.ui b/js/dbusServices/extensions/ui/extension-error-page.ui new file mode 100644 index 0000000..5ce6a62 --- /dev/null +++ b/js/dbusServices/extensions/ui/extension-error-page.ui @@ -0,0 +1,112 @@ +<?xml version="1.0" encoding="UTF-8"?> +<interface> + <template class="ExtensionPrefsErrorPage" parent="AdwPreferencesPage"> + <style> + <class name="error-page"/> + </style> + <child> + <object class="AdwPreferencesGroup"> + <child> + <object class="GtkBox"> + <property name="orientation">vertical</property> + <property name="spacing">12</property> + <child> + <object class="GtkLabel"> + <property name="label" translatable="yes">Something’s gone wrong</property> + <style> + <class name="title-1"/> + </style> + </object> + </child> + <child> + <object class="GtkLabel"> + <property name="label" translatable="yes">We’re very sorry, but there’s been a problem: the settings for this extension can’t be displayed. We recommend that you report the issue to the extension authors.</property> + <property name="justify">center</property> + <property name="wrap">True</property> + </object> + </child> + <child> + <object class="GtkFrame"> + <property name="margin-top">12</property> + <child> + <object class="GtkBox"> + <property name="hexpand">True</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkBox" id="expander"> + <property name="spacing">6</property> + <style> + <class name="expander"/> + </style> + <child> + <object class="GtkImage" id="expanderArrow"> + <property name="icon-name">pan-end-symbolic</property> + </object> + </child> + <child> + <object class="GtkLabel"> + <property name="label" translatable="yes">Technical Details</property> + </object> + </child> + </object> + </child> + <child> + <object class="GtkRevealer" id="revealer"> + <child> + <object class="GtkBox"> + <property name="orientation">vertical</property> + <child> + <object class="GtkTextView" id="errorView"> + <property name="monospace">True</property> + <property name="editable">False</property> + <property name="wrap-mode">word</property> + <property name="left-margin">12</property> + <property name="right-margin">12</property> + <property name="top-margin">12</property> + <property name="bottom-margin">12</property> + </object> + </child> + <child> + <object class="GtkBox"> + <style> + <class name="expander-toolbar"/> + </style> + <child> + <object class="GtkButton"> + <property name="receives-default">True</property> + <property name="action-name">page.copy-error</property> + <property name="has-frame">False</property> + <property name="icon-name">edit-copy-symbolic</property> + </object> + </child> + <child> + <object class="GtkButton" id="homeButton"> + <property name="visible" + bind-source="homeButton" + bind-property="sensitive" + bind-flags="sync-create"/> + <property name="hexpand">True</property> + <property name="halign">end</property> + <property name="label" translatable="yes">Homepage</property> + <property name="tooltip-text" translatable="yes">Visit extension homepage</property> + <property name="receives-default">True</property> + <property name="has-frame">False</property> + <property name="action-name">page.show-url</property> + </object> + </child> + </object> + </child> + </object> + </child> + </object> + </child> + </object> + </child> + </object> + </child> + </object> + </child> + </object> + </child> + </template> +</interface> |