gnome-shell/js/misc/signalTracker.js
Florian Müllner f45ccc9143 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>
2022-03-04 14:14:37 +00:00

215 lines
6.0 KiB
JavaScript

/* 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'];
}