const Gettext = imports.gettext; const { Gdk, GLib, Gio, GObject, Gtk, Pango } = imports.gi; const Format = imports.format; const _ = Gettext.gettext; const Config = imports.misc.config; const ExtensionUtils = imports.misc.extensionUtils; const { loadInterfaceXML } = imports.misc.fileUtils; const { ExtensionState } = ExtensionUtils; const GnomeShellIface = loadInterfaceXML('org.gnome.Shell.Extensions'); const GnomeShellProxy = Gio.DBusProxy.makeProxyWrapper(GnomeShellIface); function stripPrefix(string, prefix) { if (string.slice(0, prefix.length) == prefix) return string.slice(prefix.length); return string; } var Application = GObject.registerClass({ GTypeName: 'ExtensionPrefs_Application' }, class Application extends Gtk.Application { _init() { GLib.set_prgname('gnome-shell-extension-prefs'); super._init({ application_id: 'org.gnome.shell.ExtensionPrefs', flags: Gio.ApplicationFlags.HANDLES_COMMAND_LINE }); this._startupUuid = null; this._loaded = false; this._skipMainWindow = false; this._shellProxy = null; } get shellProxy() { return this._shellProxy; } _showPrefs(uuid) { let row = this._extensionSelector.get_children().find(c => { return c.uuid === uuid && c.hasPrefs; }); if (!row) return false; let widget; try { widget = row.prefsModule.buildPrefsWidget(); } catch (e) { widget = this._buildErrorUI(row, e); } let dialog = new Gtk.Window({ modal: !this._skipMainWindow, type_hint: Gdk.WindowTypeHint.DIALOG }); dialog.set_titlebar(new Gtk.HeaderBar({ show_close_button: true, title: row.name, visible: true })); if (this._skipMainWindow) { this.add_window(dialog); if (this._window) this._window.destroy(); this._window = dialog; this._window.window_position = Gtk.WindowPosition.CENTER; } else { dialog.transient_for = this._window; } dialog.set_default_size(600, 400); dialog.add(widget); dialog.show(); } _buildErrorUI(row, exc) { let scroll = new Gtk.ScrolledWindow({ hscrollbar_policy: Gtk.PolicyType.NEVER, propagate_natural_height: true }); let box = new Gtk.Box({ orientation: Gtk.Orientation.VERTICAL, spacing: 12, margin: 100, margin_bottom: 60 }); scroll.add(box); let label = new Gtk.Label({ label: '%s'.format(_("Something’s gone wrong")), use_markup: true }); label.get_style_context().add_class(Gtk.STYLE_CLASS_DIM_LABEL); box.add(label); label = new Gtk.Label({ label: _("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."), justify: Gtk.Justification.CENTER, wrap: true }); box.add(label); let expander = new Expander({ label: _("Technical Details"), margin_top: 12 }); box.add(expander); let errortext = `${exc}\n\nStack trace:\n${ // Indent stack trace. exc.stack.split('\n').map(line => ` ${line}`).join('\n') }`; let buffer = new Gtk.TextBuffer({ text: errortext }); let textview = new Gtk.TextView({ buffer: buffer, wrap_mode: Gtk.WrapMode.WORD, monospace: true, editable: false, top_margin: 12, bottom_margin: 12, left_margin: 12, right_margin: 12 }); let toolbar = new Gtk.Toolbar(); let provider = new Gtk.CssProvider(); provider.load_from_data(`* { border: 0 solid @borders; border-top-width: 1px; }`); toolbar.get_style_context().add_provider( provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION ); let copyButton = new Gtk.ToolButton({ icon_name: 'edit-copy-symbolic', tooltip_text: _("Copy Error") }); toolbar.add(copyButton); copyButton.connect('clicked', w => { let clipboard = Gtk.Clipboard.get_default(w.get_display()); // markdown for pasting in gitlab issues let lines = [ `The settings of extension ${row.uuid} had an error:`, '```', `${exc}`, '```', '', 'Stack trace:', '```', exc.stack.replace(/\n$/, ''), // stack without trailing newline '```', '' ]; clipboard.set_text(lines.join('\n'), -1); }); let spacing = new Gtk.SeparatorToolItem({ draw: false }); toolbar.add(spacing); toolbar.child_set_property(spacing, "expand", true); let urlButton = new Gtk.ToolButton({ label: _("Homepage"), tooltip_text: _("Visit extension homepage"), no_show_all: true, visible: row.url != null }); toolbar.add(urlButton); urlButton.connect('clicked', w => { let context = w.get_display().get_app_launch_context(); Gio.AppInfo.launch_default_for_uri(row.url, context); }); let expandedBox = new Gtk.Box({ orientation: Gtk.Orientation.VERTICAL }); expandedBox.add(textview); expandedBox.add(toolbar); expander.add(expandedBox); scroll.show_all(); return scroll; } _buildUI() { this._window = new Gtk.ApplicationWindow({ application: this, window_position: Gtk.WindowPosition.CENTER }); this._window.set_default_size(800, 500); this._titlebar = new Gtk.HeaderBar({ show_close_button: true, title: _("Shell Extensions") }); this._window.set_titlebar(this._titlebar); let killSwitch = new Gtk.Switch({ valign: Gtk.Align.CENTER }); this._titlebar.pack_end(killSwitch); this._settings = new Gio.Settings({ schema_id: 'org.gnome.shell' }); this._settings.bind('disable-user-extensions', killSwitch, 'active', Gio.SettingsBindFlags.DEFAULT | Gio.SettingsBindFlags.INVERT_BOOLEAN); this._mainStack = new Gtk.Stack({ transition_type: Gtk.StackTransitionType.CROSSFADE }); this._window.add(this._mainStack); let scroll = new Gtk.ScrolledWindow({ hscrollbar_policy: Gtk.PolicyType.NEVER }); this._extensionSelector = new Gtk.ListBox({ selection_mode: Gtk.SelectionMode.NONE }); this._extensionSelector.set_sort_func(this._sortList.bind(this)); this._extensionSelector.set_header_func(this._updateHeader.bind(this)); scroll.add(this._extensionSelector); this._mainStack.add_named(scroll, 'listing'); this._mainStack.add_named(new EmptyPlaceholder(), 'placeholder'); this._shellProxy = new GnomeShellProxy(Gio.DBus.session, 'org.gnome.Shell', '/org/gnome/Shell'); this._shellProxy.connectSignal('ExtensionStateChanged', this._onExtensionStateChanged.bind(this)); this._window.show_all(); } _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._extensionSelector.get_children().find(c => c.uuid === uuid); } _onExtensionStateChanged(proxy, senderName, [uuid, newState]) { let row = this._findExtensionRow(uuid); if (row) { let { state } = ExtensionUtils.deserializeExtension(newState); if (state == ExtensionState.UNINSTALLED) row.destroy(); return; // we only deal with new and deleted extensions here } this._shellProxy.GetExtensionInfoRemote(uuid, ([serialized]) => { let extension = ExtensionUtils.deserializeExtension(serialized); if (!extension) return; // check the extension wasn't added in between if (this._findExtensionRow(uuid) != null) return; this._addExtensionRow(extension); }); } _scanExtensions() { this._shellProxy.ListExtensionsRemote(([extensionsMap], e) => { if (e) { if (e instanceof Gio.DBusError) { log(`Failed to connect to shell proxy: ${e}`); this._mainStack.add_named(new NoShellPlaceholder(), 'noshell'); 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.prefsButton.connect('clicked', () => { this._showPrefs(row.uuid); }); row.show_all(); this._extensionSelector.add(row); } _extensionsLoaded() { if (this._extensionSelector.get_children().length > 0) this._mainStack.visible_child_name = 'listing'; else this._mainStack.visible_child_name = 'placeholder'; if (this._startupUuid) this._showPrefs(this._startupUuid); this._startupUuid = null; this._skipMainWindow = false; this._loaded = true; } vfunc_activate() { this._window.present(); } vfunc_startup() { super.vfunc_startup(); this._buildUI(); this._scanExtensions(); } vfunc_command_line(commandLine) { this.activate(); let args = commandLine.get_arguments(); if (args.length) { let uuid = args[0]; this._skipMainWindow = true; // Strip off "extension:///" prefix which fakes a URI, if it exists uuid = stripPrefix(uuid, "extension:///"); if (!this._loaded) this._startupUuid = uuid; else if (!this._showPrefs(uuid)) this._skipMainWindow = false; } return 0; } }); var Expander = GObject.registerClass({ Properties: { 'label': GObject.ParamSpec.string( 'label', 'label', 'label', GObject.ParamFlags.READWRITE, null ) } }, class Expander extends Gtk.Box { _init(params = {}) { this._labelText = null; super._init(Object.assign(params, { orientation: Gtk.Orientation.VERTICAL, spacing: 0 })); this._frame = new Gtk.Frame({ shadow_type: Gtk.ShadowType.IN, hexpand: true }); let eventBox = new Gtk.EventBox(); this._frame.add(eventBox); let hbox = new Gtk.Box({ spacing: 6, margin: 12 }); eventBox.add(hbox); this._arrow = new Gtk.Image({ icon_name: 'pan-end-symbolic' }); hbox.add(this._arrow); this._label = new Gtk.Label({ label: this._labelText }); hbox.add(this._label); this._revealer = new Gtk.Revealer(); this._childBin = new Gtk.Frame({ shadow_type: Gtk.ShadowType.IN }); this._revealer.add(this._childBin); // Directly chain up to parent for internal children super.add(this._frame); super.add(this._revealer); let provider = new Gtk.CssProvider(); provider.load_from_data('* { border-top-width: 0; }'); this._childBin.get_style_context().add_provider( provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION ); this._gesture = new Gtk.GestureMultiPress({ widget: this._frame, button: 0, exclusive: true }); this._gesture.connect('released', (gesture, nPress) => { if (nPress == 1) this._revealer.reveal_child = !this._revealer.reveal_child; }); this._revealer.connect('notify::reveal-child', () => { if (this._revealer.reveal_child) this._arrow.icon_name = 'pan-down-symbolic'; else this._arrow.icon_name = 'pan-end-symbolic'; }); } get label() { return this._labelText; } set label(text) { if (this._labelText == text) return; if (this._label) this._label.label = text; this._labelText = text; this.notify('label'); } add(child) { // set expanded child this._childBin.get_children().forEach(c => { this._childBin.remove(c); }); if (child) this._childBin.add(child); } }); var EmptyPlaceholder = GObject.registerClass( class EmptyPlaceholder extends Gtk.Box { _init() { super._init({ orientation: Gtk.Orientation.VERTICAL, spacing: 6, margin: 32 }); let image = new Gtk.Image({ icon_name: 'application-x-addon-symbolic', pixel_size: 96, visible: true, vexpand: true, valign: Gtk.Align.END }); image.get_style_context().add_class(Gtk.STYLE_CLASS_DIM_LABEL); this.add(image); let label = new Gtk.Label({ label: `${_("No Extensions Installed" )}`, use_markup: true, visible: true }); label.get_style_context().add_class(Gtk.STYLE_CLASS_DIM_LABEL); this.add(label); let appInfo = Gio.DesktopAppInfo.new('org.gnome.Software.desktop'); let desc = new Gtk.Label({ label: _("Extensions can be installed through Software or extensions.gnome.org."), use_markup: true, wrap: true, justify: Gtk.Justification.CENTER, visible: true, max_width_chars: 50, hexpand: true, vexpand: (appInfo == null), halign: Gtk.Align.CENTER, valign: Gtk.Align.START }); this.add(desc); if (appInfo) { let button = new Gtk.Button({ label: _("Browse in Software"), image: new Gtk.Image({ icon_name: "org.gnome.Software-symbolic" }), always_show_image: true, margin_top: 12, visible: true, halign: Gtk.Align.CENTER, valign: Gtk.Align.START, vexpand: true }); this.add(button); button.connect('clicked', w => { let context = w.get_display().get_app_launch_context(); appInfo.launch([], context); }); } } }); var NoShellPlaceholder = GObject.registerClass( class NoShellPlaceholder extends Gtk.Box { _init() { super._init({ orientation: Gtk.Orientation.VERTICAL, spacing: 12, margin: 100, margin_bottom: 60 }); let label = new Gtk.Label({ label: '%s'.format( _("Something’s gone wrong")), use_markup: true }); label.get_style_context().add_class(Gtk.STYLE_CLASS_DIM_LABEL); this.add(label); label = new Gtk.Label({ label: _("We’re very sorry, but it was not possible to get the list of installed extensions. Make sure you are logged into GNOME and try again."), justify: Gtk.Justification.CENTER, wrap: true }); this.add(label); this.show_all(); } }); var DescriptionLabel = GObject.registerClass( class DescriptionLabel extends Gtk.Label { vfunc_get_preferred_height_for_width(width) { // Hack: Request the maximum height allowed by the line limit if (this.lines > 0) return super.vfunc_get_preferred_height_for_width(0); return super.vfunc_get_preferred_height_for_width(width); } }); var ExtensionRow = GObject.registerClass( class ExtensionRow extends Gtk.ListBoxRow { _init(extension) { super._init(); this._app = Gio.Application.get_default(); this._extension = extension; this._prefsModule = null; this._extensionStateChangedId = this._app.shellProxy.connectSignal( 'ExtensionStateChanged', (p, sender, [uuid, newState]) => { if (this.uuid !== uuid) return; this._extension = ExtensionUtils.deserializeExtension(newState); let state = (this._extension.state == ExtensionState.ENABLED); this._switch.state = state; this._switch.sensitive = this._canToggle(); }); this.connect('destroy', this._onDestroy.bind(this)); this._buildUI(); } get uuid() { return this._extension.uuid; } get name() { return this._extension.metadata.name; } get hasPrefs() { return this._extension.hasPrefs; } get url() { return this._extension.metadata.url; } _onDestroy() { if (!this._app.shellProxy) return; if (this._extensionStateChangedId) this._app.shellProxy.disconnectSignal(this._extensionStateChangedId); this._extensionStateChangedId = 0; } _buildUI() { let hbox = new Gtk.Box({ orientation: Gtk.Orientation.HORIZONTAL, hexpand: true, margin_end: 24, spacing: 24, margin: 12 }); this.add(hbox); let vbox = new Gtk.Box({ orientation: Gtk.Orientation.VERTICAL, spacing: 6, hexpand: true }); hbox.add(vbox); let name = GLib.markup_escape_text(this.name, -1); let label = new Gtk.Label({ label: '' + name + '', use_markup: true, halign: Gtk.Align.START }); vbox.add(label); let desc = this._extension.metadata.description.split('\n')[0]; label = new DescriptionLabel({ label: desc, wrap: true, lines: 2, ellipsize: Pango.EllipsizeMode.END, xalign: 0, yalign: 0 }); vbox.add(label); let button = new Gtk.Button({ valign: Gtk.Align.CENTER, visible: this.hasPrefs, no_show_all: true }); button.set_image(new Gtk.Image({ icon_name: 'emblem-system-symbolic', icon_size: Gtk.IconSize.BUTTON, visible: true })); button.get_style_context().add_class('circular'); hbox.add(button); this.prefsButton = button; this._switch = new Gtk.Switch({ valign: Gtk.Align.CENTER, sensitive: this._canToggle(), state: this._extension.state === ExtensionState.ENABLED }); this._switch.connect('notify::active', () => { if (this._switch.active) this._app.shellProxy.EnableExtensionRemote(this.uuid); else this._app.shellProxy.DisableExtensionRemote(this.uuid); }); this._switch.connect('state-set', () => true); hbox.add(this._switch); } _canToggle() { return this._extension.canChange; } get prefsModule() { if (!this._prefsModule) { ExtensionUtils.installImporter(this._extension); // give extension prefs access to their own extension object ExtensionUtils.getCurrentExtension = () => this._extension; this._prefsModule = this._extension.imports.prefs; this._prefsModule.init(this._extension.metadata); } return this._prefsModule; } }); 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}`); }, userdatadir: GLib.build_filenamev([GLib.get_user_data_dir(), 'gnome-shell']) }; String.prototype.format = Format.format; } function main(argv) { initEnvironment(); Gettext.bindtextdomain(Config.GETTEXT_PACKAGE, Config.LOCALEDIR); Gettext.textdomain(Config.GETTEXT_PACKAGE); new Application().run(argv); }