[gjs: 1/2] object: Add overrides for signal matching methods



commit 94d49c61aad09be69dce165ea6a8bddb227c5900
Author: Philip Chimento <philip chimento gmail com>
Date:   Sat Nov 30 13:47:18 2019 -0800

    object: Add overrides for signal matching methods
    
    g_signal_handler_find(), g_signal_handlers_block_matched(),
    g_signal_handlers_unblock_matched(), and
    g_signal_handlers_disconnect_matched() are all technically
    introspectable but don't work in GJS when matching by function, because
    when connecting signals we create a GClosure object for each signal
    callback. The JS code has no way to search for this object, since it is
    private, and additionally there would be no way to match more than one
    signal with the same handler connected because they would have different
    GClosure objects.
    
    We solve this by storing a pointer to the callable JS object in the data
    field of the GClosure so we can search for it. This is normally a bad
    idea since bare pointers to GC things may be moved or finalized, but I
    believe it's OK here because signal connections are never rooted, always
    traced, and the pointer can only be moved or finalized after tracing, so
    we can just update or null it out at that point.
    
    We add private C++ methods to ObjectBase/ObjectInstance, and overrides
    to GObject.js to call the C++ methods, with API that matches the C API
    as much as possible. We do change the mask/optional arguments mechanism
    to a more natural properties-object mechanism. We also add convenience
    API to match g_signal_handlers_block_by_func() etc., which are macros in
    C and so not introspectable at all, but easy to implement here.
    
    Closes: #290

 gi/closure.cpp                         |  11 ++-
 gi/object.cpp                          | 175 ++++++++++++++++++++++++++++++++-
 gi/object.h                            |  20 ++++
 gi/private.cpp                         |  33 +++++--
 gjs/atoms.h                            |   9 +-
 installed-tests/js/testGObjectClass.js | 134 +++++++++++++++++++++++++
 modules/overrides/GObject.js           | 153 ++++++++++++++++++++++++++++
 7 files changed, 523 insertions(+), 12 deletions(-)
