[libadwaita] animation-target: Add AdwPropertyAnimationTarget



commit 5f8fe1c9951791f828fb6f97309a552a840c4475
Author: George Barrett <bob bob131 so>
Date:   Fri May 13 01:31:27 2022 +1000

    animation-target: Add AdwPropertyAnimationTarget
    
    This commit adds a new AdwAnimationTarget subclass,
    AdwPropertyAnimationTarget. This facility makes it simpler to animate
    GObject properties. This commit also adds a basic smoke test to
    test-animation.
    
    Fixes #466.

 src/adw-animation-target.c    | 419 +++++++++++++++++++++++++++++++++++++++++-
 src/adw-animation-target.h    |  19 ++
 tests/meson.build             |   1 +
 tests/test-animation-target.c |  78 ++++++++
 4 files changed, 515 insertions(+), 2 deletions(-)
---
diff --git a/src/adw-animation-target.c b/src/adw-animation-target.c
index f26911e2..cbc4ab5a 100644
--- a/src/adw-animation-target.c
+++ b/src/adw-animation-target.c
@@ -15,8 +15,6 @@
  *
  * Represents a value [class@Animation] can animate.
  *
- * Currently the only implementation is [class@CallbackAnimationTarget].
- *
  * Since: 1.0
  */
 
@@ -29,6 +27,15 @@
  * Since: 1.0
  */
 
