a7ec7583aa
Each row represents an extension, so it makes sense to associate the rows with the actual extensions instead of linking rows and extensions by looking up the UUID in the external extensions map in ExtensionUtils. This will also make it much easier to stop using the shared extension loading / map in favor of the extension D-Bus API. https://bugzilla.gnome.org/show_bug.cgi?id=789852
641 lines
19 KiB
JavaScript
641 lines
19 KiB
JavaScript
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 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;
|
||
}
|
||
|
||
_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: '<span size="x-large">%s</span>'.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', (proxy, senderName, [uuid, state]) => {
|
||
if (ExtensionUtils.extensions[uuid] !== undefined)
|
||
this._scanExtensions();
|
||
});
|
||
|
||
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);
|
||
}
|
||
|
||
_scanExtensions() {
|
||
let finder = new ExtensionUtils.ExtensionFinder();
|
||
finder.connect('extension-found', this._extensionFound.bind(this));
|
||
finder.scanExtensions();
|
||
this._extensionsLoaded();
|
||
}
|
||
|
||
_extensionFound(finder, 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: `<b><span size="x-large">${_("No Extensions Installed" )}</span></b>`,
|
||
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 <a href=\"https://extensions.gnome.org\">extensions.gnome.org</a>."),
|
||
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 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._extension = extension;
|
||
this._prefsModule = null;
|
||
|
||
this._settings = new Gio.Settings({ schema_id: 'org.gnome.shell' });
|
||
this._settings.connect('changed::enabled-extensions', () => {
|
||
this._switch.state = this._isEnabled();
|
||
});
|
||
this._settings.connect('changed::disable-extension-version-validation',
|
||
() => {
|
||
this._switch.sensitive = this._canEnable();
|
||
});
|
||
this._settings.connect('changed::disable-user-extensions',
|
||
() => {
|
||
this._switch.sensitive = this._canEnable();
|
||
});
|
||
|
||
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;
|
||
}
|
||
|
||
_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: '<b>' + name + '</b>',
|
||
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._canEnable(),
|
||
state: this._isEnabled() });
|
||
this._switch.connect('notify::active', () => {
|
||
if (this._switch.active)
|
||
this._enable();
|
||
else
|
||
this._disable();
|
||
});
|
||
this._switch.connect('state-set', () => true);
|
||
hbox.add(this._switch);
|
||
}
|
||
|
||
_canEnable() {
|
||
let checkVersion = !this._settings.get_boolean('disable-extension-version-validation');
|
||
|
||
return !this._settings.get_boolean('disable-user-extensions') &&
|
||
!(checkVersion && ExtensionUtils.isOutOfDate(this._extension));
|
||
}
|
||
|
||
_isEnabled() {
|
||
let extensions = this._settings.get_strv('enabled-extensions');
|
||
return extensions.includes(this.uuid);
|
||
}
|
||
|
||
_enable() {
|
||
let extensions = this._settings.get_strv('enabled-extensions');
|
||
if (extensions.includes(this.uuid))
|
||
return;
|
||
|
||
extensions.push(this.uuid);
|
||
this._settings.set_strv('enabled-extensions', extensions);
|
||
}
|
||
|
||
_disable() {
|
||
let extensions = this._settings.get_strv('enabled-extensions');
|
||
let pos = extensions.indexOf(this.uuid);
|
||
if (pos == -1)
|
||
return;
|
||
do {
|
||
extensions.splice(pos, 1);
|
||
pos = extensions.indexOf(this.uuid);
|
||
} while (pos != -1);
|
||
this._settings.set_strv('enabled-extensions', extensions);
|
||
}
|
||
|
||
get prefsModule() {
|
||
if (!this._prefsModule) {
|
||
ExtensionUtils.installImporter(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);
|
||
}
|