---
diff --git a/gi/closure.cpp b/gi/closure.cpp
index f0793c6b..ac162da3 100644
--- a/gi/closure.cpp
+++ b/gi/closure.cpp
@@ -281,6 +281,7 @@ gjs_closure_trace(GClosure *closure,
         return;
 
     c->func.trace(tracer, "signal connection");
+    closure->data = c->func.debug_addr();  // update in case GC moved it
 }
 
 GClosure* gjs_closure_new(JSContext* context, JSFunction* callable,
@@ -288,8 +289,16 @@ GClosure* gjs_closure_new(JSContext* context, JSFunction* callable,
                           bool root_function) {
     Closure *c;
 
+    // We store a bare pointer to the JSFunction in the GClosure's data field
+    // so that g_signal_handlers_block_matched() and friends can work. We are
+    // not supposed to store bare pointers to GC things, but in this particular
+    // case it will work because:
+    //   - we update the data field in gjs_closure_trace() so if the pointer is
+    //     moved or finalized, the data field will still be accurate
+    //   - signal handlers are always in trace mode, so it's not the case that
+    //     JS::PersistentRooted will move the pointer out from under us.
     auto* gc = reinterpret_cast<GjsClosure*>(
-        g_closure_new_simple(sizeof(GjsClosure), nullptr));
+        g_closure_new_simple(sizeof(GjsClosure), callable));
     c = new (&gc->priv) Closure();
 
     /* The saved context is used for lifetime management, so that the closure will
diff --git a/gi/object.cpp b/gi/object.cpp
index 7e49d269..3b1e87a7 100644
--- a/gi/object.cpp
+++ b/gi/object.cpp
@@ -1934,6 +1934,165 @@ ObjectInstance::emit_impl(JSContext          *context,
     return !failed;
 }
 
+bool ObjectInstance::signal_match_arguments_from_object(
+    JSContext* cx, JS::HandleObject match_obj, GSignalMatchType* mask_out,
+    unsigned* signal_id_out, GQuark* detail_out, void** func_ptr_out) {
+    g_assert(mask_out && signal_id_out && detail_out && func_ptr_out &&
+             "forgot out parameter");
+
+    int mask = 0;
+    const GjsAtoms& atoms = GjsContextPrivate::atoms(cx);
+
+    bool has_id;
+    unsigned signal_id = 0;
+    if (!JS_HasOwnPropertyById(cx, match_obj, atoms.signal_id(), &has_id))
+        return false;
+    if (has_id) {
+        mask |= G_SIGNAL_MATCH_ID;
+
+        JS::RootedValue value(cx);
+        if (!JS_GetPropertyById(cx, match_obj, atoms.signal_id(), &value))
+            return false;
+
+        JS::UniqueChars signal_name = gjs_string_to_utf8(cx, value);
+        if (!signal_name)
+            return false;
+
+        signal_id = g_signal_lookup(signal_name.get(), gtype());
+    }
+
+    bool has_detail;
+    GQuark detail = 0;
+    if (!JS_HasOwnPropertyById(cx, match_obj, atoms.detail(), &has_detail))
+        return false;
+    if (has_detail) {
+        mask |= G_SIGNAL_MATCH_DETAIL;
+
+        JS::RootedValue value(cx);
+        if (!JS_GetPropertyById(cx, match_obj, atoms.detail(), &value))
+            return false;
+
+        JS::UniqueChars detail_string = gjs_string_to_utf8(cx, value);
+        if (!detail_string)
+            return false;
+
+        detail = g_quark_from_string(detail_string.get());
+    }
+
+    bool has_func;
+    void* func_ptr = nullptr;
+    if (!JS_HasOwnPropertyById(cx, match_obj, atoms.func(), &has_func))
+        return false;
+    if (has_func) {
+        mask |= G_SIGNAL_MATCH_DATA;
+
+        JS::RootedValue value(cx);
+        if (!JS_GetPropertyById(cx, match_obj, atoms.func(), &value))
+            return false;
+
+        if (!value.isObject() || !JS_ObjectIsFunction(&value.toObject())) {
+            gjs_throw(cx, "'func' property must be a function");
+            return false;
+        }
+
+        func_ptr = JS_GetObjectFunction(&value.toObject());
+    }
+
+    if (!has_id && !has_detail && !has_func) {
+        gjs_throw(cx, "Must specify at least one of signalId, detail, or func");
+        return false;
+    }
+
+    *mask_out = GSignalMatchType(mask);
+    if (has_id)
+        *signal_id_out = signal_id;
+    if (has_detail)
+        *detail_out = detail;
+    if (has_func)
+        *func_ptr_out = func_ptr;
+    return true;
+}
+
+bool ObjectBase::signal_find(JSContext* cx, unsigned argc, JS::Value* vp) {
+    GJS_GET_WRAPPER_PRIV(cx, argc, vp, args, obj, ObjectBase, priv);
+    if (!priv->check_is_instance(cx, "find signal"))
+        return false;
+
+    return priv->to_instance()->signal_find_impl(cx, args);
+}
+
+bool ObjectInstance::signal_find_impl(JSContext* cx, const JS::CallArgs& args) {
+    gjs_debug_gsignal("[Gi.signal_find_symbol]() obj %p priv %p argc %d",
+                      m_wrapper.get(), this, args.length());
+
+    if (!check_gobject_disposed("find any signal on"))
+        return true;
+
+    JS::RootedObject match(cx);
+    if (!gjs_parse_call_args(cx, "[Gi.signal_find_symbol]", args, "o", "match",
+                             &match))
+        return false;
+
+    GSignalMatchType mask;
+    unsigned signal_id;
+    GQuark detail;
+    void* func_ptr;
+    if (!signal_match_arguments_from_object(cx, match, &mask, &signal_id,
+                                            &detail, &func_ptr))
+        return false;
+
+    uint64_t handler = g_signal_handler_find(m_ptr, mask, signal_id, detail,
+                                             nullptr, nullptr, func_ptr);
+
+    args.rval().setNumber(static_cast<double>(handler));
+    return true;
+}
+
+#define DEFINE_SIGNAL_MATCH_METHOD(action)                                    \
+    bool ObjectBase::signals_##action(JSContext* cx, unsigned argc,           \
+                                      JS::Value* vp) {                        \
+        GJS_GET_WRAPPER_PRIV(cx, argc, vp, args, obj, ObjectBase, priv);      \
+        if (!priv->check_is_instance(cx, #action " signal")) {                \
+            return false;                                                     \
+        }                                                                     \
+        return priv->to_instance()->signals_##action##_impl(cx, args);        \
+    }                                                                         \
+                                                                              \
+    bool ObjectInstance::signals_##action##_impl(JSContext* cx,               \
+                                                 const JS::CallArgs& args) {  \
+        gjs_debug_gsignal("[Gi.signals_" #action                              \
+                          "_symbol]() obj %p priv %p argc %d",                \
+                          m_wrapper.get(), this, args.length());              \
+                                                                              \
+        if (!check_gobject_disposed(#action " any signal on")) {              \
+            return true;                                                      \
+        }                                                                     \
+        JS::RootedObject match(cx);                                           \
+        if (!gjs_parse_call_args(cx, "[Gi.signals_" #action "_symbol]", args, \
+                                 "o", "match", &match)) {                     \
+            return false;                                                     \
+        }                                                                     \
+        GSignalMatchType mask;                                                \
+        unsigned signal_id;                                                   \
+        GQuark detail;                                                        \
+        void* func_ptr;                                                       \
+        if (!signal_match_arguments_from_object(cx, match, &mask, &signal_id, \
+                                                &detail, &func_ptr)) {        \
+            return false;                                                     \
+        }                                                                     \
+        unsigned n_matched = g_signal_handlers_##action##_matched(            \
+            m_ptr, mask, signal_id, detail, nullptr, nullptr, func_ptr);      \
+                                                                              \
+        args.rval().setNumber(n_matched);                                     \
+        return true;                                                          \
+    }
+
+DEFINE_SIGNAL_MATCH_METHOD(block)
+DEFINE_SIGNAL_MATCH_METHOD(unblock)
+DEFINE_SIGNAL_MATCH_METHOD(disconnect)
+
+#undef DEFINE_SIGNAL_MATCH_METHOD
+
 bool ObjectBase::to_string(JSContext* cx, unsigned argc, JS::Value* vp) {
     GJS_GET_WRAPPER_PRIV(cx, argc, vp, args, obj, ObjectBase, priv);
     return gjs_wrapper_to_string_func(
@@ -2041,11 +2200,23 @@ bool ObjectPrototype::define_class(JSContext* context,
                                        constructor, prototype))
         return false;
 
-    /* Hook_up_vfunc can't be included in gjs_object_instance_proto_funcs
-     * because it's a custom symbol. */
+    // hook_up_vfunc and the signal handler matcher functions can't be included
+    // in gjs_object_instance_proto_funcs because they are custom symbols.
     const GjsAtoms& atoms = GjsContextPrivate::atoms(context);
     return JS_DefineFunctionById(context, prototype, atoms.hook_up_vfunc(),
                                  &ObjectBase::hook_up_vfunc, 3,
+                                 GJS_MODULE_PROP_FLAGS) &&
+           JS_DefineFunctionById(context, prototype, atoms.signal_find(),
+                                 &ObjectBase::signal_find, 1,
+                                 GJS_MODULE_PROP_FLAGS) &&
+           JS_DefineFunctionById(context, prototype, atoms.signals_block(),
+                                 &ObjectBase::signals_block, 1,
+                                 GJS_MODULE_PROP_FLAGS) &&
+           JS_DefineFunctionById(context, prototype, atoms.signals_unblock(),
+                                 &ObjectBase::signals_unblock, 1,
+                                 GJS_MODULE_PROP_FLAGS) &&
+           JS_DefineFunctionById(context, prototype, atoms.signals_disconnect(),
+                                 &ObjectBase::signals_disconnect, 1,
                                  GJS_MODULE_PROP_FLAGS);
 }
 
