From 0d9de09192a1984865154d5c6f39578a956b27c4 Mon Sep 17 00:00:00 2001 From: Christian Hergert Date: Thu, 26 Aug 2021 16:32:32 -0700 Subject: [PATCH 1/2] gobject: add GBindingGroup Originally, GBindingGroup started with Builder as a way to simplify all of the third-degree object bindings necessary around Model-Controller objects such as TextBuffer/TextView. Over time, it has grown to be useful in a number of scenarios outside of Builder and has been copied into a number of projects such as GNOME Text Editor, GtkSourceView, libdazzle, and more. It makes sense at this point to unify on a single implementation and include that upstream in GObject directly alongside GBinding. --- docs/reference/gobject/gobject-docs.xml | 1 + docs/reference/gobject/gobject-sections.txt | 18 + glib/glib-object.h | 1 + gobject/gbindinggroup.c | 679 +++++++++++++++++++ gobject/gbindinggroup.h | 85 +++ gobject/meson.build | 2 + gobject/tests/bindinggroup.c | 694 ++++++++++++++++++++ gobject/tests/meson.build | 1 + 8 files changed, 1481 insertions(+) create mode 100644 gobject/gbindinggroup.c create mode 100644 gobject/gbindinggroup.h create mode 100644 gobject/tests/bindinggroup.c diff --git a/docs/reference/gobject/gobject-docs.xml b/docs/reference/gobject/gobject-docs.xml index 3a83bd1a6..1718d9e23 100644 --- a/docs/reference/gobject/gobject-docs.xml +++ b/docs/reference/gobject/gobject-docs.xml @@ -84,6 +84,7 @@ + Tools Reference diff --git a/docs/reference/gobject/gobject-sections.txt b/docs/reference/gobject/gobject-sections.txt index 25ecd3aec..2450a367d 100644 --- a/docs/reference/gobject/gobject-sections.txt +++ b/docs/reference/gobject/gobject-sections.txt @@ -1006,3 +1006,21 @@ G_IS_BINDING g_binding_flags_get_type g_binding_get_type + +
+gbindinggroup +GBindingGroup +g_binding_group_new +g_binding_group_dup_source +g_binding_group_set_source +g_binding_group_bind +g_binding_group_bind_full +g_binding_group_bind_with_closures + +G_TYPE_BINDING_GROUP +G_TYPE_BINDING_GROUP_CLASS +G_BINDING_GROUP +G_IS_BINDING_GROUP + +g_binding_group_get_type +
diff --git a/glib/glib-object.h b/glib/glib-object.h index fa824f3bb..d259a3d39 100644 --- a/glib/glib-object.h +++ b/glib/glib-object.h @@ -20,6 +20,7 @@ #define __GLIB_GOBJECT_H_INSIDE__ #include +#include #include #include #include diff --git a/gobject/gbindinggroup.c b/gobject/gbindinggroup.c new file mode 100644 index 000000000..e5c8980bf --- /dev/null +++ b/gobject/gbindinggroup.c @@ -0,0 +1,679 @@ +/* GObject - GLib Type, Object, Parameter and Signal Library + * + * Copyright (C) 2015-2022 Christian Hergert + * Copyright (C) 2015 Garrett Regier + * + * This library 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 library 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 Lesser General + * Public License along with this library; if not, see . + * + * SPDX-License-Identifier: LGPL-2.1-or-later + */ + +#include "config.h" +#include "glib.h" +#include "glibintl.h" + +#include "gbindinggroup.h" +#include "gparamspecs.h" + +/** + * SECTION:gbindinggroup + * @Title: GBindingGroup + * @Short_description: Binding multiple properties as a group + * @include: glib-object.h + * + * The #GBindingGroup can be used to bind multiple properties + * from an object collectively. + * + * Use the various methods to bind properties from a single source + * object to multiple destination objects. Properties can be bound + * bidirectionally and are connected when the source object is set + * with g_binding_group_set_source(). + * + * Since: 2.72 + */ + +#if 0 +# define DEBUG_BINDINGS +#endif + +struct _GBindingGroup +{ + GObject parent_instance; + GMutex mutex; + GObject *source; /* (owned weak) */ + GPtrArray *lazy_bindings; /* (owned) (element-type LazyBinding) */ +}; + +typedef struct _GBindingGroupClass +{ + GObjectClass parent_class; +} GBindingGroupClass; + +typedef struct +{ + GBindingGroup *group; /* (unowned) */ + const char *source_property; /* (interned) */ + const char *target_property; /* (interned) */ + GObject *target; /* (owned weak) */ + GBinding *binding; /* (unowned) */ + gpointer user_data; + GDestroyNotify user_data_destroy; + gpointer transform_to; /* (nullable) (owned) */ + gpointer transform_from; /* (nullable) (owned) */ + GBindingFlags binding_flags; + guint using_closures : 1; +} LazyBinding; + +G_DEFINE_TYPE (GBindingGroup, g_binding_group, G_TYPE_OBJECT) + +typedef enum +{ + PROP_SOURCE = 1, + N_PROPS +} GBindingGroupProperty; + +static void lazy_binding_free (gpointer data); + +static GParamSpec *properties[N_PROPS]; + +static void +g_binding_group_connect (GBindingGroup *self, + LazyBinding *lazy_binding) +{ + GBinding *binding; + + g_assert (G_IS_BINDING_GROUP (self)); + g_assert (self->source != NULL); + g_assert (lazy_binding != NULL); + g_assert (lazy_binding->binding == NULL); + g_assert (lazy_binding->target != NULL); + g_assert (lazy_binding->target_property != NULL); + g_assert (lazy_binding->source_property != NULL); + +#ifdef DEBUG_BINDINGS + { + GFlagsClass *flags_class; + g_autofree gchar *flags_str = NULL; + + flags_class = g_type_class_ref (G_TYPE_BINDING_FLAGS); + flags_str = g_flags_to_string (flags_class, lazy_binding->binding_flags); + + g_print ("Binding %s(%p):%s to %s(%p):%s (flags=%s)\n", + G_OBJECT_TYPE_NAME (self->source), + self->source, + lazy_binding->source_property, + G_OBJECT_TYPE_NAME (lazy_binding->target), + lazy_binding->target, + lazy_binding->target_property, + flags_str); + + g_type_class_unref (flags_class); + } +#endif + + if (!lazy_binding->using_closures) + binding = g_object_bind_property_full (self->source, + lazy_binding->source_property, + lazy_binding->target, + lazy_binding->target_property, + lazy_binding->binding_flags, + lazy_binding->transform_to, + lazy_binding->transform_from, + lazy_binding->user_data, + NULL); + else + binding = g_object_bind_property_with_closures (self->source, + lazy_binding->source_property, + lazy_binding->target, + lazy_binding->target_property, + lazy_binding->binding_flags, + lazy_binding->transform_to, + lazy_binding->transform_from); + + lazy_binding->binding = binding; +} + +static void +g_binding_group_disconnect (LazyBinding *lazy_binding) +{ + g_assert (lazy_binding != NULL); + + if (lazy_binding->binding != NULL) + { + g_binding_unbind (lazy_binding->binding); + lazy_binding->binding = NULL; + } +} + +static void +g_binding_group__source_weak_notify (gpointer data, + GObject *where_object_was) +{ + GBindingGroup *self = data; + guint i; + + g_assert (G_IS_BINDING_GROUP (self)); + + g_mutex_lock (&self->mutex); + + self->source = NULL; + + for (i = 0; i < self->lazy_bindings->len; i++) + { + LazyBinding *lazy_binding = g_ptr_array_index (self->lazy_bindings, i); + + lazy_binding->binding = NULL; + } + + g_mutex_unlock (&self->mutex); +} + +static void +g_binding_group__target_weak_notify (gpointer data, + GObject *where_object_was) +{ + GBindingGroup *self = data; + LazyBinding *to_free = NULL; + guint i; + + g_assert (G_IS_BINDING_GROUP (self)); + + g_mutex_lock (&self->mutex); + + for (i = 0; i < self->lazy_bindings->len; i++) + { + LazyBinding *lazy_binding = g_ptr_array_index (self->lazy_bindings, i); + + if (lazy_binding->target == where_object_was) + { + lazy_binding->target = NULL; + lazy_binding->binding = NULL; + + to_free = g_ptr_array_steal_index_fast (self->lazy_bindings, i); + break; + } + } + + g_mutex_unlock (&self->mutex); + + if (to_free != NULL) + lazy_binding_free (to_free); +} + +static void +lazy_binding_free (gpointer data) +{ + LazyBinding *lazy_binding = data; + + if (lazy_binding->target != NULL) + { + g_object_weak_unref (lazy_binding->target, + g_binding_group__target_weak_notify, + lazy_binding->group); + lazy_binding->target = NULL; + } + + g_binding_group_disconnect (lazy_binding); + + lazy_binding->group = NULL; + lazy_binding->source_property = NULL; + lazy_binding->target_property = NULL; + + if (lazy_binding->user_data_destroy) + lazy_binding->user_data_destroy (lazy_binding->user_data); + + if (lazy_binding->using_closures) + { + g_clear_pointer (&lazy_binding->transform_to, g_closure_unref); + g_clear_pointer (&lazy_binding->transform_from, g_closure_unref); + } + + g_slice_free (LazyBinding, lazy_binding); +} + +static void +g_binding_group_dispose (GObject *object) +{ + GBindingGroup *self = (GBindingGroup *)object; + LazyBinding **lazy_bindings = NULL; + gsize len = 0; + gsize i; + + g_assert (G_IS_BINDING_GROUP (self)); + + g_mutex_lock (&self->mutex); + + if (self->source != NULL) + { + g_object_weak_unref (self->source, + g_binding_group__source_weak_notify, + self); + self->source = NULL; + } + + if (self->lazy_bindings->len > 0) + lazy_bindings = (LazyBinding **)g_ptr_array_steal (self->lazy_bindings, &len); + + g_mutex_unlock (&self->mutex); + + /* Free bindings without holding self->mutex to avoid re-entrancy + * from collateral damage through release of binding closure data, + * GDataList, etc. + */ + for (i = 0; i < len; i++) + lazy_binding_free (lazy_bindings[i]); + g_free (lazy_bindings); + + G_OBJECT_CLASS (g_binding_group_parent_class)->dispose (object); +} + +static void +g_binding_group_finalize (GObject *object) +{ + GBindingGroup *self = (GBindingGroup *)object; + + g_assert (self->lazy_bindings != NULL); + g_assert (self->lazy_bindings->len == 0); + + g_clear_pointer (&self->lazy_bindings, g_ptr_array_unref); + g_mutex_clear (&self->mutex); + + G_OBJECT_CLASS (g_binding_group_parent_class)->finalize (object); +} + +static void +g_binding_group_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + GBindingGroup *self = G_BINDING_GROUP (object); + + switch ((GBindingGroupProperty) prop_id) + { + case PROP_SOURCE: + g_value_take_object (value, g_binding_group_dup_source (self)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +g_binding_group_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + GBindingGroup *self = G_BINDING_GROUP (object); + + switch ((GBindingGroupProperty) prop_id) + { + case PROP_SOURCE: + g_binding_group_set_source (self, g_value_get_object (value)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +g_binding_group_class_init (GBindingGroupClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->dispose = g_binding_group_dispose; + object_class->finalize = g_binding_group_finalize; + object_class->get_property = g_binding_group_get_property; + object_class->set_property = g_binding_group_set_property; + + /** + * GBindingGroup:source: (nullable) + * + * The source object used for binding properties. + * + * Since: 2.72 + */ + properties[PROP_SOURCE] = + g_param_spec_object ("source", + "Source", + "The source GObject used for binding properties.", + G_TYPE_OBJECT, + (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS)); + + g_object_class_install_properties (object_class, N_PROPS, properties); +} + +static void +g_binding_group_init (GBindingGroup *self) +{ + g_mutex_init (&self->mutex); + self->lazy_bindings = g_ptr_array_new_with_free_func (lazy_binding_free); +} + +/** + * g_binding_group_new: + * + * Creates a new #GBindingGroup. + * + * Returns: (transfer full): a new #GBindingGroup + * + * Since: 2.72 + */ +GBindingGroup * +g_binding_group_new (void) +{ + return g_object_new (G_TYPE_BINDING_GROUP, NULL); +} + +/** + * g_binding_group_dup_source: + * @self: the #GBindingGroup + * + * Gets the source object used for binding properties. + * + * Returns: (transfer none) (nullable) (type GObject): a #GObject or %NULL. + * + * Since: 2.72 + */ +gpointer +g_binding_group_dup_source (GBindingGroup *self) +{ + GObject *source; + + g_return_val_if_fail (G_IS_BINDING_GROUP (self), NULL); + + g_mutex_lock (&self->mutex); + source = self->source ? g_object_ref (self->source) : NULL; + g_mutex_unlock (&self->mutex); + + return source; +} + +static gboolean +g_binding_group_check_source (GBindingGroup *self, + gpointer source) +{ + guint i; + + g_assert (G_IS_BINDING_GROUP (self)); + g_assert (!source || G_IS_OBJECT (source)); + + for (i = 0; i < self->lazy_bindings->len; i++) + { + LazyBinding *lazy_binding = g_ptr_array_index (self->lazy_bindings, i); + + g_return_val_if_fail (g_object_class_find_property (G_OBJECT_GET_CLASS (source), + lazy_binding->source_property) != NULL, + FALSE); + } + + return TRUE; +} + +/** + * g_binding_group_set_source: + * @self: the #GBindingGroup + * @source: (type GObject) (nullable) (transfer none): the source #GObject, + * or %NULL to clear it + * + * Sets @source as the source object used for creating property + * bindings. If there is already a source object all bindings from it + * will be removed. + * + * Note that all properties that have been bound must exist on @source. + * + * Since: 2.72 + */ +void +g_binding_group_set_source (GBindingGroup *self, + gpointer source) +{ + gboolean notify = FALSE; + + g_return_if_fail (G_IS_BINDING_GROUP (self)); + g_return_if_fail (!source || G_IS_OBJECT (source)); + g_return_if_fail (source != (gpointer) self); + + g_mutex_lock (&self->mutex); + + if (source == (gpointer) self->source) + goto unlock; + + if (self->source != NULL) + { + guint i; + + g_object_weak_unref (self->source, + g_binding_group__source_weak_notify, + self); + self->source = NULL; + + for (i = 0; i < self->lazy_bindings->len; i++) + { + LazyBinding *lazy_binding = g_ptr_array_index (self->lazy_bindings, i); + + g_binding_group_disconnect (lazy_binding); + } + } + + if (source != NULL && g_binding_group_check_source (self, source)) + { + guint i; + + self->source = source; + g_object_weak_ref (self->source, + g_binding_group__source_weak_notify, + self); + + for (i = 0; i < self->lazy_bindings->len; i++) + { + LazyBinding *lazy_binding; + + lazy_binding = g_ptr_array_index (self->lazy_bindings, i); + g_binding_group_connect (self, lazy_binding); + } + } + + notify = TRUE; + +unlock: + g_mutex_unlock (&self->mutex); + + if (notify) + g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_SOURCE]); +} + +static void +g_binding_group_bind_helper (GBindingGroup *self, + const gchar *source_property, + gpointer target, + const gchar *target_property, + GBindingFlags flags, + gpointer transform_to, + gpointer transform_from, + gpointer user_data, + GDestroyNotify user_data_destroy, + gboolean using_closures) +{ + LazyBinding *lazy_binding; + + g_return_if_fail (G_IS_BINDING_GROUP (self)); + g_return_if_fail (source_property != NULL); + g_return_if_fail (self->source == NULL || + g_object_class_find_property (G_OBJECT_GET_CLASS (self->source), + source_property) != NULL); + g_return_if_fail (G_IS_OBJECT (target)); + g_return_if_fail (target_property != NULL); + g_return_if_fail (g_object_class_find_property (G_OBJECT_GET_CLASS (target), + target_property) != NULL); + g_return_if_fail (target != (gpointer) self || + strcmp (source_property, target_property) != 0); + + g_mutex_lock (&self->mutex); + + lazy_binding = g_slice_new0 (LazyBinding); + lazy_binding->group = self; + lazy_binding->source_property = g_intern_string (source_property); + lazy_binding->target_property = g_intern_string (target_property); + lazy_binding->target = target; + lazy_binding->binding_flags = flags | G_BINDING_SYNC_CREATE; + lazy_binding->user_data = user_data; + lazy_binding->user_data_destroy = user_data_destroy; + lazy_binding->transform_to = transform_to; + lazy_binding->transform_from = transform_from; + + if (using_closures) + { + lazy_binding->using_closures = TRUE; + + if (transform_to != NULL) + g_closure_sink (g_closure_ref (transform_to)); + + if (transform_from != NULL) + g_closure_sink (g_closure_ref (transform_from)); + } + + g_object_weak_ref (target, + g_binding_group__target_weak_notify, + self); + + g_ptr_array_add (self->lazy_bindings, lazy_binding); + + if (self->source != NULL) + g_binding_group_connect (self, lazy_binding); + + g_mutex_unlock (&self->mutex); +} + +/** + * g_binding_group_bind: + * @self: the #GBindingGroup + * @source_property: the property on the source to bind + * @target: (type GObject) (transfer none) (not nullable): the target #GObject + * @target_property: the property on @target to bind + * @flags: the flags used to create the #GBinding + * + * Creates a binding between @source_property on the source object + * and @target_property on @target. Whenever the @source_property + * is changed the @target_property is updated using the same value. + * The binding flag %G_BINDING_SYNC_CREATE is automatically specified. + * + * See g_object_bind_property() for more information. + * + * Since: 2.72 + */ +void +g_binding_group_bind (GBindingGroup *self, + const gchar *source_property, + gpointer target, + const gchar *target_property, + GBindingFlags flags) +{ + g_binding_group_bind_full (self, source_property, + target, target_property, + flags, + NULL, NULL, + NULL, NULL); +} + +/** + * g_binding_group_bind_full: + * @self: the #GBindingGroup + * @source_property: the property on the source to bind + * @target: (type GObject) (transfer none) (not nullable): the target #GObject + * @target_property: the property on @target to bind + * @flags: the flags used to create the #GBinding + * @transform_to: (scope notified) (nullable): the transformation function + * from the source object to the @target, or %NULL to use the default + * @transform_from: (scope notified) (nullable): the transformation function + * from the @target to the source object, or %NULL to use the default + * @user_data: custom data to be passed to the transformation + * functions, or %NULL + * @user_data_destroy: function to be called when disposing the binding, + * to free the resources used by the transformation functions + * + * Creates a binding between @source_property on the source object and + * @target_property on @target, allowing you to set the transformation + * functions to be used by the binding. The binding flag + * %G_BINDING_SYNC_CREATE is automatically specified. + * + * See g_object_bind_property_full() for more information. + * + * Since: 2.72 + */ +void +g_binding_group_bind_full (GBindingGroup *self, + const gchar *source_property, + gpointer target, + const gchar *target_property, + GBindingFlags flags, + GBindingTransformFunc transform_to, + GBindingTransformFunc transform_from, + gpointer user_data, + GDestroyNotify user_data_destroy) +{ + g_binding_group_bind_helper (self, source_property, + target, target_property, + flags, + transform_to, transform_from, + user_data, user_data_destroy, + FALSE); +} + +/** + * g_binding_group_bind_with_closures: (rename-to g_binding_group_bind_full) + * @self: the #GBindingGroup + * @source_property: the property on the source to bind + * @target: (type GObject) (transfer none) (not nullable): the target #GObject + * @target_property: the property on @target to bind + * @flags: the flags used to create the #GBinding + * @transform_to: (nullable) (transfer none): a #GClosure wrapping the + * transformation function from the source object to the @target, + * or %NULL to use the default + * @transform_from: (nullable) (transfer none): a #GClosure wrapping the + * transformation function from the @target to the source object, + * or %NULL to use the default + * + * Creates a binding between @source_property on the source object and + * @target_property on @target, allowing you to set the transformation + * functions to be used by the binding. The binding flag + * %G_BINDING_SYNC_CREATE is automatically specified. + * + * This function is the language bindings friendly version of + * g_binding_group_bind_property_full(), using #GClosures + * instead of function pointers. + * + * See g_object_bind_property_with_closures() for more information. + * + * Since: 2.72 + */ +void +g_binding_group_bind_with_closures (GBindingGroup *self, + const gchar *source_property, + gpointer target, + const gchar *target_property, + GBindingFlags flags, + GClosure *transform_to, + GClosure *transform_from) +{ + g_binding_group_bind_helper (self, source_property, + target, target_property, + flags, + transform_to, transform_from, + NULL, NULL, + TRUE); +} diff --git a/gobject/gbindinggroup.h b/gobject/gbindinggroup.h new file mode 100644 index 000000000..472ebc5b3 --- /dev/null +++ b/gobject/gbindinggroup.h @@ -0,0 +1,85 @@ +/* GObject - GLib Type, Object, Parameter and Signal Library + * + * Copyright (C) 2015-2022 Christian Hergert + * Copyright (C) 2015 Garrett Regier + * + * This library 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 library 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 Lesser General + * Public License along with this library; if not, see . + * + * SPDX-License-Identifier: LGPL-2.1-or-later + */ + +#ifndef __G_BINDING_GROUP_H__ +#define __G_BINDING_GROUP_H__ + +#if !defined (__GLIB_GOBJECT_H_INSIDE__) && !defined (GOBJECT_COMPILATION) +#error "Only can be included directly." +#endif + +#include +#include +#include + +G_BEGIN_DECLS + +#define G_BINDING_GROUP(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), G_TYPE_BINDING_GROUP, GBindingGroup)) +#define G_IS_BINDING_GROUP(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), G_TYPE_BINDING_GROUP)) +#define G_TYPE_BINDING_GROUP (g_binding_group_get_type()) + +/** + * GBindingGroup: + * + * GBindingGroup is an opaque structure whose members + * cannot be accessed directly. + * + * Since: 2.72 + */ +typedef struct _GBindingGroup GBindingGroup; + +GLIB_AVAILABLE_IN_2_72 +GType g_binding_group_get_type (void) G_GNUC_CONST; +GLIB_AVAILABLE_IN_2_72 +GBindingGroup *g_binding_group_new (void); +GLIB_AVAILABLE_IN_2_72 +gpointer g_binding_group_dup_source (GBindingGroup *self); +GLIB_AVAILABLE_IN_2_72 +void g_binding_group_set_source (GBindingGroup *self, + gpointer source); +GLIB_AVAILABLE_IN_2_72 +void g_binding_group_bind (GBindingGroup *self, + const gchar *source_property, + gpointer target, + const gchar *target_property, + GBindingFlags flags); +GLIB_AVAILABLE_IN_2_72 +void g_binding_group_bind_full (GBindingGroup *self, + const gchar *source_property, + gpointer target, + const gchar *target_property, + GBindingFlags flags, + GBindingTransformFunc transform_to, + GBindingTransformFunc transform_from, + gpointer user_data, + GDestroyNotify user_data_destroy); +GLIB_AVAILABLE_IN_2_72 +void g_binding_group_bind_with_closures (GBindingGroup *self, + const gchar *source_property, + gpointer target, + const gchar *target_property, + GBindingFlags flags, + GClosure *transform_to, + GClosure *transform_from); + +G_END_DECLS + +#endif /* __G_BINDING_GROUP_H__ */ diff --git a/gobject/meson.build b/gobject/meson.build index db2baecf1..48bd87601 100644 --- a/gobject/meson.build +++ b/gobject/meson.build @@ -2,6 +2,7 @@ gobject_install_headers = files( 'gobject-autocleanups.h', 'glib-types.h', 'gbinding.h', + 'gbindinggroup.h', 'gboxed.h', 'gclosure.h', 'genums.h', @@ -25,6 +26,7 @@ install_headers(gobject_install_headers, subdir : 'glib-2.0/gobject') gobject_sources = files( 'gatomicarray.c', 'gbinding.c', + 'gbindinggroup.c', 'gboxed.c', 'gclosure.c', 'genums.c', diff --git a/gobject/tests/bindinggroup.c b/gobject/tests/bindinggroup.c new file mode 100644 index 000000000..94bc9b968 --- /dev/null +++ b/gobject/tests/bindinggroup.c @@ -0,0 +1,694 @@ +/* GObject - GLib Type, Object, Parameter and Signal Library + * + * Copyright (C) 2015-2022 Christian Hergert + * Copyright (C) 2015 Garrett Regier + * + * This library 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 library 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 Lesser General + * Public License along with this library; if not, see . + * + * SPDX-License-Identifier: LGPL-2.1-or-later + */ + +#include + +/* Copied from glib */ +typedef struct _BindingSource +{ + GObject parent_instance; + + gint foo; + gint bar; + gdouble value; + gboolean toggle; +} BindingSource; + +typedef struct _BindingSourceClass +{ + GObjectClass parent_class; +} BindingSourceClass; + +enum +{ + PROP_SOURCE_FOO = 1, + PROP_SOURCE_BAR, + PROP_SOURCE_VALUE, + PROP_SOURCE_TOGGLE +}; + +static GType binding_source_get_type (void); +G_DEFINE_TYPE (BindingSource, binding_source, G_TYPE_OBJECT); + +static void +binding_source_set_property (GObject *gobject, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + BindingSource *source = (BindingSource *) gobject; + + switch (prop_id) + { + case PROP_SOURCE_FOO: + source->foo = g_value_get_int (value); + break; + + case PROP_SOURCE_BAR: + source->bar = g_value_get_int (value); + break; + + case PROP_SOURCE_VALUE: + source->value = g_value_get_double (value); + break; + + case PROP_SOURCE_TOGGLE: + source->toggle = g_value_get_boolean (value); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (gobject, prop_id, pspec); + } +} + +static void +binding_source_get_property (GObject *gobject, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + BindingSource *source = (BindingSource *) gobject; + + switch (prop_id) + { + case PROP_SOURCE_FOO: + g_value_set_int (value, source->foo); + break; + + case PROP_SOURCE_BAR: + g_value_set_int (value, source->bar); + break; + + case PROP_SOURCE_VALUE: + g_value_set_double (value, source->value); + break; + + case PROP_SOURCE_TOGGLE: + g_value_set_boolean (value, source->toggle); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (gobject, prop_id, pspec); + } +} + +static void +binding_source_class_init (BindingSourceClass *klass) +{ + GObjectClass *gobject_class = G_OBJECT_CLASS (klass); + + gobject_class->set_property = binding_source_set_property; + gobject_class->get_property = binding_source_get_property; + + g_object_class_install_property (gobject_class, PROP_SOURCE_FOO, + g_param_spec_int ("foo", "Foo", "Foo", + -1, 100, + 0, + G_PARAM_READWRITE)); + g_object_class_install_property (gobject_class, PROP_SOURCE_BAR, + g_param_spec_int ("bar", "Bar", "Bar", + -1, 100, + 0, + G_PARAM_READWRITE)); + g_object_class_install_property (gobject_class, PROP_SOURCE_VALUE, + g_param_spec_double ("value", "Value", "Value", + -100.0, 200.0, + 0.0, + G_PARAM_READWRITE)); + g_object_class_install_property (gobject_class, PROP_SOURCE_TOGGLE, + g_param_spec_boolean ("toggle", "Toggle", "Toggle", + FALSE, + G_PARAM_READWRITE)); +} + +static void +binding_source_init (BindingSource *self) +{ +} + +typedef struct _BindingTarget +{ + GObject parent_instance; + + gint bar; + gdouble value; + gboolean toggle; +} BindingTarget; + +typedef struct _BindingTargetClass +{ + GObjectClass parent_class; +} BindingTargetClass; + +enum +{ + PROP_TARGET_BAR = 1, + PROP_TARGET_VALUE, + PROP_TARGET_TOGGLE +}; + +static GType binding_target_get_type (void); +G_DEFINE_TYPE (BindingTarget, binding_target, G_TYPE_OBJECT); + +static void +binding_target_set_property (GObject *gobject, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + BindingTarget *target = (BindingTarget *) gobject; + + switch (prop_id) + { + case PROP_TARGET_BAR: + target->bar = g_value_get_int (value); + break; + + case PROP_TARGET_VALUE: + target->value = g_value_get_double (value); + break; + + case PROP_TARGET_TOGGLE: + target->toggle = g_value_get_boolean (value); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (gobject, prop_id, pspec); + } +} + +static void +binding_target_get_property (GObject *gobject, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + BindingTarget *target = (BindingTarget *) gobject; + + switch (prop_id) + { + case PROP_TARGET_BAR: + g_value_set_int (value, target->bar); + break; + + case PROP_TARGET_VALUE: + g_value_set_double (value, target->value); + break; + + case PROP_TARGET_TOGGLE: + g_value_set_boolean (value, target->toggle); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (gobject, prop_id, pspec); + } +} + +static void +binding_target_class_init (BindingTargetClass *klass) +{ + GObjectClass *gobject_class = G_OBJECT_CLASS (klass); + + gobject_class->set_property = binding_target_set_property; + gobject_class->get_property = binding_target_get_property; + + g_object_class_install_property (gobject_class, PROP_TARGET_BAR, + g_param_spec_int ("bar", "Bar", "Bar", + -1, 100, + 0, + G_PARAM_READWRITE)); + g_object_class_install_property (gobject_class, PROP_TARGET_VALUE, + g_param_spec_double ("value", "Value", "Value", + -100.0, 200.0, + 0.0, + G_PARAM_READWRITE)); + g_object_class_install_property (gobject_class, PROP_TARGET_TOGGLE, + g_param_spec_boolean ("toggle", "Toggle", "Toggle", + FALSE, + G_PARAM_READWRITE)); +} + +static void +binding_target_init (BindingTarget *self) +{ +} + +static gboolean +celsius_to_fahrenheit (GBinding *binding, + const GValue *from_value, + GValue *to_value, + gpointer user_data G_GNUC_UNUSED) +{ + gdouble celsius, fahrenheit; + + g_assert_true (G_VALUE_HOLDS (from_value, G_TYPE_DOUBLE)); + g_assert_true (G_VALUE_HOLDS (to_value, G_TYPE_DOUBLE)); + + celsius = g_value_get_double (from_value); + fahrenheit = (9 * celsius / 5) + 32.0; + + if (g_test_verbose ()) + g_printerr ("Converting %.2fC to %.2fF\n", celsius, fahrenheit); + + g_value_set_double (to_value, fahrenheit); + + return TRUE; +} + +static gboolean +fahrenheit_to_celsius (GBinding *binding, + const GValue *from_value, + GValue *to_value, + gpointer user_data G_GNUC_UNUSED) +{ + gdouble celsius, fahrenheit; + + g_assert_true (G_VALUE_HOLDS (from_value, G_TYPE_DOUBLE)); + g_assert_true (G_VALUE_HOLDS (to_value, G_TYPE_DOUBLE)); + + fahrenheit = g_value_get_double (from_value); + celsius = 5 * (fahrenheit - 32.0) / 9; + + if (g_test_verbose ()) + g_printerr ("Converting %.2fF to %.2fC\n", fahrenheit, celsius); + + g_value_set_double (to_value, celsius); + + return TRUE; +} + +static void +test_binding_group_invalid (void) +{ + GBindingGroup *group = g_binding_group_new (); + BindingSource *source = g_object_new (binding_source_get_type (), NULL); + BindingTarget *target = g_object_new (binding_target_get_type (), NULL); + + /* Invalid Target Property */ + g_test_expect_message (G_LOG_DOMAIN, G_LOG_LEVEL_CRITICAL, + "*find_property*target_property*!=*NULL*"); + g_binding_group_bind (group, "value", + target, "does-not-exist", + G_BINDING_DEFAULT); + g_test_assert_expected_messages (); + + g_binding_group_set_source (group, NULL); + + /* Invalid Source Property */ + g_test_expect_message (G_LOG_DOMAIN, G_LOG_LEVEL_CRITICAL, + "*find_property*source_property*!=*NULL*"); + g_binding_group_set_source (group, source); + g_binding_group_bind (group, "does-not-exist", + target, "value", + G_BINDING_DEFAULT); + g_test_assert_expected_messages (); + + g_binding_group_set_source (group, NULL); + + /* Invalid Source */ + g_test_expect_message (G_LOG_DOMAIN, G_LOG_LEVEL_CRITICAL, + "*find_property*->source_property*!=*NULL*"); + g_binding_group_bind (group, "does-not-exist", + target, "value", + G_BINDING_DEFAULT); + g_binding_group_set_source (group, source); + g_test_assert_expected_messages (); + + g_object_unref (target); + g_object_unref (source); + g_object_unref (group); +} + +static void +test_binding_group_default (void) +{ + gsize i, j; + GBindingGroup *group = g_binding_group_new (); + BindingSource *source = g_object_new (binding_source_get_type (), NULL); + BindingTarget *targets[5]; + BindingSource *readback; + + for (i = 0; i < G_N_ELEMENTS (targets); ++i) + { + targets[i] = g_object_new (binding_target_get_type (), NULL); + g_binding_group_bind (group, "foo", + targets[i], "bar", + G_BINDING_DEFAULT); + } + + g_assert_null (g_binding_group_dup_source (group)); + g_binding_group_set_source (group, source); + readback = g_binding_group_dup_source (group); + g_assert_true (readback == source); + g_object_unref (readback); + + for (i = 0; i < 2; ++i) + { + g_object_set (source, "foo", 42, NULL); + for (j = 0; j < G_N_ELEMENTS (targets); ++j) + g_assert_cmpint (source->foo, ==, targets[j]->bar); + + g_object_set (targets[0], "bar", 47, NULL); + g_assert_cmpint (source->foo, !=, targets[0]->bar); + + /* Check that we transition the source correctly */ + g_binding_group_set_source (group, NULL); + g_assert_null (g_binding_group_dup_source (group)); + g_binding_group_set_source (group, source); + readback = g_binding_group_dup_source (group); + g_assert_true (readback == source); + g_object_unref (readback); + } + + g_object_unref (group); + + g_object_set (source, "foo", 0, NULL); + for (i = 0; i < G_N_ELEMENTS (targets); ++i) + g_assert_cmpint (source->foo, !=, targets[i]->bar); + + g_object_unref (source); + for (i = 0; i < G_N_ELEMENTS (targets); ++i) + g_object_unref (targets[i]); +} + +static void +test_binding_group_bidirectional (void) +{ + gsize i, j; + GBindingGroup *group = g_binding_group_new (); + BindingSource *source = g_object_new (binding_source_get_type (), NULL); + BindingTarget *targets[5]; + BindingSource *readback; + + for (i = 0; i < G_N_ELEMENTS (targets); ++i) + { + targets[i] = g_object_new (binding_target_get_type (), NULL); + g_binding_group_bind (group, "value", + targets[i], "value", + G_BINDING_BIDIRECTIONAL); + } + + g_assert_null (g_binding_group_dup_source (group)); + g_binding_group_set_source (group, source); + readback = g_binding_group_dup_source (group); + g_assert_true (readback == source); + g_object_unref (readback); + + for (i = 0; i < 2; ++i) + { + g_object_set (source, "value", 42.0, NULL); + for (j = 0; j < G_N_ELEMENTS (targets); ++j) + g_assert_cmpfloat (source->value, ==, targets[j]->value); + + g_object_set (targets[0], "value", 47.0, NULL); + g_assert_cmpfloat (source->value, ==, targets[0]->value); + + /* Check that we transition the source correctly */ + g_binding_group_set_source (group, NULL); + g_assert_null (g_binding_group_dup_source (group)); + g_binding_group_set_source (group, source); + readback = g_binding_group_dup_source (group); + g_assert_true (readback == source); + g_object_unref (readback); + } + + g_object_unref (group); + + g_object_set (targets[0], "value", 0.0, NULL); + g_assert_cmpfloat (source->value, !=, targets[0]->value); + + g_object_unref (source); + for (i = 0; i < G_N_ELEMENTS (targets); ++i) + g_object_unref (targets[i]); +} + +static void +transform_destroy_notify (gpointer data) +{ + gboolean *transform_destroy_called = data; + + *transform_destroy_called = TRUE; +} + +static void +test_binding_group_transform (void) +{ + gboolean transform_destroy_called = FALSE; + GBindingGroup *group = g_binding_group_new (); + BindingSource *source = g_object_new (binding_source_get_type (), NULL); + BindingTarget *target = g_object_new (binding_target_get_type (), NULL); + + g_binding_group_set_source (group, source); + g_binding_group_bind_full (group, "value", + target, "value", + G_BINDING_BIDIRECTIONAL, + celsius_to_fahrenheit, + fahrenheit_to_celsius, + &transform_destroy_called, + transform_destroy_notify); + + g_object_set (source, "value", 24.0, NULL); + g_assert_cmpfloat (target->value, ==, ((9 * 24.0 / 5) + 32.0)); + + g_object_set (target, "value", 69.0, NULL); + g_assert_cmpfloat (source->value, ==, (5 * (69.0 - 32.0) / 9)); + + /* The GDestroyNotify should only be called when the + * set is freed, not when the various GBindings are freed + */ + g_binding_group_set_source (group, NULL); + g_assert_false (transform_destroy_called); + + g_object_unref (group); + g_assert_true (transform_destroy_called); + + g_object_unref (source); + g_object_unref (target); +} + +static void +test_binding_group_transform_closures (void) +{ + gboolean transform_destroy_called_1 = FALSE; + gboolean transform_destroy_called_2 = FALSE; + GBindingGroup *group = g_binding_group_new (); + BindingSource *source = g_object_new (binding_source_get_type (), NULL); + BindingTarget *target = g_object_new (binding_target_get_type (), NULL); + GClosure *c2f_closure, *f2c_closure; + + c2f_closure = g_cclosure_new (G_CALLBACK (celsius_to_fahrenheit), + &transform_destroy_called_1, + (GClosureNotify) transform_destroy_notify); + f2c_closure = g_cclosure_new (G_CALLBACK (fahrenheit_to_celsius), + &transform_destroy_called_2, + (GClosureNotify) transform_destroy_notify); + + g_binding_group_set_source (group, source); + g_binding_group_bind_with_closures (group, "value", + target, "value", + G_BINDING_BIDIRECTIONAL, + c2f_closure, + f2c_closure); + + g_object_set (source, "value", 24.0, NULL); + g_assert_cmpfloat (target->value, ==, ((9 * 24.0 / 5) + 32.0)); + + g_object_set (target, "value", 69.0, NULL); + g_assert_cmpfloat (source->value, ==, (5 * (69.0 - 32.0) / 9)); + + /* The GClsoureNotify should only be called when the + * set is freed, not when the various GBindings are freed + */ + g_binding_group_set_source (group, NULL); + g_assert_false (transform_destroy_called_1); + g_assert_false (transform_destroy_called_2); + + g_object_unref (group); + g_assert_true (transform_destroy_called_1); + g_assert_true (transform_destroy_called_2); + + g_object_unref (source); + g_object_unref (target); +} + +static void +test_binding_group_same_object (void) +{ + gsize i; + GBindingGroup *group = g_binding_group_new (); + BindingSource *source = g_object_new (binding_source_get_type (), + "foo", 100, + "bar", 50, + NULL); + + g_binding_group_set_source (group, source); + g_binding_group_bind (group, "foo", + source, "bar", + G_BINDING_BIDIRECTIONAL); + + for (i = 0; i < 2; ++i) + { + g_object_set (source, "foo", 10, NULL); + g_assert_cmpint (source->foo, ==, 10); + g_assert_cmpint (source->bar, ==, 10); + + g_object_set (source, "bar", 30, NULL); + g_assert_cmpint (source->foo, ==, 30); + g_assert_cmpint (source->bar, ==, 30); + + /* Check that it is possible both when initially + * adding the binding and when changing the source + */ + g_binding_group_set_source (group, NULL); + g_binding_group_set_source (group, source); + } + + g_object_unref (source); + g_object_unref (group); +} + +static void +test_binding_group_weak_ref_source (void) +{ + GBindingGroup *group = g_binding_group_new (); + BindingSource *source = g_object_new (binding_source_get_type (), NULL); + BindingTarget *target = g_object_new (binding_target_get_type (), NULL); + BindingSource *readback; + + g_binding_group_set_source (group, source); + g_binding_group_bind (group, "value", + target, "value", + G_BINDING_BIDIRECTIONAL); + + g_object_add_weak_pointer (G_OBJECT (source), (gpointer)&source); + readback = g_binding_group_dup_source (group); + g_assert_true (readback == source); + g_object_unref (readback); + g_object_unref (source); + g_assert_null (source); + g_assert_null (g_binding_group_dup_source (group)); + + /* Hopefully this would explode if the binding was still alive */ + g_object_set (target, "value", 0.0, NULL); + + g_object_unref (target); + g_object_unref (group); +} + +static void +test_binding_group_weak_ref_target (void) +{ + GBindingGroup *group = g_binding_group_new (); + BindingSource *source = g_object_new (binding_source_get_type (), NULL); + BindingTarget *target = g_object_new (binding_target_get_type (), NULL); + + g_binding_group_set_source (group, source); + g_binding_group_bind (group, "value", + target, "value", + G_BINDING_BIDIRECTIONAL); + + g_object_set (source, "value", 47.0, NULL); + g_assert_cmpfloat (target->value, ==, 47.0); + + g_object_add_weak_pointer (G_OBJECT (target), (gpointer)&target); + g_object_unref (target); + g_assert_null (target); + + /* Hopefully this would explode if the binding was still alive */ + g_object_set (source, "value", 0.0, NULL); + + g_object_unref (source); + g_object_unref (group); +} + +static void +test_binding_group_properties (void) +{ + GBindingGroup *group = g_binding_group_new (); + BindingSource *source = g_object_new (binding_source_get_type (), NULL); + BindingTarget *target = g_object_new (binding_target_get_type (), NULL); + BindingSource *other; + + g_binding_group_set_source (group, source); + g_binding_group_bind (group, "value", + target, "value", + G_BINDING_BIDIRECTIONAL); + + g_object_get (group, "source", &other, NULL); + g_assert_true (other == source); + g_object_unref (other); + + g_object_set (group, "source", NULL, NULL); + g_object_get (group, "source", &other, NULL); + g_assert_null (other); + + g_object_add_weak_pointer (G_OBJECT (target), (gpointer)&target); + g_object_unref (target); + g_assert_null (target); + + g_object_unref (source); + g_object_unref (group); +} + +static void +test_binding_group_weak_notify_no_bindings (void) +{ + GBindingGroup *group = g_binding_group_new (); + BindingSource *source = g_object_new (binding_source_get_type (), NULL); + + g_binding_group_set_source (group, source); + g_assert_finalize_object (source); + g_assert_finalize_object (group); +} + +static void +test_binding_group_empty_closures (void) +{ + GBindingGroup *group = g_binding_group_new (); + BindingSource *source = g_object_new (binding_source_get_type (), NULL); + BindingTarget *target = g_object_new (binding_target_get_type (), NULL); + + g_binding_group_bind_full (group, "value", target, "value", 0, + NULL, NULL, NULL, NULL); + + g_assert_finalize_object (group); + g_assert_finalize_object (target); + g_assert_finalize_object (source); +} + +gint +main (gint argc, + gchar *argv[]) +{ + g_test_init (&argc, &argv, NULL); + g_test_add_func ("/GObject/BindingGroup/invalid", test_binding_group_invalid); + g_test_add_func ("/GObject/BindingGroup/default", test_binding_group_default); + g_test_add_func ("/GObject/BindingGroup/bidirectional", test_binding_group_bidirectional); + g_test_add_func ("/GObject/BindingGroup/transform", test_binding_group_transform); + g_test_add_func ("/GObject/BindingGroup/transform-closures", test_binding_group_transform_closures); + g_test_add_func ("/GObject/BindingGroup/same-object", test_binding_group_same_object); + g_test_add_func ("/GObject/BindingGroup/weak-ref-source", test_binding_group_weak_ref_source); + g_test_add_func ("/GObject/BindingGroup/weak-ref-target", test_binding_group_weak_ref_target); + g_test_add_func ("/GObject/BindingGroup/properties", test_binding_group_properties); + g_test_add_func ("/GObject/BindingGroup/weak-notify-no-bindings", test_binding_group_weak_notify_no_bindings); + g_test_add_func ("/GObject/BindingGroup/empty-closures", test_binding_group_empty_closures); + return g_test_run (); +} diff --git a/gobject/tests/meson.build b/gobject/tests/meson.build index 54e0a76b9..901590471 100644 --- a/gobject/tests/meson.build +++ b/gobject/tests/meson.build @@ -37,6 +37,7 @@ gobject_tests = { 'threadtests' : {}, 'dynamictests' : {}, 'binding' : {}, + 'bindinggroup' : {}, 'properties' : {}, 'reference' : {}, 'flags' : {}, From dd43471f606466e239757e33bfd1bdeee152b36e Mon Sep 17 00:00:00 2001 From: Christian Hergert Date: Fri, 27 Aug 2021 13:01:00 -0700 Subject: [PATCH 2/2] gobject: add GSignalGroup 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. --- docs/reference/gobject/gobject-docs.xml | 1 + docs/reference/gobject/gobject-sections.txt | 22 + glib/glib-object.h | 1 + gobject/gsignalgroup.c | 912 ++++++++++++++++++++ gobject/gsignalgroup.h | 93 ++ gobject/meson.build | 2 + gobject/tests/meson.build | 1 + gobject/tests/signalgroup.c | 650 ++++++++++++++ 8 files changed, 1682 insertions(+) create mode 100644 gobject/gsignalgroup.c create mode 100644 gobject/gsignalgroup.h create mode 100644 gobject/tests/signalgroup.c diff --git a/docs/reference/gobject/gobject-docs.xml b/docs/reference/gobject/gobject-docs.xml index 1718d9e23..aa5a9c722 100644 --- a/docs/reference/gobject/gobject-docs.xml +++ b/docs/reference/gobject/gobject-docs.xml @@ -81,6 +81,7 @@ + diff --git a/docs/reference/gobject/gobject-sections.txt b/docs/reference/gobject/gobject-sections.txt index 2450a367d..cbab92406 100644 --- a/docs/reference/gobject/gobject-sections.txt +++ b/docs/reference/gobject/gobject-sections.txt @@ -1024,3 +1024,25 @@ G_IS_BINDING_GROUP g_binding_group_get_type + +
+gsignalgroup +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_dup_target +g_signal_group_get_type +g_signal_group_new +g_signal_group_set_target +g_signal_group_unblock + +G_IS_SIGNAL_GROUP +G_SIGNAL_GROUP +G_TYPE_SIGNAL_GROUP + +g_signal_group_get_type +
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 #include #include +#include #include #include #include diff --git a/gobject/gsignalgroup.c b/gobject/gsignalgroup.c new file mode 100644 index 000000000..8feba543c --- /dev/null +++ b/gobject/gsignalgroup.c @@ -0,0 +1,912 @@ +/* GObject - GLib Type, Object, Parameter and Signal Library + * + * Copyright (C) 2015-2022 Christian Hergert + * Copyright (C) 2015 Garrett Regier + * + * This library 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 library 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 Lesser General + * Public License along with this library; if not, see . + * + * SPDX-License-Identifier: LGPL-2.1-or-later + */ + +#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.72 + */ + +struct _GSignalGroup +{ + GObject parent_instance; + + GWeakRef target_ref; + GRecMutex mutex; + GPtrArray *handlers; + GType target_type; + gssize 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) + +typedef enum +{ + PROP_TARGET = 1, + PROP_TARGET_TYPE, + LAST_PROP +} GSignalGroupProperty; + +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) +{ + guint i; + + 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 (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; + guint i; + + g_assert (G_IS_SIGNAL_GROUP (self)); + g_assert (where_object_was != NULL); + + g_rec_mutex_lock (&self->mutex); + + g_weak_ref_set (&self->target_ref, NULL); + + for (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]); + + g_rec_mutex_unlock (&self->mutex); +} + +static void +g_signal_group_bind_handler (GSignalGroup *self, + SignalHandler *handler, + GObject *target) +{ + gssize i; + + 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 (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; + guint i; + + 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 (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; + guint i; + + 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 (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.72 + */ +void +g_signal_group_block (GSignalGroup *self) +{ + GObject *target; + guint i; + + g_return_if_fail (G_IS_SIGNAL_GROUP (self)); + g_return_if_fail (self->block_count >= 0); + + g_rec_mutex_lock (&self->mutex); + + self->block_count++; + + target = g_weak_ref_get (&self->target_ref); + + if (target == NULL) + goto unlock; + + for (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); + +unlock: + g_rec_mutex_unlock (&self->mutex); +} + +/** + * 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.72 + */ +void +g_signal_group_unblock (GSignalGroup *self) +{ + GObject *target; + guint i; + + g_return_if_fail (G_IS_SIGNAL_GROUP (self)); + g_return_if_fail (self->block_count > 0); + + g_rec_mutex_lock (&self->mutex); + + self->block_count--; + + target = g_weak_ref_get (&self->target_ref); + if (target == NULL) + goto unlock; + + for (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); + +unlock: + g_rec_mutex_unlock (&self->mutex); +} + +/** + * g_signal_group_dup_target: + * @self: the #GSignalGroup + * + * Gets the target instance used when connecting signals. + * + * Returns: (nullable) (transfer full) (type GObject): The target instance + * + * Since: 2.72 + */ +gpointer +g_signal_group_dup_target (GSignalGroup *self) +{ + GObject *target; + + g_return_val_if_fail (G_IS_SIGNAL_GROUP (self), NULL); + + g_rec_mutex_lock (&self->mutex); + target = g_weak_ref_get (&self->target_ref); + g_rec_mutex_unlock (&self->mutex); + + return target; +} + +/** + * g_signal_group_set_target: + * @self: the #GSignalGroup. + * @target: (nullable) (type GObject) (transfer none): 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.72 + */ +void +g_signal_group_set_target (GSignalGroup *self, + gpointer target) +{ + GObject *object; + + g_return_if_fail (G_IS_SIGNAL_GROUP (self)); + + g_rec_mutex_lock (&self->mutex); + + object = g_weak_ref_get (&self->target_ref); + + if (object == (GObject *)target) + goto cleanup; + + if (!g_signal_group_check_target_type (self, target)) + goto cleanup; + + /* 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]); + +cleanup: + g_clear_object (&object); + g_rec_mutex_unlock (&self->mutex); +} + +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_rec_mutex_lock (&self->mutex); + + 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); + + g_rec_mutex_unlock (&self->mutex); +} + +static void +g_signal_group_dispose (GObject *object) +{ + GSignalGroup *self = (GSignalGroup *)object; + + g_rec_mutex_lock (&self->mutex); + + 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_rec_mutex_unlock (&self->mutex); + + 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_rec_mutex_clear (&self->mutex); + + 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 ((GSignalGroupProperty) prop_id) + { + case PROP_TARGET: + g_value_take_object (value, g_signal_group_dup_target (self)); + 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 ((GSignalGroupProperty) 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. + * + * Since: 2.72 + */ + 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. + * + * Since: 2.72 + */ + 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 containing the new value for #GSignalGroup:target + * + * This signal is emitted when #GSignalGroup:target is set to a new value + * other than %NULL. It is similar to #GObject::notify on `target` except it + * will not emit when #GSignalGroup:target is %NULL and also allows for + * receiving the #GObject without a data-race. + * + * Since: 2.72 + */ + 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. + * + * Since: 2.72 + */ + 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) +{ + g_rec_mutex_init (&self->mutex); + 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: (transfer full): a new #GSignalGroup + * + * Since: 2.72 + */ +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 c_handler, + 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 (c_handler != NULL); + g_return_if_fail (!is_object || G_IS_OBJECT (data)); + + g_rec_mutex_lock (&self->mutex); + + if (self->has_bound_at_least_once) + { + g_critical ("Cannot add signals after setting target"); + g_rec_mutex_unlock (&self->mutex); + return; + } + + if ((flags & G_CONNECT_SWAPPED) != 0) + closure = g_cclosure_new_swap (c_handler, data, notify); + else + closure = g_cclosure_new (c_handler, 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_rec_mutex_unlock (&self->mutex); +} + +/** + * g_signal_group_connect_object: (skip) + * @self: a #GSignalGroup + * @detailed_signal: a string of the form `signal-name` with optional `::signal-detail` + * @c_handler: (scope notified): the #GCallback to connect + * @object: (not nullable) (transfer none): the #GObject to pass as data to @c_handler calls + * @flags: #GConnectFlags for the signal connection + * + * Connects @c_handler to the signal @detailed_signal on #GSignalGroup:target. + * + * Ensures that the @object stays alive during the call to @c_handler + * by temporarily adding a reference count. When the @object is destroyed + * the signal handler will automatically be removed. + * + * You cannot connect a signal handler after #GSignalGroup:target has been set. + * + * Since: 2.72 + */ +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 @c_handler calls + * @notify: function to be called when disposing of @self + * @flags: the flags used to create the signal connection + * + * Connects @c_handler to the signal @detailed_signal + * on the target instance of @self. + * + * You cannot connect a signal handler after #GSignalGroup:target has been set. + * + * Since: 2.72 + */ +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 @c_handler calls + * + * Connects @c_handler to the signal @detailed_signal + * on the target instance of @self. + * + * You cannot connect a signal handler after #GSignalGroup:target has been set. + * + * Since: 2.72 + */ +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 @c_handler calls + * + * Connects @c_handler to the signal @detailed_signal + * on the target instance of @self. + * + * The @c_handler will be called after the default handler of the signal. + * + * You cannot connect a signal handler after #GSignalGroup:target has been set. + * + * Since: 2.72 + */ +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 @c_handler calls + * + * Connects @c_handler 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 @c_handler. + * + * You cannot connect a signal handler after #GSignalGroup:target has been set. + * + * Since: 2.72 + */ +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..c82a5cd4f --- /dev/null +++ b/gobject/gsignalgroup.h @@ -0,0 +1,93 @@ +/* GObject - GLib Type, Object, Parameter and Signal Library + * + * Copyright (C) 2015-2022 Christian Hergert + * Copyright (C) 2015 Garrett Regier + * + * This library 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 library 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 Lesser General + * Public License along with this library; if not, see . + * + * SPDX-License-Identifier: LGPL-2.1-or-later + */ + +#ifndef __G_SIGNAL_GROUP_H__ +#define __G_SIGNAL_GROUP_H__ + +#if !defined (__GLIB_GOBJECT_H_INSIDE__) && !defined (GOBJECT_COMPILATION) +#error "Only can be included directly." +#endif + +#include +#include +#include + +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.72 + */ +typedef struct _GSignalGroup GSignalGroup; + +GLIB_AVAILABLE_IN_2_72 +GType g_signal_group_get_type (void) G_GNUC_CONST; +GLIB_AVAILABLE_IN_2_72 +GSignalGroup *g_signal_group_new (GType target_type); +GLIB_AVAILABLE_IN_2_72 +void g_signal_group_set_target (GSignalGroup *self, + gpointer target); +GLIB_AVAILABLE_IN_2_72 +gpointer g_signal_group_dup_target (GSignalGroup *self); +GLIB_AVAILABLE_IN_2_72 +void g_signal_group_block (GSignalGroup *self); +GLIB_AVAILABLE_IN_2_72 +void g_signal_group_unblock (GSignalGroup *self); +GLIB_AVAILABLE_IN_2_72 +void g_signal_group_connect_object (GSignalGroup *self, + const gchar *detailed_signal, + GCallback c_handler, + gpointer object, + GConnectFlags flags); +GLIB_AVAILABLE_IN_2_72 +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_72 +void g_signal_group_connect (GSignalGroup *self, + const gchar *detailed_signal, + GCallback c_handler, + gpointer data); +GLIB_AVAILABLE_IN_2_72 +void g_signal_group_connect_after (GSignalGroup *self, + const gchar *detailed_signal, + GCallback c_handler, + gpointer data); +GLIB_AVAILABLE_IN_2_72 +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 48bd87601..74feb09d9 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 901590471..8d568f00e 100644 --- a/gobject/tests/meson.build +++ b/gobject/tests/meson.build @@ -54,6 +54,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..5d1f17a81 --- /dev/null +++ b/gobject/tests/signalgroup.c @@ -0,0 +1,650 @@ +/* GObject - GLib Type, Object, Parameter and Signal Library + * + * Copyright (C) 2015-2022 Christian Hergert + * Copyright (C) 2015 Garrett Regier + * + * This library 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 library 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 Lesser General + * Public License along with this library; if not, see . + * + * SPDX-License-Identifier: LGPL-2.1-or-later + */ + +#include + +G_DECLARE_FINAL_TYPE (SignalTarget, signal_target, TEST, SIGNAL_TARGET, GObject) + +struct _SignalTarget +{ + GObject parent_instance; +}; + +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) +{ + SignalTarget *readback; + + g_assert_true (TEST_IS_SIGNAL_TARGET (target)); + g_assert_true (G_IS_SIGNAL_GROUP (group)); + g_assert_nonnull (signal_calls); + g_assert_true (signal_calls == &global_signal_calls); + + readback = g_signal_group_dup_target (group); + g_assert_true (readback == target); + g_object_unref (readback); + + *signal_calls += 1; +} + +static void +connect_after_cb (SignalTarget *target, + GSignalGroup *group, + gint *signal_calls) +{ + SignalTarget *readback; + + g_assert_true (TEST_IS_SIGNAL_TARGET (target)); + g_assert_true (G_IS_SIGNAL_GROUP (group)); + g_assert_nonnull (signal_calls); + g_assert_true (signal_calls == &global_signal_calls); + + readback = g_signal_group_dup_target (group); + g_assert_true (readback == target); + g_object_unref (readback); + + g_assert_cmpint (*signal_calls, ==, 4); + *signal_calls += 1; +} + +static void +connect_swapped_cb (gint *signal_calls, + GSignalGroup *group, + SignalTarget *target) +{ + SignalTarget *readback; + + g_assert_true (signal_calls != NULL); + g_assert_true (signal_calls == &global_signal_calls); + g_assert_true (G_IS_SIGNAL_GROUP (group)); + g_assert_true (TEST_IS_SIGNAL_TARGET (target)); + + readback = g_signal_group_dup_target (group); + g_assert_true (readback == target); + g_object_unref (readback); + + *signal_calls += 1; +} + +static void +connect_object_cb (SignalTarget *target, + GSignalGroup *group, + GObject *object) +{ + SignalTarget *readback; + gint *signal_calls; + + g_assert_true (TEST_IS_SIGNAL_TARGET (target)); + g_assert_true (G_IS_SIGNAL_GROUP (group)); + g_assert_true (G_IS_OBJECT (object)); + + readback = g_signal_group_dup_target (group); + g_assert_true (readback == target); + g_object_unref (readback); + + signal_calls = g_object_get_data (object, "signal-calls"); + g_assert_nonnull (signal_calls); + g_assert_true (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_nonnull (weak_notify_called); + g_assert_true (weak_notify_called == &global_weak_notify_called); + g_assert_nonnull (closure); + + 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_nonnull (weak_notify_called); + g_assert_true (weak_notify_called == &global_weak_notify_called); + g_assert_true (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 +dummy_handler (void) +{ +} + +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 */ + 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_assert_finalize_object (group); + g_test_assert_expected_messages (); + + /* Invalid Signal Name */ + g_test_expect_message (G_LOG_DOMAIN, G_LOG_LEVEL_CRITICAL, + "*g_signal_parse_name*"); + group = g_signal_group_new (signal_target_get_type ()); + g_signal_group_connect (group, + "does-not-exist", + G_CALLBACK (connect_before_cb), + NULL); + g_test_assert_expected_messages (); + g_assert_finalize_object (group); + + /* Invalid Callback */ + g_test_expect_message (G_LOG_DOMAIN, G_LOG_LEVEL_CRITICAL, + "*c_handler != NULL*"); + group = g_signal_group_new (signal_target_get_type ()); + g_signal_group_connect (group, + "the-signal", + G_CALLBACK (NULL), + NULL); + g_test_assert_expected_messages (); + g_assert_finalize_object (group); + + /* Connecting after setting target */ + g_test_expect_message (G_LOG_DOMAIN, G_LOG_LEVEL_CRITICAL, + "*Cannot add signals after setting target*"); + group = g_signal_group_new (signal_target_get_type ()); + g_signal_group_set_target (group, target); + g_signal_group_connect (group, + "the-signal", + G_CALLBACK (dummy_handler), + NULL); + g_test_assert_expected_messages (); + g_assert_finalize_object (group); + + g_assert_finalize_object (target); + g_assert_finalize_object (invalid_target); +} + +static void +test_signal_group_simple (void) +{ + SignalTarget *target; + GSignalGroup *group; + SignalTarget *readback; + + /* Set the target before connecting the signals */ + group = g_signal_group_new (signal_target_get_type ()); + target = g_object_new (signal_target_get_type (), NULL); + g_assert_null (g_signal_group_dup_target (group)); + g_signal_group_set_target (group, target); + readback = g_signal_group_dup_target (group); + g_assert_true (readback == target); + g_object_unref (readback); + g_assert_finalize_object (group); + assert_signals (target, NULL, FALSE); + g_assert_finalize_object (target); + + group = g_signal_group_new (signal_target_get_type ()); + target = g_object_new (signal_target_get_type (), NULL); + connect_all_signals (group); + g_signal_group_set_target (group, target); + assert_signals (target, group, TRUE); + g_assert_finalize_object (target); + g_assert_finalize_object (group); +} + +static void +test_signal_group_changing_target (void) +{ + SignalTarget *target1, *target2; + GSignalGroup *group = g_signal_group_new (signal_target_get_type ()); + SignalTarget *readback; + + connect_all_signals (group); + g_assert_null (g_signal_group_dup_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); + readback = g_signal_group_dup_target (group); + g_assert_true (readback == target1); + g_object_unref (readback); + + assert_signals (target1, group, TRUE); + + /* Set the same target */ + readback = g_signal_group_dup_target (group); + g_assert_true (readback == target1); + g_object_unref (readback); + g_signal_group_set_target (group, target1); + + readback = g_signal_group_dup_target (group); + g_assert_true (readback == target1); + g_object_unref (readback); + + 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); + readback = g_signal_group_dup_target (group); + g_assert_true (readback == target1); + g_object_unref (readback); + + g_signal_group_set_target (group, target2); + readback = g_signal_group_dup_target (group); + g_assert_true (readback == target2); + g_object_unref (readback); + + assert_signals (target2, group, TRUE); + + g_assert_finalize_object (target2); + g_assert_finalize_object (target1); + g_assert_finalize_object (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, *readback; + GSignalGroup *group = g_signal_group_new (signal_target_get_type ()); + + /* Test blocking and unblocking null target */ + g_signal_group_block (group); + g_signal_group_unblock (group); + + connect_all_signals (group); + g_assert_null (g_signal_group_dup_target (group)); + + target1 = g_object_new (signal_target_get_type (), NULL); + g_signal_group_set_target (group, target1); + readback = g_signal_group_dup_target (group); + g_assert_true (readback == target1); + g_object_unref (readback); + + 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); + readback = g_signal_group_dup_target (group); + g_assert_true (readback == target1); + g_object_unref (readback); + g_signal_group_set_target (group, target2); + readback = g_signal_group_dup_target (group); + g_assert_true (readback == target2); + g_object_unref (readback); + + 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_assert_finalize_object (target2); + g_assert_finalize_object (target1); + g_assert_finalize_object (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 ()); + SignalTarget *readback; + + g_assert_null (g_signal_group_dup_target (group)); + g_signal_group_set_target (group, target); + readback = g_signal_group_dup_target (group); + g_assert_true (readback == target); + g_object_unref (readback); + + g_assert_finalize_object (target); + g_assert_null (g_signal_group_dup_target (group)); + g_assert_finalize_object (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 ()); + SignalTarget *readback; + + /* 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_dup_target (group)); + g_signal_group_set_target (group, target); + readback = g_signal_group_dup_target (group); + g_assert_true (readback == target); + g_object_unref (readback); + + g_assert_finalize_object (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_null (g_signal_group_dup_target (group)); + g_signal_group_set_target (group, target); + readback = g_signal_group_dup_target (group); + g_assert_true (readback == target); + g_object_unref (readback); + + g_assert_finalize_object (group); + g_assert_finalize_object (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_assert_finalize_object (group); +} + +static void +test_signal_group_properties (void) +{ + GSignalGroup *group; + SignalTarget *target, *other; + GType gtype; + + group = g_signal_group_new (signal_target_get_type ()); + g_object_get (group, + "target", &target, + "target-type", >ype, + NULL); + g_assert_cmpint (gtype, ==, signal_target_get_type ()); + g_assert_null (target); + + target = g_object_new (signal_target_get_type (), NULL); + g_object_set (group, "target", target, NULL); + g_object_get (group, "target", &other, NULL); + g_assert_true (target == other); + g_object_unref (other); + + g_assert_finalize_object (target); + g_assert_null (g_signal_group_dup_target (group)); + g_assert_finalize_object (group); +} + +G_DECLARE_INTERFACE (SignalThing, signal_thing, SIGNAL, THING, GObject) + +struct _SignalThingInterface +{ + GTypeInterface iface; + void (*changed) (SignalThing *thing); +}; + +G_DEFINE_INTERFACE (SignalThing, signal_thing, G_TYPE_OBJECT) + +static guint signal_thing_changed; + +static void +signal_thing_default_init (SignalThingInterface *iface) +{ + signal_thing_changed = + g_signal_new ("changed", + G_TYPE_FROM_INTERFACE (iface), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (SignalThingInterface, changed), + NULL, NULL, NULL, + G_TYPE_NONE, 0); +} + +G_GNUC_NORETURN static void +thing_changed_cb (SignalThing *thing, + gpointer user_data G_GNUC_UNUSED) +{ + g_assert_not_reached (); +} + +static void +test_signal_group_interface (void) +{ + GSignalGroup *group; + + group = g_signal_group_new (signal_thing_get_type ()); + g_signal_group_connect (group, + "changed", + G_CALLBACK (thing_changed_cb), + NULL); + g_assert_finalize_object (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); + g_test_add_func ("/GObject/SignalGroup/properties", test_signal_group_properties); + g_test_add_func ("/GObject/SignalGroup/interface", test_signal_group_interface); + return g_test_run (); +}