[gnome-shell] js: Use (dis)connectObject()



commit 26235bbe5433c48b81bd3917aaf9ef14ff1929b2
Author: Florian Müllner <fmuellner gnome org>
Date:   Mon Aug 16 00:36:59 2021 +0200

    js: Use (dis)connectObject()
    
    Start using the new methods to simplify signal cleanup. For now,
    focus on replacing existing cleanups; in most cases this means
    signals connected in the constructor and disconnected on destroy,
    but also other cases with a similarly defined lifetime (say: from
    show to hide).
    
    This doesn't change signal connections that only exist for a short
    time (say: once), handlers that are connected on-demand (say: the
    first time a particular method is called), or connections that
    aren't tracked (read: disconnected) at all.
    
    We will eventually replace the latter with connectObject() as
    well - especially from actor subclasses - but the changeset is
    already big enough as-is :-)
    
    Part-of: <https://gitlab.gnome.org/GNOME/gnome-shell/-/merge_requests/1953>

 js/gdm/loginDialog.js                | 100 +++++------------
 js/gdm/realmd.js                     |   6 +-
 js/gdm/util.js                       |  60 ++++------
 js/misc/util.js                      |   7 +-
 js/ui/altTab.js                      |  38 +++----
 js/ui/animation.js                   |   9 +-
 js/ui/appDisplay.js                  |  89 +++++----------
 js/ui/appMenu.js                     |  64 ++++-------
 js/ui/background.js                  |  36 ++----
 js/ui/backgroundMenu.js              |   6 +-
 js/ui/boxpointer.js                  |  22 +---
 js/ui/calendar.js                    |  94 +++++-----------
 js/ui/closeDialog.js                 |  19 +---
 js/ui/components/automountManager.js |  17 ++-
 js/ui/components/autorunManager.js   |   8 +-
 js/ui/components/polkitAgent.js      |  56 ++++------
 js/ui/components/telepathyClient.js  |  72 +++++-------
 js/ui/dash.js                        |   7 +-
 js/ui/dialog.js                      |  14 +--
 js/ui/endSessionDialog.js            |  12 +-
 js/ui/iconGrid.js                    |  18 +--
 js/ui/keyboard.js                    | 185 +++++++++++--------------------
 js/ui/layout.js                      |  15 +--
 js/ui/lightbox.js                    |  14 +--
 js/ui/magnifier.js                   |   7 +-
 js/ui/messageList.js                 |  22 +---
 js/ui/messageTray.js                 |  89 ++++-----------
 js/ui/modalDialog.js                 |   9 +-
 js/ui/mpris.js                       |  25 ++---
 js/ui/overviewControls.js            |  11 +-
 js/ui/padOsd.js                      |  47 ++++----
 js/ui/panel.js                       |  54 ++-------
 js/ui/popupMenu.js                   | 196 +++++++++++++--------------------
 js/ui/search.js                      |  15 +--
 js/ui/shellEntry.js                  |  12 +-
 js/ui/shellMountOperation.js         |  19 ++--
 js/ui/status/keyboard.js             |  27 ++---
 js/ui/status/location.js             |  31 ++----
 js/ui/status/network.js              | 208 ++++++++++-------------------------
 js/ui/status/thunderbolt.js          |   5 +-
 js/ui/status/volume.js               |  19 +---
 js/ui/swipeTracker.js                |   9 +-
 js/ui/switcherPopup.js               |   6 +-
 js/ui/unlockDialog.js                |  83 ++++----------
 js/ui/userWidget.js                  |  56 ++--------
 js/ui/windowAttentionHandler.js      |  30 ++---
 js/ui/windowManager.js               |  57 ++++------
 js/ui/windowPreview.js               |  24 +---
 js/ui/workspace.js                   |  63 +++--------
 js/ui/workspaceAnimation.js          |  13 +--
 js/ui/workspaceSwitcherPopup.js      |  14 +--
 js/ui/workspaceThumbnail.js          | 179 +++++++++---------------------
 js/ui/workspacesView.js              | 125 ++++++---------------
 js/ui/xdndHandler.js                 |  12 +-
 54 files changed, 757 insertions(+), 1678 deletions(-)
---
diff --git a/js/gdm/loginDialog.js b/js/gdm/loginDialog.js
index 3bde6fb06e..bead5c8e3e 100644
--- a/js/gdm/loginDialog.js
+++ b/js/gdm/loginDialog.js
@@ -55,10 +55,8 @@ var UserListItem = GObject.registerClass({
         });
 
         this.user = user;
-        this._userChangedId = this.user.connect('changed',
-                                                this._onUserChanged.bind(this));
+        this.user.connectObject('changed', this._onUserChanged.bind(this), this);
 
-        this.connect('destroy', this._onDestroy.bind(this));
         this.connect('notify::hover', () => {
             this._setSelected(this.hover);
         });
