diff options
author | Christian Hergert <chergert@redhat.com> | 2021-08-27 13:01:00 -0700 |
---|---|---|
committer | Christian Hergert <chergert@redhat.com> | 2021-08-27 13:01:15 -0700 |
commit | 8bd8f4b29634447c66e66e26434c7405ffec0758 (patch) | |
tree | 71871d3ff5d30b616737b2a7026535f8db96129c | |
parent | 01a5efe0e3c8ef6a6bc3de9781afb8b03c6df301 (diff) | |
download | glib-wip/chergert/add-gbindinggroup.tar.gz |
gobject: add GSignalGroupwip/chergert/add-gbindinggroup
Much like GBindingGroup, the GSignalGroup object allows you to connect many
signal connections for an object and connect/disconnect/block/unblock them
as a group.
This is useful when using many connections on an object to ensure that they
are properly removed when changing state or disposing a third-party
object.
This has been used for years in various GNOME projects and makes sense to
have upstream instead of multiple copies.
-rw-r--r-- | docs/reference/gobject/gobject-docs.xml | 1 | ||||
-rw-r--r-- | docs/reference/gobject/gobject-sections.txt | 22 | ||||
-rw-r--r-- | glib/glib-object.h | 1 | ||||
-rw-r--r-- | gobject/gsignalgroup.c | 857 | ||||
-rw-r--r-- | gobject/gsignalgroup.h | 91 | ||||
-rw-r--r-- | gobject/meson.build | 2 | ||||
-rw-r--r-- | gobject/tests/meson.build | 1 | ||||
-rw-r--r-- | gobject/tests/signalgroup.c | 486 |
8 files changed, 1461 insertions, 0 deletions
diff --git a/docs/reference/gobject/gobject-docs.xml b/docs/reference/gobject/gobject-docs.xml index 7deb9a548..1a94d0f82 100644 --- a/docs/reference/gobject/gobject-docs.xml +++ b/docs/reference/gobject/gobject-docs.xml @@ -81,6 +81,7 @@ <xi:include href="xml/gparamspec.xml" /> <xi:include href="xml/value_collection.xml" /> <xi:include href="xml/signals.xml" /> + <xi:include href="xml/gsignalgroup.xml" /> <xi:include href="xml/gclosure.xml" /> <xi:include href="xml/value_arrays.xml" /> <xi:include href="xml/gbinding.xml" /> diff --git a/docs/reference/gobject/gobject-sections.txt b/docs/reference/gobject/gobject-sections.txt index 8b207ffd3..48c5a7040 100644 --- a/docs/reference/gobject/gobject-sections.txt +++ b/docs/reference/gobject/gobject-sections.txt @@ -1024,3 +1024,25 @@ G_IS_BINDING_GROUP <SUBSECTION Private> g_binding_group_get_type </SECTION> + +<SECTION> +<FILE>gsignalgroup</FILE> +GSignalGroup +g_signal_group_block +g_signal_group_connect +g_signal_group_connect_after +g_signal_group_connect_data +g_signal_group_connect_object +g_signal_group_connect_swapped +g_signal_group_get_target +g_signal_group_get_type +g_signal_group_new +g_signal_group_set_target +g_signal_group_unblock +<SUBSECTION Standard> +G_IS_SIGNAL_GROUP +G_SIGNAL_GROUP +G_TYPE_SIGNAL_GROUP +<SUBSECTION Private> +g_signal_group_get_type +</SECTION> diff --git a/glib/glib-object.h b/glib/glib-object.h index d259a3d39..915a29901 100644 --- a/glib/glib-object.h +++ b/glib/glib-object.h @@ -28,6 +28,7 @@ #include <gobject/gparam.h> #include <gobject/gparamspecs.h> #include <gobject/gsignal.h> +#include <gobject/gsignalgroup.h> #include <gobject/gsourceclosure.h> #include <gobject/gtype.h> #include <gobject/gtypemodule.h> diff --git a/gobject/gsignalgroup.c b/gobject/gsignalgroup.c new file mode 100644 index 000000000..13270b1fd --- /dev/null +++ b/gobject/gsignalgroup.c @@ -0,0 +1,857 @@ +/* gsignalgroup.c + * + * Copyright (C) 2015 Christian Hergert <christian@hergert.me> + * Copyright (C) 2015 Garrett Regier <garrettregier@gmail.com> + * + * This file is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 of the + * License, or (at your option) any later version. + * + * This file is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#include "config.h" +#include "glib.h" +#include "glibintl.h" + +#include "gparamspecs.h" +#include "gsignalgroup.h" +#include "gvaluetypes.h" + +/** + * SECTION:gsignalgroup + * @Title: GSignalGroup + * @Short_description: Manage a collection of signals on a GObject + * + * #GSignalGroup manages to simplify the process of connecting + * many signals to a #GObject as a group. As such there is no API + * to disconnect a signal from the group. + * + * In particular, this allows you to: + * + * - Change the target instance, which automatically causes disconnection + * of the signals from the old instance and connecting to the new instance. + * - Block and unblock signals as a group + * - Ensuring that blocked state transfers across target instances. + * + * One place you might want to use such a structure is with #GtkTextView and + * #GtkTextBuffer. Often times, you'll need to connect to many signals on + * #GtkTextBuffer from a #GtkTextView subclass. This allows you to create a + * signal group during instance construction, simply bind the + * #GtkTextView:buffer property to #GSignalGroup:target and connect + * all the signals you need. When the #GtkTextView:buffer property changes + * all of the signals will be transitioned correctly. + * + * Since: 2.70 + */ + +struct _GSignalGroup +{ + GObject parent_instance; + + GWeakRef target_ref; + GPtrArray *handlers; + GType target_type; + gsize block_count; + + guint has_bound_at_least_once : 1; +}; + +typedef struct _GSignalGroupClass +{ + GObjectClass parent_class; + + void (*bind) (GSignalGroup *self, + GObject *target); +} GSignalGroupClass; + +typedef struct +{ + GSignalGroup *group; + gulong handler_id; + GClosure *closure; + guint signal_id; + GQuark signal_detail; + guint connect_after : 1; +} SignalHandler; + +G_DEFINE_TYPE (GSignalGroup, g_signal_group, G_TYPE_OBJECT) + +enum { + PROP_0, + PROP_TARGET, + PROP_TARGET_TYPE, + LAST_PROP +}; + +enum { + BIND, + UNBIND, + LAST_SIGNAL +}; + +static GParamSpec *properties [LAST_PROP]; +static guint signals [LAST_SIGNAL]; + +static void +g_signal_group_set_target_type (GSignalGroup *self, + GType target_type) +{ + g_assert (G_IS_SIGNAL_GROUP (self)); + g_assert (g_type_is_a (target_type, G_TYPE_OBJECT)); + + self->target_type = target_type; + + /* The class must be created at least once for the signals + * to be registered, otherwise g_signal_parse_name() will fail + */ + if (G_TYPE_IS_INTERFACE (target_type)) + { + if (g_type_default_interface_peek (target_type) == NULL) + g_type_default_interface_unref (g_type_default_interface_ref (target_type)); + } + else + { + if (g_type_class_peek (target_type) == NULL) + g_type_class_unref (g_type_class_ref (target_type)); + } +} + +static void +g_signal_group_gc_handlers (GSignalGroup *self) +{ + g_assert (G_IS_SIGNAL_GROUP (self)); + + /* + * Remove any handlers for which the closures have become invalid. We do + * this cleanup lazily to avoid situations where we could have disposal + * active on both the signal group and the peer object. + */ + + for (guint i = self->handlers->len; i > 0; i--) + { + const SignalHandler *handler = g_ptr_array_index (self->handlers, i - 1); + + g_assert (handler != NULL); + g_assert (handler->closure != NULL); + + if (handler->closure->is_invalid) + g_ptr_array_remove_index (self->handlers, i - 1); + } +} + +static void +g_signal_group__target_weak_notify (gpointer data, + GObject *where_object_was) +{ + GSignalGroup *self = data; + + g_assert (G_IS_SIGNAL_GROUP (self)); + g_assert (where_object_was != NULL); + + g_weak_ref_set (&self->target_ref, NULL); + + for (guint i = 0; i < self->handlers->len; i++) + { + SignalHandler *handler = g_ptr_array_index (self->handlers, i); + + handler->handler_id = 0; + } + + g_signal_emit (self, signals [UNBIND], 0); + g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_TARGET]); +} + +static void +g_signal_group_bind_handler (GSignalGroup *self, + SignalHandler *handler, + GObject *target) +{ + g_assert (self != NULL); + g_assert (G_IS_OBJECT (target)); + g_assert (handler != NULL); + g_assert (handler->signal_id != 0); + g_assert (handler->closure != NULL); + g_assert (handler->closure->is_invalid == 0); + g_assert (handler->handler_id == 0); + + handler->handler_id = g_signal_connect_closure_by_id (target, + handler->signal_id, + handler->signal_detail, + handler->closure, + handler->connect_after); + + g_assert (handler->handler_id != 0); + + for (guint i = 0; i < self->block_count; i++) + g_signal_handler_block (target, handler->handler_id); +} + +static void +g_signal_group_bind (GSignalGroup *self, + GObject *target) +{ + GObject *hold; + + g_assert (G_IS_SIGNAL_GROUP (self)); + g_assert (!target || G_IS_OBJECT (target)); + + if (target == NULL) + return; + + self->has_bound_at_least_once = TRUE; + + hold = g_object_ref (target); + + g_weak_ref_set (&self->target_ref, hold); + g_object_weak_ref (hold, g_signal_group__target_weak_notify, self); + + g_signal_group_gc_handlers (self); + + for (guint i = 0; i < self->handlers->len; i++) + { + SignalHandler *handler = g_ptr_array_index (self->handlers, i); + + g_signal_group_bind_handler (self, handler, hold); + } + + g_signal_emit (self, signals [BIND], 0, hold); + + g_object_unref (hold); +} + +static void +g_signal_group_unbind (GSignalGroup *self) +{ + GObject *target; + + g_return_if_fail (G_IS_SIGNAL_GROUP (self)); + + target = g_weak_ref_get (&self->target_ref); + + /* + * Target may be NULL by this point, as we got notified of its destruction. + * However, if we're early enough, we may get a full reference back and can + * cleanly disconnect our connections. + */ + + if (target != NULL) + { + g_weak_ref_set (&self->target_ref, NULL); + + /* + * Let go of our weak reference now that we have a full reference + * for the life of this function. + */ + g_object_weak_unref (target, + g_signal_group__target_weak_notify, + self); + } + + g_signal_group_gc_handlers (self); + + for (guint i = 0; i < self->handlers->len; i++) + { + SignalHandler *handler; + gulong handler_id; + + handler = g_ptr_array_index (self->handlers, i); + + g_assert (handler != NULL); + g_assert (handler->signal_id != 0); + g_assert (handler->closure != NULL); + + handler_id = handler->handler_id; + handler->handler_id = 0; + + /* + * If @target is NULL, we lost a race to cleanup the weak + * instance and the signal connections have already been + * finalized and therefore nothing to do. + */ + + if (target != NULL && handler_id != 0) + g_signal_handler_disconnect (target, handler_id); + } + + g_signal_emit (self, signals [UNBIND], 0); + + g_clear_object (&target); +} + +static gboolean +g_signal_group_check_target_type (GSignalGroup *self, + gpointer target) +{ + if ((target != NULL) && + !g_type_is_a (G_OBJECT_TYPE (target), self->target_type)) + { + g_critical ("Failed to set GSignalGroup of target type %s " + "using target %p of type %s", + g_type_name (self->target_type), + target, G_OBJECT_TYPE_NAME (target)); + return FALSE; + } + + return TRUE; +} + +/** + * g_signal_group_block: + * @self: the #GSignalGroup + * + * Blocks all signal handlers managed by @self so they will not + * be called during any signal emissions. Must be unblocked exactly + * the same number of times it has been blocked to become active again. + * + * This blocked state will be kept across changes of the target instance. + * + * Since: 2.70 + */ +void +g_signal_group_block (GSignalGroup *self) +{ + GObject *target; + + g_return_if_fail (G_IS_SIGNAL_GROUP (self)); + g_return_if_fail (self->block_count != G_MAXSIZE); + + self->block_count++; + + target = g_weak_ref_get (&self->target_ref); + + if (target == NULL) + return; + + for (guint i = 0; i < self->handlers->len; i++) + { + const SignalHandler *handler = g_ptr_array_index (self->handlers, i); + + g_assert (handler != NULL); + g_assert (handler->signal_id != 0); + g_assert (handler->closure != NULL); + g_assert (handler->handler_id != 0); + + g_signal_handler_block (target, handler->handler_id); + } + + g_object_unref (target); +} + +/** + * g_signal_group_unblock: + * @self: the #GSignalGroup + * + * Unblocks all signal handlers managed by @self so they will be + * called again during any signal emissions unless it is blocked + * again. Must be unblocked exactly the same number of times it + * has been blocked to become active again. + * + * Since: 2.70 + */ +void +g_signal_group_unblock (GSignalGroup *self) +{ + GObject *target; + + g_return_if_fail (G_IS_SIGNAL_GROUP (self)); + g_return_if_fail (self->block_count != 0); + + self->block_count--; + + target = g_weak_ref_get (&self->target_ref); + + if (target == NULL) + return; + + for (guint i = 0; i < self->handlers->len; i++) + { + const SignalHandler *handler = g_ptr_array_index (self->handlers, i); + + g_assert (handler != NULL); + g_assert (handler->signal_id != 0); + g_assert (handler->closure != NULL); + g_assert (handler->handler_id != 0); + + g_signal_handler_unblock (target, handler->handler_id); + } + + g_object_unref (target); +} + +/** + * g_signal_group_get_target: + * @self: the #GSignalGroup + * + * Gets the target instance used when connecting signals. + * + * Returns: (nullable) (transfer none) (type GObject): The target instance + * + * Since: 2.70 + */ +gpointer +g_signal_group_get_target (GSignalGroup *self) +{ + GObject *target; + + g_return_val_if_fail (G_IS_SIGNAL_GROUP (self), NULL); + + target = g_weak_ref_get (&self->target_ref); + + /* + * It is expected that this is called from a thread that owns a reference to + * the target, so we can pass back a borrowed reference. However, to ensure + * that we aren't racing in finalization of @target, we must ensure that the + * ref_count >= 2 (as our get just incremented by one). + */ + + if (target == NULL || target->ref_count < 2) + return NULL; + + /* Unref and pass back a borrowed reference. This looks unsafe, but is safe + * because of our reference check above, so much as the assertion holds that + * the caller obeyed the ownership rules of this class. + */ + g_object_unref (target); + return target; +} + +/** + * g_signal_group_set_target: + * @self: the #GSignalGroup. + * @target: (nullable) (type GObject): The target instance used + * when connecting signals. + * + * Sets the target instance used when connecting signals. Any signal + * that has been registered with g_signal_group_connect_object() or + * similar functions will be connected to this object. + * + * If the target instance was previously set, signals will be + * disconnected from that object prior to connecting to @target. + * + * Since: 2.70 + */ +void +g_signal_group_set_target (GSignalGroup *self, + gpointer target) +{ + GObject *object; + + g_return_if_fail (G_IS_SIGNAL_GROUP (self)); + + object = g_weak_ref_get (&self->target_ref); + + if (object == (GObject *)target) + goto unref; + + if (!g_signal_group_check_target_type (self, target)) + goto unref; + + /* Only emit unbind if we've ever called bind */ + if (self->has_bound_at_least_once) + g_signal_group_unbind (self); + + g_signal_group_bind (self, target); + + g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_TARGET]); + +unref: + g_clear_object (&object); +} + +static void +signal_handler_free (gpointer data) +{ + SignalHandler *handler = data; + + if (handler->closure != NULL) + g_closure_invalidate (handler->closure); + + handler->handler_id = 0; + handler->signal_id = 0; + handler->signal_detail = 0; + g_clear_pointer (&handler->closure, g_closure_unref); + g_slice_free (SignalHandler, handler); +} + +static void +g_signal_group_constructed (GObject *object) +{ + GSignalGroup *self = (GSignalGroup *)object; + GObject *target = g_weak_ref_get (&self->target_ref); + + if (!g_signal_group_check_target_type (self, target)) + g_signal_group_set_target (self, NULL); + + G_OBJECT_CLASS (g_signal_group_parent_class)->constructed (object); + + g_clear_object (&target); +} + +static void +g_signal_group_dispose (GObject *object) +{ + GSignalGroup *self = (GSignalGroup *)object; + + g_signal_group_gc_handlers (self); + + if (self->has_bound_at_least_once) + g_signal_group_unbind (self); + + g_clear_pointer (&self->handlers, g_ptr_array_unref); + + G_OBJECT_CLASS (g_signal_group_parent_class)->dispose (object); +} + +static void +g_signal_group_finalize (GObject *object) +{ + GSignalGroup *self = (GSignalGroup *)object; + + g_weak_ref_clear (&self->target_ref); + + G_OBJECT_CLASS (g_signal_group_parent_class)->finalize (object); +} + +static void +g_signal_group_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + GSignalGroup *self = G_SIGNAL_GROUP (object); + + switch (prop_id) + { + case PROP_TARGET: + g_value_take_object (value, g_weak_ref_get (&self->target_ref)); + break; + + case PROP_TARGET_TYPE: + g_value_set_gtype (value, self->target_type); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +g_signal_group_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + GSignalGroup *self = G_SIGNAL_GROUP (object); + + switch (prop_id) + { + case PROP_TARGET: + g_signal_group_set_target (self, g_value_get_object (value)); + break; + + case PROP_TARGET_TYPE: + g_signal_group_set_target_type (self, g_value_get_gtype (value)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +g_signal_group_class_init (GSignalGroupClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->constructed = g_signal_group_constructed; + object_class->dispose = g_signal_group_dispose; + object_class->finalize = g_signal_group_finalize; + object_class->get_property = g_signal_group_get_property; + object_class->set_property = g_signal_group_set_property; + + /** + * GSignalGroup:target + * + * The target instance used when connecting signals. + */ + properties [PROP_TARGET] = + g_param_spec_object ("target", + "Target", + "The target instance used when connecting signals.", + G_TYPE_OBJECT, + (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS)); + + /** + * GSignalGroup:target-type + * + * The GType of the target property. + */ + properties [PROP_TARGET_TYPE] = + g_param_spec_gtype ("target-type", + "Target Type", + "The GType of the target property.", + G_TYPE_OBJECT, + (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS)); + + g_object_class_install_properties (object_class, LAST_PROP, properties); + + /** + * GSignalGroup::bind: + * @self: the #GSignalGroup + * @instance: a #GObject + * + * This signal is emitted when the target instance of @self + * is set to a new #GObject. + * + * This signal will only be emitted if the target of @self is non-%NULL. + */ + signals [BIND] = + g_signal_new ("bind", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + 0, + NULL, NULL, NULL, + G_TYPE_NONE, + 1, + G_TYPE_OBJECT); + + /** + * GSignalGroup::unbind: + * @self: a #GSignalGroup + * + * This signal is emitted when the target instance of @self + * is set to a new #GObject. + * + * This signal will only be emitted if the previous target + * of @self is non-%NULL. + */ + signals [UNBIND] = + g_signal_new ("unbind", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + 0, + NULL, NULL, NULL, + G_TYPE_NONE, + 0); +} + +static void +g_signal_group_init (GSignalGroup *self) +{ + self->handlers = g_ptr_array_new_with_free_func (signal_handler_free); + self->target_type = G_TYPE_OBJECT; +} + +/** + * g_signal_group_new: + * @target_type: the #GType of the target instance. + * + * Creates a new #GSignalGroup for target instances of @target_type. + * + * Returns: a new #GSignalGroup + * + * Since: 2.70 + */ +GSignalGroup * +g_signal_group_new (GType target_type) +{ + g_return_val_if_fail (g_type_is_a (target_type, G_TYPE_OBJECT), NULL); + + return g_object_new (G_TYPE_SIGNAL_GROUP, + "target-type", target_type, + NULL); +} + +static void +g_signal_group_connect_full (GSignalGroup *self, + const gchar *detailed_signal, + GCallback callback, + gpointer data, + GClosureNotify notify, + GConnectFlags flags, + gboolean is_object) +{ + GObject *target; + SignalHandler *handler; + GClosure *closure; + guint signal_id; + GQuark signal_detail; + + g_return_if_fail (G_IS_SIGNAL_GROUP (self)); + g_return_if_fail (detailed_signal != NULL); + g_return_if_fail (g_signal_parse_name (detailed_signal, self->target_type, + &signal_id, &signal_detail, TRUE) != 0); + g_return_if_fail (callback != NULL); + g_return_if_fail (!is_object || G_IS_OBJECT (data)); + + if ((flags & G_CONNECT_SWAPPED) != 0) + closure = g_cclosure_new_swap (callback, data, notify); + else + closure = g_cclosure_new (callback, data, notify); + + handler = g_slice_new0 (SignalHandler); + handler->group = self; + handler->signal_id = signal_id; + handler->signal_detail = signal_detail; + handler->closure = g_closure_ref (closure); + handler->connect_after = ((flags & G_CONNECT_AFTER) != 0); + + g_closure_sink (closure); + + if (is_object) + { + /* Set closure->is_invalid when data is disposed. We only track this to avoid + * reconnecting in the future. However, we do a round of cleanup when ever we + * connect a new object or the target changes to GC the old handlers. + */ + g_object_watch_closure (data, closure); + } + + g_ptr_array_add (self->handlers, handler); + + target = g_weak_ref_get (&self->target_ref); + + if (target != NULL) + { + g_signal_group_bind_handler (self, handler, target); + g_object_unref (target); + } + + /* Lazily remove any old handlers on connect */ + g_signal_group_gc_handlers (self); +} + +/** + * g_signal_group_connect_object: (skip) + * @self: a #GSignalGroup + * @detailed_signal: a string of the form "signal-name::detail" + * @c_handler: (scope notified): the #GCallback to connect + * @object: the #GObject to pass as data to @callback calls + * @flags: #GConnectFlags for the signal connection + * + * Connects @callback to the signal @detailed_signal + * on the target object of @self. + * + * Ensures that the @object stays alive during the call to @callback + * by temporarily adding a reference count. When the @object is destroyed + * the signal handler will automatically be removed. + * + * Since: 2.70 + */ +void +g_signal_group_connect_object (GSignalGroup *self, + const gchar *detailed_signal, + GCallback c_handler, + gpointer object, + GConnectFlags flags) +{ + g_return_if_fail (G_IS_OBJECT (object)); + + g_signal_group_connect_full (self, detailed_signal, c_handler, object, NULL, + flags, TRUE); +} + +/** + * g_signal_group_connect_data: + * @self: a #GSignalGroup + * @detailed_signal: a string of the form "signal-name::detail" + * @c_handler: (scope notified) (closure data) (destroy notify): the #GCallback to connect + * @data: the data to pass to @callback calls + * @notify: function to be called when disposing of @self + * @flags: the flags used to create the signal connection + * + * Connects @callback to the signal @detailed_signal + * on the target instance of @self. + * + * Since: 2.70 + */ +void +g_signal_group_connect_data (GSignalGroup *self, + const gchar *detailed_signal, + GCallback c_handler, + gpointer data, + GClosureNotify notify, + GConnectFlags flags) +{ + g_signal_group_connect_full (self, detailed_signal, c_handler, data, notify, + flags, FALSE); +} + +/** + * g_signal_group_connect: (skip) + * @self: a #GSignalGroup + * @detailed_signal: a string of the form "signal-name::detail" + * @c_handler: (scope notified): the #GCallback to connect + * @data: the data to pass to @callback calls + * + * Connects @callback to the signal @detailed_signal + * on the target instance of @self. + * + * Since: 2.70 + */ +void +g_signal_group_connect (GSignalGroup *self, + const gchar *detailed_signal, + GCallback c_handler, + gpointer data) +{ + g_signal_group_connect_full (self, detailed_signal, c_handler, data, NULL, + 0, FALSE); +} + +/** + * g_signal_group_connect_after: (skip) + * @self: a #GSignalGroup + * @detailed_signal: a string of the form "signal-name::detail" + * @c_handler: (scope notified): the #GCallback to connect + * @data: the data to pass to @callback calls + * + * Connects @callback to the signal @detailed_signal + * on the target instance of @self. + * + * The @callback will be called after the default handler of the signal. + * + * Since: 2.70 + */ +void +g_signal_group_connect_after (GSignalGroup *self, + const gchar *detailed_signal, + GCallback c_handler, + gpointer data) +{ + g_signal_group_connect_full (self, detailed_signal, c_handler, + data, NULL, G_CONNECT_AFTER, FALSE); +} + +/** + * g_signal_group_connect_swapped: + * @self: a #GSignalGroup + * @detailed_signal: a string of the form "signal-name::detail" + * @c_handler: (scope async): the #GCallback to connect + * @data: the data to pass to @callback calls + * + * Connects @callback to the signal @detailed_signal + * on the target instance of @self. + * + * The instance on which the signal is emitted and @data + * will be swapped when calling @callback. + * + * Since: 2.70 + */ +void +g_signal_group_connect_swapped (GSignalGroup *self, + const gchar *detailed_signal, + GCallback c_handler, + gpointer data) +{ + g_signal_group_connect_full (self, detailed_signal, c_handler, data, NULL, + G_CONNECT_SWAPPED, FALSE); +} diff --git a/gobject/gsignalgroup.h b/gobject/gsignalgroup.h new file mode 100644 index 000000000..3ced61682 --- /dev/null +++ b/gobject/gsignalgroup.h @@ -0,0 +1,91 @@ +/* gsignalgroup.h + * + * Copyright (C) 2015 Christian Hergert <christian@hergert.me> + * Copyright (C) 2015 Garrett Regier <garrettregier@gmail.com> + * + * This file is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 of the + * License, or (at your option) any later version. + * + * This file is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#ifndef __G_SIGNAL_GROUP_H__ +#define __G_SIGNAL_GROUP_H__ + +#if !defined (__GLIB_GOBJECT_H_INSIDE__) && !defined (GOBJECT_COMPILATION) +#error "Only <glib-object.h> can be included directly." +#endif + +#include <glib.h> +#include <gobject/gobject.h> +#include <gobject/gsignal.h> + +G_BEGIN_DECLS + +#define G_SIGNAL_GROUP(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), G_TYPE_SIGNAL_GROUP, GSignalGroup)) +#define G_IS_SIGNAL_GROUP(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), G_TYPE_SIGNAL_GROUP)) +#define G_TYPE_SIGNAL_GROUP (g_signal_group_get_type()) + +/** + * GSignalGroup: + * + * GSignalGroup is an opaque structure whose members + * cannot be accessed directly. + * + * Since: 2.70 + */ +typedef struct _GSignalGroup GSignalGroup; + +GLIB_AVAILABLE_IN_2_70 +GType g_signal_group_get_type (void) G_GNUC_CONST; +GLIB_AVAILABLE_IN_2_70 +GSignalGroup *g_signal_group_new (GType target_type); +GLIB_AVAILABLE_IN_2_70 +void g_signal_group_set_target (GSignalGroup *self, + gpointer target); +GLIB_AVAILABLE_IN_2_70 +gpointer g_signal_group_get_target (GSignalGroup *self); +GLIB_AVAILABLE_IN_2_70 +void g_signal_group_block (GSignalGroup *self); +GLIB_AVAILABLE_IN_2_70 +void g_signal_group_unblock (GSignalGroup *self); +GLIB_AVAILABLE_IN_2_70 +void g_signal_group_connect_object (GSignalGroup *self, + const gchar *detailed_signal, + GCallback c_handler, + gpointer object, + GConnectFlags flags); +GLIB_AVAILABLE_IN_2_70 +void g_signal_group_connect_data (GSignalGroup *self, + const gchar *detailed_signal, + GCallback c_handler, + gpointer data, + GClosureNotify notify, + GConnectFlags flags); +GLIB_AVAILABLE_IN_2_70 +void g_signal_group_connect (GSignalGroup *self, + const gchar *detailed_signal, + GCallback c_handler, + gpointer data); +GLIB_AVAILABLE_IN_2_70 +void g_signal_group_connect_after (GSignalGroup *self, + const gchar *detailed_signal, + GCallback c_handler, + gpointer data); +GLIB_AVAILABLE_IN_2_70 +void g_signal_group_connect_swapped (GSignalGroup *self, + const gchar *detailed_signal, + GCallback c_handler, + gpointer data); + +G_END_DECLS + +#endif /* __G_SIGNAL_GROUP_H__ */ diff --git a/gobject/meson.build b/gobject/meson.build index 40443b54f..721036ca7 100644 --- a/gobject/meson.build +++ b/gobject/meson.build @@ -11,6 +11,7 @@ gobject_install_headers = files( 'gparam.h', 'gparamspecs.h', 'gsignal.h', + 'gsignalgroup.h', 'gsourceclosure.h', 'gtype.h', 'gtypemodule.h', @@ -35,6 +36,7 @@ gobject_sources = files( 'gparam.c', 'gparamspecs.c', 'gsignal.c', + 'gsignalgroup.c', 'gsourceclosure.c', 'gtype.c', 'gtypemodule.c', diff --git a/gobject/tests/meson.build b/gobject/tests/meson.build index 867a9b5f0..47dda1ea7 100644 --- a/gobject/tests/meson.build +++ b/gobject/tests/meson.build @@ -51,6 +51,7 @@ gobject_tests = { 'signals' : { 'source' : ['signals.c', marshalers_h, marshalers_c], }, + 'signalgroup' : {}, 'testing' : {}, 'type-flags' : {}, } diff --git a/gobject/tests/signalgroup.c b/gobject/tests/signalgroup.c new file mode 100644 index 000000000..0e1cf92b0 --- /dev/null +++ b/gobject/tests/signalgroup.c @@ -0,0 +1,486 @@ +#include <glib-object.h> + +typedef struct _SignalTarget +{ + GObject parent_instance; +} SignalTarget; + +G_DECLARE_FINAL_TYPE (SignalTarget, signal_target, TEST, SIGNAL_TARGET, GObject) +G_DEFINE_TYPE (SignalTarget, signal_target, G_TYPE_OBJECT) + +static +G_DEFINE_QUARK (detail, signal_detail) + +enum { + THE_SIGNAL, + NEVER_EMITTED, + LAST_SIGNAL +}; + +static guint signals [LAST_SIGNAL]; + +static void +signal_target_class_init (SignalTargetClass *klass) +{ + signals [THE_SIGNAL] = + g_signal_new ("the-signal", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST | G_SIGNAL_DETAILED, + 0, + NULL, NULL, NULL, + G_TYPE_NONE, + 1, + G_TYPE_OBJECT); + + signals [NEVER_EMITTED] = + g_signal_new ("never-emitted", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + 0, + NULL, NULL, NULL, + G_TYPE_NONE, + 1, + G_TYPE_OBJECT); +} + +static void +signal_target_init (SignalTarget *self) +{ +} + +static gint global_signal_calls; +static gint global_weak_notify_called; + +static void +connect_before_cb (SignalTarget *target, + GSignalGroup *group, + gint *signal_calls) +{ + g_assert (TEST_IS_SIGNAL_TARGET (target)); + g_assert (G_IS_SIGNAL_GROUP (group)); + g_assert (g_signal_group_get_target (group) == (GObject *)target); + g_assert (signal_calls != NULL); + g_assert (signal_calls == &global_signal_calls); + + *signal_calls += 1; +} + +static void +connect_after_cb (SignalTarget *target, + GSignalGroup *group, + gint *signal_calls) +{ + g_assert (TEST_IS_SIGNAL_TARGET (target)); + g_assert (G_IS_SIGNAL_GROUP (group)); + g_assert (g_signal_group_get_target (group) == (GObject *)target); + g_assert (signal_calls != NULL); + g_assert (signal_calls == &global_signal_calls); + + g_assert_cmpint (*signal_calls, ==, 4); + *signal_calls += 1; +} + +static void +connect_swapped_cb (gint *signal_calls, + GSignalGroup *group, + SignalTarget *target) +{ + g_assert (signal_calls != NULL); + g_assert (signal_calls == &global_signal_calls); + g_assert (G_IS_SIGNAL_GROUP (group)); + g_assert (TEST_IS_SIGNAL_TARGET (target)); + g_assert (g_signal_group_get_target (group) == (GObject *)target); + + *signal_calls += 1; +} + +static void +connect_object_cb (SignalTarget *target, + GSignalGroup *group, + GObject *object) +{ + gint *signal_calls; + + g_assert (TEST_IS_SIGNAL_TARGET (target)); + g_assert (G_IS_SIGNAL_GROUP (group)); + g_assert (g_signal_group_get_target (group) == (GObject *)target); + g_assert (G_IS_OBJECT (object)); + + signal_calls = g_object_get_data (object, "signal-calls"); + g_assert (signal_calls != NULL); + g_assert (signal_calls == &global_signal_calls); + + *signal_calls += 1; +} + +static void +connect_bad_detail_cb (SignalTarget *target, + GSignalGroup *group, + GObject *object) +{ + g_error ("This detailed signal is never emitted!"); +} + +static void +connect_never_emitted_cb (SignalTarget *target, + gboolean *weak_notify_called) +{ + g_error ("This signal is never emitted!"); +} + +static void +connect_data_notify_cb (gboolean *weak_notify_called, + GClosure *closure) +{ + g_assert (weak_notify_called != NULL); + g_assert (weak_notify_called == &global_weak_notify_called); + g_assert (closure != NULL); + + g_assert_false (*weak_notify_called); + *weak_notify_called = TRUE; +} + +static void +connect_data_weak_notify_cb (gboolean *weak_notify_called, + GSignalGroup *group) +{ + g_assert (weak_notify_called != NULL); + g_assert (weak_notify_called == &global_weak_notify_called); + g_assert (G_IS_SIGNAL_GROUP (group)); + + g_assert_true (*weak_notify_called); +} + +static void +connect_all_signals (GSignalGroup *group) +{ + GObject *object; + + /* Check that these are called in the right order */ + g_signal_group_connect (group, + "the-signal", + G_CALLBACK (connect_before_cb), + &global_signal_calls); + g_signal_group_connect_after (group, + "the-signal", + G_CALLBACK (connect_after_cb), + &global_signal_calls); + + /* Check that this is called with the arguments swapped */ + g_signal_group_connect_swapped (group, + "the-signal", + G_CALLBACK (connect_swapped_cb), + &global_signal_calls); + + /* Check that this is called with the arguments swapped */ + object = g_object_new (G_TYPE_OBJECT, NULL); + g_object_set_data (object, "signal-calls", &global_signal_calls); + g_signal_group_connect_object (group, + "the-signal", + G_CALLBACK (connect_object_cb), + object, + 0); + g_object_weak_ref (G_OBJECT (group), + (GWeakNotify)g_object_unref, + object); + + /* Check that a detailed signal is handled correctly */ + g_signal_group_connect (group, + "the-signal::detail", + G_CALLBACK (connect_before_cb), + &global_signal_calls); + g_signal_group_connect (group, + "the-signal::bad-detail", + G_CALLBACK (connect_bad_detail_cb), + NULL); + + /* Check that the notify is called correctly */ + global_weak_notify_called = FALSE; + g_signal_group_connect_data (group, + "never-emitted", + G_CALLBACK (connect_never_emitted_cb), + &global_weak_notify_called, + (GClosureNotify)connect_data_notify_cb, + 0); + g_object_weak_ref (G_OBJECT (group), + (GWeakNotify)connect_data_weak_notify_cb, + &global_weak_notify_called); +} + +static void +assert_signals (SignalTarget *target, + GSignalGroup *group, + gboolean success) +{ + g_assert (TEST_IS_SIGNAL_TARGET (target)); + g_assert (group == NULL || G_IS_SIGNAL_GROUP (group)); + + global_signal_calls = 0; + g_signal_emit (target, signals [THE_SIGNAL], + signal_detail_quark (), group); + g_assert_cmpint (global_signal_calls, ==, success ? 5 : 0); +} + +static void +test_signal_group_invalid (void) +{ + GObject *invalid_target = g_object_new (G_TYPE_OBJECT, NULL); + SignalTarget *target = g_object_new (signal_target_get_type (), NULL); + GSignalGroup *group = g_signal_group_new (signal_target_get_type ()); + + /* Invalid Target Type */ + g_test_expect_message (G_LOG_DOMAIN, G_LOG_LEVEL_CRITICAL, + "*g_type_is_a*G_TYPE_OBJECT*"); + g_signal_group_new (G_TYPE_DATE_TIME); + g_test_assert_expected_messages (); + + /* Invalid Target */ +#if 0 + g_test_expect_message (G_LOG_DOMAIN, G_LOG_LEVEL_CRITICAL, + "*Failed to set GSignalGroup of target type SignalTarget using target * of type GObject*"); + g_signal_group_set_target (group, invalid_target); + g_test_assert_expected_messages (); +#endif + + /* Invalid Signal Name */ + g_test_expect_message (G_LOG_DOMAIN, G_LOG_LEVEL_CRITICAL, + "*g_signal_parse_name*"); + g_signal_group_connect (group, + "does-not-exist", + G_CALLBACK (connect_before_cb), + NULL); + g_test_assert_expected_messages (); + + /* Invalid Callback */ + g_test_expect_message (G_LOG_DOMAIN, G_LOG_LEVEL_CRITICAL, + "*callback != NULL*"); + g_signal_group_connect (group, + "the-signal", + G_CALLBACK (NULL), + NULL); + g_test_assert_expected_messages (); + + g_object_unref (group); + g_object_unref (target); + g_object_unref (invalid_target); +} + +static void +test_signal_group_simple (void) +{ + SignalTarget *target = g_object_new (signal_target_get_type (), NULL); + GSignalGroup *group = g_signal_group_new (signal_target_get_type ()); + + /* Set the target before connecting the signals */ + g_assert_null (g_signal_group_get_target (group)); + g_signal_group_set_target (group, target); + g_assert (g_signal_group_get_target (group) == (GObject *)target); + + connect_all_signals (group); + assert_signals (target, group, TRUE); + + /* Destroying the SignalGroup should disconnect the signals */ + g_object_unref (group); + assert_signals (target, NULL, FALSE); + + g_object_unref (target); +} + +static void +test_signal_group_changing_target (void) +{ + SignalTarget *target1, *target2; + GSignalGroup *group = g_signal_group_new (signal_target_get_type ()); + + connect_all_signals (group); + g_assert_null (g_signal_group_get_target (group)); + + /* Set the target after connecting the signals */ + target1 = g_object_new (signal_target_get_type (), NULL); + g_signal_group_set_target (group, target1); + g_assert (g_signal_group_get_target (group) == (GObject *)target1); + + assert_signals (target1, group, TRUE); + + /* Set the same target */ + g_assert (g_signal_group_get_target (group) == (GObject *)target1); + g_signal_group_set_target (group, target1); + g_assert (g_signal_group_get_target (group) == (GObject *)target1); + + assert_signals (target1, group, TRUE); + + /* Set a new target when the current target is non-NULL */ + target2 = g_object_new (signal_target_get_type (), NULL); + g_assert (g_signal_group_get_target (group) == (GObject *)target1); + g_signal_group_set_target (group, target2); + g_assert (g_signal_group_get_target (group) == (GObject *)target2); + + assert_signals (target2, group, TRUE); + + g_object_unref (target2); + g_object_unref (target1); + g_object_unref (group); +} + +static void +assert_blocking (SignalTarget *target, + GSignalGroup *group, + gint count) +{ + gint i; + + assert_signals (target, group, TRUE); + + /* Assert that multiple blocks are effective */ + for (i = 0; i < count; ++i) + { + g_signal_group_block (group); + assert_signals (target, group, FALSE); + } + + /* Assert that the signal is not emitted after the first unblock */ + for (i = 0; i < count; ++i) + { + assert_signals (target, group, FALSE); + g_signal_group_unblock (group); + } + + assert_signals (target, group, TRUE); +} + +static void +test_signal_group_blocking (void) +{ + SignalTarget *target1, *target2; + GSignalGroup *group = g_signal_group_new (signal_target_get_type ()); + + connect_all_signals (group); + g_assert_null (g_signal_group_get_target (group)); + + target1 = g_object_new (signal_target_get_type (), NULL); + g_signal_group_set_target (group, target1); + g_assert (g_signal_group_get_target (group) == (GObject *)target1); + + assert_blocking (target1, group, 1); + assert_blocking (target1, group, 3); + assert_blocking (target1, group, 15); + + /* Assert that blocking transfers across changing the target */ + g_signal_group_block (group); + g_signal_group_block (group); + + /* Set a new target when the current target is non-NULL */ + target2 = g_object_new (signal_target_get_type (), NULL); + g_assert (g_signal_group_get_target (group) == (GObject *)target1); + g_signal_group_set_target (group, target2); + g_assert (g_signal_group_get_target (group) == (GObject *)target2); + + assert_signals (target2, group, FALSE); + g_signal_group_unblock (group); + assert_signals (target2, group, FALSE); + g_signal_group_unblock (group); + assert_signals (target2, group, TRUE); + + g_object_unref (target2); + g_object_unref (target1); + g_object_unref (group); +} + +static void +test_signal_group_weak_ref_target (void) +{ + SignalTarget *target = g_object_new (signal_target_get_type (), NULL); + GSignalGroup *group = g_signal_group_new (signal_target_get_type ()); + + g_assert_null (g_signal_group_get_target (group)); + g_signal_group_set_target (group, target); + g_assert (g_signal_group_get_target (group) == (GObject *)target); + + g_object_add_weak_pointer (G_OBJECT (target), (gpointer)&target); + g_object_unref (target); + g_assert_null (target); + g_assert_null (g_signal_group_get_target (group)); + + g_object_unref (group); +} + +static void +test_signal_group_connect_object (void) +{ + GObject *object = g_object_new (G_TYPE_OBJECT, NULL); + SignalTarget *target = g_object_new (signal_target_get_type (), NULL); + GSignalGroup *group = g_signal_group_new (signal_target_get_type ()); + + /* We already do basic connect_object() tests in connect_signals(), + * this is only needed to test the specifics of connect_object() + */ + g_signal_group_connect_object (group, + "the-signal", + G_CALLBACK (connect_object_cb), + object, + 0); + + g_assert_null (g_signal_group_get_target (group)); + g_signal_group_set_target (group, target); + g_assert (g_signal_group_get_target (group) == (GObject *)target); + + g_object_add_weak_pointer (G_OBJECT (object), (gpointer)&object); + g_object_unref (object); + g_assert_null (object); + + /* This would cause a warning if the SignalGroup did not + * have a weakref on the object as it would try to connect again + */ + g_signal_group_set_target (group, NULL); + g_assert (g_signal_group_get_target (group) == (GObject *)NULL); + g_signal_group_set_target (group, target); + g_assert (g_signal_group_get_target (group) == (GObject *)target); + + g_object_unref (group); + g_object_unref (target); +} + +static void +test_signal_group_signal_parsing (void) +{ + g_test_trap_subprocess ("/GObject/SignalGroup/signal-parsing/subprocess", 0, + G_TEST_SUBPROCESS_INHERIT_STDERR); + g_test_trap_assert_passed (); + g_test_trap_assert_stderr (""); +} + +static void +test_signal_group_signal_parsing_subprocess (void) +{ + GSignalGroup *group; + + /* Check that the class has not been created and with it the + * signals registered. This will cause g_signal_parse_name() + * to fail unless GSignalGroup calls g_type_class_ref(). + */ + g_assert_null (g_type_class_peek (signal_target_get_type ())); + + group = g_signal_group_new (signal_target_get_type ()); + g_signal_group_connect (group, + "the-signal", + G_CALLBACK (connect_before_cb), + NULL); + + g_object_unref (group); +} + +gint +main (gint argc, + gchar *argv[]) +{ + g_test_init (&argc, &argv, NULL); + g_test_add_func ("/GObject/SignalGroup/invalid", test_signal_group_invalid); + g_test_add_func ("/GObject/SignalGroup/simple", test_signal_group_simple); + g_test_add_func ("/GObject/SignalGroup/changing-target", test_signal_group_changing_target); + g_test_add_func ("/GObject/SignalGroup/blocking", test_signal_group_blocking); + g_test_add_func ("/GObject/SignalGroup/weak-ref-target", test_signal_group_weak_ref_target); + g_test_add_func ("/GObject/SignalGroup/connect-object", test_signal_group_connect_object); + g_test_add_func ("/GObject/SignalGroup/signal-parsing", test_signal_group_signal_parsing); + g_test_add_func ("/GObject/SignalGroup/signal-parsing/subprocess", test_signal_group_signal_parsing_subprocess); + return g_test_run (); +} |