diff --git a/.gitignore b/.gitignore
index 4ec7bad8b..f6dc70b02 100644
--- a/.gitignore
+++ b/.gitignore
@@ -18,6 +18,8 @@ config
configure
data/gnome-shell.desktop
data/gnome-shell.desktop.in
+data/gnome-shell-extension-prefs.desktop
+data/gnome-shell-extension-prefs.desktop.in
data/gschemas.compiled
data/org.gnome.shell.gschema.xml
data/org.gnome.shell.gschema.valid
@@ -62,6 +64,7 @@ src/calendar-server/org.gnome.Shell.CalendarServer.service
src/gnome-shell
src/gnome-shell-calendar-server
src/gnome-shell-extension-tool
+src/gnome-shell-extension-prefs
src/gnome-shell-hotplug-sniffer
src/gnome-shell-jhbuild
src/gnome-shell-perf-helper
diff --git a/data/Makefile.am b/data/Makefile.am
index 04c8f4f6a..c077aa22d 100644
--- a/data/Makefile.am
+++ b/data/Makefile.am
@@ -1,5 +1,5 @@
desktopdir=$(datadir)/applications
-desktop_DATA = gnome-shell.desktop
+desktop_DATA = gnome-shell.desktop gnome-shell-extension-prefs.desktop
# We substitute in bindir so it works as an autostart
# file when built in a non-system prefix
@@ -69,6 +69,7 @@ shaders_DATA = \
EXTRA_DIST = \
gnome-shell.desktop.in.in \
+ gnome-shell-extension-prefs.in.in \
$(menu_DATA) \
$(shaders_DATA) \
$(convert_DATA) \
@@ -76,6 +77,7 @@ EXTRA_DIST = \
CLEANFILES = \
gnome-shell.desktop.in \
+ gnome-shell-extension-prefs.in \
$(desktop_DATA) \
$(gsettings_SCHEMAS) \
gschemas.compiled
diff --git a/data/gnome-shell-extension-prefs.desktop.in.in b/data/gnome-shell-extension-prefs.desktop.in.in
new file mode 100644
index 000000000..9f8bbf405
--- /dev/null
+++ b/data/gnome-shell-extension-prefs.desktop.in.in
@@ -0,0 +1,12 @@
+[Desktop Entry]
+Type=Application
+_Name=GNOME Shell Extension Preferences
+_Comment=Configure GNOME Shell Extensions
+Exec=@bindir@/gnome-shell-extension-prefs %u
+X-GNOME-Bugzilla-Bugzilla=GNOME
+X-GNOME-Bugzilla-Product=gnome-shell
+X-GNOME-Bugzilla-Component=extensions
+X-GNOME-Bugzilla-Version=@VERSION@
+Categories=GNOME;GTK;
+OnlyShowIn=GNOME;
+NoDisplay=true
diff --git a/js/Makefile.am b/js/Makefile.am
index f7ce7cd69..4c41bffee 100644
--- a/js/Makefile.am
+++ b/js/Makefile.am
@@ -7,6 +7,7 @@ nobase_dist_js_DATA = \
gdm/fingerprint.js \
gdm/loginDialog.js \
gdm/powerMenu.js \
+ extensionPrefs/main.js \
misc/config.js \
misc/docInfo.js \
misc/extensionUtils.js \
diff --git a/js/extensionPrefs/main.js b/js/extensionPrefs/main.js
new file mode 100644
index 000000000..82b67b4b1
--- /dev/null
+++ b/js/extensionPrefs/main.js
@@ -0,0 +1,280 @@
+
+const Lang = imports.lang;
+const Gettext = imports.gettext;
+const GLib = imports.gi.GLib;
+const GObject = imports.gi.GObject;
+const Gio = imports.gi.Gio;
+const Gtk = imports.gi.Gtk;
+const Pango = imports.gi.Pango;
+
+const _ = Gettext.gettext;
+
+const Config = imports.misc.config;
+const Format = imports.misc.format;
+const ExtensionUtils = imports.misc.extensionUtils;
+
+
+const GnomeShellIface =
+
+
+
+
+
+;
+
+const GnomeShellProxy = Gio.DBusProxy.makeProxyWrapper(GnomeShellIface);
+
+function stripPrefix(string, prefix) {
+ if (string.slice(0, prefix.length) == prefix)
+ return string.slice(prefix.length);
+ return string;
+}
+
+const Application = new Lang.Class({
+ Name: 'Application',
+ _init: function() {
+ GLib.set_prgname('gnome-shell-extension-prefs');
+ this.application = new Gtk.Application({
+ application_id: 'org.gnome.shell.ExtensionPrefs',
+ flags: Gio.ApplicationFlags.HANDLES_COMMAND_LINE
+ });
+
+ this.application.connect('activate', Lang.bind(this, this._onActivate));
+ this.application.connect('command-line', Lang.bind(this, this._onCommandLine));
+ this.application.connect('startup', Lang.bind(this, this._onStartup));
+
+ this._extensionMetas = {};
+ this._extensionPrefsModules = {};
+
+ this._extensionIters = {};
+ },
+
+ _buildModel: function() {
+ this._model = new Gtk.ListStore();
+ this._model.set_column_types([GObject.TYPE_STRING, GObject.TYPE_STRING]);
+ },
+
+ _extensionAvailable: function(uuid) {
+ let meta = this._extensionMetas[uuid];
+
+ if (!meta)
+ return false;
+
+ if (ExtensionUtils.isOutOfDate(meta))
+ return false;
+
+ if (!meta.dir.get_child('prefs.js').query_exists(null))
+ return false;
+
+ return true;
+ },
+
+ _setExtensionInsensitive: function(layout, cell, model, iter, data) {
+ let uuid = model.get_value(iter, 0);
+ if (!this._extensionAvailable(uuid))
+ cell.set_sensitive(false);
+ },
+
+ _getExtensionPrefsModule: function(meta) {
+ if (this._extensionPrefsModules.hasOwnProperty(meta.uuid))
+ return this._extensionPrefsModules[meta.uuid];
+
+ ExtensionUtils.installImporter(meta);
+
+ let prefsModule = meta.importer.prefs;
+ prefsModule.init(meta);
+
+ this._extensionPrefsModules[meta.uuid] = prefsModule;
+ return prefsModule;
+ },
+
+ _selectExtension: function(uuid) {
+ if (!this._extensionAvailable(uuid))
+ return;
+
+ let meta = this._extensionMetas[uuid];
+ let widget;
+
+ try {
+ let prefsModule = this._getExtensionPrefsModule(meta);
+ widget = prefsModule.buildPrefsWidget(meta);
+ } catch (e) {
+ widget = this._buildErrorUI(meta, e);
+ }
+
+ // Destroy the current prefs widget, if it exists
+ if (this._extensionPrefsBin.get_child())
+ this._extensionPrefsBin.get_child().destroy();
+
+ this._extensionPrefsBin.add(widget);
+ this._extensionSelector.set_active_iter(this._extensionIters[uuid]);
+ },
+
+ _extensionSelected: function() {
+ let [success, iter] = this._extensionSelector.get_active_iter();
+ if (!success)
+ return;
+
+ let uuid = this._model.get_value(iter, 0);
+ this._selectExtension(uuid);
+ },
+
+ _buildErrorUI: function(meta, exc) {
+ let box = new Gtk.Box({ orientation: Gtk.Orientation.VERTICAL });
+ let label = new Gtk.Label({
+ label: _("There was an error loading the preferences dialog for %s:").format(meta.name)
+ });
+ box.add(label);
+
+ let errortext = '';
+ errortext += exc;
+ errortext += '\n\n';
+ errortext += 'Stack trace:\n';
+
+ // Indent stack trace.
+ errortext += exc.stack.split('\n').map(function(line) {
+ return ' ' + line;
+ }).join('\n');
+
+ let scroll = new Gtk.ScrolledWindow({ vexpand: true });
+ let buffer = new Gtk.TextBuffer({ text: errortext });
+ let textview = new Gtk.TextView({ buffer: buffer });
+ textview.override_font(Pango.font_description_from_string('monospace'));
+ scroll.add(textview);
+ box.add(scroll);
+
+ box.show_all();
+ return box;
+ },
+
+ _buildUI: function(app) {
+ this._window = new Gtk.ApplicationWindow({ application: app,
+ window_position: Gtk.WindowPosition.CENTER,
+ title: _("GNOME Shell Extension Preferences") });
+
+ this._window.set_size_request(600, 400);
+
+ let vbox = new Gtk.Box({ orientation: Gtk.Orientation.VERTICAL });
+ this._window.add(vbox);
+
+ let toolbar = new Gtk.Toolbar();
+ toolbar.get_style_context().add_class(Gtk.STYLE_CLASS_PRIMARY_TOOLBAR);
+ vbox.add(toolbar);
+ let toolitem;
+
+ let label = new Gtk.Label({ label: _("Extension"),
+ use_markup: true });
+ toolitem = new Gtk.ToolItem({ child: label });
+ toolbar.add(toolitem);
+
+ this._extensionSelector = new Gtk.ComboBox({ model: this._model,
+ margin_left: 8,
+ hexpand: true });
+ this._extensionSelector.get_style_context().add_class(Gtk.STYLE_CLASS_RAISED);
+
+ let renderer = new Gtk.CellRendererText();
+ this._extensionSelector.pack_start(renderer, true);
+ this._extensionSelector.add_attribute(renderer, 'text', 1);
+ this._extensionSelector.set_cell_data_func(renderer, Lang.bind(this, this._setExtensionInsensitive), null);
+ this._extensionSelector.connect('changed', Lang.bind(this, this._extensionSelected));
+
+ toolitem = new Gtk.ToolItem({ child: this._extensionSelector });
+ toolitem.set_expand(true);
+ toolbar.add(toolitem);
+
+ this._extensionPrefsBin = new Gtk.Frame();
+ vbox.add(this._extensionPrefsBin);
+
+ let label = new Gtk.Label({
+ label: _("Select an extension to configure using the combobox above."),
+ vexpand: true
+ });
+
+ this._extensionPrefsBin.add(label);
+
+ this._shellProxy = new GnomeShellProxy(Gio.DBus.session, 'org.gnome.Shell', '/org/gnome/Shell');
+ this._shellProxy.connectSignal('ExtensionStatusChanged', Lang.bind(this, function(proxy, senderName, [uuid, state, error]) {
+ if (!this._extensionMetas.hasOwnProperty(uuid))
+ this._scanExtensions();
+ }));
+
+ this._window.show_all();
+ },
+
+ _scanExtensions: function() {
+ ExtensionUtils.scanExtensions(Lang.bind(this, function(uuid, dir, type) {
+ if (this._extensionMetas.hasOwnProperty(uuid))
+ return;
+
+ let meta;
+ try {
+ meta = ExtensionUtils.loadMetadata(uuid, dir, type);
+ } catch(e) {
+ global.logError('' + e);
+ return;
+ }
+
+ this._extensionMetas[uuid] = meta;
+
+ let iter = this._model.append();
+ this._model.set(iter, [0, 1], [uuid, meta.name]);
+ this._extensionIters[uuid] = iter;
+ }));
+ },
+
+ _onActivate: function() {
+ this._window.present();
+ },
+
+ _onStartup: function(app) {
+ this._buildModel();
+ this._buildUI(app);
+ this._scanExtensions();
+ },
+
+ _onCommandLine: function(app, commandLine) {
+ app.activate();
+ let args = commandLine.get_arguments();
+ if (args.length) {
+ let uuid = args[0];
+
+ // Strip off "extension:///" prefix which fakes a URI, if it exists
+ uuid = stripPrefix(uuid, "extension:///");
+
+ if (!this._extensionAvailable(uuid))
+ return 1;
+
+ this._selectExtension(uuid);
+ }
+ return 0;
+ }
+});
+
+function initEnvironment() {
+ // Monkey-patch in a "global" object that fakes some Shell utilities
+ // that ExtensionUtils depends on.
+ window.global = {
+ log: function() {
+ print([].join.call(arguments, ', '));
+ },
+
+ logError: function(s) {
+ global.log('ERROR: ' + s);
+ },
+
+ userdatadir: GLib.build_filenamev([GLib.get_user_data_dir(), 'gnome-shell'])
+ };
+
+ String.prototype.format = Format.format;
+}
+
+function main(argv) {
+ initEnvironment();
+ ExtensionUtils.init();
+
+ Gettext.bindtextdomain(Config.GETTEXT_PACKAGE, Config.LOCALEDIR);
+ Gettext.textdomain(Config.GETTEXT_PACKAGE);
+
+ let app = new Application();
+ app.application.run(argv);
+}
diff --git a/js/misc/config.js.in b/js/misc/config.js.in
index a35fe28b2..6cbf9324d 100644
--- a/js/misc/config.js.in
+++ b/js/misc/config.js.in
@@ -10,3 +10,7 @@ const GJS_VERSION = '@GJS_VERSION@';
const HAVE_BLUETOOTH = @HAVE_BLUETOOTH@;
/* The system TLS CA list */
const SHELL_SYSTEM_CA_FILE = '@SHELL_SYSTEM_CA_FILE@';
+/* gettext package */
+const GETTEXT_PACKAGE = '@GETTEXT_PACKAGE@';
+/* locale dir */
+const LOCALEDIR = '@datadir@/locale';
diff --git a/src/Makefile.am b/src/Makefile.am
index a46d779b4..464831a76 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -27,8 +27,8 @@ CLEANFILES += $(service_DATA)
CLEANFILES += $(gir_DATA) $(typelib_DATA)
-bin_SCRIPTS += gnome-shell-extension-tool
-EXTRA_DIST += gnome-shell-extension-tool.in
+bin_SCRIPTS += gnome-shell-extension-tool gnome-shell-extension-prefs
+EXTRA_DIST += gnome-shell-extension-tool.in gnome-shell-extension-prefs.in
bin_PROGRAMS = gnome-shell-real
if USE_JHBUILD_WRAPPER_SCRIPT
@@ -52,11 +52,13 @@ generated_script_substitutions = \
-e "s|@datadir[@]|$(datadir)|g" \
-e "s|@libexecdir[@]|$(libexecdir)|g" \
-e "s|@libdir[@]|$(libdir)|g" \
+ -e "s|@pkglibdir[@]|$(pkglibdir)|g" \
-e "s|@JHBUILD_TYPELIBDIR[@]|$(JHBUILD_TYPELIBDIR)|g" \
-e "s|@pkgdatadir[@]|$(pkgdatadir)|g" \
-e "s|@PYTHON[@]|$(PYTHON)|g" \
-e "s|@VERSION[@]|$(VERSION)|g" \
- -e "s|@sysconfdir[@]|$(sysconfdir)|g"
+ -e "s|@sysconfdir[@]|$(sysconfdir)|g" \
+ -e "s|@GJS_CONSOLE[@]|$(GJS_CONSOLE)|g"
gnome-shell-jhbuild: gnome-shell-jhbuild.in gnome-shell-real Makefile
$(AM_V_GEN) sed $(generated_script_substitutions) $< > $@.tmp && mv $@.tmp $@ && chmod a+x $@
@@ -66,6 +68,9 @@ gnome-shell-jhbuild: gnome-shell-jhbuild.in gnome-shell-real Makefile
gnome-shell-extension-tool: gnome-shell-extension-tool.in Makefile
$(AM_V_GEN) sed $(generated_script_substitutions) $< > $@.tmp && mv $@.tmp $@ && chmod a+x $@
+gnome-shell-extension-prefs: gnome-shell-extension-prefs.in Makefile
+ $(AM_V_GEN) sed $(generated_script_substitutions) $< > $@.tmp && mv $@.tmp $@ && chmod a+x $@
+
CLEANFILES += gnome-shell $(bin_SCRIPTS)
include Makefile-st.am
diff --git a/src/gnome-shell-extension-prefs.in b/src/gnome-shell-extension-prefs.in
new file mode 100644
index 000000000..fa3725457
--- /dev/null
+++ b/src/gnome-shell-extension-prefs.in
@@ -0,0 +1,15 @@
+#!/bin/bash
+
+if [ -n "$GI_TYPELIB_PATH" ]; then
+ export GI_TYPELIB_PATH=@pkglibdir@
+else
+ export GI_TYPELIB_PATH=@pkglibdir@:$GI_TYPELIB_PATH
+fi
+
+if [ -n "$LD_LIBRARY_PATH" ] ; then
+ export LD_LIBRARY_PATH=@pkglibdir@
+else
+ export LD_LIBRARY_PATH=@pkglibdir@:$LD_LIBRARY_PATH
+fi
+
+@GJS_CONSOLE@ -I @pkgdatadir@/js -c "const Main = imports.extensionPrefs.main; Main.main(ARGV);" "$@"