From e4283f6d48b98e764b988b43bbc86b9d52e6ec94 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 7 Apr 2024 19:54:43 +0200 Subject: Adding upstream version 43.9. Signed-off-by: Daniel Baumann --- js/misc/signalTracker.js | 269 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 269 insertions(+) create mode 100644 js/misc/signalTracker.js (limited to 'js/misc/signalTracker.js') diff --git a/js/misc/signalTracker.js b/js/misc/signalTracker.js new file mode 100644 index 0000000..3b71fbc --- /dev/null +++ b/js/misc/signalTracker.js @@ -0,0 +1,269 @@ +/* exported TransientSignalHolder, connectObject, disconnectObject */ +const { GObject } = imports.gi; + +const destroyableTypes = []; + +/** + * @private + * @param {Object} obj - an object + * @returns {bool} - true if obj has a 'destroy' GObject signal + */ +function _hasDestroySignal(obj) { + return destroyableTypes.some(type => obj instanceof type); +} + +var TransientSignalHolder = GObject.registerClass( +class TransientSignalHolder extends GObject.Object { + static [GObject.signals] = { + 'destroy': {}, + }; + + constructor(owner) { + super(); + + if (_hasDestroySignal(owner)) + owner.connectObject('destroy', () => this.destroy(), this); + } + + destroy() { + this.emit('destroy'); + } +}); +registerDestroyableType(TransientSignalHolder); + +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) { + let signalTracker = this._signalTrackers.get(obj); + if (signalTracker === undefined) { + signalTracker = new SignalTracker(obj); + this._signalTrackers.set(obj, signalTracker); + } + return signalTracker; + } + + /** + * @param {Object} obj - object to get signal tracker for + * @returns {?SignalTracker} - the signal tracker for object if it exists + */ + maybeGetSignalTracker(obj) { + return this._signalTrackers.get(obj) ?? null; + } + + /* + * @param {Object} obj - object to remove signal tracker for + * @returns {void} + */ + removeSignalTracker(obj) { + this._signalTrackers.delete(obj); + } +} + +class SignalTracker { + /** + * @param {Object=} owner - object that owns the tracker + */ + constructor(owner) { + if (_hasDestroySignal(owner)) + this._ownerDestroyId = owner.connect_after('destroy', () => this.clear()); + + this._owner = owner; + this._map = new Map(); + } + + /** + * @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) { + let data = this._map.get(obj); + if (data === undefined) { + data = { ownerSignals: [], destroyId: 0 }; + this._map.set(obj, data); + } + return data; + } + + /** + * @private + * @param {GObject.Object} obj - tracked widget + */ + _trackDestroy(obj) { + const signalData = this._getSignalData(obj); + if (signalData.destroyId) + return; + signalData.destroyId = obj.connect_after('destroy', () => this.untrack(obj)); + } + + _disconnectSignalForProto(proto, obj, id) { + proto['disconnect'].call(obj, id); + } + + _getObjectProto(obj) { + return obj instanceof GObject.Object + ? GObject.Object.prototype + : Object.getPrototypeOf(obj); + } + + _disconnectSignal(obj, id) { + this._disconnectSignalForProto(this._getObjectProto(obj), obj, id); + } + + _removeTracker() { + if (this._ownerDestroyId) + this._disconnectSignal(this._owner, this._ownerDestroyId); + + SignalManager.getDefault().removeSignalTracker(this._owner); + + delete this._ownerDestroyId; + delete this._owner; + } + + /** + * @param {Object} obj - tracked object + * @param {...number} handlerIds - tracked handler IDs + * @returns {void} + */ + track(obj, ...handlerIds) { + if (_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); + + const ownerProto = this._getObjectProto(this._owner); + ownerSignals.forEach(id => + this._disconnectSignalForProto(ownerProto, this._owner, id)); + if (destroyId) + this._disconnectSignal(obj, destroyId); + + if (this._map.size === 0) + this._removeTracker(); + } + + /** + * @returns {void} + */ + clear() { + this._map.forEach((_, obj) => this.untrack(obj)); + } + + /** + * @returns {void} + */ + destroy() { + this.clear(); + this._removeTracker(); + } +} + +/** + * 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; + let flagsMask = 0; + Object.values(GObject.ConnectFlags).forEach(v => (flagsMask |= v)); + if (!(flags & flagsMask)) + 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; + } + + const obj = args.at(0) ?? 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().maybeGetSignalTracker(thisObj)?.untrack(obj); +} + +/** + * Register a GObject type as having a 'destroy' signal + * that should disconnect all handlers + * + * @param {GObject.Type} gtype - a GObject type + */ +function registerDestroyableType(gtype) { + if (!GObject.type_is_a(gtype, GObject.Object)) + throw new Error(`${gtype} is not a GObject subclass`); + + if (!GObject.signal_lookup('destroy', gtype)) + throw new Error(`${gtype} does not have a destroy signal`); + + destroyableTypes.push(gtype); +} -- cgit v1.2.3