timeline-element: add signals for child properties
authorHenry Wilkes <hwilkes@igalia.com>
Tue, 3 Mar 2020 14:31:10 +0000 (14:31 +0000)
committerHenry Wilkes <hwilkes@igalia.com>
Mon, 16 Mar 2020 14:19:52 +0000 (14:19 +0000)
Add the child-property-added and child-property-removed signals to
GESTimelineElement.

GESContainer is able to use this to keep their child properties in sync
with their children: if they are added or removed from the child, they
are also added or removed from the container.

ges/ges-container.c
ges/ges-internal.h
ges/ges-timeline-element.c
tests/check/ges/clip.c
tests/check/ges/group.c
tests/check/ges/test-utils.c
tests/check/ges/test-utils.h

index 67235b7..d6b2487 100644 (file)
@@ -61,9 +61,11 @@ typedef struct
   GstClockTime inpoint_offset;
   gint32 priority_offset;
 
-  guint start_notifyid;
-  guint duration_notifyid;
-  guint inpoint_notifyid;
+  gulong start_notifyid;
+  gulong duration_notifyid;
+  gulong inpoint_notifyid;
+  gulong child_property_added_notifyid;
+  gulong child_property_removed_notifyid;
 } ChildMapping;
 
 enum
@@ -120,6 +122,11 @@ _free_mapping (ChildMapping * mapping)
     g_signal_handler_disconnect (child, mapping->duration_notifyid);
   if (mapping->inpoint_notifyid)
     g_signal_handler_disconnect (child, mapping->inpoint_notifyid);
+  if (mapping->child_property_added_notifyid)
+    g_signal_handler_disconnect (child, mapping->child_property_added_notifyid);
+  if (mapping->child_property_removed_notifyid)
+    g_signal_handler_disconnect (child,
+        mapping->child_property_removed_notifyid);
 
   ges_timeline_element_set_parent (child, NULL);
   g_slice_free (ChildMapping, mapping);
@@ -213,59 +220,103 @@ _set_duration (GESTimelineElement * element, GstClockTime duration)
 }
 
 static void
