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>
This commit is contained in:
parent
96df498450
commit
f45ccc9143
@ -27,6 +27,7 @@
|
|||||||
<file>misc/params.js</file>
|
<file>misc/params.js</file>
|
||||||
<file>misc/parentalControlsManager.js</file>
|
<file>misc/parentalControlsManager.js</file>
|
||||||
<file>misc/permissionStore.js</file>
|
<file>misc/permissionStore.js</file>
|
||||||
|
<file>misc/signalTracker.js</file>
|
||||||
<file>misc/smartcardManager.js</file>
|
<file>misc/smartcardManager.js</file>
|
||||||
<file>misc/systemActions.js</file>
|
<file>misc/systemActions.js</file>
|
||||||
<file>misc/util.js</file>
|
<file>misc/util.js</file>
|
||||||
|
214
js/misc/signalTracker.js
Normal file
214
js/misc/signalTracker.js
Normal file
@ -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'];
|
||||||
|
}
|
@ -26,7 +26,9 @@ try {
|
|||||||
|
|
||||||
const { Clutter, Gio, GLib, GObject, Meta, Polkit, Shell, St } = imports.gi;
|
const { Clutter, Gio, GLib, GObject, Meta, Polkit, Shell, St } = imports.gi;
|
||||||
const Gettext = imports.gettext;
|
const Gettext = imports.gettext;
|
||||||
|
const Signals = imports.signals;
|
||||||
const System = imports.system;
|
const System = imports.system;
|
||||||
|
const SignalTracker = imports.misc.signalTracker;
|
||||||
|
|
||||||
Gio._promisify(Gio.DataInputStream.prototype, 'fill_async');
|
Gio._promisify(Gio.DataInputStream.prototype, 'fill_async');
|
||||||
Gio._promisify(Gio.DataInputStream.prototype, 'read_line_async');
|
Gio._promisify(Gio.DataInputStream.prototype, 'read_line_async');
|
||||||
@ -324,6 +326,25 @@ function init() {
|
|||||||
|
|
||||||
GObject.gtypeNameBasedOnJSPath = true;
|
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
|
// Miscellaneous monkeypatching
|
||||||
_patchContainerClass(St.BoxLayout);
|
_patchContainerClass(St.BoxLayout);
|
||||||
|
|
||||||
|
@ -16,6 +16,7 @@ tests = [
|
|||||||
'jsParse',
|
'jsParse',
|
||||||
'markup',
|
'markup',
|
||||||
'params',
|
'params',
|
||||||
|
'signalTracker',
|
||||||
'url',
|
'url',
|
||||||
'versionCompare',
|
'versionCompare',
|
||||||
]
|
]
|
||||||
|
79
tests/unit/signalTracker.js
Normal file
79
tests/unit/signalTracker.js
Normal file
@ -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);
|
Loading…
Reference in New Issue
Block a user