+/**
+ * AdwPropertyAnimationTarget:
+ *
+ * An [class@AnimationTarget] changing the value of a property of a
+ * [class@GObject.Object] instance.
+ *
+ * Since: 1.2
+ */
+
 struct _AdwAnimationTarget
 {
   GObject parent_instance;
@@ -146,3 +153,411 @@ adw_callback_animation_target_new (AdwAnimationTargetFunc callback,
 
   return ADW_ANIMATION_TARGET (self);
 }
+
+struct _AdwPropertyAnimationTarget
+{
+  AdwAnimationTarget parent_instance;
+
+  GObject *object;
+
+  /* `property_name` should only be set during construction; if set, `pspec`
+     should be unset, and vice-versa. */
+  char *property_name;
+  GParamSpec *pspec;
+};
+
+struct _AdwPropertyAnimationTargetClass
+{
+  AdwAnimationTargetClass parent_class;
+};
+
+G_DEFINE_FINAL_TYPE (AdwPropertyAnimationTarget, adw_property_animation_target, ADW_TYPE_ANIMATION_TARGET)
+
+enum {
+  PROPERTY_PROP_0,
+  PROPERTY_PROP_OBJECT,
+  PROPERTY_PROP_PROPERTY_NAME,
+  PROPERTY_PROP_PSPEC,
+  LAST_PROPERTY_PROP
+};
+
+static GParamSpec *property_props[LAST_PROPERTY_PROP];
+
+static void
+object_weak_notify (gpointer  data,
+                    GObject  *object)
+{
+  AdwPropertyAnimationTarget *self = ADW_PROPERTY_ANIMATION_TARGET (data);
+  self->object = NULL;
+  g_critical ("Finalizing object associated with an AdwPropertyAnimationTarget");
+}
+
+static void
+set_object (AdwPropertyAnimationTarget *self,
+            GObject                    *object)
+{
+  if (self->object)
+    g_object_weak_unref (self->object, object_weak_notify, self);
+  self->object = object;
+  g_object_weak_ref (self->object, object_weak_notify, self);
+}
+
+static void
+set_property_name (AdwPropertyAnimationTarget *self,
+                   const char                 *property_name)
+{
+  if (self->pspec) {
+    g_critical ("Attempt to set property 'property-name' to '%s' on "
+                "AdwPropertyAnimationTarget with property 'pspec' already set "
+                "to '%s:%s'. Using 'property-name' instead",
+                property_name,
+                g_type_name (self->pspec->owner_type), self->pspec->name);
+    g_clear_pointer (&self->pspec, g_param_spec_unref);
+  }
+
+  g_clear_pointer (&self->property_name, g_free);
+  self->property_name = g_strdup (property_name);
+}
+
+static void
+set_pspec (AdwPropertyAnimationTarget *self,
+           GParamSpec                 *pspec)
+{
+  if (self->property_name) {
+    g_critical ("Attempt to set property 'pspec' to '%s:%s' on "
+                "AdwPropertyAnimationTarget with property 'property-name' "
+                "already set to '%s'. Using 'pspec' instead",
+                g_type_name (pspec->owner_type), pspec->name,
+                self->property_name);
+    g_clear_pointer (&self->property_name, g_free);
+  }
+
+  g_clear_pointer (&self->pspec, g_param_spec_unref);
+  self->pspec = g_param_spec_ref (pspec);
+}
+
+static void
+adw_property_animation_target_set_value (AdwAnimationTarget *target,
+                                         double              value)
+{
+  AdwPropertyAnimationTarget *self = ADW_PROPERTY_ANIMATION_TARGET (target);
+  GValue gvalue = G_VALUE_INIT;
+
+  if (!self->object || !self->pspec)
+    return;
+
+  g_value_init (&gvalue, G_TYPE_DOUBLE);
+  g_value_set_double (&gvalue, value);
+  g_object_set_property (self->object, self->pspec->name, &gvalue);
+}
+
+static void
+adw_property_animation_target_constructed (GObject *object)
+{
+  AdwPropertyAnimationTarget *self = ADW_PROPERTY_ANIMATION_TARGET (object);
+
+  G_OBJECT_CLASS (adw_property_animation_target_parent_class)->constructed (object);
+
+  if (!self->object) {
+    g_error ("AdwPropertyAnimationTarget constructed without specifying a value "
+             "for the 'object' property");
+    return;
+  }
+
+  if (!self->property_name && !self->pspec) {
+    g_error ("AdwPropertyAnimationTarget constructed without specifying a value "
+             "for either the 'property-name' or 'pspec' properties");
+    return;
+  }
+
+  /* Only one of these should be set. */
+  g_assert (!(self->property_name && self->pspec));
+
+  if (self->property_name) {
+    GParamSpec *pspec =
+      g_object_class_find_property (G_OBJECT_GET_CLASS (self->object),
+                                    self->property_name);
+
+    if (pspec) {
+      self->pspec = g_param_spec_ref (pspec);
+
+    } else {
+      g_error ("Type '%s' does not have a property named '%s'",
+               G_OBJECT_TYPE_NAME (self->object),
+               self->property_name);
+    }
+
+    g_clear_pointer (&self->property_name, g_free);
+  } else if (self->pspec) {
+    if (!g_type_is_a (G_OBJECT_TYPE (self->object), self->pspec->owner_type)) {
+      g_error ("Cannot create AdwPropertyAnimationTarget: %s doesn't have the "
+               "%s:%s property",
+               G_OBJECT_TYPE_NAME (self->object),
+               g_type_name (self->pspec->owner_type),
+               self->pspec->name);
+      g_clear_pointer (&self->pspec, g_param_spec_unref);
+    }
+  }
+}
+
+static void
+adw_property_animation_target_dispose (GObject *object)
+{
+  AdwPropertyAnimationTarget *self = ADW_PROPERTY_ANIMATION_TARGET (object);
+
+  if (self->object)
+    g_object_weak_unref (self->object, object_weak_notify, self);
+  self->object = NULL;
+
+  G_OBJECT_CLASS (adw_property_animation_target_parent_class)->dispose (object);
+}
+
+static void
+adw_property_animation_target_finalize (GObject *object)
+{
+  AdwPropertyAnimationTarget *self = ADW_PROPERTY_ANIMATION_TARGET (object);
+
+  g_free (self->property_name);
+  g_clear_pointer (&self->pspec, g_param_spec_unref);
+
+  G_OBJECT_CLASS (adw_property_animation_target_parent_class)->finalize (object);
+}
+
+static void
+adw_property_animation_target_get_property (GObject    *object,
+                                            guint       prop_id,
+                                            GValue     *value,
+                                            GParamSpec *pspec)
+{
+  AdwPropertyAnimationTarget *self = ADW_PROPERTY_ANIMATION_TARGET (object);
+
+  switch (prop_id) {
+  case PROPERTY_PROP_OBJECT:
+    g_value_set_object (value, adw_property_animation_target_get_object (self));
+    break;
+
+  case PROPERTY_PROP_PROPERTY_NAME:
+    g_value_set_string (value,
+                        adw_property_animation_target_get_property_name (self));
+    break;
+
+  case PROPERTY_PROP_PSPEC:
+    g_value_set_param (value, adw_property_animation_target_get_pspec (self));
+    break;
+
+  default:
+    G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+  }
+}
+
+static void
+adw_property_animation_target_set_property (GObject      *object,
+                                            guint         prop_id,
+                                            const GValue *value,
+                                            GParamSpec   *pspec)
+{
+  AdwPropertyAnimationTarget *self = ADW_PROPERTY_ANIMATION_TARGET (object);
+
+  switch (prop_id) {
+  case PROPERTY_PROP_OBJECT:
+    if (g_value_get_object (value) != NULL)
+      set_object (self, g_value_get_object (value));
+    break;
+
+  case PROPERTY_PROP_PROPERTY_NAME:
+    if (g_value_get_string (value) != NULL)
+      set_property_name (self, g_value_get_string (value));
+    break;
+
+  case PROPERTY_PROP_PSPEC:
+    if (g_value_get_param (value) != NULL)
+      set_pspec (self, g_value_get_param (value));
+    break;
+
+  default:
+    G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+  }
+}
+
+static void
+adw_property_animation_target_class_init (AdwPropertyAnimationTargetClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  AdwAnimationTargetClass *target_class = ADW_ANIMATION_TARGET_CLASS (klass);
+
+  object_class->constructed = adw_property_animation_target_constructed;
+  object_class->dispose = adw_property_animation_target_dispose;
+  object_class->finalize = adw_property_animation_target_finalize;
+  object_class->get_property = adw_property_animation_target_get_property;
+  object_class->set_property = adw_property_animation_target_set_property;
+
+  target_class->set_value = adw_property_animation_target_set_value;
+
+  /**
+   * AdwPropertyAnimationTarget:object: (attributes 
org.gtk.Property.get=adw_property_animation_target_get_object)
+   *
+   * The object whose property will be animated.
+   *
+   * The `AdwPropertyAnimationTarget` instance does not hold a strong reference
+   * on the object; make sure the object is kept alive throughout the target's
+   * lifetime.
+   *
+   * Since: 1.2
+   */
+  property_props[PROPERTY_PROP_OBJECT] =
+    g_param_spec_object ("object",
+                         "Object",
+                         "The object whose property will be animated",
+                         G_TYPE_OBJECT,
+                         G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY);
+
+  /**
+   * AdwPropertyAnimationTarget:property-name: (attributes 
org.gtk.Property.get=adw_property_animation_target_get_property_name)
+   *
+   * The name of the property to be animated.
+   *
+   * Only one of `property-name` or [property@PropertyAnimationTarget:pspec]
+   * should be set.
+   *
+   * Since: 1.2
+   */
+  property_props[PROPERTY_PROP_PROPERTY_NAME] =
+    g_param_spec_string ("property-name",
+                         "Property name",
+                         "The name of the property to be animated",
+                         NULL,
+                         G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY);
+
+  /**
+   * AdwPropertyAnimationTarget:pspec: (attributes 
org.gtk.Property.get=adw_property_animation_target_get_pspec)
+   *
+   * The `GParamSpec` of the property to be animated.
+   *
+   * Only one of `pspec` or [property@PropertyAnimationTarget:property-name]
+   * should be set.
+   *
+   * Since: 1.2
+   */
+  property_props[PROPERTY_PROP_PSPEC] =
+    g_param_spec_param ("pspec",
+                        "Param spec",
+                        "The param spec of the property to be animated",
+                        G_TYPE_PARAM,
+                        G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY);
+
+  g_object_class_install_properties (object_class,
+                                     LAST_PROPERTY_PROP,
+                                     property_props);
+}
+
+static void
+adw_property_animation_target_init (AdwPropertyAnimationTarget *self)
+{
+}
+
+/**
+ * adw_property_animation_target_new:
+ * @object: an object to be animated
+ * @property_name: the name of the property on @object to animate
+ *
+ * Creates a new `AdwPropertyAnimationTarget` for the @property_name property on
+ * @object.
+ *
+ * Returns: the newly created `AdwPropertyAnimationTarget`
+ *
+ * Since: 1.2
+ */
+AdwAnimationTarget *
+adw_property_animation_target_new (GObject    *object,
+                                   const char *property_name)
+{
+  g_return_val_if_fail (G_IS_OBJECT (object), NULL);
+  g_return_val_if_fail (property_name != NULL, NULL);
+
+  return g_object_new (ADW_TYPE_PROPERTY_ANIMATION_TARGET,
+                       "object", object,
+                       "property-name", property_name,
+                       NULL);
+}
+
+/**
+ * adw_property_animation_target_new_for_pspec:
+ * @object: an object to be animated
+ * @pspec: the param spec of the property on @object to animate
+ *
+ * Creates a new `AdwPropertyAnimationTarget` for the @pspec property on
+ * @object.
+ *
+ * Returns: new newly created `AdwPropertyAnimationTarget`
+ *
+ * Since: 1.2
+ */
+AdwAnimationTarget *
+adw_property_animation_target_new_for_pspec (GObject    *object,
+                                             GParamSpec *pspec)
+{
+  g_return_val_if_fail (G_IS_OBJECT (object), NULL);
+  g_return_val_if_fail (G_IS_PARAM_SPEC (pspec), NULL);
+
+  return g_object_new (ADW_TYPE_PROPERTY_ANIMATION_TARGET,
+                       "object", object,
+                       "pspec", pspec,
+                       NULL);
+}
+
+/**
+ * adw_property_animation_target_get_object: (attributes org.gtk.Method.get_property=object)
+ * @self: a property animation target
+ *
+ * Gets the object animated by @self.
+ *
+ * Returns: (transfer none): the animated object
+ *
+ * Since: 1.2
+ */
+GObject *
+adw_property_animation_target_get_object (AdwPropertyAnimationTarget *self)
+{
+  g_return_val_if_fail (ADW_IS_PROPERTY_ANIMATION_TARGET (self), NULL);
+
+  return self->object;
+}
+
+/**
+ * adw_property_animation_target_get_property_name: (attributes org.gtk.Method.get_property=property-name)
+ * @self: a property animation target
+ *
+ * Gets the name of the property animated by @self.
+ *
+ * Returns: the animated property name
+ *
+ * Since: 1.2
+ */
+const char *
+adw_property_animation_target_get_property_name (AdwPropertyAnimationTarget *self)
+{
+  g_return_val_if_fail (ADW_IS_PROPERTY_ANIMATION_TARGET (self), NULL);
+
+  if (self->pspec)
+    return self->pspec->name;
+  else
+    return self->property_name;
+}
+
+/**
+ * adw_property_animation_target_get_pspec: (attributes org.gtk.Method.get_property=pspec)
+ * @self: a property animation target
+ *
+ * Gets the `GParamSpec` of the property animated by @self.
+ *
+ * Returns: (transfer none): the animated property's `GParamSpec`
+ *
+ * Since: 1.2
+ */
+GParamSpec *
+adw_property_animation_target_get_pspec (AdwPropertyAnimationTarget *self)
+{
+  g_return_val_if_fail (ADW_IS_PROPERTY_ANIMATION_TARGET (self), NULL);
+
+  return self->pspec;
+}
diff --git a/src/adw-animation-target.h b/src/adw-animation-target.h
index 6f9c4383..d85d9b52 100644
--- a/src/adw-animation-target.h
+++ b/src/adw-animation-target.h
@@ -44,4 +44,23 @@ AdwAnimationTarget *adw_callback_animation_target_new (AdwAnimationTargetFunc ca
                                                        gpointer               user_data,
                                                        GDestroyNotify         destroy) 
G_GNUC_WARN_UNUSED_RESULT;
 
+#define ADW_TYPE_PROPERTY_ANIMATION_TARGET (adw_property_animation_target_get_type())
+
+ADW_AVAILABLE_IN_1_2
+GDK_DECLARE_INTERNAL_TYPE (AdwPropertyAnimationTarget, adw_property_animation_target, ADW, 
PROPERTY_ANIMATION_TARGET, AdwAnimationTarget)
+
+ADW_AVAILABLE_IN_1_2
+AdwAnimationTarget *adw_property_animation_target_new           (GObject    *object,
+                                                                 const char *property_name) 
G_GNUC_WARN_UNUSED_RESULT;
+ADW_AVAILABLE_IN_1_2
+AdwAnimationTarget *adw_property_animation_target_new_for_pspec (GObject    *object,
+                                                                 GParamSpec *pspec) 
G_GNUC_WARN_UNUSED_RESULT;
+
+ADW_AVAILABLE_IN_1_2
+GObject    *adw_property_animation_target_get_object        (AdwPropertyAnimationTarget *self);
+ADW_AVAILABLE_IN_1_2
+const char *adw_property_animation_target_get_property_name (AdwPropertyAnimationTarget *self);
+ADW_AVAILABLE_IN_1_2
+GParamSpec *adw_property_animation_target_get_pspec         (AdwPropertyAnimationTarget *self);
+
 G_END_DECLS
diff --git a/tests/meson.build b/tests/meson.build
index 03293446..aa57172d 100644
--- a/tests/meson.build
+++ b/tests/meson.build
@@ -26,6 +26,7 @@ endif
 test_names = [
   'test-action-row',
   'test-animation',
+  'test-animation-target',
   'test-application-window',
   'test-avatar',
   'test-bin',
diff --git a/tests/test-animation-target.c b/tests/test-animation-target.c
new file mode 100644
index 00000000..c43796d6
--- /dev/null
+++ b/tests/test-animation-target.c
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2022 George Barrett <bob bob131 so>
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#include <adwaita.h>
+
+static void
+test_adw_property_animation_target_construct (void)
+{
+  GObject *widget = g_object_ref_sink (G_OBJECT (gtk_button_new ()));
+  AdwPropertyAnimationTarget *named_target, *pspec_target;
+  GParamSpec *target_pspec;
+  const char *target_prop_name;
+
+  named_target =
+    ADW_PROPERTY_ANIMATION_TARGET (adw_property_animation_target_new (widget, "opacity"));
+
+  target_pspec = adw_property_animation_target_get_pspec (named_target);
+  g_assert_nonnull (target_pspec);
+  g_assert_cmpstr (target_pspec->name, ==, "opacity");
+
+  target_prop_name =
+    adw_property_animation_target_get_property_name (named_target);
+  g_assert_nonnull (target_prop_name);
+  g_assert_cmpstr (target_prop_name, ==, "opacity");
+
+  pspec_target =
+    ADW_PROPERTY_ANIMATION_TARGET (adw_property_animation_target_new_for_pspec (widget, target_pspec));
+
+  g_assert_true (adw_property_animation_target_get_pspec (pspec_target) == target_pspec);
+
+  target_prop_name =
+    adw_property_animation_target_get_property_name (pspec_target);
+  g_assert_nonnull (target_prop_name);
+  g_assert_cmpstr (target_prop_name, ==, "opacity");
+
+  g_assert_finalize_object (named_target);
+  g_assert_finalize_object (pspec_target);
+  g_assert_finalize_object (widget);
+}
+
+static void
+test_adw_property_animation_target_basic (void)
+{
+  GtkWidget *widget = g_object_ref_sink (gtk_button_new ());
+  AdwAnimationTarget *target =
+    adw_property_animation_target_new (G_OBJECT (widget), "opacity");
+  AdwAnimation *animation =
+    adw_timed_animation_new (widget, 1, 0, 100, g_object_ref (target));
+
+  g_assert_cmpfloat (gtk_widget_get_opacity (widget), ==, 1);
+
+  adw_animation_play (animation);
+
+  /* Since the widget is not mapped, the animation will immediately finish */
+  g_assert_cmpfloat (gtk_widget_get_opacity (widget), ==, 0);
+
+  g_assert_finalize_object (animation);
+  g_assert_finalize_object (target);
+  g_assert_finalize_object (widget);
+}
+
+int
+main (int   argc,
+      char *argv[])
+{
+  gtk_test_init (&argc, &argv, NULL);
+  adw_init ();
+
+  g_test_add_func("/Adwaita/PropertyAnimationTarget/construct",
+                  test_adw_property_animation_target_construct);
+  g_test_add_func("/Adwaita/PropertyAnimationTarget/basic",
+                  test_adw_property_animation_target_basic);
+
+  return g_test_run();
+}


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