[gnome-shell] signalTracker: Provide monkey-patching for (dis)connectObject()



commit f45ccc9143b21a51aa7677252276aec7d4220a72
Author: Florian Müllner <fmuellner gnome org>
Date:   Mon Aug 16 00:01:40 2021 +0200

    signalTracker: Provide monkey-patching for (dis)connectObject()
    
    The module exports a `addObjectSignalMethods()` method that extends
    the provided prototype with `connectObject()` and `disconnectObject()`
    methods.
    
    In its simplest form, `connectObject()` looks like the regular
    `connect()` method, except for an additional parameter:
    
    ```js
        this._button.connectObject('clicked',
            () => this._onButtonClicked(), this);
    ```
    
    The additional object can be used to disconnect all handlers on the
    instance that were connected with that object, similar to
    `g_signal_handlers_disconnect_by_data()` (which cannot be used
    from introspection).
    
    For objects that are subclasses of Clutter.Actor, that will happen
    automatically when the actor is destroyed, similar to
    `g_signal_connect_object()`.
    
    Finally, `connectObject()` allows to conveniently connect multiple
    signals at once, similar to `g_object_connect()`:
    
    ```js
        this._toggleButton.connect(
            'clicked', () => this._onClicked(),
            'notify::checked', () => this._onChecked(), this);
    ```
    
    Part-of: <https://gitlab.gnome.org/GNOME/gnome-shell/-/merge_requests/1953>

 js/js-resources.gresource.xml |   1 +
 js/misc/signalTracker.js      | 214 ++++++++++++++++++++++++++++++++++++++++++
 js/ui/environment.js          |  21 +++++
 tests/meson.build             |   1 +
 tests/unit/signalTracker.js   |  79 ++++++++++++++++
 5 files changed, 316 insertions(+)
---
diff --git a/js/js-resources.gresource.xml b/js/js-resources.gresource.xml
index 6efbe723a6..eb4e365ae7 100644
--- a/js/js-resources.gresource.xml
+++ b/js/js-resources.gresource.xml
@@ -27,6 +27,7 @@
     <file>misc/params.js</file>
     <file>misc/parentalControlsManager.js</file>
     <file>misc/permissionStore.js</file>
+    <file>misc/signalTracker.js</file>
     <file>misc/smartcardManager.js</file>
     <file>misc/systemActions.js</file>
     <file>misc/util.js</file>
