From 7f32e65a87878ce3be89f3d79fd5ebe6d76d4d37 Mon Sep 17 00:00:00 2001 From: CentOS Sources Date: Tue, 18 May 2021 02:42:55 -0400 Subject: [PATCH] import gnome-shell-3.32.2-30.el8 --- ...Notify-about-extension-issues-on-upd.patch | 72 - SOURCES/0001-gdm-add-AuthList-control.patch | 2 +- ...able-keyboard-if-ClutterDeviceManage.patch | 39 + ...n-Dump-stack-on-segfaults-by-default.patch | 38 + ...recording-when-screen-size-or-resour.patch | 253 ++ ...-workspace-from-startup-notification.patch | 78 + SOURCES/caps-lock-warning.patch | 488 +++ SOURCES/extension-updates.patch | 3811 +++++++++++++++++ SOURCES/fix-some-js-warnings.patch | 182 + SOURCES/osk-fixes.patch | 117 + SOURCES/wake-up-on-deactivate.patch | 79 + SPECS/gnome-shell.spec | 62 +- 12 files changed, 5146 insertions(+), 75 deletions(-) delete mode 100644 SOURCES/0001-extensionSystem-Notify-about-extension-issues-on-upd.patch create mode 100644 SOURCES/0001-keyboard-Only-enable-keyboard-if-ClutterDeviceManage.patch create mode 100644 SOURCES/0001-main-Dump-stack-on-segfaults-by-default.patch create mode 100644 SOURCES/0001-screencast-Stop-recording-when-screen-size-or-resour.patch create mode 100644 SOURCES/0001-shell-app-Handle-workspace-from-startup-notification.patch create mode 100644 SOURCES/caps-lock-warning.patch create mode 100644 SOURCES/extension-updates.patch create mode 100644 SOURCES/fix-some-js-warnings.patch create mode 100644 SOURCES/osk-fixes.patch create mode 100644 SOURCES/wake-up-on-deactivate.patch diff --git a/SOURCES/0001-extensionSystem-Notify-about-extension-issues-on-upd.patch b/SOURCES/0001-extensionSystem-Notify-about-extension-issues-on-upd.patch deleted file mode 100644 index e5efa14..0000000 --- a/SOURCES/0001-extensionSystem-Notify-about-extension-issues-on-upd.patch +++ /dev/null @@ -1,72 +0,0 @@ -From d1a20dc80c3414ba4cb7bf839a25de49d30ab400 Mon Sep 17 00:00:00 2001 -From: =?UTF-8?q?Florian=20M=C3=BCllner?= -Date: Mon, 21 Sep 2015 20:18:12 +0200 -Subject: [PATCH] extensionSystem: Notify about extension issues on update - ---- - js/ui/extensionSystem.js | 34 +++++++++++++++++++++++++++++++++- - 1 file changed, 33 insertions(+), 1 deletion(-) - -diff --git a/js/ui/extensionSystem.js b/js/ui/extensionSystem.js -index 9ffdb4f3d..eb820ba4f 100644 ---- a/js/ui/extensionSystem.js -+++ b/js/ui/extensionSystem.js -@@ -1,8 +1,9 @@ - // -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*- - --const { Gio, St } = imports.gi; -+const { Gio, GLib, St } = imports.gi; - const Signals = imports.signals; - -+const Config = imports.misc.config; - const ExtensionUtils = imports.misc.extensionUtils; - const Main = imports.ui.main; - -@@ -312,6 +313,36 @@ function _onVersionValidationChanged() { - } - } - -+function _doUpdateCheck() { -+ let version = Config.PACKAGE_VERSION.split('.'); -+ if (parseInt(version[1]) % 2 == 0) -+ version.pop(); -+ -+ let pkgCacheDir = GLib.get_user_cache_dir() + '/gnome-shell/'; -+ let updateStamp = Gio.file_new_for_path(pkgCacheDir + -+ 'update-check-' + version.join('.')); -+ if (updateStamp.query_exists(null)) -+ return; -+ -+ GLib.mkdir_with_parents (pkgCacheDir, 0o755); -+ updateStamp.create(0, null).close(null); -+ -+ let nOutdated = enabledExtensions.reduce(function(n, uuid) { -+ let extension = ExtensionUtils.extensions[uuid]; -+ if (extension && extension.state == ExtensionState.OUT_OF_DATE) -+ n++; -+ return n; -+ }, 0); -+ -+ if (nOutdated == 0) -+ return; -+ -+ Main.notify(ngettext("%d extension is out of date", -+ "%d extensions are out of date", -+ nOutdated).format(nOutdated), -+ _("You can visit http://extensions.gnome.org for updates")); -+} -+ - function _loadExtensions() { - global.settings.connect('changed::' + ENABLED_EXTENSIONS_KEY, onEnabledExtensionsChanged); - global.settings.connect('changed::' + DISABLE_USER_EXTENSIONS_KEY, onEnabledExtensionsChanged); -@@ -326,6 +357,7 @@ function _loadExtensions() { - extension.type = ExtensionUtils.ExtensionType.SESSION_MODE; - }); - finder.scanExtensions(); -+ _doUpdateCheck(); - } - - function enableAllExtensions() { --- -2.21.0 - diff --git a/SOURCES/0001-gdm-add-AuthList-control.patch b/SOURCES/0001-gdm-add-AuthList-control.patch index 0ca4a8f..2bd503b 100644 --- a/SOURCES/0001-gdm-add-AuthList-control.patch +++ b/SOURCES/0001-gdm-add-AuthList-control.patch @@ -100,7 +100,7 @@ index 000000000..fc1c3d6e4 +}); +Signals.addSignalMethods(AuthListItem.prototype); + -+const AuthList = new Lang.Class({ ++var AuthList = new Lang.Class({ + Name: 'AuthList', + + _init() { diff --git a/SOURCES/0001-keyboard-Only-enable-keyboard-if-ClutterDeviceManage.patch b/SOURCES/0001-keyboard-Only-enable-keyboard-if-ClutterDeviceManage.patch new file mode 100644 index 0000000..a781d43 --- /dev/null +++ b/SOURCES/0001-keyboard-Only-enable-keyboard-if-ClutterDeviceManage.patch @@ -0,0 +1,39 @@ +From 2acede02f30833c3fb891db8483f933f7b41508c Mon Sep 17 00:00:00 2001 +From: rpm-build +Date: Wed, 21 Oct 2020 21:32:03 +0200 +Subject: [PATCH] keyboard: Only enable keyboard if + ClutterDeviceManager::touch-mode is enabled + +--- + js/ui/keyboard.js | 7 ++++++- + 1 file changed, 6 insertions(+), 1 deletion(-) + +diff --git a/js/ui/keyboard.js b/js/ui/keyboard.js +index 94b5325..b1ee270 100644 +--- a/js/ui/keyboard.js ++++ b/js/ui/keyboard.js +@@ -1051,6 +1051,9 @@ var Keyboard = class Keyboard { + this._suggestions = null; + this._emojiKeyVisible = true; + ++ let manager = Clutter.DeviceManager.get_default(); ++ manager.connect('notify::touch-mode', this._syncEnabled.bind(this)); ++ + this._focusTracker = new FocusTracker(); + this._focusTracker.connect('position-changed', this._onFocusPositionChanged.bind(this)); + this._focusTracker.connect('reset', () => { +@@ -1120,8 +1123,10 @@ var Keyboard = class Keyboard { + + _syncEnabled() { + let wasEnabled = this._enabled; ++ let manager = Clutter.DeviceManager.get_default(); ++ let autoEnabled = manager.get_touch_mode() && this._lastDeviceIsTouchscreen(); + this._enableKeyboard = this._a11yApplicationsSettings.get_boolean(SHOW_KEYBOARD); +- this._enabled = this._enableKeyboard || this._lastDeviceIsTouchscreen(); ++ this._enabled = this._enableKeyboard || autoEnabled; + if (!this._enabled && !this._keyboardController) + return; + +-- +2.26.2 + diff --git a/SOURCES/0001-main-Dump-stack-on-segfaults-by-default.patch b/SOURCES/0001-main-Dump-stack-on-segfaults-by-default.patch new file mode 100644 index 0000000..75b9b1a --- /dev/null +++ b/SOURCES/0001-main-Dump-stack-on-segfaults-by-default.patch @@ -0,0 +1,38 @@ +From ba3ce64fbbce20192a55f9d438d1032c0bac0557 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Florian=20M=C3=BCllner?= +Date: Thu, 29 Oct 2020 18:21:06 +0100 +Subject: [PATCH] main: Dump stack on segfaults by default + +--- + src/main.c | 8 ++++++-- + 1 file changed, 6 insertions(+), 2 deletions(-) + +diff --git a/src/main.c b/src/main.c +index 245837783..788309de7 100644 +--- a/src/main.c ++++ b/src/main.c +@@ -39,6 +39,7 @@ static int caught_signal = 0; + #define DBUS_REQUEST_NAME_REPLY_PRIMARY_OWNER 1 + #define DBUS_REQUEST_NAME_REPLY_ALREADY_OWNER 4 + ++#define DEFAULT_SHELL_DEBUG SHELL_DEBUG_BACKTRACE_SEGFAULTS + enum { + SHELL_DEBUG_BACKTRACE_WARNINGS = 1, + SHELL_DEBUG_BACKTRACE_SEGFAULTS = 2, +@@ -268,8 +269,11 @@ shell_init_debug (const char *debug_env) + { "backtrace-segfaults", SHELL_DEBUG_BACKTRACE_SEGFAULTS }, + }; + +- _shell_debug = g_parse_debug_string (debug_env, keys, +- G_N_ELEMENTS (keys)); ++ if (debug_env) ++ _shell_debug = g_parse_debug_string (debug_env, keys, ++ G_N_ELEMENTS (keys)); ++ else ++ _shell_debug = DEFAULT_SHELL_DEBUG; + } + + static void +-- +2.29.2 + diff --git a/SOURCES/0001-screencast-Stop-recording-when-screen-size-or-resour.patch b/SOURCES/0001-screencast-Stop-recording-when-screen-size-or-resour.patch new file mode 100644 index 0000000..582ff4e --- /dev/null +++ b/SOURCES/0001-screencast-Stop-recording-when-screen-size-or-resour.patch @@ -0,0 +1,253 @@ +From 67a4506d4d8a0cbbaca5df4adfc309e54e557aee Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Jonas=20=C3=85dahl?= +Date: Tue, 5 Jan 2021 12:04:23 +0100 +Subject: [PATCH] screencast: Stop recording when screen size or resource scale + change + +Video encoders don't really handle changing the size of the video, and if +we'd e.g. change resolution while recording, we would end up with a corrupt +video file. Handle this more gracefully by stopping the recording if the +conditions change. +--- + js/ui/screencast.js | 92 +++++++++++++++++++++++++++++++++++++++++--- + src/shell-recorder.c | 50 ++++++++++++------------ + src/shell-recorder.h | 1 + + 3 files changed, 114 insertions(+), 29 deletions(-) + +diff --git a/js/ui/screencast.js b/js/ui/screencast.js +index 0b0b14a8e..54f8fb5ae 100644 +--- a/js/ui/screencast.js ++++ b/js/ui/screencast.js +@@ -1,6 +1,6 @@ + // -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*- + +-const { Gio, GLib, Shell } = imports.gi; ++const { Gio, GLib, Meta, Shell } = imports.gi; + const Signals = imports.signals; + + const Main = imports.ui.main; +@@ -53,16 +53,27 @@ var ScreencastService = class { + this._stopRecordingForSender(name); + } + +- _stopRecordingForSender(sender) { ++ _stopRecordingForSender(sender, closeNow=false) { + let recorder = this._recorders.get(sender); + if (!recorder) + return false; + + Gio.bus_unwatch_name(recorder._watchNameId); +- recorder.close(); ++ if (closeNow) ++ recorder.close_now(); ++ else ++ recorder.close(); + this._recorders.delete(sender); + this.emit('updated'); + ++ let connection = this._dbusImpl.get_connection(); ++ let info = this._dbusImpl.get_info(); ++ connection.emit_signal(sender, ++ this._dbusImpl.get_object_path(), ++ info ? info.name : null, ++ 'Stopped', ++ null); ++ + return true; + } + +@@ -78,6 +89,53 @@ var ScreencastService = class { + recorder.set_draw_cursor(options['draw-cursor']); + } + ++ _ensureResourceScaleChangedHandler() { ++ if (this._resourceScaleChangedHandlerId) ++ return; ++ ++ this._resourceScaleChangedHandlerId = ++ global.stage.connect('notify::resource-scale', ++ () => { ++ for (let sender of this._recorders.keys()) { ++ let recorder = this._recorders.get(sender); ++ ++ if (!recorder.is_recording()) ++ continue; ++ ++ this._stopRecordingForSender(sender, true); ++ } ++ }); ++ } ++ ++ _ensureMonitorsChangedHandler() { ++ if (this._monitorsChangedHandlerId) ++ return; ++ ++ this._monitorsChangedHandlerId = Main.layoutManager.connect('monitors-changed', ++ () => { ++ for (let sender of this._recorders.keys()) { ++ let recorder = this._recorders.get(sender); ++ ++ if (!recorder.is_recording()) ++ continue; ++ ++ let geometry = recorder._geometry; ++ let screenWidth = global.screen_width; ++ let screenHeight = global.screen_height; ++ ++ if (recorder._isAreaScreecast) { ++ if (geometry.x + geometry.width > screenWidth || ++ geometry.y + geometry.height > screenHeight) ++ this._stopRecordingForSender(sender, true); ++ } else { ++ if (geometry.width != screenWidth || ++ geometry.height != screenHeight) ++ this._stopRecordingForSender(sender, true); ++ } ++ } ++ }); ++ } ++ + ScreencastAsync(params, invocation) { + let returnValue = [false, '']; + if (!Main.sessionMode.allowScreencast || +@@ -95,8 +153,20 @@ var ScreencastService = class { + this._applyOptionalParameters(recorder, options); + let [success, fileName] = recorder.record(); + returnValue = [success, fileName ? fileName : '']; +- if (!success) ++ if (success) { ++ recorder._isAreaScreecast = false; ++ recorder._geometry = ++ new Meta.Rectangle({ ++ x: 0, ++ y: 0, ++ width: global.screen_width, ++ height: global.screen_height ++ }); ++ this._ensureResourceScaleChangedHandler(); ++ this._ensureMonitorsChangedHandler(); ++ } else { + this._stopRecordingForSender(sender); ++ } + } + + invocation.return_value(GLib.Variant.new('(bs)', returnValue)); +@@ -131,8 +201,20 @@ var ScreencastService = class { + this._applyOptionalParameters(recorder, options); + let [success, fileName] = recorder.record(); + returnValue = [success, fileName ? fileName : '']; +- if (!success) ++ if (success) { ++ recorder._isAreaScreecast = true; ++ recorder._geometry = ++ new Meta.Rectangle({ ++ x: x, ++ y: y, ++ width: width, ++ height: height ++ }); ++ this._ensureResourceScaleChangedHandler(); ++ this._ensureMonitorsChangedHandler(); ++ } else { + this._stopRecordingForSender(sender); ++ } + } + + invocation.return_value(GLib.Variant.new('(bs)', returnValue)); +diff --git a/src/shell-recorder.c b/src/shell-recorder.c +index 0203ecf1c..e561a0152 100644 +--- a/src/shell-recorder.c ++++ b/src/shell-recorder.c +@@ -511,21 +511,6 @@ recorder_update_size (ShellRecorder *recorder) + } + } + +-static void +-recorder_on_stage_notify_size (GObject *object, +- GParamSpec *pspec, +- ShellRecorder *recorder) +-{ +- recorder_update_size (recorder); +- +- /* This breaks the recording but tweaking the GStreamer pipeline a bit +- * might make it work, at least if the codec can handle a stream where +- * the frame size changes in the middle. +- */ +- if (recorder->current_pipeline) +- recorder_pipeline_set_caps (recorder->current_pipeline); +-} +- + static gboolean + recorder_idle_redraw (gpointer data) + { +@@ -622,12 +607,6 @@ recorder_connect_stage_callbacks (ShellRecorder *recorder) + G_CALLBACK (recorder_on_stage_destroy), recorder); + g_signal_connect_after (recorder->stage, "paint", + G_CALLBACK (recorder_on_stage_paint), recorder); +- g_signal_connect (recorder->stage, "notify::width", +- G_CALLBACK (recorder_on_stage_notify_size), recorder); +- g_signal_connect (recorder->stage, "notify::height", +- G_CALLBACK (recorder_on_stage_notify_size), recorder); +- g_signal_connect (recorder->stage, "notify::resource-scale", +- G_CALLBACK (recorder_on_stage_notify_size), recorder); + } + + static void +@@ -639,9 +618,6 @@ recorder_disconnect_stage_callbacks (ShellRecorder *recorder) + g_signal_handlers_disconnect_by_func (recorder->stage, + (void *)recorder_on_stage_paint, + recorder); +- g_signal_handlers_disconnect_by_func (recorder->stage, +- (void *)recorder_on_stage_notify_size, +- recorder); + + /* We don't don't deselect for cursor changes in case someone else just + * happened to be selecting for cursor events on the same window; sending +@@ -1578,6 +1554,32 @@ shell_recorder_record (ShellRecorder *recorder, + return TRUE; + } + ++/** ++ * shell_recorder_close_now: ++ * @recorder: the #ShellRecorder ++ * ++ * Stops recording immediately. It's possible to call shell_recorder_record() ++ * again to reopen a new recording stream, but unless change the recording ++ * filename, this may result in the old recording being overwritten. ++ */ ++void ++shell_recorder_close_now (ShellRecorder *recorder) ++{ ++ g_return_if_fail (SHELL_IS_RECORDER (recorder)); ++ g_return_if_fail (recorder->state != RECORDER_STATE_CLOSED); ++ ++ recorder_remove_update_pointer_timeout (recorder); ++ recorder_close_pipeline (recorder); ++ ++ recorder->state = RECORDER_STATE_CLOSED; ++ ++ /* Reenable after the recording */ ++ meta_enable_unredirect_for_display (shell_global_get_display (shell_global_get ())); ++ ++ /* Release the refcount we took when we started recording */ ++ g_object_unref (recorder); ++} ++ + /** + * shell_recorder_close: + * @recorder: the #ShellRecorder +diff --git a/src/shell-recorder.h b/src/shell-recorder.h +index c1e0e6368..1c3e6aab4 100644 +--- a/src/shell-recorder.h ++++ b/src/shell-recorder.h +@@ -37,6 +37,7 @@ void shell_recorder_set_area (ShellRecorder *recorder, + gboolean shell_recorder_record (ShellRecorder *recorder, + char **filename_used); + void shell_recorder_close (ShellRecorder *recorder); ++void shell_recorder_close_now (ShellRecorder *recorder); + void shell_recorder_pause (ShellRecorder *recorder); + gboolean shell_recorder_is_recording (ShellRecorder *recorder); + +-- +2.27.0 + diff --git a/SOURCES/0001-shell-app-Handle-workspace-from-startup-notification.patch b/SOURCES/0001-shell-app-Handle-workspace-from-startup-notification.patch new file mode 100644 index 0000000..e7f0db8 --- /dev/null +++ b/SOURCES/0001-shell-app-Handle-workspace-from-startup-notification.patch @@ -0,0 +1,78 @@ +From 391f262aee82ac12fcf99951d6b2df362f734b31 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Florian=20M=C3=BCllner?= +Date: Mon, 15 Jun 2020 20:41:45 +0200 +Subject: [PATCH] shell/app: Handle workspace from startup notifications + +Launching applications on a particular workspace works through +launch contexts and startup notifications. While this is no +longer required by a launcher/WM split, in theory this allows +us to reliably identify the correct window to apply startup +properties to. + +However in practice we fail more often than not: Missing support in +toolkits, differences between display protocols, D-Bus activation +and single-instance applications all provide their own pitfalls. + +So instead, take advantage of the fact that launcher and WM live in +the same process, and go with the unsophisticated approach: Just +remember the last workspace that was requested when launching an +app, then move the next window that is associated with the app to +that workspace. + +This will break X11 applications that set an initial workspace, but +that's legacy functionality anyway (given that there's no wayland +protocol for that functionality), and seems a price worth paying +for making launching apps on workspaces more reliable. +--- + src/shell-app.c | 19 +++++++++++-------- + 1 file changed, 11 insertions(+), 8 deletions(-) + +diff --git a/src/shell-app.c b/src/shell-app.c +index 7d40186c9..f716bc5f8 100644 +--- a/src/shell-app.c ++++ b/src/shell-app.c +@@ -1067,6 +1067,10 @@ _shell_app_add_window (ShellApp *app, + if (!app->running_state) + create_running_state (app); + ++ if (app->started_on_workspace >= 0) ++ meta_window_change_workspace_by_index (window, app->started_on_workspace, FALSE); ++ app->started_on_workspace = -1; ++ + app->running_state->window_sort_stale = TRUE; + app->running_state->windows = g_slist_prepend (app->running_state->windows, g_object_ref (window)); + g_signal_connect_object (window, "unmanaged", G_CALLBACK(shell_app_on_unmanaged), app, 0); +@@ -1156,16 +1160,14 @@ _shell_app_handle_startup_sequence (ShellApp *app, + shell_app_state_transition (app, SHELL_APP_STATE_STARTING); + meta_x11_display_focus_the_no_focus_window (x11_display, + meta_startup_sequence_get_timestamp (sequence)); +- app->started_on_workspace = meta_startup_sequence_get_workspace (sequence); + } + +- if (!starting) +- { +- if (app->running_state && app->running_state->windows) +- shell_app_state_transition (app, SHELL_APP_STATE_RUNNING); +- else /* application have > 1 .desktop file */ +- shell_app_state_transition (app, SHELL_APP_STATE_STOPPED); +- } ++ if (starting) ++ app->started_on_workspace = meta_startup_sequence_get_workspace (sequence); ++ else if (app->running_state && app->running_state->windows) ++ shell_app_state_transition (app, SHELL_APP_STATE_RUNNING); ++ else /* application have > 1 .desktop file */ ++ shell_app_state_transition (app, SHELL_APP_STATE_STOPPED); + } + + /** +@@ -1473,6 +1475,7 @@ static void + shell_app_init (ShellApp *self) + { + self->state = SHELL_APP_STATE_STOPPED; ++ self->started_on_workspace = -1; + } + + static void +-- +2.29.2 + diff --git a/SOURCES/caps-lock-warning.patch b/SOURCES/caps-lock-warning.patch new file mode 100644 index 0000000..d3c2daa --- /dev/null +++ b/SOURCES/caps-lock-warning.patch @@ -0,0 +1,488 @@ +From 7b514e637837e00372e20fa52f841e993966b734 Mon Sep 17 00:00:00 2001 +From: Umang Jain +Date: Fri, 13 Dec 2019 13:36:14 +0530 +Subject: [PATCH 1/7] shellEntry: Add CapsLockWarning class + +https://gitlab.gnome.org/GNOME/gnome-shell/merge_requests/619 +--- + data/theme/gnome-shell-sass/_common.scss | 5 +++ + js/ui/shellEntry.js | 39 +++++++++++++++++++++++- + 2 files changed, 43 insertions(+), 1 deletion(-) + +diff --git a/data/theme/gnome-shell-sass/_common.scss b/data/theme/gnome-shell-sass/_common.scss +index b1eeb0ce97..19a736ab7d 100644 +--- a/data/theme/gnome-shell-sass/_common.scss ++++ b/data/theme/gnome-shell-sass/_common.scss +@@ -391,6 +391,11 @@ StScrollBar { + padding-bottom: 8px; + } + ++ .prompt-dialog-caps-lock-warning { ++ @extend .prompt-dialog-error-label; ++ padding-left: 6.2em; ++ } ++ + .prompt-dialog-info-label { + font-size: 10pt; + padding-bottom: 8px; +diff --git a/js/ui/shellEntry.js b/js/ui/shellEntry.js +index 79f1aad3e7..c1738c4064 100644 +--- a/js/ui/shellEntry.js ++++ b/js/ui/shellEntry.js +@@ -1,6 +1,6 @@ + // -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*- + +-const { Clutter, Shell, St } = imports.gi; ++const { Clutter, GObject, Pango, Shell, St } = imports.gi; + + const BoxPointer = imports.ui.boxpointer; + const Main = imports.ui.main; +@@ -170,3 +170,40 @@ function addContextMenu(entry, params) { + entry._menuManager = null; + }); + } ++ ++var CapsLockWarning = GObject.registerClass( ++class CapsLockWarning extends St.Label { ++ _init(params) { ++ let defaultParams = { style_class: 'prompt-dialog-error-label' }; ++ super._init(Object.assign(defaultParams, params)); ++ ++ this.text = _('Caps lock is on.'); ++ ++ this._keymap = Clutter.get_default_backend().get_keymap(); ++ ++ this.connect('notify::mapped', () => { ++ if (this.is_mapped()) { ++ this.stateChangedId = this._keymap.connect('state-changed', ++ this._updateCapsLockWarningOpacity.bind(this)); ++ } else { ++ this._keymap.disconnect(this.stateChangedId); ++ this.stateChangedId = 0; ++ } ++ ++ this._updateCapsLockWarningOpacity(); ++ }); ++ ++ this.connect('destroy', () => { ++ if (this.stateChangedId > 0) ++ this._keymap.disconnect(this.stateChangedId); ++ }); ++ ++ this.clutter_text.ellipsize = Pango.EllipsizeMode.NONE; ++ this.clutter_text.line_wrap = true; ++ } ++ ++ _updateCapsLockWarningOpacity() { ++ let capsLockOn = this._keymap.get_caps_lock_state(); ++ this.opacity = capsLockOn ? 255 : 0; ++ } ++}); +-- +2.21.1 + + +From aa4938f261454f85c782e59e40d4e5a9e1a01dbc Mon Sep 17 00:00:00 2001 +From: Umang Jain +Date: Wed, 18 Dec 2019 01:33:45 +0530 +Subject: [PATCH 2/7] js: Add caps-lock Warning to the dialogs + +https://gitlab.gnome.org/GNOME/gnome-shell/merge_requests/619 +--- + js/gdm/authPrompt.js | 4 ++++ + js/ui/components/keyring.js | 6 ++++++ + js/ui/components/networkAgent.js | 8 ++++++++ + js/ui/components/polkitAgent.js | 2 ++ + js/ui/shellMountOperation.js | 3 +++ + 5 files changed, 23 insertions(+) + +diff --git a/js/gdm/authPrompt.js b/js/gdm/authPrompt.js +index 71069e93b8..3ce9fd0d01 100644 +--- a/js/gdm/authPrompt.js ++++ b/js/gdm/authPrompt.js +@@ -113,6 +113,9 @@ var AuthPrompt = class { + + this._entry.grab_key_focus(); + ++ this._capsLockWarningLabel = new ShellEntry.CapsLockWarning(); ++ this.actor.add_child(this._capsLockWarningLabel); ++ + this._timedLoginIndicator = new St.Bin({ style_class: 'login-dialog-timed-login-indicator', + scale_x: 0 }); + +@@ -432,6 +435,7 @@ var AuthPrompt = class { + setPasswordChar(passwordChar) { + this._entry.clutter_text.set_password_char(passwordChar); + this._entry.menu.isPassword = passwordChar != ''; ++ this._capsLockWarningLabel.visible = passwordChar !== ''; + } + + setQuestion(question) { +diff --git a/js/ui/components/keyring.js b/js/ui/components/keyring.js +index 0d9f1e4663..3512fb63b1 100644 +--- a/js/ui/components/keyring.js ++++ b/js/ui/components/keyring.js +@@ -128,6 +128,12 @@ var KeyringDialog = class extends ModalDialog.ModalDialog { + this.prompt.set_password_actor(this._passwordEntry ? this._passwordEntry.clutter_text : null); + this.prompt.set_confirm_actor(this._confirmEntry ? this._confirmEntry.clutter_text : null); + ++ if (this._passwordEntry || this._confirmEntry) { ++ this._capsLockWarningLabel = new ShellEntry.CapsLockWarning(); ++ layout.attach(this._capsLockWarningLabel, 1, row, 1, 1); ++ row++; ++ } ++ + if (this.prompt.choice_visible) { + let choice = new CheckBox.CheckBox(); + this.prompt.bind_property('choice-label', choice.getLabelActor(), 'text', GObject.BindingFlags.SYNC_CREATE); +diff --git a/js/ui/components/networkAgent.js b/js/ui/components/networkAgent.js +index f871c732d9..32d40fb2b9 100644 +--- a/js/ui/components/networkAgent.js ++++ b/js/ui/components/networkAgent.js +@@ -95,6 +95,14 @@ var NetworkSecretDialog = class extends ModalDialog.ModalDialog { + secret.entry.clutter_text.set_password_char('\u25cf'); + } + ++ if (this._content.secrets.some(s => s.password)) { ++ this._capsLockWarningLabel = new ShellEntry.CapsLockWarning(); ++ if (rtl) ++ layout.attach(this._capsLockWarningLabel, 0, pos, 1, 1); ++ else ++ layout.attach(this._capsLockWarningLabel, 1, pos, 1, 1); ++ } ++ + contentBox.messageBox.add(secretTable); + + if (flags & NM.SecretAgentGetSecretsFlags.WPS_PBC_ACTIVE) { +diff --git a/js/ui/components/polkitAgent.js b/js/ui/components/polkitAgent.js +index 21feb40903..734a217335 100644 +--- a/js/ui/components/polkitAgent.js ++++ b/js/ui/components/polkitAgent.js +@@ -108,6 +108,8 @@ var AuthenticationDialog = class extends ModalDialog.ModalDialog { + + this.setInitialKeyFocus(this._passwordEntry); + this._passwordBox.hide(); ++ this._capsLockWarningLabel = new ShellEntry.CapsLockWarning({ style_class: 'prompt-dialog-caps-lock-warning' }); ++ content.messageBox.add(this._capsLockWarningLabel); + + this._errorMessageLabel = new St.Label({ style_class: 'prompt-dialog-error-label' }); + this._errorMessageLabel.clutter_text.ellipsize = Pango.EllipsizeMode.NONE; +diff --git a/js/ui/shellMountOperation.js b/js/ui/shellMountOperation.js +index f976f400f4..3a2377ddaf 100644 +--- a/js/ui/shellMountOperation.js ++++ b/js/ui/shellMountOperation.js +@@ -305,6 +305,9 @@ var ShellMountPasswordDialog = class extends ModalDialog.ModalDialog { + this._passwordBox.add(this._passwordEntry, {expand: true }); + this.setInitialKeyFocus(this._passwordEntry); + ++ this._capsLockWarningLabel = new ShellEntry.CapsLockWarning(); ++ content.messageBox.add(this._capsLockWarningLabel); ++ + this._errorMessageLabel = new St.Label({ style_class: 'prompt-dialog-error-label', + text: _("Sorry, that didn’t work. Please try again.") }); + this._errorMessageLabel.clutter_text.ellipsize = Pango.EllipsizeMode.NONE; +-- +2.21.1 + + +From 016cbd971711665844d40ec678d2779c160f791b Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Jonas=20Dre=C3=9Fler?= +Date: Thu, 23 Jan 2020 22:37:06 +0100 +Subject: [PATCH 3/7] shellEntry: Make signal id variable private + +Signal connection IDs should be private variables, so make this one +private. + +https://gitlab.gnome.org/GNOME/gnome-shell/merge_requests/952 +--- + js/ui/shellEntry.js | 10 +++++----- + 1 file changed, 5 insertions(+), 5 deletions(-) + +diff --git a/js/ui/shellEntry.js b/js/ui/shellEntry.js +index c1738c4064..cd7c9a6c88 100644 +--- a/js/ui/shellEntry.js ++++ b/js/ui/shellEntry.js +@@ -183,19 +183,19 @@ class CapsLockWarning extends St.Label { + + this.connect('notify::mapped', () => { + if (this.is_mapped()) { +- this.stateChangedId = this._keymap.connect('state-changed', ++ this._stateChangedId = this._keymap.connect('state-changed', + this._updateCapsLockWarningOpacity.bind(this)); + } else { +- this._keymap.disconnect(this.stateChangedId); +- this.stateChangedId = 0; ++ this._keymap.disconnect(this._stateChangedId); ++ this._stateChangedId = 0; + } + + this._updateCapsLockWarningOpacity(); + }); + + this.connect('destroy', () => { +- if (this.stateChangedId > 0) +- this._keymap.disconnect(this.stateChangedId); ++ if (this._stateChangedId > 0) ++ this._keymap.disconnect(this._stateChangedId); + }); + + this.clutter_text.ellipsize = Pango.EllipsizeMode.NONE; +-- +2.21.1 + + +From ba65f9066d72731e345a5aced61f35d39c1c1376 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Jonas=20Dre=C3=9Fler?= +Date: Thu, 23 Jan 2020 23:26:45 +0100 +Subject: [PATCH 4/7] theme: Move caps-lock warning to entry widget stylesheet + +The caps-lock warning is more related to entries than dialogs and is +also used in gdm, which is not realated to dialogs at all. Rename the +css class to caps-lock-warning-label and move it to the entry +stylesheet. + +https://gitlab.gnome.org/GNOME/gnome-shell/merge_requests/952 +--- + data/theme/gnome-shell-sass/_common.scss | 12 +++++++----- + js/ui/shellEntry.js | 2 +- + 2 files changed, 8 insertions(+), 6 deletions(-) + +diff --git a/data/theme/gnome-shell-sass/_common.scss b/data/theme/gnome-shell-sass/_common.scss +index 19a736ab7d..4661533de2 100644 +--- a/data/theme/gnome-shell-sass/_common.scss ++++ b/data/theme/gnome-shell-sass/_common.scss +@@ -94,6 +94,13 @@ StEntry { + } + } + ++.caps-lock-warning-label { ++ padding-left: 6.2em; ++ @include fontsize($font-size - 1); ++ color: $warning_color; ++} ++ ++ + + /* Scrollbars */ + +@@ -391,11 +398,6 @@ StScrollBar { + padding-bottom: 8px; + } + +- .prompt-dialog-caps-lock-warning { +- @extend .prompt-dialog-error-label; +- padding-left: 6.2em; +- } +- + .prompt-dialog-info-label { + font-size: 10pt; + padding-bottom: 8px; +diff --git a/js/ui/shellEntry.js b/js/ui/shellEntry.js +index cd7c9a6c88..46eba88d54 100644 +--- a/js/ui/shellEntry.js ++++ b/js/ui/shellEntry.js +@@ -174,7 +174,7 @@ function addContextMenu(entry, params) { + var CapsLockWarning = GObject.registerClass( + class CapsLockWarning extends St.Label { + _init(params) { +- let defaultParams = { style_class: 'prompt-dialog-error-label' }; ++ let defaultParams = { style_class: 'caps-lock-warning-label' }; + super._init(Object.assign(defaultParams, params)); + + this.text = _('Caps lock is on.'); +-- +2.21.1 + + +From afd764c82febe21aec70bdfc19d256f3401530e1 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Jonas=20Dre=C3=9Fler?= +Date: Thu, 23 Jan 2020 22:36:09 +0100 +Subject: [PATCH 5/7] shellEntry: Hide caps lock warning and use animation to + show it + +Since the caps-lock warning adds a lot of spacing to dialogs and the +lock screen, hide it by default and only show it when necessary. To make +the transition smooth instead of just showing the label, animate it in +using the height and opacity. + +Also add some bottom padding to the label so we can show or hide that +padding, too. + +https://gitlab.gnome.org/GNOME/gnome-shell/merge_requests/952 +--- + data/theme/gnome-shell-sass/_common.scss | 1 + + js/ui/shellEntry.js | 33 ++++++++++++++++++------ + 2 files changed, 26 insertions(+), 8 deletions(-) + +diff --git a/data/theme/gnome-shell-sass/_common.scss b/data/theme/gnome-shell-sass/_common.scss +index 4661533de2..9e0751c8c5 100644 +--- a/data/theme/gnome-shell-sass/_common.scss ++++ b/data/theme/gnome-shell-sass/_common.scss +@@ -95,6 +95,7 @@ StEntry { + } + + .caps-lock-warning-label { ++ padding-bottom: 8px; + padding-left: 6.2em; + @include fontsize($font-size - 1); + color: $warning_color; +diff --git a/js/ui/shellEntry.js b/js/ui/shellEntry.js +index 46eba88d54..fc8ee37a9a 100644 +--- a/js/ui/shellEntry.js ++++ b/js/ui/shellEntry.js +@@ -6,6 +6,7 @@ const BoxPointer = imports.ui.boxpointer; + const Main = imports.ui.main; + const Params = imports.misc.params; + const PopupMenu = imports.ui.popupMenu; ++const Tweener = imports.ui.tweener; + + var EntryMenu = class extends PopupMenu.PopupMenu { + constructor(entry) { +@@ -179,31 +180,47 @@ class CapsLockWarning extends St.Label { + + this.text = _('Caps lock is on.'); + ++ this.clutter_text.ellipsize = Pango.EllipsizeMode.NONE; ++ this.clutter_text.line_wrap = true; ++ + this._keymap = Clutter.get_default_backend().get_keymap(); + + this.connect('notify::mapped', () => { + if (this.is_mapped()) { + this._stateChangedId = this._keymap.connect('state-changed', +- this._updateCapsLockWarningOpacity.bind(this)); ++ () => this._sync(true)); + } else { + this._keymap.disconnect(this._stateChangedId); + this._stateChangedId = 0; + } + +- this._updateCapsLockWarningOpacity(); ++ this._sync(false); + }); + + this.connect('destroy', () => { +- if (this._stateChangedId > 0) ++ if (this._stateChangedId) + this._keymap.disconnect(this._stateChangedId); + }); +- +- this.clutter_text.ellipsize = Pango.EllipsizeMode.NONE; +- this.clutter_text.line_wrap = true; + } + +- _updateCapsLockWarningOpacity() { ++ _sync(animate) { + let capsLockOn = this._keymap.get_caps_lock_state(); +- this.opacity = capsLockOn ? 255 : 0; ++ ++ Tweener.removeTweens(this); ++ ++ this.natural_height_set = false; ++ let [, height] = this.get_preferred_height(-1); ++ this.natural_height_set = true; ++ ++ Tweener.addTween(this, { ++ height: capsLockOn ? height : 0, ++ opacity: capsLockOn ? 255 : 0, ++ time: animate ? 0.2 : 0, ++ transition: 'easeOutQuad', ++ onComplete: () => { ++ if (capsLockOn) ++ this.height = -1; ++ }, ++ }); + } + }); +-- +2.21.1 + + +From 1ef3dafb51da380c54635d0565dc098e40bbb3e1 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Florian=20M=C3=BCllner?= +Date: Wed, 29 Jan 2020 17:48:57 +0100 +Subject: [PATCH 6/7] js: Initialize some properties + +Otherwise those can result in the (harmless) "reference to undefined +property" warnings. + +https://gitlab.gnome.org/GNOME/gnome-shell/merge_requests/970 +--- + js/ui/overview.js | 1 + + js/ui/shellEntry.js | 1 + + 2 files changed, 2 insertions(+) + +diff --git a/js/ui/overview.js b/js/ui/overview.js +index dc6ad1821b..5bad4cbd62 100644 +--- a/js/ui/overview.js ++++ b/js/ui/overview.js +@@ -80,6 +80,7 @@ var Overview = class { + constructor() { + this._overviewCreated = false; + this._initCalled = false; ++ this._visible = false; + + Main.sessionMode.connect('updated', this._sessionUpdated.bind(this)); + this._sessionUpdated(); +diff --git a/js/ui/shellEntry.js b/js/ui/shellEntry.js +index fc8ee37a9a..55267e7c87 100644 +--- a/js/ui/shellEntry.js ++++ b/js/ui/shellEntry.js +@@ -184,6 +184,7 @@ class CapsLockWarning extends St.Label { + this.clutter_text.line_wrap = true; + + this._keymap = Clutter.get_default_backend().get_keymap(); ++ this._stateChangedId = 0; + + this.connect('notify::mapped', () => { + if (this.is_mapped()) { +-- +2.21.1 + + +From 273f7adb43cfee907342d017e1454ea90d42d262 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Florian=20M=C3=BCllner?= +Date: Fri, 21 Feb 2020 19:38:53 +0100 +Subject: [PATCH 7/7] shellEntry: Restore natural-height-set instead of forcing + it + +If we are transitioning the label from 0 to its natural height, we +must set natural-height-set again after querying the preferred height, +otherwise Clutter would skip the transition. + +However when transitioning in the opposite direction, setting the +property to true can go horribly wrong: +If the actor hasn't been allocated before, it will store a fixed +natural height of 0. But as there is no fixed min-height, we can +end up with min-height > natural-height, which is a fatal error. + +(This isn't an issue when *actually* setting a fixed height, as +that will set both natural and minimum height) + +So instead of always setting natural-height-set to true, restore +its previous value to fix the issue. + +https://gitlab.gnome.org/GNOME/gnome-shell/issues/2255 +--- + js/ui/shellEntry.js | 3 ++- + 1 file changed, 2 insertions(+), 1 deletion(-) + +diff --git a/js/ui/shellEntry.js b/js/ui/shellEntry.js +index 55267e7c87..4a30b22f7a 100644 +--- a/js/ui/shellEntry.js ++++ b/js/ui/shellEntry.js +@@ -209,9 +209,10 @@ class CapsLockWarning extends St.Label { + + Tweener.removeTweens(this); + ++ const naturalHeightSet = this.natural_height_set; + this.natural_height_set = false; + let [, height] = this.get_preferred_height(-1); +- this.natural_height_set = true; ++ this.natural_height_set = naturalHeightSet; + + Tweener.addTween(this, { + height: capsLockOn ? height : 0, +-- +2.21.1 + diff --git a/SOURCES/extension-updates.patch b/SOURCES/extension-updates.patch new file mode 100644 index 0000000..fb2dbcd --- /dev/null +++ b/SOURCES/extension-updates.patch @@ -0,0 +1,3811 @@ +From ab3a275e20c36cc21e529bb2c4328ea36024ecba Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Florian=20M=C3=BCllner?= +Date: Sat, 6 Jul 2019 15:31:57 +0200 +Subject: [PATCH 01/26] extensionUtils: Move ExtensionState definition here + +It makes sense to keep extension-related enums in the same module instead +of spreading them between ExtensionSystem and ExtensionUtils. + +More importantly, this will make the type available to the extensions-prefs +tool (which runs in a different process and therefore only has access to +a limited set of modules). + +https://bugzilla.gnome.org/show_bug.cgi?id=789852 +--- + js/misc/extensionUtils.js | 13 +++++++++++++ + js/ui/extensionSystem.js | 13 +------------ + js/ui/lookingGlass.js | 14 ++++++++------ + 3 files changed, 22 insertions(+), 18 deletions(-) + +diff --git a/js/misc/extensionUtils.js b/js/misc/extensionUtils.js +index fb1e2b506..dc6e74cf8 100644 +--- a/js/misc/extensionUtils.js ++++ b/js/misc/extensionUtils.js +@@ -17,6 +17,19 @@ var ExtensionType = { + SESSION_MODE: 3 + }; + ++var ExtensionState = { ++ ENABLED: 1, ++ DISABLED: 2, ++ ERROR: 3, ++ OUT_OF_DATE: 4, ++ DOWNLOADING: 5, ++ INITIALIZED: 6, ++ ++ // Used as an error state for operations on unknown extensions, ++ // should never be in a real extensionMeta object. ++ UNINSTALLED: 99 ++}; ++ + // Maps uuid -> metadata object + var extensions = {}; + +diff --git a/js/ui/extensionSystem.js b/js/ui/extensionSystem.js +index 9ffdb4f3d..3091af2ba 100644 +--- a/js/ui/extensionSystem.js ++++ b/js/ui/extensionSystem.js +@@ -6,18 +6,7 @@ const Signals = imports.signals; + const ExtensionUtils = imports.misc.extensionUtils; + const Main = imports.ui.main; + +-var ExtensionState = { +- ENABLED: 1, +- DISABLED: 2, +- ERROR: 3, +- OUT_OF_DATE: 4, +- DOWNLOADING: 5, +- INITIALIZED: 6, +- +- // Used as an error state for operations on unknown extensions, +- // should never be in a real extensionMeta object. +- UNINSTALLED: 99 +-}; ++const { ExtensionState } = ExtensionUtils; + + // Arrays of uuids + var enabledExtensions; +diff --git a/js/ui/lookingGlass.js b/js/ui/lookingGlass.js +index 958211df0..fefb3f731 100644 +--- a/js/ui/lookingGlass.js ++++ b/js/ui/lookingGlass.js +@@ -14,6 +14,8 @@ const Tweener = imports.ui.tweener; + const Main = imports.ui.main; + const JsParse = imports.misc.jsParse; + ++const { ExtensionState } = ExtensionUtils; ++ + const CHEVRON = '>>> '; + + /* Imports...feel free to add here as needed */ +@@ -684,16 +686,16 @@ var Extensions = class Extensions { + + _stateToString(extensionState) { + switch (extensionState) { +- case ExtensionSystem.ExtensionState.ENABLED: ++ case ExtensionState.ENABLED: + return _("Enabled"); +- case ExtensionSystem.ExtensionState.DISABLED: +- case ExtensionSystem.ExtensionState.INITIALIZED: ++ case ExtensionState.DISABLED: ++ case ExtensionState.INITIALIZED: + return _("Disabled"); +- case ExtensionSystem.ExtensionState.ERROR: ++ case ExtensionState.ERROR: + return _("Error"); +- case ExtensionSystem.ExtensionState.OUT_OF_DATE: ++ case ExtensionState.OUT_OF_DATE: + return _("Out of date"); +- case ExtensionSystem.ExtensionState.DOWNLOADING: ++ case ExtensionState.DOWNLOADING: + return _("Downloading"); + } + return 'Unknown'; // Not translated, shouldn't appear +-- +2.29.2 + + +From c16d1589d093dac4e0efe21c7f1aeb635afabd0f Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Florian=20M=C3=BCllner?= +Date: Thu, 7 Mar 2019 01:45:45 +0100 +Subject: [PATCH 02/26] extensionSystem: Turn into a class + +The extension system started out as a set of simple functions, but +gained more state later, and even some hacks to emit signals without +having an object to emit them on. + +There is no good reason for that weirdness, so rather than imitating an +object, wrap the existing system into a real ExtensionManager object. + +https://bugzilla.gnome.org/show_bug.cgi?id=789852 +--- + js/ui/extensionDownloader.js | 17 +- + js/ui/extensionSystem.js | 569 +++++++++++++++++------------------ + js/ui/lookingGlass.js | 5 +- + js/ui/main.js | 3 +- + js/ui/shellDBus.js | 7 +- + 5 files changed, 297 insertions(+), 304 deletions(-) + +diff --git a/js/ui/extensionDownloader.js b/js/ui/extensionDownloader.js +index 9aed29c69..fe37463f2 100644 +--- a/js/ui/extensionDownloader.js ++++ b/js/ui/extensionDownloader.js +@@ -6,6 +6,7 @@ const Config = imports.misc.config; + const ExtensionUtils = imports.misc.extensionUtils; + const ExtensionSystem = imports.ui.extensionSystem; + const FileUtils = imports.misc.fileUtils; ++const Main = imports.ui.main; + const ModalDialog = imports.ui.modalDialog; + + const _signals = ExtensionSystem._signals; +@@ -25,7 +26,7 @@ function installExtension(uuid, invocation) { + + _httpSession.queue_message(message, (session, message) => { + if (message.status_code != Soup.KnownStatusCode.OK) { +- ExtensionSystem.logExtensionError(uuid, 'downloading info: ' + message.status_code); ++ Main.extensionManager.logExtensionError(uuid, 'downloading info: ' + message.status_code); + invocation.return_dbus_error('org.gnome.Shell.DownloadInfoError', message.status_code.toString()); + return; + } +@@ -34,7 +35,7 @@ function installExtension(uuid, invocation) { + try { + info = JSON.parse(message.response_body.data); + } catch (e) { +- ExtensionSystem.logExtensionError(uuid, 'parsing info: ' + e); ++ Main.extensionManager.logExtensionError(uuid, 'parsing info: ' + e); + invocation.return_dbus_error('org.gnome.Shell.ParseInfoError', e.toString()); + return; + } +@@ -53,7 +54,7 @@ function uninstallExtension(uuid) { + if (extension.type != ExtensionUtils.ExtensionType.PER_USER) + return false; + +- if (!ExtensionSystem.unloadExtension(extension)) ++ if (!Main.extensionManager.unloadExtension(extension)) + return false; + + FileUtils.recursivelyDeleteDir(extension.dir, true); +@@ -117,7 +118,7 @@ function updateExtension(uuid) { + let oldExtension = ExtensionUtils.extensions[uuid]; + let extensionDir = oldExtension.dir; + +- if (!ExtensionSystem.unloadExtension(oldExtension)) ++ if (!Main.extensionManager.unloadExtension(oldExtension)) + return; + + FileUtils.recursivelyMoveDir(extensionDir, oldExtensionTmpDir); +@@ -127,10 +128,10 @@ function updateExtension(uuid) { + + try { + extension = ExtensionUtils.createExtensionObject(uuid, extensionDir, ExtensionUtils.ExtensionType.PER_USER); +- ExtensionSystem.loadExtension(extension); ++ Main.extensionManager.loadExtension(extension); + } catch(e) { + if (extension) +- ExtensionSystem.unloadExtension(extension); ++ Main.extensionManager.unloadExtension(extension); + + logError(e, 'Error loading extension %s'.format(uuid)); + +@@ -139,7 +140,7 @@ function updateExtension(uuid) { + + // Restore what was there before. We can't do much if we + // fail here. +- ExtensionSystem.loadExtension(oldExtension); ++ Main.extensionManager.loadExtension(oldExtension); + return; + } + +@@ -239,7 +240,7 @@ class InstallExtensionDialog extends ModalDialog.ModalDialog { + + try { + let extension = ExtensionUtils.createExtensionObject(uuid, dir, ExtensionUtils.ExtensionType.PER_USER); +- ExtensionSystem.loadExtension(extension); ++ Main.extensionManager.loadExtension(extension); + } catch(e) { + uninstallExtension(uuid); + errback('LoadExtensionError', e); +diff --git a/js/ui/extensionSystem.js b/js/ui/extensionSystem.js +index 3091af2ba..b7e908223 100644 +--- a/js/ui/extensionSystem.js ++++ b/js/ui/extensionSystem.js +@@ -8,358 +8,351 @@ const Main = imports.ui.main; + + const { ExtensionState } = ExtensionUtils; + +-// Arrays of uuids +-var enabledExtensions; +-// Contains the order that extensions were enabled in. +-var extensionOrder = []; +- +-// We don't really have a class to add signals on. So, create +-// a simple dummy object, add the signal methods, and export those +-// publically. +-var _signals = {}; +-Signals.addSignalMethods(_signals); +- +-var connect = _signals.connect.bind(_signals); +-var disconnect = _signals.disconnect.bind(_signals); +- + const ENABLED_EXTENSIONS_KEY = 'enabled-extensions'; + const DISABLE_USER_EXTENSIONS_KEY = 'disable-user-extensions'; + const EXTENSION_DISABLE_VERSION_CHECK_KEY = 'disable-extension-version-validation'; + +-var initted = false; +-var enabled; ++var ExtensionManager = class { ++ constructor() { ++ this._initted = false; ++ this._enabled = false; + +-function disableExtension(uuid) { +- let extension = ExtensionUtils.extensions[uuid]; +- if (!extension) +- return; ++ this._enabledExtensions = []; ++ this._extensionOrder = []; + +- if (extension.state != ExtensionState.ENABLED) +- return; ++ Main.sessionMode.connect('updated', this._sessionUpdated.bind(this)); ++ this._sessionUpdated(); ++ } + +- // "Rebase" the extension order by disabling and then enabling extensions +- // in order to help prevent conflicts. ++ disableExtension(uuid) { ++ let extension = ExtensionUtils.extensions[uuid]; ++ if (!extension) ++ return; + +- // Example: +- // order = [A, B, C, D, E] +- // user disables C +- // this should: disable E, disable D, disable C, enable D, enable E ++ if (extension.state != ExtensionState.ENABLED) ++ return; + +- let orderIdx = extensionOrder.indexOf(uuid); +- let order = extensionOrder.slice(orderIdx + 1); +- let orderReversed = order.slice().reverse(); ++ // "Rebase" the extension order by disabling and then enabling extensions ++ // in order to help prevent conflicts. ++ ++ // Example: ++ // order = [A, B, C, D, E] ++ // user disables C ++ // this should: disable E, disable D, disable C, enable D, enable E ++ ++ let orderIdx = this._extensionOrder.indexOf(uuid); ++ let order = this._extensionOrder.slice(orderIdx + 1); ++ let orderReversed = order.slice().reverse(); ++ ++ for (let i = 0; i < orderReversed.length; i++) { ++ let uuid = orderReversed[i]; ++ try { ++ ExtensionUtils.extensions[uuid].stateObj.disable(); ++ } catch (e) { ++ this.logExtensionError(uuid, e); ++ } ++ } ++ ++ if (extension.stylesheet) { ++ let theme = St.ThemeContext.get_for_stage(global.stage).get_theme(); ++ theme.unload_stylesheet(extension.stylesheet); ++ delete extension.stylesheet; ++ } + +- for (let i = 0; i < orderReversed.length; i++) { +- let uuid = orderReversed[i]; + try { +- ExtensionUtils.extensions[uuid].stateObj.disable(); ++ extension.stateObj.disable(); + } catch(e) { +- logExtensionError(uuid, e); ++ this.logExtensionError(uuid, e); + } +- } + +- if (extension.stylesheet) { +- let theme = St.ThemeContext.get_for_stage(global.stage).get_theme(); +- theme.unload_stylesheet(extension.stylesheet); +- delete extension.stylesheet; +- } ++ for (let i = 0; i < order.length; i++) { ++ let uuid = order[i]; ++ try { ++ ExtensionUtils.extensions[uuid].stateObj.enable(); ++ } catch (e) { ++ this.logExtensionError(uuid, e); ++ } ++ } + +- try { +- extension.stateObj.disable(); +- } catch(e) { +- logExtensionError(uuid, e); +- } ++ this._extensionOrder.splice(orderIdx, 1); + +- for (let i = 0; i < order.length; i++) { +- let uuid = order[i]; +- try { +- ExtensionUtils.extensions[uuid].stateObj.enable(); +- } catch(e) { +- logExtensionError(uuid, e); ++ if (extension.state != ExtensionState.ERROR) { ++ extension.state = ExtensionState.DISABLED; ++ this.emit('extension-state-changed', extension); + } + } + +- extensionOrder.splice(orderIdx, 1); +- +- if ( extension.state != ExtensionState.ERROR ) { +- extension.state = ExtensionState.DISABLED; +- _signals.emit('extension-state-changed', extension); +- } +-} ++ enableExtension(uuid) { ++ let extension = ExtensionUtils.extensions[uuid]; ++ if (!extension) ++ return; + +-function enableExtension(uuid) { +- let extension = ExtensionUtils.extensions[uuid]; +- if (!extension) +- return; ++ if (extension.state == ExtensionState.INITIALIZED) ++ this.initExtension(uuid); + +- if (extension.state == ExtensionState.INITIALIZED) +- initExtension(uuid); ++ if (extension.state != ExtensionState.DISABLED) ++ return; + +- if (extension.state != ExtensionState.DISABLED) +- return; ++ this._extensionOrder.push(uuid); + +- extensionOrder.push(uuid); ++ let stylesheetNames = [global.session_mode + '.css', 'stylesheet.css']; ++ let theme = St.ThemeContext.get_for_stage(global.stage).get_theme(); ++ for (let i = 0; i < stylesheetNames.length; i++) { ++ try { ++ let stylesheetFile = extension.dir.get_child(stylesheetNames[i]); ++ theme.load_stylesheet(stylesheetFile); ++ extension.stylesheet = stylesheetFile; ++ break; ++ } catch (e) { ++ if (e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.NOT_FOUND)) ++ continue; // not an error ++ log(`Failed to load stylesheet for extension ${uuid}: ${e.message}`); ++ return; ++ } ++ } + +- let stylesheetNames = [global.session_mode + '.css', 'stylesheet.css']; +- let theme = St.ThemeContext.get_for_stage(global.stage).get_theme(); +- for (let i = 0; i < stylesheetNames.length; i++) { + try { +- let stylesheetFile = extension.dir.get_child(stylesheetNames[i]); +- theme.load_stylesheet(stylesheetFile); +- extension.stylesheet = stylesheetFile; +- break; ++ extension.stateObj.enable(); ++ extension.state = ExtensionState.ENABLED; ++ this.emit('extension-state-changed', extension); ++ return; + } catch (e) { +- if (e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.NOT_FOUND)) +- continue; // not an error +- log(`Failed to load stylesheet for extension ${uuid}: ${e.message}`); ++ if (extension.stylesheet) { ++ theme.unload_stylesheet(extension.stylesheet); ++ delete extension.stylesheet; ++ } ++ this.logExtensionError(uuid, e); + return; + } + } + +- try { +- extension.stateObj.enable(); +- extension.state = ExtensionState.ENABLED; +- _signals.emit('extension-state-changed', extension); +- return; +- } catch(e) { +- if (extension.stylesheet) { +- theme.unload_stylesheet(extension.stylesheet); +- delete extension.stylesheet; +- } +- logExtensionError(uuid, e); +- return; +- } +-} +- +-function logExtensionError(uuid, error) { +- let extension = ExtensionUtils.extensions[uuid]; +- if (!extension) +- return; ++ logExtensionError(uuid, error) { ++ let extension = ExtensionUtils.extensions[uuid]; ++ if (!extension) ++ return; + +- let message = '' + error; ++ let message = '' + error; + +- extension.state = ExtensionState.ERROR; +- if (!extension.errors) +- extension.errors = []; +- extension.errors.push(message); ++ extension.state = ExtensionState.ERROR; ++ if (!extension.errors) ++ extension.errors = []; ++ extension.errors.push(message); + +- log('Extension "%s" had error: %s'.format(uuid, message)); +- _signals.emit('extension-state-changed', { uuid: uuid, ++ log('Extension "%s" had error: %s'.format(uuid, message)); ++ this.emit('extension-state-changed', { uuid: uuid, + error: message, + state: extension.state }); +-} ++ } + +-function loadExtension(extension) { +- // Default to error, we set success as the last step +- extension.state = ExtensionState.ERROR; ++ loadExtension(extension) { ++ // Default to error, we set success as the last step ++ extension.state = ExtensionState.ERROR; + +- let checkVersion = !global.settings.get_boolean(EXTENSION_DISABLE_VERSION_CHECK_KEY); ++ let checkVersion = !global.settings.get_boolean(EXTENSION_DISABLE_VERSION_CHECK_KEY); + +- if (checkVersion && ExtensionUtils.isOutOfDate(extension)) { +- extension.state = ExtensionState.OUT_OF_DATE; +- } else { +- let enabled = enabledExtensions.indexOf(extension.uuid) != -1; +- if (enabled) { +- if (!initExtension(extension.uuid)) +- return; +- if (extension.state == ExtensionState.DISABLED) +- enableExtension(extension.uuid); ++ if (checkVersion && ExtensionUtils.isOutOfDate(extension)) { ++ extension.state = ExtensionState.OUT_OF_DATE; + } else { +- extension.state = ExtensionState.INITIALIZED; ++ let enabled = this._enabledExtensions.includes(extension.uuid); ++ if (enabled) { ++ if (!this.initExtension(extension.uuid)) ++ return; ++ if (extension.state == ExtensionState.DISABLED) ++ this.enableExtension(extension.uuid); ++ } else { ++ extension.state = ExtensionState.INITIALIZED; ++ } + } ++ ++ this.emit('extension-state-changed', extension); + } + +- _signals.emit('extension-state-changed', extension); +-} +- +-function unloadExtension(extension) { +- // Try to disable it -- if it's ERROR'd, we can't guarantee that, +- // but it will be removed on next reboot, and hopefully nothing +- // broke too much. +- disableExtension(extension.uuid); +- +- extension.state = ExtensionState.UNINSTALLED; +- _signals.emit('extension-state-changed', extension); +- +- delete ExtensionUtils.extensions[extension.uuid]; +- return true; +-} +- +-function reloadExtension(oldExtension) { +- // Grab the things we'll need to pass to createExtensionObject +- // to reload it. +- let { uuid: uuid, dir: dir, type: type } = oldExtension; +- +- // Then unload the old extension. +- unloadExtension(oldExtension); +- +- // Now, recreate the extension and load it. +- let newExtension; +- try { +- newExtension = ExtensionUtils.createExtensionObject(uuid, dir, type); +- } catch(e) { +- logExtensionError(uuid, e); +- return; ++ unloadExtension(extension) { ++ // Try to disable it -- if it's ERROR'd, we can't guarantee that, ++ // but it will be removed on next reboot, and hopefully nothing ++ // broke too much. ++ this.disableExtension(extension.uuid); ++ ++ extension.state = ExtensionState.UNINSTALLED; ++ this.emit('extension-state-changed', extension); ++ ++ delete ExtensionUtils.extensions[extension.uuid]; ++ return true; + } + +- loadExtension(newExtension); +-} ++ reloadExtension(oldExtension) { ++ // Grab the things we'll need to pass to createExtensionObject ++ // to reload it. ++ let { uuid: uuid, dir: dir, type: type } = oldExtension; + +-function initExtension(uuid) { +- let extension = ExtensionUtils.extensions[uuid]; +- let dir = extension.dir; ++ // Then unload the old extension. ++ this.unloadExtension(oldExtension); + +- if (!extension) +- throw new Error("Extension was not properly created. Call loadExtension first"); ++ // Now, recreate the extension and load it. ++ let newExtension; ++ try { ++ newExtension = ExtensionUtils.createExtensionObject(uuid, dir, type); ++ } catch (e) { ++ this.logExtensionError(uuid, e); ++ return; ++ } + +- let extensionJs = dir.get_child('extension.js'); +- if (!extensionJs.query_exists(null)) { +- logExtensionError(uuid, new Error('Missing extension.js')); +- return false; ++ this.loadExtension(newExtension); + } + +- let extensionModule; +- let extensionState = null; ++ initExtension(uuid) { ++ let extension = ExtensionUtils.extensions[uuid]; ++ let dir = extension.dir; + +- ExtensionUtils.installImporter(extension); +- try { +- extensionModule = extension.imports.extension; +- } catch(e) { +- logExtensionError(uuid, e); +- return false; +- } ++ if (!extension) ++ throw new Error("Extension was not properly created. Call loadExtension first"); ++ ++ let extensionJs = dir.get_child('extension.js'); ++ if (!extensionJs.query_exists(null)) { ++ this.logExtensionError(uuid, new Error('Missing extension.js')); ++ return false; ++ } ++ ++ let extensionModule; ++ let extensionState = null; + +- if (extensionModule.init) { ++ ExtensionUtils.installImporter(extension); + try { +- extensionState = extensionModule.init(extension); ++ extensionModule = extension.imports.extension; + } catch(e) { +- logExtensionError(uuid, e); ++ this.logExtensionError(uuid, e); + return false; + } ++ ++ if (extensionModule.init) { ++ try { ++ extensionState = extensionModule.init(extension); ++ } catch (e) { ++ this.logExtensionError(uuid, e); ++ return false; ++ } ++ } ++ ++ if (!extensionState) ++ extensionState = extensionModule; ++ extension.stateObj = extensionState; ++ ++ extension.state = ExtensionState.DISABLED; ++ this.emit('extension-loaded', uuid); ++ return true; + } + +- if (!extensionState) +- extensionState = extensionModule; +- extension.stateObj = extensionState; +- +- extension.state = ExtensionState.DISABLED; +- _signals.emit('extension-loaded', uuid); +- return true; +-} +- +-function getEnabledExtensions() { +- let extensions; +- if (Array.isArray(Main.sessionMode.enabledExtensions)) +- extensions = Main.sessionMode.enabledExtensions; +- else +- extensions = []; +- +- if (global.settings.get_boolean(DISABLE_USER_EXTENSIONS_KEY)) +- return extensions; +- +- return extensions.concat(global.settings.get_strv(ENABLED_EXTENSIONS_KEY)); +-} +- +-function onEnabledExtensionsChanged() { +- let newEnabledExtensions = getEnabledExtensions(); +- +- if (!enabled) +- return; +- +- // Find and enable all the newly enabled extensions: UUIDs found in the +- // new setting, but not in the old one. +- newEnabledExtensions.filter( +- uuid => !enabledExtensions.includes(uuid) +- ).forEach(uuid => { +- enableExtension(uuid); +- }); +- +- // Find and disable all the newly disabled extensions: UUIDs found in the +- // old setting, but not in the new one. +- enabledExtensions.filter( +- item => !newEnabledExtensions.includes(item) +- ).forEach(uuid => { +- disableExtension(uuid); +- }); +- +- enabledExtensions = newEnabledExtensions; +-} +- +-function _onVersionValidationChanged() { +- // we want to reload all extensions, but only enable +- // extensions when allowed by the sessionMode, so +- // temporarily disable them all +- enabledExtensions = []; +- for (let uuid in ExtensionUtils.extensions) +- reloadExtension(ExtensionUtils.extensions[uuid]); +- enabledExtensions = getEnabledExtensions(); +- +- if (Main.sessionMode.allowExtensions) { +- enabledExtensions.forEach(uuid => { +- enableExtension(uuid); +- }); ++ _getEnabledExtensions() { ++ let extensions; ++ if (Array.isArray(Main.sessionMode.enabledExtensions)) ++ extensions = Main.sessionMode.enabledExtensions; ++ else ++ extensions = []; ++ ++ if (global.settings.get_boolean(DISABLE_USER_EXTENSIONS_KEY)) ++ return extensions; ++ ++ return extensions.concat(global.settings.get_strv(ENABLED_EXTENSIONS_KEY)); + } +-} +- +-function _loadExtensions() { +- global.settings.connect('changed::' + ENABLED_EXTENSIONS_KEY, onEnabledExtensionsChanged); +- global.settings.connect('changed::' + DISABLE_USER_EXTENSIONS_KEY, onEnabledExtensionsChanged); +- global.settings.connect('changed::' + EXTENSION_DISABLE_VERSION_CHECK_KEY, _onVersionValidationChanged); +- +- enabledExtensions = getEnabledExtensions(); +- +- let finder = new ExtensionUtils.ExtensionFinder(); +- finder.connect('extension-found', (finder, extension) => { +- loadExtension(extension); +- if (Main.sessionMode.enabledExtensions.indexOf(extension.uuid) != -1) +- extension.type = ExtensionUtils.ExtensionType.SESSION_MODE; +- }); +- finder.scanExtensions(); +-} +- +-function enableAllExtensions() { +- if (enabled) +- return; +- +- if (!initted) { +- _loadExtensions(); +- initted = true; +- } else { +- enabledExtensions.forEach(uuid => { +- enableExtension(uuid); ++ ++ _onEnabledExtensionsChanged() { ++ let newEnabledExtensions = this._getEnabledExtensions(); ++ ++ if (!this._enabled) ++ return; ++ ++ // Find and enable all the newly enabled extensions: UUIDs found in the ++ // new setting, but not in the old one. ++ newEnabledExtensions.filter( ++ uuid => !this._enabledExtensions.includes(uuid) ++ ).forEach(uuid => { ++ this.enableExtension(uuid); + }); ++ ++ // Find and disable all the newly disabled extensions: UUIDs found in the ++ // old setting, but not in the new one. ++ this._enabledExtensions.filter( ++ item => !newEnabledExtensions.includes(item) ++ ).forEach(uuid => { ++ this.disableExtension(uuid); ++ }); ++ ++ this._enabledExtensions = newEnabledExtensions; ++ } ++ ++ _onVersionValidationChanged() { ++ // we want to reload all extensions, but only enable ++ // extensions when allowed by the sessionMode, so ++ // temporarily disable them all ++ this._enabledExtensions = []; ++ for (let uuid in ExtensionUtils.extensions) ++ this.reloadExtension(ExtensionUtils.extensions[uuid]); ++ this._enabledExtensions = this._getEnabledExtensions(); ++ ++ if (Main.sessionMode.allowExtensions) { ++ this._enabledExtensions.forEach(uuid => { ++ this.enableExtension(uuid); ++ }); ++ } + } +- enabled = true; +-} + +-function disableAllExtensions() { +- if (!enabled) +- return; ++ _loadExtensions() { ++ global.settings.connect(`changed::${ENABLED_EXTENSIONS_KEY}`, ++ this._onEnabledExtensionsChanged.bind(this)); ++ global.settings.connect(`changed::${DISABLE_USER_EXTENSIONS_KEY}`, ++ this._onEnabledExtensionsChanged.bind(this)); ++ global.settings.connect(`changed::${EXTENSION_DISABLE_VERSION_CHECK_KEY}`, ++ this._onVersionValidationChanged.bind(this)); ++ ++ this._enabledExtensions = this._getEnabledExtensions(); + +- if (initted) { +- extensionOrder.slice().reverse().forEach(uuid => { +- disableExtension(uuid); ++ let finder = new ExtensionUtils.ExtensionFinder(); ++ finder.connect('extension-found', (finder, extension) => { ++ this.loadExtension(extension); + }); ++ finder.scanExtensions(); ++ } ++ ++ enableAllExtensions() { ++ if (this._enabled) ++ return; ++ ++ if (!this._initted) { ++ this._loadExtensions(); ++ this._initted = true; ++ } else { ++ this._enabledExtensions.forEach(uuid => { ++ this.enableExtension(uuid); ++ }); ++ } ++ this._enabled = true; + } + +- enabled = false; +-} +- +-function _sessionUpdated() { +- // For now sessionMode.allowExtensions controls extensions from both the +- // 'enabled-extensions' preference and the sessionMode.enabledExtensions +- // property; it might make sense to make enabledExtensions independent +- // from allowExtensions in the future +- if (Main.sessionMode.allowExtensions) { +- if (initted) +- enabledExtensions = getEnabledExtensions(); +- enableAllExtensions(); +- } else { +- disableAllExtensions(); ++ disableAllExtensions() { ++ if (!this._enabled) ++ return; ++ ++ if (this._initted) { ++ this._extensionOrder.slice().reverse().forEach(uuid => { ++ this.disableExtension(uuid); ++ }); ++ } ++ ++ this._enabled = false; + } +-} + +-function init() { +- Main.sessionMode.connect('updated', _sessionUpdated); +- _sessionUpdated(); +-} ++ _sessionUpdated() { ++ // For now sessionMode.allowExtensions controls extensions from both the ++ // 'enabled-extensions' preference and the sessionMode.enabledExtensions ++ // property; it might make sense to make enabledExtensions independent ++ // from allowExtensions in the future ++ if (Main.sessionMode.allowExtensions) { ++ if (this._initted) ++ this._enabledExtensions = this._getEnabledExtensions(); ++ this.enableAllExtensions(); ++ } else { ++ this.disableAllExtensions(); ++ } ++ } ++}; ++Signals.addSignalMethods(ExtensionManager.prototype); +diff --git a/js/ui/lookingGlass.js b/js/ui/lookingGlass.js +index fefb3f731..e947574f2 100644 +--- a/js/ui/lookingGlass.js ++++ b/js/ui/lookingGlass.js +@@ -7,7 +7,6 @@ const Signals = imports.signals; + const System = imports.system; + + const History = imports.misc.history; +-const ExtensionSystem = imports.ui.extensionSystem; + const ExtensionUtils = imports.misc.extensionUtils; + const ShellEntry = imports.ui.shellEntry; + const Tweener = imports.ui.tweener; +@@ -624,8 +623,8 @@ var Extensions = class Extensions { + for (let uuid in ExtensionUtils.extensions) + this._loadExtension(null, uuid); + +- ExtensionSystem.connect('extension-loaded', +- this._loadExtension.bind(this)); ++ Main.extensionManager.connect('extension-loaded', ++ this._loadExtension.bind(this)); + } + + _loadExtension(o, uuid) { +diff --git a/js/ui/main.js b/js/ui/main.js +index 8dde95bf9..7bfbce497 100644 +--- a/js/ui/main.js ++++ b/js/ui/main.js +@@ -43,6 +43,7 @@ const STICKY_KEYS_ENABLE = 'stickykeys-enable'; + const GNOMESHELL_STARTED_MESSAGE_ID = 'f3ea493c22934e26811cd62abe8e203a'; + + var componentManager = null; ++var extensionManager = null; + var panel = null; + var overview = null; + var runDialog = null; +@@ -218,7 +219,7 @@ function _initializeUI() { + _startDate = new Date(); + + ExtensionDownloader.init(); +- ExtensionSystem.init(); ++ extensionManager = new ExtensionSystem.ExtensionManager(); + + if (sessionMode.isGreeter && screenShield) { + layoutManager.connect('startup-prepared', () => { +diff --git a/js/ui/shellDBus.js b/js/ui/shellDBus.js +index 112d60feb..4b04e68ac 100644 +--- a/js/ui/shellDBus.js ++++ b/js/ui/shellDBus.js +@@ -4,7 +4,6 @@ const { Gio, GLib, Meta, Shell } = imports.gi; + const Lang = imports.lang; + + const Config = imports.misc.config; +-const ExtensionSystem = imports.ui.extensionSystem; + const ExtensionDownloader = imports.ui.extensionDownloader; + const ExtensionUtils = imports.misc.extensionUtils; + const Main = imports.ui.main; +@@ -250,8 +249,8 @@ var GnomeShellExtensions = class { + constructor() { + this._dbusImpl = Gio.DBusExportedObject.wrapJSObject(GnomeShellExtensionsIface, this); + this._dbusImpl.export(Gio.DBus.session, '/org/gnome/Shell'); +- ExtensionSystem.connect('extension-state-changed', +- this._extensionStateChanged.bind(this)); ++ Main.extensionManager.connect('extension-state-changed', ++ this._extensionStateChanged.bind(this)); + } + + ListExtensions() { +@@ -334,7 +333,7 @@ var GnomeShellExtensions = class { + if (!extension) + return; + +- ExtensionSystem.reloadExtension(extension); ++ Main.extensionManager.reloadExtension(extension); + } + + CheckForUpdates() { +-- +2.29.2 + + +From 7419ee28b5b568dd4478db7f3c890ff01678637a Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Florian=20M=C3=BCllner?= +Date: Mon, 8 Jul 2019 02:53:32 +0200 +Subject: [PATCH 03/26] extensionSystem: Make methods to call extension + functions private + +While public methods to enable/disable extensions make sense for an +extension manager, the existing ones are only used internally. Make +them private and rename them, so that we can re-use the current +names for more useful public methods. + +https://bugzilla.gnome.org/show_bug.cgi?id=789852 +--- + js/ui/extensionSystem.js | 32 ++++++++++++++++---------------- + 1 file changed, 16 insertions(+), 16 deletions(-) + +diff --git a/js/ui/extensionSystem.js b/js/ui/extensionSystem.js +index b7e908223..c5fb007ae 100644 +--- a/js/ui/extensionSystem.js ++++ b/js/ui/extensionSystem.js +@@ -24,7 +24,7 @@ var ExtensionManager = class { + this._sessionUpdated(); + } + +- disableExtension(uuid) { ++ _callExtensionDisable(uuid) { + let extension = ExtensionUtils.extensions[uuid]; + if (!extension) + return; +@@ -82,13 +82,13 @@ var ExtensionManager = class { + } + } + +- enableExtension(uuid) { ++ _callExtensionEnable(uuid) { + let extension = ExtensionUtils.extensions[uuid]; + if (!extension) + return; + + if (extension.state == ExtensionState.INITIALIZED) +- this.initExtension(uuid); ++ this._callExtensionInit(uuid); + + if (extension.state != ExtensionState.DISABLED) + return; +@@ -155,10 +155,10 @@ var ExtensionManager = class { + } else { + let enabled = this._enabledExtensions.includes(extension.uuid); + if (enabled) { +- if (!this.initExtension(extension.uuid)) ++ if (!this._callExtensionInit(extension.uuid)) + return; + if (extension.state == ExtensionState.DISABLED) +- this.enableExtension(extension.uuid); ++ this._callExtensionEnable(extension.uuid); + } else { + extension.state = ExtensionState.INITIALIZED; + } +@@ -171,7 +171,7 @@ var ExtensionManager = class { + // Try to disable it -- if it's ERROR'd, we can't guarantee that, + // but it will be removed on next reboot, and hopefully nothing + // broke too much. +- this.disableExtension(extension.uuid); ++ this._callExtensionDisable(extension.uuid); + + extension.state = ExtensionState.UNINSTALLED; + this.emit('extension-state-changed', extension); +@@ -200,7 +200,7 @@ var ExtensionManager = class { + this.loadExtension(newExtension); + } + +- initExtension(uuid) { ++ _callExtensionInit(uuid) { + let extension = ExtensionUtils.extensions[uuid]; + let dir = extension.dir; + +@@ -266,7 +266,7 @@ var ExtensionManager = class { + newEnabledExtensions.filter( + uuid => !this._enabledExtensions.includes(uuid) + ).forEach(uuid => { +- this.enableExtension(uuid); ++ this._callExtensionEnable(uuid); + }); + + // Find and disable all the newly disabled extensions: UUIDs found in the +@@ -274,7 +274,7 @@ var ExtensionManager = class { + this._enabledExtensions.filter( + item => !newEnabledExtensions.includes(item) + ).forEach(uuid => { +- this.disableExtension(uuid); ++ this._callExtensionDisable(uuid); + }); + + this._enabledExtensions = newEnabledExtensions; +@@ -291,7 +291,7 @@ var ExtensionManager = class { + + if (Main.sessionMode.allowExtensions) { + this._enabledExtensions.forEach(uuid => { +- this.enableExtension(uuid); ++ this._callExtensionEnable(uuid); + }); + } + } +@@ -313,7 +313,7 @@ var ExtensionManager = class { + finder.scanExtensions(); + } + +- enableAllExtensions() { ++ _enableAllExtensions() { + if (this._enabled) + return; + +@@ -322,19 +322,19 @@ var ExtensionManager = class { + this._initted = true; + } else { + this._enabledExtensions.forEach(uuid => { +- this.enableExtension(uuid); ++ this._callExtensionEnable(uuid); + }); + } + this._enabled = true; + } + +- disableAllExtensions() { ++ _disableAllExtensions() { + if (!this._enabled) + return; + + if (this._initted) { + this._extensionOrder.slice().reverse().forEach(uuid => { +- this.disableExtension(uuid); ++ this._callExtensionDisable(uuid); + }); + } + +@@ -349,9 +349,9 @@ var ExtensionManager = class { + if (Main.sessionMode.allowExtensions) { + if (this._initted) + this._enabledExtensions = this._getEnabledExtensions(); +- this.enableAllExtensions(); ++ this._enableAllExtensions(); + } else { +- this.disableAllExtensions(); ++ this._disableAllExtensions(); + } + } + }; +-- +2.29.2 + + +From e88419278531b136984f9f05a8c056145d03edba Mon Sep 17 00:00:00 2001 +From: Didier Roche +Date: Wed, 17 Jan 2018 13:43:11 +0100 +Subject: [PATCH 04/26] extensionSystem: Add methods to enable/disable + extensions + +Extensions are currently enabled or disabled by directly changing the +list in the 'enabled-extensions' GSettings key. As we will soon add +an overriding 'disabled-extensions' key as well, it makes sense to +offer explicit API for enabling/disabling to avoid duplicating the +logic. + +For the corresponding D-Bus API, the methods were even mentioned in +the GSettings schema, albeit unimplemented until now. + +https://bugzilla.gnome.org/show_bug.cgi?id=789852 +--- + .../org.gnome.Shell.Extensions.xml | 24 +++++++++++++++++ + js/ui/extensionDownloader.js | 12 ++------- + js/ui/extensionSystem.js | 26 +++++++++++++++++++ + js/ui/shellDBus.js | 8 ++++++ + 4 files changed, 60 insertions(+), 10 deletions(-) + +diff --git a/data/dbus-interfaces/org.gnome.Shell.Extensions.xml b/data/dbus-interfaces/org.gnome.Shell.Extensions.xml +index ce69439fc..22273f889 100644 +--- a/data/dbus-interfaces/org.gnome.Shell.Extensions.xml ++++ b/data/dbus-interfaces/org.gnome.Shell.Extensions.xml +@@ -173,6 +173,30 @@ + + + ++ ++ \ ++ \ ++ \ ++ \ ++ ++ ++ \ ++ \ ++ \ ++ \ ++ + + + + +diff --git a/js/extensionPrefs/main.js b/js/extensionPrefs/main.js +index 43efa95e9..2b4ce5753 100644 +--- a/js/extensionPrefs/main.js ++++ b/js/extensionPrefs/main.js +@@ -241,7 +241,7 @@ var Application = class { + this._mainStack.add_named(new EmptyPlaceholder(), 'placeholder'); + + this._shellProxy = new GnomeShellProxy(Gio.DBus.session, 'org.gnome.Shell', '/org/gnome/Shell'); +- this._shellProxy.connectSignal('ExtensionStatusChanged', (proxy, senderName, [uuid, state, error]) => { ++ this._shellProxy.connectSignal('ExtensionStateChanged', (proxy, senderName, [uuid, state]) => { + if (ExtensionUtils.extensions[uuid] !== undefined) + this._scanExtensions(); + }); +diff --git a/js/ui/extensionSystem.js b/js/ui/extensionSystem.js +index 8ff0fa56f..98eaf5259 100644 +--- a/js/ui/extensionSystem.js ++++ b/js/ui/extensionSystem.js +@@ -159,15 +159,14 @@ var ExtensionManager = class { + + let message = '' + error; + ++ extension.error = message; + extension.state = ExtensionState.ERROR; + if (!extension.errors) + extension.errors = []; + extension.errors.push(message); + + log('Extension "%s" had error: %s'.format(uuid, message)); +- this.emit('extension-state-changed', { uuid: uuid, +- error: message, +- state: extension.state }); ++ this.emit('extension-state-changed', extension); + } + + loadExtension(extension) { +diff --git a/js/ui/shellDBus.js b/js/ui/shellDBus.js +index af5889789..23274c0a3 100644 +--- a/js/ui/shellDBus.js ++++ b/js/ui/shellDBus.js +@@ -335,6 +335,10 @@ var GnomeShellExtensions = class { + } + + _extensionStateChanged(_, newState) { ++ let state = ExtensionUtils.serializeExtension(newState); ++ this._dbusImpl.emit_signal('ExtensionStateChanged', ++ new GLib.Variant('(sa{sv})', [newState.uuid, state])); ++ + this._dbusImpl.emit_signal('ExtensionStatusChanged', + GLib.Variant.new('(sis)', [newState.uuid, newState.state, newState.error])); + } +-- +2.29.2 + + +From 47d185f8964f9e04430f4ef97d6d712faaf32078 Mon Sep 17 00:00:00 2001 +From: Didier Roche +Date: Tue, 4 Dec 2018 09:31:27 +0100 +Subject: [PATCH 07/26] extensionSystem: Add canChange property to extensions + +Whether or not an extension can be enabled/disabled depends on various +factors: Whether the extension is in error state, whether user extensions +are disabled and whether the underlying GSettings keys are writable. + +This is complex enough to share the logic, so add it to the extension +properties that are exposed over D-Bus. + +https://bugzilla.gnome.org/show_bug.cgi?id=789852 +--- + js/misc/extensionUtils.js | 3 ++- + js/ui/extensionSystem.js | 44 +++++++++++++++++++++++++++++++++------ + 2 files changed, 40 insertions(+), 7 deletions(-) + +diff --git a/js/misc/extensionUtils.js b/js/misc/extensionUtils.js +index bc9c36f4e..025cd042e 100644 +--- a/js/misc/extensionUtils.js ++++ b/js/misc/extensionUtils.js +@@ -31,7 +31,7 @@ var ExtensionState = { + UNINSTALLED: 99 + }; + +-const SERIALIZED_PROPERTIES = ['type', 'state', 'path', 'error', 'hasPrefs']; ++const SERIALIZED_PROPERTIES = ['type', 'state', 'path', 'error', 'hasPrefs', 'canChange']; + + // Maps uuid -> metadata object + var extensions = {}; +@@ -222,6 +222,7 @@ function createExtensionObject(uuid, dir, type) { + extension.path = dir.get_path(); + extension.error = ''; + extension.hasPrefs = dir.get_child('prefs.js').query_exists(null); ++ extension.canChange = false; + + extensions[uuid] = extension; + +diff --git a/js/ui/extensionSystem.js b/js/ui/extensionSystem.js +index 98eaf5259..a83e53c83 100644 +--- a/js/ui/extensionSystem.js ++++ b/js/ui/extensionSystem.js +@@ -189,6 +189,7 @@ var ExtensionManager = class { + } + } + ++ this._updateCanChange(extension); + this.emit('extension-state-changed', extension); + } + +@@ -267,12 +268,28 @@ var ExtensionManager = class { + return true; + } + +- _getEnabledExtensions() { +- let extensions; ++ _getModeExtensions() { + if (Array.isArray(Main.sessionMode.enabledExtensions)) +- extensions = Main.sessionMode.enabledExtensions; +- else +- extensions = []; ++ return Main.sessionMode.enabledExtensions; ++ return []; ++ } ++ ++ _updateCanChange(extension) { ++ let hasError = ++ extension.state == ExtensionState.ERROR || ++ extension.state == ExtensionState.OUT_OF_DATE; ++ ++ let isMode = this._getModeExtensions().includes(extension.uuid); ++ let modeOnly = global.settings.get_boolean(DISABLE_USER_EXTENSIONS_KEY); ++ ++ extension.canChange = ++ !hasError && ++ global.settings.is_writable(ENABLED_EXTENSIONS_KEY) && ++ (isMode || !modeOnly); ++ } ++ ++ _getEnabledExtensions() { ++ let extensions = this._getModeExtensions(); + + if (global.settings.get_boolean(DISABLE_USER_EXTENSIONS_KEY)) + return extensions; +@@ -280,6 +297,11 @@ var ExtensionManager = class { + return extensions.concat(global.settings.get_strv(ENABLED_EXTENSIONS_KEY)); + } + ++ _onUserExtensionsEnabledChanged() { ++ this._onEnabledExtensionsChanged(); ++ this._onSettingsWritableChanged(); ++ } ++ + _onEnabledExtensionsChanged() { + let newEnabledExtensions = this._getEnabledExtensions(); + +@@ -305,6 +327,14 @@ var ExtensionManager = class { + this._enabledExtensions = newEnabledExtensions; + } + ++ _onSettingsWritableChanged() { ++ for (let uuid in ExtensionUtils.extensions) { ++ let extension = ExtensionUtils.extensions[uuid]; ++ this._updateCanChange(extension); ++ this.emit('extension-state-changed', extension); ++ } ++ } ++ + _onVersionValidationChanged() { + // we want to reload all extensions, but only enable + // extensions when allowed by the sessionMode, so +@@ -325,9 +355,11 @@ var ExtensionManager = class { + global.settings.connect(`changed::${ENABLED_EXTENSIONS_KEY}`, + this._onEnabledExtensionsChanged.bind(this)); + global.settings.connect(`changed::${DISABLE_USER_EXTENSIONS_KEY}`, +- this._onEnabledExtensionsChanged.bind(this)); ++ this._onUserExtensionsEnabledChanged.bind(this)); + global.settings.connect(`changed::${EXTENSION_DISABLE_VERSION_CHECK_KEY}`, + this._onVersionValidationChanged.bind(this)); ++ global.settings.connect(`writable-changed::${ENABLED_EXTENSIONS_KEY}`, ++ this._onSettingsWritableChanged.bind(this)); + + this._enabledExtensions = this._getEnabledExtensions(); + +-- +2.29.2 + + +From ce14c00ca0707c78dc920ede3a157b7c9d55fff5 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Marco=20Trevisan=20=28Trevi=C3=B1o=29?= +Date: Tue, 28 May 2019 23:22:37 +0200 +Subject: [PATCH 08/26] extensionPrefs: Inherit from Gtk.Application + +Extension preferences Application class is just a container for a GtkApplication +so instead of using composition we can inherit from the base GObject class. + +Also replace signal connections with vfunc's. + +https://gitlab.gnome.org/GNOME/gnome-shell/merge_requests/631 +--- + js/extensionPrefs/main.js | 35 +++++++++++++++++------------------ + 1 file changed, 17 insertions(+), 18 deletions(-) + +diff --git a/js/extensionPrefs/main.js b/js/extensionPrefs/main.js +index 2b4ce5753..94a5b12fe 100644 +--- a/js/extensionPrefs/main.js ++++ b/js/extensionPrefs/main.js +@@ -17,18 +17,16 @@ function stripPrefix(string, prefix) { + return string; + } + +-var Application = class { +- constructor() { ++var Application = GObject.registerClass({ ++ GTypeName: 'ExtensionPrefs_Application' ++}, class Application extends Gtk.Application { ++ _init() { + GLib.set_prgname('gnome-shell-extension-prefs'); +- this.application = new Gtk.Application({ ++ super._init({ + application_id: 'org.gnome.shell.ExtensionPrefs', + flags: Gio.ApplicationFlags.HANDLES_COMMAND_LINE + }); + +- this.application.connect('activate', this._onActivate.bind(this)); +- this.application.connect('command-line', this._onCommandLine.bind(this)); +- this.application.connect('startup', this._onStartup.bind(this)); +- + this._extensionPrefsModules = {}; + + this._startupUuid = null; +@@ -84,7 +82,7 @@ var Application = class { + visible: true })); + + if (this._skipMainWindow) { +- this.application.add_window(dialog); ++ this.add_window(dialog); + if (this._window) + this._window.destroy(); + this._window = dialog; +@@ -206,8 +204,8 @@ var Application = class { + return scroll; + } + +- _buildUI(app) { +- this._window = new Gtk.ApplicationWindow({ application: app, ++ _buildUI() { ++ this._window = new Gtk.ApplicationWindow({ application: this, + window_position: Gtk.WindowPosition.CENTER }); + + this._window.set_default_size(800, 500); +@@ -295,17 +293,19 @@ var Application = class { + this._loaded = true; + } + +- _onActivate() { ++ vfunc_activate() { + this._window.present(); + } + +- _onStartup(app) { +- this._buildUI(app); ++ vfunc_startup() { ++ super.vfunc_startup(); ++ ++ this._buildUI(); + this._scanExtensions(); + } + +- _onCommandLine(app, commandLine) { +- app.activate(); ++ vfunc_command_line(commandLine) { ++ this.activate(); + let args = commandLine.get_arguments(); + + if (args.length) { +@@ -325,7 +325,7 @@ var Application = class { + } + return 0; + } +-}; ++}); + + var Expander = GObject.registerClass({ + Properties: { +@@ -631,6 +631,5 @@ function main(argv) { + Gettext.bindtextdomain(Config.GETTEXT_PACKAGE, Config.LOCALEDIR); + Gettext.textdomain(Config.GETTEXT_PACKAGE); + +- let app = new Application(); +- app.application.run(argv); ++ new Application().run(argv); + } +-- +2.29.2 + + +From ea202d21b65c027db03e773a51bd8fecf4a2fb0a Mon Sep 17 00:00:00 2001 +From: Didier Roche +Date: Thu, 1 Nov 2018 13:50:30 +0100 +Subject: [PATCH 09/26] extensionPrefs: Attach extension object to each row + +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 +--- + js/extensionPrefs/main.js | 120 +++++++++++++++++++------------------- + 1 file changed, 60 insertions(+), 60 deletions(-) + +diff --git a/js/extensionPrefs/main.js b/js/extensionPrefs/main.js +index 94a5b12fe..7e7b2dcc7 100644 +--- a/js/extensionPrefs/main.js ++++ b/js/extensionPrefs/main.js +@@ -34,52 +34,31 @@ var Application = GObject.registerClass({ + this._skipMainWindow = false; + } + +- _extensionAvailable(uuid) { +- let extension = ExtensionUtils.extensions[uuid]; +- +- if (!extension) +- return false; ++ _showPrefs(uuid) { ++ let row = this._extensionSelector.get_children().find(c => { ++ return c.uuid === uuid && c.hasPrefs; ++ }); + +- if (!extension.dir.get_child('prefs.js').query_exists(null)) ++ if (!row) + return false; + +- return true; +- } +- +- _getExtensionPrefsModule(extension) { +- let uuid = extension.metadata.uuid; +- +- if (this._extensionPrefsModules.hasOwnProperty(uuid)) +- return this._extensionPrefsModules[uuid]; +- +- ExtensionUtils.installImporter(extension); +- +- let prefsModule = extension.imports.prefs; +- prefsModule.init(extension.metadata); +- +- this._extensionPrefsModules[uuid] = prefsModule; +- return prefsModule; +- } +- +- _selectExtension(uuid) { +- if (!this._extensionAvailable(uuid)) +- return; +- +- let extension = ExtensionUtils.extensions[uuid]; + let widget; + + try { +- let prefsModule = this._getExtensionPrefsModule(extension); +- widget = prefsModule.buildPrefsWidget(); ++ widget = row.prefsModule.buildPrefsWidget(); + } catch (e) { +- widget = this._buildErrorUI(extension, 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: extension.metadata.name, +- visible: true })); ++ 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); +@@ -96,7 +75,7 @@ var Application = GObject.registerClass({ + dialog.show(); + } + +- _buildErrorUI(extension, exc) { ++ _buildErrorUI(row, exc) { + let scroll = new Gtk.ScrolledWindow({ + hscrollbar_policy: Gtk.PolicyType.NEVER, + propagate_natural_height: true +@@ -183,13 +162,13 @@ var Application = GObject.registerClass({ + label: _("Homepage"), + tooltip_text: _("Visit extension homepage"), + no_show_all: true, +- visible: extension.metadata.url != null ++ 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(extension.metadata.url, context); ++ Gio.AppInfo.launch_default_for_uri(row.url, context); + }); + + let expandedBox = new Gtk.Box({ +@@ -248,9 +227,7 @@ var Application = GObject.registerClass({ + } + + _sortList(row1, row2) { +- let name1 = ExtensionUtils.extensions[row1.uuid].metadata.name; +- let name2 = ExtensionUtils.extensions[row2.uuid].metadata.name; +- return name1.localeCompare(name2); ++ return row1.name.localeCompare(row2.name); + } + + _updateHeader(row, before) { +@@ -269,11 +246,10 @@ var Application = GObject.registerClass({ + } + + _extensionFound(finder, extension) { +- let row = new ExtensionRow(extension.uuid); ++ let row = new ExtensionRow(extension); + +- row.prefsButton.visible = this._extensionAvailable(row.uuid); + row.prefsButton.connect('clicked', () => { +- this._selectExtension(row.uuid); ++ this._showPrefs(row.uuid); + }); + + row.show_all(); +@@ -286,8 +262,8 @@ var Application = GObject.registerClass({ + else + this._mainStack.visible_child_name = 'placeholder'; + +- if (this._startupUuid && this._extensionAvailable(this._startupUuid)) +- this._selectExtension(this._startupUuid); ++ if (this._startupUuid) ++ this._showPrefs(this._startupUuid); + this._startupUuid = null; + this._skipMainWindow = false; + this._loaded = true; +@@ -316,11 +292,9 @@ var Application = GObject.registerClass({ + // Strip off "extension:///" prefix which fakes a URI, if it exists + uuid = stripPrefix(uuid, "extension:///"); + +- if (this._extensionAvailable(uuid)) +- this._selectExtension(uuid); +- else if (!this._loaded) ++ if (!this._loaded) + this._startupUuid = uuid; +- else ++ else if (!this._showPrefs(uuid)) + this._skipMainWindow = false; + } + return 0; +@@ -504,10 +478,11 @@ class DescriptionLabel extends Gtk.Label { + + var ExtensionRow = GObject.registerClass( + class ExtensionRow extends Gtk.ListBoxRow { +- _init(uuid) { ++ _init(extension) { + super._init(); + +- this.uuid = uuid; ++ this._extension = extension; ++ this._prefsModule = null; + + this._settings = new Gio.Settings({ schema_id: 'org.gnome.shell' }); + this._settings.connect('changed::enabled-extensions', () => { +@@ -525,9 +500,23 @@ class ExtensionRow extends Gtk.ListBoxRow { + this._buildUI(); + } + +- _buildUI() { +- let extension = ExtensionUtils.extensions[this.uuid]; ++ 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 }); +@@ -537,19 +526,20 @@ class ExtensionRow extends Gtk.ListBoxRow { + spacing: 6, hexpand: true }); + hbox.add(vbox); + +- let name = GLib.markup_escape_text(extension.metadata.name, -1); ++ 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 = extension.metadata.description.split('\n')[0]; ++ 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, +@@ -573,11 +563,10 @@ class ExtensionRow extends Gtk.ListBoxRow { + } + + _canEnable() { +- let extension = ExtensionUtils.extensions[this.uuid]; + let checkVersion = !this._settings.get_boolean('disable-extension-version-validation'); + + return !this._settings.get_boolean('disable-user-extensions') && +- !(checkVersion && ExtensionUtils.isOutOfDate(extension)); ++ !(checkVersion && ExtensionUtils.isOutOfDate(this._extension)); + } + + _isEnabled() { +@@ -605,6 +594,17 @@ class ExtensionRow extends Gtk.ListBoxRow { + } 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() { +-- +2.29.2 + + +From 8d80e6667ded38dac53fe245a10191b5d4a3150b Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Florian=20M=C3=BCllner?= +Date: Sat, 6 Jul 2019 01:48:05 +0200 +Subject: [PATCH 10/26] extensionPrefs: Override getCurrentExtension() for + extensions + +Extensions are used to calling the getCurrentExtension() utility function, +both from the extension itself and from its preferences. For the latter, +that relies on the extensions map in ExtensionUtils being populated from +the separated extension-prefs process just like from gnome-shell. + +This won't be the case anymore when we switch to the extensions D-Bus API, +but as we know which extension we are showing the prefs dialog for, we +can patch in a simple replacement that gives extensions the expected API. + +https://bugzilla.gnome.org/show_bug.cgi?id=789852 +--- + js/extensionPrefs/main.js | 3 +++ + 1 file changed, 3 insertions(+) + +diff --git a/js/extensionPrefs/main.js b/js/extensionPrefs/main.js +index 7e7b2dcc7..29de8202a 100644 +--- a/js/extensionPrefs/main.js ++++ b/js/extensionPrefs/main.js +@@ -599,6 +599,9 @@ class ExtensionRow extends Gtk.ListBoxRow { + 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); + } +-- +2.29.2 + + +From e03a21b1f768405050bbfda1eb2bbf2ffcf7b4ca Mon Sep 17 00:00:00 2001 +From: Didier Roche +Date: Thu, 1 Nov 2018 13:55:17 +0100 +Subject: [PATCH 11/26] extensionPrefs: Switch to D-Bus API to get extension + live state + +By direclty using the underlying GSetting, whether or not an extension +appears as enabled or disabled currently depends only on whether it is +included in the 'enabled-extensions' list or not. + +However this doesn't necessarily reflect the real extension state, as an +extension may be in error state, or enabled via the session mode. + +Switch to the extensions D-Bus API to ensure that the list of extensions +and each extension's state correctly reflects the state in gnome-shell. + +https://bugzilla.gnome.org/show_bug.cgi?id=789852 +--- + js/extensionPrefs/main.js | 166 +++++++++++++++++++++++++------------- + 1 file changed, 110 insertions(+), 56 deletions(-) + +diff --git a/js/extensionPrefs/main.js b/js/extensionPrefs/main.js +index 29de8202a..f1b732e85 100644 +--- a/js/extensionPrefs/main.js ++++ b/js/extensionPrefs/main.js +@@ -8,6 +8,8 @@ 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); + +@@ -32,6 +34,11 @@ var Application = GObject.registerClass({ + this._startupUuid = null; + this._loaded = false; + this._skipMainWindow = false; ++ this._shellProxy = null; ++ } ++ ++ get shellProxy() { ++ return this._shellProxy; + } + + _showPrefs(uuid) { +@@ -218,10 +225,8 @@ var Application = GObject.registerClass({ + 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._shellProxy.connectSignal('ExtensionStateChanged', ++ this._onExtensionStateChanged.bind(this)); + + this._window.show_all(); + } +@@ -238,14 +243,51 @@ var Application = GObject.registerClass({ + 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() { +- let finder = new ExtensionUtils.ExtensionFinder(); +- finder.connect('extension-found', this._extensionFound.bind(this)); +- finder.scanExtensions(); +- this._extensionsLoaded(); ++ 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(); ++ }); + } + +- _extensionFound(finder, extension) { ++ _addExtensionRow(extension) { + let row = new ExtensionRow(extension); + + row.prefsButton.connect('clicked', () => { +@@ -466,6 +508,35 @@ class EmptyPlaceholder extends Gtk.Box { + } + }); + ++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) { +@@ -481,22 +552,23 @@ class ExtensionRow extends Gtk.ListBoxRow { + _init(extension) { + super._init(); + ++ this._app = Gio.Application.get_default(); + 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._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(); + } + +@@ -516,6 +588,15 @@ class ExtensionRow extends Gtk.ListBoxRow { + 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, +@@ -549,50 +630,23 @@ class ExtensionRow extends Gtk.ListBoxRow { + + this.prefsButton = button; + +- this._switch = new Gtk.Switch({ valign: Gtk.Align.CENTER, +- sensitive: this._canEnable(), +- state: this._isEnabled() }); ++ 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._enable(); ++ this._app.shellProxy.EnableExtensionRemote(this.uuid); + else +- this._disable(); ++ this._app.shellProxy.DisableExtensionRemote(this.uuid); + }); + 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.indexOf(this.uuid) != -1; +- } +- +- _enable() { +- let extensions = this._settings.get_strv('enabled-extensions'); +- if (extensions.indexOf(this.uuid) != -1) +- 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); ++ _canToggle() { ++ return this._extension.canChange; + } + + get prefsModule() { +-- +2.29.2 + + +From 9baf77dcae765618902d958549801276156f1255 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Florian=20M=C3=BCllner?= +Date: Sun, 7 Jul 2019 23:38:27 +0200 +Subject: [PATCH 12/26] extensionSystem: Move extension loading into + ExtensionManager + +Now that extension loading and the extensions map are no longer shared +between the gnome-shell and gnome-shell-extension-prefs processes, we +can move both into the ExtensionManager which makes much more sense +conceptually. + +https://bugzilla.gnome.org/show_bug.cgi?id=789852 +--- + js/misc/extensionUtils.js | 95 ++---------------------------- + js/ui/extensionDownloader.js | 12 ++-- + js/ui/extensionSystem.js | 110 +++++++++++++++++++++++++++++------ + js/ui/lookingGlass.js | 4 +- + js/ui/main.js | 1 + + js/ui/shellDBus.js | 8 +-- + 6 files changed, 111 insertions(+), 119 deletions(-) + +diff --git a/js/misc/extensionUtils.js b/js/misc/extensionUtils.js +index 025cd042e..c513ebc06 100644 +--- a/js/misc/extensionUtils.js ++++ b/js/misc/extensionUtils.js +@@ -7,10 +7,8 @@ const { Gio, GLib } = imports.gi; + + const Gettext = imports.gettext; + const Lang = imports.lang; +-const Signals = imports.signals; + + const Config = imports.misc.config; +-const FileUtils = imports.misc.fileUtils; + + var ExtensionType = { + SYSTEM: 1, +@@ -33,9 +31,6 @@ var ExtensionState = { + + const SERIALIZED_PROPERTIES = ['type', 'state', 'path', 'error', 'hasPrefs', 'canChange']; + +-// Maps uuid -> metadata object +-var extensions = {}; +- + /** + * getCurrentExtension: + * +@@ -66,13 +61,17 @@ function getCurrentExtension() { + if (!match) + return null; + ++ // local import, as the module is used from outside the gnome-shell process ++ // as well (not this function though) ++ let extensionManager = imports.ui.main.extensionManager; ++ + let path = match[1]; + let file = Gio.File.new_for_path(path); + + // Walk up the directory tree, looking for an extension with + // the same UUID as a directory name. + while (file != null) { +- let extension = extensions[file.get_basename()]; ++ let extension = extensionManager.extensions[file.get_basename()]; + if (extension !== undefined) + return extension; + file = file.get_parent(); +@@ -178,57 +177,6 @@ function isOutOfDate(extension) { + return false; + } + +-function createExtensionObject(uuid, dir, type) { +- let info; +- +- let metadataFile = dir.get_child('metadata.json'); +- if (!metadataFile.query_exists(null)) { +- throw new Error('Missing metadata.json'); +- } +- +- let metadataContents, success, tag; +- try { +- [success, metadataContents, tag] = metadataFile.load_contents(null); +- if (metadataContents instanceof Uint8Array) +- metadataContents = imports.byteArray.toString(metadataContents); +- } catch (e) { +- throw new Error('Failed to load metadata.json: ' + e); +- } +- let meta; +- try { +- meta = JSON.parse(metadataContents); +- } catch (e) { +- throw new Error('Failed to parse metadata.json: ' + e); +- } +- +- let requiredProperties = ['uuid', 'name', 'description', 'shell-version']; +- for (let i = 0; i < requiredProperties.length; i++) { +- let prop = requiredProperties[i]; +- if (!meta[prop]) { +- throw new Error('missing "' + prop + '" property in metadata.json'); +- } +- } +- +- if (uuid != meta.uuid) { +- throw new Error('uuid "' + meta.uuid + '" from metadata.json does not match directory name "' + uuid + '"'); +- } +- +- let extension = {}; +- +- extension.metadata = meta; +- extension.uuid = meta.uuid; +- extension.type = type; +- extension.dir = dir; +- extension.path = dir.get_path(); +- extension.error = ''; +- extension.hasPrefs = dir.get_child('prefs.js').query_exists(null); +- extension.canChange = false; +- +- extensions[uuid] = extension; +- +- return extension; +-} +- + function serializeExtension(extension) { + let obj = {}; + Lang.copyProperties(extension.metadata, obj); +@@ -283,36 +231,3 @@ function installImporter(extension) { + extension.imports = imports[extension.uuid]; + imports.searchPath = oldSearchPath; + } +- +-var ExtensionFinder = class { +- _loadExtension(extensionDir, info, perUserDir) { +- let fileType = info.get_file_type(); +- if (fileType != Gio.FileType.DIRECTORY) +- return; +- let uuid = info.get_name(); +- let existing = extensions[uuid]; +- if (existing) { +- log('Extension %s already installed in %s. %s will not be loaded'.format(uuid, existing.path, extensionDir.get_path())); +- return; +- } +- +- let extension; +- let type = extensionDir.has_prefix(perUserDir) ? ExtensionType.PER_USER +- : ExtensionType.SYSTEM; +- try { +- extension = createExtensionObject(uuid, extensionDir, type); +- } catch(e) { +- logError(e, 'Could not load extension %s'.format(uuid)); +- return; +- } +- this.emit('extension-found', extension); +- } +- +- scanExtensions() { +- let perUserDir = Gio.File.new_for_path(global.userdatadir); +- FileUtils.collectFromDatadirs('extensions', true, (dir, info) => { +- this._loadExtension(dir, info, perUserDir); +- }); +- } +-}; +-Signals.addSignalMethods(ExtensionFinder.prototype); +diff --git a/js/ui/extensionDownloader.js b/js/ui/extensionDownloader.js +index de52edfa6..1d92a5740 100644 +--- a/js/ui/extensionDownloader.js ++++ b/js/ui/extensionDownloader.js +@@ -43,7 +43,7 @@ function installExtension(uuid, invocation) { + } + + function uninstallExtension(uuid) { +- let extension = ExtensionUtils.extensions[uuid]; ++ let extension = Main.extensionManager.extensions[uuid]; + if (!extension) + return false; + +@@ -112,7 +112,7 @@ function updateExtension(uuid) { + + _httpSession.queue_message(message, (session, message) => { + gotExtensionZipFile(session, message, uuid, newExtensionTmpDir, () => { +- let oldExtension = ExtensionUtils.extensions[uuid]; ++ let oldExtension = Main.extensionManager.extensions[uuid]; + let extensionDir = oldExtension.dir; + + if (!Main.extensionManager.unloadExtension(oldExtension)) +@@ -124,7 +124,7 @@ function updateExtension(uuid) { + let extension = null; + + try { +- extension = ExtensionUtils.createExtensionObject(uuid, extensionDir, ExtensionUtils.ExtensionType.PER_USER); ++ extension = Main.extensionManager.createExtensionObject(uuid, extensionDir, ExtensionUtils.ExtensionType.PER_USER); + Main.extensionManager.loadExtension(extension); + } catch(e) { + if (extension) +@@ -150,8 +150,8 @@ function updateExtension(uuid) { + + function checkForUpdates() { + let metadatas = {}; +- for (let uuid in ExtensionUtils.extensions) { +- metadatas[uuid] = ExtensionUtils.extensions[uuid].metadata; ++ for (let uuid in Main.extensionManager.extensions) { ++ metadatas[uuid] = Main.extensionManager.extensions[uuid].metadata; + } + + let params = { shell_version: Config.PACKAGE_VERSION, +@@ -229,7 +229,7 @@ class InstallExtensionDialog extends ModalDialog.ModalDialog { + + function callback() { + try { +- let extension = ExtensionUtils.createExtensionObject(uuid, dir, ExtensionUtils.ExtensionType.PER_USER); ++ let extension = Main.extensionManager.createExtensionObject(uuid, dir, ExtensionUtils.ExtensionType.PER_USER); + Main.extensionManager.loadExtension(extension); + if (!Main.extensionManager.enableExtension(uuid)) + throw new Error(`Cannot add ${uuid} to enabled extensions gsettings key`); +diff --git a/js/ui/extensionSystem.js b/js/ui/extensionSystem.js +index a83e53c83..0fd49c5ca 100644 +--- a/js/ui/extensionSystem.js ++++ b/js/ui/extensionSystem.js +@@ -4,9 +4,10 @@ const { Gio, St } = imports.gi; + const Signals = imports.signals; + + const ExtensionUtils = imports.misc.extensionUtils; ++const FileUtils = imports.misc.fileUtils; + const Main = imports.ui.main; + +-const { ExtensionState } = ExtensionUtils; ++const { ExtensionState, ExtensionType } = ExtensionUtils; + + const ENABLED_EXTENSIONS_KEY = 'enabled-extensions'; + const DISABLE_USER_EXTENSIONS_KEY = 'disable-user-extensions'; +@@ -17,15 +18,23 @@ var ExtensionManager = class { + this._initted = false; + this._enabled = false; + ++ this._extensions = {}; + this._enabledExtensions = []; + this._extensionOrder = []; + + Main.sessionMode.connect('updated', this._sessionUpdated.bind(this)); ++ } ++ ++ init() { + this._sessionUpdated(); + } + ++ get extensions() { ++ return this._extensions; ++ } ++ + _callExtensionDisable(uuid) { +- let extension = ExtensionUtils.extensions[uuid]; ++ let extension = this._extensions[uuid]; + if (!extension) + return; + +@@ -47,7 +56,7 @@ var ExtensionManager = class { + for (let i = 0; i < orderReversed.length; i++) { + let uuid = orderReversed[i]; + try { +- ExtensionUtils.extensions[uuid].stateObj.disable(); ++ this._extensions[uuid].stateObj.disable(); + } catch (e) { + this.logExtensionError(uuid, e); + } +@@ -68,7 +77,7 @@ var ExtensionManager = class { + for (let i = 0; i < order.length; i++) { + let uuid = order[i]; + try { +- ExtensionUtils.extensions[uuid].stateObj.enable(); ++ this._extensions[uuid].stateObj.enable(); + } catch (e) { + this.logExtensionError(uuid, e); + } +@@ -83,7 +92,7 @@ var ExtensionManager = class { + } + + _callExtensionEnable(uuid) { +- let extension = ExtensionUtils.extensions[uuid]; ++ let extension = this._extensions[uuid]; + if (!extension) + return; + +@@ -127,7 +136,7 @@ var ExtensionManager = class { + } + + enableExtension(uuid) { +- if (!ExtensionUtils.extensions[uuid]) ++ if (!this._extensions[uuid]) + return false; + + let enabledExtensions = global.settings.get_strv(ENABLED_EXTENSIONS_KEY); +@@ -140,7 +149,7 @@ var ExtensionManager = class { + } + + disableExtension(uuid) { +- if (!ExtensionUtils.extensions[uuid]) ++ if (!this._extensions[uuid]) + return false; + + let enabledExtensions = global.settings.get_strv(ENABLED_EXTENSIONS_KEY); +@@ -153,7 +162,7 @@ var ExtensionManager = class { + } + + logExtensionError(uuid, error) { +- let extension = ExtensionUtils.extensions[uuid]; ++ let extension = this._extensions[uuid]; + if (!extension) + return; + +@@ -169,6 +178,54 @@ var ExtensionManager = class { + this.emit('extension-state-changed', extension); + } + ++ createExtensionObject(uuid, dir, type) { ++ let metadataFile = dir.get_child('metadata.json'); ++ if (!metadataFile.query_exists(null)) { ++ throw new Error('Missing metadata.json'); ++ } ++ ++ let metadataContents, success; ++ try { ++ [success, metadataContents] = metadataFile.load_contents(null); ++ if (metadataContents instanceof Uint8Array) ++ metadataContents = imports.byteArray.toString(metadataContents); ++ } catch (e) { ++ throw new Error(`Failed to load metadata.json: ${e}`); ++ } ++ let meta; ++ try { ++ meta = JSON.parse(metadataContents); ++ } catch (e) { ++ throw new Error(`Failed to parse metadata.json: ${e}`); ++ } ++ ++ let requiredProperties = ['uuid', 'name', 'description', 'shell-version']; ++ for (let i = 0; i < requiredProperties.length; i++) { ++ let prop = requiredProperties[i]; ++ if (!meta[prop]) { ++ throw new Error(`missing "${prop}" property in metadata.json`); ++ } ++ } ++ ++ if (uuid != meta.uuid) { ++ throw new Error(`uuid "${meta.uuid}" from metadata.json does not match directory name "${uuid}"`); ++ } ++ ++ let extension = { ++ metadata: meta, ++ uuid: meta.uuid, ++ type, ++ dir, ++ path: dir.get_path(), ++ error: '', ++ hasPrefs: dir.get_child('prefs.js').query_exists(null), ++ canChange: false ++ }; ++ this._extensions[uuid] = extension; ++ ++ return extension; ++ } ++ + loadExtension(extension) { + // Default to error, we set success as the last step + extension.state = ExtensionState.ERROR; +@@ -202,7 +259,7 @@ var ExtensionManager = class { + extension.state = ExtensionState.UNINSTALLED; + this.emit('extension-state-changed', extension); + +- delete ExtensionUtils.extensions[extension.uuid]; ++ delete this._extensions[extension.uuid]; + return true; + } + +@@ -217,7 +274,7 @@ var ExtensionManager = class { + // Now, recreate the extension and load it. + let newExtension; + try { +- newExtension = ExtensionUtils.createExtensionObject(uuid, dir, type); ++ newExtension = this.createExtensionObject(uuid, dir, type); + } catch (e) { + this.logExtensionError(uuid, e); + return; +@@ -227,7 +284,7 @@ var ExtensionManager = class { + } + + _callExtensionInit(uuid) { +- let extension = ExtensionUtils.extensions[uuid]; ++ let extension = this._extensions[uuid]; + let dir = extension.dir; + + if (!extension) +@@ -328,7 +385,7 @@ var ExtensionManager = class { + } + + _onSettingsWritableChanged() { +- for (let uuid in ExtensionUtils.extensions) { ++ for (let uuid in this._extensions) { + let extension = ExtensionUtils.extensions[uuid]; + this._updateCanChange(extension); + this.emit('extension-state-changed', extension); +@@ -340,8 +397,8 @@ var ExtensionManager = class { + // extensions when allowed by the sessionMode, so + // temporarily disable them all + this._enabledExtensions = []; +- for (let uuid in ExtensionUtils.extensions) +- this.reloadExtension(ExtensionUtils.extensions[uuid]); ++ for (let uuid in this._extensions) ++ this.reloadExtension(this._extensions[uuid]); + this._enabledExtensions = this._getEnabledExtensions(); + + if (Main.sessionMode.allowExtensions) { +@@ -363,11 +420,30 @@ var ExtensionManager = class { + + this._enabledExtensions = this._getEnabledExtensions(); + +- let finder = new ExtensionUtils.ExtensionFinder(); +- finder.connect('extension-found', (finder, extension) => { ++ let perUserDir = Gio.File.new_for_path(global.userdatadir); ++ FileUtils.collectFromDatadirs('extensions', true, (dir, info) => { ++ let fileType = info.get_file_type(); ++ if (fileType != Gio.FileType.DIRECTORY) ++ return; ++ let uuid = info.get_name(); ++ let existing = this._extensions[uuid]; ++ if (existing) { ++ log(`Extension ${uuid} already installed in ${existing.path}. ${dir.get_path()} will not be loaded`); ++ return; ++ } ++ ++ let extension; ++ let type = dir.has_prefix(perUserDir) ++ ? ExtensionType.PER_USER ++ : ExtensionType.SYSTEM; ++ try { ++ extension = this.createExtensionObject(uuid, dir, type); ++ } catch (e) { ++ logError(e, `Could not load extension ${uuid}`); ++ return; ++ } + this.loadExtension(extension); + }); +- finder.scanExtensions(); + } + + _enableAllExtensions() { +diff --git a/js/ui/lookingGlass.js b/js/ui/lookingGlass.js +index e947574f2..b8f8b14c9 100644 +--- a/js/ui/lookingGlass.js ++++ b/js/ui/lookingGlass.js +@@ -620,7 +620,7 @@ var Extensions = class Extensions { + this._extensionsList.add(this._noExtensions); + this.actor.add(this._extensionsList); + +- for (let uuid in ExtensionUtils.extensions) ++ for (let uuid in Main.extensionManager.extensions) + this._loadExtension(null, uuid); + + Main.extensionManager.connect('extension-loaded', +@@ -628,7 +628,7 @@ var Extensions = class Extensions { + } + + _loadExtension(o, uuid) { +- let extension = ExtensionUtils.extensions[uuid]; ++ let extension = Main.extensionManager.extensions[uuid]; + // There can be cases where we create dummy extension metadata + // that's not really a proper extension. Don't bother with these. + if (!extension.metadata.name) +diff --git a/js/ui/main.js b/js/ui/main.js +index 7bfbce497..5fa5a8077 100644 +--- a/js/ui/main.js ++++ b/js/ui/main.js +@@ -220,6 +220,7 @@ function _initializeUI() { + + ExtensionDownloader.init(); + extensionManager = new ExtensionSystem.ExtensionManager(); ++ extensionManager.init(); + + if (sessionMode.isGreeter && screenShield) { + layoutManager.connect('startup-prepared', () => { +diff --git a/js/ui/shellDBus.js b/js/ui/shellDBus.js +index 23274c0a3..dc3a61df6 100644 +--- a/js/ui/shellDBus.js ++++ b/js/ui/shellDBus.js +@@ -254,7 +254,7 @@ var GnomeShellExtensions = class { + + ListExtensions() { + let out = {}; +- for (let uuid in ExtensionUtils.extensions) { ++ for (let uuid in Main.extensionManager.extensions) { + let dbusObj = this.GetExtensionInfo(uuid); + out[uuid] = dbusObj; + } +@@ -262,12 +262,12 @@ var GnomeShellExtensions = class { + } + + GetExtensionInfo(uuid) { +- let extension = ExtensionUtils.extensions[uuid] || {}; ++ let extension = Main.extensionManager.extensions[uuid] || {}; + return ExtensionUtils.serializeExtension(extension); + } + + GetExtensionErrors(uuid) { +- let extension = ExtensionUtils.extensions[uuid]; ++ let extension = Main.extensionManager.extensions[uuid]; + if (!extension) + return []; + +@@ -303,7 +303,7 @@ var GnomeShellExtensions = class { + } + + ReloadExtension(uuid) { +- let extension = ExtensionUtils.extensions[uuid]; ++ let extension = Main.extensionManager.extensions[uuid]; + if (!extension) + return; + +-- +2.29.2 + + +From 776b82542aa705ea527dfbdd1a6d3fb1588092e2 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Florian=20M=C3=BCllner?= +Date: Mon, 8 Jul 2019 00:01:11 +0200 +Subject: [PATCH 13/26] extensionSystem: Store extensions in a Map + +After making the extensions map private to the ExtensionManager, we can +switch it to a proper hash table which is more appropriate. + +https://bugzilla.gnome.org/show_bug.cgi?id=789852 +--- + js/misc/extensionUtils.js | 2 +- + js/ui/extensionDownloader.js | 8 +++---- + js/ui/extensionSystem.js | 42 ++++++++++++++++++++---------------- + js/ui/lookingGlass.js | 5 +++-- + js/ui/shellDBus.js | 10 ++++----- + 5 files changed, 37 insertions(+), 30 deletions(-) + +diff --git a/js/misc/extensionUtils.js b/js/misc/extensionUtils.js +index c513ebc06..62b25d46c 100644 +--- a/js/misc/extensionUtils.js ++++ b/js/misc/extensionUtils.js +@@ -71,7 +71,7 @@ function getCurrentExtension() { + // Walk up the directory tree, looking for an extension with + // the same UUID as a directory name. + while (file != null) { +- let extension = extensionManager.extensions[file.get_basename()]; ++ let extension = extensionManager.lookup(file.get_basename()); + if (extension !== undefined) + return extension; + file = file.get_parent(); +diff --git a/js/ui/extensionDownloader.js b/js/ui/extensionDownloader.js +index 1d92a5740..77d013ffb 100644 +--- a/js/ui/extensionDownloader.js ++++ b/js/ui/extensionDownloader.js +@@ -43,7 +43,7 @@ function installExtension(uuid, invocation) { + } + + function uninstallExtension(uuid) { +- let extension = Main.extensionManager.extensions[uuid]; ++ let extension = Main.extensionManager.lookup(uuid); + if (!extension) + return false; + +@@ -112,7 +112,7 @@ function updateExtension(uuid) { + + _httpSession.queue_message(message, (session, message) => { + gotExtensionZipFile(session, message, uuid, newExtensionTmpDir, () => { +- let oldExtension = Main.extensionManager.extensions[uuid]; ++ let oldExtension = Main.extensionManager.lookup(uuid); + let extensionDir = oldExtension.dir; + + if (!Main.extensionManager.unloadExtension(oldExtension)) +@@ -150,9 +150,9 @@ function updateExtension(uuid) { + + function checkForUpdates() { + let metadatas = {}; +- for (let uuid in Main.extensionManager.extensions) { ++ Main.extensionManager.getUuids().forEach(uuid => { + metadatas[uuid] = Main.extensionManager.extensions[uuid].metadata; +- } ++ }); + + let params = { shell_version: Config.PACKAGE_VERSION, + installed: JSON.stringify(metadatas) }; +diff --git a/js/ui/extensionSystem.js b/js/ui/extensionSystem.js +index 0fd49c5ca..cd3e78301 100644 +--- a/js/ui/extensionSystem.js ++++ b/js/ui/extensionSystem.js +@@ -18,7 +18,7 @@ var ExtensionManager = class { + this._initted = false; + this._enabled = false; + +- this._extensions = {}; ++ this._extensions = new Map(); + this._enabledExtensions = []; + this._extensionOrder = []; + +@@ -29,12 +29,16 @@ var ExtensionManager = class { + this._sessionUpdated(); + } + +- get extensions() { +- return this._extensions; ++ lookup(uuid) { ++ return this._extensions.get(uuid); ++ } ++ ++ getUuids() { ++ return [...this._extensions.keys()]; + } + + _callExtensionDisable(uuid) { +- let extension = this._extensions[uuid]; ++ let extension = this.lookup(uuid); + if (!extension) + return; + +@@ -56,7 +60,7 @@ var ExtensionManager = class { + for (let i = 0; i < orderReversed.length; i++) { + let uuid = orderReversed[i]; + try { +- this._extensions[uuid].stateObj.disable(); ++ this.lookup(uuid).stateObj.disable(); + } catch (e) { + this.logExtensionError(uuid, e); + } +@@ -77,7 +81,7 @@ var ExtensionManager = class { + for (let i = 0; i < order.length; i++) { + let uuid = order[i]; + try { +- this._extensions[uuid].stateObj.enable(); ++ this.lookup(uuid).stateObj.enable(); + } catch (e) { + this.logExtensionError(uuid, e); + } +@@ -92,7 +96,7 @@ var ExtensionManager = class { + } + + _callExtensionEnable(uuid) { +- let extension = this._extensions[uuid]; ++ let extension = this.lookup(uuid); + if (!extension) + return; + +@@ -136,7 +140,7 @@ var ExtensionManager = class { + } + + enableExtension(uuid) { +- if (!this._extensions[uuid]) ++ if (!this._extensions.has(uuid)) + return false; + + let enabledExtensions = global.settings.get_strv(ENABLED_EXTENSIONS_KEY); +@@ -149,7 +153,7 @@ var ExtensionManager = class { + } + + disableExtension(uuid) { +- if (!this._extensions[uuid]) ++ if (!this._extensions.has(uuid)) + return false; + + let enabledExtensions = global.settings.get_strv(ENABLED_EXTENSIONS_KEY); +@@ -162,7 +166,7 @@ var ExtensionManager = class { + } + + logExtensionError(uuid, error) { +- let extension = this._extensions[uuid]; ++ let extension = this.lookup(uuid); + if (!extension) + return; + +@@ -221,7 +225,7 @@ var ExtensionManager = class { + hasPrefs: dir.get_child('prefs.js').query_exists(null), + canChange: false + }; +- this._extensions[uuid] = extension; ++ this._extensions.set(uuid, extension); + + return extension; + } +@@ -259,7 +263,7 @@ var ExtensionManager = class { + extension.state = ExtensionState.UNINSTALLED; + this.emit('extension-state-changed', extension); + +- delete this._extensions[extension.uuid]; ++ this._extensions.delete(extension.uuid); + return true; + } + +@@ -284,7 +288,7 @@ var ExtensionManager = class { + } + + _callExtensionInit(uuid) { +- let extension = this._extensions[uuid]; ++ let extension = this.lookup(uuid); + let dir = extension.dir; + + if (!extension) +@@ -385,8 +389,7 @@ var ExtensionManager = class { + } + + _onSettingsWritableChanged() { +- for (let uuid in this._extensions) { +- let extension = ExtensionUtils.extensions[uuid]; ++ for (let extension of this._extensions.values()) { + this._updateCanChange(extension); + this.emit('extension-state-changed', extension); + } +@@ -397,8 +400,11 @@ var ExtensionManager = class { + // extensions when allowed by the sessionMode, so + // temporarily disable them all + this._enabledExtensions = []; +- for (let uuid in this._extensions) +- this.reloadExtension(this._extensions[uuid]); ++ ++ // The loop modifies the extensions map, so iterate over a copy ++ let extensions = [...this._extensions.values()]; ++ for (let extension of extensions) ++ this.reloadExtension(extension); + this._enabledExtensions = this._getEnabledExtensions(); + + if (Main.sessionMode.allowExtensions) { +@@ -426,7 +432,7 @@ var ExtensionManager = class { + if (fileType != Gio.FileType.DIRECTORY) + return; + let uuid = info.get_name(); +- let existing = this._extensions[uuid]; ++ let existing = this.lookup(uuid); + if (existing) { + log(`Extension ${uuid} already installed in ${existing.path}. ${dir.get_path()} will not be loaded`); + return; +diff --git a/js/ui/lookingGlass.js b/js/ui/lookingGlass.js +index b8f8b14c9..9196959bd 100644 +--- a/js/ui/lookingGlass.js ++++ b/js/ui/lookingGlass.js +@@ -620,15 +620,16 @@ var Extensions = class Extensions { + this._extensionsList.add(this._noExtensions); + this.actor.add(this._extensionsList); + +- for (let uuid in Main.extensionManager.extensions) ++ Main.extensionManager.getUuids().forEach(uuid => { + this._loadExtension(null, uuid); ++ }); + + Main.extensionManager.connect('extension-loaded', + this._loadExtension.bind(this)); + } + + _loadExtension(o, uuid) { +- let extension = Main.extensionManager.extensions[uuid]; ++ let extension = Main.extensionManager.lookup(uuid); + // There can be cases where we create dummy extension metadata + // that's not really a proper extension. Don't bother with these. + if (!extension.metadata.name) +diff --git a/js/ui/shellDBus.js b/js/ui/shellDBus.js +index dc3a61df6..be9b10491 100644 +--- a/js/ui/shellDBus.js ++++ b/js/ui/shellDBus.js +@@ -254,20 +254,20 @@ var GnomeShellExtensions = class { + + ListExtensions() { + let out = {}; +- for (let uuid in Main.extensionManager.extensions) { ++ Main.extensionManager.getUuids().forEach(uuid => { + let dbusObj = this.GetExtensionInfo(uuid); + out[uuid] = dbusObj; +- } ++ }); + return out; + } + + GetExtensionInfo(uuid) { +- let extension = Main.extensionManager.extensions[uuid] || {}; ++ let extension = Main.extensionManager.lookup(uuid) || {}; + return ExtensionUtils.serializeExtension(extension); + } + + GetExtensionErrors(uuid) { +- let extension = Main.extensionManager.extensions[uuid]; ++ let extension = Main.extensionManager.lookup(uuid); + if (!extension) + return []; + +@@ -303,7 +303,7 @@ var GnomeShellExtensions = class { + } + + ReloadExtension(uuid) { +- let extension = Main.extensionManager.extensions[uuid]; ++ let extension = Main.extensionManager.lookup(uuid); + if (!extension) + return; + +-- +2.29.2 + + +From 2cc95ba7c93c04bae0006b7d018928600d9cbb13 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Florian=20M=C3=BCllner?= +Date: Wed, 22 Jan 2020 14:45:15 +0100 +Subject: [PATCH 14/26] extensionSystem: Add hasUpdate state + +The current support for extension updates is half-baked at best. +We are about to change that, and implement offline updates similar +to gnome-software. + +As a first step, add a hasUpdate property to the extension state +which will communicate available updates. + +https://gitlab.gnome.org/GNOME/gnome-shell/merge_requests/945 +--- + js/misc/extensionUtils.js | 10 +++++++++- + js/ui/extensionSystem.js | 10 ++++++++++ + 2 files changed, 19 insertions(+), 1 deletion(-) + +diff --git a/js/misc/extensionUtils.js b/js/misc/extensionUtils.js +index 62b25d46c..a812acdb1 100644 +--- a/js/misc/extensionUtils.js ++++ b/js/misc/extensionUtils.js +@@ -29,7 +29,15 @@ var ExtensionState = { + UNINSTALLED: 99 + }; + +-const SERIALIZED_PROPERTIES = ['type', 'state', 'path', 'error', 'hasPrefs', 'canChange']; ++const SERIALIZED_PROPERTIES = [ ++ 'type', ++ 'state', ++ 'path', ++ 'error', ++ 'hasPrefs', ++ 'hasUpdate', ++ 'canChange', ++]; + + /** + * getCurrentExtension: +diff --git a/js/ui/extensionSystem.js b/js/ui/extensionSystem.js +index cd3e78301..93faf48d4 100644 +--- a/js/ui/extensionSystem.js ++++ b/js/ui/extensionSystem.js +@@ -165,6 +165,15 @@ var ExtensionManager = class { + return true; + } + ++ notifyExtensionUpdate(uuid) { ++ let extension = this.lookup(uuid); ++ if (!extension) ++ return; ++ ++ extension.hasUpdate = true; ++ this.emit('extension-state-changed', extension); ++ } ++ + logExtensionError(uuid, error) { + let extension = this.lookup(uuid); + if (!extension) +@@ -223,6 +232,7 @@ var ExtensionManager = class { + path: dir.get_path(), + error: '', + hasPrefs: dir.get_child('prefs.js').query_exists(null), ++ hasUpdate: false, + canChange: false + }; + this._extensions.set(uuid, extension); +-- +2.29.2 + + +From 07330eaac64fc115851ec9d5a0969bd046599e12 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Florian=20M=C3=BCllner?= +Date: Wed, 22 Jan 2020 15:07:45 +0100 +Subject: [PATCH 15/26] extensionDownloader: Make checkForUpdates() check for + updates + +Currently the method installs updates instead of merely checking for +them (or it would do, if it actually worked). + +This is not just surprising considering the method name, the whole idea +of live updates is problematic and will not work properly more often +than not: + - imports are cached, so any local modules will stay at their + original version until a shell restart + - GTypes cannot be unregistered + +So change the method to only download available updates, and set the +extensions' hasUpdate state accordingly. + +https://gitlab.gnome.org/GNOME/gnome-shell/merge_requests/945 +--- + js/ui/extensionDownloader.js | 51 +++++++----------------------------- + 1 file changed, 9 insertions(+), 42 deletions(-) + +diff --git a/js/ui/extensionDownloader.js b/js/ui/extensionDownloader.js +index 77d013ffb..66cb13d56 100644 +--- a/js/ui/extensionDownloader.js ++++ b/js/ui/extensionDownloader.js +@@ -97,53 +97,20 @@ function gotExtensionZipFile(session, message, uuid, dir, callback, errback) { + }); + } + +-function updateExtension(uuid) { +- // This gets a bit tricky. We want the update to be seamless - +- // if we have any error during downloading or extracting, we +- // want to not unload the current version. +- +- let oldExtensionTmpDir = GLib.Dir.make_tmp('XXXXXX-shell-extension'); +- let newExtensionTmpDir = GLib.Dir.make_tmp('XXXXXX-shell-extension'); ++function downloadExtensionUpdate(uuid) { ++ let dir = Gio.File.new_for_path( ++ GLib.build_filenamev([global.userdatadir, 'extension-updates', uuid])); + + let params = { shell_version: Config.PACKAGE_VERSION }; + + let url = REPOSITORY_URL_DOWNLOAD.format(uuid); + let message = Soup.form_request_new_from_hash('GET', url, params); + +- _httpSession.queue_message(message, (session, message) => { +- gotExtensionZipFile(session, message, uuid, newExtensionTmpDir, () => { +- let oldExtension = Main.extensionManager.lookup(uuid); +- let extensionDir = oldExtension.dir; +- +- if (!Main.extensionManager.unloadExtension(oldExtension)) +- return; +- +- FileUtils.recursivelyMoveDir(extensionDir, oldExtensionTmpDir); +- FileUtils.recursivelyMoveDir(newExtensionTmpDir, extensionDir); +- +- let extension = null; +- +- try { +- extension = Main.extensionManager.createExtensionObject(uuid, extensionDir, ExtensionUtils.ExtensionType.PER_USER); +- Main.extensionManager.loadExtension(extension); +- } catch(e) { +- if (extension) +- Main.extensionManager.unloadExtension(extension); +- +- logError(e, 'Error loading extension %s'.format(uuid)); +- +- FileUtils.recursivelyDeleteDir(extensionDir, false); +- FileUtils.recursivelyMoveDir(oldExtensionTmpDir, extensionDir); +- +- // Restore what was there before. We can't do much if we +- // fail here. +- Main.extensionManager.loadExtension(oldExtension); +- return; +- } +- +- FileUtils.recursivelyDeleteDir(oldExtensionTmpDir, true); +- }, (code, message) => { +- log('Error while updating extension %s: %s (%s)'.format(uuid, code, message ? message : '')); ++ _httpSession.queue_message(message, session => { ++ gotExtensionZipFile(session, message, uuid, dir, () => { ++ Main.extensionManager.notifyExtensionUpdate(uuid); ++ }, (code, msg) => { ++ log(`Error while downloading update for extension ${uuid}: ${code} (${msg})`); + }); + }); + } +@@ -169,7 +136,7 @@ function checkForUpdates() { + if (operation == 'blacklist') + uninstallExtension(uuid); + else if (operation == 'upgrade' || operation == 'downgrade') +- updateExtension(uuid); ++ downloadExtensionUpdate(uuid); + } + }); + } +-- +2.29.2 + + +From 49eaf28202787f0802663aa609ee9f87eb548b03 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Florian=20M=C3=BCllner?= +Date: Wed, 22 Jan 2020 15:42:06 +0100 +Subject: [PATCH 16/26] extensionDownloader: Only check updates for user + extensions + +System extensions cannot be updated through the website, so don't +even try. + +https://gitlab.gnome.org/GNOME/gnome-shell/merge_requests/945 +--- + js/ui/extensionDownloader.js | 5 ++++- + 1 file changed, 4 insertions(+), 1 deletion(-) + +diff --git a/js/ui/extensionDownloader.js b/js/ui/extensionDownloader.js +index 66cb13d56..c8f6735c5 100644 +--- a/js/ui/extensionDownloader.js ++++ b/js/ui/extensionDownloader.js +@@ -118,7 +118,10 @@ function downloadExtensionUpdate(uuid) { + function checkForUpdates() { + let metadatas = {}; + Main.extensionManager.getUuids().forEach(uuid => { +- metadatas[uuid] = Main.extensionManager.extensions[uuid].metadata; ++ let extension = Main.extensionManager.lookup(uuid); ++ if (extension.type !== ExtensionUtils.ExtensionType.PER_USER) ++ return; ++ metadatas[uuid] = extension.metadata; + }); + + let params = { shell_version: Config.PACKAGE_VERSION, +-- +2.29.2 + + +From 67d709de14b083a013b3b1160e5cc451cf96bfde Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Florian=20M=C3=BCllner?= +Date: Mon, 27 Jan 2020 01:13:49 +0100 +Subject: [PATCH 17/26] extensionDownloader: Exclude extensions with pending + updates from check + +While it is possible that an extension has a newer version available +than the previously downloaded update, it's more likely that we end up +downloading the same archive again. That would be a bit silly despite +the usually small size, so we can either use the metadata from the +update, or exclude the extension from the check. + +The latter is much easier, so let's go with that for now. + +https://gitlab.gnome.org/GNOME/gnome-shell/merge_requests/945 +--- + js/ui/extensionDownloader.js | 2 ++ + 1 file changed, 2 insertions(+) + +diff --git a/js/ui/extensionDownloader.js b/js/ui/extensionDownloader.js +index c8f6735c5..ede276c37 100644 +--- a/js/ui/extensionDownloader.js ++++ b/js/ui/extensionDownloader.js +@@ -121,6 +121,8 @@ function checkForUpdates() { + let extension = Main.extensionManager.lookup(uuid); + if (extension.type !== ExtensionUtils.ExtensionType.PER_USER) + return; ++ if (extension.hasUpdate) ++ return; + metadatas[uuid] = extension.metadata; + }); + +-- +2.29.2 + + +From 2fa19162c71787fbb9aa9af1d35e0e9cab11c1d1 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Florian=20M=C3=BCllner?= +Date: Wed, 22 Jan 2020 16:53:32 +0100 +Subject: [PATCH 18/26] extensionDownloader: Include version validation in + update check + +The extensions website will consider the setting to find the best suitable +extension version, so we should transmit the parameter for better results. + +https://gitlab.gnome.org/GNOME/gnome-shell/merge_requests/945 +--- + js/ui/extensionDownloader.js | 9 +++++++-- + 1 file changed, 7 insertions(+), 2 deletions(-) + +diff --git a/js/ui/extensionDownloader.js b/js/ui/extensionDownloader.js +index ede276c37..f957c6c62 100644 +--- a/js/ui/extensionDownloader.js ++++ b/js/ui/extensionDownloader.js +@@ -126,8 +126,13 @@ function checkForUpdates() { + metadatas[uuid] = extension.metadata; + }); + +- let params = { shell_version: Config.PACKAGE_VERSION, +- installed: JSON.stringify(metadatas) }; ++ let versionCheck = global.settings.get_boolean( ++ 'disable-extension-version-validation'); ++ let params = { ++ shell_version: Config.PACKAGE_VERSION, ++ installed: JSON.stringify(metadatas), ++ disable_version_validation: `${versionCheck}`, ++ }; + + let url = REPOSITORY_URL_UPDATE; + let message = Soup.form_request_new_from_hash('GET', url, params); +-- +2.29.2 + + +From ccb0095b1981233ca980d44c260c0d36eef910bd Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Florian=20M=C3=BCllner?= +Date: Wed, 22 Jan 2020 15:09:05 +0100 +Subject: [PATCH 19/26] extensionSystem: Install pending updates on startup + +Now that we have a way to check for updates and download them, we +should actually apply them as well. Do this on startup before any +extensions are initialized, to make sure we don't run into any +conflicts with a previously loaded version. + +https://gitlab.gnome.org/GNOME/gnome-shell/merge_requests/945 +--- + js/ui/extensionSystem.js | 18 +++++++++++++++++- + 1 file changed, 17 insertions(+), 1 deletion(-) + +diff --git a/js/ui/extensionSystem.js b/js/ui/extensionSystem.js +index 93faf48d4..36a248dc1 100644 +--- a/js/ui/extensionSystem.js ++++ b/js/ui/extensionSystem.js +@@ -1,6 +1,6 @@ + // -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*- + +-const { Gio, St } = imports.gi; ++const { GLib, Gio, St } = imports.gi; + const Signals = imports.signals; + + const ExtensionUtils = imports.misc.extensionUtils; +@@ -26,6 +26,7 @@ var ExtensionManager = class { + } + + init() { ++ this._installExtensionUpdates(); + this._sessionUpdated(); + } + +@@ -424,6 +425,21 @@ var ExtensionManager = class { + } + } + ++ _installExtensionUpdates() { ++ FileUtils.collectFromDatadirs('extension-updates', true, (dir, info) => { ++ let fileType = info.get_file_type(); ++ if (fileType !== Gio.FileType.DIRECTORY) ++ return; ++ let uuid = info.get_name(); ++ let extensionDir = Gio.File.new_for_path( ++ GLib.build_filenamev([global.userdatadir, 'extensions', uuid])); ++ ++ FileUtils.recursivelyDeleteDir(extensionDir, false); ++ FileUtils.recursivelyMoveDir(dir, extensionDir); ++ FileUtils.recursivelyDeleteDir(dir, true); ++ }); ++ } ++ + _loadExtensions() { + global.settings.connect(`changed::${ENABLED_EXTENSIONS_KEY}`, + this._onEnabledExtensionsChanged.bind(this)); +-- +2.29.2 + + +From e377b16ffb667be40a850ff03e092f2f9dfe8fe8 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Florian=20M=C3=BCllner?= +Date: Fri, 24 Jan 2020 18:09:34 +0100 +Subject: [PATCH 20/26] extensionPrefs: Add application icon + +We are about to make the tool a user-visible application, so we +need an icon. Add one (plus its symbolic variant). + +https://gitlab.gnome.org/GNOME/gnome-shell/issues/1968 +--- + data/gnome-shell-extension-prefs.desktop.in.in | 1 + + data/icons/hicolor/scalable/apps/org.gnome.Extensions.svg | 7 +++++++ + .../symbolic/apps/org.gnome.Extensions-symbolic.svg | 1 + + data/icons/meson.build | 1 + + data/meson.build | 1 + + meson.build | 1 + + 6 files changed, 12 insertions(+) + create mode 100644 data/icons/hicolor/scalable/apps/org.gnome.Extensions.svg + create mode 100644 data/icons/hicolor/symbolic/apps/org.gnome.Extensions-symbolic.svg + create mode 100644 data/icons/meson.build + +diff --git a/data/gnome-shell-extension-prefs.desktop.in.in b/data/gnome-shell-extension-prefs.desktop.in.in +index 1b144c5bd..1b58c424e 100644 +--- a/data/gnome-shell-extension-prefs.desktop.in.in ++++ b/data/gnome-shell-extension-prefs.desktop.in.in +@@ -1,6 +1,7 @@ + [Desktop Entry] + Type=Application + Name=Shell Extensions ++Icon=org.gnome.Extensions + Comment=Configure GNOME Shell Extensions + Exec=@bindir@/gnome-shell-extension-prefs %u + X-GNOME-Bugzilla-Bugzilla=GNOME +diff --git a/data/icons/hicolor/scalable/apps/org.gnome.Extensions.svg b/data/icons/hicolor/scalable/apps/org.gnome.Extensions.svg +new file mode 100644 +index 000000000..49d63888b +--- /dev/null ++++ b/data/icons/hicolor/scalable/apps/org.gnome.Extensions.svg +@@ -0,0 +1,7 @@ ++ ++ ++ ++ ++ ++ ++ +diff --git a/data/icons/hicolor/symbolic/apps/org.gnome.Extensions-symbolic.svg b/data/icons/hicolor/symbolic/apps/org.gnome.Extensions-symbolic.svg +new file mode 100644 +index 000000000..43786ff4a +--- /dev/null ++++ b/data/icons/hicolor/symbolic/apps/org.gnome.Extensions-symbolic.svg +@@ -0,0 +1 @@ ++ +\ No newline at end of file +diff --git a/data/icons/meson.build b/data/icons/meson.build +new file mode 100644 +index 000000000..eff6e4b53 +--- /dev/null ++++ b/data/icons/meson.build +@@ -0,0 +1 @@ ++install_subdir('hicolor', install_dir: icondir) +diff --git a/data/meson.build b/data/meson.build +index 31ac4514e..33edb58c4 100644 +--- a/data/meson.build ++++ b/data/meson.build +@@ -42,6 +42,7 @@ endforeach + + + subdir('dbus-interfaces') ++subdir('icons') + subdir('theme') + + data_resources = [ +diff --git a/meson.build b/meson.build +index 0acaba705..2dd1bbc7a 100644 +--- a/meson.build ++++ b/meson.build +@@ -52,6 +52,7 @@ pkglibdir = join_paths(libdir, meson.project_name()) + autostartdir = join_paths(sysconfdir, 'xdg', 'autostart') + convertdir = join_paths(datadir, 'GConf', 'gsettings') + desktopdir = join_paths(datadir, 'applications') ++icondir = join_paths(datadir, 'icons') + ifacedir = join_paths(datadir, 'dbus-1', 'interfaces') + localedir = join_paths(datadir, 'locale') + portaldir = join_paths(datadir, 'xdg-desktop-portal', 'portals') +-- +2.29.2 + + +From 6b3fa1549f9682f54f55cdd963a242cd279ff17c Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Florian=20M=C3=BCllner?= +Date: Sun, 26 Jan 2020 23:47:24 +0100 +Subject: [PATCH 21/26] extensionSystem: Show notification when updates are + available + +Now that the extensions app has the ability to handle updates, we +can use it as source of updates notifications. + +https://gitlab.gnome.org/GNOME/gnome-shell/issues/1968 +--- + js/ui/extensionSystem.js | 39 ++++++++++++++++++++++++++++++++++++++- + 1 file changed, 38 insertions(+), 1 deletion(-) + +diff --git a/js/ui/extensionSystem.js b/js/ui/extensionSystem.js +index 36a248dc1..805e08cae 100644 +--- a/js/ui/extensionSystem.js ++++ b/js/ui/extensionSystem.js +@@ -1,11 +1,12 @@ + // -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*- + +-const { GLib, Gio, St } = imports.gi; ++const { GLib, Gio, GObject, St } = imports.gi; + const Signals = imports.signals; + + const ExtensionUtils = imports.misc.extensionUtils; + const FileUtils = imports.misc.fileUtils; + const Main = imports.ui.main; ++const MessageTray = imports.ui.messageTray; + + const { ExtensionState, ExtensionType } = ExtensionUtils; + +@@ -17,6 +18,7 @@ var ExtensionManager = class { + constructor() { + this._initted = false; + this._enabled = false; ++ this._updateNotified = false; + + this._extensions = new Map(); + this._enabledExtensions = []; +@@ -173,6 +175,18 @@ var ExtensionManager = class { + + extension.hasUpdate = true; + this.emit('extension-state-changed', extension); ++ ++ if (!this._updateNotified) { ++ this._updateNotified = true; ++ ++ let source = new ExtensionUpdateSource(); ++ Main.messageTray.add(source); ++ ++ let notification = new MessageTray.Notification(source, ++ _('Extension Updates Available'), ++ _('Extension updates are ready to be installed.')); ++ source.notify(notification); ++ } + } + + logExtensionError(uuid, error) { +@@ -521,3 +535,26 @@ var ExtensionManager = class { + } + }; + Signals.addSignalMethods(ExtensionManager.prototype); ++ ++class ExtensionUpdateSource extends MessageTray.Source { ++ constructor() { ++ const appSys = Shell.AppSystem.get_default(); ++ this._app = appSys.lookup_app('gnome-shell-extension-prefs.desktop'); ++ ++ super(this._app.get_name()); ++ } ++ ++ getIcon() { ++ return this._app.app_info.get_icon(); ++ } ++ ++ _createPolicy() { ++ return new MessageTray.NotificationApplicationPolicy(this._app.id); ++ } ++ ++ open() { ++ this._app.activate(); ++ Main.overview.hide(); ++ Main.panel.closeCalendar(); ++ } ++} +-- +2.29.2 + + +From f6a5e2731f487d7a0ac088aff53ca1e76006c118 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Florian=20M=C3=BCllner?= +Date: Mon, 27 Jan 2020 00:59:19 +0100 +Subject: [PATCH 22/26] extensionSystem: Periodically check for extension + updates + +Now that we can download, apply and display extension updates, it is time +to actually check for updates. Schedule an update check right on startup, +then every 24 hours. + +https://gitlab.gnome.org/GNOME/gnome-shell/issues/1968 +--- + js/ui/extensionSystem.js | 9 +++++++++ + 1 file changed, 9 insertions(+) + +diff --git a/js/ui/extensionSystem.js b/js/ui/extensionSystem.js +index 805e08cae..914abb309 100644 +--- a/js/ui/extensionSystem.js ++++ b/js/ui/extensionSystem.js +@@ -3,6 +3,7 @@ + const { GLib, Gio, GObject, St } = imports.gi; + const Signals = imports.signals; + ++const ExtensionDownloader = imports.ui.extensionDownloader; + const ExtensionUtils = imports.misc.extensionUtils; + const FileUtils = imports.misc.fileUtils; + const Main = imports.ui.main; +@@ -14,6 +15,8 @@ const ENABLED_EXTENSIONS_KEY = 'enabled-extensions'; + const DISABLE_USER_EXTENSIONS_KEY = 'disable-user-extensions'; + const EXTENSION_DISABLE_VERSION_CHECK_KEY = 'disable-extension-version-validation'; + ++const UPDATE_CHECK_TIMEOUT = 24 * 60 * 60; // 1 day in seconds ++ + var ExtensionManager = class { + constructor() { + this._initted = false; +@@ -30,6 +33,12 @@ var ExtensionManager = class { + init() { + this._installExtensionUpdates(); + this._sessionUpdated(); ++ ++ GLib.timeout_add_seconds(GLib.PRIORITY_DEFAULT, UPDATE_CHECK_TIMEOUT, () => { ++ ExtensionDownloader.checkForUpdates(); ++ return GLib.SOURCE_CONTINUE; ++ }); ++ ExtensionDownloader.checkForUpdates(); + } + + lookup(uuid) { +-- +2.29.2 + + +From cb3ed33a72fea3ae6b8df031abca48b99dba75a5 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Florian=20M=C3=BCllner?= +Date: Mon, 9 Mar 2020 16:49:34 +0100 +Subject: [PATCH 23/26] extensionDownloader: Remove pending updates with + extension + +When an extension is uninstalled, there is no point in keeping +a pending update: If the update didn't fail (which it currently +does), we would end up sneakily reinstalling the extension. + +https://gitlab.gnome.org/GNOME/gnome-shell/issues/2343 +--- + js/ui/extensionDownloader.js | 9 +++++++++ + 1 file changed, 9 insertions(+) + +diff --git a/js/ui/extensionDownloader.js b/js/ui/extensionDownloader.js +index f957c6c62..0bd77e125 100644 +--- a/js/ui/extensionDownloader.js ++++ b/js/ui/extensionDownloader.js +@@ -55,6 +55,15 @@ function uninstallExtension(uuid) { + return false; + + FileUtils.recursivelyDeleteDir(extension.dir, true); ++ ++ try { ++ const updatesDir = Gio.File.new_for_path(GLib.build_filenamev( ++ [global.userdatadir, 'extension-updates', extension.uuid])); ++ FileUtils.recursivelyDeleteDir(updatesDir, true); ++ } catch (e) { ++ // not an error ++ } ++ + return true; + } + +-- +2.29.2 + + +From 6abb5a189a7c97de8c0ed28c40f34fb625363223 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Florian=20M=C3=BCllner?= +Date: Mon, 9 Mar 2020 16:45:22 +0100 +Subject: [PATCH 24/26] extensionSystem: Catch errors when updating extensions + +Extension updates are installed at startup, so any errors that bubble +up uncaught will prevent the startup to complete. + +While the most likely error reason was addressed in the previous commit +(pending update for a no-longer exitent extension), it makes sense to +catch any kind of corrupt updates to not interfere with shell startup. + +https://gitlab.gnome.org/GNOME/gnome-shell/issues/2343 +--- + js/ui/extensionSystem.js | 11 ++++++++--- + 1 file changed, 8 insertions(+), 3 deletions(-) + +diff --git a/js/ui/extensionSystem.js b/js/ui/extensionSystem.js +index 914abb309..320af54e4 100644 +--- a/js/ui/extensionSystem.js ++++ b/js/ui/extensionSystem.js +@@ -457,9 +457,14 @@ var ExtensionManager = class { + let extensionDir = Gio.File.new_for_path( + GLib.build_filenamev([global.userdatadir, 'extensions', uuid])); + +- FileUtils.recursivelyDeleteDir(extensionDir, false); +- FileUtils.recursivelyMoveDir(dir, extensionDir); +- FileUtils.recursivelyDeleteDir(dir, true); ++ try { ++ FileUtils.recursivelyDeleteDir(extensionDir, false); ++ FileUtils.recursivelyMoveDir(dir, extensionDir); ++ } catch (e) { ++ log('Failed to install extension updates for %s'.format(uuid)); ++ } finally { ++ FileUtils.recursivelyDeleteDir(dir, true); ++ } + }); + } + +-- +2.29.2 + + +From 405897a9930362dad590eb8bd425c130dc636083 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Florian=20M=C3=BCllner?= +Date: Tue, 26 Jan 2021 17:12:04 +0100 +Subject: [PATCH 25/26] extensionSystem: Fix opening Extensions app from + notification + +Launching the app is implemented by the source's open() method, but +only external notifications are hooked up to call into the source +when no default action was provided. +--- + js/ui/extensionSystem.js | 2 ++ + 1 file changed, 2 insertions(+) + +diff --git a/js/ui/extensionSystem.js b/js/ui/extensionSystem.js +index 320af54e4..81804ea5e 100644 +--- a/js/ui/extensionSystem.js ++++ b/js/ui/extensionSystem.js +@@ -194,6 +194,8 @@ var ExtensionManager = class { + let notification = new MessageTray.Notification(source, + _('Extension Updates Available'), + _('Extension updates are ready to be installed.')); ++ notification.connect('activated', ++ () => source.open()); + source.notify(notification); + } + } +-- +2.29.2 + + +From 8d50b96701eefa7f9bff4af8c855087eee35739a Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Florian=20M=C3=BCllner?= +Date: Mon, 1 Feb 2021 18:26:00 +0100 +Subject: [PATCH 26/26] extensionDownloader: Refuse to override system + extensions + +The website allows to "update" system extensions by installing the +upstream version into the user's home directory. + +Prevent that by refusing to download and install extensions that are +already installed system-wide. +--- + js/ui/extensionDownloader.js | 8 ++++++++ + 1 file changed, 8 insertions(+) + +diff --git a/js/ui/extensionDownloader.js b/js/ui/extensionDownloader.js +index 0bd77e125..1e6f5340a 100644 +--- a/js/ui/extensionDownloader.js ++++ b/js/ui/extensionDownloader.js +@@ -16,6 +16,14 @@ var REPOSITORY_URL_UPDATE = REPOSITORY_URL_BASE + '/update-info/'; + let _httpSession; + + function installExtension(uuid, invocation) { ++ const oldExt = Main.extensionManager.lookup(uuid); ++ if (oldExt && oldExt.type === ExtensionUtils.ExtensionType.SYSTEM) { ++ log('extensionDownloader: Trying to replace system extension %s'.format(uuid)); ++ invocation.return_dbus_error('org.gnome.Shell.InstallError', ++ 'System extensions cannot be replaced'); ++ return; ++ } ++ + let params = { uuid: uuid, + shell_version: Config.PACKAGE_VERSION }; + +-- +2.29.2 + diff --git a/SOURCES/fix-some-js-warnings.patch b/SOURCES/fix-some-js-warnings.patch new file mode 100644 index 0000000..18b12eb --- /dev/null +++ b/SOURCES/fix-some-js-warnings.patch @@ -0,0 +1,182 @@ +From 43d6305bfbe079a3bf80a96d40a3a176c165ef7a Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Marco=20Trevisan=20=28Trevi=C3=B1o=29?= +Date: Thu, 23 May 2019 06:12:56 +0200 +Subject: [PATCH 1/5] realmd: Set login format to null on start and update if + invalid + +We were checking an undefined property but that would lead to a a warning. +Instead we can consider the login format unset until is null, and in case +update it. + +https://gitlab.gnome.org/GNOME/gnome-shell/merge_requests/700 +--- + js/gdm/realmd.js | 3 ++- + 1 file changed, 2 insertions(+), 1 deletion(-) + +diff --git a/js/gdm/realmd.js b/js/gdm/realmd.js +index 50f3c5899..04cd99787 100644 +--- a/js/gdm/realmd.js ++++ b/js/gdm/realmd.js +@@ -21,6 +21,7 @@ var Manager = class { + '/org/freedesktop/realmd', + this._reloadRealms.bind(this)) + this._realms = {}; ++ this._loginFormat = null; + + this._signalId = this._aggregateProvider.connect('g-properties-changed', + (proxy, properties) => { +@@ -86,7 +87,7 @@ var Manager = class { + } + + get loginFormat() { +- if (this._loginFormat !== undefined) ++ if (this._loginFormat) + return this._loginFormat; + + this._updateLoginFormat(); +-- +2.21.1 + + +From 80836cd1ea4ef5d69a35bdfd7931b0e2c202f5b3 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Florian=20M=C3=BCllner?= +Date: Tue, 9 Jun 2020 19:42:21 +0200 +Subject: [PATCH 2/5] popupMenu: Guard against non-menu-item children + +This avoid a harmless but annoying warning. +--- + js/ui/popupMenu.js | 3 ++- + 1 file changed, 2 insertions(+), 1 deletion(-) + +diff --git a/js/ui/popupMenu.js b/js/ui/popupMenu.js +index 44818533a..b5115d7f7 100644 +--- a/js/ui/popupMenu.js ++++ b/js/ui/popupMenu.js +@@ -696,7 +696,8 @@ var PopupMenuBase = class { + } + + _getMenuItems() { +- return this.box.get_children().map(a => a._delegate).filter(item => { ++ const children = this.box.get_children().filter(a => a._delegate !== undefined); ++ return children.map(a => a._delegate).filter(item => { + return item instanceof PopupBaseMenuItem || item instanceof PopupMenuSection; + }); + } +-- +2.21.1 + + +From f0af67381cf0fb9a9ab766fa6b3d3e6ff5707122 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Florian=20M=C3=BCllner?= +Date: Tue, 9 Jun 2020 19:48:06 +0200 +Subject: [PATCH 3/5] st/shadow: Check pipeline when painting + +We shouldn't simply assume that st_shadow_helper_update() has been +called before paint() or that the pipeline was created successfully. +--- + src/st/st-shadow.c | 11 ++++++----- + 1 file changed, 6 insertions(+), 5 deletions(-) + +diff --git a/src/st/st-shadow.c b/src/st/st-shadow.c +index f3a22f034..7665de755 100644 +--- a/src/st/st-shadow.c ++++ b/src/st/st-shadow.c +@@ -289,9 +289,10 @@ st_shadow_helper_paint (StShadowHelper *helper, + ClutterActorBox *actor_box, + guint8 paint_opacity) + { +- _st_paint_shadow_with_opacity (helper->shadow, +- framebuffer, +- helper->pipeline, +- actor_box, +- paint_opacity); ++ if (helper->pipeline != NULL) ++ _st_paint_shadow_with_opacity (helper->shadow, ++ framebuffer, ++ helper->pipeline, ++ actor_box, ++ paint_opacity); + } +-- +2.21.1 + + +From a500f3c59a485755b8361e8f4dd48f8df4af95ba Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Florian=20M=C3=BCllner?= +Date: Tue, 5 Jan 2021 21:42:24 +0100 +Subject: [PATCH 4/5] viewSelector: Don't set page parent during construction + +gjs now aggressively garbage-collects objects that fall out of scope, +sometimes too aggressively: + + - we pass a child as construct property to StBin + - as a result, the child's ::parent-set handler runs + - when calling clutter_actor_get_parent() from that + handler, the returned object is garbage-collected + *before* the constructor returns (and thus the + assignment that would keep it alive) + +This is a bug on the gjs side that should be fixed, but we can easily +work around the issue by setting the child after constructing the +parent. +--- + js/ui/viewSelector.js | 12 +++++++----- + 1 file changed, 7 insertions(+), 5 deletions(-) + +diff --git a/js/ui/viewSelector.js b/js/ui/viewSelector.js +index 77146552d..6529ac9a5 100644 +--- a/js/ui/viewSelector.js ++++ b/js/ui/viewSelector.js +@@ -301,11 +301,13 @@ var ViewSelector = class { + _addPage(actor, name, a11yIcon, params) { + params = Params.parse(params, { a11yFocus: null }); + +- let page = new St.Bin({ child: actor, +- x_align: St.Align.START, +- y_align: St.Align.START, +- x_fill: true, +- y_fill: true }); ++ let page = new St.Bin({ ++ x_align: St.Align.START, ++ y_align: St.Align.START, ++ x_fill: true, ++ y_fill: true, ++ }); ++ page.set_child(actor); + if (params.a11yFocus) + Main.ctrlAltTabManager.addGroup(params.a11yFocus, name, a11yIcon); + else +-- +2.21.1 + + +From a53d1a74fed3aee896a6930130bd7e3a39a24255 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Florian=20M=C3=BCllner?= +Date: Fri, 23 Oct 2020 23:44:48 +0200 +Subject: [PATCH 5/5] workspacesView: Don't set up MetaLater when unparented + +We already do the check in the later handler, but if we got +unparented because the actor is destroyed, then the call to +get_parent() itself will trigger a (harmless but annoying) +warning. +--- + js/ui/workspacesView.js | 3 +++ + 1 file changed, 3 insertions(+) + +diff --git a/js/ui/workspacesView.js b/js/ui/workspacesView.js +index e302296a6..3270900b2 100644 +--- a/js/ui/workspacesView.js ++++ b/js/ui/workspacesView.js +@@ -715,6 +715,9 @@ var WorkspacesDisplay = class { + oldParent.disconnect(this._notifyOpacityId); + this._notifyOpacityId = 0; + ++ if (!this.actor.get_parent()) ++ return; ++ + Meta.later_add(Meta.LaterType.BEFORE_REDRAW, () => { + let newParent = this.actor.get_parent(); + if (!newParent) +-- +2.21.1 + diff --git a/SOURCES/osk-fixes.patch b/SOURCES/osk-fixes.patch new file mode 100644 index 0000000..6c5648d --- /dev/null +++ b/SOURCES/osk-fixes.patch @@ -0,0 +1,117 @@ +From 96ccb155bbe6ce570832a9f3d27a0a08698127ea Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Jonas=20Dre=C3=9Fler?= +Date: Sat, 28 Mar 2020 14:15:09 +0100 +Subject: [PATCH 1/3] keyboard: Don't include keyboard devices when updating + lastDevice + +We're dealing with attached keyboards now using the touch_mode property +of ClutterSeat: If a device has a keyboard attached, the touch-mode is +FALSE and we won't automatically show the OSK on touches, also the +touch-mode gets set to FALSE when an external keyboard is being plugged +in, so that also hides the OSK automatically. + +With that, we can now ignore keyboard devices when updating the last +used device and no longer have to special-case our own virtual devices. + +Because there was no special-case for the virtual device we use on +Wayland now, this fixes a bug where the keyboard disappeared after +touching keys like Enter or Backspace. + +Fixes: https://gitlab.gnome.org/GNOME/gnome-shell/-/issues/2287 + +https://gitlab.gnome.org/GNOME/gnome-shell/-/merge_requests/1142 +--- + js/ui/keyboard.js | 3 +++ + 1 file changed, 3 insertions(+) + +diff --git a/js/ui/keyboard.js b/js/ui/keyboard.js +index c4ac72d..94b5325 100644 +--- a/js/ui/keyboard.js ++++ b/js/ui/keyboard.js +@@ -1075,6 +1075,9 @@ var Keyboard = class Keyboard { + let device = manager.get_device(deviceId); + + if (device.get_device_name().indexOf('XTEST') < 0) { ++ if (device.device_type == Clutter.InputDeviceType.KEYBOARD_DEVICE) ++ return; ++ + this._lastDeviceId = deviceId; + this._syncEnabled(); + } +-- +2.26.2 + + +From 3106746ae424287d8644643a2ef46d565e4cd7ed Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Jonas=20Dre=C3=9Fler?= +Date: Sat, 28 Mar 2020 14:34:24 +0100 +Subject: [PATCH 2/3] layout: Use translation_y of 0 to hide keyboard + +Since we show the keyboard using a translation_y of -keyboardHeight, the +keyboard will be moved down far enough to be out of sight by setting +translation_y to 0. + +https://gitlab.gnome.org/GNOME/gnome-shell/-/merge_requests/1142 +--- + js/ui/layout.js | 6 +++--- + 1 file changed, 3 insertions(+), 3 deletions(-) + +diff --git a/js/ui/layout.js b/js/ui/layout.js +index beb4c0a..4382f6e 100644 +--- a/js/ui/layout.js ++++ b/js/ui/layout.js +@@ -719,7 +719,7 @@ var LayoutManager = GObject.registerClass({ + showKeyboard() { + this.keyboardBox.show(); + Tweener.addTween(this.keyboardBox, +- { anchor_y: this.keyboardBox.height, ++ { translation_y: -this.keyboardBox.height, + opacity: 255, + time: KEYBOARD_ANIMATION_TIME, + transition: 'easeOutQuad', +@@ -735,7 +735,7 @@ var LayoutManager = GObject.registerClass({ + this._updateRegions(); + + this._keyboardHeightNotifyId = this.keyboardBox.connect('notify::height', () => { +- this.keyboardBox.anchor_y = this.keyboardBox.height; ++ this.keyboardBox.translation_y = -this.keyboardBox.height; + }); + } + +@@ -745,7 +745,7 @@ var LayoutManager = GObject.registerClass({ + this._keyboardHeightNotifyId = 0; + } + Tweener.addTween(this.keyboardBox, +- { anchor_y: 0, ++ { translation_y: 0, + opacity: 0, + time: immediate ? 0 : KEYBOARD_ANIMATION_TIME, + transition: 'easeInQuad', +-- +2.26.2 + + +From 642822308a72be6a47f4eb285f32539499f0d3e4 Mon Sep 17 00:00:00 2001 +From: rpm-build +Date: Wed, 21 Oct 2020 20:29:34 +0200 +Subject: [PATCH 3/3] layout: queue redraw after hiding keyboard + +--- + js/ui/layout.js | 1 + + 1 file changed, 1 insertion(+) + +diff --git a/js/ui/layout.js b/js/ui/layout.js +index 4382f6e..1824313 100644 +--- a/js/ui/layout.js ++++ b/js/ui/layout.js +@@ -759,6 +759,7 @@ var LayoutManager = GObject.registerClass({ + _hideKeyboardComplete() { + this.keyboardBox.hide(); + this._updateRegions(); ++ global.stage.queue_redraw(); + } + + // setDummyCursorGeometry: +-- +2.26.2 + diff --git a/SOURCES/wake-up-on-deactivate.patch b/SOURCES/wake-up-on-deactivate.patch new file mode 100644 index 0000000..b20cbbd --- /dev/null +++ b/SOURCES/wake-up-on-deactivate.patch @@ -0,0 +1,79 @@ +From d6ead50fe230df58ddab822966d69760b00ec920 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Florian=20M=C3=BCllner?= +Date: Wed, 1 Apr 2020 14:48:10 +0200 +Subject: [PATCH 1/2] screenShield: Switch lightboxes off before unlock + transition + +There is no point in animating a transition with fullscreen black +rectangles stacked on top, so switch them off before rather than +after the transition. + +https://gitlab.gnome.org/GNOME/gnome-shell/-/merge_requests/1158 +--- + js/ui/screenShield.js | 5 +++-- + 1 file changed, 3 insertions(+), 2 deletions(-) + +diff --git a/js/ui/screenShield.js b/js/ui/screenShield.js +index cd38f11fc8..282f29fa30 100644 +--- a/js/ui/screenShield.js ++++ b/js/ui/screenShield.js +@@ -1221,6 +1221,9 @@ var ScreenShield = class { + this._isModal = false; + } + ++ this._longLightbox.hide(); ++ this._shortLightbox.hide(); ++ + Tweener.addTween(this._lockDialogGroup, { + scale_x: 0, + scale_y: 0, +@@ -1237,8 +1240,6 @@ var ScreenShield = class { + this._dialog = null; + } + +- this._longLightbox.hide(); +- this._shortLightbox.hide(); + this.actor.hide(); + + if (this._becameActiveId != 0) { +-- +2.28.0 + + +From 39ac7cad68d8c00d98c900b35add637b01eddbbf Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Florian=20M=C3=BCllner?= +Date: Tue, 31 Mar 2020 21:07:59 +0200 +Subject: [PATCH 2/2] screenShield: Wake up on deactivate() + +Usually the screen is woken up before the shield is deactivated, but +it is also possible to unlock the session programmatically via the +org.gnome.ScreenSaver D-Bus API. + +The intention is very likely not to unlock a turned off screen in +that case. Nor does it seem like a good idea to change the lock +state without any indication. + +Waking up the screen is more likely to meet expectations and is +more reasonable too, so do that. + +https://gitlab.gnome.org/GNOME/gnome-shell/-/merge_requests/1158 +--- + js/ui/screenShield.js | 2 ++ + 1 file changed, 2 insertions(+) + +diff --git a/js/ui/screenShield.js b/js/ui/screenShield.js +index 282f29fa30..2d0a429bee 100644 +--- a/js/ui/screenShield.js ++++ b/js/ui/screenShield.js +@@ -1200,6 +1200,8 @@ var ScreenShield = class { + if (Main.sessionMode.currentMode == 'unlock-dialog') + Main.sessionMode.popMode('unlock-dialog'); + ++ this.emit('wake-up-screen'); ++ + if (this._isGreeter) { + // We don't want to "deactivate" any more than + // this. In particular, we don't want to drop +-- +2.28.0 + diff --git a/SPECS/gnome-shell.spec b/SPECS/gnome-shell.spec index 2ee05b4..b3eb970 100644 --- a/SPECS/gnome-shell.spec +++ b/SPECS/gnome-shell.spec @@ -1,6 +1,6 @@ Name: gnome-shell Version: 3.32.2 -Release: 20%{?dist} +Release: 30%{?dist} Summary: Window management and application launching for GNOME Group: User Interface/Desktops @@ -24,14 +24,18 @@ Patch16: 0001-data-install-process-working.svg-to-filesystem.patch Patch17: 0001-loginDialog-make-info-messages-themed.patch Patch18: 0001-gdm-add-AuthList-control.patch Patch19: 0002-gdmUtil-enable-support-for-GDM-s-ChoiceList-PAM-exte.patch +Patch20: wake-up-on-deactivate.patch +Patch21: caps-lock-warning.patch # Misc. Patch30: 0001-shellDBus-Add-a-DBus-method-to-load-a-single-extensi.patch Patch31: 0001-extensions-Add-a-SESSION_MODE-extension-type.patch -Patch32: 0001-extensionSystem-Notify-about-extension-issues-on-upd.patch +Patch32: extension-updates.patch Patch33: 0001-panel-add-an-icon-to-the-ActivitiesButton.patch Patch34: 0001-app-Fall-back-to-window-title-instead-of-WM_CLASS.patch Patch35: 0001-windowMenu-Bring-back-workspaces-submenu-for-static-.patch +Patch36: 0001-shell-app-Handle-workspace-from-startup-notification.patch +Patch37: 0001-main-Dump-stack-on-segfaults-by-default.patch Patch38: 0001-appDisplay-Show-full-app-name-on-hover.patch Patch39: horizontal-workspace-support.patch Patch40: 0001-animation-fix-unintentional-loop-while-polkit-dialog.patch @@ -51,6 +55,7 @@ Patch52: 0001-popupMenu-Handle-keypress-if-numlock-is-enabled.patch # Backport JS invalid access warnings (#1651894, #1663171, #1642482, #1637622) Patch54: fix-invalid-access-warnings.patch Patch55: more-spurious-allocation-warnings.patch +Patch56: fix-some-js-warnings.patch # Backport performance fixes under load (#1820760) Patch60: 0001-environment-reduce-calls-to-g_time_zone_new_local.patch @@ -60,6 +65,13 @@ Patch63: 0004-global-force-fsync-to-worker-thread-when-saving-stat.patch Patch64: 0005-app-cache-add-ShellAppCache-for-GAppInfo-caching.patch Patch65: 0006-js-Always-use-AppSystem-to-lookup-apps.patch +# Stop screen recording on monitor changes (#1705392) +Patch70: 0001-screencast-Stop-recording-when-screen-size-or-resour.patch + +# Backport OSK fixes (#1871041) +Patch75: osk-fixes.patch +Patch76: 0001-keyboard-Only-enable-keyboard-if-ClutterDeviceManage.patch + # suspend/resume fix on nvidia (#1663440) Patch10001: 0001-background-refresh-after-suspend-on-wayland.patch Patch10002: 0002-background-rebuild-background-not-just-animation-on-.patch @@ -217,6 +229,8 @@ desktop-file-validate %{buildroot}%{_datadir}/applications/evolution-calendar.de %{_datadir}/dbus-1/interfaces/org.gnome.Shell.Screenshot.xml %{_datadir}/dbus-1/interfaces/org.gnome.ShellSearchProvider.xml %{_datadir}/dbus-1/interfaces/org.gnome.ShellSearchProvider2.xml +%{_datadir}/icons/hicolor/scalable/apps/org.gnome.Extensions.svg +%{_datadir}/icons/hicolor/symbolic/apps/org.gnome.Extensions-symbolic.svg %{_userunitdir}/gnome-shell.service %{_userunitdir}/gnome-shell-wayland.target %{_userunitdir}/gnome-shell-x11.target @@ -239,6 +253,50 @@ desktop-file-validate %{buildroot}%{_datadir}/applications/evolution-calendar.de %{_mandir}/man1/%{name}.1.gz %changelog +* Mon Feb 22 2021 Carlos Garnacho - 3.32.2-30 +- Backport of touch mode + Resolves: #1833787 + +* Mon Feb 01 2021 Florian Müllner - 3.32.2-29 +- Refuse to override system extensions + Related: #1802105 + +* Mon Jan 25 2021 Florian Müllner - 3.32.2-28 +- Backport extension updates support + Related: #1802105 + +* Thu Jan 14 2021 Florian Müllner - 3.32.2-27 +- Default to printing JS backtrace on segfaults + Resolves: #1883868 + +* Wed Jan 13 2021 Carlos Garnacho - 3.32.2-26 +- Backport OSK fixes + Resolves: #1871041 + +* Tue Jan 12 2021 Jonas Ådahl - 3.32.2-25 +- Stop screen recording on monitor changes + Resolves: #1705392 + +* Thu Jan 07 2021 Florian Müllner - 3.32.2-24 +- Handle workspace from startup notification + Resolves: #1671761 + +* Tue Jan 05 2021 Florian Müllner - 3.32.2-23 +- Work around aggressive garbage collection + Related: #1881312 + +* Fri Oct 23 2020 Florian Müllner - 3.32.2-22 +- Wake up lock screen when deactivated programmatically + Resolves: #1854290 +- Backport better caps-lock warning + Resolves: #1861357 +- Fix more (harmless) JS warnings + Resolves: #1881312 + +* Thu Oct 01 2020 Michael Catanzaro - 3.32.2-21 +- Fix JS warning in AuthList downstream patch + Resolves: #1860946 + * Thu Jul 30 2020 Florian Müllner - 3.32.2-20 - Fix popupMenu keynav when NumLock is active Resolves: #1840080