diff --git a/gi/object.h b/gi/object.h
index 80b4c083..8995e437 100644
--- a/gi/object.h
+++ b/gi/object.h
@@ -190,6 +190,14 @@ class ObjectBase
     GJS_JSAPI_RETURN_CONVENTION
     static bool emit(JSContext* cx, unsigned argc, JS::Value* vp);
     GJS_JSAPI_RETURN_CONVENTION
+    static bool signal_find(JSContext* cx, unsigned argc, JS::Value* vp);
+    GJS_JSAPI_RETURN_CONVENTION
+    static bool signals_block(JSContext* cx, unsigned argc, JS::Value* vp);
+    GJS_JSAPI_RETURN_CONVENTION
+    static bool signals_unblock(JSContext* cx, unsigned argc, JS::Value* vp);
+    GJS_JSAPI_RETURN_CONVENTION
+    static bool signals_disconnect(JSContext* cx, unsigned argc, JS::Value* vp);
+    GJS_JSAPI_RETURN_CONVENTION
     static bool to_string(JSContext* cx, unsigned argc, JS::Value* vp);
     GJS_USE const char* to_string_kind(void) const;
     GJS_JSAPI_RETURN_CONVENTION
@@ -377,6 +385,10 @@ class ObjectInstance : public GIWrapperInstance<ObjectBase, ObjectPrototype,
     void check_js_object_finalized(void);
     void ensure_uses_toggle_ref(JSContext* cx);
     GJS_USE bool check_gobject_disposed(const char* for_what) const;