diff --git a/js/misc/signalTracker.js b/js/misc/signalTracker.js
new file mode 100644
index 0000000000..9366814756
--- /dev/null
+++ b/js/misc/signalTracker.js
@@ -0,0 +1,214 @@
+/* exported addObjectSignalMethods */
+const { GObject } = imports.gi;
+
+class SignalManager {
+    /**
+     * @returns {SignalManager} - the SignalManager singleton
+     */
+    static getDefault() {
+        if (!this._singleton)
+            this._singleton = new SignalManager();
+        return this._singleton;
+    }
+
+    constructor() {
+        this._signalTrackers = new Map();
+    }
+
+    /**
+     * @param {Object} obj - object to get signal tracker for
+     * @returns {SignalTracker} - the signal tracker for object
+     */
+    getSignalTracker(obj) {
+        if (!this._signalTrackers.has(obj))
+            this._signalTrackers.set(obj, new SignalTracker(obj));
+        return this._signalTrackers.get(obj);
+    }
+}
+
+class SignalTracker {
+    /**
+     * @param {Object=} owner - object that owns the tracker
+     */
+    constructor(owner) {
+        if (this._hasDestroySignal(owner))
+            this._ownerDestroyId = owner.connect('destroy', () => this.clear());
+
+        this._owner = owner;
+        this._map = new Map();
+    }
+
+    /**
+     * @private
+     * @param {Object} obj - an object
+     * @returns {bool} - true if obj has a 'destroy' GObject signal
+     */
+    _hasDestroySignal(obj) {
+        return obj instanceof GObject.Object &&
+            GObject.signal_lookup('destroy', obj);
+    }
+
+    /**
+     * @typedef SignalData
+     * @property {number[]} ownerSignals - a list of handler IDs
+     * @property {number} destroyId - destroy handler ID of tracked object
+     */
+
+    /**
+     * @private
+     * @param {Object} obj - a tracked object
+     * @returns {SignalData} - signal data for object
+     */
+    _getSignalData(obj) {
+        if (!this._map.has(obj))
+            this._map.set(obj, { ownerSignals: [], destroyId: 0 });
+        return this._map.get(obj);
+    }
+
+    /**
+     * @private
+     * @param {GObject.Object} obj - tracked widget
+     */
+    _trackDestroy(obj) {
+        const signalData = this._getSignalData(obj);
+        if (signalData.destroyId)
+            return;
+        signalData.destroyId = obj.connect('destroy', () => this.untrack(obj));
+    }
+
+    _disconnectSignal(obj, id) {
+        const proto = obj instanceof GObject.Object
+            ? GObject.Object.prototype
+            : Object.getPrototypeOf(obj);
+        proto['disconnect'].call(obj, id);
+    }
+
+    /**
+     * @param {Object} obj - tracked object
+     * @param {...number} handlerIds - tracked handler IDs
+     * @returns {void}
+     */
+    track(obj, ...handlerIds) {
+        if (this._hasDestroySignal(obj))
+            this._trackDestroy(obj);
+
+        this._getSignalData(obj).ownerSignals.push(...handlerIds);
+    }
+
+    /**
+     * @param {Object} obj - tracked object instance
+     * @returns {void}
+     */
+    untrack(obj) {
+        const { ownerSignals, destroyId } = this._getSignalData(obj);
+        this._map.delete(obj);
+
+        ownerSignals.forEach(id => this._disconnectSignal(this._owner, id));
+        if (destroyId)
+            this._disconnectSignal(obj, destroyId);
+    }
+
+    /**
+     * @returns {void}
+     */
+    clear() {
+        [...this._map.keys()].forEach(obj => this.untrack(obj));
+    }
+
+    /**
+     * @returns {void}
+     */
+    destroy() {
+        this.clear();
+
+        if (this._ownerDestroyId)
+            this._disconnectSignal(this._owner, this._ownerDestroyId);
+
+        delete this._ownerDestroyId;
+        delete this._owner;
+    }
+}
+
+/**
+ * Connect one or more signals, and associate the handlers
+ * with a tracked object.
+ *
+ * All handlers for a particular object can be disconnected
+ * by calling disconnectObject(). If object is a {Clutter.widget},
+ * this is done automatically when the widget is destroyed.
+ *
+ * @param {object} thisObj - the emitter object
+ * @param {...any} args - a sequence of signal-name/handler pairs
+ * with an optional flags value, followed by an object to track
+ * @returns {void}
+ */
+function connectObject(thisObj, ...args) {
+    const getParams = argArray => {
+        const [signalName, handler, arg, ...rest] = argArray;
+        if (typeof arg !== 'number')
+            return [signalName, handler, 0, arg, ...rest];
+
+        const flags = arg;
+        if (flags > GObject.ConnectFlags.SWAPPED)
+            throw new Error(`Invalid flag value ${flags}`);
+        if (flags === GObject.ConnectFlags.SWAPPED)
+            throw new Error('Swapped signals are not supported');
+        return [signalName, handler, flags, ...rest];
+    };
+
+    const connectSignal = (emitter, signalName, handler, flags) => {
+        const isGObject = emitter instanceof GObject.Object;
+        const func = flags === GObject.ConnectFlags.AFTER && isGObject
+            ? 'connect_after'
+            : 'connect';
+        const emitterProto = isGObject
+            ? GObject.Object.prototype
+            : Object.getPrototypeOf(emitter);
+        return emitterProto[func].call(emitter, signalName, handler);
+    };
+
+    const signalIds = [];
+    while (args.length > 1) {
+        const [signalName, handler, flags, ...rest] = getParams(args);
+        signalIds.push(connectSignal(thisObj, signalName, handler, flags));
+        args = rest;
+    }
+
+    let [obj] = args;
+    if (!obj)
+        obj = globalThis;
+
+    const tracker = SignalManager.getDefault().getSignalTracker(thisObj);
+    tracker.track(obj, ...signalIds);
+}
+
+/**
+ * Disconnect all signals that were connected for
+ * the specified tracked object
+ *
+ * @param {Object} thisObj - the emitter object
+ * @param {Object} obj - the tracked object
+ * @returns {void}
+ */
+function disconnectObject(thisObj, obj) {
+    SignalManager.getDefault().getSignalTracker(thisObj).untrack(obj);
+}
+
+/**
+ * Add connectObject()/disconnectObject() methods
+ * to prototype. The prototype must have the connect()
+ * and disconnect() signal methods.
+ *
+ * @param {prototype} proto - a prototype
+ */
+function addObjectSignalMethods(proto) {
+    proto['connectObject'] = function (...args) {
+        connectObject(this, ...args);
+    };
+    proto['connect_object'] = proto['connectObject'];
+
+    proto['disconnectObject'] = function (obj) {
+        disconnectObject(this, obj);
+    };
+    proto['disconnect_object'] = proto['disconnectObject'];
+}
diff --git a/js/ui/environment.js b/js/ui/environment.js
index 099cf10f6b..2d3baf7225 100644
--- a/js/ui/environment.js
+++ b/js/ui/environment.js
@@ -26,7 +26,9 @@ try {
 
 const { Clutter, Gio, GLib, GObject, Meta, Polkit, Shell, St } = imports.gi;
 const Gettext = imports.gettext;
+const Signals = imports.signals;
 const System = imports.system;
+const SignalTracker = imports.misc.signalTracker;
 
 Gio._promisify(Gio.DataInputStream.prototype, 'fill_async');
 Gio._promisify(Gio.DataInputStream.prototype, 'read_line_async');
@@ -324,6 +326,25 @@ function init() {
 
     GObject.gtypeNameBasedOnJSPath = true;
 
+    GObject.Object.prototype.connectObject = function (...args) {
+        SignalTracker.connectObject(this, ...args);
+    };
+    GObject.Object.prototype.connect_object = function (...args) {
+        SignalTracker.connectObject(this, ...args);
+    };
+    GObject.Object.prototype.disconnectObject = function (...args) {
+        SignalTracker.disconnectObject(this, ...args);
+    };
+    GObject.Object.prototype.disconnect_object = function (...args) {
+        SignalTracker.disconnectObject(this, ...args);
+    };
+
+    const _addSignalMethods = Signals.addSignalMethods;
+    Signals.addSignalMethods = function (prototype) {
+        _addSignalMethods(prototype);
+        SignalTracker.addObjectSignalMethods(prototype);
+    };
+
     // Miscellaneous monkeypatching
     _patchContainerClass(St.BoxLayout);
 
diff --git a/tests/meson.build b/tests/meson.build
index 50f8313e9b..9d3925d365 100644
--- a/tests/meson.build
+++ b/tests/meson.build
@@ -16,6 +16,7 @@ tests = [
     'jsParse',
     'markup',
     'params',
+    'signalTracker',
     'url',
     'versionCompare',
 ]
diff --git a/tests/unit/signalTracker.js b/tests/unit/signalTracker.js
new file mode 100644
index 0000000000..f13327ec14
--- /dev/null
+++ b/tests/unit/signalTracker.js
@@ -0,0 +1,79 @@
+// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
+
+// Test cases for version comparison
+
+const { GObject } = imports.gi;
+
+const JsUnit = imports.jsUnit;
+const Signals = imports.signals;
+
+const Environment = imports.ui.environment;
+Environment.init();
+
+const Destroyable = GObject.registerClass({
+    Signals: { 'destroy': {} },
+}, class Destroyable extends GObject.Object {});
+
+class PlainEmitter {}
+Signals.addSignalMethods(PlainEmitter.prototype);
+
+const GObjectEmitter = GObject.registerClass({
+    Signals: { 'signal': {} },
+}, class GObjectEmitter extends Destroyable {});
+
+const emitter1 = new PlainEmitter();
+const emitter2 = new GObjectEmitter();
+
+const tracked1 = new Destroyable();
+const tracked2 = {};
+
+let count = 0;
+const handler = () => count++;
+
+emitter1.connectObject('signal', handler, tracked1);
+emitter2.connectObject('signal', handler, tracked1);
+
+emitter1.connectObject('signal', handler, tracked2);
+emitter2.connectObject('signal', handler, tracked2);
+
+JsUnit.assertEquals(count, 0);
+
+emitter1.emit('signal');
+emitter2.emit('signal');
+
+JsUnit.assertEquals(count, 4);
+
+tracked1.emit('destroy');
+
+emitter1.emit('signal');
+emitter2.emit('signal');
+
+JsUnit.assertEquals(count, 6);
+
+emitter1.disconnectObject(tracked2);
+emitter2.emit('destroy');
+
+emitter1.emit('signal');
+emitter2.emit('signal');
+
+JsUnit.assertEquals(count, 6);
+
+emitter1.connectObject(
+    'signal', handler,
+    'signal', handler, GObject.ConnectFlags.AFTER,
+    tracked1);
+emitter2.connectObject(
+    'signal', handler,
+    'signal', handler, GObject.ConnectFlags.AFTER,
+    tracked1);
+
+emitter1.emit('signal');
+emitter2.emit('signal');
+
+JsUnit.assertEquals(count, 10);
+
+tracked1.emit('destroy');
+emitter1.emit('signal');
+emitter2.emit('signal');
+
+JsUnit.assertEquals(count, 10);


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