@@ -100,10 +98,6 @@ var UserListItem = GObject.registerClass({
             this.remove_style_pseudo_class('logged-in');
     }
 
-    _onDestroy() {
-        this.user.disconnect(this._userChangedId);
-    }
-
     vfunc_clicked() {
         this.emit('activate');
     }
@@ -441,8 +435,8 @@ var LoginDialog = GObject.registerClass({
                                this._updateLogo.bind(this));
 
         this._textureCache = St.TextureCache.get_default();
-        this._updateLogoTextureId = this._textureCache.connect('texture-file-changed',
-                                                               this._updateLogoTexture.bind(this));
+        this._textureCache.connectObject('texture-file-changed',
+            this._updateLogoTexture.bind(this), this);
 
         this._userSelectionBox = new St.BoxLayout({
             style_class: 'login-dialog-user-selection-box',
@@ -533,16 +527,16 @@ var LoginDialog = GObject.registerClass({
         this._userListLoaded = false;
 
         this._realmManager = new Realmd.Manager();
-        this._realmSignalId = this._realmManager.connect('login-format-changed',
-                                                         this._showRealmLoginHint.bind(this));
+        this._realmManager.connectObject('login-format-changed',
+            this._showRealmLoginHint.bind(this), this);
 
         LoginManager.getLoginManager().getCurrentSessionProxy(this._gotGreeterSessionProxy.bind(this));
 
         // If the user list is enabled, it should take key focus; make sure the
         // screen shield is initialized first to prevent it from stealing the
         // focus later
-        this._startupCompleteId = Main.layoutManager.connect('startup-complete',
-                                                             this._updateDisableUserList.bind(this));
+        Main.layoutManager.connectObject('startup-complete',
+            this._updateDisableUserList.bind(this), this);
     }
 
     _getBannerAllocation(dialogBox) {
@@ -755,12 +749,11 @@ var LoginDialog = GObject.registerClass({
 
     _ensureUserListLoaded() {
         if (!this._userManager.is_loaded) {
-            this._userManagerLoadedId = this._userManager.connect('notify::is-loaded',
+            this._userManager.connectObject('notify::is-loaded',
                 () => {
                     if (this._userManager.is_loaded) {
+                        this._userManager.disconnectObject(this);
                         this._loadUserList();
-                        this._userManager.disconnect(this._userManagerLoadedId);
-                        this._userManagerLoadedId = 0;
                     }
                 });
         } else {
@@ -864,12 +857,10 @@ var LoginDialog = GObject.registerClass({
 
             this._greeter = this._gdmClient.get_greeter_sync(null);
 
-            this._defaultSessionChangedId = this._greeter.connect('default-session-name-changed',
-                                                                  this._onDefaultSessionChanged.bind(this));
-            this._sessionOpenedId = this._greeter.connect('session-opened',
-                                                          this._onSessionOpened.bind(this));
-            this._timedLoginRequestedId = this._greeter.connect('timed-login-requested',
-                                                                this._onTimedLoginRequested.bind(this));
+            this._greeter.connectObject(
+                'default-session-name-changed', this._onDefaultSessionChanged.bind(this),
+                'session-opened', this._onSessionOpened.bind(this),
+                'timed-login-requested', this._onTimedLoginRequested.bind(this), this);
         }
     }
 
@@ -994,11 +985,10 @@ var LoginDialog = GObject.registerClass({
 
     _gotGreeterSessionProxy(proxy) {
         this._greeterSessionProxy = proxy;
-        this._greeterSessionProxyChangedId =
-            proxy.connect('g-properties-changed', () => {
-                if (proxy.Active)
-                    this._loginScreenSessionActivated();
-            });
+        proxy.connectObject('g-properties-changed', () => {
+            if (proxy.Active)
+                this._loginScreenSessionActivated();
+        }, this);
     }
 
     _startSession(serviceName) {
@@ -1214,44 +1204,14 @@ var LoginDialog = GObject.registerClass({
     }
 
     _onDestroy() {
-        if (this._userManagerLoadedId) {
-            this._userManager.disconnect(this._userManagerLoadedId);
-            this._userManagerLoadedId = 0;
-        }
-        if (this._userAddedId) {
-            this._userManager.disconnect(this._userAddedId);
-            this._userAddedId = 0;
-        }
-        if (this._userRemovedId) {
-            this._userManager.disconnect(this._userRemovedId);
-            this._userRemovedId = 0;
-        }
-        if (this._userChangedId) {
-            this._userManager.disconnect(this._userChangedId);
-            this._userChangedId = 0;
-        }
-        this._textureCache.disconnect(this._updateLogoTextureId);
-        Main.layoutManager.disconnect(this._startupCompleteId);
         if (this._settings) {
             this._settings.run_dispose();
             this._settings = null;
         }
-        if (this._greeter) {
-            this._greeter.disconnect(this._defaultSessionChangedId);
-            this._greeter.disconnect(this._sessionOpenedId);
-            this._greeter.disconnect(this._timedLoginRequestedId);
-            this._greeter = null;
-        }
-        if (this._greeterSessionProxy) {
-            this._greeterSessionProxy.disconnect(this._greeterSessionProxyChangedId);
-            this._greeterSessionProxy = null;
-        }
-        if (this._realmManager) {
-            this._realmManager.disconnect(this._realmSignalId);
-            this._realmSignalId = 0;
-            this._realmManager.release();
-            this._realmManager = null;
-        }
+        this._greeter = null;
+        this._greeterSessionProxy = null;
+        this._realmManager?.release();
+        this._realmManager = null;
     }
 
     _loadUserList() {
@@ -1267,26 +1227,22 @@ var LoginDialog = GObject.registerClass({
 
         this._updateDisableUserList();
 
-        this._userAddedId = this._userManager.connect('user-added',
-            (userManager, user) => {
+        this._userManager.connectObject(
+            'user-added', (userManager, user) => {
                 this._userList.addUser(user);
                 this._updateDisableUserList();
-            });
-
-        this._userRemovedId = this._userManager.connect('user-removed',
-            (userManager, user) => {
+            },
+            'user-removed', (userManager, user) => {
                 this._userList.removeUser(user);
                 this._updateDisableUserList();
-            });
-
-        this._userChangedId = this._userManager.connect('user-changed',
-            (userManager, user) => {
+            },
+            'user-changed', (userManager, user) => {
                 if (this._userList.containsUser(user) && user.locked)
                     this._userList.removeUser(user);
                 else if (!this._userList.containsUser(user) && !user.locked)
                     this._userList.addUser(user);
                 this._updateDisableUserList();
-            });
+            }, this);
 
         return GLib.SOURCE_REMOVE;
     }
diff --git a/js/gdm/realmd.js b/js/gdm/realmd.js
index 7584968209..ba38bcd628 100644
--- a/js/gdm/realmd.js
+++ b/js/gdm/realmd.js
@@ -23,11 +23,11 @@ var Manager = class {
         this._realms = {};
         this._loginFormat = null;
 
-        this._signalId = this._aggregateProvider.connect('g-properties-changed',
+        this._aggregateProvider.connectObject('g-properties-changed',
             (proxy, properties) => {
                 if ('Realms' in properties.deep_unpack())
                     this._reloadRealms();
-            });
+            }, this);
     }
 
     _reloadRealms() {
@@ -100,7 +100,7 @@ var Manager = class {
                 'org.freedesktop.realmd',
                 '/org/freedesktop/realmd',
                 service => service.ReleaseRemote());
-        this._aggregateProvider.disconnect(this._signalId);
+        this._aggregateProvider.disconnectObject(this);
         this._realms = { };
         this._updateLoginFormat();
     }
diff --git a/js/gdm/util.js b/js/gdm/util.js
index 959fd44bac..53cbb72ff9 100644
--- a/js/gdm/util.js
+++ b/js/gdm/util.js
@@ -165,10 +165,9 @@ var ShellUserVerifier = class {
         this.smartcardDetected = false;
         this._checkForSmartcard();
 
-        this._smartcardInsertedId = this._smartcardManager.connect('smartcard-inserted',
-                                                                   this._checkForSmartcard.bind(this));
-        this._smartcardRemovedId = this._smartcardManager.connect('smartcard-removed',
-                                                                  this._checkForSmartcard.bind(this));
+        this._smartcardManager.connectObject(
+            'smartcard-inserted', this._checkForSmartcard.bind(this),
+            'smartcard-removed', this._checkForSmartcard.bind(this), this);
 
         this._messageQueue = [];
         this._messageQueueTimeoutId = 0;
@@ -187,9 +186,8 @@ var ShellUserVerifier = class {
                     this._credentialManagers[service].token);
             }
 
-            this._credentialManagers[service]._authenticatedSignalId =
-                this._credentialManagers[service].connect('user-authenticated',
-                                                          this._onCredentialManagerAuthenticated.bind(this));
+            this._credentialManagers[service].connectObject('user-authenticated',
+                this._onCredentialManagerAuthenticated.bind(this), this);
         }
     }
 
@@ -259,13 +257,12 @@ var ShellUserVerifier = class {
         this._settings.run_dispose();
         this._settings = null;
 
-        this._smartcardManager.disconnect(this._smartcardInsertedId);
-        this._smartcardManager.disconnect(this._smartcardRemovedId);
+        this._smartcardManager.disconnectObject(this);
         this._smartcardManager = null;
 
         for (let service in this._credentialManagers) {
             let credentialManager = this._credentialManagers[service];
-            credentialManager.disconnect(credentialManager._authenticatedSignalId);
+            credentialManager.disconnectObject(this);
             credentialManager = null;
         }
     }
@@ -495,35 +492,26 @@ var ShellUserVerifier = class {
 
     _connectSignals() {
         this._disconnectSignals();
-        this._signalIds = [];
-
-        let id = this._userVerifier.connect('info', this._onInfo.bind(this));
-        this._signalIds.push(id);
-        id = this._userVerifier.connect('problem', this._onProblem.bind(this));
-        this._signalIds.push(id);
-        id = this._userVerifier.connect('info-query', this._onInfoQuery.bind(this));
-        this._signalIds.push(id);
-        id = this._userVerifier.connect('secret-info-query', this._onSecretInfoQuery.bind(this));
-        this._signalIds.push(id);
-        id = this._userVerifier.connect('conversation-stopped', this._onConversationStopped.bind(this));
-        this._signalIds.push(id);
-        id = this._userVerifier.connect('service-unavailable', this._onServiceUnavailable.bind(this));
-        this._signalIds.push(id);
-        id = this._userVerifier.connect('reset', this._onReset.bind(this));
-        this._signalIds.push(id);
-        id = this._userVerifier.connect('verification-complete', this._onVerificationComplete.bind(this));
-        this._signalIds.push(id);
-
-        if (this._userVerifierChoiceList)
-            this._userVerifierChoiceList.connect('choice-query', this._onChoiceListQuery.bind(this));
+
+        this._userVerifier.connectObject(
+            'info', this._onInfo.bind(this),
+            'problem', this._onProblem.bind(this),
+            'info-query', this._onInfoQuery.bind(this),
+            'secret-info-query', this._onSecretInfoQuery.bind(this),
+            'conversation-stopped', this._onConversationStopped.bind(this),
+            'service-unavailable', this._onServiceUnavailable.bind(this),
+            'reset', this._onReset.bind(this),
+            'verification-complete', this._onVerificationComplete.bind(this),
+            this);
+
+        if (this._userVerifierChoiceList) {
+            this._userVerifierChoiceList.connectObject('choice-query',
+                this._onChoiceListQuery.bind(this), this);
+        }
     }
 
     _disconnectSignals() {
-        if (!this._signalIds || !this._userVerifier)
-            return;
-
-        this._signalIds.forEach(s => this._userVerifier.disconnect(s));
-        this._signalIds = [];
+        this._userVerifier?.disconnectObject(this);
     }
 
     _getForegroundService() {
diff --git a/js/misc/util.js b/js/misc/util.js
index 8f7192f53e..e6065c446a 100644
--- a/js/misc/util.js
+++ b/js/misc/util.js
@@ -315,10 +315,9 @@ function createTimeLabel(date, params) {
         _desktopSettings = new Gio.Settings({ schema_id: 'org.gnome.desktop.interface' });
 
     let label = new St.Label({ text: formatTime(date, params) });
-    let id = _desktopSettings.connect('changed::clock-format', () => {
-        label.text = formatTime(date, params);
-    });
-    label.connect('destroy', () => _desktopSettings.disconnect(id));
+    _desktopSettings.connectObject(
+        'changed::clock-format', () => (label.text = formatTime(date, params)),
+        label);
     return label;
 }
 
diff --git a/js/ui/altTab.js b/js/ui/altTab.js
index f8e79a1761..95dc75f02a 100644
--- a/js/ui/altTab.js
+++ b/js/ui/altTab.js
@@ -400,7 +400,6 @@ class CyclerHighlight extends St.Widget {
     _init() {
         super._init({ layout_manager: new Clutter.BinLayout() });
         this._window = null;
-        this._sizeChangedId = 0;
 
         this._clone = new Clutter.Clone();
         this.add_actor(this._clone);
@@ -421,8 +420,7 @@ class CyclerHighlight extends St.Widget {
         if (this._window == w)
             return;
 
-        if (this._sizeChangedId)
-            this._window.disconnect(this._sizeChangedId);
+        this._window?.disconnectObject(this);
 
         this._window = w;
 
@@ -438,8 +436,8 @@ class CyclerHighlight extends St.Widget {
 
         if (this._window) {
             this._onSizeChanged();
-            this._sizeChangedId = this._window.connect('size-changed',
-                this._onSizeChanged.bind(this));
+            this._window.connectObject('size-changed',
+                this._onSizeChanged.bind(this), this);
         } else {
             this._highlight.set_size(0, 0);
             this._highlight.hide();
@@ -723,9 +721,8 @@ class AppSwitcher extends SwitcherPopup.SwitcherList {
         if (this._mouseTimeOutId != 0)
             GLib.source_remove(this._mouseTimeOutId);
 
-        this.icons.forEach(icon => {
-            icon.app.disconnect(icon._stateChangedId);
-        });
+        this.icons.forEach(
+            icon => icon.app.disconnectObject(this));
     }
 
     _setIconSize() {
@@ -868,10 +865,10 @@ class AppSwitcher extends SwitcherPopup.SwitcherList {
         this.icons.push(appIcon);
         let item = this.addItem(appIcon, appIcon.label);
 
-        appIcon._stateChangedId = appIcon.app.connect('notify::state', app => {
+        appIcon.app.connectObject('notify::state', app => {
             if (app.state != Shell.AppState.RUNNING)
                 this._removeIcon(app);
-        });
+        }, this);
 
         let arrow = new St.DrawingArea({ style_class: 'switcher-arrow' });
         arrow.connect('repaint', () => SwitcherPopup.drawArrow(arrow, St.Side.BOTTOM));
@@ -962,9 +959,8 @@ class ThumbnailSwitcher extends SwitcherPopup.SwitcherList {
             this._thumbnailBins[i].set_height(binHeight);
             this._thumbnailBins[i].add_actor(clone);
 
-            clone._destroyId = mutterWindow.connect('destroy', source => {
-                this._removeThumbnail(source, clone);
-            });
+            mutterWindow.connectObject('destroy',
+                source => this._removeThumbnail(source, clone), this);
             this._clones.push(clone);
         }
 
@@ -989,10 +985,8 @@ class ThumbnailSwitcher extends SwitcherPopup.SwitcherList {
     }
 
     _onDestroy() {
-        this._clones.forEach(clone => {
-            if (clone.source)
-                clone.source.disconnect(clone._destroyId);
-        });
+        this._clones.forEach(
+            clone => clone?.source.disconnectObject(this));
     }
 });
 
@@ -1077,18 +1071,16 @@ class WindowSwitcher extends SwitcherPopup.SwitcherList {
             this.addItem(icon, icon.label);
             this.icons.push(icon);
 
-            icon._unmanagedSignalId = icon.window.connect('unmanaged', window => {
-                this._removeWindow(window);
-            });
+            icon.window.connectObject('unmanaged',
+                window => this._removeWindow(window), this);
         }
 
         this.connect('destroy', this._onDestroy.bind(this));
     }
 
     _onDestroy() {
-        this.icons.forEach(icon => {
-            icon.window.disconnect(icon._unmanagedSignalId);
-        });
+        this.icons.forEach(
+            icon => icon.window.disconnectObject(this));
     }
 
     vfunc_get_preferred_height(forWidth) {
diff --git a/js/ui/animation.js b/js/ui/animation.js
index 5cb3a83c13..c2ed248ef2 100644
--- a/js/ui/animation.js
+++ b/js/ui/animation.js
@@ -22,11 +22,11 @@ class Animation extends St.Bin {
         this.connect('resource-scale-changed',
             this._loadFile.bind(this, file, width, height));
 
-        this._scaleChangedId = themeContext.connect('notify::scale-factor',
+        themeContext.connectObject('notify::scale-factor',
             () => {
                 this._loadFile(file, width, height);
                 this.set_size(width * themeContext.scale_factor, height * themeContext.scale_factor);
-            });
+            }, this);
 
         this._speed = speed;
 
@@ -122,11 +122,6 @@ class Animation extends St.Bin {
 
     _onDestroy() {
         this.stop();
-
-        let themeContext = St.ThemeContext.get_for_stage(global.stage);
-        if (this._scaleChangedId)
-            themeContext.disconnect(this._scaleChangedId);
-        this._scaleChangedId = 0;
     }
 });
 
diff --git a/js/ui/appDisplay.js b/js/ui/appDisplay.js
index 3ff32c9f7a..d8d0ff4f76 100644
--- a/js/ui/appDisplay.js
+++ b/js/ui/appDisplay.js
@@ -330,15 +330,13 @@ var BaseAppView = GObject.registerClass({
 
         // Filter the apps through the user’s parental controls.
         this._parentalControlsManager = ParentalControlsManager.getDefault();
-        this._appFilterChangedId =
-            this._parentalControlsManager.connect('app-filter-changed', () => {
-                this._redisplay();
-            });
+        this._parentalControlsManager.connectObject('app-filter-changed',
+            () => this._redisplay(), this);
 
         // Don't duplicate favorites
         this._appFavorites = AppFavorites.getAppFavorites();
-        this._appFavoritesChangedId =
-            this._appFavorites.connect('changed', () => this._redisplay());
+        this._appFavorites.connectObject('changed',
+            () => this._redisplay(), this);
 
         // Drag n' Drop
         this._lastOvershoot = -1;
@@ -355,16 +353,6 @@ var BaseAppView = GObject.registerClass({
     }
 
     _onDestroy() {
-        if (this._appFilterChangedId > 0) {
-            this._parentalControlsManager.disconnect(this._appFilterChangedId);
-            this._appFilterChangedId = 0;
-        }
-
-        if (this._appFavoritesChangedId > 0) {
-            this._appFavorites.disconnect(this._appFavoritesChangedId);
-            this._appFavoritesChangedId = 0;
-        }
-
         if (this._swipeTracker) {
             this._swipeTracker.destroy();
             delete this._swipeTracker;
@@ -1165,14 +1153,9 @@ var BaseAppView = GObject.registerClass({
         return (translationX - baseOffset) * page;
     }
 
-    _getPagePreviewAdjustment(page) {
-        const previewedPage = this._previewedPages.get(page);
-        return previewedPage?.adjustment;
-    }
-
     _syncClip() {
-        const nextPageAdjustment = this._getPagePreviewAdjustment(1);
-        const prevPageAdjustment = this._getPagePreviewAdjustment(-1);
+        const nextPageAdjustment = this._previewedPages.get(1);
+        const prevPageAdjustment = this._previewedPages.get(-1);
         this._grid.clip_to_view =
             (!prevPageAdjustment || prevPageAdjustment.value === 0) &&
             (!nextPageAdjustment || nextPageAdjustment.value === 0);
@@ -1180,7 +1163,7 @@ var BaseAppView = GObject.registerClass({
 
     _setupPagePreview(page, state) {
         if (this._previewedPages.has(page))
-            return this._previewedPages.get(page).adjustment;
+            return this._previewedPages.get(page);
 
         const adjustment = new St.Adjustment({
             actor: this,
@@ -1191,7 +1174,7 @@ var BaseAppView = GObject.registerClass({
         const indicator = page > 0
             ? this._nextPageIndicator : this._prevPageIndicator;
 
-        const notifyId = adjustment.connect('notify::value', () => {
+        adjustment.connectObject('notify::value', () => {
             const nextPage = this._grid.currentPage + page;
             const hasFollowingPage = nextPage >= 0 &&
                 nextPage < this._grid.nPages;
@@ -1229,23 +1212,20 @@ var BaseAppView = GObject.registerClass({
                 });
             }
             this._syncClip();
-        });
+        }, this);
 
-        this._previewedPages.set(page, {
-            adjustment,
-            notifyId,
-        });
+        this._previewedPages.set(page, adjustment);
 
         return adjustment;
     }
 
     _teardownPagePreview(page) {
-        const previewedPage = this._previewedPages.get(page);
-        if (!previewedPage)
+        const adjustment = this._previewedPages.get(page);
+        if (!adjustment)
             return;
 
-        previewedPage.adjustment.value = 1;
-        previewedPage.adjustment.disconnect(previewedPage.notifyId);
+        adjustment.value = 1;
+        adjustment.disconnectObject(this);
         this._previewedPages.delete(page);
     }
 
@@ -1266,7 +1246,7 @@ var BaseAppView = GObject.registerClass({
             this._prevPageIndicator.remove_style_class_name('dnd');
         }
 
-        adjustment = this._getPagePreviewAdjustment(1);
+        adjustment = this._previewedPages.get(1);
         if (showingNextPage) {
             adjustment = this._setupPagePreview(1, state);
 
@@ -1289,7 +1269,7 @@ var BaseAppView = GObject.registerClass({
             });
         }
 
-        adjustment = this._getPagePreviewAdjustment(-1);
+        adjustment = this._previewedPages.get(-1);
         if (showingPrevPage) {
             adjustment = this._setupPagePreview(-1, state);
 
@@ -1403,7 +1383,6 @@ class AppDisplay extends BaseAppView {
 
         this._currentDialog = null;
         this._displayingDialog = false;
-        this._currentDialogDestroyId = 0;
 
         this._placeholder = null;
 
@@ -1698,19 +1677,14 @@ class AppDisplay extends BaseAppView {
     addFolderDialog(dialog) {
         Main.layoutManager.overviewGroup.add_child(dialog);
         dialog.connect('open-state-changed', (o, isOpen) => {
-            if (this._currentDialog) {
-                this._currentDialog.disconnect(this._currentDialogDestroyId);
-                this._currentDialogDestroyId = 0;
-            }
+            this._currentDialog?.disconnectObject(this);
 
             this._currentDialog = null;
 
             if (isOpen) {
                 this._currentDialog = dialog;
-                this._currentDialogDestroyId = dialog.connect('destroy', () => {
-                    this._currentDialog = null;
-                    this._currentDialogDestroyId = 0;
-                });
+                this._currentDialog.connectObject('destroy',
+                    () => (this._currentDialog = null), this);
             }
             this._displayingDialog = isOpen;
         });
@@ -2418,8 +2392,8 @@ var FolderIcon = GObject.registerClass({
 
         this.view = new FolderView(this._folder, id, parentView);
 
-        this._folderChangedId = this._folder.connect(
-            'changed', this._sync.bind(this));
+        this._folder.connectObject(
+            'changed', this._sync.bind(this), this);
         this._sync();
     }
 
@@ -2430,11 +2404,6 @@ var FolderIcon = GObject.registerClass({
             this._dialog.destroy();
         else
             this.view.destroy();
-
-        if (this._folderChangedId) {
-            this._folder.disconnect(this._folderChangedId);
-            delete this._folderChangedId;
-        }
     }
 
     vfunc_clicked() {
@@ -3110,9 +3079,8 @@ var AppIcon = GObject.registerClass({
         this._menuManager = new PopupMenu.PopupMenuManager(this);
 
         this._menuTimeoutId = 0;
-        this._stateChangedId = this.app.connect('notify::state', () => {
-            this._updateRunningStyle();
-        });
+        this.app.connectObject('notify::state',
+            () => this._updateRunningStyle(), this);
         this._updateRunningStyle();
     }
 
@@ -3123,10 +3091,7 @@ var AppIcon = GObject.registerClass({
             GLib.source_remove(this._folderPreviewId);
             this._folderPreviewId = 0;
         }
-        if (this._stateChangedId > 0)
-            this.app.disconnect(this._stateChangedId);
 
-        this._stateChangedId = 0;
         this._removeMenuTimeout();
     }
 
@@ -3221,12 +3186,8 @@ var AppIcon = GObject.registerClass({
                 if (!isPoppedUp)
                     this._onMenuPoppedDown();
             });
-            let id = Main.overview.connect('hiding', () => {
-                this._menu.close();
-            });
-            this.connect('destroy', () => {
-                Main.overview.disconnect(id);
-            });
+            Main.overview.connectObject('hiding',
+                () => this._menu.close(), this);
 
             Main.uiGroup.add_actor(this._menu.actor);
             this._menuManager.addMenu(this._menu);
diff --git a/js/ui/appMenu.js b/js/ui/appMenu.js
index 671f223f3e..3ee64bf92d 100644
--- a/js/ui/appMenu.js
+++ b/js/ui/appMenu.js
@@ -95,32 +95,25 @@ var AppMenu = class AppMenu extends PopupMenu.PopupMenu {
         this._quitItem =
             this.addAction(_('Quit'), () => this._app.request_quit());
 
-        this._signals = [];
-        this._signals.push([
-            this._appSystem,
-            this._appSystem.connect('installed-changed',
-                () => this._updateDetailsVisibility()),
-        ], [
-            this._appSystem,
-            this._appSystem.connect('app-state-changed',
-                this._onAppStateChanged.bind(this)),
-        ], [
-            this._parentalControlsManager,
-            this._parentalControlsManager.connect('app-filter-changed',
-                () => this._updateFavoriteItem()),
-        ], [
-            this._appFavorites,
-            this._appFavorites.connect('changed',
-                () => this._updateFavoriteItem()),
-        ], [
-            global.settings,
-            global.settings.connect('writable-changed::favorite-apps',
-                () => this._updateFavoriteItem()),
-        ], [
-            global,
-            global.connect('notify::switcheroo-control',
-                () => this._updateGpuItem()),
-        ]);
+        this._appSystem.connectObject(
+            'installed-changed', () => this._updateDetailsVisibility(),
+            'app-state-changed', this._onAppStateChanged.bind(this),
+            this.actor);
+
+        this._parentalControlsManager.connectObject(
+            'app-filter-changed', () => this._updateFavoriteItem(), this.actor);
+
+        this._appFavorites.connectObject(
+            'changed', () => this._updateFavoriteItem(), this.actor);
+
+        global.settings.connectObject(
+            'writable-changed::favorite-apps', () => this._updateFavoriteItem(),
+            this.actor);
+
+        global.connectObject(
+            'notify::switcheroo-control', () => this._updateGpuItem(),
+            this.actor);
+
         this._updateQuitItem();
         this._updateFavoriteItem();
         this._updateGpuItem();
@@ -202,10 +195,6 @@ var AppMenu = class AppMenu extends PopupMenu.PopupMenu {
     destroy() {
         super.destroy();
 
-        for (const [obj, id] of this._signals)
-            obj.disconnect(id);
-        this._signals = [];
-
         this.setApp(null);
     }
 
@@ -225,16 +214,12 @@ var AppMenu = class AppMenu extends PopupMenu.PopupMenu {
         if (this._app === app)
             return;
 
-        if (this._windowsChangedId)
-            this._app.disconnect(this._windowsChangedId);
-        this._windowsChangedId = 0;
+        this._app?.disconnectObject(this);
 
         this._app = app;
 
-        if (app) {
-            this._windowsChangedId = app.connect('windows-changed',
-                () => this._queueUpdateWindowsSection());
-        }
+        this._app?.connectObject('windows-changed',
+            () => this._queueUpdateWindowsSection(), this);
 
         this._updateWindowsSection();
 
@@ -293,10 +278,9 @@ var AppMenu = class AppMenu extends PopupMenu.PopupMenu {
             const item = this._windowSection.addAction(title, event => {
                 Main.activateWindow(window, event.get_time());
             });
-            const id = window.connect('notify::title', () => {
+            window.connectObject('notify::title', () => {
                 item.label.text = window.title || this._app.get_name();
-            });
-            item.connect('destroy', () => window.disconnect(id));
+            }, item);
         });
     }
 };
diff --git a/js/ui/background.js b/js/ui/background.js
index 1b77043b9a..198194a50b 100644
--- a/js/ui/background.js
+++ b/js/ui/background.js
@@ -255,26 +255,25 @@ var Background = GObject.registerClass({
         this._interfaceSettings = new Gio.Settings({ schema_id: INTERFACE_SCHEMA });
 
         this._clock = new GnomeDesktop.WallClock();
-        this._timezoneChangedId = this._clock.connect('notify::timezone',
+        this._clock.connectObject('notify::timezone',
             () => {
                 if (this._animation)
                     this._loadAnimation(this._animation.file);
-            });
+            }, this);
 
         let loginManager = LoginManager.getLoginManager();
-        this._prepareForSleepId = loginManager.connect('prepare-for-sleep',
+        loginManager.connectObject('prepare-for-sleep',
             (lm, aboutToSuspend) => {
                 if (aboutToSuspend)
                     return;
                 this._refreshAnimation();
-            });
+            }, this);
 
-        this._settingsChangedSignalId =
-            this._settings.connect('changed', this._emitChangedSignal.bind(this));
+        this._settings.connectObject('changed',
+            this._emitChangedSignal.bind(this), this);
 
-        this._colorSchemeChangedSignalId =
-            this._interfaceSettings.connect(`changed::${COLOR_SCHEME_KEY}`,
-                this._emitChangedSignal.bind(this));
+        this._interfaceSettings.connectObject(`changed::${COLOR_SCHEME_KEY}`,
+            this._emitChangedSignal.bind(this), this);
 
         this._load();
     }
@@ -290,23 +289,12 @@ var Background = GObject.registerClass({
 
         this._fileWatches = null;
 
-        if (this._timezoneChangedId != 0)
-            this._clock.disconnect(this._timezoneChangedId);
-        this._timezoneChangedId = 0;
-
+        this._clock.disconnectObject(this);
         this._clock = null;
 
-        if (this._prepareForSleepId != 0)
-            LoginManager.getLoginManager().disconnect(this._prepareForSleepId);
-        this._prepareForSleepId = 0;
-
-        if (this._settingsChangedSignalId != 0)
-            this._settings.disconnect(this._settingsChangedSignalId);
-        this._settingsChangedSignalId = 0;
-
-        if (this._colorSchemeChangedSignalId !== 0)
-            this._interfaceSettings.disconnect(this._colorSchemeChangedSignalId);
-        this._colorSchemeChangedSignalId = 0;
+        LoginManager.getLoginManager().disconnectObject(this);
+        this._settings.disconnectObject(this);
+        this._interfaceSettings.disconnectObject(this);
 
         if (this._changedIdleId) {
             GLib.source_remove(this._changedIdleId);
diff --git a/js/ui/backgroundMenu.js b/js/ui/backgroundMenu.js
index c5763d9ad9..4c7372a4b7 100644
--- a/js/ui/backgroundMenu.js
+++ b/js/ui/backgroundMenu.js
@@ -56,14 +56,12 @@ function addBackgroundMenu(actor, layoutManager) {
     });
     actor.add_action(clickAction);
 
-    let grabOpBeginId = global.display.connect('grab-op-begin', () => {
-        clickAction.release();
-    });
+    global.display.connectObject('grab-op-begin',
+        () => clickAction.release(), actor);
 
     actor.connect('destroy', () => {
         actor._backgroundMenu.destroy();
         actor._backgroundMenu = null;
         actor._backgroundManager = null;
-        global.display.disconnect(grabOpBeginId);
     });
 }
diff --git a/js/ui/boxpointer.js b/js/ui/boxpointer.js
index b3b98f83a0..3987d62a27 100644
--- a/js/ui/boxpointer.js
+++ b/js/ui/boxpointer.js
@@ -55,8 +55,6 @@ var BoxPointer = GObject.registerClass({
             else
                 Meta.enable_unredirect_for_display(global.display);
         });
-
-        this.connect('destroy', this._onDestroy.bind(this));
     }
 
     vfunc_captured_event(event) {
@@ -74,13 +72,6 @@ var BoxPointer = GObject.registerClass({
         return Clutter.EVENT_PROPAGATE;
     }
 
-    _onDestroy() {
-        if (this._sourceActorDestroyId) {
-            this._sourceActor.disconnect(this._sourceActorDestroyId);
-            delete this._sourceActorDestroyId;
-        }
-    }
-
     get arrowSide() {
         return this._arrowSide;
     }
@@ -439,19 +430,12 @@ var BoxPointer = GObject.registerClass({
 
     setPosition(sourceActor, alignment) {
         if (!this._sourceActor || sourceActor != this._sourceActor) {
-            if (this._sourceActorDestroyId) {
-                this._sourceActor.disconnect(this._sourceActorDestroyId);
-                delete this._sourceActorDestroyId;
-            }
+            this._sourceActor?.disconnectObject(this);
 
             this._sourceActor = sourceActor;
 
-            if (this._sourceActor) {
-                this._sourceActorDestroyId = this._sourceActor.connect('destroy', () => {
-                    this._sourceActor = null;
-                    delete this._sourceActorDestroyId;
-                });
-            }
+            this._sourceActor?.connectObject('destroy',
+                () => (this._sourceActor = null), this);
         }
 
         this._arrowAlignment = alignment;
diff --git a/js/ui/calendar.js b/js/ui/calendar.js
index 7335c92942..5741faa81b 100644
--- a/js/ui/calendar.js
+++ b/js/ui/calendar.js
@@ -753,14 +753,13 @@ class NotificationMessage extends MessageList.Message {
             if (this.notification)
                 this.notification.destroy(MessageTray.NotificationDestroyedReason.DISMISSED);
         });
-        this._destroyId = notification.connect('destroy', () => {
-            this._disconnectNotificationSignals();
-            this.notification = null;
-            if (!this._closed)
-                this.close();
-        });
-        this._updatedId =
-            notification.connect('updated', this._onUpdated.bind(this));
+        notification.connectObject(
+            'updated', this._onUpdated.bind(this),
+            'destroy', () => {
+                this.notification = null;
+                if (!this._closed)
+                    this.close();
+            }, this);
     }
 
     _getIcon() {
@@ -785,21 +784,6 @@ class NotificationMessage extends MessageList.Message {
         this.notification.activate();
     }
 
-    _onDestroy() {
-        super._onDestroy();
-        this._disconnectNotificationSignals();
-    }
-
-    _disconnectNotificationSignals() {
-        if (this._updatedId)
-            this.notification.disconnect(this._updatedId);
-        this._updatedId = 0;
-
-        if (this._destroyId)
-            this.notification.disconnect(this._destroyId);
-        this._destroyId = 0;
-    }
-
     canClose() {
         return true;
     }
@@ -827,7 +811,6 @@ class NotificationSection extends MessageList.MessageListSection {
     _init() {
         super._init();
 
-        this._sources = new Map();
         this._nUrgent = 0;
 
         Main.messageTray.connect('source-added', this._sourceAdded.bind(this));
@@ -842,18 +825,8 @@ class NotificationSection extends MessageList.MessageListSection {
     }
 
     _sourceAdded(tray, source) {
-        let obj = {
-            destroyId: 0,
-            notificationAddedId: 0,
-        };
-
-        obj.destroyId = source.connect('destroy', () => {
-            this._onSourceDestroy(source, obj);
-        });
-        obj.notificationAddedId = source.connect('notification-added',
-            this._onNotificationAdded.bind(this));
-
-        this._sources.set(source, obj);
+        source.connectObject('notification-added',
+            this._onNotificationAdded.bind(this), this);
     }
 
     _onNotificationAdded(source, notification) {
@@ -862,16 +835,15 @@ class NotificationSection extends MessageList.MessageListSection {
 
         let isUrgent = notification.urgency == MessageTray.Urgency.CRITICAL;
 
-        let updatedId = notification.connect('updated', () => {
-            message.setSecondaryActor(new TimeLabel(notification.datetime));
-            this.moveMessage(message, isUrgent ? 0 : this._nUrgent, this.mapped);
-        });
-        let destroyId = notification.connect('destroy', () => {
-            notification.disconnect(destroyId);
-            notification.disconnect(updatedId);
-            if (isUrgent)
-                this._nUrgent--;
-        });
+        notification.connectObject(
+            'destroy', () => {
+                if (isUrgent)
+                    this._nUrgent--;
+            },
+            'updated', () => {
+                message.setSecondaryActor(new TimeLabel(notification.datetime));
+                this.moveMessage(message, isUrgent ? 0 : this._nUrgent, this.mapped);
+            }, this);
 
         if (isUrgent) {
             // Keep track of urgent notifications to keep them on top
@@ -887,13 +859,6 @@ class NotificationSection extends MessageList.MessageListSection {
         this.addMessageAtIndex(message, index, this.mapped);
     }
 
-    _onSourceDestroy(source, obj) {
-        source.disconnect(obj.destroyId);
-        source.disconnect(obj.notificationAddedId);
-
-        this._sources.delete(source);
-    }
-
     vfunc_map() {
         this._messages.forEach(message => {
             if (message.notification.urgency != MessageTray.Urgency.CRITICAL)
@@ -1025,21 +990,14 @@ class CalendarMessageList extends St.Widget {
     }
 
     _addSection(section) {
-        let connectionsIds = [];
-
-        for (let prop of ['visible', 'empty', 'can-clear']) {
-            connectionsIds.push(
-                section.connect(`notify::${prop}`, this._sync.bind(this)));
-        }
-        connectionsIds.push(section.connect('message-focused', (_s, messageActor) => {
-            Util.ensureActorVisibleInScrollView(this._scrollView, messageActor);
-        }));
-
-        connectionsIds.push(section.connect('destroy', () => {
-            connectionsIds.forEach(id => section.disconnect(id));
-            this._sectionList.remove_actor(section);
-        }));
-
+        section.connectObject(
+            'notify::visible', this._sync.bind(this),
+            'notify::empty', this._sync.bind(this),
+            'notify::can-clear', this._sync.bind(this),
+            'destroy', () => this._sectionList.remove_actor(section),
+            'message-focused', (_s, messageActor) => {
+                Util.ensureActorVisibleInScrollView(this._scrollView, messageActor);
+            }, this);
         this._sectionList.add_actor(section);
     }
 
diff --git a/js/ui/closeDialog.js b/js/ui/closeDialog.js
index 317d775933..f5ddecd2b3 100644
--- a/js/ui/closeDialog.js
+++ b/js/ui/closeDialog.js
@@ -22,8 +22,6 @@ var CloseDialog = GObject.registerClass({
         this._dialog = null;
         this._tracked = undefined;
         this._timeoutId = 0;
-        this._windowFocusChangedId = 0;
-        this._keyFocusChangedId = 0;
     }
 
     get window() {
@@ -155,13 +153,11 @@ var CloseDialog = GObject.registerClass({
                 return GLib.SOURCE_CONTINUE;
             });
 
-        this._windowFocusChangedId =
-            global.display.connect('notify::focus-window',
-                                   this._onFocusChanged.bind(this));
+        global.display.connectObject(
+            'notify::focus-window', this._onFocusChanged.bind(this), this);
 
-        this._keyFocusChangedId =
-            global.stage.connect('notify::key-focus',
-                                 this._onFocusChanged.bind(this));
+        global.stage.connectObject(
+            'notify::key-focus', this._onFocusChanged.bind(this), this);
 
         this._addWindowEffect();
         this._initDialog();
@@ -186,11 +182,8 @@ var CloseDialog = GObject.registerClass({
         GLib.source_remove(this._timeoutId);
         this._timeoutId = 0;
 
-        global.display.disconnect(this._windowFocusChangedId);
-        this._windowFocusChangedId = 0;
-
-        global.stage.disconnect(this._keyFocusChangedId);
-        this._keyFocusChangedId = 0;
+        global.display.disconnectObject(this);
+        global.stage.disconnectObject(this);
 
         this._dialog._dialog.remove_all_transitions();
 
diff --git a/js/ui/components/automountManager.js b/js/ui/components/automountManager.js
index 762e2befc0..85d0644937 100644
--- a/js/ui/components/automountManager.js
+++ b/js/ui/components/automountManager.js
@@ -31,22 +31,19 @@ var AutomountManager = class {
     }
 
     enable() {
-        this._volumeAddedId = this._volumeMonitor.connect('volume-added', this._onVolumeAdded.bind(this));
-        this._volumeRemovedId = this._volumeMonitor.connect('volume-removed', 
this._onVolumeRemoved.bind(this));
-        this._driveConnectedId = this._volumeMonitor.connect('drive-connected', 
this._onDriveConnected.bind(this));
-        this._driveDisconnectedId = this._volumeMonitor.connect('drive-disconnected', 
this._onDriveDisconnected.bind(this));
-        this._driveEjectButtonId = this._volumeMonitor.connect('drive-eject-button', 
this._onDriveEjectButton.bind(this));
+        this._volumeMonitor.connectObject(
+            'volume-added', this._onVolumeAdded.bind(this),
+            'volume-removed', this._onVolumeRemoved.bind(this),
+            'drive-connected', this._onDriveConnected.bind(this),
+            'drive-disconnected', this._onDriveDisconnected.bind(this),
+            'drive-eject-button', this._onDriveEjectButton.bind(this), this);
 
         this._mountAllId = GLib.idle_add(GLib.PRIORITY_DEFAULT, this._startupMountAll.bind(this));
         GLib.Source.set_name_by_id(this._mountAllId, '[gnome-shell] this._startupMountAll');
     }
 
     disable() {
-        this._volumeMonitor.disconnect(this._volumeAddedId);
-        this._volumeMonitor.disconnect(this._volumeRemovedId);
-        this._volumeMonitor.disconnect(this._driveConnectedId);
-        this._volumeMonitor.disconnect(this._driveDisconnectedId);
-        this._volumeMonitor.disconnect(this._driveEjectButtonId);
+        this._volumeMonitor.disconnectObject(this);
 
         if (this._mountAllId > 0) {
             GLib.source_remove(this._mountAllId);
diff --git a/js/ui/components/autorunManager.js b/js/ui/components/autorunManager.js
index df7cd42212..c4a0dd2ec7 100644
--- a/js/ui/components/autorunManager.js
+++ b/js/ui/components/autorunManager.js
@@ -151,13 +151,13 @@ var AutorunManager = class {
     }
 
     enable() {
-        this._mountAddedId = this._volumeMonitor.connect('mount-added', this._onMountAdded.bind(this));
-        this._mountRemovedId = this._volumeMonitor.connect('mount-removed', this._onMountRemoved.bind(this));
+        this._volumeMonitor.connectObject(
+            'mount-added', this._onMountAdded.bind(this),
+            'mount-removed', this._onMountRemoved.bind(this), this);
     }
 
     disable() {
-        this._volumeMonitor.disconnect(this._mountAddedId);
-        this._volumeMonitor.disconnect(this._mountRemovedId);
+        this._volumeMonitor.disconnectObject(this);
     }
 
     _onMountAdded(monitor, mount) {
diff --git a/js/ui/components/polkitAgent.js b/js/ui/components/polkitAgent.js
index 4f416c27c5..74b89090c0 100644
--- a/js/ui/components/polkitAgent.js
+++ b/js/ui/components/polkitAgent.js
@@ -32,9 +32,9 @@ var AuthenticationDialog = GObject.registerClass({
         this.message = description;
         this.userNames = userNames;
 
-        this._sessionUpdatedId = Main.sessionMode.connect('updated', () => {
+        Main.sessionMode.connectObject('updated', () => {
             this.visible = !Main.sessionMode.isLocked;
-        });
+        }, this);
 
         this.connect('closed', this._onDialogClosed.bind(this));
 
@@ -164,10 +164,9 @@ var AuthenticationDialog = GObject.registerClass({
         this._identityToAuth = Polkit.UnixUser.new_for_name(userName);
         this._cookie = cookie;
 
-        this._userLoadedId = this._user.connect('notify::is-loaded',
-            this._onUserChanged.bind(this));
-        this._userChangedId = this._user.connect('changed',
-            this._onUserChanged.bind(this));
+        this._user.connectObject(
+            'notify::is-loaded', this._onUserChanged.bind(this),
+            'changed', this._onUserChanged.bind(this), this);
         this._onUserChanged();
     }
 
@@ -178,10 +177,11 @@ var AuthenticationDialog = GObject.registerClass({
             identity: this._identityToAuth,
             cookie: this._cookie,
         });
-        this._sessionCompletedId = this._session.connect('completed', this._onSessionCompleted.bind(this));
-        this._sessionRequestId = this._session.connect('request', this._onSessionRequest.bind(this));
-        this._sessionShowErrorId = this._session.connect('show-error', this._onSessionShowError.bind(this));
-        this._sessionShowInfoId = this._session.connect('show-info', this._onSessionShowInfo.bind(this));
+        this._session.connectObject(
+            'completed', this._onSessionCompleted.bind(this),
+            'request', this._onSessionRequest.bind(this),
+            'show-error', this._onSessionShowError.bind(this),
+            'show-info', this._onSessionShowInfo.bind(this), this);
         this._session.initiate();
     }
 
@@ -314,18 +314,13 @@ var AuthenticationDialog = GObject.registerClass({
     }
 
     _destroySession(delay = 0) {
-        if (this._session) {
-            this._session.disconnect(this._sessionCompletedId);
-            this._session.disconnect(this._sessionRequestId);
-            this._session.disconnect(this._sessionShowErrorId);
-            this._session.disconnect(this._sessionShowInfoId);
+        this._session?.disconnectObject(this);
 
-            if (!this._completed)
-                this._session.cancel();
+        if (!this._completed)
+            this._session?.cancel();
 
-            this._completed = false;
-            this._session = null;
-        }
+        this._completed = false;
+        this._session = null;
 
         if (this._sessionRequestTimeoutId) {
             GLib.source_remove(this._sessionRequestTimeoutId);
@@ -401,18 +396,14 @@ var AuthenticationDialog = GObject.registerClass({
     }
 
     _onDialogClosed() {
-        if (this._sessionUpdatedId)
-            Main.sessionMode.disconnect(this._sessionUpdatedId);
+        Main.sessionMode.disconnectObject(this);
 
         if (this._sessionRequestTimeoutId)
             GLib.source_remove(this._sessionRequestTimeoutId);
         this._sessionRequestTimeoutId = 0;
 
-        if (this._user) {
-            this._user.disconnect(this._userLoadedId);
-            this._user.disconnect(this._userChangedId);
-            this._user = null;
-        }
+        this._user?.disconnectObject(this);
+        this._user = null;
 
         this._destroySession();
     }
@@ -448,12 +439,11 @@ class AuthenticationAgent extends Shell.PolkitAuthenticationAgent {
     _onInitiate(nativeAgent, actionId, message, iconName, cookie, userNames) {
         // Don't pop up a dialog while locked
         if (Main.sessionMode.isLocked) {
-            this._sessionUpdatedId = Main.sessionMode.connect('updated', () => {
-                Main.sessionMode.disconnect(this._sessionUpdatedId);
-                this._sessionUpdatedId = 0;
+            Main.sessionMode.connectObject('updated', () => {
+                Main.sessionMode.disconnectObject(this);
 
                 this._onInitiate(nativeAgent, actionId, message, iconName, cookie, userNames);
-            });
+            }, this);
             return;
         }
 
@@ -473,9 +463,7 @@ class AuthenticationAgent extends Shell.PolkitAuthenticationAgent {
         this._currentDialog.close();
         this._currentDialog = null;
 
-        if (this._sessionUpdatedId)
-            Main.sessionMode.disconnect(this._sessionUpdatedId);
-        this._sessionUpdatedId = 0;
+        Main.sessionMode.disconnectObject(this);
 
         this.complete(dismissed);
     }
diff --git a/js/ui/components/telepathyClient.js b/js/ui/components/telepathyClient.js
index 1252654fe3..d317822451 100644
--- a/js/ui/components/telepathyClient.js
+++ b/js/ui/components/telepathyClient.js
@@ -316,19 +316,21 @@ class ChatSource extends MessageTray.Source {
 
         this._conn = conn;
         this._channel = channel;
-        this._closedId = this._channel.connect('invalidated', this._channelClosed.bind(this));
 
         this._notifyTimeoutId = 0;
 
         this._presence = contact.get_presence_type();
 
-        this._sentId = this._channel.connect('message-sent', this._messageSent.bind(this));
-        this._receivedId = this._channel.connect('message-received', this._messageReceived.bind(this));
-        this._pendingId = this._channel.connect('pending-message-removed', this._pendingRemoved.bind(this));
+        this._channel.connectObject(
+            'invalidated', this._channelClosed.bind(this),
+            'message-sent', this._messageSent.bind(this),
+            'message-received', this._messageReceived.bind(this),
+            'pending-message-removed', this._pendingRemoved.bind(this), this);
 
-        this._notifyAliasId = this._contact.connect('notify::alias', this._updateAlias.bind(this));
-        this._notifyAvatarId = this._contact.connect('notify::avatar-file', 
this._updateAvatarIcon.bind(this));
-        this._presenceChangedId = this._contact.connect('presence-changed', 
this._presenceChanged.bind(this));
+        this._contact.connectObject(
+            'notify::alias', this._updateAlias.bind(this),
+            'notify::avatar-file', this._updateAvatarIcon.bind(this),
+            'presence-changed', this._presenceChanged.bind(this), this);
 
         // Add ourselves as a source.
         Main.messageTray.add(this);
@@ -341,14 +343,13 @@ class ChatSource extends MessageTray.Source {
             return;
 
         this._notification = new ChatNotification(this);
-        this._notification.connect('activated', this.open.bind(this));
-        this._notification.connect('updated', () => {
-            if (this._banner && this._banner.expanded)
-                this._ackMessages();
-        });
-        this._notification.connect('destroy', () => {
-            this._notification = null;
-        });
+        this._notification.connectObject(
+            'activated', this.open.bind(this),
+            'destroy', () => (this._notification = null),
+            'updated', () => {
+                if (this._banner && this._banner.expanded)
+                    this._ackMessages();
+            }, this);
         this.pushNotification(this._notification);
     }
 
@@ -362,11 +363,9 @@ class ChatSource extends MessageTray.Source {
         this._banner = new ChatNotificationBanner(this._notification);
 
         // We ack messages when the user expands the new notification
-        let id = this._banner.connect('expanded', this._ackMessages.bind(this));
-        this._banner.connect('destroy', () => {
-            this._banner.disconnect(id);
-            this._banner = null;
-        });
+        this._banner.connectObject(
+            'expanded', this._ackMessages.bind(this),
+            'destroy', () => (this._banner = null), this);
 
         return this._banner;
     }
@@ -535,14 +534,8 @@ class ChatSource extends MessageTray.Source {
             return;
 
         this._destroyed = true;
-        this._channel.disconnect(this._closedId);
-        this._channel.disconnect(this._receivedId);
-        this._channel.disconnect(this._pendingId);
-        this._channel.disconnect(this._sentId);
-
-        this._contact.disconnect(this._notifyAliasId);
-        this._contact.disconnect(this._notifyAvatarId);
-        this._contact.disconnect(this._presenceChangedId);
+        this._channel.disconnectObject(this);
+        this._contact.disconnectObject(this);
 
         super.destroy(reason);
     }
@@ -907,32 +900,19 @@ class ChatNotificationBanner extends MessageTray.NotificationBanner {
 
         this._messageActors = new Map();
 
-        this._messageAddedId = this.notification.connect('message-added',
-            (n, message) => {
-                this._addMessage(message);
-            });
-        this._messageRemovedId = this.notification.connect('message-removed',
-            (n, message) => {
+        this.notification.connectObject(
+            'timestamp-changed', (n, message) => this._updateTimestamp(message),
+            'message-added', (n, message) => this._addMessage(message),
+            'message-removed', (n, message) => {
                 let actor = this._messageActors.get(message);
                 if (this._messageActors.delete(message))
                     actor.destroy();
-            });
-        this._timestampChangedId = this.notification.connect('timestamp-changed',
-            (n, message) => {
-                this._updateTimestamp(message);
-            });
+            }, this);
 
         for (let i = this.notification.messages.length - 1; i >= 0; i--)
             this._addMessage(this.notification.messages[i]);
     }
 
-    _onDestroy() {
-        super._onDestroy();
-        this.notification.disconnect(this._messageAddedId);
-        this.notification.disconnect(this._messageRemovedId);
-        this.notification.disconnect(this._timestampChangedId);
-    }
-
     scrollTo(side) {
         let adjustment = this._scrollArea.vscroll.adjustment;
         if (side == St.Side.TOP)
diff --git a/js/ui/dash.js b/js/ui/dash.js
index 24d3a8907e..a40377cf7b 100644
--- a/js/ui/dash.js
+++ b/js/ui/dash.js
@@ -487,13 +487,10 @@ var Dash = GObject.registerClass({
             item.hideLabel();
         });
 
-        let id = Main.overview.connect('hiding', () => {
+        Main.overview.connectObject('hiding', () => {
             this._labelShowing = false;
             item.hideLabel();
-        });
-        item.child.connect('destroy', () => {
-            Main.overview.disconnect(id);
-        });
+        }, item.child);
 
         if (appIcon) {
             appIcon.connect('sync-tooltip', () => {
diff --git a/js/ui/dialog.js b/js/ui/dialog.js
index a62a3a35b0..1ae27a1345 100644
--- a/js/ui/dialog.js
+++ b/js/ui/dialog.js
@@ -20,7 +20,6 @@ class Dialog extends St.Widget {
         this.connect('destroy', this._onDestroy.bind(this));
 
         this._initialKeyFocus = null;
-        this._initialKeyFocusDestroyId = 0;
         this._pressedKey = null;
         this._buttonKeys = {};
         this._createDialog();
@@ -61,9 +60,7 @@ class Dialog extends St.Widget {
     }
 
     makeInactive() {
-        if (this._eventId != 0)
-            this._parentActor.disconnect(this._eventId);
-        this._eventId = 0;
+        this._parentActor.disconnectObject(this);
 
         this.buttonLayout.get_children().forEach(c => c.set_reactive(false));
     }
@@ -99,15 +96,12 @@ class Dialog extends St.Widget {
     }
 
     _setInitialKeyFocus(actor) {
-        if (this._initialKeyFocus)
-            this._initialKeyFocus.disconnect(this._initialKeyFocusDestroyId);
+        this._initialKeyFocus?.disconnectObject(this);
 
         this._initialKeyFocus = actor;
 
-        this._initialKeyFocusDestroyId = actor.connect('destroy', () => {
-            this._initialKeyFocus = null;
-            this._initialKeyFocusDestroyId = 0;
-        });
+        actor.connectObject('destroy',
+            () => (this._initialKeyFocus = null), this);
     }
 
     get initialKeyFocus() {
diff --git a/js/ui/endSessionDialog.js b/js/ui/endSessionDialog.js
index 94e0bef248..b0f71f5cff 100644
--- a/js/ui/endSessionDialog.js
+++ b/js/ui/endSessionDialog.js
@@ -269,13 +269,12 @@ class EndSessionDialog extends ModalDialog.ModalDialog {
         this._rebootButton = null;
         this._rebootButtonAlt = null;
 
-        this.connect('destroy',
-                     this._onDestroy.bind(this));
         this.connect('opened',
                      this._onOpened.bind(this));
 
-        this._userLoadedId = this._user.connect('notify::is-loaded', this._sync.bind(this));
-        this._userChangedId = this._user.connect('changed', this._sync.bind(this));
+        this._user.connectObject(
+            'notify::is-loaded', this._sync.bind(this),
+            'changed', this._sync.bind(this), this);
 
         this._messageDialogContent = new Dialog.MessageDialogContent();
 
@@ -330,11 +329,6 @@ class EndSessionDialog extends ModalDialog.ModalDialog {
         }
     }
 
-    _onDestroy() {
-        this._user.disconnect(this._userLoadedId);
-        this._user.disconnect(this._userChangedId);
-    }
-
     _isDischargingBattery() {
         return this._powerProxy.IsPresent &&
             this._powerProxy.State !== UPower.DeviceState.CHARGING &&
diff --git a/js/ui/iconGrid.js b/js/ui/iconGrid.js
index 0bba7e9aec..362cfda915 100644
--- a/js/ui/iconGrid.js
+++ b/js/ui/iconGrid.js
@@ -67,8 +67,6 @@ class BaseIcon extends Shell.SquareBin {
 
         super._init({ style_class: styleClass });
 
-        this.connect('destroy', this._onDestroy.bind(this));
-
         this._box = new St.BoxLayout({
             vertical: true,
             x_expand: true,
@@ -99,7 +97,8 @@ class BaseIcon extends Shell.SquareBin {
         this.icon = null;
 
         let cache = St.TextureCache.get_default();
-        this._iconThemeChangedId = cache.connect('icon-theme-changed', this._onIconThemeChanged.bind(this));
+        cache.connectObject(
+            'icon-theme-changed', this._onIconThemeChanged.bind(this), this);
     }
 
     // This can be overridden by a subclass, or by the createIcon
@@ -148,14 +147,6 @@ class BaseIcon extends Shell.SquareBin {
         this._createIconTexture(size);
     }
 
-    _onDestroy() {
-        if (this._iconThemeChangedId > 0) {
-            let cache = St.TextureCache.get_default();
-            cache.disconnect(this._iconThemeChangedId);
-            this._iconThemeChangedId = 0;
-        }
-    }
-
     _onIconThemeChanged() {
         this._createIconTexture(this.iconSize);
     }
@@ -690,13 +681,12 @@ var IconGridLayout = GObject.registerClass({
     }
 
     vfunc_set_container(container) {
-        if (this._container)
-            this._container.disconnect(this._containerDestroyedId);
+        this._container?.disconnectObject(this);
 
         this._container = container;
 
         if (this._container)
-            this._containerDestroyedId = this._container.connect('destroy', this._onDestroy.bind(this));
+            this._container.connectObject('destroy', this._onDestroy.bind(this), this);
     }
 
     vfunc_get_preferred_width(_container, _forHeight) {
diff --git a/js/ui/keyboard.js b/js/ui/keyboard.js
index 69afc08b16..6512bf0d23 100644
--- a/js/ui/keyboard.js
+++ b/js/ui/keyboard.js
@@ -250,12 +250,10 @@ var LanguageSelectionPopup = class extends PopupMenu.PopupMenu {
         item = this.addSettingsAction(_("Region & Language Settings"), 'gnome-region-panel.desktop');
         item.can_focus = false;
 
-        this._capturedEventId = 0;
-
-        this._unmapId = actor.connect('notify::mapped', () => {
+        actor.connectObject('notify::mapped', () => {
             if (!actor.is_mapped())
                 this.close(true);
-        });
+        }, this);
     }
 
     _onCapturedEvent(actor, event) {
@@ -273,23 +271,18 @@ var LanguageSelectionPopup = class extends PopupMenu.PopupMenu {
 
     open(animate) {
         super.open(animate);
-        this._capturedEventId = global.stage.connect('captured-event',
-                                                     this._onCapturedEvent.bind(this));
+        global.stage.connectObject(
+            'captured-event', this._onCapturedEvent.bind(this), this);
     }
 
     close(animate) {
         super.close(animate);
-        if (this._capturedEventId != 0) {
-            global.stage.disconnect(this._capturedEventId);
-            this._capturedEventId = 0;
-        }
+        global.stage.disconnectObject(this);
     }
 
     destroy() {
-        if (this._capturedEventId != 0)
-            global.stage.disconnect(this._capturedEventId);
-        if (this._unmapId != 0)
-            this.sourceActor.disconnect(this._unmapId);
+        global.stage.disconnectObject(this);
+        this.sourceActor.disconnectObject(this);
         super.destroy();
     }
 };
@@ -318,9 +311,6 @@ var Key = GObject.registerClass({
         this._extendedKeyboard = null;
         this._pressTimeoutId = 0;
         this._capturedPress = false;
-
-        this._capturedEventId = 0;
-        this._unmapId = 0;
     }
 
     _onDestroy() {
@@ -425,25 +415,19 @@ var Key = GObject.registerClass({
 
     _showSubkeys() {
         this._boxPointer.open(BoxPointer.PopupAnimation.FULL);
-        this._capturedEventId = global.stage.connect('captured-event',
-                                                     this._onCapturedEvent.bind(this));
-        this._unmapId = this.keyButton.connect('notify::mapped', () => {
+        global.stage.connectObject(
+            'captured-event', this._onCapturedEvent.bind(this), this);
+        this.keyButton.connectObject('notify::mapped', () => {
             if (!this.keyButton.is_mapped())
                 this._hideSubkeys();
-        });
+        }, this);
     }
 
     _hideSubkeys() {
         if (this._boxPointer)
             this._boxPointer.close(BoxPointer.PopupAnimation.FULL);
-        if (this._capturedEventId) {
-            global.stage.disconnect(this._capturedEventId);
-            this._capturedEventId = 0;
-        }
-        if (this._unmapId) {
-            this.keyButton.disconnect(this._unmapId);
-            this._unmapId = 0;
-        }
+        global.stage.disconnectObject(this);
+        this.keyButton.disconnectObject(this);
         this._capturedPress = false;
     }
 
@@ -581,28 +565,26 @@ var FocusTracker = class {
     constructor() {
         this._rect = null;
 
-        this._notifyFocusId = global.display.connect('notify::focus-window', () => {
-            this._setCurrentWindow(global.display.focus_window);
-            this.emit('window-changed', this._currentWindow);
-        });
+        global.display.connectObject(
+            'notify::focus-window', () => {
+                this._setCurrentWindow(global.display.focus_window);
+                this.emit('window-changed', this._currentWindow);
+            },
+            'grab-op-begin', (display, window, op) => {
+                if (window === this._currentWindow &&
+                    (op === Meta.GrabOp.MOVING || op === Meta.GrabOp.KEYBOARD_MOVING))
+                    this.emit('window-grabbed');
+            }, this);
 
         this._setCurrentWindow(global.display.focus_window);
 
-        this._grabOpBeginId = global.display.connect('grab-op-begin', (display, window, op) => {
-            if (window == this._currentWindow &&
-                (op == Meta.GrabOp.MOVING || op == Meta.GrabOp.KEYBOARD_MOVING))
-                this.emit('window-grabbed');
-        });
-
         /* Valid for wayland clients */
-        this._cursorLocationChangedId =
-            Main.inputMethod.connect('cursor-location-changed', (o, rect) => {
-                this._setCurrentRect(rect);
-            });
+        Main.inputMethod.connectObject('cursor-location-changed',
+            (o, rect) => this._setCurrentRect(rect), this);
 
         this._ibusManager = IBusManager.getIBusManager();
-        this._setCursorLocationId =
-            this._ibusManager.connect('set-cursor-location', (manager, rect) => {
+        this._ibusManager.connectObject(
+            'set-cursor-location', (manager, rect) => {
                 /* Valid for X11 clients only */
                 if (Main.inputMethod.currentFocus)
                     return;
@@ -611,27 +593,17 @@ var FocusTracker = class {
                 grapheneRect.init(rect.x, rect.y, rect.width, rect.height);
 
                 this._setCurrentRect(grapheneRect);
-            });
-        this._focusInId = this._ibusManager.connect('focus-in', () => {
-            this.emit('focus-changed', true);
-        });
-        this._focusOutId = this._ibusManager.connect('focus-out', () => {
-            this.emit('focus-changed', false);
-        });
+            },
+            'focus-in', () => this.emit('focus-changed', true),
+            'focus-out', () => this.emit('focus-changed', false),
+            this);
     }
 
     destroy() {
-        if (this._currentWindow) {
-            this._currentWindow.disconnect(this._currentWindowPositionChangedId);
-            delete this._currentWindowPositionChangedId;
-        }
-
-        global.display.disconnect(this._notifyFocusId);
-        global.display.disconnect(this._grabOpBeginId);
-        Main.inputMethod.disconnect(this._cursorLocationChangedId);
-        this._ibusManager.disconnect(this._setCursorLocationId);
-        this._ibusManager.disconnect(this._focusInId);
-        this._ibusManager.disconnect(this._focusOutId);
+        this._currentWindow?.disconnectObject(this);
+        global.display.disconnectObject(this);
+        Main.inputMethod.disconnectObject(this);
+        this._ibusManager.disconnectObject(this);
     }
 
     get currentWindow() {
@@ -639,17 +611,13 @@ var FocusTracker = class {
     }
 
     _setCurrentWindow(window) {
-        if (this._currentWindow) {
-            this._currentWindow.disconnect(this._currentWindowPositionChangedId);
-            delete this._currentWindowPositionChangedId;
-        }
+        this._currentWindow?.disconnectObject(this);
 
         this._currentWindow = window;
 
         if (this._currentWindow) {
-            this._currentWindowPositionChangedId =
-                this._currentWindow.connect('position-changed', () =>
-                    this.emit('window-moved'));
+            this._currentWindow.connectObject(
+                'position-changed', () => this.emit('window-moved'), this);
         }
     }
 
@@ -1325,22 +1293,21 @@ var Keyboard = GObject.registerClass({
         this._emojiKeyVisible = Meta.is_wayland_compositor();
 
         this._focusTracker = new FocusTracker();
-        this._connectSignal(this._focusTracker, 'position-changed',
-            this._onFocusPositionChanged.bind(this));
-        this._connectSignal(this._focusTracker, 'window-grabbed',
-            this._onFocusWindowMoving.bind(this));
+        this._focusTracker.connectObject(
+            'position-changed', this._onFocusPositionChanged.bind(this),
+            'window-grabbed', this._onFocusWindowMoving.bind(this), this);
 
         this._windowMovedId = this._focusTracker.connect('window-moved',
             this._onFocusWindowMoving.bind(this));
 
         // Valid only for X11
         if (!Meta.is_wayland_compositor()) {
-            this._connectSignal(this._focusTracker, 'focus-changed', (_tracker, focused) => {
+            this._focusTracker.connectObject('focus-changed', (_tracker, focused) => {
                 if (focused)
                     this.open(Main.layoutManager.focusIndex);
                 else
                     this.close();
-            });
+            }, this);
         }
 
         this._showIdleId = 0;
@@ -1349,22 +1316,14 @@ var Keyboard = GObject.registerClass({
         this._keyboardRequested = false;
         this._keyboardRestingId = 0;
 
-        this._connectSignal(Main.layoutManager, 'monitors-changed', this._relayout.bind(this));
+        Main.layoutManager.connectObject('monitors-changed',
+            this._relayout.bind(this), this);
 
         this._setupKeyboard();
 
         this.connect('destroy', this._onDestroy.bind(this));
     }
 
-    _connectSignal(obj, signal, callback) {
-        if (!this._connectionsIDs)
-            this._connectionsIDs = [];
-
-        let id = obj.connect(signal, callback);
-        this._connectionsIDs.push([obj, id]);
-        return id;
-    }
-
     get visible() {
         return this._keyboardVisible && super.visible;
     }
@@ -1389,10 +1348,6 @@ var Keyboard = GObject.registerClass({
             delete this._focusTracker;
         }
 
-        for (let [obj, id] of this._connectionsIDs)
-            obj.disconnect(id);
-        delete this._connectionsIDs;
-
         this._clearShowIdle();
 
         this._keyboardController.destroy();
@@ -1436,10 +1391,10 @@ var Keyboard = GObject.registerClass({
         this._emojiSelection.hide();
 
         this._keypad = new Keypad();
-        this._connectSignal(this._keypad, 'keyval', (_keypad, keyval) => {
+        this._keypad.connectObject('keyval', (_keypad, keyval) => {
             this._keyboardController.keyvalPress(keyval);
             this._keyboardController.keyvalRelease(keyval);
-        });
+        }, this);
         this._aspectContainer.add_child(this._keypad);
         this._keypad.hide();
         this._keypadVisible = false;
@@ -1452,20 +1407,18 @@ var Keyboard = GObject.registerClass({
         // keyboard on RTL locales.
         this.text_direction = Clutter.TextDirection.LTR;
 
-        this._connectSignal(this._keyboardController, 'active-group',
-            this._onGroupChanged.bind(this));
-        this._connectSignal(this._keyboardController, 'groups-changed',
-            this._onKeyboardGroupsChanged.bind(this));
-        this._connectSignal(this._keyboardController, 'panel-state',
-            this._onKeyboardStateChanged.bind(this));
-        this._connectSignal(this._keyboardController, 'keypad-visible',
-            this._onKeypadVisible.bind(this));
-        this._connectSignal(global.stage, 'notify::key-focus',
-            this._onKeyFocusChanged.bind(this));
+        this._keyboardController.connectObject(
+            'active-group', this._onGroupChanged.bind(this),
+            'groups-changed', this._onKeyboardGroupsChanged.bind(this),
+            'panel-state', this._onKeyboardStateChanged.bind(this),
+            'keypad-visible', this._onKeypadVisible.bind(this),
+            this);
+        global.stage.connectObject('notify::key-focus',
+            this._onKeyFocusChanged.bind(this), this);
 
         if (Meta.is_wayland_compositor()) {
-            this._connectSignal(this._keyboardController, 'emoji-visible',
-                this._onEmojiKeyVisible.bind(this));
+            this._keyboardController.connectObject('emoji-visible',
+                this._onEmojiKeyVisible.bind(this), this);
         }
 
         this._relayout();
@@ -2079,26 +2032,20 @@ var KeyboardController = class {
         this._virtualDevice = seat.create_virtual_device(Clutter.InputDeviceType.KEYBOARD_DEVICE);
 
         this._inputSourceManager = InputSourceManager.getInputSourceManager();
-        this._sourceChangedId = this._inputSourceManager.connect('current-source-changed',
-                                                                 this._onSourceChanged.bind(this));
-        this._sourcesModifiedId = this._inputSourceManager.connect('sources-changed',
-                                                                   this._onSourcesModified.bind(this));
+        this._inputSourceManager.connectObject(
+            'current-source-changed', this._onSourceChanged.bind(this),
+            'sources-changed', this._onSourcesModified.bind(this), this);
         this._currentSource = this._inputSourceManager.currentSource;
 
-        this._notifyContentPurposeId = Main.inputMethod.connect(
-            'notify::content-purpose', this._onContentPurposeHintsChanged.bind(this));
-        this._notifyContentHintsId = Main.inputMethod.connect(
-            'notify::content-hints', this._onContentPurposeHintsChanged.bind(this));
-        this._notifyInputPanelStateId = Main.inputMethod.connect(
-            'input-panel-state', (o, state) => this.emit('panel-state', state));
+        Main.inputMethod.connectObject(
+            'notify::content-purpose', this._onContentPurposeHintsChanged.bind(this),
+            'notify::content-hints', this._onContentPurposeHintsChanged.bind(this),
+            'input-panel-state', (o, state) => this.emit('panel-state', state), this);
     }
 
     destroy() {
-        this._inputSourceManager.disconnect(this._sourceChangedId);
-        this._inputSourceManager.disconnect(this._sourcesModifiedId);
-        Main.inputMethod.disconnect(this._notifyContentPurposeId);
-        Main.inputMethod.disconnect(this._notifyContentHintsId);
-        Main.inputMethod.disconnect(this._notifyInputPanelStateId);
+        this._inputSourceManager.disconnectObject(this);
+        Main.inputMethod.disconnectObject(this);
 
         // Make sure any buttons pressed by the virtual device are released
         // immediately instead of waiting for the next GC cycle
diff --git a/js/ui/layout.js b/js/ui/layout.js
index d03a72dbda..fe0983174b 100644
--- a/js/ui/layout.js
+++ b/js/ui/layout.js
@@ -891,12 +891,10 @@ var LayoutManager = GObject.registerClass({
 
         let actorData = Params.parse(params, defaultParams);
         actorData.actor = actor;
-        actorData.visibleId = actor.connect('notify::visible',
-                                            this._queueUpdateRegions.bind(this));
-        actorData.allocationId = actor.connect('notify::allocation',
-                                               this._queueUpdateRegions.bind(this));
-        actorData.destroyId = actor.connect('destroy',
-                                            this._untrackActor.bind(this));
+        actor.connectObject(
+            'notify::visible', this._queueUpdateRegions.bind(this),
+            'notify::allocation', this._queueUpdateRegions.bind(this),
+            'destroy', this._untrackActor.bind(this), this);
         // Note that destroying actor will unset its parent, so we don't
         // need to connect to 'destroy' too.
 
@@ -910,12 +908,9 @@ var LayoutManager = GObject.registerClass({
 
         if (i == -1)
             return;
-        let actorData = this._trackedActors[i];
 
         this._trackedActors.splice(i, 1);
-        actor.disconnect(actorData.visibleId);
-        actor.disconnect(actorData.allocationId);
-        actor.disconnect(actorData.destroyId);
+        actor.disconnectObject(this);
 
         this._queueUpdateRegions();
     }
diff --git a/js/ui/lightbox.js b/js/ui/lightbox.js
index abb72eb5ad..b0ca77a6d2 100644
--- a/js/ui/lightbox.js
+++ b/js/ui/lightbox.js
@@ -152,8 +152,9 @@ var Lightbox = GObject.registerClass({
             }));
         }
 
-        this._actorAddedSignalId = container.connect('actor-added', this._actorAdded.bind(this));
-        this._actorRemovedSignalId = container.connect('actor-removed', this._actorRemoved.bind(this));
+        container.connectObject(
+            'actor-added', this._actorAdded.bind(this),
+            'actor-removed', this._actorRemoved.bind(this), this);
 
         this._highlighted = null;
     }
@@ -283,15 +284,6 @@ var Lightbox = GObject.registerClass({
      * by destroying its container or by explicitly calling this.destroy().
      */
     _onDestroy() {
-        if (this._actorAddedSignalId) {
-            this._container.disconnect(this._actorAddedSignalId);
-            this._actorAddedSignalId = 0;
-        }
-        if (this._actorRemovedSignalId) {
-            this._container.disconnect(this._actorRemovedSignalId);
-            this._actorRemovedSignalId = 0;
-        }
-
         this.highlight(null);
     }
 });
diff --git a/js/ui/magnifier.js b/js/ui/magnifier.js
index bd66bc4794..c0c750e8e0 100644
--- a/js/ui/magnifier.js
+++ b/js/ui/magnifier.js
@@ -158,13 +158,12 @@ var Magnifier = class Magnifier {
 
         if (activate) {
             this._updateMouseSprite();
-            this._cursorSpriteChangedId =
-                this._cursorTracker.connect('cursor-changed',
-                                            this._updateMouseSprite.bind(this));
+            this._cursorTracker.connectObject(
+                'cursor-changed', this._updateMouseSprite.bind(this), this);
             Meta.disable_unredirect_for_display(global.display);
             this.startTrackingMouse();
         } else {
-            this._cursorTracker.disconnect(this._cursorSpriteChangedId);
+            this._cursorTracker.disconnectObject(this);
             this._mouseSprite.content.texture = null;
             Meta.enable_unredirect_for_display(global.display);
             this.stopTrackingMouse();
diff --git a/js/ui/messageList.js b/js/ui/messageList.js
index fb87fbdc72..6151bb4a57 100644
--- a/js/ui/messageList.js
+++ b/js/ui/messageList.js
@@ -171,21 +171,14 @@ class ScaleLayout extends Clutter.BinLayout {
         if (this._container == container)
             return;
 
-        if (this._container) {
-            for (let id of this._signals)
-                this._container.disconnect(id);
-        }
+        this._container?.disconnectObject(this);
 
         this._container = container;
-        this._signals = [];
 
         if (this._container) {
-            for (let signal of ['notify::scale-x', 'notify::scale-y']) {
-                let id = this._container.connect(signal, () => {
-                    this.layout_changed();
-                });
-                this._signals.push(id);
-            }
+            this._container.connectObject(
+                'notify::scale-x', () => this.layout_changed(),
+                'notify::scale-y', () => this.layout_changed(), this);
         }
     }
 
@@ -586,11 +579,8 @@ var MessageListSection = GObject.registerClass({
         this._list.connect('actor-added', this._sync.bind(this));
         this._list.connect('actor-removed', this._sync.bind(this));
 
-        let id = Main.sessionMode.connect('updated',
-                                          this._sync.bind(this));
-        this.connect('destroy', () => {
-            Main.sessionMode.disconnect(id);
-        });
+        Main.sessionMode.connectObject(
+            'updated', () => this._sync(), this);
 
         this._empty = true;
         this._canClear = false;
diff --git a/js/ui/messageTray.js b/js/ui/messageTray.js
index edb5ad1481..862ade6ef2 100644
--- a/js/ui/messageTray.js
+++ b/js/ui/messageTray.js
@@ -76,7 +76,6 @@ var FocusGrabber = class FocusGrabber {
     constructor(actor) {
         this._actor = actor;
         this._prevKeyFocusActor = null;
-        this._focusActorChangedId = 0;
         this._focused = false;
     }
 
@@ -86,7 +85,8 @@ var FocusGrabber = class FocusGrabber {
 
         this._prevKeyFocusActor = global.stage.get_key_focus();
 
-        this._focusActorChangedId = global.stage.connect('notify::key-focus', 
this._focusActorChanged.bind(this));
+        global.stage.connectObject('notify::key-focus',
+            this._focusActorChanged.bind(this), this);
 
         if (!this._actor.navigate_focus(null, St.DirectionType.TAB_FORWARD, false))
             this._actor.grab_key_focus();
@@ -98,10 +98,7 @@ var FocusGrabber = class FocusGrabber {
         if (!this._focused)
             return false;
 
-        if (this._focusActorChangedId > 0) {
-            global.stage.disconnect(this._focusActorChangedId);
-            this._focusActorChangedId = 0;
-        }
+        global.stage.disconnectObject(this);
 
         this._focused = false;
         return true;
@@ -445,15 +442,6 @@ var Notification = GObject.registerClass({
 
     setResident(resident) {
         this.resident = resident;
-
-        if (this.resident) {
-            if (this._activatedId) {
-                this.disconnect(this._activatedId);
-                this._activatedId = 0;
-            }
-        } else if (!this._activatedId) {
-            this._activatedId = this.connect_after('activated', () => this.destroy());
-        }
     }
 
     setTransient(isTransient) {
@@ -495,14 +483,12 @@ var Notification = GObject.registerClass({
 
     activate() {
         this.emit('activated');
+
+        if (!this.resident)
+            this.destroy();
     }
 
     destroy(reason = NotificationDestroyedReason.DISMISSED) {
-        if (this._activatedId) {
-            this.disconnect(this._activatedId);
-            delete this._activatedId;
-        }
-
         this.emit('destroy', reason);
         this.run_dispose();
     }
@@ -525,21 +511,13 @@ var NotificationBanner = GObject.registerClass({
         this._addActions();
         this._addSecondaryIcon();
 
-        this._activatedId = this.notification.connect('activated', () => {
+        this.notification.connectObject('activated', () => {
             // We hide all types of notifications once the user clicks on
             // them because the common outcome of clicking should be the
             // relevant window being brought forward and the user's
             // attention switching to the window.
             this.emit('done-displaying');
-        });
-    }
-
-    _disconnectNotificationSignals() {
-        super._disconnectNotificationSignals();
-
-        if (this._activatedId)
-            this.notification.disconnect(this._activatedId);
-        this._activatedId = 0;
+        }, this);
     }
 
     _onUpdated(n, clear) {
@@ -621,10 +599,8 @@ class SourceActor extends St.Widget {
         this._source = source;
         this._size = size;
 
-        this.connect('destroy', () => {
-            this._source.disconnect(this._iconUpdatedId);
-            this._actorDestroyed = true;
-        });
+        this.connect('destroy',
+            () => (this._actorDestroyed = true));
         this._actorDestroyed = false;
 
         let scaleFactor = St.ThemeContext.get_for_stage(global.stage).scale_factor;
@@ -636,7 +612,8 @@ class SourceActor extends St.Widget {
 
         this.add_actor(this._iconBin);
 
-        this._iconUpdatedId = this._source.connect('icon-updated', this._updateIcon.bind(this));
+        this._source.connectObject('icon-updated',
+            this._updateIcon.bind(this), this);
         this._updateIcon();
     }
 
@@ -868,7 +845,6 @@ var MessageTray = GObject.registerClass({
         this._notificationQueue = [];
         this._notification = null;
         this._banner = null;
-        this._bannerClickedId = 0;
 
         this._userActiveWhileNotificationShown = false;
 
@@ -924,7 +900,7 @@ var MessageTray = GObject.registerClass({
                               Shell.ActionMode.OVERVIEW,
                               this._expandActiveNotification.bind(this));
 
-        this._sources = new Map();
+        this._sources = new Set();
 
         this._sessionUpdated();
     }
@@ -995,26 +971,18 @@ var MessageTray = GObject.registerClass({
     }
 
     _addSource(source) {
-        let obj = {
-            showId: 0,
-            destroyId: 0,
-        };
-
-        this._sources.set(source, obj);
+        this._sources.add(source);
 
-        obj.showId = source.connect('notification-show', this._onNotificationShow.bind(this));
-        obj.destroyId = source.connect('destroy', this._onSourceDestroy.bind(this));
+        source.connectObject(
+            'notification-show', this._onNotificationShow.bind(this),
+            'destroy', () => this._removeSource(source), this);
 
         this.emit('source-added', source);
     }
 
     _removeSource(source) {
-        let obj = this._sources.get(source);
         this._sources.delete(source);
-
-        source.disconnect(obj.showId);
-        source.disconnect(obj.destroyId);
-
+        source.disconnectObject(this);
         this.emit('source-removed', source);
     }
 
@@ -1034,10 +1002,6 @@ var MessageTray = GObject.registerClass({
         }
     }
 
-    _onSourceDestroy(source) {
-        this._removeSource(source);
-    }
-
     _onNotificationDestroy(notification) {
         this._notificationRemoved = this._notification === notification;
 
@@ -1264,11 +1228,9 @@ var MessageTray = GObject.registerClass({
         }
 
         this._banner = this._notification.createBanner();
-        this._bannerClickedId = this._banner.connect('done-displaying',
-                                                     this._escapeTray.bind(this));
-        this._bannerUnfocusedId = this._banner.connect('unfocused', () => {
-            this._updateState();
-        });
+        this._banner.connectObject(
+            'done-displaying', this._escapeTray.bind(this),
+            'unfocused', () => this._updateState(), this);
 
         this._bannerBin.add_actor(this._banner);
 
@@ -1381,14 +1343,7 @@ var MessageTray = GObject.registerClass({
     _hideNotification(animate) {
         this._notificationFocusGrabber.ungrabFocus();
 
-        if (this._bannerClickedId) {
-            this._banner.disconnect(this._bannerClickedId);
-            this._bannerClickedId = 0;
-        }
-        if (this._bannerUnfocusedId) {
-            this._banner.disconnect(this._bannerUnfocusedId);
-            this._bannerUnfocusedId = 0;
-        }
+        this._banner.disconnectObject(this);
 
         this._resetNotificationLeftTimeout();
         this._bannerBin.remove_all_transitions();
diff --git a/js/ui/modalDialog.js b/js/ui/modalDialog.js
index 6d1d45ceb5..9d5521be93 100644
--- a/js/ui/modalDialog.js
+++ b/js/ui/modalDialog.js
@@ -154,15 +154,12 @@ var ModalDialog = GObject.registerClass({
     }
 
     setInitialKeyFocus(actor) {
-        if (this._initialKeyFocusDestroyId)
-            this._initialKeyFocus.disconnect(this._initialKeyFocusDestroyId);
+        this._initialKeyFocus?.disconnectObject(this);
 
         this._initialKeyFocus = actor;
 
-        this._initialKeyFocusDestroyId = actor.connect('destroy', () => {
-            this._initialKeyFocus = null;
-            this._initialKeyFocusDestroyId = 0;
-        });
+        actor.connectObject('destroy',
+            () => (this._initialKeyFocus = null), this);
     }
 
     open(timestamp, onPrimary) {
diff --git a/js/ui/mpris.js b/js/ui/mpris.js
index c672dcb900..232e172cda 100644
--- a/js/ui/mpris.js
+++ b/js/ui/mpris.js
@@ -47,19 +47,12 @@ class MediaMessage extends MessageList.Message {
                 this._player.next();
             });
 
-        this._updateHandlerId =
-            this._player.connect('changed', this._update.bind(this));
-        this._closedHandlerId =
-            this._player.connect('closed', this.close.bind(this));
+        this._player.connectObject(
+            'changed', this._update.bind(this),
+            'closed', this.close.bind(this), this);
         this._update();
     }
 
-    _onDestroy() {
-        super._onDestroy();
-        this._player.disconnect(this._updateHandlerId);
-        this._player.disconnect(this._closedHandlerId);
-    }
-
     vfunc_clicked() {
         this._player.raise();
         Main.panel.closeCalendar();
@@ -161,21 +154,21 @@ var MprisPlayer = class MprisPlayer {
     }
 
     _close() {
-        this._mprisProxy.disconnect(this._ownerNotifyId);
+        this._mprisProxy.disconnectObject(this);
         this._mprisProxy = null;
 
-        this._playerProxy.disconnect(this._propsChangedId);
+        this._playerProxy.disconnectObject(this);
         this._playerProxy = null;
 
         this.emit('closed');
     }
 
     _onMprisProxyReady() {
-        this._ownerNotifyId = this._mprisProxy.connect('notify::g-name-owner',
+        this._mprisProxy.connectObject('notify::g-name-owner',
             () => {
                 if (!this._mprisProxy.g_name_owner)
                     this._close();
-            });
+            }, this);
         // It is possible for the bus to disappear before the previous signal
         // is connected, so we must ensure that the bus still exists at this
         // point.
@@ -184,8 +177,8 @@ var MprisPlayer = class MprisPlayer {
     }
 
     _onPlayerProxyReady() {
-        this._propsChangedId = this._playerProxy.connect('g-properties-changed',
-                                                         this._updateState.bind(this));
+        this._playerProxy.connectObject(
+            'g-properties-changed', () => this._updateState(), this);
         this._updateState();
     }
 
diff --git a/js/ui/overviewControls.js b/js/ui/overviewControls.js
index d3df21ea07..b0b502f648 100644
--- a/js/ui/overviewControls.js
+++ b/js/ui/overviewControls.js
@@ -338,9 +338,8 @@ class ControlsManager extends St.Widget {
         this._stateAdjustment = new OverviewAdjustment(this);
         this._stateAdjustment.connect('notify::value', this._update.bind(this));
 
-        this._nWorkspacesNotifyId =
-            workspaceManager.connect('notify::n-workspaces',
-                this._updateAdjustment.bind(this));
+        workspaceManager.connectObject(
+            'notify::n-workspaces', () => this._updateAdjustment(), this);
 
         this._searchController = new SearchController.SearchController(
             this._searchEntry,
@@ -489,8 +488,6 @@ class ControlsManager extends St.Widget {
             Shell.ActionMode.NORMAL | Shell.ActionMode.OVERVIEW,
             () => this._shiftState(Meta.MotionDirection.DOWN));
 
-        this.connect('destroy', this._onDestroy.bind(this));
-
         this._update();
     }
 
@@ -686,10 +683,6 @@ class ControlsManager extends St.Widget {
         }
     }
 
-    _onDestroy() {
-        global.workspace_manager.disconnect(this._nWorkspacesNotifyId);
-    }
-
     _updateAdjustment() {
         let workspaceManager = global.workspace_manager;
         let newNumWorkspaces = workspaceManager.n_workspaces;
diff --git a/js/ui/padOsd.js b/js/ui/padOsd.js
index 67864026ef..065df47788 100644
--- a/js/ui/padOsd.js
+++ b/js/ui/padOsd.js
@@ -652,24 +652,25 @@ var PadOsd = GObject.registerClass({
         this._padChooser = null;
 
         let seat = Clutter.get_default_backend().get_default_seat();
-        this._deviceAddedId = seat.connect('device-added', (_seat, device) => {
-            if (device.get_device_type() == Clutter.InputDeviceType.PAD_DEVICE &&
-                this.padDevice.is_grouped(device)) {
-                this._groupPads.push(device);
-                this._updatePadChooser();
-            }
-        });
-        this._deviceRemovedId = seat.connect('device-removed', (_seat, device) => {
-            // If the device is being removed, destroy the padOsd.
-            if (device == this.padDevice) {
-                this.destroy();
-            } else if (this._groupPads.includes(device)) {
-                // Or update the pad chooser if the device belongs to
-                // the same group.
-                this._groupPads.splice(this._groupPads.indexOf(device), 1);
-                this._updatePadChooser();
-            }
-        });
+        seat.connectObject(
+            'device-added', (_seat, device) => {
+                if (device.get_device_type() === Clutter.InputDeviceType.PAD_DEVICE &&
+                    this.padDevice.is_grouped(device)) {
+                    this._groupPads.push(device);
+                    this._updatePadChooser();
+                }
+            },
+            'device-removed', (_seat, device) => {
+                // If the device is being removed, destroy the padOsd.
+                if (device === this.padDevice) {
+                    this.destroy();
+                } else if (this._groupPads.includes(device)) {
+                    // Or update the pad chooser if the device belongs to
+                    // the same group.
+                    this._groupPads.splice(this._groupPads.indexOf(device), 1);
+                    this._updatePadChooser();
+                }
+            }, this);
 
         seat.list_devices().forEach(device => {
             if (device != this.padDevice &&
@@ -944,16 +945,6 @@ var PadOsd = GObject.registerClass({
         this._grab = null;
         this._actionEditor.close();
 
-        let seat = Clutter.get_default_backend().get_default_seat();
-        if (this._deviceRemovedId != 0) {
-            seat.disconnect(this._deviceRemovedId);
-            this._deviceRemovedId = 0;
-        }
-        if (this._deviceAddedId != 0) {
-            seat.disconnect(this._deviceAddedId);
-            this._deviceAddedId = 0;
-        }
-
         this.emit('closed');
     }
 });
diff --git a/js/ui/panel.js b/js/ui/panel.js
index 727520637c..b9780c0bd9 100644
--- a/js/ui/panel.js
+++ b/js/ui/panel.js
@@ -38,7 +38,6 @@ var AppMenuButton = GObject.registerClass({
 
         this._menuManager = panel.menuManager;
         this._targetApp = null;
-        this._busyNotifyId = 0;
 
         let bin = new St.Bin({ name: 'appMenu' });
         this.add_actor(bin);
@@ -75,8 +74,9 @@ var AppMenuButton = GObject.registerClass({
         this._visible = !Main.overview.visible;
         if (!this._visible)
             this.hide();
-        this._overviewHidingId = Main.overview.connect('hiding', this._sync.bind(this));
-        this._overviewShowingId = Main.overview.connect('showing', this._sync.bind(this));
+        Main.overview.connectObject(
+            'hiding', this._sync.bind(this),
+            'showing', this._sync.bind(this), this);
 
         this._spinner = new Animation.Spinner(PANEL_ICON_SIZE, {
             animate: true,
@@ -88,14 +88,12 @@ var AppMenuButton = GObject.registerClass({
         this.setMenu(menu);
         this._menuManager.addMenu(menu);
 
-        let tracker = Shell.WindowTracker.get_default();
-        let appSys = Shell.AppSystem.get_default();
-        this._focusAppNotifyId =
-            tracker.connect('notify::focus-app', this._focusAppChanged.bind(this));
-        this._appStateChangedSignalId =
-            appSys.connect('app-state-changed', this._onAppStateChanged.bind(this));
-        this._switchWorkspaceNotifyId =
-            global.window_manager.connect('switch-workspace', this._sync.bind(this));
+        Shell.WindowTracker.get_default().connectObject('notify::focus-app',
+            this._focusAppChanged.bind(this), this);
+        Shell.AppSystem.get_default().connectObject('app-state-changed',
+            this._onAppStateChanged.bind(this), this);
+        global.window_manager.connectObject('switch-workspace',
+            this._sync.bind(this), this);
 
         this._sync();
     }
@@ -195,15 +193,12 @@ var AppMenuButton = GObject.registerClass({
         let targetApp = this._findTargetApp();
 
         if (this._targetApp != targetApp) {
-            if (this._busyNotifyId) {
-                this._targetApp.disconnect(this._busyNotifyId);
-                this._busyNotifyId = 0;
-            }
+            this._targetApp?.disconnectObject(this);
 
             this._targetApp = targetApp;
 
             if (this._targetApp) {
-                this._busyNotifyId = this._targetApp.connect('notify::busy', this._sync.bind(this));
+                this._targetApp.connectObject('notify::busy', this._sync.bind(this), this);
                 this._label.set_text(this._targetApp.get_name());
                 this.set_accessible_name(this._targetApp.get_name());
 
@@ -230,33 +225,6 @@ var AppMenuButton = GObject.registerClass({
         this.menu.setApp(this._targetApp);
         this.emit('changed');
     }
-
-    _onDestroy() {
-        if (this._appStateChangedSignalId > 0) {
-            let appSys = Shell.AppSystem.get_default();
-            appSys.disconnect(this._appStateChangedSignalId);
-            this._appStateChangedSignalId = 0;
-        }
-        if (this._focusAppNotifyId > 0) {
-            let tracker = Shell.WindowTracker.get_default();
-            tracker.disconnect(this._focusAppNotifyId);
-            this._focusAppNotifyId = 0;
-        }
-        if (this._overviewHidingId > 0) {
-            Main.overview.disconnect(this._overviewHidingId);
-            this._overviewHidingId = 0;
-        }
-        if (this._overviewShowingId > 0) {
-            Main.overview.disconnect(this._overviewShowingId);
-            this._overviewShowingId = 0;
-        }
-        if (this._switchWorkspaceNotifyId > 0) {
-            global.window_manager.disconnect(this._switchWorkspaceNotifyId);
-            this._switchWorkspaceNotifyId = 0;
-        }
-
-        super._onDestroy();
-    }
 });
 
 var ActivitiesButton = GObject.registerClass(
diff --git a/js/ui/popupMenu.js b/js/ui/popupMenu.js
index 4b51a7a9c8..f9f3a3b1aa 100644
--- a/js/ui/popupMenu.js
+++ b/js/ui/popupMenu.js
@@ -506,7 +506,7 @@ var PopupMenuBase = class {
 
         this._sensitive = true;
 
-        this._sessionUpdatedId = Main.sessionMode.connect('updated', this._sessionUpdated.bind(this));
+        Main.sessionMode.connectObject('updated', () => this._sessionUpdated(), this);
     }
 
     _getTopMenu() {
@@ -609,52 +609,41 @@ var PopupMenuBase = class {
     }
 
     _connectItemSignals(menuItem) {
-        menuItem._activeChangeId = menuItem.connect('notify::active', () => {
-            let active = menuItem.active;
-            if (active && this._activeMenuItem != menuItem) {
-                if (this._activeMenuItem)
-                    this._activeMenuItem.active = false;
-                this._activeMenuItem = menuItem;
-                this.emit('active-changed', menuItem);
-            } else if (!active && this._activeMenuItem == menuItem) {
-                this._activeMenuItem = null;
-                this.emit('active-changed', null);
-            }
-        });
-        menuItem._sensitiveChangeId = menuItem.connect('notify::sensitive', () => {
-            let sensitive = menuItem.sensitive;
-            if (!sensitive && this._activeMenuItem == menuItem) {
-                if (!this.actor.navigate_focus(menuItem.actor,
-                                               St.DirectionType.TAB_FORWARD,
-                                               true))
-                    this.actor.grab_key_focus();
-            } else if (sensitive && this._activeMenuItem == null) {
-                if (global.stage.get_key_focus() == this.actor)
-                    menuItem.actor.grab_key_focus();
-            }
-        });
-        menuItem._activateId = menuItem.connect_after('activate', () => {
-            this.emit('activate', menuItem);
-            this.itemActivated(BoxPointer.PopupAnimation.FULL);
-        });
-
-        menuItem._parentSensitiveChangeId = this.connect('notify::sensitive', () => {
-            menuItem.syncSensitive();
-        });
-
-        // the weird name is to avoid a conflict with some random property
-        // the menuItem may have, called destroyId
-        // (FIXME: in the future it may make sense to have container objects
-        // like PopupMenuManager does)
-        menuItem._popupMenuDestroyId = menuItem.connect('destroy', () => {
-            menuItem.disconnect(menuItem._popupMenuDestroyId);
-            menuItem.disconnect(menuItem._activateId);
-            menuItem.disconnect(menuItem._activeChangeId);
-            menuItem.disconnect(menuItem._sensitiveChangeId);
-            this.disconnect(menuItem._parentSensitiveChangeId);
-            if (menuItem == this._activeMenuItem)
-                this._activeMenuItem = null;
-        });
+        menuItem.connectObject(
+            'notify::active', () => {
+                const { active } = menuItem;
+                if (active && this._activeMenuItem !== menuItem) {
+                    if (this._activeMenuItem)
+                        this._activeMenuItem.active = false;
+                    this._activeMenuItem = menuItem;
+                    this.emit('active-changed', menuItem);
+                } else if (!active && this._activeMenuItem === menuItem) {
+                    this._activeMenuItem = null;
+                    this.emit('active-changed', null);
+                }
+            },
+            'notify::sensitive', () => {
+                const { sensitive } = menuItem;
+                if (!sensitive && this._activeMenuItem === menuItem) {
+                    if (!this.actor.navigate_focus(menuItem.actor,
+                        St.DirectionType.TAB_FORWARD, true))
+                        this.actor.grab_key_focus();
+                } else if (sensitive && this._activeMenuItem === null) {
+                    if (global.stage.get_key_focus() === this.actor)
+                        menuItem.actor.grab_key_focus();
+                }
+            },
+            'activate', () => {
+                this.emit('activate', menuItem);
+                this.itemActivated(BoxPointer.PopupAnimation.FULL);
+            }, GObject.ConnectFlags.AFTER,
+            'destroy', () => {
+                if (menuItem === this._activeMenuItem)
+                    this._activeMenuItem = null;
+            }, this);
+
+        this.connectObject('notify::sensitive',
+            () => menuItem.syncSensitive(), menuItem);
     }
 
     _updateSeparatorVisibility(menuItem) {
@@ -726,28 +715,20 @@ var PopupMenuBase = class {
         }
 
         if (menuItem instanceof PopupMenuSection) {
-            let activeChangeId = menuItem.connect('active-changed', this._subMenuActiveChanged.bind(this));
-
-            let parentOpenStateChangedId = this.connect('open-state-changed', (self, open) => {
-                if (open)
-                    menuItem.open();
-                else
-                    menuItem.close();
-            });
-            let parentClosingId = this.connect('menu-closed', () => {
-                menuItem.emit('menu-closed');
-            });
-            let subMenuSensitiveChangedId = this.connect('notify::sensitive', () => {
-                menuItem.emit('notify::sensitive');
-            });
-
-            menuItem.connect('destroy', () => {
-                menuItem.disconnect(activeChangeId);
-                this.disconnect(subMenuSensitiveChangedId);
-                this.disconnect(parentOpenStateChangedId);
-                this.disconnect(parentClosingId);
-                this.length--;
-            });
+            menuItem.connectObject(
+                'active-changed', this._subMenuActiveChanged.bind(this),
+                'destroy', () => this.length--, this);
+
+            this.connectObject(
+                'open-state-changed', (self, open) => {
+                    if (open)
+                        menuItem.open();
+                    else
+                        menuItem.close();
+                },
+                'menu-closed', () => menuItem.emit('menu-closed'),
+                'notify::sensitive', () => menuItem.emit('notify::sensitive'),
+                menuItem);
         } else if (menuItem instanceof PopupSubMenuMenuItem) {
             if (beforeItem == null)
                 this.box.add(menuItem.menu.actor);
@@ -755,15 +736,11 @@ var PopupMenuBase = class {
                 this.box.insert_child_below(menuItem.menu.actor, beforeItem);
 
             this._connectItemSignals(menuItem);
-            let subMenuActiveChangeId = menuItem.menu.connect('active-changed', 
this._subMenuActiveChanged.bind(this));
-            let closingId = this.connect('menu-closed', () => {
+            menuItem.menu.connectObject('active-changed',
+                this._subMenuActiveChanged.bind(this), this);
+            this.connectObject('menu-closed', () => {
                 menuItem.menu.close(BoxPointer.PopupAnimation.NONE);
-            });
-
-            menuItem.connect('destroy', () => {
-                menuItem.menu.disconnect(subMenuActiveChangeId);
-                this.disconnect(closingId);
-            });
+            }, menuItem);
         } else if (menuItem instanceof PopupSeparatorMenuItem) {
             this._connectItemSignals(menuItem);
 
@@ -771,13 +748,9 @@ var PopupMenuBase = class {
             // separator's adjacent siblings change visibility or position.
             // open-state-changed isn't exactly that, but doing it in more
             // precise ways would require a lot more bookkeeping.
-            let openStateChangeId = this.connect('open-state-changed', () => {
+            this.connectObject('open-state-changed', () => {
                 this._updateSeparatorVisibility(menuItem);
-            });
-            let destroyId = menuItem.connect('destroy', () => {
-                this.disconnect(openStateChangeId);
-                menuItem.disconnect(destroyId);
-            });
+            }, menuItem);
         } else if (menuItem instanceof PopupBaseMenuItem) {
             this._connectItemSignals(menuItem);
         } else {
@@ -829,8 +802,7 @@ var PopupMenuBase = class {
 
         this.emit('destroy');
 
-        Main.sessionMode.disconnect(this._sessionUpdatedId);
-        this._sessionUpdatedId = 0;
+        Main.sessionMode.disconnectObject(this);
     }
 };
 Signals.addSignalMethods(PopupMenuBase.prototype);
@@ -854,13 +826,12 @@ var PopupMenu = class extends PopupMenuBase {
         this.actor.reactive = true;
 
         if (this.sourceActor) {
-            this._keyPressId = this.sourceActor.connect('key-press-event',
-                this._onKeyPress.bind(this));
-            this._notifyMappedId = this.sourceActor.connect('notify::mapped',
-                () => {
+            this.sourceActor.connectObject(
+                'key-press-event', this._onKeyPress.bind(this),
+                'notify::mapped', () => {
                     if (!this.sourceActor.mapped)
                         this.close();
-                });
+                }, this);
         }
 
         this._systemModalOpenedId = 0;
@@ -970,11 +941,7 @@ var PopupMenu = class extends PopupMenuBase {
     }
 
     destroy() {
-        if (this._keyPressId)
-            this.sourceActor.disconnect(this._keyPressId);
-
-        if (this._notifyMappedId)
-            this.sourceActor.disconnect(this._notifyMappedId);
+        this.sourceActor?.disconnectObject(this);
 
         if (this._systemModalOpenedId)
             Main.layoutManager.disconnect(this._systemModalOpenedId);
@@ -1333,20 +1300,19 @@ var PopupMenuManager = class {
     }
 
     addMenu(menu, position) {
-        if (this._findMenu(menu) > -1)
+        if (this._menus.includes(menu))
             return;
 
-        let menudata = {
-            menu,
-            openStateChangeId: menu.connect('open-state-changed', this._onMenuOpenState.bind(this)),
-            destroyId:         menu.connect('destroy', this._onMenuDestroy.bind(this)),
-            capturedEventId:   menu.actor.connect('captured-event', this._onCapturedEvent.bind(this)),
-        };
+        menu.connectObject(
+            'open-state-changed', this._onMenuOpenState.bind(this),
+            'destroy', () => this.removeMenu(menu), this);
+        menu.actor.connectObject('captured-event',
+            this._onCapturedEvent.bind(this), this);
 
         if (position == undefined)
-            this._menus.push(menudata);
+            this._menus.push(menu);
         else
-            this._menus.splice(position, 0, menudata);
+            this._menus.splice(position, 0, menu);
     }
 
     removeMenu(menu) {
@@ -1355,13 +1321,12 @@ var PopupMenuManager = class {
             this._grab = null;
         }
 
-        let position = this._findMenu(menu);
+        const position = this._menus.indexOf(menu);
         if (position == -1) // not a menu we manage
             return;
 
-        let menudata = this._menus[position];
-        menu.disconnect(menudata.openStateChangeId);
-        menu.disconnect(menudata.destroyId);
+        menu.disconnectObject(this);
+        menu.actor.disconnectObject(this);
 
         this._menus.splice(position, 1);
     }
@@ -1426,7 +1391,7 @@ var PopupMenuManager = class {
     _findMenuForSource(source) {
         while (source) {
             let actor = source;
-            const menu = this._menus.map(m => m.menu).find(m => m.sourceActor === actor);
+            const menu = this._menus.find(m => m.sourceActor === actor);
             if (menu)
                 return menu;
             source = source.get_parent();
@@ -1435,19 +1400,6 @@ var PopupMenuManager = class {
         return null;
     }
 
-    _onMenuDestroy(menu) {
-        this.removeMenu(menu);
-    }
-
-    _findMenu(item) {
-        for (let i = 0; i < this._menus.length; i++) {
-            let menudata = this._menus[i];
-            if (item == menudata.menu)
-                return i;
-        }
-        return -1;
-    }
-
     _closeMenu(isUser, menu) {
         // If this isn't a user action, we called close()
         // on the BoxPointer ourselves, so we shouldn't
diff --git a/js/ui/search.js b/js/ui/search.js
index cd5474db30..3bfb83f9f7 100644
--- a/js/ui/search.js
+++ b/js/ui/search.js
@@ -79,8 +79,6 @@ class ListSearchResult extends SearchResult {
         });
         this.set_child(content);
 
-        this._termsChangedId = 0;
-
         let titleBox = new St.BoxLayout({
             style_class: 'list-search-result-title',
             y_align: Clutter.ActorAlign.CENTER,
@@ -108,14 +106,11 @@ class ListSearchResult extends SearchResult {
             });
             content.add_child(this._descriptionLabel);
 
-            this._termsChangedId =
-                this._resultsView.connect('terms-changed',
-                                          this._highlightTerms.bind(this));
+            this._resultsView.connectObject(
+                'terms-changed', this._highlightTerms.bind(this), this);
 
             this._highlightTerms();
         }
-
-        this.connect('destroy', this._onDestroy.bind(this));
     }
 
     get ICON_SIZE() {
@@ -126,12 +121,6 @@ class ListSearchResult extends SearchResult {
         let markup = this._resultsView.highlightTerms(this.metaInfo['description'].split('\n')[0]);
         this._descriptionLabel.clutter_text.set_markup(markup);
     }
-
-    _onDestroy() {
-        if (this._termsChangedId)
-            this._resultsView.disconnect(this._termsChangedId);
-        this._termsChangedId = 0;
-    }
 });
 
 var GridSearchResult = GObject.registerClass(
diff --git a/js/ui/shellEntry.js b/js/ui/shellEntry.js
index a2010b39e0..4af0c88091 100644
--- a/js/ui/shellEntry.js
+++ b/js/ui/shellEntry.js
@@ -174,20 +174,14 @@ class CapsLockWarning extends St.Label {
 
         this.connect('notify::mapped', () => {
             if (this.is_mapped()) {
-                this._stateChangedId = this._keymap.connect('state-changed',
-                    () => this._sync(true));
+                this._keymap.connectObject(
+                    'state-changed', () => this._sync(true), this);
             } else {
-                this._keymap.disconnect(this._stateChangedId);
-                this._stateChangedId = 0;
+                this._keymap.disconnectObject(this);
             }
 
             this._sync(false);
         });
-
-        this.connect('destroy', () => {
-            if (this._stateChangedId)
-                this._keymap.disconnect(this._stateChangedId);
-        });
     }
 
     _sync(animate) {
diff --git a/js/ui/shellMountOperation.js b/js/ui/shellMountOperation.js
index 6e213cd62a..b04156d1e0 100644
--- a/js/ui/shellMountOperation.js
+++ b/js/ui/shellMountOperation.js
@@ -54,7 +54,6 @@ var ShellMountOperation = class {
         params = Params.parse(params, { existingDialog: null });
 
         this._dialog = null;
-        this._dialogId = 0;
         this._existingDialog = params.existingDialog;
         this._processesDialog = null;
 
@@ -84,13 +83,13 @@ var ShellMountOperation = class {
         this._closeExistingDialog();
         this._dialog = new ShellMountQuestionDialog();
 
-        this._dialogId = this._dialog.connect('response',
+        this._dialog.connectObject('response',
             (object, choice) => {
                 this.mountOp.set_choice(choice);
                 this.mountOp.reply(Gio.MountOperationResult.HANDLED);
 
                 this.close();
-            });
+            }, this);
 
         this._dialog.update(message, choices);
         this._dialog.open();
@@ -104,7 +103,7 @@ var ShellMountOperation = class {
             this._dialog = new ShellMountPasswordDialog(message, flags);
         }
 
-        this._dialogId = this._dialog.connect('response',
+        this._dialog.connectObject('response',
             (object, choice, password, remember, hiddenVolume, systemVolume, pim) => {
                 if (choice == -1) {
                     this.mountOp.reply(Gio.MountOperationResult.ABORTED);
@@ -120,7 +119,7 @@ var ShellMountOperation = class {
                     this.mountOp.set_pim(pim);
                     this.mountOp.reply(Gio.MountOperationResult.HANDLED);
                 }
-            });
+            }, this);
         this._dialog.open();
     }
 
@@ -150,7 +149,7 @@ var ShellMountOperation = class {
             this._processesDialog = new ShellProcessesDialog();
             this._dialog = this._processesDialog;
 
-            this._dialogId = this._processesDialog.connect('response',
+            this._processesDialog.connectObject('response',
                 (object, choice) => {
                     if (choice == -1) {
                         this.mountOp.reply(Gio.MountOperationResult.ABORTED);
@@ -160,7 +159,7 @@ var ShellMountOperation = class {
                     }
 
                     this.close();
-                });
+                }, this);
             this._processesDialog.open();
         }
 
@@ -178,11 +177,7 @@ var ShellMountOperation = class {
     }
 
     borrowDialog() {
-        if (this._dialogId != 0) {
-            this._dialog.disconnect(this._dialogId);
-            this._dialogId = 0;
-        }
-
+        this._dialog?.disconnectObject(this);
         return this._dialog;
     }
 };
diff --git a/js/ui/status/keyboard.js b/js/ui/status/keyboard.js
index f7527313c8..15b4ddb6f4 100644
--- a/js/ui/status/keyboard.js
+++ b/js/ui/status/keyboard.js
@@ -349,8 +349,6 @@ var InputSourceManager = class {
 
         this._sourcesPerWindow = false;
         this._focusWindowNotifyId = 0;
-        this._overviewShowingId = 0;
-        this._overviewHiddenId = 0;
         this._settings.connect('per-window-changed', this._sourcesPerWindowChanged.bind(this));
         this._sourcesPerWindowChanged();
         this._disableIBus = false;
@@ -731,17 +729,13 @@ var InputSourceManager = class {
         if (this._sourcesPerWindow && this._focusWindowNotifyId == 0) {
             this._focusWindowNotifyId = global.display.connect('notify::focus-window',
                                                                this._setPerWindowInputSource.bind(this));
-            this._overviewShowingId = Main.overview.connect('showing',
-                                                            this._setPerWindowInputSource.bind(this));
-            this._overviewHiddenId = Main.overview.connect('hidden',
-                                                           this._setPerWindowInputSource.bind(this));
+            Main.overview.connectObject(
+                'showing', this._setPerWindowInputSource.bind(this),
+                'hidden', this._setPerWindowInputSource.bind(this), this);
         } else if (!this._sourcesPerWindow && this._focusWindowNotifyId != 0) {
             global.display.disconnect(this._focusWindowNotifyId);
             this._focusWindowNotifyId = 0;
-            Main.overview.disconnect(this._overviewShowingId);
-            this._overviewShowingId = 0;
-            Main.overview.disconnect(this._overviewHiddenId);
-            this._overviewHiddenId = 0;
+            Main.overview.disconnectObject(this);
 
             let windows = global.get_window_actors().map(w => w.meta_window);
             for (let i = 0; i < windows.length; ++i) {
@@ -853,19 +847,14 @@ class InputSourceIndicator extends PanelMenu.Button {
         this._sessionUpdated();
 
         this._inputSourceManager = getInputSourceManager();
-        this._inputSourceManagerSourcesChangedId =
-            this._inputSourceManager.connect('sources-changed', this._sourcesChanged.bind(this));
-        this._inputSourceManagerCurrentSourceChangedId =
-            this._inputSourceManager.connect('current-source-changed', 
this._currentSourceChanged.bind(this));
+        this._inputSourceManager.connectObject(
+            'sources-changed', this._sourcesChanged.bind(this),
+            'current-source-changed', this._currentSourceChanged.bind(this), this);
         this._inputSourceManager.reload();
     }
 
     _onDestroy() {
-        if (this._inputSourceManager) {
-            this._inputSourceManager.disconnect(this._inputSourceManagerSourcesChangedId);
-            this._inputSourceManager.disconnect(this._inputSourceManagerCurrentSourceChangedId);
-            this._inputSourceManager = null;
-        }
+        this._inputSourceManager = null;
     }
 
     _sessionUpdated() {
diff --git a/js/ui/status/location.js b/js/ui/status/location.js
index f213d80821..aa849d64ae 100644
--- a/js/ui/status/location.js
+++ b/js/ui/status/location.js
@@ -69,10 +69,10 @@ var GeoclueAgent = GObject.registerClass({
         super._init();
 
         this._settings = new Gio.Settings({ schema_id: LOCATION_SCHEMA });
-        this._settings.connect(`changed::${ENABLED}`,
-            () => this.notify('enabled'));
-        this._settings.connect(`changed::${MAX_ACCURACY_LEVEL}`,
-            this._onMaxAccuracyLevelChanged.bind(this));
+        this._settings.connectObject(
+            `changed::${ENABLED}`, () => this.notify('enabled'),
+            `changed::${MAX_ACCURACY_LEVEL}`, () => this._onMaxAccuracyLevelChanged(),
+            this);
 
         this._agent = Gio.DBusExportedObject.wrapJSObject(AgentIface, this);
         this._agent.export(Gio.DBus.system, '/org/freedesktop/GeoClue2/Agent');
@@ -149,8 +149,8 @@ var GeoclueAgent = GObject.registerClass({
         }
 
         this._managerProxy = proxy;
-        this._propertiesChangedId = this._managerProxy.connect('g-properties-changed',
-                                                               this._onGeocluePropsChanged.bind(this));
+        this._managerProxy.connectObject('g-properties-changed',
+            this._onGeocluePropsChanged.bind(this), this);
 
         this.notify('in-use');
 
@@ -166,10 +166,7 @@ var GeoclueAgent = GObject.registerClass({
     }
 
     _onGeoclueVanished() {
-        if (this._propertiesChangedId) {
-            this._managerProxy.disconnect(this._propertiesChangedId);
-            this._propertiesChangedId = 0;
-        }
+        this._managerProxy.disconnectObject(this);
         this._managerProxy = null;
 
         this.notify('in-use');
@@ -238,22 +235,14 @@ class Indicator extends PanelMenu.SystemIndicator {
 
         this.menu.addMenuItem(this._item);
 
-        this._agentSignals = [
-            this._agent.connect('notify::enabled', () => this._sync()),
-            this._agent.connect('notify::in-use', () => this._sync()),
-        ];
-
-        this.connect('destroy', this._onDestroy.bind(this));
+        this._agent.connectObject(
+            'notify::enabled', () => this._sync(),
+            'notify::in-use', () => this._sync(), this);
 
         Main.sessionMode.connect('updated', this._onSessionUpdated.bind(this));
         this._onSessionUpdated();
     }
 
-    _onDestroy() {
-        this._agentSignals.forEach(id => this._agent.disconnect(id));
-        this._agentSignals = [];
-    }
-
     _onSessionUpdated() {
         let sensitive = !Main.sessionMode.isLocked && !Main.sessionMode.isGreeter;
         this.menu.setSensitive(sensitive);
diff --git a/js/ui/status/network.js b/js/ui/status/network.js
index 4b88be547b..05f27fcdf6 100644
--- a/js/ui/status/network.js
+++ b/js/ui/status/network.js
@@ -112,7 +112,6 @@ var NMConnectionItem = class {
         this._section = section;
         this._connection = connection;
         this._activeConnection = null;
-        this._activeConnectionChangedId = 0;
 
         this._buildUI();
         this._sync();
@@ -127,11 +126,7 @@ var NMConnectionItem = class {
     }
 
     destroy() {
-        if (this._activeConnectionChangedId) {
-            this._activeConnection.disconnect(this._activeConnectionChangedId);
-            this._activeConnectionChangedId = 0;
-        }
-
+        this._activeConnection?.disconnectObject(this);
         this.labelItem.destroy();
         this.radioItem.destroy();
     }
@@ -188,17 +183,12 @@ var NMConnectionItem = class {
     }
 
     setActiveConnection(activeConnection) {
-        if (this._activeConnectionChangedId > 0) {
-            this._activeConnection.disconnect(this._activeConnectionChangedId);
-            this._activeConnectionChangedId = 0;
-        }
+        this._activeConnection?.disconnectObject(this);
 
         this._activeConnection = activeConnection;
 
-        if (this._activeConnection) {
-            this._activeConnectionChangedId = this._activeConnection.connect('notify::state',
-                                                                             
this._connectionStateChanged.bind(this));
-        }
+        this._activeConnection?.connectObject('notify::state',
+            this._connectionStateChanged.bind(this), this);
 
         this._sync();
     }
@@ -222,15 +212,12 @@ var NMConnectionSection = class NMConnectionSection {
         this.item.menu.addMenuItem(this._labelSection);
         this.item.menu.addMenuItem(this._radioSection);
 
-        this._notifyConnectivityId = this._client.connect('notify::connectivity', 
this._iconChanged.bind(this));
+        this._client.connectObject('notify::connectivity',
+            this._iconChanged.bind(this), this);
     }
 
     destroy() {
-        if (this._notifyConnectivityId != 0) {
-            this._client.disconnect(this._notifyConnectivityId);
-            this._notifyConnectivityId = 0;
-        }
-
+        this._client.disconnectObject(this);
         this.item.destroy();
     }
 
@@ -348,8 +335,10 @@ var NMConnectionDevice = class NMConnectionDevice extends NMConnectionSection {
         this._autoConnectItem = this.item.menu.addAction(_("Connect"), this._autoConnect.bind(this));
         this._deactivateItem = this._radioSection.addAction(_("Turn Off"), 
this.deactivateConnection.bind(this));
 
-        this._stateChangedId = this._device.connect('state-changed', this._deviceStateChanged.bind(this));
-        this._activeConnectionChangedId = this._device.connect('notify::active-connection', 
this._activeConnectionChanged.bind(this));
+        this._device.connectObject(
+            'state-changed', this._deviceStateChanged.bind(this),
+            'notify::active-connection', this._activeConnectionChanged.bind(this),
+            this);
     }
 
     _canReachInternet() {
@@ -365,14 +354,7 @@ var NMConnectionDevice = class NMConnectionDevice extends NMConnectionSection {
     }
 
     destroy() {
-        if (this._stateChangedId) {
-            GObject.signal_handler_disconnect(this._device, this._stateChangedId);
-            this._stateChangedId = 0;
-        }
-        if (this._activeConnectionChangedId) {
-            GObject.signal_handler_disconnect(this._device, this._activeConnectionChangedId);
-            this._activeConnectionChangedId = 0;
-        }
+        this._device.disconnectObject(this);
 
         super.destroy();
     }
@@ -560,15 +542,12 @@ var NMDeviceModem = class extends NMConnectionDevice {
         else if (capabilities & NM.DeviceModemCapabilities.LTE)
             this._mobileDevice = new ModemManager.ModemGsm(device.udi);
 
-        if (this._mobileDevice) {
-            this._operatorNameId = this._mobileDevice.connect('notify::operator-name', 
this._sync.bind(this));
-            this._signalQualityId = this._mobileDevice.connect('notify::signal-quality', () => {
-                this._iconChanged();
-            });
-        }
+        this._mobileDevice?.connectObject(
+            'notify::operator-name', this._sync.bind(this),
+            'notify::signal-quality', () => this._iconChanged(), this);
 
-        this._sessionUpdatedId =
-            Main.sessionMode.connect('updated', this._sessionUpdated.bind(this));
+        Main.sessionMode.connectObject('updated',
+            this._sessionUpdated.bind(this), this);
         this._sessionUpdated();
     }
 
@@ -596,18 +575,8 @@ var NMDeviceModem = class extends NMConnectionDevice {
     }
 
     destroy() {
-        if (this._operatorNameId) {
-            this._mobileDevice.disconnect(this._operatorNameId);
-            this._operatorNameId = 0;
-        }
-        if (this._signalQualityId) {
-            this._mobileDevice.disconnect(this._signalQualityId);
-            this._signalQualityId = 0;
-        }
-        if (this._sessionUpdatedId) {
-            Main.sessionMode.disconnect(this._sessionUpdatedId);
-            this._sessionUpdatedId = 0;
-        }
+        this._mobileDevice?.disconnectObject(this);
+        Main.sessionMode.disconnectObject(this);
 
         super.destroy();
     }
@@ -775,12 +744,12 @@ class NMWirelessDialog extends ModalDialog.ModalDialog {
         this._client = client;
         this._device = device;
 
-        this._wirelessEnabledChangedId = this._client.connect('notify::wireless-enabled',
-                                                              this._syncView.bind(this));
+        this._client.connectObject('notify::wireless-enabled',
+            this._syncView.bind(this), this);
 
         this._rfkill = Rfkill.getRfkillManager();
-        this._airplaneModeChangedId = this._rfkill.connect('airplane-mode-changed',
-                                                           this._syncView.bind(this));
+        this._rfkill.connectObject('airplane-mode-changed',
+            this._syncView.bind(this), this);
 
         this._networks = [];
         this._buildLayout();
@@ -789,9 +758,10 @@ class NMWirelessDialog extends ModalDialog.ModalDialog {
         this._connections = connections.filter(
             connection => device.connection_valid(connection));
 
-        this._apAddedId = device.connect('access-point-added', this._accessPointAdded.bind(this));
-        this._apRemovedId = device.connect('access-point-removed', this._accessPointRemoved.bind(this));
-        this._activeApChangedId = device.connect('notify::active-access-point', 
this._activeApChanged.bind(this));
+        device.connectObject(
+            'access-point-added', this._accessPointAdded.bind(this),
+            'access-point-removed', this._accessPointRemoved.bind(this),
+            'notify::active-access-point', this._activeApChanged.bind(this), this);
 
         // accessPointAdded will also create dialog items
         let accessPoints = device.get_access_points() || [];
@@ -820,27 +790,6 @@ class NMWirelessDialog extends ModalDialog.ModalDialog {
     }
 
     _onDestroy() {
-        if (this._apAddedId) {
-            GObject.Object.prototype.disconnect.call(this._device, this._apAddedId);
-            this._apAddedId = 0;
-        }
-        if (this._apRemovedId) {
-            GObject.Object.prototype.disconnect.call(this._device, this._apRemovedId);
-            this._apRemovedId = 0;
-        }
-        if (this._activeApChangedId) {
-            GObject.Object.prototype.disconnect.call(this._device, this._activeApChangedId);
-            this._activeApChangedId = 0;
-        }
-        if (this._wirelessEnabledChangedId) {
-            this._client.disconnect(this._wirelessEnabledChangedId);
-            this._wirelessEnabledChangedId = 0;
-        }
-        if (this._airplaneModeChangedId) {
-            this._rfkill.disconnect(this._airplaneModeChangedId);
-            this._airplaneModeChangedId = 0;
-        }
-
         if (this._scanTimeoutId) {
             GLib.source_remove(this._scanTimeoutId);
             this._scanTimeoutId = 0;
@@ -1043,8 +992,7 @@ class NMWirelessDialog extends ModalDialog.ModalDialog {
 
     _notifySsidCb(accessPoint) {
         if (accessPoint.get_ssid() != null) {
-            accessPoint.disconnect(accessPoint._notifySsidId);
-            accessPoint._notifySsidId = 0;
+            accessPoint.disconnectObject(this);
             this._accessPointAdded(this._device, accessPoint);
         }
     }
@@ -1168,7 +1116,8 @@ class NMWirelessDialog extends ModalDialog.ModalDialog {
         if (accessPoint.get_ssid() == null) {
             // This access point is not visible yet
             // Wait for it to get a ssid
-            accessPoint._notifySsidId = accessPoint.connect('notify::ssid', this._notifySsidCb.bind(this));
+            accessPoint.connectObject('notify::ssid',
+                this._notifySsidCb.bind(this), this);
             return;
         }
 
@@ -1309,11 +1258,14 @@ var NMDeviceWireless = class {
 
         this.item.menu.addSettingsAction(_("Wi-Fi Settings"), 'gnome-wifi-panel.desktop');
 
-        this._wirelessEnabledChangedId = this._client.connect('notify::wireless-enabled', 
this._sync.bind(this));
-        this._wirelessHwEnabledChangedId = this._client.connect('notify::wireless-hardware-enabled', 
this._sync.bind(this));
-        this._activeApChangedId = this._device.connect('notify::active-access-point', 
this._activeApChanged.bind(this));
-        this._stateChangedId = this._device.connect('state-changed', this._deviceStateChanged.bind(this));
-        this._notifyConnectivityId = this._client.connect('notify::connectivity', 
this._iconChanged.bind(this));
+        this._client.connectObject(
+            'notify::wireless-enabled', this._sync.bind(this),
+            'notify::wireless-hardware-enabled', this._sync.bind(this),
+            'notify::connectivity', this._iconChanged.bind(this), this);
+
+        this._device.connectObject(
+            'notify::active-access-point', this._activeApChanged.bind(this),
+            'state-changed', this._deviceStateChanged.bind(this), this);
 
         this._sync();
     }
@@ -1328,34 +1280,14 @@ var NMDeviceWireless = class {
     }
 
     destroy() {
-        if (this._activeApChangedId) {
-            GObject.signal_handler_disconnect(this._device, this._activeApChangedId);
-            this._activeApChangedId = 0;
-        }
-        if (this._stateChangedId) {
-            GObject.signal_handler_disconnect(this._device, this._stateChangedId);
-            this._stateChangedId = 0;
-        }
-        if (this._strengthChangedId > 0) {
-            this._activeAccessPoint.disconnect(this._strengthChangedId);
-            this._strengthChangedId = 0;
-        }
-        if (this._wirelessEnabledChangedId) {
-            this._client.disconnect(this._wirelessEnabledChangedId);
-            this._wirelessEnabledChangedId = 0;
-        }
-        if (this._wirelessHwEnabledChangedId) {
-            this._client.disconnect(this._wirelessHwEnabledChangedId);
-            this._wirelessHwEnabledChangedId = 0;
-        }
+        this._device.disconnectObject(this);
+        this._activeAccessPoint?.disconnectObject(this);
+        this._client.disconnectObject(this);
+
         if (this._dialog) {
             this._dialog.destroy();
             this._dialog = null;
         }
-        if (this._notifyConnectivityId) {
-            this._client.disconnect(this._notifyConnectivityId);
-            this._notifyConnectivityId = 0;
-        }
 
         this.item.destroy();
     }
@@ -1395,17 +1327,12 @@ var NMDeviceWireless = class {
     }
 
     _activeApChanged() {
-        if (this._activeAccessPoint) {
-            this._activeAccessPoint.disconnect(this._strengthChangedId);
-            this._strengthChangedId = 0;
-        }
+        this._activeAccessPoint?.disconnectObject(this);
 
         this._activeAccessPoint = this._device.active_access_point;
 
-        if (this._activeAccessPoint) {
-            this._strengthChangedId = this._activeAccessPoint.connect('notify::strength',
-                                                                      this._strengthChanged.bind(this));
-        }
+        this._activeAccessPoint?.connectObject('notify::strength',
+            this._strengthChanged.bind(this), this);
 
         this._sync();
     }
@@ -1568,17 +1495,12 @@ var NMVpnConnectionItem = class extends NMConnectionItem {
     }
 
     setActiveConnection(activeConnection) {
-        if (this._activeConnectionChangedId > 0) {
-            this._activeConnection.disconnect(this._activeConnectionChangedId);
-            this._activeConnectionChangedId = 0;
-        }
+        this._activeConnection?.disconnectObject(this);
 
         this._activeConnection = activeConnection;
 
-        if (this._activeConnection) {
-            this._activeConnectionChangedId = this._activeConnection.connect('vpn-state-changed',
-                                                                             
this._connectionStateChanged.bind(this));
-        }
+        this._activeConnection?.connectObject('vpn-state-changed',
+            this._connectionStateChanged.bind(this), this);
 
         this._sync();
     }
@@ -1766,8 +1688,6 @@ class Indicator extends PanelMenu.SystemIndicator {
         this._connectivityQueue = [];
 
         this._mainConnection = null;
-        this._mainConnectionIconChangedId = 0;
-        this._mainConnectionStateChangedId = 0;
 
         this._notification = null;
 
@@ -1919,8 +1839,8 @@ class Indicator extends PanelMenu.SystemIndicator {
     }
 
     _addDeviceWrapper(wrapper) {
-        wrapper._activationFailedId = wrapper.connect('activation-failed',
-                                                      this._onActivationFailed.bind(this));
+        wrapper.connectObject('activation-failed',
+            this._onActivationFailed.bind(this), this);
 
         let section = this._devices[wrapper.category].section;
         section.addMenuItem(wrapper.item);
@@ -1946,7 +1866,7 @@ class Indicator extends PanelMenu.SystemIndicator {
     }
 
     _removeDeviceWrapper(wrapper) {
-        wrapper.disconnect(wrapper._activationFailedId);
+        wrapper.disconnectObject(this);
         wrapper.destroy();
 
         let devices = this._devices[wrapper.category].devices;
@@ -1973,22 +1893,16 @@ class Indicator extends PanelMenu.SystemIndicator {
     }
 
     _syncMainConnection() {
-        if (this._mainConnectionIconChangedId > 0) {
-            this._mainConnection._primaryDevice.disconnect(this._mainConnectionIconChangedId);
-            this._mainConnectionIconChangedId = 0;
-        }
-
-        if (this._mainConnectionStateChangedId > 0) {
-            this._mainConnection.disconnect(this._mainConnectionStateChangedId);
-            this._mainConnectionStateChangedId = 0;
-        }
+        this._mainConnection?._primaryDevice?.disconnectObject(this);
+        this._mainConnection?.disconnectObject(this);
 
         this._mainConnection = this._getMainConnection();
 
         if (this._mainConnection) {
-            if (this._mainConnection._primaryDevice)
-                this._mainConnectionIconChangedId = 
this._mainConnection._primaryDevice.connect('icon-changed', this._updateIcon.bind(this));
-            this._mainConnectionStateChangedId = this._mainConnection.connect('notify::state', 
this._mainConnectionStateChanged.bind(this));
+            this._mainConnection._primaryDevice?.connectObject('icon-changed',
+                this._updateIcon.bind(this), this);
+            this._mainConnection.connectObject('notify::state',
+                this._mainConnectionStateChanged.bind(this), this);
             this._mainConnectionStateChanged();
         }
 
@@ -2028,12 +1942,13 @@ class Indicator extends PanelMenu.SystemIndicator {
     _addConnection(connection) {
         if (this._ignoreConnection(connection))
             return;
-        if (connection._updatedId) {
+        if (this._connections.includes(connection)) {
             // connection was already seen
             return;
         }
 
-        connection._updatedId = connection.connect('changed', this._updateConnection.bind(this));
+        connection.connectObject('changed',
+            this._updateConnection.bind(this), this);
 
         this._updateConnection(connection);
         this._connections.push(connection);
@@ -2068,8 +1983,7 @@ class Indicator extends PanelMenu.SystemIndicator {
             }
         }
 
-        connection.disconnect(connection._updatedId);
-        connection._updatedId = 0;
+        connection.disconnectObject(this);
     }
 
     _updateConnection(connection) {
diff --git a/js/ui/status/thunderbolt.js b/js/ui/status/thunderbolt.js
index de87b9d5ba..348bf643c1 100644
--- a/js/ui/status/thunderbolt.js
+++ b/js/ui/status/thunderbolt.js
@@ -71,7 +71,8 @@ var Client = class {
             log(`error creating bolt proxy: ${e.message}`);
             return;
         }
-        this._propsChangedId = this._proxy.connect('g-properties-changed', 
this._onPropertiesChanged.bind(this));
+        this._proxy.connectObject('g-properties-changed',
+            this._onPropertiesChanged.bind(this), this);
         this._deviceAddedId = this._proxy.connectSignal('DeviceAdded', this._onDeviceAdded.bind(this));
 
         this.probing = this._proxy.Probing;
@@ -102,7 +103,7 @@ var Client = class {
             return;
 
         this._proxy.disconnectSignal(this._deviceAddedId);
-        this._proxy.disconnect(this._propsChangedId);
+        this._proxy.disconnectObject(this);
         this._proxy = null;
     }
 
diff --git a/js/ui/status/volume.js b/js/ui/status/volume.js
index 5bf08ca7f8..7164e10543 100644
--- a/js/ui/status/volume.js
+++ b/js/ui/status/volume.js
@@ -91,15 +91,13 @@ var StreamSlider = class {
     }
 
     _disconnectStream(stream) {
-        stream.disconnect(this._mutedChangedId);
-        this._mutedChangedId = 0;
-        stream.disconnect(this._volumeChangedId);
-        this._volumeChangedId = 0;
+        stream.disconnectObject(this);
     }
 
     _connectStream(stream) {
-        this._mutedChangedId = stream.connect('notify::is-muted', this._updateVolume.bind(this));
-        this._volumeChangedId = stream.connect('notify::volume', this._updateVolume.bind(this));
+        stream.connectObject(
+            'notify::is-muted', this._updateVolume.bind(this),
+            'notify::volume', this._updateVolume.bind(this), this);
     }
 
     _shouldBeVisible() {
@@ -231,7 +229,8 @@ var OutputStreamSlider = class extends StreamSlider {
 
     _connectStream(stream) {
         super._connectStream(stream);
-        this._portChangedId = stream.connect('notify::port', this._portChanged.bind(this));
+        stream.connectObject('notify::port',
+            this._portChanged.bind(this), this);
         this._portChanged();
     }
 
@@ -250,12 +249,6 @@ var OutputStreamSlider = class extends StreamSlider {
         return false;
     }
 
-    _disconnectStream(stream) {
-        super._disconnectStream(stream);
-        stream.disconnect(this._portChangedId);
-        this._portChangedId = 0;
-    }
-
     _updateSliderIcon() {
         this._icon.icon_name = this._hasHeadphones
             ? 'audio-headphones-symbolic'
diff --git a/js/ui/swipeTracker.js b/js/ui/swipeTracker.js
index c93d8bfdc2..0781fec81e 100644
--- a/js/ui/swipeTracker.js
+++ b/js/ui/swipeTracker.js
@@ -112,8 +112,8 @@ const TouchpadSwipeGesture = GObject.registerClass({
             schema_id: 'org.gnome.desktop.peripherals.touchpad',
         });
 
-        this._stageCaptureEvent =
-            global.stage.connect('captured-event::touchpad', this._handleEvent.bind(this));
+        global.stage.connectObject(
+            'captured-event::touchpad', this._handleEvent.bind(this), this);
     }
 
     _handleEvent(actor, event) {
@@ -203,10 +203,7 @@ const TouchpadSwipeGesture = GObject.registerClass({
     }
 
     destroy() {
-        if (this._stageCaptureEvent) {
-            global.stage.disconnect(this._stageCaptureEvent);
-            delete this._stageCaptureEvent;
-        }
+        global.stage.disconnectObject(this);
     }
 });
 
diff --git a/js/ui/switcherPopup.js b/js/ui/switcherPopup.js
index a380c861a3..4b0479b6d7 100644
--- a/js/ui/switcherPopup.js
+++ b/js/ui/switcherPopup.js
@@ -48,8 +48,8 @@ var SwitcherPopup = GObject.registerClass({
 
         Main.uiGroup.add_actor(this);
 
-        this._systemModalOpenedId =
-            Main.layoutManager.connect('system-modal-opened', () => this.destroy());
+        Main.layoutManager.connectObject(
+            'system-modal-opened', () => this.destroy(), this);
 
         this._haveModal = false;
         this._modifierMask = 0;
@@ -337,8 +337,6 @@ var SwitcherPopup = GObject.registerClass({
     _onDestroy() {
         this._popModal();
 
-        Main.layoutManager.disconnect(this._systemModalOpenedId);
-
         if (this._motionTimeoutId != 0)
             GLib.source_remove(this._motionTimeoutId);
         if (this._initialDelayTimeoutId != 0)
diff --git a/js/ui/unlockDialog.js b/js/ui/unlockDialog.js
index 61e277540a..866513c460 100644
--- a/js/ui/unlockDialog.js
+++ b/js/ui/unlockDialog.js
@@ -58,17 +58,13 @@ var NotificationsBox = GObject.registerClass({
         });
         this._updateVisibility();
 
-        this._sourceAddedId = Main.messageTray.connect('source-added', this._sourceAdded.bind(this));
+        Main.messageTray.connectObject('source-added',
+            this._sourceAdded.bind(this), this);
 
         this.connect('destroy', this._onDestroy.bind(this));
     }
 
     _onDestroy() {
-        if (this._sourceAddedId) {
-            Main.messageTray.disconnect(this._sourceAddedId);
-            this._sourceAddedId = 0;
-        }
-
         let items = this._sources.entries();
         for (let [source, obj] of items)
             this._removeSource(source, obj);
@@ -194,10 +190,6 @@ var NotificationsBox = GObject.registerClass({
         let obj = {
             visible: source.policy.showInLockScreen,
             detailed: this._shouldShowDetails(source),
-            sourceDestroyId: 0,
-            sourceCountChangedId: 0,
-            sourceTitleChangedId: 0,
-            sourceUpdatedId: 0,
             sourceBox: null,
             titleLabel: null,
             countLabel: null,
@@ -211,21 +203,19 @@ var NotificationsBox = GObject.registerClass({
         this._showSource(source, obj, obj.sourceBox);
         this._notificationBox.add_child(obj.sourceBox);
 
-        obj.sourceCountChangedId = source.connect('notify::count', () => {
-            this._countChanged(source, obj);
-        });
-        obj.sourceTitleChangedId = source.connect('notify::title', () => {
-            this._titleChanged(source, obj);
-        });
+        source.connectObject(
+            'notify::count', () => this._countChanged(source, obj),
+            'notify::title', () => this._titleChanged(source, obj),
+            'destroy', () => {
+                this._removeSource(source, obj);
+                this._updateVisibility();
+            }, this);
         obj.policyChangedId = source.policy.connect('notify', (policy, pspec) => {
             if (pspec.name === 'show-in-lock-screen')
                 this._visibleChanged(source, obj);
             else
                 this._detailedChanged(source, obj);
         });
-        obj.sourceDestroyId = source.connect('destroy', () => {
-            this._onSourceDestroy(source, obj);
-        });
 
         this._sources.set(source, obj);
 
@@ -307,18 +297,10 @@ var NotificationsBox = GObject.registerClass({
         this._showSource(source, obj, obj.sourceBox);
     }
 
-    _onSourceDestroy(source, obj) {
-        this._removeSource(source, obj);
-        this._updateVisibility();
-    }
-
     _removeSource(source, obj) {
         obj.sourceBox.destroy();
         obj.sourceBox = obj.titleLabel = obj.countLabel = null;
 
-        source.disconnect(obj.sourceDestroyId);
-        source.disconnect(obj.sourceCountChangedId);
-        source.disconnect(obj.sourceTitleChangedId);
         source.policy.disconnect(obj.policyChangedId);
 
         this._sources.delete(source);
@@ -352,12 +334,12 @@ class UnlockDialogClock extends St.BoxLayout {
         this._wallClock.connect('notify::clock', this._updateClock.bind(this));
 
         this._seat = Clutter.get_default_backend().get_default_seat();
-        this._touchModeChangedId = this._seat.connect('notify::touch-mode',
-            this._updateHint.bind(this));
+        this._seat.connectObject('notify::touch-mode',
+            this._updateHint.bind(this), this);
 
         this._monitorManager = Meta.MonitorManager.get();
-        this._powerModeChangedId = this._monitorManager.connect(
-            'power-save-mode-changed', () => (this._hint.opacity = 0));
+        this._monitorManager.connectObject('power-save-mode-changed',
+            () => (this._hint.opacity = 0), this);
 
         this._idleMonitor = global.backend.get_core_idle_monitor();
         this._idleWatchId = this._idleMonitor.add_idle_watch(HINT_TIMEOUT * 1000, () => {
@@ -392,9 +374,7 @@ class UnlockDialogClock extends St.BoxLayout {
     _onDestroy() {
         this._wallClock.run_dispose();
 
-        this._seat.disconnect(this._touchModeChangedId);
         this._idleMonitor.remove_watch(this._idleWatchId);
-        this._monitorManager.disconnect(this._powerModeChangedId);
     }
 });
 
@@ -545,12 +525,12 @@ var UnlockDialog = GObject.registerClass({
         this._bgManagers = [];
 
         const themeContext = St.ThemeContext.get_for_stage(global.stage);
-        this._scaleChangedId = themeContext.connect('notify::scale-factor',
-            () => this._updateBackgroundEffects());
+        themeContext.connectObject('notify::scale-factor',
+            () => this._updateBackgroundEffects(), this);
 
         this._updateBackgrounds();
-        this._monitorsChangedId =
-            Main.layoutManager.connect('monitors-changed', this._updateBackgrounds.bind(this));
+        Main.layoutManager.connectObject('monitors-changed',
+            this._updateBackgrounds.bind(this), this);
 
         this._userManager = AccountsService.UserManager.get_default();
         this._userName = GLib.get_user_name();
@@ -593,15 +573,15 @@ var UnlockDialog = GObject.registerClass({
 
         this._screenSaverSettings = new Gio.Settings({ schema_id: 'org.gnome.desktop.screensaver' });
 
-        this._userSwitchEnabledId = this._screenSaverSettings.connect('changed::user-switch-enabled',
-            this._updateUserSwitchVisibility.bind(this));
+        this._screenSaverSettings.connectObject('changed::user-switch-enabled',
+            this._updateUserSwitchVisibility.bind(this), this);
 
         this._lockdownSettings = new Gio.Settings({ schema_id: 'org.gnome.desktop.lockdown' });
         this._lockdownSettings.connect('changed::disable-user-switching',
             this._updateUserSwitchVisibility.bind(this));
 
-        this._userLoadedId = this._user.connect('notify::is-loaded',
-            this._updateUserSwitchVisibility.bind(this));
+        this._user.connectObject('notify::is-loaded',
+            this._updateUserSwitchVisibility.bind(this), this);
 
         this._updateUserSwitchVisibility();
 
@@ -850,31 +830,10 @@ var UnlockDialog = GObject.registerClass({
             this._idleWatchId = 0;
         }
 
-        if (this._monitorsChangedId) {
-            Main.layoutManager.disconnect(this._monitorsChangedId);
-            delete this._monitorsChangedId;
-        }
-
-        let themeContext = St.ThemeContext.get_for_stage(global.stage);
-        if (this._scaleChangedId) {
-            themeContext.disconnect(this._scaleChangedId);
-            delete this._scaleChangedId;
-        }
-
         if (this._gdmClient) {
             this._gdmClient = null;
             delete this._gdmClient;
         }
-
-        if (this._userLoadedId) {
-            this._user.disconnect(this._userLoadedId);
-            this._userLoadedId = 0;
-        }
-
-        if (this._userSwitchEnabledId) {
-            this._screenSaverSettings.disconnect(this._userSwitchEnabledId);
-            this._userSwitchEnabledId = 0;
-        }
     }
 
     _updateUserSwitchVisibility() {
diff --git a/js/ui/userWidget.js b/js/ui/userWidget.js
index aa1de12543..76139e1f25 100644
--- a/js/ui/userWidget.js
+++ b/js/ui/userWidget.js
@@ -40,10 +40,7 @@ class Avatar extends St.Bin {
             GObject.BindingFlags.SYNC_CREATE);
 
         // Monitor the scaling factor to make sure we recreate the avatar when needed.
-        this._scaleFactorChangeId =
-            themeContext.connect('notify::scale-factor', this.update.bind(this));
-
-        this.connect('destroy', this._onDestroy.bind(this));
+        themeContext.connectObject('notify::scale-factor', this.update.bind(this), this);
     }
 
     vfunc_style_changed() {
@@ -63,14 +60,6 @@ class Avatar extends St.Bin {
         this.update();
     }
 
-    _onDestroy() {
-        if (this._scaleFactorChangeId) {
-            let themeContext = St.ThemeContext.get_for_stage(global.stage);
-            themeContext.disconnect(this._scaleFactorChangeId);
-            delete this._scaleFactorChangeId;
-        }
-    }
-
     setSensitive(sensitive) {
         this.reactive = sensitive;
     }
@@ -125,28 +114,10 @@ class UserWidgetLabel extends St.Widget {
 
         this._currentLabel = null;
 
-        this._userLoadedId = this._user.connect('notify::is-loaded', this._updateUser.bind(this));
-        this._userChangedId = this._user.connect('changed', this._updateUser.bind(this));
+        this._user.connectObject(
+            'notify::is-loaded', this._updateUser.bind(this),
+            'changed', this._updateUser.bind(this), this);
         this._updateUser();
-
-        // We can't override the destroy vfunc because that might be called during
-        // object finalization, and we can't call any JS inside a GC finalize callback,
-        // so we use a signal, that will be disconnected by GObject the first time
-        // the actor is destroyed (which is guaranteed to be as part of a normal
-        // destroy() call from JS, possibly from some ancestor)
-        this.connect('destroy', this._onDestroy.bind(this));
-    }
-
-    _onDestroy() {
-        if (this._userLoadedId != 0) {
-            this._user.disconnect(this._userLoadedId);
-            this._userLoadedId = 0;
-        }
-
-        if (this._userChangedId != 0) {
-            this._user.disconnect(this._userChangedId);
-            this._userChangedId = 0;
-        }
     }
 
     vfunc_allocate(box) {
@@ -207,8 +178,6 @@ class UserWidget extends St.BoxLayout {
             xAlign,
         });
 
-        this.connect('destroy', this._onDestroy.bind(this));
-
         this._avatar = new Avatar(user);
         this._avatar.x_align = Clutter.ActorAlign.CENTER;
         this.add_child(this._avatar);
@@ -222,8 +191,9 @@ class UserWidget extends St.BoxLayout {
             this._label.bind_property('label-actor', this, 'label-actor',
                                       GObject.BindingFlags.SYNC_CREATE);
 
-            this._userLoadedId = this._user.connect('notify::is-loaded', this._updateUser.bind(this));
-            this._userChangedId = this._user.connect('changed', this._updateUser.bind(this));
+            this._user.connectObject(
+                'notify::is-loaded', this._updateUser.bind(this),
+                'changed', this._updateUser.bind(this), this);
         } else {
             this._label = new St.Label({
                 style_class: 'user-widget-label',
@@ -236,18 +206,6 @@ class UserWidget extends St.BoxLayout {
         this._updateUser();
     }
 
-    _onDestroy() {
-        if (this._userLoadedId != 0) {
-            this._user.disconnect(this._userLoadedId);
-            this._userLoadedId = 0;
-        }
-
-        if (this._userChangedId != 0) {
-            this._user.disconnect(this._userChangedId);
-            this._userChangedId = 0;
-        }
-    }
-
     _updateUser() {
         this._avatar.update();
     }
diff --git a/js/ui/windowAttentionHandler.js b/js/ui/windowAttentionHandler.js
index 346fad88d1..8da30498ff 100644
--- a/js/ui/windowAttentionHandler.js
+++ b/js/ui/windowAttentionHandler.js
@@ -9,10 +9,10 @@ const MessageTray = imports.ui.messageTray;
 var WindowAttentionHandler = class {
     constructor() {
         this._tracker = Shell.WindowTracker.get_default();
-        this._windowDemandsAttentionId = global.display.connect('window-demands-attention',
-                                                                this._onWindowDemandsAttention.bind(this));
-        this._windowMarkedUrgentId = global.display.connect('window-marked-urgent',
-                                                            this._onWindowDemandsAttention.bind(this));
+        global.display.connectObject(
+            'window-demands-attention', this._onWindowDemandsAttention.bind(this),
+            'window-marked-urgent', this._onWindowDemandsAttention.bind(this),
+            this);
     }
 
     _getTitleAndBanner(app, window) {
@@ -47,10 +47,10 @@ var WindowAttentionHandler = class {
 
         source.showNotification(notification);
 
-        source.signalIDs.push(window.connect('notify::title', () => {
+        window.connectObject('notify::title', () => {
             [title, banner] = this._getTitleAndBanner(app, window);
             notification.update(title, banner);
-        }));
+        }, source);
     }
 };
 
@@ -62,15 +62,11 @@ class WindowAttentionSource extends MessageTray.Source {
 
         super._init(app.get_name());
 
-        this.signalIDs = [];
-        this.signalIDs.push(this._window.connect('notify::demands-attention',
-                                                 this._sync.bind(this)));
-        this.signalIDs.push(this._window.connect('notify::urgent',
-                                                 this._sync.bind(this)));
-        this.signalIDs.push(this._window.connect('focus',
-                                                 () => this.destroy()));
-        this.signalIDs.push(this._window.connect('unmanaged',
-                                                 () => this.destroy()));
+        this._window.connectObject(
+            'notify::demands-attention', this._sync.bind(this),
+            'notify::urgent', this._sync.bind(this),
+            'focus', () => this.destroy(),
+            'unmanaged', () => this.destroy(), this);
     }
 
     _sync() {
@@ -93,9 +89,7 @@ class WindowAttentionSource extends MessageTray.Source {
     }
 
     destroy(params) {
-        for (let i = 0; i < this.signalIDs.length; i++)
-            this._window.disconnect(this.signalIDs[i]);
-        this.signalIDs = [];
+        this._window.disconnectObject(this);
 
         super.destroy(params);
     }
diff --git a/js/ui/windowManager.js b/js/ui/windowManager.js
index de8e4cec14..cdd32dcf5c 100644
--- a/js/ui/windowManager.js
+++ b/js/ui/windowManager.js
@@ -356,9 +356,9 @@ var WorkspaceTracker = class {
                 this._workspaces[w] = workspaceManager.get_workspace_by_index(w);
 
             for (w = oldNumWorkspaces; w < newNumWorkspaces; w++) {
-                let workspace = this._workspaces[w];
-                workspace._windowAddedId = workspace.connect('window-added', 
this._queueCheckWorkspaces.bind(this));
-                workspace._windowRemovedId = workspace.connect('window-removed', 
this._windowRemoved.bind(this));
+                this._workspaces[w].connectObject(
+                    'window-added', this._queueCheckWorkspaces.bind(this),
+                    'window-removed', this._windowRemoved.bind(this), this);
             }
         } else {
             // Assume workspaces are only removed sequentially
@@ -374,10 +374,7 @@ var WorkspaceTracker = class {
             }
 
             let lostWorkspaces = this._workspaces.splice(removedIndex, removedNum);
-            lostWorkspaces.forEach(workspace => {
-                workspace.disconnect(workspace._windowAddedId);
-                workspace.disconnect(workspace._windowRemovedId);
-            });
+            lostWorkspaces.forEach(workspace => workspace.disconnectObject(this));
         }
 
         this._queueCheckWorkspaces();
@@ -1307,16 +1304,14 @@ var WindowManager = class {
             this._shellwm.completed_size_change(actor);
         }
 
-        let destroyId = actor.connect('destroy', () => {
-            this._clearAnimationInfo(actor);
-        });
+        actor.connectObject('destroy',
+            () => this._clearAnimationInfo(actor), actorClone);
 
         this._resizePending.add(actor);
         actor.__animationInfo = {
             clone: actorClone,
             oldRect: oldFrameRect,
             frozen: true,
-            destroyId,
         };
     }
 
@@ -1381,7 +1376,6 @@ var WindowManager = class {
     _clearAnimationInfo(actor) {
         if (actor.__animationInfo) {
             actor.__animationInfo.clone.destroy();
-            actor.disconnect(actor.__animationInfo.destroyId);
             if (actor.__animationInfo.frozen)
                 actor.thaw();
 
@@ -1457,20 +1451,19 @@ var WindowManager = class {
 
     async _mapWindow(shellwm, actor) {
         actor._windowType = actor.meta_window.get_window_type();
-        actor._notifyWindowTypeSignalId =
-            actor.meta_window.connect('notify::window-type', () => {
-                let type = actor.meta_window.get_window_type();
-                if (type == actor._windowType)
-                    return;
-                if (type == Meta.WindowType.MODAL_DIALOG ||
-                    actor._windowType == Meta.WindowType.MODAL_DIALOG) {
-                    let parent = actor.get_meta_window().get_transient_for();
-                    if (parent)
-                        this._checkDimming(parent);
-                }
+        actor.meta_window.connectObject('notify::window-type', () => {
+            let type = actor.meta_window.get_window_type();
+            if (type === actor._windowType)
+                return;
+            if (type === Meta.WindowType.MODAL_DIALOG ||
+                actor._windowType === Meta.WindowType.MODAL_DIALOG) {
+                let parent = actor.get_meta_window().get_transient_for();
+                if (parent)
+                    this._checkDimming(parent);
+            }
 
-                actor._windowType = type;
-            });
+            actor._windowType = type;
+        }, actor);
         actor.meta_window.connect('unmanaged', window => {
             let parent = window.get_transient_for();
             if (parent)
@@ -1547,10 +1540,7 @@ var WindowManager = class {
 
     _destroyWindow(shellwm, actor) {
         let window = actor.meta_window;
-        if (actor._notifyWindowTypeSignalId) {
-            window.disconnect(actor._notifyWindowTypeSignalId);
-            actor._notifyWindowTypeSignalId = 0;
-        }
+        window.disconnectObject(actor);
         if (window._dimmed) {
             this._dimmedWindows =
                 this._dimmedWindows.filter(win => win != window);
@@ -1590,10 +1580,10 @@ var WindowManager = class {
 
             if (window.is_attached_dialog()) {
                 let parent = window.get_transient_for();
-                actor._parentDestroyId = parent.connect('unmanaged', () => {
+                parent.connectObject('unmanaged', () => {
                     actor.remove_all_transitions();
                     this._destroyWindowDone(shellwm, actor);
-                });
+                }, actor);
             }
 
             actor.ease({
@@ -1611,10 +1601,7 @@ var WindowManager = class {
     _destroyWindowDone(shellwm, actor) {
         if (this._destroying.delete(actor)) {
             const parent = actor.get_meta_window()?.get_transient_for();
-            if (parent && actor._parentDestroyId) {
-                parent.disconnect(actor._parentDestroyId);
-                actor._parentDestroyId = 0;
-            }
+            parent?.disconnectObject(actor);
             shellwm.completed_destroy(actor);
         }
     }
diff --git a/js/ui/windowPreview.js b/js/ui/windowPreview.js
index 16bda2a7ff..373000075d 100644
--- a/js/ui/windowPreview.js
+++ b/js/ui/windowPreview.js
@@ -94,8 +94,7 @@ var WindowPreview = GObject.registerClass({
                     this.emit('size-changed');
             });
 
-        this._windowDestroyId =
-            this._windowActor.connect('destroy', () => this.destroy());
+        this._windowActor.connectObject('destroy', () => this.destroy(), this);
 
         this._updateAttachedDialogs();
 
@@ -177,9 +176,9 @@ var WindowPreview = GObject.registerClass({
         }));
         this._title.clutter_text.ellipsize = Pango.EllipsizeMode.END;
         this.label_actor = this._title;
-        this._updateCaptionId = this.metaWindow.connect('notify::title', () => {
-            this._title.text = this._getCaption();
-        });
+        this.metaWindow.connectObject(
+            'notify::title', () => (this._title.text = this._getCaption()),
+            this);
 
         const layout = Meta.prefs_get_button_layout();
         this._closeButtonSide =
@@ -213,10 +212,8 @@ var WindowPreview = GObject.registerClass({
         this.add_child(this._icon);
         this.add_child(this._closeButton);
 
-        this._adjustmentChangedId =
-            this._overviewAdjustment.connect('notify::value', () => {
-                this._updateIconScale();
-            });
+        this._overviewAdjustment.connectObject(
+            'notify::value', () => this._updateIconScale(), this);
         this._updateIconScale();
 
         this.connect('notify::realized', () => {
@@ -526,13 +523,9 @@ var WindowPreview = GObject.registerClass({
     }
 
     _onDestroy() {
-        this._windowActor.disconnect(this._windowDestroyId);
-
         this.metaWindow._delegate = null;
         this._delegate = null;
 
-        this.metaWindow.disconnect(this._updateCaptionId);
-
         if (this._longPressLater) {
             Meta.later_remove(this._longPressLater);
             delete this._longPressLater;
@@ -543,11 +536,6 @@ var WindowPreview = GObject.registerClass({
             this._idleHideOverlayId = 0;
         }
 
-        if (this._adjustmentChangedId > 0) {
-            this._overviewAdjustment.disconnect(this._adjustmentChangedId);
-            this._adjustmentChangedId = 0;
-        }
-
         if (this.inDrag) {
             this.emit('drag-end');
             this.inDrag = false;
diff --git a/js/ui/workspace.js b/js/ui/workspace.js
index bba56f9267..89f89dab92 100644
--- a/js/ui/workspace.js
+++ b/js/ui/workspace.js
@@ -941,10 +941,10 @@ class WorkspaceBackground extends St.Widget {
         this._workarea = Main.layoutManager.getWorkAreaForMonitor(monitorIndex);
 
         this._stateAdjustment = stateAdjustment;
-        this._adjustmentId = stateAdjustment.connect('notify::value', () => {
+        this._stateAdjustment.connectObject('notify::value', () => {
             this._updateBorderRadius();
             this.queue_relayout();
-        });
+        }, this);
 
         this._bin = new Clutter.Actor({
             layout_manager: new Clutter.BinLayout(),
@@ -966,12 +966,11 @@ class WorkspaceBackground extends St.Widget {
             useContentSize: false,
         });
 
-        this._workareasChangedId =
-            global.display.connect('workareas-changed', () => {
-                this._workarea = Main.layoutManager.getWorkAreaForMonitor(monitorIndex);
-                this._updateRoundedClipBounds();
-                this.queue_relayout();
-            });
+        global.display.connectObject('workareas-changed', () => {
+            this._workarea = Main.layoutManager.getWorkAreaForMonitor(monitorIndex);
+            this._updateRoundedClipBounds();
+            this.queue_relayout();
+        }, this);
         this._updateRoundedClipBounds();
 
         this._updateBorderRadius();
@@ -1051,16 +1050,6 @@ class WorkspaceBackground extends St.Widget {
             this._bgManager.destroy();
             this._bgManager = null;
         }
-
-        if (this._workareasChangedId) {
-            global.display.disconnect(this._workareasChangedId);
-            delete this._workareasChangedId;
-        }
-
-        if (this._adjustmentId) {
-            this._stateAdjustment.disconnect(this._adjustmentId);
-            delete this._adjustmentId;
-        }
     }
 });
 
@@ -1094,10 +1083,6 @@ class Workspace extends St.Widget {
         this.add_child(this._container);
 
         this.metaWorkspace = metaWorkspace;
-        this._activeWorkspaceChangedId =
-            this.metaWorkspace?.connect('notify::active', () => {
-                layoutManager.syncOverlays();
-            });
 
         this._overviewAdjustment = overviewAdjustment;
 
@@ -1138,16 +1123,14 @@ class Workspace extends St.Widget {
         }
 
         // Track window changes, but let the window tracker process them first
-        if (this.metaWorkspace) {
-            this._windowAddedId = this.metaWorkspace.connect_after(
-                'window-added', this._windowAdded.bind(this));
-            this._windowRemovedId = this.metaWorkspace.connect_after(
-                'window-removed', this._windowRemoved.bind(this));
-        }
-        this._windowEnteredMonitorId = global.display.connect_after(
-            'window-entered-monitor', this._windowEnteredMonitor.bind(this));
-        this._windowLeftMonitorId = global.display.connect_after(
-            'window-left-monitor', this._windowLeftMonitor.bind(this));
+        this.metaWorkspace?.connectObject(
+            'window-added', this._windowAdded.bind(this), GObject.ConnectFlags.AFTER,
+            'window-removed', this._windowRemoved.bind(this), GObject.ConnectFlags.AFTER,
+            'notify::active', () => layoutManager.syncOverlays(), this);
+        global.display.connectObject(
+            'window-entered-monitor', this._windowEnteredMonitor.bind(this), GObject.ConnectFlags.AFTER,
+            'window-left-monitor', this._windowLeftMonitor.bind(this), GObject.ConnectFlags.AFTER,
+            this);
         this._layoutFrozenId = 0;
 
         // DND requires this to be set
@@ -1347,25 +1330,13 @@ class Workspace extends St.Widget {
         }
 
         this._container.layout_manager.layout_frozen = true;
-        this._overviewHiddenId = Main.overview.connect('hidden', this._doneLeavingOverview.bind(this));
+        Main.overview.connectObject(
+            'hidden', this._doneLeavingOverview.bind(this), this);
     }
 
     _onDestroy() {
         this._clearSkipTaskbarSignals();
 
-        if (this._overviewHiddenId) {
-            Main.overview.disconnect(this._overviewHiddenId);
-            this._overviewHiddenId = 0;
-        }
-
-        if (this.metaWorkspace) {
-            this.metaWorkspace.disconnect(this._windowAddedId);
-            this.metaWorkspace.disconnect(this._windowRemovedId);
-            this.metaWorkspace.disconnect(this._activeWorkspaceChangedId);
-        }
-        global.display.disconnect(this._windowEnteredMonitorId);
-        global.display.disconnect(this._windowLeftMonitorId);
-
         if (this._layoutFrozenId > 0) {
             GLib.source_remove(this._layoutFrozenId);
             this._layoutFrozenId = 0;
diff --git a/js/ui/workspaceAnimation.js b/js/ui/workspaceAnimation.js
index 25144ab3ac..b807f35778 100644
--- a/js/ui/workspaceAnimation.js
+++ b/js/ui/workspaceAnimation.js
@@ -40,8 +40,8 @@ class WorkspaceGroup extends Clutter.Actor {
         this._createWindows();
 
         this.connect('destroy', this._onDestroy.bind(this));
-        this._restackedId = global.display.connect('restacked',
-            this._syncStacking.bind(this));
+        global.display.connectObject('restacked',
+            this._syncStacking.bind(this), this);
     }
 
     get workspace() {
@@ -99,26 +99,23 @@ class WorkspaceGroup extends Clutter.Actor {
 
             const record = { windowActor, clone };
 
-            record.windowDestroyId = windowActor.connect('destroy', () => {
+            windowActor.connectObject('destroy', () => {
                 clone.destroy();
                 this._windowRecords.splice(this._windowRecords.indexOf(record), 1);
-            });
+            }, this);
 
             this._windowRecords.push(record);
         }
     }
 
     _removeWindows() {
-        for (const record of this._windowRecords) {
-            record.windowActor.disconnect(record.windowDestroyId);
+        for (const record of this._windowRecords)
             record.clone.destroy();
-        }
 
         this._windowRecords = [];
     }
 
     _onDestroy() {
-        global.display.disconnect(this._restackedId);
         this._removeWindows();
 
         if (this._workspace)
diff --git a/js/ui/workspaceSwitcherPopup.js b/js/ui/workspaceSwitcherPopup.js
index 23bb983167..87445298be 100644
--- a/js/ui/workspaceSwitcherPopup.js
+++ b/js/ui/workspaceSwitcherPopup.js
@@ -38,11 +38,9 @@ class WorkspaceSwitcherPopup extends Clutter.Actor {
         this.hide();
 
         let workspaceManager = global.workspace_manager;
-        this._workspaceManagerSignals = [];
-        this._workspaceManagerSignals.push(workspaceManager.connect('workspace-added',
-                                                                    this._redisplay.bind(this)));
-        this._workspaceManagerSignals.push(workspaceManager.connect('workspace-removed',
-                                                                    this._redisplay.bind(this)));
+        workspaceManager.connectObject(
+            'workspace-added', this._redisplay.bind(this),
+            'workspace-removed', this._redisplay.bind(this), this);
 
         this.connect('destroy', this._onDestroy.bind(this));
     }
@@ -99,11 +97,5 @@ class WorkspaceSwitcherPopup extends Clutter.Actor {
         if (this._timeoutId)
             GLib.source_remove(this._timeoutId);
         this._timeoutId = 0;
-
-        let workspaceManager = global.workspace_manager;
-        for (let i = 0; i < this._workspaceManagerSignals.length; i++)
-            workspaceManager.disconnect(this._workspaceManagerSignals[i]);
-
-        this._workspaceManagerSignals = [];
     }
 });
diff --git a/js/ui/workspaceThumbnail.js b/js/ui/workspaceThumbnail.js
index 69fc7c5bd2..85456fb474 100644
--- a/js/ui/workspaceThumbnail.js
+++ b/js/ui/workspaceThumbnail.js
@@ -64,14 +64,14 @@ var WindowClone = GObject.registerClass({
         this.realWindow = realWindow;
         this.metaWindow = realWindow.meta_window;
 
-        clone._updateId = this.realWindow.connect('notify::position',
-                                                  this._onPositionChanged.bind(this));
-        clone._destroyId = this.realWindow.connect('destroy', () => {
-            // First destroy the clone and then destroy everything
-            // This will ensure that we never see it in the _disconnectSignals loop
-            clone.destroy();
-            this.destroy();
-        });
+        this.realWindow.connectObject(
+            'notify::position', this._onPositionChanged.bind(this),
+            'destroy', () => {
+                // First destroy the clone and then destroy everything
+                // This will ensure that we never see it in the _disconnectSignals loop
+                clone.destroy();
+                this.destroy();
+            }, this);
         this._onPositionChanged();
 
         this.connect('destroy', this._onDestroy.bind(this));
@@ -142,12 +142,9 @@ var WindowClone = GObject.registerClass({
         let clone = new Clutter.Clone({ source: realDialog });
         this._updateDialogPosition(realDialog, clone);
 
-        clone._updateId = realDialog.connect('notify::position', dialog => {
-            this._updateDialogPosition(dialog, clone);
-        });
-        clone._destroyId = realDialog.connect('destroy', () => {
-            clone.destroy();
-        });
+        realDialog.connectObject(
+            'notify::position', dialog => this._updateDialogPosition(dialog, clone),
+            'destroy', () => clone.destroy(), this);
         this.add_child(clone);
     }
 
@@ -163,18 +160,7 @@ var WindowClone = GObject.registerClass({
         this.set_position(this.realWindow.x, this.realWindow.y);
     }
 
-    _disconnectSignals() {
-        this.get_children().forEach(child => {
-            let realWindow = child.source;
-
-            realWindow.disconnect(child._updateId);
-            realWindow.disconnect(child._destroyId);
-        });
-    }
-
     _onDestroy() {
-        this._disconnectSignals();
-
         this._delegate = null;
 
         if (this.inDrag) {
@@ -291,27 +277,22 @@ var WorkspaceThumbnail = GObject.registerClass({
         // Create clones for windows that should be visible in the Overview
         this._windows = [];
         this._allWindows = [];
-        this._minimizedChangedIds = [];
         for (let i = 0; i < windows.length; i++) {
-            let minimizedChangedId =
-                windows[i].meta_window.connect('notify::minimized',
-                                               this._updateMinimized.bind(this));
+            windows[i].meta_window.connectObject('notify::minimized',
+                this._updateMinimized.bind(this), this);
             this._allWindows.push(windows[i].meta_window);
-            this._minimizedChangedIds.push(minimizedChangedId);
 
             if (this._isMyWindow(windows[i]) && this._isOverviewWindow(windows[i]))
                 this._addWindowClone(windows[i]);
         }
 
         // Track window changes
-        this._windowAddedId = this.metaWorkspace.connect('window-added',
-                                                         this._windowAdded.bind(this));
-        this._windowRemovedId = this.metaWorkspace.connect('window-removed',
-                                                           this._windowRemoved.bind(this));
-        this._windowEnteredMonitorId = global.display.connect('window-entered-monitor',
-                                                              this._windowEnteredMonitor.bind(this));
-        this._windowLeftMonitorId = global.display.connect('window-left-monitor',
-                                                           this._windowLeftMonitor.bind(this));
+        this.metaWorkspace.connectObject(
+            'window-added', this._windowAdded.bind(this),
+            'window-removed', this._windowRemoved.bind(this), this);
+        global.display.connectObject(
+            'window-entered-monitor', this._windowEnteredMonitor.bind(this),
+            'window-left-monitor', this._windowLeftMonitor.bind(this), this);
 
         this.state = ThumbnailState.NORMAL;
         this._slidePosition = 0; // Fully slid in
@@ -397,10 +378,9 @@ var WorkspaceThumbnail = GObject.registerClass({
         }
 
         if (!this._allWindows.includes(metaWin)) {
-            let minimizedChangedId = metaWin.connect('notify::minimized',
-                                                     this._updateMinimized.bind(this));
+            metaWin.connectObject('notify::minimized',
+                this._updateMinimized.bind(this), this);
             this._allWindows.push(metaWin);
-            this._minimizedChangedIds.push(minimizedChangedId);
         }
 
         // We might have the window in our list already if it was on all workspaces and
@@ -437,9 +417,8 @@ var WorkspaceThumbnail = GObject.registerClass({
     _windowRemoved(metaWorkspace, metaWin) {
         let index = this._allWindows.indexOf(metaWin);
         if (index != -1) {
-            metaWin.disconnect(this._minimizedChangedIds[index]);
+            metaWin.disconnectObject(this);
             this._allWindows.splice(index, 1);
-            this._minimizedChangedIds.splice(index, 1);
         }
 
         this._doRemoveWindow(metaWin);
@@ -468,13 +447,9 @@ var WorkspaceThumbnail = GObject.registerClass({
 
         this._removed = true;
 
-        this.metaWorkspace.disconnect(this._windowAddedId);
-        this.metaWorkspace.disconnect(this._windowRemovedId);
-        global.display.disconnect(this._windowEnteredMonitorId);
-        global.display.disconnect(this._windowLeftMonitorId);
-
-        for (let i = 0; i < this._allWindows.length; i++)
-            this._allWindows[i].disconnect(this._minimizedChangedIds[i]);
+        this.metaWorkspace.disconnectObject(this);
+        global.display.disconnectObject(this);
+        this._allWindows.forEach(w => w.disconnectObject(this));
     }
 
     _onDestroy() {
@@ -667,40 +642,30 @@ var ThumbnailsBox = GObject.registerClass({
 
         this._thumbnails = [];
 
-        this._overviewSignals = [
-            Main.overview.connect('showing',
-                () => this._createThumbnails()),
-            Main.overview.connect('hidden',
-                () => this._destroyThumbnails()),
-            Main.overview.connect('item-drag-begin',
-                () => this._onDragBegin()),
-            Main.overview.connect('item-drag-end',
-                () => this._onDragEnd()),
-            Main.overview.connect('item-drag-cancelled',
-                () => this._onDragCancelled()),
-            Main.overview.connect('window-drag-begin',
-                () => this._onDragBegin()),
-            Main.overview.connect('window-drag-end',
-                () => this._onDragEnd()),
-            Main.overview.connect('window-drag-cancelled',
-                () => this._onDragCancelled()),
-        ];
+        Main.overview.connectObject(
+            'showing', () => this._createThumbnails(),
+            'hidden', () => this._destroyThumbnails(),
+            'item-drag-begin', () => this._onDragBegin(),
+            'item-drag-end', () => this._onDragEnd(),
+            'item-drag-cancelled', () => this._onDragCancelled(),
+            'window-drag-begin', () => this._onDragBegin(),
+            'window-drag-end', () => this._onDragEnd(),
+            'window-drag-cancelled', () => this._onDragCancelled(), this);
 
         this._settings = new Gio.Settings({ schema_id: MUTTER_SCHEMA });
         this._settings.connect('changed::dynamic-workspaces',
             () => this._updateShouldShow());
         this._updateShouldShow();
 
-        this._monitorsChangedId =
-            Main.layoutManager.connect('monitors-changed', () => {
-                this._destroyThumbnails();
-                if (Main.overview.visible)
-                    this._createThumbnails();
-            });
+        Main.layoutManager.connectObject('monitors-changed', () => {
+            this._destroyThumbnails();
+            if (Main.overview.visible)
+                this._createThumbnails();
+        }, this);
 
         // The porthole is the part of the screen we're showing in the thumbnails
-        this._workareasChangedId = global.display.connect('workareas-changed',
-            () => this._updatePorthole());
+        global.display.connectObject('workareas-changed',
+            () => this._updatePorthole(), this);
         this._updatePorthole();
 
         this.connect('notify::visible', () => {
@@ -714,8 +679,8 @@ var ThumbnailsBox = GObject.registerClass({
         this._syncStackingId = 0;
 
         this._scrollAdjustment = scrollAdjustment;
-        this._scrollValueId = this._scrollAdjustment.connect('notify::value',
-            () => this._updateIndicator());
+        this._scrollAdjustment.connectObject('notify::value',
+            () => this._updateIndicator(), this);
     }
 
     setMonitorIndex(monitorIndex) {
@@ -726,21 +691,6 @@ var ThumbnailsBox = GObject.registerClass({
         this._destroyThumbnails();
         this._unqueueUpdateStates();
 
-        if (this._scrollValueId)
-            this._scrollAdjustment.disconnect(this._scrollValueId);
-        this._scrollValueId = 0;
-
-        if (this._monitorsChangedId)
-            Main.layoutManager.disconnect(this._monitorsChangedId);
-        this._monitorsChangedId = 0;
-
-        if (this._workareasChangedId)
-            global.display.disconnect(this._workareasChangedId);
-        this._workareasChangedId = 0;
-
-        this._overviewSignals.forEach(id => Main.overview.disconnect(id));
-        this._overviewSignals = [];
-
         if (this._settings)
             this._settings.run_dispose();
         this._settings = null;
@@ -1002,24 +952,18 @@ var ThumbnailsBox = GObject.registerClass({
         if (this._thumbnails.length > 0)
             return;
 
-        let workspaceManager = global.workspace_manager;
-
-        this._nWorkspacesNotifyId =
-            workspaceManager.connect('notify::n-workspaces',
-                                     this._workspacesChanged.bind(this));
-        this._activeWorkspaceChangedId =
-            workspaceManager.connect('active-workspace-changed',
-                () => this._updateIndicator());
-        this._workspacesReorderedId =
-            workspaceManager.connect('workspaces-reordered', () => {
+        const { workspaceManager } = global;
+        workspaceManager.connectObject(
+            'notify::n-workspaces', this._workspacesChanged.bind(this),
+            'active-workspace-changed', () => this._updateIndicator(),
+            'workspaces-reordered', () => {
                 this._thumbnails.sort((a, b) => {
                     return a.metaWorkspace.index() - b.metaWorkspace.index();
                 });
                 this.queue_relayout();
-            });
-        this._syncStackingId =
-            Main.overview.connect('windows-restacked',
-                                  this._syncStacking.bind(this));
+            }, this);
+        Main.overview.connectObject('windows-restacked',
+            this._syncStacking.bind(this), this);
 
         this._targetScale = 0;
         this._scale = 0;
@@ -1039,25 +983,8 @@ var ThumbnailsBox = GObject.registerClass({
         if (this._thumbnails.length == 0)
             return;
 
-        const { workspaceManager } = global;
-
-        if (this._nWorkspacesNotifyId > 0) {
-            workspaceManager.disconnect(this._nWorkspacesNotifyId);
-            this._nWorkspacesNotifyId = 0;
-        }
-        if (this._activeWorkspaceChangedId > 0) {
-            workspaceManager.disconnect(this._activeWorkspaceChangedId);
-            this._activeWorkspaceChangedId = 0;
-        }
-        if (this._workspacesReorderedId > 0) {
-            workspaceManager.disconnect(this._workspacesReorderedId);
-            this._workspacesReorderedId = 0;
-        }
-
-        if (this._syncStackingId > 0) {
-            Main.overview.disconnect(this._syncStackingId);
-            this._syncStackingId = 0;
-        }
+        global.workspace_manager.disconnectObject(this);
+        Main.overview.disconnectObject(this);
 
         for (let w = 0; w < this._thumbnails.length; w++)
             this._thumbnails[w].destroy();
diff --git a/js/ui/workspacesView.js b/js/ui/workspacesView.js
index f132528271..511847250d 100644
--- a/js/ui/workspacesView.js
+++ b/js/ui/workspacesView.js
@@ -37,30 +37,17 @@ var WorkspacesViewBase = GObject.registerClass({
         this._monitorIndex = monitorIndex;
 
         this._inDrag = false;
-        this._windowDragBeginId = Main.overview.connect('window-drag-begin', this._dragBegin.bind(this));
-        this._windowDragEndId = Main.overview.connect('window-drag-end', this._dragEnd.bind(this));
+        Main.overview.connectObject(
+            'window-drag-begin', this._dragBegin.bind(this),
+            'window-drag-end', this._dragEnd.bind(this), this);
 
         this._overviewAdjustment = overviewAdjustment;
-        this._overviewId = overviewAdjustment.connect('notify::value', () => {
-            this._updateWorkspaceMode();
-        });
+        overviewAdjustment.connectObject('notify::value',
+            () => this._updateWorkspaceMode(), this);
     }
 
     _onDestroy() {
         this._dragEnd();
-
-        if (this._windowDragBeginId > 0) {
-            Main.overview.disconnect(this._windowDragBeginId);
-            this._windowDragBeginId = 0;
-        }
-        if (this._windowDragEndId > 0) {
-            Main.overview.disconnect(this._windowDragEndId);
-            this._windowDragEndId = 0;
-        }
-        if (this._overviewId > 0) {
-            this._overviewAdjustment.disconnect(this._overviewId);
-            delete this._overviewId;
-        }
     }
 
     _dragBegin() {
@@ -104,36 +91,33 @@ class WorkspacesView extends WorkspacesViewBase {
 
         this._controls = controls;
         this._fitModeAdjustment = fitModeAdjustment;
-        this._fitModeNotifyId = this._fitModeAdjustment.connect('notify::value', () => {
+        this._fitModeAdjustment.connectObject('notify::value', () => {
             this._updateVisibility();
             this._updateWorkspacesState();
             this.queue_relayout();
-        });
+        }, this);
 
         this._animating = false; // tweening
         this._gestureActive = false; // touch(pad) gestures
 
         this._scrollAdjustment = scrollAdjustment;
-        this._onScrollId = this._scrollAdjustment.connect('notify::value',
-            this._onScrollAdjustmentChanged.bind(this));
+        this._scrollAdjustment.connectObject('notify::value',
+            this._onScrollAdjustmentChanged.bind(this), this);
 
         this._workspaces = [];
         this._updateWorkspaces();
-        this._updateWorkspacesId =
-            workspaceManager.connect('notify::n-workspaces',
-                                     this._updateWorkspaces.bind(this));
-        this._reorderWorkspacesId =
-            workspaceManager.connect('workspaces-reordered', () => {
+        workspaceManager.connectObject(
+            'notify::n-workspaces', this._updateWorkspaces.bind(this),
+            'workspaces-reordered', () => {
                 this._workspaces.sort((a, b) => {
                     return a.metaWorkspace.index() - b.metaWorkspace.index();
                 });
                 this._workspaces.forEach(
                     (ws, i) => this.set_child_at_index(ws, i));
-            });
+            }, this);
 
-        this._switchWorkspaceNotifyId =
-            global.window_manager.connect('switch-workspace',
-                                          this._activeWorkspaceChanged.bind(this));
+        global.window_manager.connectObject('switch-workspace',
+            this._activeWorkspaceChanged.bind(this), this);
         this._updateVisibility();
     }
 
@@ -491,12 +475,6 @@ class WorkspacesView extends WorkspacesViewBase {
         super._onDestroy();
 
         this._workspaces = [];
-        this._scrollAdjustment.disconnect(this._onScrollId);
-        this._fitModeAdjustment.disconnect(this._fitModeNotifyId);
-        global.window_manager.disconnect(this._switchWorkspaceNotifyId);
-        let workspaceManager = global.workspace_manager;
-        workspaceManager.disconnect(this._updateWorkspacesId);
-        workspaceManager.disconnect(this._reorderWorkspacesId);
     }
 
     startTouchGesture() {
@@ -623,11 +601,10 @@ class SecondaryMonitorDisplay extends St.Widget {
         this._thumbnails.connect('notify::should-show',
             () => this._updateThumbnailVisibility());
 
-        this._stateChangedId = this._overviewAdjustment.connect('notify::value',
-            () => {
-                this._updateThumbnailParams();
-                this.queue_relayout();
-            });
+        this._overviewAdjustment.connectObject('notify::value', () => {
+            this._updateThumbnailParams();
+            this.queue_relayout();
+        }, this);
 
         this._settings = new Gio.Settings({ schema_id: MUTTER_SCHEMA });
         this._settings.connect('changed::workspaces-only-on-primary',
@@ -738,10 +715,6 @@ class SecondaryMonitorDisplay extends St.Widget {
         if (this._settings)
             this._settings.run_dispose();
         this._settings = null;
-
-        if (this._stateChangedId)
-            this._overviewAdjustment.disconnect(this._stateChangedId);
-        this._stateChangedId = 0;
     }
 
     _workspacesOnPrimaryChanged() {
@@ -848,13 +821,8 @@ class WorkspacesDisplay extends St.Widget {
         let workspaceManager = global.workspace_manager;
         this._scrollAdjustment = scrollAdjustment;
 
-        this._switchWorkspaceId =
-            global.window_manager.connect('switch-workspace',
-                this._activeWorkspaceChanged.bind(this));
-
-        this._reorderWorkspacesdId =
-            workspaceManager.connect('workspaces-reordered',
-                this._workspacesReordered.bind(this));
+        global.window_manager.connectObject('switch-workspace',
+            this._activeWorkspaceChanged.bind(this), this);
 
         this._swipeTracker = new SwipeTracker.SwipeTracker(
             Main.layoutManager.overviewGroup,
@@ -867,16 +835,14 @@ class WorkspacesDisplay extends St.Widget {
         this._swipeTracker.connect('end', this._switchWorkspaceEnd.bind(this));
         this.connect('notify::mapped', this._updateSwipeTracker.bind(this));
 
-        this._layoutRowsNotifyId = workspaceManager.connect(
-            'notify::layout-rows', this._updateTrackerOrientation.bind(this));
+        workspaceManager.connectObject(
+            'workspaces-reordered', this._workspacesReordered.bind(this),
+            'notify::layout-rows', this._updateTrackerOrientation.bind(this), this);
         this._updateTrackerOrientation();
 
-        this._windowDragBeginId =
-            Main.overview.connect('window-drag-begin',
-                this._windowDragBegin.bind(this));
-        this._windowDragEndId =
-            Main.overview.connect('window-drag-end',
-                this._windowDragEnd.bind(this));
+        Main.overview.connectObject(
+            'window-drag-begin', this._windowDragBegin.bind(this),
+            'window-drag-end', this._windowDragEnd.bind(this), this);
 
         this._primaryVisible = true;
         this._primaryIndex = Main.layoutManager.primaryIndex;
@@ -884,10 +850,6 @@ class WorkspacesDisplay extends St.Widget {
 
         this._settings = new Gio.Settings({ schema_id: MUTTER_SCHEMA });
 
-        this._restackedNotifyId = 0;
-        this._scrollEventId = 0;
-        this._keyPressEventId = 0;
-
         this._inWindowDrag = false;
         this._leavingOverview = false;
 
@@ -901,12 +863,6 @@ class WorkspacesDisplay extends St.Widget {
             Meta.later_remove(this._parentSetLater);
             this._parentSetLater = 0;
         }
-
-        global.window_manager.disconnect(this._switchWorkspaceId);
-        global.workspace_manager.disconnect(this._reorderWorkspacesdId);
-        global.workspace_manager.disconnect(this._layoutRowsNotifyId);
-        Main.overview.disconnect(this._windowDragBeginId);
-        Main.overview.disconnect(this._windowDragEndId);
     }
 
     _windowDragBegin() {
@@ -1036,14 +992,12 @@ class WorkspacesDisplay extends St.Widget {
         this.show();
         this._updateWorkspacesViews();
 
-        this._restackedNotifyId =
-            Main.overview.connect('windows-restacked',
-                                  this._onRestacked.bind(this));
-        if (this._scrollEventId == 0)
-            this._scrollEventId = Main.overview.connect('scroll-event', this._onScrollEvent.bind(this));
+        Main.overview.connectObject(
+            'windows-restacked', this._onRestacked.bind(this),
+            'scroll-event', this._onScrollEvent.bind(this), this);
 
-        if (this._keyPressEventId == 0)
-            this._keyPressEventId = global.stage.connect('key-press-event', 
this._onKeyPressEvent.bind(this));
+        global.stage.connectObject(
+            'key-press-event', this._onKeyPressEvent.bind(this), this);
     }
 
     prepareToLeaveOverview() {
@@ -1055,18 +1009,9 @@ class WorkspacesDisplay extends St.Widget {
     }
 
     vfunc_hide() {
-        if (this._restackedNotifyId > 0) {
-            Main.overview.disconnect(this._restackedNotifyId);
-            this._restackedNotifyId = 0;
-        }
-        if (this._scrollEventId > 0) {
-            Main.overview.disconnect(this._scrollEventId);
-            this._scrollEventId = 0;
-        }
-        if (this._keyPressEventId > 0) {
-            global.stage.disconnect(this._keyPressEventId);
-            this._keyPressEventId = 0;
-        }
+        Main.overview.disconnectObject(this);
+        global.stage.disconnectObject(this);
+
         for (let i = 0; i < this._workspacesViews.length; i++)
             this._workspacesViews[i].destroy();
         this._workspacesViews = [];
diff --git a/js/ui/xdndHandler.js b/js/ui/xdndHandler.js
index 0ea81a397b..706e0c1a46 100644
--- a/js/ui/xdndHandler.js
+++ b/js/ui/xdndHandler.js
@@ -21,16 +21,11 @@ var XdndHandler = class {
         dnd.connect('dnd-enter', this._onEnter.bind(this));
         dnd.connect('dnd-position-change', this._onPositionChanged.bind(this));
         dnd.connect('dnd-leave', this._onLeave.bind(this));
-
-        this._windowGroupVisibilityHandlerId = 0;
     }
 
     // Called when the user cancels the drag (i.e release the button)
     _onLeave() {
-        if (this._windowGroupVisibilityHandlerId != 0) {
-            global.window_group.disconnect(this._windowGroupVisibilityHandlerId);
-            this._windowGroupVisibilityHandlerId = 0;
-        }
+        global.window_group.disconnectObject(this);
         if (this._cursorWindowClone) {
             this._cursorWindowClone.destroy();
             this._cursorWindowClone = null;
@@ -40,9 +35,8 @@ var XdndHandler = class {
     }
 
     _onEnter() {
-        this._windowGroupVisibilityHandlerId =
-            global.window_group.connect('notify::visible',
-                this._onWindowGroupVisibilityChanged.bind(this));
+        global.window_group.connectObject('notify::visible',
+            this._onWindowGroupVisibilityChanged.bind(this), this);
 
         this.emit('drag-begin', global.get_current_time());
     }


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]