+_add_childs_child_property (GESTimelineElement * container_child,
+    GObject * prop_child, GParamSpec * property, GESContainer * container)
+{
+  /* the container_child is kept as the owner of this child property when
+   * we register it on ourselves, but we use the same GObject child
+   * instance who the property comes from */
+  gboolean res =
+      ges_timeline_element_add_child_property_full (GES_TIMELINE_ELEMENT
+      (container), container_child, property, prop_child);
+  if (!res)
+    GST_INFO_OBJECT (container, "Could not register the child property '%s' "
+        "of our child %" GES_FORMAT " for the object %" GST_PTR_FORMAT,
+        property->name, GES_ARGS (container_child), prop_child);
+}
+
+static void
 _ges_container_add_child_properties (GESContainer * container,
     GESTimelineElement * child)
 {
   guint n_props, i;
 
+  /* use get_children_properties, rather than list_children_properties
+   * to ensure we are getting all the properties, without any interference
+   * from the ->list_children_properties vmethods */
   GParamSpec **child_props =
-      ges_timeline_element_list_children_properties (child,
+      ges_timeline_element_get_children_properties (child,
       &n_props);
 
   for (i = 0; i < n_props; i++) {
-    GObject *prop_child;
-    gchar *prop_name = g_strdup_printf ("%s::%s",
-        g_type_name (child_props[i]->owner_type),
-        child_props[i]->name);
-
-    if (ges_timeline_element_lookup_child (child, prop_name, &prop_child, NULL)) {
-      ges_timeline_element_add_child_property_full (GES_TIMELINE_ELEMENT
-          (container), child, child_props[i], prop_child);
-      gst_object_unref (prop_child);
-
-    }
-    g_free (prop_name);
-    g_param_spec_unref (child_props[i]);
+    GParamSpec *property = child_props[i];
+    GObject *prop_child =
+        ges_timeline_element_get_child_from_child_property (child, property);
+    if (prop_child)
+      _add_childs_child_property (child, prop_child, property, container);
+    g_param_spec_unref (property);
   }
 
   g_free (child_props);
 }
 
 static void
+_remove_childs_child_property (GESTimelineElement * container_child,
+    GObject * prop_child, GParamSpec * property, GESContainer * container)
+{
+  /* NOTE: some children may share the same GParamSpec. Currently, only
+   * the first such child added will have its children properties
+   * successfully registered for the container (even though the GObject
+   * child who the properties belong to will be a different instance). As
+   * such, we only want to remove the child property if it corresponds to
+   * the same instance that the parent container has.
+   * E.g. if we add child1 and child2, that have the same (or some
+   * overlapping) children properties. And child1 is added before child2,
+   * then child2's overlapping children properties would not be registered.
+   * If we remove child2, we do *not* want to removed the child properties
+   * for child1 because they belong to a GObject instance that we still
+   * have in our control.
+   * If we remove child1, we *do* want to remove the child properties for
+   * child1, even though child2 may overlap with some of them, because we
+   * are loosing the specific GObject instance that it belongs to!
+   * We could try and register the ones that match for the other children.
+   * However, it is probably simpler to change
+   * ges_timeline_element_add_child_property_full to accept the same
+   * GParamSpec, for different instances.
+   */
+  GESTimelineElement *element = GES_TIMELINE_ELEMENT (container);
+  GObject *our_prop_child =
+      ges_timeline_element_get_child_from_child_property (element, property);
+  if (our_prop_child == prop_child)
+    ges_timeline_element_remove_child_property (element, property);
+  else
+    GST_INFO_OBJECT (container, "Not removing child property '%s' for child"
+        " %" GES_FORMAT " because it derives from the object %" GST_PTR_FORMAT
+        "(%p) rather than the object %" GST_PTR_FORMAT "(%p)", property->name,
+        GES_ARGS (container_child), prop_child, prop_child, our_prop_child,
+        our_prop_child);
+}
+
+static void
 _ges_container_remove_child_properties (GESContainer * container,
     GESTimelineElement * child)
 {
   guint n_props, i;
 
+  /* use get_children_properties, rather than list_children_properties
+   * to ensure we are getting all the properties, without any interference
+   * from the ->list_children_properties vmethods */
   GParamSpec **child_props =
-      ges_timeline_element_list_children_properties (child,
+      ges_timeline_element_get_children_properties (child,
       &n_props);
 
   for (i = 0; i < n_props; i++) {
-    GObject *prop_child;
-    gchar *prop_name = g_strdup_printf ("%s::%s",
-        g_type_name (child_props[i]->owner_type),
-        child_props[i]->name);
-
-    if (ges_timeline_element_lookup_child (child, prop_name, &prop_child, NULL)) {
-      ges_timeline_element_remove_child_property (GES_TIMELINE_ELEMENT
-          (container), child_props[i]);
-      gst_object_unref (prop_child);
-
-    }
-
-    g_free (prop_name);
-    g_param_spec_unref (child_props[i]);
+    GParamSpec *property = child_props[i];
+    GObject *prop_child =
+        ges_timeline_element_get_child_from_child_property (child, property);
+    if (prop_child)
+      _remove_childs_child_property (child, prop_child, property, container);
+    g_param_spec_unref (property);
   }
 
   g_free (child_props);
@@ -819,6 +870,12 @@ ges_container_add (GESContainer * container, GESTimelineElement * child)
   }
 
   _ges_container_add_child_properties (container, child);
+  mapping->child_property_added_notifyid =
+      g_signal_connect (G_OBJECT (child), "child-property-added",
+      G_CALLBACK (_add_childs_child_property), container);
+  mapping->child_property_removed_notifyid =
+      g_signal_connect (G_OBJECT (child), "child-property-removed",
+      G_CALLBACK (_remove_childs_child_property), container);
 
   priv->adding_children = g_list_prepend (priv->adding_children, child);
   g_signal_emit (container, ges_container_signals[CHILD_ADDED_SIGNAL], 0,
@@ -892,7 +949,7 @@ ges_container_remove (GESContainer * container, GESTimelineElement * child)
   }
 
   container->children = g_list_remove (container->children, child);
-  /* Let it live removing from our mappings */
+  /* Let it live removing from our mappings, also disconnects signals */
   g_hash_table_remove (priv->mappings, child);
 
   _ges_container_remove_child_properties (container, child);
index e3b08ec..1197395 100644 (file)
@@ -445,10 +445,15 @@ G_GNUC_INTERNAL gdouble ges_timeline_element_get_media_duration_factor(GESTimeli
 G_GNUC_INTERNAL GESTimelineElement * ges_timeline_element_get_copied_from (GESTimelineElement *self);
 G_GNUC_INTERNAL GESTimelineElementFlags ges_timeline_element_flags (GESTimelineElement *self);
 G_GNUC_INTERNAL void                ges_timeline_element_set_flags (GESTimelineElement *self, GESTimelineElementFlags flags);
-G_GNUC_INTERNAL gboolean             ges_timeline_element_add_child_property_full (GESTimelineElement *self,
-                                                                                   GESTimelineElement *owner,
-                                                                                   GParamSpec *pspec,
-                                                                                   GObject *child);
+G_GNUC_INTERNAL gboolean            ges_timeline_element_add_child_property_full (GESTimelineElement *self,
+                                                                                  GESTimelineElement *owner,
+                                                                                  GParamSpec *pspec,
+                                                                                  GObject *child);
+
+G_GNUC_INTERNAL GObject *           ges_timeline_element_get_child_from_child_property (GESTimelineElement * self,
+                                                                                        GParamSpec * pspec);
+G_GNUC_INTERNAL GParamSpec **       ges_timeline_element_get_children_properties (GESTimelineElement * self,
+                                                                                  guint * n_properties);
 
 #define ELEMENT_FLAGS(obj)             (ges_timeline_element_flags (GES_TIMELINE_ELEMENT(obj)))
 #define ELEMENT_SET_FLAG(obj,flag)     (ges_timeline_element_set_flags(GES_TIMELINE_ELEMENT(obj), (ELEMENT_FLAGS(obj) | (flag))))
index b1a3c37..a21c8c0 100644 (file)
@@ -120,6 +120,8 @@ enum
 enum
 {
   DEEP_NOTIFY,
+  CHILD_PROPERTY_ADDED,
+  CHILD_PROPERTY_REMOVED,
   LAST_SIGNAL
 };
 
@@ -219,8 +221,8 @@ _lookup_child (GESTimelineElement * self, const gchar * prop_name,
   return res;
 }
 
-static GParamSpec **
-default_list_children_properties (GESTimelineElement * self,
+GParamSpec **
+ges_timeline_element_get_children_properties (GESTimelineElement * self,
     guint * n_properties)
 {
   GParamSpec **pspec, *spec;
@@ -495,10 +497,10 @@ ges_timeline_element_class_init (GESTimelineElementClass * klass)
   /**
    * GESTimelineElement::deep-notify:
    * @timeline_element: A #GESTtimelineElement
-   * @prop_object: The object that originated the signal
-   * @prop: The specification for the property that changed
+   * @prop_object: The child whose property has been set
+   * @prop: The specification for the property that been set
    *
-   * Emitted when a child of @timeline_element has one of its registered
+   * Emitted when a child of the element has one of its registered
    * properties set. See ges_timeline_element_add_child_property().
    * Note that unlike #GObject::notify, a child property name can not be
    * used as a signal detail.
@@ -509,6 +511,39 @@ ges_timeline_element_class_init (GESTimelineElementClass * klass)
       G_SIGNAL_NO_HOOKS, 0, NULL, NULL, NULL,
       G_TYPE_NONE, 2, G_TYPE_OBJECT, G_TYPE_PARAM);
 
+  /**
+   * GESTimelineElement::child-property-added:
+   * @timeline_element: A #GESTtimelineElement
+   * @prop_object: The child whose property has been registered
+   * @prop: The specification for the property that has been registered
+   *
+   * Emitted when the element has a new child property registered. See
+   * ges_timeline_element_add_child_property().
+   *
+   * Note that some GES elements will be automatically created with
+   * pre-registered children properties. You can use
+   * ges_timeline_element_list_children_properties() to list these.
+   */
+  ges_timeline_element_signals[CHILD_PROPERTY_ADDED] =
+      g_signal_new ("child-property-added", G_TYPE_FROM_CLASS (klass),
+      G_SIGNAL_RUN_FIRST, 0, NULL, NULL, NULL, G_TYPE_NONE, 2,
+      G_TYPE_OBJECT, G_TYPE_PARAM);
+
+  /**
+   * GESTimelineElement::child-property-removed:
+   * @timeline_element: A #GESTtimelineElement
+   * @prop_object: The child whose property has been unregistered
+   * @prop: The specification for the property that has been unregistered
+   *
+   * Emitted when the element has a child property unregistered. See
+   * ges_timeline_element_remove_child_property().
+   */
+  ges_timeline_element_signals[CHILD_PROPERTY_REMOVED] =
+      g_signal_new ("child-property-removed", G_TYPE_FROM_CLASS (klass),
+      G_SIGNAL_RUN_FIRST, 0, NULL, NULL, NULL, G_TYPE_NONE, 2,
+      G_TYPE_OBJECT, G_TYPE_PARAM);
+
+
   object_class->dispose = ges_timeline_element_dispose;
   object_class->finalize = ges_timeline_element_finalize;
 
@@ -525,7 +560,8 @@ ges_timeline_element_class_init (GESTimelineElementClass * klass)
   klass->roll_end = NULL;
   klass->trim = NULL;
 
-  klass->list_children_properties = default_list_children_properties;
+  klass->list_children_properties =
+      ges_timeline_element_get_children_properties;
   klass->lookup_child = _lookup_child;
   klass->set_child_property = _set_child_property;
 }
@@ -751,6 +787,13 @@ ges_timeline_element_add_child_property_full (GESTimelineElement * self,
   gchar *signame;
   ChildPropHandler *handler;
 
+  /* FIXME: allow the same pspec, provided the child is different. This
+   * is important for containers that may have duplicate children
+   * If this is changed, _remove_childs_child_property in ges-container.c
+   * should be changed to reflect this.
+   * We could hack around this by copying the pspec into a new instance
+   * of GParamSpec, but there is no such GLib method, and it would break
+   * the usage of get_..._from_pspec and set_..._from_pspec */
   if (g_hash_table_contains (self->priv->children_props, pspec)) {
     GST_INFO_OBJECT (self, "Child property already exists: %s", pspec->name);
     return FALSE;
@@ -769,10 +812,25 @@ ges_timeline_element_add_child_property_full (GESTimelineElement * self,
   g_hash_table_insert (self->priv->children_props, g_param_spec_ref (pspec),
       handler);
 
+  g_signal_emit (self, ges_timeline_element_signals[CHILD_PROPERTY_ADDED], 0,
+      child, pspec);
+
   g_free (signame);
   return TRUE;
 }
 
+GObject *
+ges_timeline_element_get_child_from_child_property (GESTimelineElement * self,
+    GParamSpec * pspec)
+{
+  ChildPropHandler *handler =
+      g_hash_table_lookup (self->priv->children_props, pspec);
+  if (handler)
+    return handler->child;
+  return NULL;
+}
+
+
 /*********************************************
  *            API implementation             *
  *********************************************/
@@ -1748,6 +1806,10 @@ gboolean
 ges_timeline_element_add_child_property (GESTimelineElement * self,
     GParamSpec * pspec, GObject * child)
 {
+  g_return_val_if_fail (GES_IS_TIMELINE_ELEMENT (self), FALSE);
+  g_return_val_if_fail (G_IS_PARAM_SPEC (pspec), FALSE);
+  g_return_val_if_fail (G_IS_OBJECT (child), FALSE);
+
   return ges_timeline_element_add_child_property_full (self, NULL, pspec,
       child);
 }
@@ -1769,6 +1831,7 @@ ges_timeline_element_get_child_property_by_pspec (GESTimelineElement * self,
   ChildPropHandler *handler;
 
   g_return_if_fail (GES_IS_TIMELINE_ELEMENT (self));
+  g_return_if_fail (G_IS_PARAM_SPEC (pspec));
 
   handler = g_hash_table_lookup (self->priv->children_props, pspec);
   if (!handler)
@@ -1799,7 +1862,8 @@ void
 ges_timeline_element_set_child_property_by_pspec (GESTimelineElement * self,
     GParamSpec * pspec, const GValue * value)
 {
-  g_return_if_fail (GES_IS_TRACK_ELEMENT (self));
+  g_return_if_fail (GES_IS_TIMELINE_ELEMENT (self));
+  g_return_if_fail (G_IS_PARAM_SPEC (pspec));
 
   set_child_property_by_pspec (self, pspec, value);
 }
@@ -2179,7 +2243,29 @@ gboolean
 ges_timeline_element_remove_child_property (GESTimelineElement * self,
     GParamSpec * pspec)
 {
-  return g_hash_table_remove (self->priv->children_props, pspec);
+  gpointer key, value;
+  GParamSpec *found_pspec;
+  ChildPropHandler *handler;
+
+  g_return_val_if_fail (GES_IS_TIMELINE_ELEMENT (self), FALSE);
+  g_return_val_if_fail (G_IS_PARAM_SPEC (pspec), FALSE);
+
+  if (!g_hash_table_steal_extended (self->priv->children_props, pspec,
+          &key, &value)) {
+    GST_WARNING_OBJECT (self, "No child property with pspec %p (%s) found",
+        pspec, pspec->name);
+    return FALSE;
+  }
+  found_pspec = G_PARAM_SPEC (key);
+  handler = (ChildPropHandler *) value;
+
+  g_signal_emit (self, ges_timeline_element_signals[CHILD_PROPERTY_REMOVED], 0,
+      handler->child, found_pspec);
+
+  g_param_spec_unref (found_pspec);
+  _child_prop_handler_free (handler);
+
+  return TRUE;
 }
 
 /**
index 3f8c908..68b1462 100644 (file)
@@ -926,6 +926,311 @@ GST_START_TEST (test_can_add_effect)
 
 GST_END_TEST;
 
+GST_START_TEST (test_children_properties_contain)
+{
+  GESTimeline *timeline;
+  GESLayer *layer;
+  GESClip *clip;
+  GList *tmp;
+  GParamSpec **clips_child_props, **childrens_child_props = NULL;
+  guint num_clips_props, num_childrens_props = 0;
+
+  ges_init ();
+
+  timeline = ges_timeline_new_audio_video ();
+  layer = ges_timeline_append_layer (timeline);
+  clip = GES_CLIP (ges_test_clip_new ());
+  ges_timeline_element_set_duration (GES_TIMELINE_ELEMENT (clip), 50);
+
+  fail_unless (ges_layer_add_clip (layer, clip));
+
+  clips_child_props =
+      ges_timeline_element_list_children_properties (GES_TIMELINE_ELEMENT
+      (clip), &num_clips_props);
+  fail_unless (clips_child_props);
+  fail_unless (num_clips_props);
+
+  fail_unless (GES_CONTAINER_CHILDREN (clip));
+
+  for (tmp = GES_CONTAINER_CHILDREN (clip); tmp; tmp = tmp->next)
+    childrens_child_props =
+        append_children_properties (childrens_child_props, tmp->data,
+        &num_childrens_props);
+
+  assert_property_list_match (clips_child_props, num_clips_props,
+      childrens_child_props, num_childrens_props);
+
+  free_children_properties (clips_child_props, num_clips_props);
+  free_children_properties (childrens_child_props, num_childrens_props);
+
+  gst_object_unref (timeline);
+
+  ges_deinit ();
+}
+
+GST_END_TEST;
+
+static gboolean
+_has_child_property (GESTimelineElement * element, GParamSpec * property)
+{
+  gboolean has_prop = FALSE;
+  guint num_props, i;
+  GParamSpec **props =
+      ges_timeline_element_list_children_properties (element, &num_props);
+  for (i = 0; i < num_props; i++) {
+    if (props[i] == property)
+      has_prop = TRUE;
+    g_param_spec_unref (props[i]);
+  }
+  g_free (props);
+  return has_prop;
+}
+
+typedef struct
+{
+  GstElement *child;
+  GParamSpec *property;
+  guint num_calls;
+} PropChangedData;
+
+#define _INIT_PROP_CHANGED_DATA(data) \
+  data.child = NULL; \
+  data.property = NULL; \
+  data.num_calls = 0;
+
+static void
+_prop_changed_cb (GESTimelineElement * element, GstElement * child,
+    GParamSpec * property, PropChangedData * data)
+{
+  data->num_calls++;
+  data->property = property;
+  data->child = child;
+}
+
+#define _assert_prop_changed_data(element, data, num_cmp, chld_cmp, prop_cmp) \
+  fail_unless (num_cmp == data.num_calls, \
+      "%s: num calls to callback (%u) not the expected %u", element->name, \
+      data.num_calls, num_cmp); \
+  fail_unless (prop_cmp == data.property, \
+      "%s: property %s is not the expected property %s", element->name, \
+      data.property->name, prop_cmp ? ((GParamSpec *)prop_cmp)->name : NULL); \
+  fail_unless (chld_cmp == data.child, \
+      "%s: child %s is not the expected child %s", element->name, \
+      GST_ELEMENT_NAME (data.child), \
+      chld_cmp ? GST_ELEMENT_NAME (chld_cmp) : NULL);
+
+#define _assert_int_val_child_prop(element, val, int_cmp, prop, prop_name) \
+  g_value_init (&val, G_TYPE_INT); \
+  ges_timeline_element_get_child_property_by_pspec (element, prop, &val); \
+  assert_equals_int (g_value_get_int (&val), int_cmp); \
+  g_value_unset (&val); \
+  g_value_init (&val, G_TYPE_INT); \
+  fail_unless (ges_timeline_element_get_child_property ( \
+        element, prop_name, &val)); \
+  assert_equals_int (g_value_get_int (&val), int_cmp); \
+  g_value_unset (&val); \
+
+GST_START_TEST (test_children_properties_change)
+{
+  GESTimeline *timeline;
+  GESLayer *layer;
+  GESTimelineElement *clip, *child;
+  PropChangedData clip_add_data, clip_remove_data, clip_notify_data,
+      child_add_data, child_remove_data, child_notify_data;
+  GstElement *sub_child;
+  GParamSpec *prop1, *prop2, *prop3;
+  GValue val = G_VALUE_INIT;
+  gint num_buffs;
+
+  ges_init ();
+
+  timeline = ges_timeline_new_audio_video ();
+  layer = ges_timeline_append_layer (timeline);
+  clip = GES_TIMELINE_ELEMENT (ges_test_clip_new ());
+  ges_timeline_element_set_duration (GES_TIMELINE_ELEMENT (clip), 50);
+
+  fail_unless (ges_layer_add_clip (layer, GES_CLIP (clip)));
+  fail_unless (GES_CONTAINER_CHILDREN (clip));
+  child = GES_CONTAINER_CHILDREN (clip)->data;
+
+  /* fake sub-child */
+  sub_child = gst_element_factory_make ("fakesink", "sub-child");
+  fail_unless (sub_child);
+  gst_object_ref_sink (sub_child);
+  prop1 = g_object_class_find_property (G_OBJECT_GET_CLASS (sub_child),
+      "num-buffers");
+  fail_unless (prop1);
+  prop2 = g_object_class_find_property (G_OBJECT_GET_CLASS (sub_child), "dump");
+  fail_unless (prop2);
+  prop3 = g_object_class_find_property (G_OBJECT_GET_CLASS (sub_child),
+      "silent");
+  fail_unless (prop2);
+
+  _INIT_PROP_CHANGED_DATA (clip_add_data);
+  _INIT_PROP_CHANGED_DATA (clip_remove_data);
+  _INIT_PROP_CHANGED_DATA (clip_notify_data);
+  _INIT_PROP_CHANGED_DATA (child_add_data);
+  _INIT_PROP_CHANGED_DATA (child_remove_data);
+  _INIT_PROP_CHANGED_DATA (child_notify_data);
+  g_signal_connect (clip, "child-property-added",
+      G_CALLBACK (_prop_changed_cb), &clip_add_data);
+  g_signal_connect (clip, "child-property-removed",
+      G_CALLBACK (_prop_changed_cb), &clip_remove_data);
+  g_signal_connect (clip, "deep-notify",
+      G_CALLBACK (_prop_changed_cb), &clip_notify_data);
+  g_signal_connect (child, "child-property-added",
+      G_CALLBACK (_prop_changed_cb), &child_add_data);
+  g_signal_connect (child, "child-property-removed",
+      G_CALLBACK (_prop_changed_cb), &child_remove_data);
+  g_signal_connect (child, "deep-notify",
+      G_CALLBACK (_prop_changed_cb), &child_notify_data);
+
+  /* adding to child should also add it to the parent clip */
+  fail_unless (ges_timeline_element_add_child_property (child, prop1,
+          G_OBJECT (sub_child)));
+
+  fail_unless (_has_child_property (child, prop1));
+  fail_unless (_has_child_property (clip, prop1));
+
+  _assert_prop_changed_data (clip, clip_add_data, 1, sub_child, prop1);
+  _assert_prop_changed_data (clip, clip_remove_data, 0, NULL, NULL);
+  _assert_prop_changed_data (clip, clip_notify_data, 0, NULL, NULL);
+  _assert_prop_changed_data (child, child_add_data, 1, sub_child, prop1);
+  _assert_prop_changed_data (child, child_remove_data, 0, NULL, NULL);
+  _assert_prop_changed_data (child, child_notify_data, 0, NULL, NULL);
+
+  fail_unless (ges_timeline_element_add_child_property (child, prop2,
+          G_OBJECT (sub_child)));
+
+  fail_unless (_has_child_property (child, prop2));
+  fail_unless (_has_child_property (clip, prop2));
+
+  _assert_prop_changed_data (clip, clip_add_data, 2, sub_child, prop2);
+  _assert_prop_changed_data (clip, clip_remove_data, 0, NULL, NULL);
+  _assert_prop_changed_data (clip, clip_notify_data, 0, NULL, NULL);
+  _assert_prop_changed_data (child, child_add_data, 2, sub_child, prop2);
+  _assert_prop_changed_data (child, child_remove_data, 0, NULL, NULL);
+  _assert_prop_changed_data (child, child_notify_data, 0, NULL, NULL);
+
+  /* adding to parent does not add to the child */
+
+  fail_unless (ges_timeline_element_add_child_property (clip, prop3,
+          G_OBJECT (sub_child)));
+
+  fail_if (_has_child_property (child, prop3));
+  fail_unless (_has_child_property (clip, prop3));
+
+  _assert_prop_changed_data (clip, clip_add_data, 3, sub_child, prop3);
+  _assert_prop_changed_data (clip, clip_remove_data, 0, NULL, NULL);
+  _assert_prop_changed_data (clip, clip_notify_data, 0, NULL, NULL);
+  _assert_prop_changed_data (child, child_add_data, 2, sub_child, prop2);
+  _assert_prop_changed_data (child, child_remove_data, 0, NULL, NULL);
+  _assert_prop_changed_data (child, child_notify_data, 0, NULL, NULL);
+
+  /* both should be notified of a change in the value */
+
+  g_object_set (G_OBJECT (sub_child), "num-buffers", 100, NULL);
+
+  _assert_prop_changed_data (clip, clip_add_data, 3, sub_child, prop3);
+  _assert_prop_changed_data (clip, clip_remove_data, 0, NULL, NULL);
+  _assert_prop_changed_data (clip, clip_notify_data, 1, sub_child, prop1);
+  _assert_prop_changed_data (child, child_add_data, 2, sub_child, prop2);
+  _assert_prop_changed_data (child, child_remove_data, 0, NULL, NULL);
+  _assert_prop_changed_data (child, child_notify_data, 1, sub_child, prop1);
+
+  _assert_int_val_child_prop (clip, val, 100, prop1,
+      "GstFakeSink::num-buffers");
+  _assert_int_val_child_prop (child, val, 100, prop1,
+      "GstFakeSink::num-buffers");
+
+  g_value_init (&val, G_TYPE_INT);
+  g_value_set_int (&val, 79);
+  ges_timeline_element_set_child_property_by_pspec (clip, prop1, &val);
+  g_value_unset (&val);
+
+  _assert_prop_changed_data (clip, clip_add_data, 3, sub_child, prop3);
+  _assert_prop_changed_data (clip, clip_remove_data, 0, NULL, NULL);
+  _assert_prop_changed_data (clip, clip_notify_data, 2, sub_child, prop1);
+  _assert_prop_changed_data (child, child_add_data, 2, sub_child, prop2);
+  _assert_prop_changed_data (child, child_remove_data, 0, NULL, NULL);
+  _assert_prop_changed_data (child, child_notify_data, 2, sub_child, prop1);
+
+  _assert_int_val_child_prop (clip, val, 79, prop1, "GstFakeSink::num-buffers");
+  _assert_int_val_child_prop (child, val, 79, prop1,
+      "GstFakeSink::num-buffers");
+  g_object_get (G_OBJECT (sub_child), "num-buffers", &num_buffs, NULL);
+  assert_equals_int (num_buffs, 79);
+
+  g_value_init (&val, G_TYPE_INT);
+  g_value_set_int (&val, 97);
+  fail_unless (ges_timeline_element_set_child_property (child,
+          "GstFakeSink::num-buffers", &val));
+  g_value_unset (&val);
+
+  _assert_prop_changed_data (clip, clip_add_data, 3, sub_child, prop3);
+  _assert_prop_changed_data (clip, clip_remove_data, 0, NULL, NULL);
+  _assert_prop_changed_data (clip, clip_notify_data, 3, sub_child, prop1);
+  _assert_prop_changed_data (child, child_add_data, 2, sub_child, prop2);
+  _assert_prop_changed_data (child, child_remove_data, 0, NULL, NULL);
+  _assert_prop_changed_data (child, child_notify_data, 3, sub_child, prop1);
+
+  _assert_int_val_child_prop (clip, val, 97, prop1, "GstFakeSink::num-buffers");
+  _assert_int_val_child_prop (child, val, 97, prop1,
+      "GstFakeSink::num-buffers");
+  g_object_get (G_OBJECT (sub_child), "num-buffers", &num_buffs, NULL);
+  assert_equals_int (num_buffs, 97);
+
+  /* remove a property from the child, removes from the parent */
+
+  fail_unless (ges_timeline_element_remove_child_property (child, prop2));
+
+  _assert_prop_changed_data (clip, clip_add_data, 3, sub_child, prop3);
+  _assert_prop_changed_data (clip, clip_remove_data, 1, sub_child, prop2);
+  _assert_prop_changed_data (clip, clip_notify_data, 3, sub_child, prop1);
+  _assert_prop_changed_data (child, child_add_data, 2, sub_child, prop2);
+  _assert_prop_changed_data (child, child_remove_data, 1, sub_child, prop2);
+  _assert_prop_changed_data (child, child_notify_data, 3, sub_child, prop1);
+
+  fail_if (_has_child_property (child, prop2));
+  fail_if (_has_child_property (clip, prop2));
+
+  /* removing from parent doesn't remove from child */
+
+  fail_unless (ges_timeline_element_remove_child_property (clip, prop1));
+
+  _assert_prop_changed_data (clip, clip_add_data, 3, sub_child, prop3);
+  _assert_prop_changed_data (clip, clip_remove_data, 2, sub_child, prop1);
+  _assert_prop_changed_data (clip, clip_notify_data, 3, sub_child, prop1);
+  _assert_prop_changed_data (child, child_add_data, 2, sub_child, prop2);
+  _assert_prop_changed_data (child, child_remove_data, 1, sub_child, prop2);
+  _assert_prop_changed_data (child, child_notify_data, 3, sub_child, prop1);
+
+  fail_unless (_has_child_property (child, prop1));
+  fail_if (_has_child_property (clip, prop1));
+
+  /* but still safe to remove it from the child later */
+
+  fail_unless (ges_timeline_element_remove_child_property (child, prop1));
+
+  _assert_prop_changed_data (clip, clip_add_data, 3, sub_child, prop3);
+  _assert_prop_changed_data (clip, clip_remove_data, 2, sub_child, prop1);
+  _assert_prop_changed_data (clip, clip_notify_data, 3, sub_child, prop1);
+  _assert_prop_changed_data (child, child_add_data, 2, sub_child, prop2);
+  _assert_prop_changed_data (child, child_remove_data, 2, sub_child, prop1);
+  _assert_prop_changed_data (child, child_notify_data, 3, sub_child, prop1);
+
+  fail_if (_has_child_property (child, prop1));
+  fail_if (_has_child_property (clip, prop1));
+
+  gst_object_unref (sub_child);
+  gst_object_unref (timeline);
+
+  ges_deinit ();
+}
+
+GST_END_TEST;
+
+
 static Suite *
 ges_suite (void)
 {
@@ -944,6 +1249,8 @@ ges_suite (void)
   tcase_add_test (tc_chain, test_effects_priorities);
   tcase_add_test (tc_chain, test_children_time_setters);
   tcase_add_test (tc_chain, test_can_add_effect);
+  tcase_add_test (tc_chain, test_children_properties_contain);
+  tcase_add_test (tc_chain, test_children_properties_change);
 
   return s;
 }
index 091601c..219d489 100644 (file)
@@ -710,6 +710,141 @@ GST_START_TEST (test_group_serialization)
 
 GST_END_TEST;
 
+GST_START_TEST (test_children_properties_contain)
+{
+  GESTimeline *timeline;
+  GESLayer *layer;
+  GESAsset *asset;
+  GESTimelineElement *c1, *c2, *c3, *g1, *g2;
+  GParamSpec **child_props1, **child_props2;
+  guint num_props1, num_props2;
+
+  ges_init ();
+
+  timeline = ges_timeline_new_audio_video ();
+  layer = ges_timeline_append_layer (timeline);
+
+  asset = ges_asset_request (GES_TYPE_TEST_CLIP, NULL, NULL);
+  /* choose one audio and one video to give them different properties */
+  c1 = GES_TIMELINE_ELEMENT (ges_layer_add_asset (layer, asset, 0, 0, 10,
+          GES_TRACK_TYPE_AUDIO));
+  c2 = GES_TIMELINE_ELEMENT (ges_layer_add_asset (layer, asset, 20, 0, 10,
+          GES_TRACK_TYPE_VIDEO));
+  /* but c3 will have the same child properties as c1! */
+  c3 = GES_TIMELINE_ELEMENT (ges_layer_add_asset (layer, asset, 40, 0, 10,
+          GES_TRACK_TYPE_AUDIO));
+
+  fail_unless (c1);
+  fail_unless (c2);
+
+  g1 = GES_TIMELINE_ELEMENT (ges_group_new ());
+  g2 = GES_TIMELINE_ELEMENT (ges_group_new ());
+
+  /* group should have the same as its children */
+  fail_unless (ges_container_add (GES_CONTAINER (g1), c1));
+
+  num_props1 = 0;
+  child_props1 = append_children_properties (NULL, c1, &num_props1);
+  num_props2 = 0;
+  child_props2 = append_children_properties (NULL, g1, &num_props2);
+
+  assert_property_list_match (child_props1, num_props1,
+      child_props2, num_props2);
+
+  /* add next child and gain its children properties as well */
+  fail_unless (ges_container_add (GES_CONTAINER (g1), c2));
+
+  /* add the child properties of c2 to the existing list for c1 */
+  child_props1 = append_children_properties (child_props1, c2, &num_props1);
+
+  free_children_properties (child_props2, num_props2);
+  num_props2 = 0;
+  child_props2 = append_children_properties (NULL, g1, &num_props2);
+
+  assert_property_list_match (child_props1, num_props1,
+      child_props2, num_props2);
+
+  /* FIXME: if c1 and c3 have the same child properties (they use the
+   * same GParamSpec) then ges_timeline_element_add_child_property_full
+   * will fail, even though the corresponding GObject child is not the
+   * same instance */
+
+  fail_unless (ges_container_add (GES_CONTAINER (g1), c3));
+
+  /* FIXME: regarding the above comment, ideally we would append the
+   * children properties for c3 to child_props1, so that its children
+   * properties appear twice in the list:
+   * child_props1 =
+   * append_children_properties (child_props1, c3, &num_props1); */
+
+  free_children_properties (child_props2, num_props2);
+  num_props2 = 0;
+  child_props2 = append_children_properties (NULL, g1, &num_props2);
+
+  assert_property_list_match (child_props1, num_props1,
+      child_props2, num_props2);
+
+  /* remove c3 */
+  fail_unless (ges_container_remove (GES_CONTAINER (g1), c3));
+
+  /* FIXME: regarding the above comment, ideally we would reset
+   * child_props1 to only contain the child properties for c1 and c2
+   * Currently, we at least want to make sure that the child properties
+   * for c1 remain.
+   * Currently, if we removed c1 first, all its children properties would
+   * be removed from g1, and this would *not* automatically register the
+   * children properties for c3. */
+
+  free_children_properties (child_props2, num_props2);
+  num_props2 = 0;
+  child_props2 = append_children_properties (NULL, g1, &num_props2);
+
+  assert_property_list_match (child_props1, num_props1,
+      child_props2, num_props2);
+
+  /* remove c1 */
+  fail_unless (ges_container_remove (GES_CONTAINER (g1), c1));
+
+  free_children_properties (child_props1, num_props1);
+  num_props1 = 0;
+  child_props1 = append_children_properties (NULL, c2, &num_props1);
+
+  free_children_properties (child_props2, num_props2);
+  num_props2 = 0;
+  child_props2 = append_children_properties (NULL, g1, &num_props2);
+
+  assert_property_list_match (child_props1, num_props1,
+      child_props2, num_props2);
+
+  /* add g1 and c1 to g2 */
+  fail_unless (ges_container_add (GES_CONTAINER (g2), g1));
+  fail_unless (ges_container_add (GES_CONTAINER (g2), c1));
+
+  free_children_properties (child_props1, num_props1);
+  num_props1 = 0;
+  child_props1 = append_children_properties (NULL, g2, &num_props1);
+
+  free_children_properties (child_props2, num_props2);
+  num_props2 = 0;
+  child_props2 = append_children_properties (NULL, c1, &num_props2);
+  child_props2 = append_children_properties (child_props2, g1, &num_props2);
+
+  assert_property_list_match (child_props1, num_props1,
+      child_props2, num_props2);
+
+  free_children_properties (child_props1, num_props1);
+  free_children_properties (child_props2, num_props2);
+
+  gst_object_unref (timeline);
+
+  ges_deinit ();
+}
+
+GST_END_TEST;
+
+
+
+
 static Suite *
 ges_suite (void)
 {
@@ -723,6 +858,7 @@ ges_suite (void)
   tcase_add_test (tc_chain, test_group_in_self);
   tcase_add_test (tc_chain, test_group_serialization);
   tcase_add_test (tc_chain, test_group_in_group_layer_moving);
+  tcase_add_test (tc_chain, test_children_properties_contain);
 
   return s;
 }
index bfb299b..862d487 100644 (file)
@@ -312,3 +312,33 @@ print_timeline (GESTimeline * timeline)
   g_printerr
       ("\n=====================================================================\n");
 }
+
+/* append the properties found in element to list, num_props should point
+ * to the current list length.
+ */
+GParamSpec **
+append_children_properties (GParamSpec ** list, GESTimelineElement * element,
+    guint * num_props)
+{
+  guint i, num;
+  GParamSpec **props =
+      ges_timeline_element_list_children_properties (element, &num);
+  fail_unless (props);
+  list = g_realloc_n (list, num + *num_props, sizeof (GParamSpec *));
+
+  for (i = 0; i < num; i++)
+    list[*num_props + i] = props[i];
+
+  g_free (props);
+  *num_props += num;
+  return list;
+}
+
+void
+free_children_properties (GParamSpec ** list, guint num_props)
+{
+  guint i;
+  for (i = 0; i < num_props; i++)
+    g_param_spec_unref (list[i]);
+  g_free (list);
+}
index b225c76..3f426f4 100644 (file)
@@ -51,6 +51,11 @@ ges_generate_test_file_audio_video (const gchar * filedest,
 gboolean
 play_timeline (GESTimeline * timeline);
 
+GParamSpec **
+append_children_properties (GParamSpec ** list, GESTimelineElement * element, guint * num_props);
+void
+free_children_properties (GParamSpec ** list, guint num_props);
+
 #define nle_object_check(nleobj, start, duration, mstart, mduration, priority, active) { \
   guint64 pstart, pdur, inpoint, pprio, pact;                  \
   g_object_get (nleobj, "start", &pstart, "duration", &pdur,           \
@@ -123,6 +128,53 @@ G_STMT_START {                                          \
     GES_TIMELINE_ELEMENT_LAYER_PRIORITY (clip), layer_prio);                 \
 }
 
+/* test that the two property lists contain the same properties the same
+ * number of times */
+#define assert_property_list_match(list1, len1, list2, len2) \
+  { \
+    gboolean *found_count_in_list2; \
+    guint *count_list1; \
+    guint i, j; \
+    found_count_in_list2 = g_new0 (gboolean, len1); \
+    count_list1 = g_new0 (guint, len1); \
+    for (i = 0; i < len1; i++) { \
+      found_count_in_list2[i] = 0; \
+      count_list1[i] = 0; \
+      for (j = 0; j < len1; j++) { \
+        if (list1[i] == list1[j]) \
+          count_list1[i] ++; \
+      } \
+    } \
+    for (j = 0; j < len2; j++) { \
+      guint count_list2 = 0; \
+      guint found_count_in_list1 = 0; \
+      GParamSpec *prop = list2[j]; \
+      for (i = 0; i < len2; i++) { \
+        if (list2[i] == prop) \
+          count_list2 ++; \
+      } \
+      for (i = 0; i < len1; i++) { \
+        if (list1[i] == prop) { \
+          found_count_in_list2[i] ++; \
+          found_count_in_list1 ++; \
+        } \
+      } \
+      fail_unless (found_count_in_list1 == count_list2, \
+          "Found property '%s' %u times, rather than %u times, in " #list1, \
+          prop->name, found_count_in_list1, count_list2); \
+    } \
+    /* make sure we found each one once */ \
+    for (i = 0; i < len1; i++) { \
+      GParamSpec *prop = list1[i]; \
+      fail_unless (found_count_in_list2[i] == count_list1[i], \
+          "Found property '%s' %u times, rather than %u times, in " #list2, \
+          prop->name, found_count_in_list2[i], count_list1[i]); \
+    } \
+    g_free (found_count_in_list2); \
+    g_free (count_list1); \
+  }
+
+
 void print_timeline(GESTimeline *timeline);
 
 #endif /* _GES_TEST_UTILS */