+    GJS_JSAPI_RETURN_CONVENTION
+    bool signal_match_arguments_from_object(
+        JSContext* cx, JS::HandleObject props_obj, GSignalMatchType* mask_out,
+        unsigned* signal_id_out, GQuark* detail_out, void** func_ptr_out);
 
  public:
     static GObject* copy_ptr(JSContext* G_GNUC_UNUSED, GType G_GNUC_UNUSED,
@@ -449,6 +461,14 @@ class ObjectInstance : public GIWrapperInstance<ObjectBase, ObjectPrototype,
     GJS_JSAPI_RETURN_CONVENTION
     bool emit_impl(JSContext* cx, const JS::CallArgs& args);
     GJS_JSAPI_RETURN_CONVENTION
+    bool signal_find_impl(JSContext* cx, const JS::CallArgs& args);
+    GJS_JSAPI_RETURN_CONVENTION
+    bool signals_block_impl(JSContext* cx, const JS::CallArgs& args);
+    GJS_JSAPI_RETURN_CONVENTION
+    bool signals_unblock_impl(JSContext* cx, const JS::CallArgs& args);
+    GJS_JSAPI_RETURN_CONVENTION
+    bool signals_disconnect_impl(JSContext* cx, const JS::CallArgs& args);
+    GJS_JSAPI_RETURN_CONVENTION
     bool init_impl(JSContext* cx, const JS::CallArgs& args,
                    JS::MutableHandleObject obj);
     GJS_USE const char* to_string_kind(void) const;
diff --git a/gi/private.cpp b/gi/private.cpp
index c5ce908d..36c53ad4 100644
--- a/gi/private.cpp
+++ b/gi/private.cpp
@@ -414,14 +414,23 @@ static bool gjs_signal_new(JSContext* cx, unsigned argc, JS::Value* vp) {
     return true;
 }
 
-GJS_JSAPI_RETURN_CONVENTION
-static bool hook_up_vfunc_symbol_getter(JSContext* cx, unsigned argc,
-                                        JS::Value* vp) {
-    JS::CallArgs args = JS::CallArgsFromVp(argc, vp);
-    const GjsAtoms& atoms = GjsContextPrivate::atoms(cx);
-    args.rval().setSymbol(JSID_TO_SYMBOL(atoms.hook_up_vfunc()));
-    return true;
-}
+#define DEFINE_SYMBOL_ATOM_GETTER(symbol_name)                            \
+    GJS_JSAPI_RETURN_CONVENTION                                           \
+    static bool symbol_name##_symbol_getter(JSContext* cx, unsigned argc, \
+                                            JS::Value* vp) {              \
+        JS::CallArgs args = JS::CallArgsFromVp(argc, vp);                 \
+        const GjsAtoms& atoms = GjsContextPrivate::atoms(cx);             \
+        args.rval().setSymbol(JSID_TO_SYMBOL(atoms.symbol_name()));       \
+        return true;                                                      \
+    }
+
+DEFINE_SYMBOL_ATOM_GETTER(hook_up_vfunc)
+DEFINE_SYMBOL_ATOM_GETTER(signal_find)
+DEFINE_SYMBOL_ATOM_GETTER(signals_block)
+DEFINE_SYMBOL_ATOM_GETTER(signals_unblock)
+DEFINE_SYMBOL_ATOM_GETTER(signals_disconnect)
+
+#undef DEFINE_SYMBOL_ATOM_GETTER
 
 static JSFunctionSpec module_funcs[] = {
     JS_FN("override_property", gjs_override_property, 2, GJS_MODULE_PROP_FLAGS),
@@ -435,6 +444,14 @@ static JSFunctionSpec module_funcs[] = {
 static JSPropertySpec module_props[] = {
     JS_PSG("hook_up_vfunc_symbol", hook_up_vfunc_symbol_getter,
            GJS_MODULE_PROP_FLAGS),
+    JS_PSG("signal_find_symbol", signal_find_symbol_getter,
+           GJS_MODULE_PROP_FLAGS),
+    JS_PSG("signals_block_symbol", signals_block_symbol_getter,
+           GJS_MODULE_PROP_FLAGS),
+    JS_PSG("signals_unblock_symbol", signals_unblock_symbol_getter,
+           GJS_MODULE_PROP_FLAGS),
+    JS_PSG("signals_disconnect_symbol", signals_disconnect_symbol_getter,
+           GJS_MODULE_PROP_FLAGS),
     JS_PS_END};
 
 bool gjs_define_private_gi_stuff(JSContext* cx,
diff --git a/gjs/atoms.h b/gjs/atoms.h
index 2395e4af..baf1e681 100644
--- a/gjs/atoms.h
+++ b/gjs/atoms.h
@@ -40,9 +40,11 @@
     macro(connect_after, "connect_after") \
     macro(constructor, "constructor") \
     macro(debuggee, "debuggee") \
+    macro(detail, "detail") \
     macro(emit, "emit") \
     macro(file, "__file__") \
     macro(file_name, "fileName") \
+    macro(func, "func") \
     macro(gi, "gi") \
     macro(gio, "Gio") \
     macro(glib, "GLib") \
@@ -68,6 +70,7 @@
     macro(program_invocation_name, "programInvocationName") \
     macro(prototype, "prototype") \
     macro(search_path, "searchPath") \
+    macro(signal_id, "signalId") \
     macro(stack, "stack") \
     macro(to_string, "toString") \
     macro(value_of, "valueOf") \
@@ -80,7 +83,11 @@
 
 #define FOR_EACH_SYMBOL_ATOM(macro) \
     macro(hook_up_vfunc, "__GObject__hook_up_vfunc") \
-    macro(private_ns_marker, "__gjsPrivateNS")
+    macro(private_ns_marker, "__gjsPrivateNS") \
+    macro(signal_find, "__GObject__signal_find") \
+    macro(signals_block, "__GObject__signals_block") \
+    macro(signals_disconnect, "__GObject__signals_disconnect") \
+    macro(signals_unblock, "__GObject__signals_unblock")
 // clang-format on
 
 struct GjsAtom {
diff --git a/installed-tests/js/testGObjectClass.js b/installed-tests/js/testGObjectClass.js
index ced92ad3..0d80ed38 100644
--- a/installed-tests/js/testGObjectClass.js
+++ b/installed-tests/js/testGObjectClass.js
@@ -568,3 +568,137 @@ describe('Register GType name', function () {
         expect(GtypeClass.$gtype.name).toEqual(expectedSanitized);
     });
 });
+
+describe('Signal handler matching', function () {
+    let o, handleEmpty, emptyId, handleDetailed, detailedId, handleDetailedOne,
+        detailedOneId, handleDetailedTwo, detailedTwoId, handleNotifyTwo,
+        notifyTwoId, handleMinimalOrFull, minimalId, fullId;
+
+    beforeEach(function () {
+        o = new MyObject();
+        handleEmpty = jasmine.createSpy('handleEmpty');
+        emptyId = o.connect('empty', handleEmpty);
+        handleDetailed = jasmine.createSpy('handleDetailed');
+        detailedId = o.connect('detailed', handleDetailed);
+        handleDetailedOne = jasmine.createSpy('handleDetailedOne');
+        detailedOneId = o.connect('detailed::one', handleDetailedOne);
+        handleDetailedTwo = jasmine.createSpy('handleDetailedTwo');
+        detailedTwoId = o.connect('detailed::two', handleDetailedTwo);
+        handleNotifyTwo = jasmine.createSpy('handleNotifyTwo');
+        notifyTwoId = o.connect('notify::two', handleNotifyTwo);
+        handleMinimalOrFull = jasmine.createSpy('handleMinimalOrFull');
+        minimalId = o.connect('minimal', handleMinimalOrFull);
+        fullId = o.connect('full', handleMinimalOrFull);
+    });
+
+    it('finds handlers by signal ID', function () {
+        expect(GObject.signal_handler_find(o, {signalId: 'empty'})).toEqual(emptyId);
+        // when more than one are connected, returns an arbitrary one
+        expect([detailedId, detailedOneId, detailedTwoId])
+            .toContain(GObject.signal_handler_find(o, {signalId: 'detailed'}));
+    });
+
+    it('finds handlers by signal detail', function () {
+        expect(GObject.signal_handler_find(o, {detail: 'one'})).toEqual(detailedOneId);
+        // when more than one are connected, returns an arbitrary one
+        expect([detailedTwoId, notifyTwoId])
+            .toContain(GObject.signal_handler_find(o, {detail: 'two'}));
+    });
+
+    it('finds handlers by callback', function () {
+        expect(GObject.signal_handler_find(o, {func: handleEmpty})).toEqual(emptyId);
+        expect(GObject.signal_handler_find(o, {func: handleDetailed})).toEqual(detailedId);
+        expect(GObject.signal_handler_find(o, {func: handleDetailedOne})).toEqual(detailedOneId);
+        expect(GObject.signal_handler_find(o, {func: handleDetailedTwo})).toEqual(detailedTwoId);
+        expect(GObject.signal_handler_find(o, {func: handleNotifyTwo})).toEqual(notifyTwoId);
+        // when more than one are connected, returns an arbitrary one
+        expect([minimalId, fullId])
+            .toContain(GObject.signal_handler_find(o, {func: handleMinimalOrFull}));
+    });
+
+    it('finds handlers by a combination of parameters', function () {
+        expect(GObject.signal_handler_find(o, {signalId: 'detailed', detail: 'two'}))
+            .toEqual(detailedTwoId);
+        expect(GObject.signal_handler_find(o, {signalId: 'detailed', func: handleDetailed}))
+            .toEqual(detailedId);
+    });
+
+    it('blocks a handler by callback', function () {
+        expect(GObject.signal_handlers_block_matched(o, {func: handleEmpty})).toEqual(1);
+        o.emitEmpty();
+        expect(handleEmpty).not.toHaveBeenCalled();
+
+        expect(GObject.signal_handlers_unblock_matched(o, {func: handleEmpty})).toEqual(1);
+        o.emitEmpty();
+        expect(handleEmpty).toHaveBeenCalled();
+    });
+
+    it('blocks multiple handlers by callback', function () {
+        expect(GObject.signal_handlers_block_matched(o, {func: handleMinimalOrFull})).toEqual(2);
+        o.emitMinimal();
+        o.emitFull();
+        expect(handleMinimalOrFull).not.toHaveBeenCalled();
+
+        expect(GObject.signal_handlers_unblock_matched(o, {func: handleMinimalOrFull})).toEqual(2);
+        o.emitMinimal();
+        o.emitFull();
+        expect(handleMinimalOrFull).toHaveBeenCalledTimes(2);
+    });
+
+    it('blocks handlers by a combination of parameters', function () {
+        expect(GObject.signal_handlers_block_matched(o, {signalId: 'detailed', func: handleDetailed}))
+            .toEqual(1);
+        o.emit('detailed', '');
+        o.emit('detailed::one', '');
+        expect(handleDetailed).not.toHaveBeenCalled();
+        expect(handleDetailedOne).toHaveBeenCalled();
+
+        expect(GObject.signal_handlers_unblock_matched(o, {signalId: 'detailed', func: handleDetailed}))
+            .toEqual(1);
+        o.emit('detailed', '');
+        o.emit('detailed::one', '');
+        expect(handleDetailed).toHaveBeenCalled();
+    });
+
+    it('disconnects a handler by callback', function () {
+        expect(GObject.signal_handlers_disconnect_matched(o, {func: handleEmpty})).toEqual(1);
+        o.emitEmpty();
+        expect(handleEmpty).not.toHaveBeenCalled();
+    });
+
+    it('blocks multiple handlers by callback', function () {
+        expect(GObject.signal_handlers_disconnect_matched(o, {func: handleMinimalOrFull})).toEqual(2);
+        o.emitMinimal();
+        o.emitFull();
+        expect(handleMinimalOrFull).not.toHaveBeenCalled();
+    });
+
+    it('blocks handlers by a combination of parameters', function () {
+        expect(GObject.signal_handlers_disconnect_matched(o, {signalId: 'detailed', func: handleDetailed}))
+            .toEqual(1);
+        o.emit('detailed', '');
+        o.emit('detailed::one', '');
+        expect(handleDetailed).not.toHaveBeenCalled();
+        expect(handleDetailedOne).toHaveBeenCalled();
+    });
+
+    it('blocks a handler by callback, convenience method', function () {
+        expect(GObject.signal_handlers_block_by_func(o, handleEmpty)).toEqual(1);
+        o.emitEmpty();
+        expect(handleEmpty).not.toHaveBeenCalled();
+
+        expect(GObject.signal_handlers_unblock_by_func(o, handleEmpty)).toEqual(1);
+        o.emitEmpty();
+        expect(handleEmpty).toHaveBeenCalled();
+    });
+
+    it('disconnects a handler by callback, convenience method', function () {
+        expect(GObject.signal_handlers_disconnect_by_func(o, handleEmpty)).toEqual(1);
+        o.emitEmpty();
+        expect(handleEmpty).not.toHaveBeenCalled();
+    });
+
+    it('does not support disconnecting a handler by callback data', function () {
+        expect(() => GObject.signal_handlers_disconnect_by_data(o, null)).toThrow();
+    });
+});
diff --git a/modules/overrides/GObject.js b/modules/overrides/GObject.js
index b22d63b3..a5747c21 100644
--- a/modules/overrides/GObject.js
+++ b/modules/overrides/GObject.js
@@ -593,4 +593,157 @@ function _init() {
     GObject.signal_emit_by_name = function (object, ...nameAndArgs) {
         return GObject.Object.prototype.emit.apply(object, nameAndArgs);
     };
+
+    // Replacements for signal_handler_find() and similar functions, which can't
+    // work normally since we connect private closures
+    GObject._real_signal_handler_find = GObject.signal_handler_find;
+    GObject._real_signal_handlers_block_matched = GObject.signal_handlers_block_matched;
+    GObject._real_signal_handlers_unblock_matched = GObject.signal_handlers_unblock_matched;
+    GObject._real_signal_handlers_disconnect_matched = GObject.signal_handlers_disconnect_matched;
+
+    /**
+     * Finds the first signal handler that matches certain selection criteria.
+     * The criteria are passed as properties of a match object.
+     * The match object has to be non-empty for successful matches.
+     * If no handler was found, a falsy value is returned.
+     * @function
+     * @param {GObject.Object} instance - the instance owning the signal handler
+     *   to be found.
+     * @param {Object} match - a properties object indicating whether to match
+     *   by signal ID, detail, or callback function.
+     * @param {string} [match.signalId] - signal the handler has to be connected
+     *   to.
+     * @param {string} [match.detail] - signal detail the handler has to be
+     *   connected to.
+     * @param {Function} [match.func] - the callback function the handler will
+     *   invoke.
+     * @returns {number|BigInt|Object|null} A valid non-0 signal handler ID for
+     *   a successful match.
+     */
+    GObject.signal_handler_find = function (instance, match) {
+        // For backwards compatibility
+        if (arguments.length === 7)
+            // eslint-disable-next-line prefer-rest-params
+            return GObject._real_signal_handler_find(...arguments);
+        return instance[Gi.signal_find_symbol](match);
+    };
+    /**
+     * Blocks all handlers on an instance that match certain selection criteria.
+     * The criteria are passed as properties of a match object.
+     * The match object has to have at least `func` for successful matches.
+     * If no handlers were found, 0 is returned, the number of blocked handlers
+     * otherwise.
+     * @function
+     * @param {GObject.Object} instance - the instance owning the signal handler
+     *   to be found.
+     * @param {Object} match - a properties object indicating whether to match
+     *   by signal ID, detail, or callback function.
+     * @param {string} [match.signalId] - signal the handler has to be connected
+     *   to.
+     * @param {string} [match.detail] - signal detail the handler has to be
+     *   connected to.
+     * @param {Function} match.func - the callback function the handler will
+     *   invoke.
+     * @returns {number} The number of handlers that matched.
+     */
+    GObject.signal_handlers_block_matched = function (instance, match) {
+        // For backwards compatibility
+        if (arguments.length === 7)
+            // eslint-disable-next-line prefer-rest-params
+            return GObject._real_signal_handlers_block_matched(...arguments);
+        return instance[Gi.signals_block_symbol](match);
+    };
+    /**
+     * Unblocks all handlers on an instance that match certain selection
+     * criteria.
+     * The criteria are passed as properties of a match object.
+     * The match object has to have at least `func` for successful matches.
+     * If no handlers were found, 0 is returned, the number of unblocked
+     * handlers otherwise.
+     * The match criteria should not apply to any handlers that are not
+     * currently blocked.
+     * @function
+     * @param {GObject.Object} instance - the instance owning the signal handler
+     *   to be found.
+     * @param {Object} match - a properties object indicating whether to match
+     *   by signal ID, detail, or callback function.
+     * @param {string} [match.signalId] - signal the handler has to be connected
+     *   to.
+     * @param {string} [match.detail] - signal detail the handler has to be
+     *   connected to.
+     * @param {Function} match.func - the callback function the handler will
+     *   invoke.
+     * @returns {number} The number of handlers that matched.
+     */
+    GObject.signal_handlers_unblock_matched = function (instance, match) {
+        // For backwards compatibility
+        if (arguments.length === 7)
+            // eslint-disable-next-line prefer-rest-params
+            return GObject._real_signal_handlers_unblock_matched(...arguments);
+        return instance[Gi.signals_unblock_symbol](match);
+    };
+    /**
+     * Disconnects all handlers on an instance that match certain selection
+     * criteria.
+     * The criteria are passed as properties of a match object.
+     * The match object has to have at least `func` for successful matches.
+     * If no handlers were found, 0 is returned, the number of disconnected
+     * handlers otherwise.
+     * @function
+     * @param {GObject.Object} instance - the instance owning the signal handler
+     *   to be found.
+     * @param {Object} match - a properties object indicating whether to match
+     *   by signal ID, detail, or callback function.
+     * @param {string} [match.signalId] - signal the handler has to be connected
+     *   to.
+     * @param {string} [match.detail] - signal detail the handler has to be
+     *   connected to.
+     * @param {Function} match.func - the callback function the handler will
+     *   invoke.
+     * @returns {number} The number of handlers that matched.
+     */
+    GObject.signal_handlers_disconnect_matched = function (instance, match) {
+        // For backwards compatibility
+        if (arguments.length === 7)
+            // eslint-disable-next-line prefer-rest-params
+            return GObject._real_signal_handlers_disconnect_matched(...arguments);
+        return instance[Gi.signals_disconnect_symbol](match);
+    };
+
+    // Also match the macros used in C APIs, even though they're not introspected
+
+    /**
+     * Blocks all handlers on an instance that match `func`.
+     * @function
+     * @param {GObject.Object} instance - the instance to block handlers from.
+     * @param {Function} func - the callback function the handler will invoke.
+     * @returns {number} The number of handlers that matched.
+     */
+    GObject.signal_handlers_block_by_func = function (instance, func) {
+        return instance[Gi.signals_block_symbol]({func});
+    };
+    /**
+     * Unblocks all handlers on an instance that match `func`.
+     * @function
+     * @param {GObject.Object} instance - the instance to unblock handlers from.
+     * @param {Function} func - the callback function the handler will invoke.
+     * @returns {number} The number of handlers that matched.
+     */
+    GObject.signal_handlers_unblock_by_func = function (instance, func) {
+        return instance[Gi.signals_unblock_symbol]({func});
+    };
+    /**
+     * Disconnects all handlers on an instance that match `func`.
+     * @function
+     * @param {GObject.Object} instance - the instance to remove handlers from.
+     * @param {Function} func - the callback function the handler will invoke.
+     * @returns {number} The number of handlers that matched.
+     */
+    GObject.signal_handlers_disconnect_by_func = function (instance, func) {
+        return instance[Gi.signals_disconnect_symbol]({func});
+    };
+    GObject.signal_handlers_disconnect_by_data = function () {
+        throw new Error('GObject.signal_handlers_disconnect_by_data() is not \
+introspectable. Use GObject.signal_handlers_disconnect_by_func() instead.');
+    };
 }


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