timeline-tree: simplify and fix editing
authorHenry Wilkes <hwilkes@igalia.com>
Mon, 27 Apr 2020 12:58:38 +0000 (13:58 +0100)
committerHenry Wilkes <hwilkes@igalia.com>
Thu, 7 May 2020 08:37:15 +0000 (09:37 +0100)
Editing has been simplified by breaking down each edit into a
combination of three basic single-element edits: MOVE, TRIM_START, and
TRIM_END.

Each edit follows these steps:
+ Determine which elements are to be edited and under which basic mode
+ Determine which track elements will move as a result
+ Snap the edit position to one of the edges of the main edited element,
  (or the edge of one of its descendants, in the case of MOVE), avoiding
  moving elements.
  NOTE: in particular, we can *not* snap to the edge of a neighbouring
  element in a roll edit. This was previously possible, even though the
  neighbour was moving!
+ Determine the edit positions for clips (or track elements with no
  parent) using the snapped value. In addition, we replace any edits of
  a group with an edit of its descendant clips. If any value would be
  out of bounds (e.g. negative start) we do not edit.
  NOTE: this is now done *after* checking the snapping. This allows the
  edit to succeed if snapping would cause it to go from being invalid to
  valid!
+ Determine whether the collection of edits would result in a valid
  timeline-configuration which does not break the rules for sources
  overlapping.
+ If all this succeeds, we emit snapping-started on the timeline.
+ We then perform all the edits. At this point they should all succeed.

The simplification/unification should make it easier to make other
changes.

Fixes https://gitlab.freedesktop.org/gstreamer/gst-editing-services/-/issues/97
Fixes https://gitlab.freedesktop.org/gstreamer/gst-editing-services/-/issues/98

Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-editing-services/-/merge_requests/169>

14 files changed:
ges/ges-clip.c
ges/ges-enums.c
ges/ges-enums.h
ges/ges-group.c
ges/ges-internal.h
ges/ges-timeline-element.c
ges/ges-timeline-tree.c
ges/ges-timeline-tree.h
ges/ges-timeline.c
ges/ges-track.c
tests/check/ges/layer.c
tests/check/ges/timelineedition.c
tests/check/python/test_timeline.py
tests/check/scenarios/check_edit_in_frames_with_framerate_mismatch.scenario

index 1b36a2e21ddd32653fc3f0f03b73be137ec49bfe..3b4f81462ca66982b21fe02a76fb51e2465a9ce1 100644 (file)
@@ -296,7 +296,8 @@ _update_duration_limit (GESClip * self)
     GST_INFO_OBJECT (self, "duration-limit for the clip is %"
         GST_TIME_FORMAT, GST_TIME_ARGS (duration_limit));
 
-    if (_CLOCK_TIME_IS_LESS (duration_limit, element->duration)) {
+    if (_CLOCK_TIME_IS_LESS (duration_limit, element->duration)
+        && !ELEMENT_FLAG_IS_SET (self, GES_TIMELINE_ELEMENT_SET_SIMPLE)) {
       gboolean res;
 
       GST_INFO_OBJECT (self, "Automatically reducing duration to %"
@@ -1849,11 +1850,12 @@ ges_clip_move_to_layer (GESClip * clip, GESLayer * layer)
 {
   gboolean ret = FALSE;
   GESLayer *current_layer;
-  GESTimeline *current_timeline = GES_TIMELINE_ELEMENT_TIMELINE (clip);
+  GESTimelineElement *element;
 
   g_return_val_if_fail (GES_IS_CLIP (clip), FALSE);
   g_return_val_if_fail (GES_IS_LAYER (layer), FALSE);
 
+  element = GES_TIMELINE_ELEMENT (clip);
   current_layer = clip->priv->layer;
 
   if (current_layer == layer) {
@@ -1861,39 +1863,34 @@ ges_clip_move_to_layer (GESClip * clip, GESLayer * layer)
     return TRUE;
   }
 
-  gst_object_ref (clip);
 
   if (current_layer == NULL) {
     GST_DEBUG ("Not moving %p, only adding it to %p", clip, layer);
 
-    ret = ges_layer_add_clip (layer, clip);
-    goto done;
+    return ges_layer_add_clip (layer, clip);
   }
 
-  ELEMENT_SET_FLAG (clip, GES_CLIP_IS_MOVING);
-
-  if (current_timeline != layer->timeline) {
+  if (element->timeline != layer->timeline) {
     /* make sure we can perform the can_move_element_check in the timeline
      * of the layer */
     GST_WARNING_OBJECT (layer, "Cannot move clip %" GES_FORMAT " into "
         "the layer because its timeline %" GST_PTR_FORMAT " does not "
         "match the timeline of the layer %" GST_PTR_FORMAT,
-        GES_ARGS (clip), current_timeline, layer->timeline);
-    ret = FALSE;
-    goto done;
+        GES_ARGS (clip), element->timeline, layer->timeline);
+    return FALSE;
   }
 
   if (layer->timeline
-      && !timeline_tree_can_move_element (timeline_get_tree (layer->timeline),
-          GES_TIMELINE_ELEMENT (clip),
-          ges_layer_get_priority (layer),
-          GES_TIMELINE_ELEMENT_START (clip),
-          GES_TIMELINE_ELEMENT_DURATION (clip), NULL)) {
-    GST_INFO_OBJECT (layer, "Clip %" GES_FORMAT " can't move to layer %d",
-        GES_ARGS (clip), ges_layer_get_priority (layer));
-    goto done;
+      && !ELEMENT_FLAG_IS_SET (clip, GES_TIMELINE_ELEMENT_SET_SIMPLE)) {
+    /* move to new layer, also checks moving of toplevel */
+    return timeline_tree_move (timeline_get_tree (layer->timeline),
+        element, (gint64) ges_layer_get_priority (current_layer) -
+        (gint64) ges_layer_get_priority (layer), 0, GES_EDGE_NONE, 0);
   }
 
+  gst_object_ref (clip);
+  ELEMENT_SET_FLAG (clip, GES_CLIP_IS_MOVING);
+
   GST_DEBUG_OBJECT (clip, "moving to layer %p, priority: %d", layer,
       ges_layer_get_priority (layer));
 
@@ -2250,7 +2247,7 @@ ges_clip_split (GESClip * clip, guint64 position)
   if (timeline && !timeline_tree_can_move_element (timeline_get_tree
           (timeline), element,
           ges_timeline_element_get_layer_priority (element),
-          start, old_duration, NULL)) {
+          start, old_duration)) {
     GST_WARNING_OBJECT (clip,
         "Can not split %" GES_FORMAT " at %" GST_TIME_FORMAT
         " as timeline would be in an illegal" " state.", GES_ARGS (clip),
@@ -2262,7 +2259,7 @@ ges_clip_split (GESClip * clip, guint64 position)
   if (timeline && !timeline_tree_can_move_element (timeline_get_tree
           (timeline), element,
           ges_timeline_element_get_layer_priority (element),
-          position, new_duration, NULL)) {
+          position, new_duration)) {
     GST_WARNING_OBJECT (clip,
         "Can not split %" GES_FORMAT " at %" GST_TIME_FORMAT
         " as timeline would end up in an illegal" " state.", GES_ARGS (clip),
index fe261cacdc70ae6daa0b14fd22e7ca7ddb04d7c8..8a684161146af979fb9c4df31baa660d0fe1dced 100644 (file)
@@ -144,6 +144,25 @@ register_ges_edit_mode (GType * id)
   *id = g_enum_register_static ("GESEditMode", edit_mode);
 }
 
+const gchar *
+ges_edit_mode_name (GESEditMode mode)
+{
+  switch (mode) {
+    case GES_EDIT_MODE_NORMAL:
+      return "normal";
+    case GES_EDIT_MODE_RIPPLE:
+      return "ripple";
+    case GES_EDIT_MODE_ROLL:
+      return "roll";
+    case GES_EDIT_MODE_TRIM:
+      return "trim";
+    case GES_EDIT_MODE_SLIDE:
+      return "slide";
+    default:
+      return "unknown";
+  }
+}
+
 GType
 ges_edit_mode_get_type (void)
 {
index 5cbf40d3f344ec675b2450c757c79b7deb9d01f0..554eabfb12896124feab5bb716de243fa5992bf9 100644 (file)
@@ -365,73 +365,150 @@ GType ges_pipeline_flags_get_type (void);
 /**
  * GESEditMode:
  * @GES_EDIT_MODE_NORMAL: The element is edited the normal way (default).
- *  This only moves a single element. If acting on the start edge of the
- *  element, the element's start time is set to the edit position.
- *  If acting on end edge of the element, the element's duration time
- *  is set such that its end time matches the edit position.
- * @GES_EDIT_MODE_RIPPLE: The element is edited in ripple mode. This
- *  shifts the element and all later elements (those with equal or later
- *  start times) in the timeline by the same amount. If acting on the
- *  element as a whole, the element's start time is shifted to the edit
- *  position, and later elements are also shifted by the same amount. If
- *  acting on the end edge of the element, the element's **duration time**
- *  is shifted so that the element's end time matches the edit position,
- *  and later elements have their **start time** shifted by the same
- *  amount.
- * @GES_EDIT_MODE_ROLL: The element is edited in roll mode. This trims
- *  the edge of an element and neighbouring edges (opposite edges of other
- *  elements in the timeline with the same corresponding time value), such
- *  that the edges remain in contact. If acting on the start edge of the
- *  element, the start edge is trimmed to the edit position (see
- *  #GES_EDIT_MODE_TRIM), and any other elements in the timeline whose end
- *  time matches the edited element's start time (evaluated before the
- *  edit) will have their **end** edge trimmed to the same edit position.
- *  Similarly, if acting on the end edge of the element, the end edge is
- *  trimmed to the edit position, and any other elements in the timeline
- *  whose start time matches the edited element's end time will have
- *  their start edge trimmed to the same edit position.
- * @GES_EDIT_MODE_TRIM: The element is edited in trim mode. This shifts
- *  the edge of a single element while maintaining the timing of
- *  its internal content in the timeline, so the samples/frames/etc of a
- *  source would still appear at the same timeline time when it is played.
- *  If acting on the start edge of the element, the element's start time
- *  will be shifted to the edit position and the element's in-point time
- *  will be shifted by the same amount. Additionally, the element's
- *  duration time will be shifted the other way such that the element's
- *  end time remains the same. If acting on end edge of the element, the
- *  element's duration time is set such that its end time matches the edit
- *  position.
+ *  If acting on the element as a whole (#GES_EDGE_NONE), this will MOVE
+ *  the element by MOVING its toplevel. When acting on the start of the
+ *  element (#GES_EDGE_START), this will only MOVE the element, but not
+ *  its toplevel parent. This can allow you to move a #GESClip or
+ *  #GESGroup to a new start time or layer within its container group,
+ *  without effecting other members of the group. When acting on the end
+ *  of the element (#GES_EDGE_END), this will END-TRIM the element,
+ *  leaving its toplevel unchanged.
+ * @GES_EDIT_MODE_RIPPLE: The element is edited in ripple mode: moving
+ *  itself as well as later elements, keeping their relative times. This
+ *  edits the element the same as #GES_EDIT_MODE_NORMAL. In addition, if
+ *  acting on the element as a whole, or the start of the element, any
+ *  toplevel element in the same timeline (including different layers)
+ *  whose start time is later than the *current* start time of the MOVED
+ *  element will also be MOVED by the same shift as the edited element.
+ *  If acting on the end of the element, any toplevel element whose start
+ *  time is later than the *current* end time of the edited element will
+ *  also be MOVED by the same shift as the change in the end of the
+ *  edited element. These additional elements will also be shifted by
+ *  the same shift in layers as the edited element.
+ * @GES_EDIT_MODE_ROLL: The element is edited in roll mode: swapping its
+ *  content for its neighbour's, or vis versa, in the timeline output.
+ *  This edits the element the same as #GES_EDIT_MODE_TRIM. In addition,
+ *  any neighbours are also TRIMMED at their opposite edge to the same
+ *  timeline position. When acting on the start of the element, a
+ *  neighbour is any earlier element in the timeline whose end time
+ *  matches the *current* start time of the edited element. When acting on
+ *  the end of the element, a neighbour is any later element in the
+ *  timeline whose start time matches the *current* start time of the
+ *  edited element. In addition, a neighbour have a #GESSource at its
+ *  end/start edge that shares a track with a #GESSource at the start/end
+ *  edge of the edited element. Basically, a neighbour is an element that
+ *  can be extended, or cut, to have its content replace, or be replaced
+ *  by, the content of the edited element. Acting on the element as a
+ *  whole (#GES_EDGE_NONE) is not defined. The element can not shift
+ *  layers under this mode.
+ * @GES_EDIT_MODE_TRIM: The element is edited in trim mode. When acting
+ *  on the start of the element, this will START-TRIM it. When acting on
+ *  the end of the element, this will END-TRIM it. Acting on the element
+ *  as a whole (#GES_EDGE_NONE) is not defined.
  * @GES_EDIT_MODE_SLIDE: The element is edited in slide mode (not yet
- *  implemented). This shifts the element and will trim the edges of
- *  neighbouring edges on either side accordingly. If acting on the
- *  element as a whole, the element's start time is shifted to the edit
- *  position. Any other elements in the timeline whose end time matches
- *  the edited element's start time (evaluated before the edit) will have
- *  their end edge trimmed to the same edit position. Additionally, any
- *  other elements in the timeline whose start time matches the edited
- *  element's end time will have their start edge trimmed to match the
- *  edited element's **new** end time.
+ *  implemented): moving the element replacing or consuming content on
+ *  each end. When acting on the element as a whole, this will MOVE the
+ *  element, and TRIM any neighbours on either side. A neighbour is
+ *  defined in the same way as in #GES_EDIT_MODE_ROLL, but they may be on
+ *  either side of the edited elements. Elements at the end with be
+ *  START-TRIMMED to the new end position of the edited element. Elements
+ *  at the start will be END-TRIMMED to the new start position of the
+ *  edited element. Acting on the start or end of the element
+ *  (#GES_EDGE_START and #GES_EDGE_END) is not defined. The element can
+ *  not shift layers under this mode.
  *
- * When a single timeline element is edited within its timeline, using
- * ges_timeline_element_edit(), depending on the edit mode, its
- * #GESTimelineElement:start, #GESTimelineElement:duration or
+ * When a single timeline element is edited within its timeline at some
+ * position, using ges_timeline_element_edit(), depending on the edit
+ * mode, its #GESTimelineElement:start, #GESTimelineElement:duration or
  * #GESTimelineElement:in-point will be adjusted accordingly. In addition,
- * other elements in the timeline may also have their properties adjusted.
+ * any clips may change #GESClip:layer.
  *
- * In fact, the edit is actually performed on the toplevel of the edited
- * element (usually a #GESClip), which is responsible for moving its
- * children along with it. For simplicity, in the descriptions we will
- * use "element" to exclusively refer to toplevel elements.
+ * Each edit can be broken down into a combination of three basic edits:
  *
- * In the edit mode descriptions, the "start edge", "end edge" and the
- * "element as a whole" correspond to using #GES_EDGE_START, #GES_EDGE_END
- * and #GES_EDGE_NONE as part of the edit, respectively. The "start time",
- * "duration time" and "in-point time" correspond to the
- * #GESTimelineElement:start, #GESTimelineElement:duration and
- * #GESTimelineElement:in-point properties, respectively. Moreover, the
- * "end time" refers to the final time of the element:
- * #GESTimelineElement:start + #GESTimelineElement:duration. Finally,
- * the "edit position" is the timeline time used as part of the edit.
+ * + MOVE: This moves the start of the element to the edit position.
+ * + START-TRIM: This cuts or grows the start of the element, whilst
+ *   maintaining the time at which its internal content appears in the
+ *   timeline data output. If the element is made shorter, the data that
+ *   appeared at the edit position and later will still appear in the
+ *   timeline at the same time. If the element is made longer, the data
+ *   that appeared at the previous start of the element and later will
+ *   still appear in the timeline at the same time.
+ * + END-TRIM: Similar to START-TRIM, but the end of the element is cut or
+ *   grown.
+ *
+ * In particular, when editing a #GESClip:
+ *
+ * + MOVE: This will set the #GESTimelineElement:start of the clip to the
+ *   edit position.
+ * + START-TRIM: This will set the #GESTimelineElement:start of the clip
+ *   to the edit position. To keep the end time the same, the
+ *   #GESTimelineElement:duration of the clip will be adjusted in the
+ *   opposite direction. In addition, the #GESTimelineElement:in-point of
+ *   the clip will be shifted such that the content that appeared at the
+ *   new or previous start time, whichever is latest, still appears at the
+ *   same timeline time. For example, if a frame appeared at the start of
+ *   the clip, and the start of the clip is reduced, the in-point of the
+ *   clip will also reduce such that the frame will appear later within
+ *   the clip, but at the same timeline position.
+ * + END-TRIM: This will set the #GESTimelineElement:duration of the clip
+ *   such that its end time will match the edit position.
+ *
+ * When editing a #GESGroup:
+ *
+ * + MOVE: This will set the #GESGroup:start of the clip to the edit
+ *   position by shifting all of its children by the same amount. So each
+ *   child will maintain their relative positions.
+ * + START-TRIM: If the group is made shorter, this will START-TRIM any
+ *   clips under the group that start after the edit position to the same
+ *   edit position. If the group is made longer, this will START-TRIM any
+ *   clip under the group whose start matches the start of the group to
+ *   the same edit position.
+ * + END-TRIM: If the group is made shorter, this will END-TRIM any clips
+ *   under the group that end after the edit position to the same edit
+ *   position. If the group is made longer, this will END-TRIM any clip
+ *   under the group whose end matches the end of the group to the same
+ *   edit position.
+ *
+ * When editing a #GESTrackElement, if it has a #GESClip parent, this
+ * will be edited instead. Otherwise it is edited in the same way as a
+ * #GESClip.
+ *
+ * The layer priority of a #GESGroup is the lowest layer priority of any
+ * #GESClip underneath it. When a group is edited to a new layer
+ * priority, it will shift all clips underneath it by the same amount,
+ * such that their relative layers stay the same.
+ *
+ * If the #GESTimeline has a #GESTimeline:snapping-distance, then snapping
+ * may occur for some of the edges of the **main** edited element:
+ *
+ * + MOVE: The start or end edge of *any* #GESSource under the element may
+ *   be snapped.
+ * + START-TRIM: The start edge of a #GESSource whose start edge touches
+ *   the start edge of the element may snap.
+ * + END-TRIM: The end edge of a #GESSource whose end edge touches the end
+ *   edge of the element may snap.
+ *
+ * These edges may snap with either the start or end edge of *any* other
+ * #GESSource in the timeline that is not also being moved by the element,
+ * including those in different layers, if they are within the
+ * #GESTimeline:snapping-distance. During an edit, only up to one snap can
+ * occur. This will shift the edit position such that the snapped edges
+ * will touch once the edit has completed.
+ *
+ * Note that snapping can cause an edit to fail where it would have
+ * otherwise succeeded because it may push the edit position such that the
+ * edit would result in an unsupported timeline configuration. Similarly,
+ * snapping can cause an edit to succeed where it would have otherwise
+ * failed.
+ *
+ * For example, in #GES_EDIT_MODE_RIPPLE acting on #GES_EDGE_NONE, the
+ * main element is the MOVED toplevel of the edited element. Any source
+ * under the main MOVED toplevel may have its start or end edge snapped.
+ * Note, these sources cannot snap with each other. The edit may also
+ * push other elements, but any sources under these elements cannot snap,
+ * nor can they be snapped with. If a snap does occur, the MOVE of the
+ * toplevel *and* all other elements pushed by the ripple will be shifted
+ * by the same amount such that the snapped edges will touch.
  *
  * You can also find more explanation about the behaviour of those modes at:
  * [trim, ripple and roll](http://pitivi.org/manual/trimming.html)
@@ -445,6 +522,9 @@ typedef enum {
     GES_EDIT_MODE_SLIDE
 } GESEditMode;
 
+GES_API
+const gchar * ges_edit_mode_name (GESEditMode mode);
+
 #define GES_TYPE_EDIT_MODE ges_edit_mode_get_type()
 
 GES_API
index 3fb622a49d8a2f1732dc177a73a8e13ab4fcd328..589bb4bb36690161ef92be9244b25df431640894 100644 (file)
@@ -436,12 +436,6 @@ _set_duration (GESTimelineElement * element, GstClockTime duration)
     return GES_TIMELINE_ELEMENT_CLASS (parent_class)->set_duration (element,
         duration);
 
-  if (element->timeline
-      && !timeline_tree_can_move_element (timeline_get_tree (element->timeline),
-          element, _PRIORITY (element), element->start, duration, NULL)) {
-    return FALSE;
-  }
-
   if (container->initiated_move == NULL) {
     gboolean expending = (_DURATION (element) < duration);
 
index d7898dec578fb237b2d9f6344856e25423b31857..64643a4c7ebb93e215c05916d6acbc42611cbace 100644 (file)
@@ -88,37 +88,11 @@ GstDebugCategory * _ges_debug (void);
 
 #define SUPRESS_UNUSED_WARNING(a) (void)a
 
-G_GNUC_INTERNAL gboolean
-timeline_ripple_object         (GESTimeline *timeline, GESTimelineElement *obj,
-                                gint new_layer_priority,
-                                GList * layers, GESEdge edge,
-                                guint64 position);
-
-G_GNUC_INTERNAL gboolean
-timeline_slide_object          (GESTimeline *timeline, GESTrackElement *obj,
-                                    GList * layers, GESEdge edge, guint64 position);
-
-G_GNUC_INTERNAL gboolean
-timeline_roll_object           (GESTimeline *timeline, GESTimelineElement *obj,
-                                GList * layers, GESEdge edge, guint64 position);
-
-G_GNUC_INTERNAL gboolean
-timeline_trim_object           (GESTimeline *timeline, GESTimelineElement * object,
-                                guint32 new_layer_priority, GList * layers, GESEdge edge,
-                                guint64 position);
-G_GNUC_INTERNAL gboolean
-ges_timeline_trim_object_simple (GESTimeline * timeline, GESTimelineElement * obj,
-                                 guint32 new_layer_priority, GList * layers, GESEdge edge,
-                                 guint64 position, gboolean snapping);
-
-G_GNUC_INTERNAL gboolean
-ges_timeline_move_object_simple (GESTimeline * timeline, GESTimelineElement * object,
-                                 GList * layers, GESEdge edge, guint64 position);
 
 G_GNUC_INTERNAL gboolean
-timeline_move_object           (GESTimeline *timeline, GESTimelineElement * object,
-                                guint32 new_layer_priority, GList * layers, GESEdge edge,
-                                guint64 position);
+ges_timeline_edit (GESTimeline * timeline, GESTimelineElement * element,
+    GList * layers, gint64 new_layer_priority, GESEditMode mode, GESEdge edge,
+    guint64 position);
 
 G_GNUC_INTERNAL void
 timeline_add_group             (GESTimeline *timeline,
index 3498d8fca2685c54155f51323b1931d2394aa05f..a456f2a3c2fc1c215a36bd1e7de8ba41dc8d725b 100644 (file)
@@ -1045,21 +1045,21 @@ ges_timeline_element_get_timeline (GESTimelineElement * self)
  * @self: A #GESTimelineElement
  * @start: The desired start position of the element in its timeline
  *
- * Sets #GESTimelineElement:start for the element. This may fail if it
+ * Sets #GESTimelineElement:start for the element. If the element has a
+ * parent, this will also move its siblings with the same shift.
+ *
+ * Whilst the element is part of a #GESTimeline, this is the same as
+ * editing the element with ges_timeline_element_edit() under
+ * #GES_EDIT_MODE_NORMAL with #GES_EDGE_NONE. In particular, the
+ * #GESTimelineElement:start of the element may be snapped to a different
+ * timeline time from the one given. In addition, setting may fail if it
  * would place the timeline in an unsupported configuration.
  *
- * Note that if the element's timeline has a
- * #GESTimeline:snapping-distance set, then the element's
- * #GESTimelineElement:start may instead be set to the edge of some other
- * element in the neighbourhood of @start. In such a case, the return
- * value will still be %TRUE on success.
- *
  * Returns: %TRUE if @start could be set for @self.
  */
 gboolean
 ges_timeline_element_set_start (GESTimelineElement * self, GstClockTime start)
 {
-  gboolean emit_notify = TRUE;
   GESTimelineElementClass *klass;
   GESTimelineElement *toplevel_container, *parent;
 
@@ -1078,18 +1078,18 @@ ges_timeline_element_set_start (GESTimelineElement * self, GstClockTime start)
       && !ELEMENT_FLAG_IS_SET (self, GES_TIMELINE_ELEMENT_SET_SIMPLE)
       && !ELEMENT_FLAG_IS_SET (toplevel_container,
           GES_TIMELINE_ELEMENT_SET_SIMPLE)) {
-    if (!ges_timeline_move_object_simple (self->timeline, self, NULL,
-            GES_EDGE_NONE, start)) {
-      gst_object_unref (toplevel_container);
-      return FALSE;
-    }
 
-    emit_notify = FALSE;
+    gst_object_unref (toplevel_container);
+
+    return ges_timeline_element_edit (self, NULL, -1, GES_EDIT_MODE_NORMAL,
+        GES_EDGE_NONE, start);
   }
   parent = self->parent;
 
   /* FIXME This should not belong to GESTimelineElement */
-  if (toplevel_container &&
+  /* only check if no timeline, otherwise the timeline-tree will handle this
+   * check */
+  if (!self->timeline && toplevel_container &&
       ((gint64) (_START (toplevel_container) + start - _START (self))) < 0 &&
       parent
       && GES_CONTAINER (parent)->children_control_mode == GES_CHILDREN_UPDATE) {
@@ -1107,7 +1107,7 @@ ges_timeline_element_set_start (GESTimelineElement * self, GstClockTime start)
     gint res = klass->set_start (self, start);
     if (res == FALSE)
       return FALSE;
-    if (res == TRUE && emit_notify) {
+    if (res == TRUE) {
       self->start = start;
       g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_START]);
     }
@@ -1238,17 +1238,16 @@ ges_timeline_element_set_max_duration (GESTimelineElement * self,
  * @self: A #GESTimelineElement
  * @duration: The desired duration in its timeline
  *
- * Sets #GESTimelineElement:duration for the element. This may fail if it
- * would place the timeline in an unsupported configuration, or if the
- * element does not have enough internal content to last for the desired
- * @duration.
+ * Sets #GESTimelineElement:duration for the element.
  *
- * Note that if the element's timeline has a
- * #GESTimeline:snapping-distance set, then the element's
- * #GESTimelineElement:duration may instead be adjusted around @duration
- * such that the edge of @self matches the edge of some other element in
- * the neighbourhood. In such a case, the return value will still be %TRUE
- * on success.
+ * Whilst the element is part of a #GESTimeline, this is the same as
+ * editing the element with ges_timeline_element_edit() under
+ * #GES_EDIT_MODE_TRIM with #GES_EDGE_END. In particular, the
+ * #GESTimelineElement:duration of the element may be snapped to a
+ * different timeline time difference from the one given. In addition,
+ * setting may fail if it would place the timeline in an unsupported
+ * configuration, or the element does not have enough internal content to
+ * last the desired duration.
  *
  * Returns: %TRUE if @duration could be set for @self.
  */
@@ -1257,7 +1256,6 @@ ges_timeline_element_set_duration (GESTimelineElement * self,
     GstClockTime duration)
 {
   GESTimelineElementClass *klass;
-  gboolean emit_notify = TRUE;
   GESTimelineElement *toplevel;
 
   g_return_val_if_fail (GES_IS_TIMELINE_ELEMENT (self), FALSE);
@@ -1269,19 +1267,9 @@ ges_timeline_element_set_duration (GESTimelineElement * self,
   if (self->timeline &&
       !ELEMENT_FLAG_IS_SET (self, GES_TIMELINE_ELEMENT_SET_SIMPLE) &&
       !ELEMENT_FLAG_IS_SET (toplevel, GES_TIMELINE_ELEMENT_SET_SIMPLE)) {
-    gboolean res;
-
-    res = timeline_trim_object (self->timeline, self,
-        GES_TIMELINE_ELEMENT_LAYER_PRIORITY (self), NULL,
+    gst_object_unref (toplevel);
+    return ges_timeline_element_edit (self, NULL, -1, GES_EDIT_MODE_TRIM,
         GES_EDGE_END, self->start + duration);
-
-    if (!res) {
-      gst_object_unref (toplevel);
-
-      return FALSE;
-    }
-
-    emit_notify = res == -1;
   }
   gst_object_unref (toplevel);
 
@@ -1295,7 +1283,7 @@ ges_timeline_element_set_duration (GESTimelineElement * self,
     gint res = klass->set_duration (self, duration);
     if (res == FALSE)
       return FALSE;
-    if (res == TRUE && emit_notify) {
+    if (res == TRUE) {
       self->duration = duration;
       g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_DURATION]);
     }
@@ -2428,9 +2416,15 @@ ges_timeline_element_get_layer_priority (GESTimelineElement * self)
  * out of bounds, or if it would place the timeline in an unsupported
  * configuration.
  *
+ * Note that if you act on a #GESTrackElement, this will edit its parent
+ * #GESClip instead. Moreover, for any #GESTimelineElement, if you select
+ * #GES_EDGE_NONE for #GES_EDIT_MODE_NORMAL or #GES_EDIT_MODE_RIPPLE, this
+ * will edit the toplevel instead, but still in such a way as to make the
+ * #GESTimelineElement:start of @self reach the edit @position.
+ *
  * Note that if the element's timeline has a
- * #GESTimeline:snapping-distance set, then the edit position may be set
- * to the edge of some element in the neighbourhood of @position.
+ * #GESTimeline:snapping-distance set, then the edit position may be
+ * snapped to the edge of some element under the edited element.
  *
  * @new_layer_priority can be used to switch @self, and other elements
  * moved by the edit, to a new layer. New layers may be be created if the
@@ -2455,35 +2449,26 @@ ges_timeline_element_edit (GESTimelineElement * self, GList * layers,
     gint64 new_layer_priority, GESEditMode mode, GESEdge edge, guint64 position)
 {
   GESTimeline *timeline;
+  guint32 layer_prio;
 
   g_return_val_if_fail (GES_IS_TIMELINE_ELEMENT (self), FALSE);
   g_return_val_if_fail (GST_CLOCK_TIME_IS_VALID (position), FALSE);
 
   timeline = GES_TIMELINE_ELEMENT_TIMELINE (self);
-  /* FIXME: handle a NULL timeline! */
-  switch (mode) {
-    case GES_EDIT_MODE_RIPPLE:
-      return timeline_ripple_object (timeline, self,
-          new_layer_priority <
-          0 ? GES_TIMELINE_ELEMENT_LAYER_PRIORITY (self) :
-          new_layer_priority, layers, edge, position);
-    case GES_EDIT_MODE_TRIM:
-      return timeline_trim_object (timeline, self,
-          new_layer_priority <
-          0 ? GES_TIMELINE_ELEMENT_LAYER_PRIORITY (self) :
-          new_layer_priority, layers, edge, position);
-    case GES_EDIT_MODE_NORMAL:
-      return timeline_move_object (timeline, self,
-          new_layer_priority <
-          0 ? GES_TIMELINE_ELEMENT_LAYER_PRIORITY (self) :
-          new_layer_priority, layers, edge, position);
-    case GES_EDIT_MODE_ROLL:
-      return timeline_roll_object (timeline, self, layers, edge, position);
-    case GES_EDIT_MODE_SLIDE:
-      GST_ERROR_OBJECT (self, "Sliding not implemented.");
-      return FALSE;
-  }
-  return FALSE;
+  g_return_val_if_fail (timeline, FALSE);
+
+  layer_prio = GES_TIMELINE_ELEMENT_LAYER_PRIORITY (self);
+
+  if (new_layer_priority < 0)
+    new_layer_priority = layer_prio;
+
+  GST_DEBUG_OBJECT (self, "Editing %s at edge %s to position %"
+      GST_TIME_FORMAT " under %s mode, and to layer %" G_GINT64_FORMAT,
+      self->name, ges_edge_name (edge), GST_TIME_ARGS (position),
+      ges_edit_mode_name (mode), new_layer_priority);
+
+  return ges_timeline_edit (timeline, self, layers, new_layer_priority, mode,
+      edge, position);
 }
 
 /**
index d0a03d689ca801adc246051459988734dcafbda5..dd681419752c822388988f653dd193ff174747a7 100644 (file)
@@ -29,20 +29,50 @@ GST_DEBUG_CATEGORY_STATIC (tree_debug);
 #undef GST_CAT_DEFAULT
 #define GST_CAT_DEFAULT tree_debug
 
-#define ELEMENT_EDGE_VALUE(e, edge) ((edge == GES_EDGE_END) ? ((GstClockTimeDiff) _END (e)) : ((GstClockTimeDiff) _START (e)))
-typedef struct
+#define ELEMENT_EDGE_VALUE(e, edge) ((edge == GES_EDGE_END) ? _END (e) : _START (e))
+
+typedef struct _SnappedPosition
 {
+  /* the element that was being snapped */
+  GESTrackElement *element;
+  /* the position of element, and whether it is a negative position */
+  gboolean negative;
+  GstClockTime position;
+  /* the element that was snapped to */
+  GESTrackElement *snapped_to;
+  /* the snapped positioned */
+  GstClockTime snapped;
+  /* the distance below which two elements can snap */
   GstClockTime distance;
-  gboolean on_end_only;
-  gboolean on_start_only;
+} SnappedPosition;
 
-  GESEdge edge;
-  GESTimelineElement *element;
+typedef enum
+{
+  EDIT_MOVE,
+  EDIT_TRIM_START,
+  EDIT_TRIM_END,
+} ElementEditMode;
 
-  GESTimelineElement *moving_element;
-  GESEdge moving_edge;
-  GstClockTimeDiff diff;
-} SnappingData;
+typedef struct _EditData
+{
+  /* offsets to use */
+  GstClockTime offset;
+  gint64 layer_offset;
+  /* actual values */
+  GstClockTime duration;
+  GstClockTime start;
+  GstClockTime inpoint;
+  guint32 layer_priority;
+  /* mode */
+  ElementEditMode mode;
+} EditData;
+
+typedef struct _PositionData
+{
+  guint32 layer_priority;
+  GstClockTime start;
+  GstClockTime end;
+} PositionData;
 
 /*  *INDENT-OFF* */
 struct _TreeIterationData
@@ -50,16 +80,13 @@ struct _TreeIterationData
   GNode *root;
   gboolean res;
 
-  GstClockTimeDiff start_diff;
-  GstClockTimeDiff inpoint_diff;
-  GstClockTimeDiff duration_diff;
-  gint64 priority_diff;
-
   /* The element we are visiting */
   GESTimelineElement *element;
+  /* the position data of the visited element */
+  PositionData *pos_data;
 
-  /* All the TrackElement currently moving */
-  GList *movings;
+  /* All the TrackElement currently moving: owned by data */
+  GHashTable *moving;
 
   /* Elements overlaping on the start/end of @element */
   GESTimelineElement *overlaping_on_start;
@@ -67,52 +94,78 @@ struct _TreeIterationData
   GstClockTime overlap_start_final_time;
   GstClockTime overlap_end_first_time;
 
-  /* Timestamp after which elements will be rippled */
-  GstClockTime ripple_time;
-
-  SnappingData *snapping;
+  SnappedPosition *snap;
+  GList *sources;
+  GstClockTime position;
+  GstClockTime negative;
 
-  /* The edge being trimmed or rippled */
   GESEdge edge;
-  GHashTable *moved_clips;
-
   GList *neighbours;
-
-  /* Data related to trimming groups */
-  GstClockTime trim_group_start;
-  GstClockTime trim_group_end;
 } tree_iteration_data_init = {
    .root = NULL,
    .res = TRUE,
-   .start_diff = 0,
-   .inpoint_diff = 0,
-   .duration_diff = 0,
-   .priority_diff = 0,
    .element = NULL,
-   .movings = NULL,
+   .pos_data = NULL,
+   .moving = NULL,
    .overlaping_on_start = NULL,
    .overlaping_on_end = NULL,
    .overlap_start_final_time = GST_CLOCK_TIME_NONE,
    .overlap_end_first_time = GST_CLOCK_TIME_NONE,
-   .ripple_time = GST_CLOCK_TIME_NONE,
-   .snapping = NULL,
+   .snap = NULL,
+   .sources = NULL,
+   .position = GST_CLOCK_TIME_NONE,
+   .negative = FALSE,
    .edge = GES_EDGE_NONE,
-   .moved_clips = NULL,
    .neighbours = NULL,
-   .trim_group_start = GST_CLOCK_TIME_NONE,
-   .trim_group_end = GST_CLOCK_TIME_NONE,
 };
 /*  *INDENT-ON* */
 
 typedef struct _TreeIterationData TreeIterationData;
 
-static void
-clean_iteration_data (TreeIterationData * data)
+static EditData *
+new_edit_data (ElementEditMode mode, GstClockTimeDiff offset,
+    gint64 layer_offset)
+{
+  EditData *data = g_new (EditData, 1);
+
+  data->start = GST_CLOCK_TIME_NONE;
+  data->duration = GST_CLOCK_TIME_NONE;
+  data->inpoint = GST_CLOCK_TIME_NONE;
+  data->layer_priority = GES_TIMELINE_ELEMENT_NO_LAYER_PRIORITY;
+
+  data->mode = mode;
+  data->offset = offset;
+  data->layer_offset = layer_offset;
+
+  return data;
+}
+
+static SnappedPosition *
+new_snapped_position (GstClockTime distance)
+{
+  SnappedPosition *snap;
+
+  if (distance == 0)
+    return NULL;
+
+  snap = g_new0 (SnappedPosition, 1);
+  snap->position = GST_CLOCK_TIME_NONE;
+  snap->snapped = GST_CLOCK_TIME_NONE;
+  snap->distance = distance;
+
+  return snap;
+}
+
+static GHashTable *
+new_edit_table ()
 {
-  g_list_free (data->neighbours);
-  g_list_free (data->movings);
-  if (data->moved_clips)
-    g_hash_table_unref (data->moved_clips);
+  return g_hash_table_new_full (NULL, NULL, NULL, g_free);
+}
+
+static GHashTable *
+new_position_table ()
+{
+  return g_hash_table_new_full (NULL, NULL, NULL, g_free);
 }
 
 void
@@ -157,16 +210,6 @@ get_toplevel_container (gpointer element)
   return ret;
 }
 
-static gboolean
-timeline_tree_can_move_element_internal (GNode * root,
-    GESTimelineElement * element,
-    gint64 priority,
-    GstClockTimeDiff start,
-    GstClockTimeDiff inpoint,
-    GstClockTimeDiff duration,
-    GList * moving_track_elements,
-    GstClockTime ripple_time, SnappingData * snapping, GESEdge edge);
-
 static GNode *
 find_node (GNode * root, gpointer element)
 {
@@ -257,231 +300,463 @@ timeline_tree_stop_tracking_element (GNode * root, GESTimelineElement * element)
   timeline_update_duration (root->data);
 }
 
-static inline gboolean
-check_can_move_to_layer (GESTimelineElement * element,
-    gint layer_priority_offset)
+/****************************************************
+ *     GstClockTime with over/underflow checking    *
+ ****************************************************/
+
+static GstClockTime
+_clock_time_plus (GstClockTime time, GstClockTime add)
 {
-  return (((gint64) GES_TIMELINE_ELEMENT_LAYER_PRIORITY (element) -
-          layer_priority_offset) >= 0);
+  if (!GST_CLOCK_TIME_IS_VALID (time) || !GST_CLOCK_TIME_IS_VALID (add))
+    return GST_CLOCK_TIME_NONE;
+
+  if (time >= (G_MAXUINT64 - add)) {
+    GST_INFO ("The time %" G_GUINT64_FORMAT " would overflow when "
+        "adding %" G_GUINT64_FORMAT, time, add);
+    return GST_CLOCK_TIME_NONE;
+  }
+  return time + add;
 }
 
-/*  *INDENT-OFF* */
-#define CHECK_AND_SNAP(diff_val,moving_edge_,edge_) \
-if (snapping->distance >= ABS(diff_val) && ABS(diff_val) <= ABS(snapping->diff)) { \
-  snapping->element = element; \
-  snapping->edge = edge_; \
-  snapping->moving_element = moving_elem; \
-  snapping->moving_edge = moving_edge_; \
-  snapping->diff = (diff_val); \
-  GST_LOG("Snapping %" GES_FORMAT "with %" GES_FORMAT " - diff: %" G_GINT64_FORMAT "", GES_ARGS (moving_elem), GES_ARGS(element), (diff_val)); \
+static GstClockTime
+_clock_time_minus (GstClockTime time, GstClockTime minus, gboolean * negative)
+{
+  if (negative)
+    *negative = FALSE;
+
+  if (!GST_CLOCK_TIME_IS_VALID (time) || !GST_CLOCK_TIME_IS_VALID (minus))
+    return GST_CLOCK_TIME_NONE;
+
+  if (time < minus) {
+    if (negative) {
+      *negative = TRUE;
+      return minus - time;
+    }
+    /* otherwise don't allow negative */
+    GST_INFO ("The time %" G_GUINT64_FORMAT " would underflow when "
+        "subtracting %" G_GUINT64_FORMAT, time, minus);
+    return GST_CLOCK_TIME_NONE;
+  }
+  return time - minus;
 }
 
-static void
-check_snapping (GESTimelineElement * element, GESTimelineElement * moving_elem,
-    SnappingData * snapping, GstClockTime start, GstClockTime end,
-    GstClockTime moving_start, GstClockTime moving_end)
+static GstClockTime
+_clock_time_minus_diff (GstClockTime time, GstClockTimeDiff diff,
+    gboolean * negative)
 {
-  GstClockTimeDiff snap_end_end_diff;
-  GstClockTimeDiff snap_end_start_diff;
+  if (negative)
+    *negative = FALSE;
 
-  if (element == moving_elem)
-    return;
+  if (!GST_CLOCK_TIME_IS_VALID (time))
+    return GST_CLOCK_TIME_NONE;
 
-  if (!snapping || (
-      GES_IS_CLIP (element->parent) && element->parent == moving_elem->parent))
-    return;
+  if (diff < 0)
+    return _clock_time_plus (time, -diff);
+  else
+    return _clock_time_minus (time, diff, negative);
+}
+
+static GstClockTime
+_abs_clock_time_distance (GstClockTime time1, GstClockTime time2)
+{
+  if (!GST_CLOCK_TIME_IS_VALID (time1) || !GST_CLOCK_TIME_IS_VALID (time2))
+    return GST_CLOCK_TIME_NONE;
+  if (time1 > time2)
+    return time1 - time2;
+  else
+    return time2 - time1;
+}
 
-  snap_end_end_diff = (GstClockTimeDiff) moving_end - (GstClockTimeDiff) end;
-  snap_end_start_diff = (GstClockTimeDiff) moving_end - (GstClockTimeDiff) start;
+static gboolean
+get_start_end_from_offset (GESTimelineElement * element, ElementEditMode mode,
+    GstClockTimeDiff offset, GstClockTime * start, gboolean * negative_start,
+    GstClockTime * end, gboolean * negative_end)
+{
+  GstClockTime current_end =
+      _clock_time_plus (element->start, element->duration);
+  GstClockTime new_start = GST_CLOCK_TIME_NONE, new_end = GST_CLOCK_TIME_NONE;
+
+  switch (mode) {
+    case EDIT_MOVE:
+      new_start =
+          _clock_time_minus_diff (element->start, offset, negative_start);
+      new_end = _clock_time_minus_diff (current_end, offset, negative_end);
+      break;
+    case EDIT_TRIM_START:
+      new_start =
+          _clock_time_minus_diff (element->start, offset, negative_start);
+      new_end = current_end;
+      if (negative_end)
+        *negative_end = FALSE;
+      break;
+    case EDIT_TRIM_END:
+      new_start = element->start;
+      if (negative_start)
+        *negative_start = FALSE;
+      new_end = _clock_time_minus_diff (current_end, offset, negative_end);
+      break;
+  }
+  if (start)
+    *start = new_start;
+  if (end)
+    *end = new_end;
+
+  if (start && !GST_CLOCK_TIME_IS_VALID (new_start)) {
+    GST_INFO_OBJECT (element, "Cannot edit element %" GES_FORMAT " with "
+        "offset %" G_GINT64_FORMAT " because it would result in an invalid "
+        "start", GES_ARGS (element), offset);
+    return FALSE;
+  }
 
-  GST_DEBUG("Moving [%" G_GINT64_FORMAT "-%" G_GINT64_FORMAT "] element [%" G_GINT64_FORMAT "-%" G_GINT64_FORMAT "]", moving_start, moving_end, start, end);
-  /* Prefer snapping end */
-  if (!snapping->on_start_only) {
-    CHECK_AND_SNAP(snap_end_end_diff, GES_EDGE_END, GES_EDGE_END)
-    else CHECK_AND_SNAP(snap_end_start_diff, GES_EDGE_END, GES_EDGE_START)
+  if (end && !GST_CLOCK_TIME_IS_VALID (new_end)) {
+    GST_INFO_OBJECT (element, "Cannot edit element %" GES_FORMAT " with "
+        "offset %" G_GINT64_FORMAT " because it would result in an invalid "
+        "end", GES_ARGS (element), offset);
+    return FALSE;
   }
 
-  if (!snapping->on_end_only) {
-    GstClockTimeDiff snap_start_end_diff = GST_CLOCK_DIFF(end, moving_start);
-    GstClockTimeDiff snap_start_start_diff = GST_CLOCK_DIFF(start, moving_start);
+  return TRUE;
+}
+
+/****************************************************
+ *                   Snapping                       *
+ ****************************************************/
 
-    CHECK_AND_SNAP(snap_start_end_diff, GES_EDGE_START, GES_EDGE_END)
-    else CHECK_AND_SNAP(snap_start_start_diff, GES_EDGE_START, GES_EDGE_START)
+static void
+snap_to_edge (GESTrackElement * element, GstClockTime position,
+    gboolean negative, GESTrackElement * snap_to, GESEdge edge,
+    SnappedPosition * snap)
+{
+  GstClockTime edge_pos = ELEMENT_EDGE_VALUE (snap_to, edge);
+  GstClockTime distance;
+
+  if (negative)
+    distance = _clock_time_plus (position, edge_pos);
+  else
+    distance = _abs_clock_time_distance (position, edge_pos);
+
+  if (GST_CLOCK_TIME_IS_VALID (distance) && distance <= snap->distance) {
+    GESTimelineElement *parent = GES_TIMELINE_ELEMENT_PARENT (element);
+    GESTimelineElement *snap_parent = GES_TIMELINE_ELEMENT_PARENT (snap_to);
+    GST_LOG_OBJECT (element, "%s (under %s) snapped with %" GES_FORMAT
+        "(under %s) from position %s%" GST_TIME_FORMAT " to %"
+        GST_TIME_FORMAT, GES_TIMELINE_ELEMENT_NAME (element),
+        parent ? parent->name : NULL, GES_ARGS (snap_to),
+        snap_parent ? snap_parent->name : NULL, negative ? "-" : "",
+        GST_TIME_ARGS (position), GST_TIME_ARGS (edge_pos));
+    snap->negative = negative;
+    snap->position = position;
+    snap->distance = distance;
+    snap->snapped = edge_pos;
+    snap->element = element;
+    snap->snapped_to = snap_to;
   }
 }
-#undef CHECK_AND_SNAP
-/*  *INDENT-ON* */
 
 static gboolean
-check_track_elements_overlaps_and_values (GNode * node,
-    TreeIterationData * data)
+find_snap (GNode * node, TreeIterationData * data)
 {
-  GESTimelineElement *e = (GESTimelineElement *) node->data;
-  GstClockTimeDiff moving_start, moving_end, start, inpoint, end, duration;
-  gint64 priority = ((gint64) GES_TIMELINE_ELEMENT_LAYER_PRIORITY (e));
-  gint64 moving_priority =
-      (gint64) GES_TIMELINE_ELEMENT_LAYER_PRIORITY (data->element) -
-      data->priority_diff;
+  GESTimelineElement *element = node->data;
+  GESTrackElement *track_el, *moving;
 
-  gboolean can_overlap = e != data->element, in_movings, rippling, moving;
+  /* Only snap to sources */
+  /* Maybe we should allow snapping to anything that isn't an
+   * auto-transition? */
+  if (!GES_IS_SOURCE (element))
+    return FALSE;
 
-  if (!GES_IS_SOURCE (e))
+  /* don't snap to anything we are moving */
+  if (g_hash_table_contains (data->moving, element))
     return FALSE;
 
-  in_movings = ! !g_list_find (data->movings, e);
-  rippling = e != data->element && !in_movings
-      && (GST_CLOCK_TIME_IS_VALID (data->ripple_time)
-      && e->start >= data->ripple_time);
-  moving = in_movings || rippling || e == data->element;
-
-  start = (GstClockTimeDiff) e->start;
-  inpoint = (GstClockTimeDiff) e->inpoint;
-  end = (GstClockTimeDiff) e->start + e->duration;
-  duration = e->duration;
-  moving_start = GST_CLOCK_DIFF (data->start_diff, data->element->start);
-  moving_end =
-      GST_CLOCK_DIFF (data->duration_diff,
-      moving_start + data->element->duration);
-
-  if (moving) {
-    if (rippling) {
-      if (data->edge == GES_EDGE_END) {
-        /* Moving as rippled from the end of a previous element */
-        start -= data->duration_diff;
-      } else
-        start -= data->start_diff;
-    } else {
-      start -= data->start_diff;
-      if (GES_TIMELINE_ELEMENT_GET_CLASS (e)->set_inpoint)
-        inpoint -= data->inpoint_diff;
-      duration -= data->duration_diff;
-    }
-    end = start + duration;
-    priority -= data->priority_diff;
+  track_el = GES_TRACK_ELEMENT (element);
+  moving = GES_TRACK_ELEMENT (data->element);
+  snap_to_edge (moving, data->position, data->negative, track_el,
+      GES_EDGE_END, data->snap);
+  snap_to_edge (moving, data->position, data->negative, track_el,
+      GES_EDGE_START, data->snap);
 
-    GST_DEBUG ("%s %" GES_FORMAT "to [%" G_GINT64_FORMAT "(%"
-        G_GINT64_FORMAT ") - %" G_GINT64_FORMAT "] - layer: %" G_GINT64_FORMAT,
-        rippling ? "Rippling" : "Moving", GES_ARGS (e), start, inpoint,
-        duration, priority);
-  }
+  return FALSE;
+}
+
+static void
+find_snap_for_element (GESTrackElement * element, GstClockTime position,
+    gboolean negative, TreeIterationData * data)
+{
+  data->element = GES_TIMELINE_ELEMENT (element);
+  data->position = position;
+  data->negative = negative;
+  g_node_traverse (data->root, G_IN_ORDER, G_TRAVERSE_LEAVES, -1,
+      (GNodeTraverseFunc) find_snap, data);
+}
+
+/* find up to one source at the edge */
+static gboolean
+find_source_at_edge (GNode * node, TreeIterationData * data)
+{
+  GESEdge edge = data->edge;
+  GESTimelineElement *element = node->data;
+  GESTimelineElement *ancestor = data->element;
+
+  if (!GES_IS_SOURCE (element))
+    return FALSE;
 
-  /* Not in the same track */
-  if (ges_track_element_get_track ((GESTrackElement *) node->data) !=
-      ges_track_element_get_track ((GESTrackElement *) data->element)) {
-    GST_LOG ("%" GES_FORMAT " and %" GES_FORMAT
-        " are not in the same track", GES_ARGS (node->data),
-        GES_ARGS (data->element));
-    can_overlap = FALSE;
+  if (ELEMENT_EDGE_VALUE (element, edge) == ELEMENT_EDGE_VALUE (ancestor, edge)) {
+    data->sources = g_list_append (data->sources, element);
+    return TRUE;
   }
+  return FALSE;
+}
+
+static gboolean
+find_sources (GNode * node, TreeIterationData * data)
+{
+  GESTimelineElement *element = node->data;
+  if (GES_IS_SOURCE (element))
+    data->sources = g_list_append (data->sources, element);
+  return FALSE;
+}
+
+/* Tries to find a new snap to the start or end edge of one of the
+ * descendant sources of @element, depending on @mode, and updates @offset
+ * by the size of the jump.
+ * Any elements in @moving are not snapped to.
+ */
+static gboolean
+timeline_tree_snap (GNode * root, GESTimelineElement * element,
+    ElementEditMode mode, GstClockTimeDiff * offset, GHashTable * moving,
+    SnappedPosition * snap)
+{
+  gboolean ret = FALSE;
+  TreeIterationData data = tree_iteration_data_init;
+  GList *tmp;
+  GNode *node;
 
-  /* Not in the same layer */
-  if (priority != moving_priority) {
-    GST_LOG ("%" GST_PTR_FORMAT " and %" GST_PTR_FORMAT
-        " are not on the same layer (%d != %" G_GINT64_FORMAT ")", node->data,
-        data->element, GES_TIMELINE_ELEMENT_LAYER_PRIORITY (e),
-        moving_priority);
-    can_overlap = FALSE;
+  if (!snap)
+    return TRUE;
+
+  /* get the sources we can snap to */
+  data.root = root;
+  data.moving = moving;
+  data.sources = NULL;
+  data.snap = snap;
+  data.element = element;
+
+  node = find_node (root, element);
+
+  if (!node) {
+    GST_ERROR_OBJECT (element, "Not being tracked");
+    goto done;
   }
 
-  if (start < 0) {
-    GST_INFO ("%" GES_FORMAT "start would be %" G_GINT64_FORMAT " < 0",
-        GES_ARGS (e), start);
-    goto error;
+  switch (mode) {
+    case EDIT_MOVE:
+      /* can snap with any source below the element, if any */
+      g_node_traverse (node, G_IN_ORDER, G_TRAVERSE_LEAVES, -1,
+          (GNodeTraverseFunc) find_sources, &data);
+      break;
+    case EDIT_TRIM_START:
+      /* can only snap with sources at the start of the element.
+       * only need one such source since all will share the same start.
+       * if there is no source at the start edge, then snapping is not
+       * possible */
+      data.edge = GES_EDGE_START;
+      g_node_traverse (node, G_IN_ORDER, G_TRAVERSE_LEAVES, -1,
+          (GNodeTraverseFunc) find_source_at_edge, &data);
+      break;
+    case EDIT_TRIM_END:
+      /* can only snap with sources at the end of the element.
+       * only need one such source since all will share the same end.
+       * if there is no source at the end edge, then snapping is not
+       * possible */
+      data.edge = GES_EDGE_END;
+      g_node_traverse (node, G_IN_ORDER, G_TRAVERSE_LEAVES, -1,
+          (GNodeTraverseFunc) find_source_at_edge, &data);
+      break;
   }
 
-  if (duration < 0) {
-    GST_INFO ("%" GES_FORMAT "duration would be %" G_GINT64_FORMAT " < 0",
-        GES_ARGS (e), duration);
-    goto error;
+  for (tmp = data.sources; tmp; tmp = tmp->next) {
+    GESTrackElement *source = tmp->data;
+    GstClockTime end, start;
+    gboolean negative_end, negative_start;
+
+    /* Allow negative start/end positions in case a snap makes them valid!
+     * But we can still only snap to an existing edge in the timeline,
+     * which should be a valid time */
+    if (!get_start_end_from_offset (GES_TIMELINE_ELEMENT (source), mode,
+            *offset, &start, &negative_start, &end, &negative_end))
+      goto done;
+
+    switch (mode) {
+      case EDIT_MOVE:
+        /* try snap start and end */
+        find_snap_for_element (source, end, negative_end, &data);
+        find_snap_for_element (source, start, negative_start, &data);
+        break;
+      case EDIT_TRIM_START:
+        /* only snap the start of the source */
+        find_snap_for_element (source, start, negative_start, &data);
+        break;
+      case EDIT_TRIM_END:
+        /* only snap the start of the source */
+        find_snap_for_element (source, end, negative_end, &data);
+        break;
+    }
   }
 
-  if (priority < 0) {
-    GST_INFO ("%" GES_FORMAT "priority would be %" G_GINT64_FORMAT " < 0",
-        GES_ARGS (e), priority);
-    goto error;
+  if (GST_CLOCK_TIME_IS_VALID (snap->snapped)) {
+    if (snap->negative)
+      *offset -= (snap->position + snap->snapped);
+    else
+      *offset += (snap->position - snap->snapped);
+    GST_INFO_OBJECT (element, "Element %s under %s snapped with %" GES_FORMAT
+        " from %s%" GST_TIME_FORMAT " to %" GST_TIME_FORMAT,
+        GES_TIMELINE_ELEMENT_NAME (snap->element), element->name,
+        GES_ARGS (snap->snapped_to), snap->negative ? "-" : "",
+        GST_TIME_ARGS (snap->position), GST_TIME_ARGS (snap->snapped));
+  } else {
+    GST_INFO_OBJECT (element, "Nothing within snapping distance of %s",
+        element->name);
   }
 
-  if (inpoint < 0) {
-    GST_INFO ("%" GES_FORMAT " can't set inpoint %" G_GINT64_FORMAT,
-        GES_ARGS (e), inpoint);
-    goto error;
+  ret = TRUE;
+
+done:
+  g_list_free (data.sources);
+
+  return ret;
+}
+
+/****************************************************
+ *                 Check Overlaps                   *
+ ****************************************************/
+
+#define _ELEMENT_FORMAT \
+  "%s (under %s) [%" GST_TIME_FORMAT " - %" GST_TIME_FORMAT "] " \
+  "(layer: %" G_GUINT32_FORMAT ") (track :%" GST_PTR_FORMAT ")"
+#define _E_ARGS e->name, e->parent ? e->parent->name : NULL, \
+  GST_TIME_ARGS (start), GST_TIME_ARGS (end), layer_prio, track
+#define _CMP_ARGS cmp->name, cmp->parent ? cmp->parent->name : NULL, \
+  GST_TIME_ARGS (cmp_start), GST_TIME_ARGS (cmp_end), cmp_layer_prio, \
+  cmp_track
+
+static gboolean
+check_overlap_with_element (GNode * node, TreeIterationData * data)
+{
+  GESTimelineElement *e = node->data, *cmp = data->element;
+  GstClockTime start, end, cmp_start, cmp_end;
+  guint32 layer_prio, cmp_layer_prio;
+  GESTrack *track, *cmp_track;
+  PositionData *pos_data;
+
+  if (e == cmp)
+    return FALSE;
+
+  if (!GES_IS_SOURCE (e) || !GES_IS_SOURCE (cmp))
+    return FALSE;
+
+  /* get position of compared element */
+  pos_data = data->pos_data;
+  if (pos_data) {
+    cmp_start = pos_data->start;
+    cmp_end = pos_data->end;
+    cmp_layer_prio = pos_data->layer_priority;
+  } else {
+    cmp_start = cmp->start;
+    cmp_end = cmp_start + cmp->duration;
+    cmp_layer_prio = ges_timeline_element_get_layer_priority (cmp);
   }
 
-  if (inpoint + duration > e->maxduration) {
-    GST_INFO ("%" GES_FORMAT " inpoint + duration %" G_GINT64_FORMAT
-        " > max_duration %" G_GINT64_FORMAT,
-        GES_ARGS (e), inpoint + duration, e->maxduration);
-    goto error;
+  /* get position of the node */
+  if (data->moving)
+    pos_data = g_hash_table_lookup (data->moving, e);
+  else
+    pos_data = NULL;
+
+  if (pos_data) {
+    start = pos_data->start;
+    end = pos_data->end;
+    layer_prio = pos_data->layer_priority;
+  } else {
+    start = e->start;
+    end = start + e->duration;
+    layer_prio = ges_timeline_element_get_layer_priority (e);
   }
 
-  if (!moving)
-    check_snapping (e, data->element, data->snapping, start, end, moving_start,
-        moving_end);
+  track = ges_track_element_get_track (GES_TRACK_ELEMENT (e));
+  cmp_track = ges_track_element_get_track (GES_TRACK_ELEMENT (cmp));
+  GST_LOG ("Checking overlap between " _ELEMENT_FORMAT " and "
+      _ELEMENT_FORMAT, _CMP_ARGS, _E_ARGS);
 
-  if (!can_overlap)
+  if (track != cmp_track || track == NULL || cmp_track == NULL) {
+    GST_LOG (_ELEMENT_FORMAT " and " _ELEMENT_FORMAT " are not in the "
+        "same track", _CMP_ARGS, _E_ARGS);
     return FALSE;
+  }
 
-  if (start > moving_end || moving_start > end) {
-    /* They do not overlap at all */
-    GST_LOG ("%" GES_FORMAT " and %" GES_FORMAT
-        " do not overlap at all.",
-        GES_ARGS (node->data), GES_ARGS (data->element));
+  if (layer_prio != cmp_layer_prio) {
+    GST_LOG (_ELEMENT_FORMAT " and " _ELEMENT_FORMAT " are not in the "
+        "same layer", _CMP_ARGS, _E_ARGS);
     return FALSE;
   }
 
-  if ((moving_start <= start && moving_end >= end) ||
-      (moving_start >= start && moving_end <= end)) {
-    GST_INFO ("Fully overlaped: %s<%p> [%" G_GINT64_FORMAT " - %"
-        G_GINT64_FORMAT "] and %s<%p> [%" G_GINT64_FORMAT " - %" G_GINT64_FORMAT
-        " (%" G_GINT64_FORMAT ")]", e->name, e, start, end, data->element->name,
-        data->element, moving_start, moving_end, data->duration_diff);
+  if (start >= cmp_end || cmp_start >= end) {
+    /* They do not overlap at all */
+    GST_LOG (_ELEMENT_FORMAT " and " _ELEMENT_FORMAT " do not overlap",
+        _CMP_ARGS, _E_ARGS);
+    return FALSE;
+  }
 
+  if ((cmp_start <= start && cmp_end >= end) ||
+      (cmp_start >= start && cmp_end <= end)) {
+    GST_INFO (_ELEMENT_FORMAT " and " _ELEMENT_FORMAT " fully overlap",
+        _CMP_ARGS, _E_ARGS);
     goto error;
   }
 
-  if (moving_start < end && moving_start > start) {
-    /* moving_start is between the start and end of the node */
-    GST_LOG ("Overlap start: %s<%p> [%" G_GINT64_FORMAT "-%" G_GINT64_FORMAT
-        "] and %s<%p> [%" G_GINT64_FORMAT "-%" G_GINT64_FORMAT " (%"
-        G_GINT64_FORMAT ")]", e->name, e, start, end, data->element->name,
-        data->element, moving_start, moving_end, data->duration_diff);
+  if (cmp_start < end && cmp_start > start) {
+    /* cmp_start is between the start and end of the node */
+    GST_LOG (_ELEMENT_FORMAT " is overlapped at its start by "
+        _ELEMENT_FORMAT ". Overlap ends at %" GST_TIME_FORMAT,
+        _CMP_ARGS, _E_ARGS, GST_TIME_ARGS (end));
     if (data->overlaping_on_start) {
-      GST_INFO ("Clip is overlapped by %s and %s at its start",
-          data->overlaping_on_start->name, e->name);
+      GST_INFO (_ELEMENT_FORMAT " is overlapped by %s and %s on its start",
+          _CMP_ARGS, data->overlaping_on_start->name, e->name);
       goto error;
     }
     if (GST_CLOCK_TIME_IS_VALID (data->overlap_end_first_time) &&
         end > data->overlap_end_first_time) {
-      GST_INFO ("%s overlaps %s at start and %s at end, but they already "
-          "overlap each other", data->element->name, e->name,
+      GST_INFO (_ELEMENT_FORMAT " overlaps %s on its start and %s on its "
+          "end, but they already overlap each other", _CMP_ARGS, e->name,
           data->overlaping_on_end->name);
       goto error;
     }
     /* record the time at which the overlapped ends */
     data->overlap_start_final_time = end;
-    data->overlaping_on_start = node->data;
-  } else if (moving_end < end && moving_end > start) {
-    /* moving_end is between the start and end of the node */
-    GST_LOG ("Overlap end: %s<%p> [%" G_GINT64_FORMAT "-%" G_GINT64_FORMAT
-        "] and %s<%p> [%" G_GINT64_FORMAT "-%" G_GINT64_FORMAT " (%"
-        G_GINT64_FORMAT ")]", e->name, e, start, end, data->element->name,
-        data->element, moving_start, moving_end, data->duration_diff);
+    data->overlaping_on_start = e;
+  }
+
+  if (cmp_end < end && cmp_end > start) {
+    /* cmp_end is between the start and end of the node */
+    GST_LOG (_ELEMENT_FORMAT " is overlapped at its end by "
+        _ELEMENT_FORMAT ". Overlap starts at %" GST_TIME_FORMAT,
+        _CMP_ARGS, _E_ARGS, GST_TIME_ARGS (start));
 
     if (data->overlaping_on_end) {
-      GST_INFO ("Clip is overlapped by %s and %s at its end",
-          data->overlaping_on_end->name, e->name);
+      GST_INFO (_ELEMENT_FORMAT " is overlapped by %s and %s on its end",
+          _CMP_ARGS, data->overlaping_on_end->name, e->name);
       goto error;
     }
     if (GST_CLOCK_TIME_IS_VALID (data->overlap_start_final_time) &&
         start < data->overlap_start_final_time) {
-      GST_INFO ("%s overlaps %s at end and %s at start, but they already "
-          "overlap each other", data->element->name, e->name,
+      GST_INFO (_ELEMENT_FORMAT " overlaps %s on its end and %s on its "
+          "start, but they already overlap each other", _CMP_ARGS, e->name,
           data->overlaping_on_start->name);
       goto error;
     }
     /* record the time at which the overlapped starts */
     data->overlap_end_first_time = start;
-    data->overlaping_on_end = node->data;
+    data->overlaping_on_end = e;
   }
 
   return FALSE;
@@ -491,527 +766,914 @@ error:
   return TRUE;
 }
 
+/* check and find the overlaps with the element at node */
 static gboolean
-check_can_move_children (GNode * node, TreeIterationData * data)
+check_all_overlaps_with_element (GNode * node, TreeIterationData * data)
 {
   GESTimelineElement *element = node->data;
-  GstClockTimeDiff start = GST_CLOCK_DIFF (data->start_diff, element->start);
-  GstClockTime inpoint = GST_CLOCK_DIFF (data->inpoint_diff, element->inpoint);
-  GstClockTime duration =
-      GST_CLOCK_DIFF (data->duration_diff, element->duration);
-  gint64 priority =
-      (gint64) GES_TIMELINE_ELEMENT_LAYER_PRIORITY (element) -
-      data->priority_diff;
-  if (element == data->element)
-    return FALSE;
-
-  data->res =
-      timeline_tree_can_move_element_internal (data->root, node->data, priority,
-      start, inpoint, duration, data->movings, data->ripple_time,
-      data->snapping, data->edge);
-
-  return !data->res;
-}
-
-static gboolean
-timeline_tree_can_move_element_from_data (GNode * root,
-    TreeIterationData * data)
-{
-  GNode *node = find_node (root, data->element);
-
-  g_assert (node);
-  if (G_NODE_IS_LEAF (node)) {
-    if (GES_IS_SOURCE (node->data)) {
-      g_node_traverse (root, G_IN_ORDER, G_TRAVERSE_LEAVES, -1,
-          (GNodeTraverseFunc) check_track_elements_overlaps_and_values, data);
-
-      return data->res;
-    }
-
-    return TRUE;
+  if (GES_IS_SOURCE (element)) {
+    data->element = element;
+    data->overlaping_on_start = NULL;
+    data->overlaping_on_end = NULL;
+    data->overlap_start_final_time = GST_CLOCK_TIME_NONE;
+    data->overlap_end_first_time = GST_CLOCK_TIME_NONE;
+    if (data->moving)
+      data->pos_data = g_hash_table_lookup (data->moving, element);
+    else
+      data->pos_data = NULL;
+
+    g_node_traverse (data->root, G_IN_ORDER, G_TRAVERSE_LEAVES, -1,
+        (GNodeTraverseFunc) check_overlap_with_element, data);
+
+    return !data->res;
   }
-
-  g_node_traverse (node, G_IN_ORDER, G_TRAVERSE_LEAFS, -1,
-      (GNodeTraverseFunc) check_can_move_children, data);
-
-  return data->res;
+  return FALSE;
 }
 
 static gboolean
-add_element_to_list (GNode * node, GList ** elements)
+check_moving_overlaps (GNode * node, TreeIterationData * data)
 {
-  *elements = g_list_prepend (*elements, node->data);
-
+  if (g_hash_table_contains (data->moving, node->data))
+    return check_all_overlaps_with_element (node, data);
   return FALSE;
 }
 
+/* whether the elements in moving can be moved to their corresponding
+ * PositionData */
 static gboolean
-timeline_tree_can_move_element_internal (GNode * root,
-    GESTimelineElement * element, gint64 priority, GstClockTimeDiff start,
-    GstClockTimeDiff inpoint, GstClockTimeDiff duration,
-    GList * moving_track_elements, GstClockTime ripple_time,
-    SnappingData * snapping, GESEdge edge)
+timeline_tree_can_move_elements (GNode * root, GHashTable * moving)
 {
-  gboolean res;
   TreeIterationData data = tree_iteration_data_init;
-
+  data.moving = moving;
   data.root = root;
-  data.start_diff = GST_CLOCK_DIFF (start, element->start);
-  data.inpoint_diff = GST_CLOCK_DIFF (inpoint, element->inpoint);
-  data.duration_diff = GST_CLOCK_DIFF (duration, element->duration);
-  data.priority_diff =
-      (gint64) GES_TIMELINE_ELEMENT_LAYER_PRIORITY (element) - priority;
-  data.element = element;
-  data.movings = g_list_copy (moving_track_elements);
-  data.ripple_time = ripple_time;
-  data.snapping = snapping;
-  data.edge = edge;
-
-  if (GES_IS_SOURCE (element))
-    data.priority_diff =
-        GES_TIMELINE_ELEMENT_LAYER_PRIORITY (element) - priority;
-
-  res = timeline_tree_can_move_element_from_data (root, &data);
-  clean_iteration_data (&data);
-
-  return res;
+  data.res = TRUE;
+  /* sufficient to check the leaves, which is all the track elements or
+   * empty clips
+   * should also be sufficient to only check the moving elements */
+  g_node_traverse (root, G_IN_ORDER, G_TRAVERSE_LEAVES, -1,
+      (GNodeTraverseFunc) check_moving_overlaps, &data);
+
+  return data.res;
 }
 
-gboolean
-timeline_tree_can_move_element (GNode * root,
-    GESTimelineElement * element, guint32 priority, GstClockTime start,
-    GstClockTime duration, GList * moving_track_elements)
+/****************************************************
+ *               Setting Edit Data                  *
+ ****************************************************/
+
+static gboolean
+set_layer_priority (GESTimelineElement * element, EditData * data)
 {
-  GESTimelineElement *toplevel;
-  GstClockTimeDiff start_offset, duration_offset;
-  gint64 priority_diff;
-  gboolean res;
-  GList *local_moving_track_elements = g_list_copy (moving_track_elements);
+  gint64 layer_offset = data->layer_offset;
+  guint32 layer_prio = ges_timeline_element_get_layer_priority (element);
 
-  toplevel = get_toplevel_container (element);
-  if (ELEMENT_FLAG_IS_SET (element, GES_TIMELINE_ELEMENT_SET_SIMPLE) ||
-      ELEMENT_FLAG_IS_SET (toplevel, GES_TIMELINE_ELEMENT_SET_SIMPLE))
+  if (!layer_offset)
     return TRUE;
 
-  start_offset = GST_CLOCK_DIFF (start, element->start);
-  duration_offset = GST_CLOCK_DIFF (duration, element->duration);
-  priority_diff =
-      (gint64) GES_TIMELINE_ELEMENT_LAYER_PRIORITY (toplevel) -
-      (gint64) priority;
+  if (layer_prio == GES_TIMELINE_ELEMENT_NO_LAYER_PRIORITY) {
+    GST_INFO_OBJECT (element, "Cannot shift %s to a new layer because it "
+        "has no layer priority", element->name);
+    return FALSE;
+  }
 
-  g_node_traverse (find_node (root, toplevel), G_IN_ORDER,
-      G_TRAVERSE_LEAVES, -1, (GNodeTraverseFunc) add_element_to_list,
-      &local_moving_track_elements);
+  if (layer_offset > (gint64) layer_prio) {
+    GST_INFO_OBJECT (element, "%s would have a negative layer priority (%"
+        G_GUINT32_FORMAT " - %" G_GINT64_FORMAT ")", element->name,
+        layer_prio, layer_offset);
+    return FALSE;
+  }
+  if ((layer_prio - (gint64) layer_offset) >= G_MAXUINT32) {
+    GST_INFO_OBJECT (element, "%s would have an overflowing layer priority",
+        element->name);
+    return FALSE;
+  }
 
-  res = timeline_tree_can_move_element_internal (root, toplevel,
-      GES_TIMELINE_ELEMENT_LAYER_PRIORITY (toplevel) - priority_diff,
-      GST_CLOCK_DIFF (start_offset, toplevel->start),
-      toplevel->inpoint,
-      GST_CLOCK_DIFF (duration_offset, toplevel->duration),
-      local_moving_track_elements, GST_CLOCK_TIME_NONE, NULL, GES_EDGE_NONE);
+  data->layer_priority = (guint32) (layer_prio - (gint64) layer_offset);
 
-  g_list_free (local_moving_track_elements);
-  return res;
+  GST_INFO_OBJECT (element, "%s will move to layer %" G_GUINT32_FORMAT,
+      element->name, data->layer_priority);
+
+  return TRUE;
 }
 
-static void
-move_to_new_layer (GESTimelineElement * elem, gint layer_priority_offset)
-{
-  guint32 nprio =
-      GES_TIMELINE_ELEMENT_LAYER_PRIORITY (elem) - layer_priority_offset;
-  GESTimeline *timeline = GES_TIMELINE_ELEMENT_TIMELINE (elem);
+#define _CHECK_END(element, start, duration) \
+  if (!GST_CLOCK_TIME_IS_VALID (_clock_time_plus (start, duration))) { \
+    GST_INFO_OBJECT (element, "Cannot edit %s because it would result in " \
+        "an invalid end", element->name); \
+    return FALSE; \
+  }
 
-  if (!layer_priority_offset)
-    return;
+static gboolean
+set_edit_move_values (GESTimelineElement * element, EditData * data)
+{
+  GstClockTime new_start =
+      _clock_time_minus_diff (element->start, data->offset, NULL);
+  if (!GST_CLOCK_TIME_IS_VALID (new_start)) {
+    GST_INFO_OBJECT (element, "Cannot move %" GES_FORMAT " with offset %"
+        G_GINT64_FORMAT " because it would result in an invalid start",
+        GES_ARGS (element), data->offset);
+    return FALSE;
+  }
+  _CHECK_END (element, new_start, element->duration);
+  data->start = new_start;
+  GST_INFO_OBJECT (element, "%s will move by setting start to %"
+      GST_TIME_FORMAT, element->name, GST_TIME_ARGS (data->start));
 
-  GST_DEBUG ("%s moving from %" G_GUINT32_FORMAT " to %" G_GUINT32_FORMAT " (%"
-      G_GUINT32_FORMAT ")", elem->name, elem->priority, nprio,
-      layer_priority_offset);
-  if (GES_IS_CLIP (elem)) {
-    GESLayer *layer = ges_timeline_get_layer (timeline, nprio);
+  return set_layer_priority (element, data);
+}
 
-    if (layer == NULL) {
-      do {
-        layer = ges_timeline_append_layer (timeline);
-      } while (ges_layer_get_priority (layer) < nprio);
-    } else {
-      gst_object_unref (layer);
+/* trim the start of a clip or a track element */
+static gboolean
+set_edit_trim_start_values (GESTimelineElement * element, EditData * data)
+{
+  GstClockTime new_start =
+      _clock_time_minus_diff (element->start, data->offset, NULL);
+  GstClockTime new_duration =
+      _clock_time_minus_diff (element->duration, -data->offset, NULL);
+
+  if (!GST_CLOCK_TIME_IS_VALID (new_start)) {
+    GST_INFO_OBJECT (element, "Cannot trim start of %" GES_FORMAT
+        " with offset %" G_GINT64_FORMAT " because it would result in an "
+        "invalid start", GES_ARGS (element), data->offset);
+    return FALSE;
+  }
+  if (!GST_CLOCK_TIME_IS_VALID (new_duration)) {
+    GST_INFO_OBJECT (element, "Cannot trim start of %" GES_FORMAT
+        " with offset %" G_GINT64_FORMAT " because it would result in an "
+        "invalid duration", GES_ARGS (element), data->offset);
+    return FALSE;
+  }
+  _CHECK_END (element, new_start, new_duration);
+
+  if (!GES_IS_TRACK_ELEMENT (element)
+      || ges_track_element_has_internal_source (GES_TRACK_ELEMENT (element))) {
+    GstClockTime new_inpoint =
+        _clock_time_minus_diff (element->inpoint, data->offset, NULL);
+    if (!GST_CLOCK_TIME_IS_VALID (new_inpoint)) {
+      GST_INFO_OBJECT (element, "Cannot trim start of %" GES_FORMAT
+          " with offset %" G_GINT64_FORMAT " because it would result in an "
+          "invalid in-point", GES_ARGS (element), data->offset);
+      return FALSE;
     }
 
-    ges_clip_move_to_layer (GES_CLIP (elem), layer);
-  } else if (GES_IS_GROUP (elem)) {
-    ges_timeline_element_set_priority (elem, nprio);
-  } else {
-    g_assert_not_reached ();
+    data->inpoint = new_inpoint;
   }
+  data->start = new_start;
+  data->duration = new_duration;
+
+  /* NOTE: without time effects, the duration-limit will increase with
+   * a decrease in in-point by the same amount that duration increases,
+   * and vis-versa. So the new duration-limit should remain above the
+   * new duration */
+
+  GST_INFO_OBJECT (element, "%s will trim start by setting start to %"
+      GST_TIME_FORMAT ", in-point to %" GST_TIME_FORMAT " and duration "
+      "to %" GST_TIME_FORMAT, element->name, GST_TIME_ARGS (data->start),
+      GST_TIME_ARGS (data->inpoint), GST_TIME_ARGS (data->duration));
+
+  return set_layer_priority (element, data);
 }
 
-gboolean
-timeline_tree_ripple (GNode * root, gint64 layer_priority_offset,
-    GstClockTimeDiff offset, GESTimelineElement * rippled_element,
-    GESEdge edge, GstClockTime snapping_distance)
+/* trim the end of a clip or a track element */
+static gboolean
+set_edit_trim_end_values (GESTimelineElement * element, EditData * data)
 {
-  GNode *node;
-  GHashTableIter iter;
-  GESTimelineElement *elem;
-  GstClockTimeDiff start, duration;
-  gboolean res = TRUE;
-  GHashTable *to_move = g_hash_table_new (g_direct_hash, g_direct_equal);
-  GList *moving_track_elements = NULL;
-  SnappingData snapping = {
-    .distance = snapping_distance,
-    .on_end_only = edge == GES_EDGE_END,
-    .on_start_only = FALSE,
-    .element = NULL,
-    .edge = GES_EDGE_NONE,
-    .diff = (GstClockTimeDiff) snapping_distance,
-  };
-  gint64 new_layer_priority =
-      ((gint64) GES_TIMELINE_ELEMENT_LAYER_PRIORITY (rippled_element)) -
-      layer_priority_offset;
-  GESTimelineElement *ripple_toplevel =
-      get_toplevel_container (rippled_element);
-  GstClockTimeDiff ripple_time = ELEMENT_EDGE_VALUE (rippled_element, edge);
-
-  if (edge == GES_EDGE_END) {
-    if (ripple_toplevel != rippled_element) {
-      GST_FIXME ("Trying to ripple end %" GES_FORMAT " but in %" GES_FORMAT
-          " we do not know how to do that yet!",
-          GES_ARGS (rippled_element), GES_ARGS (ripple_toplevel));
-      goto error;
+  GstClockTime new_duration =
+      _clock_time_minus_diff (element->duration, data->offset, NULL);
+  if (!GST_CLOCK_TIME_IS_VALID (new_duration)) {
+    GST_INFO_OBJECT (element, "Cannot trim end of %" GES_FORMAT
+        " with offset %" G_GINT64_FORMAT " because it would result in an "
+        "invalid duration", GES_ARGS (element), data->offset);
+    return FALSE;
+  }
+  _CHECK_END (element, element->start, new_duration);
+
+  if (GES_IS_CLIP (element)) {
+    GstClockTime limit = ges_clip_get_duration_limit (GES_CLIP (element));
+    if (GST_CLOCK_TIME_IS_VALID (limit) && new_duration > limit) {
+      GST_INFO_OBJECT (element, "Cannot trim end of %" GES_FORMAT
+          " with offset %" G_GINT64_FORMAT " because the duration would "
+          "exceed the clip's duration-limit %" G_GINT64_FORMAT,
+          GES_ARGS (element), data->offset, limit);
+      return FALSE;
     }
-  } else {
-    g_node_traverse (find_node (root, ripple_toplevel), G_IN_ORDER,
-        G_TRAVERSE_LEAVES, -1, (GNodeTraverseFunc) add_element_to_list,
-        &moving_track_elements);
   }
 
-  GST_INFO ("Moving %" GES_FORMAT " with offset %" G_GINT64_FORMAT "",
-      GES_ARGS (ripple_toplevel), offset);
+  data->duration = new_duration;
+  GST_INFO_OBJECT (element, "%s will trim end by setting duration to %"
+      GST_TIME_FORMAT, element->name, GST_TIME_ARGS (data->duration));
 
-  if (edge == GES_EDGE_END) {
-    start = _START (rippled_element);
-    duration = GST_CLOCK_DIFF (offset, _DURATION (rippled_element));
-  } else {
-    start = GST_CLOCK_DIFF (offset, _START (rippled_element));
-    duration = _DURATION (rippled_element);
-  }
+  return set_layer_priority (element, data);
+}
 
-  if (!timeline_tree_can_move_element_internal (root, rippled_element,
-          new_layer_priority, start, rippled_element->inpoint, duration, NULL,
-          ripple_time, snapping_distance ? &snapping : NULL, edge)) {
-    goto error;
+/* handles clips and track elements with no parents */
+static gboolean
+set_clip_edit_values (GESTimelineElement * element, EditData * data)
+{
+  switch (data->mode) {
+    case EDIT_MOVE:
+      return set_edit_move_values (element, data);
+    case EDIT_TRIM_START:
+      return set_edit_trim_start_values (element, data);
+    case EDIT_TRIM_END:
+      return set_edit_trim_end_values (element, data);
   }
+  return FALSE;
+}
+
+static gboolean
+add_clips_to_list (GNode * node, GList ** list)
+{
+  GESTimelineElement *element = node->data;
+  GESTimelineElement *clip = NULL;
 
-  if (snapping_distance) {
-    if (snapping.element) {
-      offset =
-          GST_CLOCK_DIFF (ELEMENT_EDGE_VALUE (snapping.element, snapping.edge),
-          ELEMENT_EDGE_VALUE (snapping.moving_element, snapping.moving_edge));
+  if (GES_IS_CLIP (element))
+    clip = element;
+  else if (GES_IS_CLIP (element->parent))
+    clip = element->parent;
 
-      if (edge == GES_EDGE_END) {
-        start = _START (rippled_element);
-        duration = GST_CLOCK_DIFF (offset, _DURATION (rippled_element));
-      } else {
-        start = GST_CLOCK_DIFF (offset, _START (rippled_element));
-        duration = _DURATION (rippled_element);
-      }
+  if (clip && !g_list_find (*list, clip))
+    *list = g_list_append (*list, clip);
 
-      GST_INFO ("Snapping on %" GES_FORMAT "%s %" G_GINT64_FORMAT "",
-          GES_ARGS (snapping.element),
-          snapping.edge == GES_EDGE_END ? "end" : "start",
-          ELEMENT_EDGE_VALUE (snapping.element, snapping.edge));
-      if (!timeline_tree_can_move_element_internal (root, rippled_element,
-              new_layer_priority, start, rippled_element->inpoint, duration,
-              NULL, ripple_time, NULL, edge)) {
-        goto error;
-      }
-    }
+  return FALSE;
+}
 
-    ges_timeline_emit_snapping (root->data, rippled_element, snapping.element,
-        snapping.element ? ELEMENT_EDGE_VALUE (snapping.element,
-            snapping.edge) : GST_CLOCK_TIME_NONE);
+static gboolean
+replace_group_with_clip_edits (GNode * root, GESTimelineElement * group,
+    GHashTable * edit_table)
+{
+  gboolean ret = TRUE;
+  GList *tmp, *clips = NULL;
+  GNode *node = find_node (root, group);
+  GstClockTime new_end, new_start;
+  ElementEditMode mode;
+  gint64 layer_offset;
+
+  if (!node) {
+    GST_ERROR_OBJECT (group, "Not being tracked");
+    goto error;
   }
 
-  /* Make sure we can ripple all toplevels after the rippled element */
-  for (node = root->children; node; node = node->next) {
-    GESTimelineElement *toplevel = get_toplevel_container (node->data);
-
-    if (GES_TIMELINE_ELEMENT_START (toplevel) < ripple_time
-        && (edge == GES_EDGE_END || toplevel != ripple_toplevel))
-      continue;
+  /* new context for the lifespan of group_data */
+  {
+    EditData *group_edit = g_hash_table_lookup (edit_table, group);
 
-    if (!timeline_tree_can_move_element_internal (root, node->data,
-            ((gint64) GES_TIMELINE_ELEMENT_LAYER_PRIORITY (node->data)) -
-            layer_priority_offset,
-            GST_CLOCK_DIFF (offset, _START (node->data)),
-            _INPOINT (node->data),
-            _DURATION (node->data), moving_track_elements, ripple_time, NULL,
-            GES_EDGE_NONE)) {
+    if (!group_edit) {
+      GST_ERROR_OBJECT (group, "Edit data for group was missing");
       goto error;
     }
 
-    if (!check_can_move_to_layer (toplevel, layer_priority_offset)) {
-      GST_INFO ("%" GES_FORMAT " would land in a layer with negative priority",
-          GES_ARGS (toplevel));
-      goto error;
-    }
+    layer_offset = group_edit->layer_offset;
+    mode = group_edit->mode;
 
-    g_hash_table_add (to_move, toplevel);
-  }
+    if (!get_start_end_from_offset (group, mode, group_edit->offset,
+            &new_start, NULL, &new_end, NULL))
+      goto error;
 
-  if (edge == GES_EDGE_END) {
-    if (!check_can_move_to_layer (rippled_element, layer_priority_offset)) {
-      GST_INFO ("%" GES_FORMAT " would land in a layer with negative priority",
-          GES_ARGS (rippled_element));
+    /* can traverse leaves to find all the clips since they are at _most_
+     * one step above the track elements */
+    g_node_traverse (node, G_IN_ORDER, G_TRAVERSE_LEAVES, -1,
+        (GNodeTraverseFunc) add_clips_to_list, &clips);
 
+    if (!clips) {
+      GST_INFO_OBJECT (group, "Contains no clips, so cannot be edited");
       goto error;
     }
 
-    if (duration < 0) {
-      GST_INFO ("Would set duration to  %" G_GINT64_FORMAT " <= 0", duration);
+    if (!g_hash_table_remove (edit_table, group)) {
+      GST_ERROR_OBJECT (group, "Could not replace the group in the edit list");
       goto error;
     }
-
-    ELEMENT_SET_FLAG (rippled_element, GES_TIMELINE_ELEMENT_SET_SIMPLE);
-    ges_timeline_element_set_duration (rippled_element, duration);
-    ELEMENT_UNSET_FLAG (rippled_element, GES_TIMELINE_ELEMENT_SET_SIMPLE);
+    /* removing the group from the table frees group_edit */
   }
 
-  g_hash_table_iter_init (&iter, to_move);
-  while (g_hash_table_iter_next (&iter, (gpointer *) & elem, NULL)) {
-    GST_LOG ("Moving %" GES_FORMAT " to %" G_GINT64_FORMAT " - layer %"
-        G_GINT64_FORMAT "", GES_ARGS (elem),
-        GES_TIMELINE_ELEMENT_START (elem) - offset,
-        (gint64) GES_TIMELINE_ELEMENT_LAYER_PRIORITY (elem) -
-        layer_priority_offset);
-
-    ELEMENT_SET_FLAG (elem, GES_TIMELINE_ELEMENT_SET_SIMPLE);
-    ges_timeline_element_set_start (elem,
-        GST_CLOCK_DIFF (offset, GES_TIMELINE_ELEMENT_START (elem)));
-    move_to_new_layer (elem, layer_priority_offset);
-    ELEMENT_UNSET_FLAG (elem, GES_TIMELINE_ELEMENT_SET_SIMPLE);
-  }
+  for (tmp = clips; tmp; tmp = tmp->next) {
+    GESTimelineElement *clip = tmp->data;
+    gboolean edit = FALSE;
+    GstClockTimeDiff offset = G_MAXINT64;
+    ElementEditMode clip_mode = mode;
+
+    /* if at the edge of the group and being trimmed forward or backward */
+    if (mode == EDIT_MOVE) {
+      /* same offset as the group */
+      edit = TRUE;
+      offset = group->start - new_start;
+
+      GST_INFO_OBJECT (clip, "Setting clip %s to moving with offset %"
+          G_GINT64_FORMAT " since an ancestor group %" GES_FORMAT
+          " is moving to %" GST_TIME_FORMAT, clip->name, offset,
+          GES_ARGS (group), GST_TIME_ARGS (new_start));
+
+    } else if ((mode == EDIT_TRIM_START)
+        && (clip->start <= new_start || clip->start == group->start)) {
+      /* trim to same start */
+      edit = TRUE;
+      offset = clip->start - new_start;
+
+      GST_INFO_OBJECT (clip, "Setting clip %s to trim start with offset %"
+          G_GINT64_FORMAT " since an ancestor group %" GES_FORMAT " is "
+          "being trimmed to start %" GST_TIME_FORMAT, clip->name, offset,
+          GES_ARGS (group), GST_TIME_ARGS (new_start));
+
+    } else if (mode == EDIT_TRIM_END
+        && (_END (clip) >= new_end || _END (clip) == _END (group))) {
+      /* trim to same end */
+      edit = TRUE;
+      offset = _END (clip) - new_end;
+
+      GST_INFO_OBJECT (clip, "Setting clip %s to trim end with offset %"
+          G_GINT64_FORMAT " since an ancestor group %" GES_FORMAT " is "
+          "being trimmed to end %" GST_TIME_FORMAT, clip->name, offset,
+          GES_ARGS (group), GST_TIME_ARGS (new_end));
+
+    } else if (layer_offset) {
+      /* still need to move layer */
+      edit = TRUE;
+      clip_mode = EDIT_MOVE;
+      offset = 0;
+    }
+    if (edit) {
+      EditData *clip_data;
 
-  ELEMENT_SET_FLAG (rippled_element, GES_TIMELINE_ELEMENT_SET_SIMPLE);
-  if (edge == GES_EDGE_END)
-    move_to_new_layer (rippled_element, layer_priority_offset);
-  ELEMENT_UNSET_FLAG (rippled_element, GES_TIMELINE_ELEMENT_SET_SIMPLE);
+      if (layer_offset)
+        GST_INFO_OBJECT (clip, "Setting clip %s to move to new layer with "
+            "offset %" G_GINT64_FORMAT " since an ancestor group %"
+            GES_FORMAT " is being moved with the same offset", clip->name,
+            layer_offset, GES_ARGS (group));
 
-  timeline_tree_create_transitions (root, ges_timeline_find_auto_transition);
-  timeline_update_transition (root->data);
-  timeline_update_duration (root->data);
+      if (g_hash_table_contains (edit_table, clip)) {
+        GST_ERROR_OBJECT (clip, "Already set to be edited");
+        goto error;
+      }
+      clip_data = new_edit_data (clip_mode, offset, layer_offset);
+      g_hash_table_insert (edit_table, clip, clip_data);
+      if (!set_clip_edit_values (clip, clip_data))
+        goto error;
+    }
+  }
 
 done:
-  g_hash_table_unref (to_move);
-  g_list_free (moving_track_elements);
-  return res;
+  g_list_free (clips);
+  return ret;
 
 error:
-  res = FALSE;
+  ret = FALSE;
   goto done;
 }
 
+/* set the edit values for the entries in @edits
+ * any groups in @edits will be replaced by their clip children */
 static gboolean
-trim_group_get_vals (TreeIterationData * data, GESTimelineElement * e,
-    GstClockTimeDiff * n_start, GstClockTimeDiff * n_inpoint,
-    GstClockTimeDiff * n_duration)
+timeline_tree_set_element_edit_values (GNode * root, GHashTable * edits)
 {
-  GstClockTimeDiff offset;
-  GstClockTimeDiff group_nstart =
-      GST_CLOCK_DIFF (data->start_diff, data->trim_group_start);
-  GstClockTimeDiff group_nend =
-      GST_CLOCK_DIFF (data->duration_diff, data->trim_group_end);
-
-  if (data->edge == GES_EDGE_START &&
-      ((group_nstart >= e->start) || (e->start == data->trim_group_start))) {
-
-    offset = GST_CLOCK_DIFF (group_nstart, e->start);
-    *n_start = group_nstart;
-    *n_inpoint = GST_CLOCK_DIFF (offset, e->inpoint);
-    *n_duration =
-        GST_CLOCK_DIFF (*n_start, (GstClockTimeDiff) e->start + e->duration);
-
-    GST_DEBUG_OBJECT (data->element, "Trimming %" GES_FORMAT " start",
-        GES_ARGS (e));
-    return TRUE;
-  } else if (data->edge == GES_EDGE_END &&
-      ((group_nend <= _END (e)) || (_END (e) == data->trim_group_end))) {
+  gboolean ret = TRUE;
+  GESTimelineElement *element;
+  EditData *edit_data;
+  /* content of edit table may change when group edits are replaced by
+   * clip edits and clip edits introduce edits for non-core children */
+  GList *tmp, *elements = g_hash_table_get_keys (edits);
+
+  for (tmp = elements; tmp; tmp = tmp->next) {
+    gboolean res;
+    element = tmp->data;
+    edit_data = g_hash_table_lookup (edits, element);
+    if (!edit_data) {
+      GST_ERROR_OBJECT (element, "No edit data for the element");
+      goto error;
+    }
+    if (GES_IS_GROUP (element))
+      res = replace_group_with_clip_edits (root, element, edits);
+    else
+      res = set_clip_edit_values (element, edit_data);
+    if (!res)
+      goto error;
+  }
 
-    offset = GST_CLOCK_DIFF (group_nend, _END (e));
-    *n_start = e->start;
-    *n_inpoint = e->inpoint;
-    *n_duration = GST_CLOCK_DIFF (offset, e->duration);
+done:
+  g_list_free (elements);
 
-    GST_DEBUG_OBJECT (data->element, "Trimming %" GES_FORMAT " end",
-        GES_ARGS (e));
+  return ret;
 
-    return TRUE;
+error:
+  ret = FALSE;
+  goto done;
+}
+
+/* set the moving PositionData by using their parent clips.
+ * @edit_table should already have had its values set, and any group edits
+ * replaced by clip edits. */
+static void
+set_moving_positions_from_edits (GHashTable * moving, GHashTable * edit_table)
+{
+  GHashTableIter iter;
+  gpointer key, value;
+
+  g_hash_table_iter_init (&iter, moving);
+  while (g_hash_table_iter_next (&iter, &key, &value)) {
+    GESTimelineElement *element = key;
+    PositionData *pos = value;
+    GESTimelineElement *parent;
+    EditData *edit;
+
+    /* a track element will end up with the same start and end as its clip */
+    /* if no parent, act as own parent */
+    parent = element->parent ? element->parent : element;
+    edit = g_hash_table_lookup (edit_table, parent);
+
+    if (edit && GST_CLOCK_TIME_IS_VALID (edit->start))
+      pos->start = edit->start;
+    else
+      pos->start = element->start;
+
+    if (edit && GST_CLOCK_TIME_IS_VALID (edit->duration))
+      pos->end = pos->start + edit->duration;
+    else
+      pos->end = pos->start + element->duration;
+
+    if (edit && edit->layer_priority != GES_TIMELINE_ELEMENT_NO_LAYER_PRIORITY)
+      pos->layer_priority = edit->layer_priority;
+    else
+      pos->layer_priority = ges_timeline_element_get_layer_priority (element);
   }
+}
+
+static void
+give_edits_same_offset (GHashTable * edits, GstClockTimeDiff offset,
+    gint64 layer_offset)
+{
+  GHashTableIter iter;
+  gpointer value;
+
+  g_hash_table_iter_init (&iter, edits);
+  while (g_hash_table_iter_next (&iter, NULL, &value)) {
+    EditData *edit_data = value;
+    edit_data->offset = offset;
+    edit_data->layer_offset = layer_offset;
+  }
+}
+
+/****************************************************
+ *         Initialise Edit Data and Moving          *
+ ****************************************************/
 
-  /* Ignoring child */
+static gboolean
+add_track_elements_to_moving (GNode * node, GHashTable * track_elements)
+{
+  GESTimelineElement *element = node->data;
+  if (GES_IS_TRACK_ELEMENT (element)) {
+    GST_LOG_OBJECT (element, "%s set as moving", element->name);
+    g_hash_table_insert (track_elements, element, g_new0 (PositionData, 1));
+  }
   return FALSE;
 }
 
+/* add all the track elements found under the elements in @edits to @moving,
+ * but does not set their position data */
+static gboolean
+timeline_tree_add_edited_to_moving (GNode * root, GHashTable * edits,
+    GHashTable * moving)
+{
+  GHashTableIter iter;
+  gpointer key;
+
+  g_hash_table_iter_init (&iter, edits);
+  while (g_hash_table_iter_next (&iter, &key, NULL)) {
+    GESTimelineElement *element = key;
+    GNode *node = find_node (root, element);
+    if (!node) {
+      GST_ERROR_OBJECT (element, "Not being tracked");
+      return FALSE;
+    }
+    g_node_traverse (node, G_IN_ORDER, G_TRAVERSE_LEAVES, -1,
+        (GNodeTraverseFunc) add_track_elements_to_moving, moving);
+  }
+
+  return TRUE;
+}
+
+/* check we can handle the top and all of its children */
 static gboolean
-check_trim_child (GNode * node, TreeIterationData * data)
+check_types (GESTimelineElement * element, gboolean is_top)
 {
-  GESTimelineElement *e = node->data;
-  GstClockTimeDiff n_start = GST_CLOCK_DIFF (data->start_diff, e->start);
-  GstClockTimeDiff n_inpoint = GST_CLOCK_DIFF (data->inpoint_diff, e->inpoint);
-  GstClockTimeDiff n_duration = data->edge == GES_EDGE_END ?
-      GST_CLOCK_DIFF (data->duration_diff, e->duration) :
-      GST_CLOCK_DIFF (n_start, (GstClockTimeDiff) e->start + e->duration);
-
-  if (GST_CLOCK_TIME_IS_VALID (data->trim_group_start)
-      && !trim_group_get_vals (data, e, &n_start, &n_inpoint, &n_duration)) {
-    GST_DEBUG_OBJECT (data->element, "Not trimming");
+  if (!GES_IS_CLIP (element) && !GES_IS_GROUP (element)
+      && !GES_IS_TRACK_ELEMENT (element)) {
+    GST_ERROR_OBJECT (element, "Cannot handle a GESTimelineElement of the "
+        "type %s", G_OBJECT_TYPE_NAME (element));
     return FALSE;
   }
+  if (!is_top && element->parent) {
+    if ((GES_IS_CLIP (element) && !GES_IS_GROUP (element->parent))
+        || (GES_IS_GROUP (element) && !GES_IS_GROUP (element->parent))
+        || (GES_IS_TRACK_ELEMENT (element) && !GES_IS_CLIP (element->parent))) {
+      GST_ERROR_OBJECT (element, "A parent of type %s is not handled",
+          G_OBJECT_TYPE_NAME (element->parent));
+      return FALSE;
+    }
+  }
+  if (GES_IS_CONTAINER (element)) {
+    GList *tmp;
+    for (tmp = GES_CONTAINER_CHILDREN (element); tmp; tmp = tmp->next) {
+      if (!check_types (tmp->data, FALSE))
+        return FALSE;
+    }
+  }
 
-  if (!timeline_tree_can_move_element_internal (data->root, e,
-          (gint64) ges_timeline_element_get_layer_priority (e) -
-          data->priority_diff, n_start, n_inpoint, n_duration, NULL,
-          GST_CLOCK_TIME_NONE, data->snapping, GES_EDGE_NONE))
-    goto error;
+  return TRUE;
+}
 
-  if (GES_IS_CLIP (e->parent))
-    g_hash_table_add (data->moved_clips, e->parent);
-  else if (GES_IS_CLIP (e))
-    g_hash_table_add (data->moved_clips, e);
+/* @edits: The table to add the edit to
+ * @element: The element to edit
+ * @mode: The mode for editing @element
+ *
+ * Adds an edit for @element it to the table with its EditData only set
+ * with @mode.
+ *
+ * The offsets for the edit will have to be set later.
+ */
+static gboolean
+add_element_edit (GHashTable * edits, GESTimelineElement * element,
+    ElementEditMode mode)
+{
+  if (!check_types (element, TRUE))
+    return FALSE;
 
-  return FALSE;
+  if (g_hash_table_contains (edits, element)) {
+    GST_ERROR_OBJECT (element, "Already set to be edited");
+    return FALSE;
+  }
 
-error:
-  data->res = FALSE;
+  switch (mode) {
+    case EDIT_MOVE:
+      GST_LOG_OBJECT (element, "%s set to move", element->name);
+      break;
+    case EDIT_TRIM_START:
+      GST_LOG_OBJECT (element, "%s set to trim start", element->name);
+      break;
+    case EDIT_TRIM_END:
+      GST_LOG_OBJECT (element, "%s set to trim end", element->name);
+      break;
+  }
+
+  g_hash_table_insert (edits, element, new_edit_data (mode, 0, 0));
 
   return TRUE;
 }
 
-static gboolean
-timeline_tree_can_trim_element_internal (GNode * root, TreeIterationData * data)
+/********************************************
+ *   Check against current configuration    *
+ ********************************************/
+
+/* can move with no snapping or change in parent! */
+gboolean
+timeline_tree_can_move_element (GNode * root,
+    GESTimelineElement * element, guint32 priority, GstClockTime start,
+    GstClockTime duration)
 {
-  g_node_traverse (find_node (root, data->element), G_IN_ORDER,
-      G_TRAVERSE_LEAVES, -1, (GNodeTraverseFunc) check_trim_child, data);
+  gboolean ret = FALSE;
+  guint32 layer_prio = ges_timeline_element_get_layer_priority (element);
+  GstClockTime distance, new_end;
+  GHashTable *move_edits, *trim_edits, *moving;
+  GHashTableIter iter;
+  gpointer key, value;
+
+  if (layer_prio == GES_TIMELINE_ELEMENT_NO_LAYER_PRIORITY
+      && priority != layer_prio) {
+    GST_INFO_OBJECT (element, "Cannot move to a layer when no layer "
+        "priority to begin with");
+    return FALSE;
+  }
+
+  distance = _abs_clock_time_distance (start, element->start);
+  if ((GstClockTimeDiff) distance >= G_MAXINT64) {
+    GST_WARNING_OBJECT (element, "Move in start from %" GST_TIME_FORMAT
+        " to %" GST_TIME_FORMAT " is too large to perform",
+        GST_TIME_ARGS (element->start), GST_TIME_ARGS (start));
+    return FALSE;
+  }
+
+  distance = _abs_clock_time_distance (duration, element->duration);
+  if ((GstClockTimeDiff) distance >= G_MAXINT64) {
+    GST_WARNING_OBJECT (element, "Move in duration from %" GST_TIME_FORMAT
+        " to %" GST_TIME_FORMAT " is too large to perform",
+        GST_TIME_ARGS (element->duration), GST_TIME_ARGS (duration));
+    return FALSE;
+  }
+
+  new_end = _clock_time_plus (start, duration);
+  if (!GST_CLOCK_TIME_IS_VALID (new_end)) {
+    GST_WARNING_OBJECT (element, "Move in start and duration to %"
+        GST_TIME_FORMAT " and %" GST_TIME_FORMAT " would produce an "
+        "invalid end", GST_TIME_ARGS (start), GST_TIME_ARGS (duration));
+    return FALSE;
+  }
+
+  /* treat as an EDIT_MOVE to the new priority, except on the element
+   * rather than the toplevel, followed by an EDIT_TRIM_END */
+  move_edits = new_edit_table ();
+  trim_edits = new_edit_table ();
+  moving = new_position_table ();
+
+  if (!add_element_edit (move_edits, element, EDIT_MOVE))
+    goto done;
+  /* moving should remain the same */
+  if (!add_element_edit (trim_edits, element, EDIT_TRIM_END))
+    goto done;
+
+  if (!timeline_tree_add_edited_to_moving (root, move_edits, moving)
+      || !timeline_tree_add_edited_to_moving (root, trim_edits, moving))
+    goto done;
+
+  /* no snapping */
+  give_edits_same_offset (move_edits, element->start - start,
+      (gint64) layer_prio - (gint64) priority);
+  give_edits_same_offset (trim_edits, element->duration - duration, 0);
+
+  /* assume both edits can be performed if each could occur individually */
+  /* should not effect duration or in-point */
+  if (!timeline_tree_set_element_edit_values (root, move_edits))
+    goto done;
+  /* should not effect start or in-point or layer */
+  if (!timeline_tree_set_element_edit_values (root, trim_edits))
+    goto done;
+
+  /* merge the two edits into moving positions */
+  g_hash_table_iter_init (&iter, moving);
+  while (g_hash_table_iter_next (&iter, &key, &value)) {
+    GESTimelineElement *el = key;
+    PositionData *pos_data = value;
+    EditData *move = NULL;
+    EditData *trim = NULL;
+
+    if (el->parent) {
+      move = g_hash_table_lookup (move_edits, el->parent);
+      trim = g_hash_table_lookup (trim_edits, el->parent);
+    }
+
+    if (!move)
+      move = g_hash_table_lookup (move_edits, el);
+    if (!trim)
+      trim = g_hash_table_lookup (trim_edits, el);
 
-  return data->res;
+    /* should always have move with a valid start */
+    if (!move || !GST_CLOCK_TIME_IS_VALID (move->start)) {
+      GST_ERROR_OBJECT (el, "Element set to moving but neither it nor its "
+          "parent are being edited");
+      goto done;
+    }
+    /* may not have trim if element is a group and the child is away
+     * from the edit position, but if we do it should have a valid duration */
+    if (trim && !GST_CLOCK_TIME_IS_VALID (trim->duration)) {
+      GST_ERROR_OBJECT (el, "Element set to trim end but neither it nor its "
+          "parent is being trimmed");
+      goto done;
+    }
+
+    pos_data->start = move->start;
+
+    if (move->layer_priority != GES_TIMELINE_ELEMENT_NO_LAYER_PRIORITY)
+      pos_data->layer_priority = move->layer_priority;
+    else
+      pos_data->layer_priority = ges_timeline_element_get_layer_priority (el);
+
+    if (trim)
+      pos_data->end = pos_data->start + trim->duration;
+    else
+      pos_data->end = pos_data->start + el->duration;
+  }
+
+  /* check overlaps */
+  if (!timeline_tree_can_move_elements (root, moving))
+    goto done;
+
+  ret = TRUE;
+
+done:
+  g_hash_table_unref (trim_edits);
+  g_hash_table_unref (move_edits);
+  g_hash_table_unref (moving);
+
+  return ret;
 }
 
-static void
-trim_simple (GESTimelineElement * element, GstClockTimeDiff offset,
-    GESEdge edge, TreeIterationData * data)
+/********************************************
+ *         Perform Element Edit             *
+ ********************************************/
+
+static gboolean
+perform_element_edit (GESTimelineElement * element, EditData * edit)
 {
-  GESTimelineElement *toplevel =
-      ges_timeline_element_get_toplevel_parent (element);
+  gboolean ret = FALSE;
+  GESTimelineElement *toplevel = get_toplevel_container (element);
+  guint32 layer_prio = ges_timeline_element_get_layer_priority (element);
+
+  switch (edit->mode) {
+    case EDIT_MOVE:
+      GST_INFO_OBJECT (element, "Moving %s from %" GST_TIME_FORMAT " to %"
+          GST_TIME_FORMAT, element->name, GST_TIME_ARGS (element->start),
+          GST_TIME_ARGS (edit->start));
+      break;
+    case EDIT_TRIM_START:
+      GST_INFO_OBJECT (element, "Trimming %s start from %" GST_TIME_FORMAT
+          " to %" GST_TIME_FORMAT, element->name,
+          GST_TIME_ARGS (element->start), GST_TIME_ARGS (edit->start));
+      break;
+    case EDIT_TRIM_END:
+      GST_INFO_OBJECT (element, "Trimming %s end from %" GST_TIME_FORMAT
+          " to %" GST_TIME_FORMAT, element->name,
+          GST_TIME_ARGS (_END (element)),
+          GST_TIME_ARGS (element->start + edit->duration));
+      break;
+  }
 
-  GstClockTimeDiff n_start = GST_CLOCK_DIFF (offset, element->start);
-  GstClockTimeDiff n_inpoint = GST_CLOCK_DIFF (offset, element->inpoint);
-  GstClockTimeDiff n_duration = edge == GES_EDGE_END
-      ? GST_CLOCK_DIFF (offset, element->duration)
-      : element->duration + offset;
+  if (!GES_IS_CLIP (element) && !GES_IS_TRACK_ELEMENT (element)) {
+    GST_ERROR_OBJECT (element, "Cannot perform edit on group");
+    return FALSE;
+  }
 
-  if (data && GST_CLOCK_TIME_IS_VALID (data->trim_group_start))
-    g_assert (trim_group_get_vals (data, element, &n_start, &n_inpoint,
-            &n_duration));
+  if (!GES_IS_CLIP (element)
+      && edit->layer_priority != GES_TIMELINE_ELEMENT_NO_LAYER_PRIORITY) {
+    GST_ERROR_OBJECT (element, "Cannot move an element that is not a "
+        "clip to a new layer");
+    return FALSE;
+  }
 
   ELEMENT_SET_FLAG (element, GES_TIMELINE_ELEMENT_SET_SIMPLE);
   ELEMENT_SET_FLAG (toplevel, GES_TIMELINE_ELEMENT_SET_SIMPLE);
-  if (edge != GES_EDGE_END) {
-    ges_timeline_element_set_start (element, n_start);
-    ges_timeline_element_set_inpoint (element, n_inpoint);
+  if (GST_CLOCK_TIME_IS_VALID (edit->start)) {
+    if (!ges_timeline_element_set_start (element, edit->start)) {
+      GST_ERROR_OBJECT (element, "Failed to set the start");
+      goto done;
+    }
+  }
+  if (GST_CLOCK_TIME_IS_VALID (edit->inpoint)) {
+    if (!ges_timeline_element_set_inpoint (element, edit->inpoint)) {
+      GST_ERROR_OBJECT (element, "Failed to set the in-point");
+      goto done;
+    }
+  }
+  if (GST_CLOCK_TIME_IS_VALID (edit->duration)) {
+    if (!ges_timeline_element_set_duration (element, edit->duration)) {
+      GST_ERROR_OBJECT (element, "Failed to set the duration");
+      goto done;
+    }
   }
-  ges_timeline_element_set_duration (element, n_duration);
+  if (edit->layer_priority != GES_TIMELINE_ELEMENT_NO_LAYER_PRIORITY) {
+    GESTimeline *timeline = GES_TIMELINE_ELEMENT_TIMELINE (element);
+    GESLayer *layer = ges_timeline_get_layer (timeline, edit->layer_priority);
+
+    GST_INFO_OBJECT (element, "Moving %s from layer %" G_GUINT32_FORMAT
+        " to layer %" G_GUINT32_FORMAT, element->name, layer_prio,
+        edit->layer_priority);
+
+    if (layer == NULL) {
+      do {
+        layer = ges_timeline_append_layer (timeline);
+      } while (ges_layer_get_priority (layer) < edit->layer_priority);
+    } else {
+      gst_object_unref (layer);
+    }
 
-  GST_LOG ("Trimmed %" GES_FORMAT, GES_ARGS (element));
+    if (!ges_clip_move_to_layer (GES_CLIP (element), layer)) {
+      GST_ERROR_OBJECT (element, "Failed to move layers");
+      goto done;
+    }
+  }
+
+  ret = TRUE;
+
+done:
   ELEMENT_UNSET_FLAG (element, GES_TIMELINE_ELEMENT_SET_SIMPLE);
   ELEMENT_UNSET_FLAG (toplevel, GES_TIMELINE_ELEMENT_SET_SIMPLE);
-  gst_object_unref (toplevel);
+
+  return ret;
+}
+
+/* perform all the element edits found in @edits.
+ * These should only be clips of track elements. */
+static gboolean
+timeline_tree_perform_edits (GNode * root, GHashTable * edits)
+{
+  gboolean no_errors = TRUE;
+  GHashTableIter iter;
+  gpointer key, value;
+
+  g_hash_table_iter_init (&iter, edits);
+  while (g_hash_table_iter_next (&iter, &key, &value)) {
+    GESTimelineElement *element = key;
+    EditData *edit_data = value;
+    if (!perform_element_edit (element, edit_data))
+      no_errors = FALSE;
+  }
+
+  timeline_tree_create_transitions (root, ges_timeline_find_auto_transition);
+  timeline_update_transition (root->data);
+  timeline_update_duration (root->data);
+
+  return no_errors;
 }
 
-#define SET_TRIMMING_DATA(data, _edge, offset) G_STMT_START { \
-  data.edge = (_edge);                                           \
-  data.start_diff = (_edge) == GES_EDGE_END ? 0 : (offset); \
-  data.inpoint_diff = (_edge) == GES_EDGE_END ? 0 : (offset); \
-  data.duration_diff = (_edge) == GES_EDGE_END ? (offset) : -(offset); \
-  if (GES_IS_GROUP (data.element)) {\
-    data.trim_group_start = data.element->start;\
-    data.trim_group_end = _END (data.element); \
-  } \
-} G_STMT_END
+#define _REPLACE_TRACK_ELEMENT_WITH_PARENT(element) \
+  element = (GES_IS_TRACK_ELEMENT (element) && element->parent) ? element->parent : element
 
+/********************************************
+ *                 Ripple                   *
+ ********************************************/
 
 gboolean
-timeline_tree_trim (GNode * root, GESTimelineElement * element,
+timeline_tree_ripple (GNode * root, GESTimelineElement * element,
     gint64 layer_priority_offset, GstClockTimeDiff offset, GESEdge edge,
     GstClockTime snapping_distance)
 {
-  GHashTableIter iter;
   gboolean res = TRUE;
-  GESTimelineElement *elem;
-  gint64 new_layer_priority =
-      ((gint64) GES_TIMELINE_ELEMENT_LAYER_PRIORITY (element)) -
-      layer_priority_offset;
-  SnappingData snapping = {
-    .distance = snapping_distance,
-    .on_end_only = edge == GES_EDGE_END,
-    .on_start_only = edge != GES_EDGE_END,
-    .element = NULL,
-    .edge = GES_EDGE_NONE,
-    .diff = (GstClockTimeDiff) snapping_distance,
-  };
-  TreeIterationData data = tree_iteration_data_init;
+  GNode *node;
+  GESTimelineElement *ripple_toplevel;
+  GstClockTime ripple_time;
+  GHashTable *edits = new_edit_table ();
+  GHashTable *moving = new_position_table ();
+  ElementEditMode mode;
+  SnappedPosition *snap = new_snapped_position (snapping_distance);
+
+  _REPLACE_TRACK_ELEMENT_WITH_PARENT (element);
+
+  ripple_toplevel = get_toplevel_container (element);
+
+  /* if EDGE_END:
+   *   TRIM_END the element, and MOVE all toplevels whose start is after
+   *   the current end of the element by the same amount
+   * otherwise:
+   *   MOVE the topevel of the element, and all other toplevel elements
+   *   whose start is after the current start of the element */
+
+  switch (edge) {
+    case GES_EDGE_END:
+      GST_INFO_OBJECT (element, "Rippling end with offset %"
+          G_GINT64_FORMAT " and layer offset %" G_GINT64_FORMAT, offset,
+          layer_priority_offset);
+      mode = EDIT_TRIM_END;
+      break;
+    case GES_EDGE_START:
+      GST_INFO_OBJECT (element, "Rippling start with offset %"
+          G_GINT64_FORMAT " and layer offset %" G_GINT64_FORMAT, offset,
+          layer_priority_offset);
+      mode = EDIT_MOVE;
+      break;
+    case GES_EDGE_NONE:
+      GST_INFO_OBJECT (element, "Rippling with toplevel with offset %"
+          G_GINT64_FORMAT " and layer offset %" G_GINT64_FORMAT, offset,
+          layer_priority_offset);
+      element = ripple_toplevel;
+      mode = EDIT_MOVE;
+      break;
+    default:
+      GST_WARNING_OBJECT (element, "Edge not supported");
+      goto done;
+  }
 
-  /* Make sure to check all children of clips */
-  if (GES_IS_TRACK_ELEMENT (element) && element->parent)
-    element = element->parent;
+  ripple_time = ELEMENT_EDGE_VALUE (element, edge);
 
-  data.root = root;
-  data.element = element;
-  data.priority_diff =
-      (gint64) ges_timeline_element_get_layer_priority (element) -
-      new_layer_priority;
-  data.snapping = snapping_distance ? &snapping : NULL;
-  data.moved_clips = g_hash_table_new (g_direct_hash, g_direct_equal);
-  SET_TRIMMING_DATA (data, edge, offset);
-  GST_INFO ("%" GES_FORMAT " trimming %s with offset %" G_GINT64_FORMAT "",
-      GES_ARGS (element), edge == GES_EDGE_END ? "end" : "start", offset);
-  g_node_traverse (find_node (root, get_toplevel_container (element)),
-      G_IN_ORDER, G_TRAVERSE_LEAVES, -1,
-      (GNodeTraverseFunc) add_element_to_list, &data.movings);
-
-  if (!timeline_tree_can_trim_element_internal (root, &data)) {
-    GST_INFO ("Can not trim object.");
+  /* add edits */
+  if (!add_element_edit (edits, element, mode))
     goto error;
-  }
 
-  if (snapping_distance) {
-    if (snapping.element) {
-      offset =
-          GST_CLOCK_DIFF (ELEMENT_EDGE_VALUE (snapping.element, snapping.edge),
-          ELEMENT_EDGE_VALUE (snapping.moving_element, snapping.moving_edge));
+  for (node = root->children; node; node = node->next) {
+    GESTimelineElement *toplevel = node->data;
+    if (toplevel == ripple_toplevel)
+      continue;
 
-      GST_INFO ("Snapping on %" GES_FORMAT "%s %" G_GINT64_FORMAT
-          " -- offset: %" G_GINT64_FORMAT "", GES_ARGS (snapping.element),
-          snapping.edge == GES_EDGE_END ? "end" : "start",
-          ELEMENT_EDGE_VALUE (snapping.element, snapping.edge), offset);
+    if (toplevel->start >= ripple_time) {
+      if (!add_element_edit (edits, toplevel, EDIT_MOVE))
+        goto error;
     }
-
-    ges_timeline_emit_snapping (root->data, element,
-        snapping.element,
-        snapping.element ? ELEMENT_EDGE_VALUE (snapping.element,
-            snapping.edge) : GST_CLOCK_TIME_NONE);
   }
 
-  g_hash_table_iter_init (&iter, data.moved_clips);
-  while (g_hash_table_iter_next (&iter, (gpointer *) & elem, NULL))
-    trim_simple (elem, offset, edge, &data);
+  if (!timeline_tree_add_edited_to_moving (root, edits, moving))
+    goto error;
+
+  /* snap */
+  if (!timeline_tree_snap (root, element, mode, &offset, moving, snap))
+    goto error;
 
-  timeline_tree_create_transitions (root, ges_timeline_find_auto_transition);
-  timeline_update_transition (root->data);
-  timeline_update_duration (root->data);
+  /* check and set edits using snapped values */
+  give_edits_same_offset (edits, offset, layer_priority_offset);
+  if (!timeline_tree_set_element_edit_values (root, edits))
+    goto error;
+
+  /* check overlaps */
+  set_moving_positions_from_edits (moving, edits);
+  if (!timeline_tree_can_move_elements (root, moving))
+    goto error;
+
+  /* emit snapping now. Edits should only fail if a programming error
+   * occured */
+  if (snap)
+    ges_timeline_emit_snapping (root->data, snap->element, snap->snapped_to,
+        snap->snapped);
+
+  res = timeline_tree_perform_edits (root, edits);
 
 done:
-  clean_iteration_data (&data);
+  g_hash_table_unref (edits);
+  g_hash_table_unref (moving);
+  g_free (snap);
   return res;
 
 error:
@@ -1019,99 +1681,164 @@ error:
   goto done;
 }
 
+/********************************************
+ *                  Trim                    *
+ ********************************************/
+
 gboolean
-timeline_tree_move (GNode * root, GESTimelineElement * element,
+timeline_tree_trim (GNode * root, GESTimelineElement * element,
     gint64 layer_priority_offset, GstClockTimeDiff offset, GESEdge edge,
     GstClockTime snapping_distance)
 {
   gboolean res = TRUE;
-  GESTimelineElement *toplevel = get_toplevel_container (element);
-  TreeIterationData data = tree_iteration_data_init;
-  SnappingData snapping = {
-    .distance = snapping_distance,
-    .on_end_only = edge == GES_EDGE_END,
-    .on_start_only = edge == GES_EDGE_END,
-    .element = NULL,
-    .edge = GES_EDGE_NONE,
-    .diff = (GstClockTimeDiff) snapping_distance,
-  };
+  GHashTable *edits = new_edit_table ();
+  GHashTable *moving = new_position_table ();
+  ElementEditMode mode;
+  SnappedPosition *snap = new_snapped_position (snapping_distance);
 
-  data.root = root;
-  data.element = edge == GES_EDGE_END ? element : toplevel;
-  data.edge = edge;
-  data.priority_diff = layer_priority_offset;
-  data.snapping = snapping_distance ? &snapping : NULL;
-  data.start_diff = edge == GES_EDGE_END ? 0 : offset;
-  data.duration_diff = edge == GES_EDGE_END ? offset : 0;
-
-  GST_INFO ("%" GES_FORMAT
-      " moving %s with offset %" G_GINT64_FORMAT ", (snaping distance: %"
-      G_GINT64_FORMAT ")", GES_ARGS (element),
-      edge == GES_EDGE_END ? "end" : "start", offset, snapping_distance);
-  g_node_traverse (find_node (root, data.element), G_IN_ORDER,
-      G_TRAVERSE_LEAVES, -1, (GNodeTraverseFunc) add_element_to_list,
-      &data.movings);
-
-  if (!timeline_tree_can_move_element_from_data (root, &data)) {
-    GST_INFO ("Can not move object.");
+  _REPLACE_TRACK_ELEMENT_WITH_PARENT (element);
+
+  /* TODO: 2.0 remove this warning and simply fail if no edge is specified */
+  if (edge == GES_EDGE_NONE) {
+    g_warning ("No edge specified for trimming. Defaulting to GES_EDGE_START");
+    edge = GES_EDGE_START;
+  }
+
+  switch (edge) {
+    case GES_EDGE_END:
+      GST_INFO_OBJECT (element, "Trimming end with offset %"
+          G_GINT64_FORMAT " and layer offset %" G_GINT64_FORMAT, offset,
+          layer_priority_offset);
+      mode = EDIT_TRIM_END;
+      break;
+    case GES_EDGE_START:
+      GST_INFO_OBJECT (element, "Trimming start with offset %"
+          G_GINT64_FORMAT " and layer offset %" G_GINT64_FORMAT, offset,
+          layer_priority_offset);
+      mode = EDIT_TRIM_START;
+      break;
+    default:
+      GST_WARNING_OBJECT (element, "Edge not supported");
+      goto done;
+  }
+
+  /* add edits */
+  if (!add_element_edit (edits, element, mode))
+    goto error;
+
+  if (!timeline_tree_add_edited_to_moving (root, edits, moving))
+    goto error;
+
+  /* snap */
+  if (!timeline_tree_snap (root, element, mode, &offset, moving, snap))
+    goto error;
+
+  /* check and set edits using snapped values */
+  give_edits_same_offset (edits, offset, layer_priority_offset);
+  if (!timeline_tree_set_element_edit_values (root, edits))
+    goto error;
+
+  /* check overlaps */
+  set_moving_positions_from_edits (moving, edits);
+  if (!timeline_tree_can_move_elements (root, moving)) {
     goto error;
   }
 
-  if (snapping_distance) {
-    if (snapping.element) {
-      gint64 noffset =
-          GST_CLOCK_DIFF (ELEMENT_EDGE_VALUE (snapping.element, snapping.edge),
-          ELEMENT_EDGE_VALUE (snapping.moving_element, snapping.moving_edge));
-
-      GST_INFO ("Snapping %" GES_FORMAT " (%s) with %" GES_FORMAT
-          "%s %" G_GINT64_FORMAT " -- offset: %" G_GINT64_FORMAT
-          " (previous offset: %" G_GINT64_FORMAT ")",
-          GES_ARGS (snapping.moving_element),
-          snapping.moving_edge == GES_EDGE_END ? "end" : "start",
-          GES_ARGS (snapping.element),
-          snapping.edge == GES_EDGE_END ? "end" : "start",
-          ELEMENT_EDGE_VALUE (snapping.element, snapping.edge), noffset,
-          offset);
-      offset = noffset;
-      data.start_diff = edge == GES_EDGE_END ? 0 : offset;
-      data.duration_diff = edge == GES_EDGE_END ? offset : 0;
-      data.snapping = NULL;
-      if (!timeline_tree_can_move_element_from_data (root, &data)) {
-        GST_INFO ("Can not move object.");
-        goto error;
-      }
-    }
+  /* emit snapping now. Edits should only fail if a programming error
+   * occured */
+  if (snap)
+    ges_timeline_emit_snapping (root->data, snap->element, snap->snapped_to,
+        snap->snapped);
+
+  res = timeline_tree_perform_edits (root, edits);
+
+done:
+  g_hash_table_unref (edits);
+  g_hash_table_unref (moving);
+  g_free (snap);
+  return res;
 
-    ges_timeline_emit_snapping (root->data, element,
-        snapping.element,
-        snapping.element ? ELEMENT_EDGE_VALUE (snapping.element,
-            snapping.edge) : GST_CLOCK_TIME_NONE);
+error:
+  res = FALSE;
+  goto done;
+}
+
+/********************************************
+ *                  Move                    *
+ ********************************************/
+
+gboolean
+timeline_tree_move (GNode * root, GESTimelineElement * element,
+    gint64 layer_priority_offset, GstClockTimeDiff offset, GESEdge edge,
+    GstClockTime snapping_distance)
+{
+  gboolean res = TRUE;
+  GHashTable *edits = new_edit_table ();
+  GHashTable *moving = new_position_table ();
+  ElementEditMode mode;
+  SnappedPosition *snap = new_snapped_position (snapping_distance);
+
+  _REPLACE_TRACK_ELEMENT_WITH_PARENT (element);
+
+  switch (edge) {
+    case GES_EDGE_END:
+      GST_INFO_OBJECT (element, "Moving end with offset %"
+          G_GINT64_FORMAT " and layer offset %" G_GINT64_FORMAT, offset,
+          layer_priority_offset);
+      mode = EDIT_TRIM_END;
+      break;
+    case GES_EDGE_START:
+      GST_INFO_OBJECT (element, "Moving start with offset %"
+          G_GINT64_FORMAT " and layer offset %" G_GINT64_FORMAT, offset,
+          layer_priority_offset);
+      mode = EDIT_MOVE;
+      break;
+    case GES_EDGE_NONE:
+      GST_INFO_OBJECT (element, "Moving with toplevel with offset %"
+          G_GINT64_FORMAT " and layer offset %" G_GINT64_FORMAT, offset,
+          layer_priority_offset);
+      element = get_toplevel_container (element);
+      mode = EDIT_MOVE;
+      break;
+    default:
+      GST_WARNING_OBJECT (element, "Edge not supported");
+      goto done;
   }
 
-  if (!check_can_move_to_layer (toplevel, layer_priority_offset)) {
-    GST_INFO ("%" GES_FORMAT " would land in a layer with negative priority",
-        GES_ARGS (toplevel));
+  /* add edits */
+  if (!add_element_edit (edits, element, mode))
     goto error;
-  }
 
-  ELEMENT_SET_FLAG (toplevel, GES_TIMELINE_ELEMENT_SET_SIMPLE);
-  if (edge == GES_EDGE_END)
-    ges_timeline_element_set_duration (element, GST_CLOCK_DIFF (offset,
-            element->duration));
-  else
-    ges_timeline_element_set_start (toplevel, GST_CLOCK_DIFF (offset,
-            toplevel->start));
-  move_to_new_layer (toplevel, layer_priority_offset);
-  ELEMENT_UNSET_FLAG (toplevel, GES_TIMELINE_ELEMENT_SET_SIMPLE);
+  if (!timeline_tree_add_edited_to_moving (root, edits, moving))
+    goto error;
 
-  timeline_tree_create_transitions (root, ges_timeline_find_auto_transition);
-  timeline_update_transition (root->data);
-  timeline_update_duration (root->data);
+  /* snap */
+  if (!timeline_tree_snap (root, element, mode, &offset, moving, snap))
+    goto error;
 
-  GST_LOG ("Moved %" GES_FORMAT, GES_ARGS (element));
+  /* check and set edits using snapped values */
+  give_edits_same_offset (edits, offset, layer_priority_offset);
+  if (!timeline_tree_set_element_edit_values (root, edits))
+    goto error;
+
+  /* check overlaps */
+  set_moving_positions_from_edits (moving, edits);
+  if (!timeline_tree_can_move_elements (root, moving)) {
+    goto error;
+  }
+
+  /* emit snapping now. Edits should only fail if a programming error
+   * occured */
+  if (snap)
+    ges_timeline_emit_snapping (root->data, snap->element, snap->snapped_to,
+        snap->snapped);
+
+  res = timeline_tree_perform_edits (root, edits);
 
 done:
-  clean_iteration_data (&data);
+  g_hash_table_unref (edits);
+  g_hash_table_unref (moving);
+  g_free (snap);
   return res;
 
 error:
@@ -1119,41 +1846,72 @@ error:
   goto done;
 }
 
+/********************************************
+ *                  Roll                    *
+ ********************************************/
+
+static gboolean
+is_descendant (GESTimelineElement * element, GESTimelineElement * ancestor)
+{
+  GESTimelineElement *parent = element;
+  while ((parent = parent->parent)) {
+    if (parent == ancestor)
+      return TRUE;
+  }
+  return FALSE;
+}
+
 static gboolean
 find_neighbour (GNode * node, TreeIterationData * data)
 {
-  gboolean in_same_track = FALSE;
   GList *tmp;
+  gboolean in_same_track = FALSE;
+  GESTimelineElement *edge_element, *element = node->data;
 
-  if (!GES_IS_SOURCE (node->data)) {
+  if (!GES_IS_SOURCE (element))
     return FALSE;
-  }
 
+  /* if the element is controlled by the trimmed element (a group or a
+   * clip) it is not a neighbour */
+  if (is_descendant (element, data->element))
+    return FALSE;
 
-  for (tmp = GES_CONTAINER_CHILDREN (data->element); tmp; tmp = tmp->next) {
-    if (tmp->data == node->data)
-      return FALSE;
-
-    if (ges_track_element_get_track (node->data) ==
+  /* test if we share a track with one of the sources at the edge */
+  for (tmp = data->sources; tmp; tmp = tmp->next) {
+    if (ges_track_element_get_track (GES_TRACK_ELEMENT (element)) ==
         ges_track_element_get_track (tmp->data)) {
       in_same_track = TRUE;
+      break;
     }
   }
 
-  if (!in_same_track) {
+  if (!in_same_track)
     return FALSE;
-  }
 
-  if (ELEMENT_EDGE_VALUE (node->data,
-          data->edge == GES_EDGE_START ? GES_EDGE_END : GES_EDGE_START) ==
-      ELEMENT_EDGE_VALUE (data->element, data->edge)) {
-    if (!g_list_find (data->neighbours,
-            GES_TIMELINE_ELEMENT_PARENT (node->data)))
-      data->neighbours =
-          g_list_prepend (data->neighbours,
-          GES_TIMELINE_ELEMENT_PARENT (node->data));
+  /* get the most toplevel element whose edge touches the position */
+  edge_element = NULL;
+  while (element && ELEMENT_EDGE_VALUE (element, data->edge) == data->position) {
+    edge_element = element;
+    element = element->parent;
   }
 
+  if (edge_element && !g_list_find (data->neighbours, edge_element))
+    data->neighbours = g_list_prepend (data->neighbours, edge_element);
+
+  return FALSE;
+}
+
+static gboolean
+find_sources_at_position (GNode * node, TreeIterationData * data)
+{
+  GESTimelineElement *element = node->data;
+
+  if (!GES_IS_SOURCE (element))
+    return FALSE;
+
+  if (ELEMENT_EDGE_VALUE (element, data->edge) == data->position)
+    data->sources = g_list_append (data->sources, element);
+
   return FALSE;
 }
 
@@ -1163,100 +1921,108 @@ timeline_tree_roll (GNode * root, GESTimelineElement * element,
 {
   gboolean res = TRUE;
   GList *tmp;
-  GESEdge neighbour_edge;
+  GNode *node;
   TreeIterationData data = tree_iteration_data_init;
-  SnappingData snapping = {
-    .distance = snapping_distance,
-    .on_end_only = edge == GES_EDGE_END,
-    .on_start_only = edge == GES_EDGE_END,
-    .element = NULL,
-    .edge = GES_EDGE_NONE,
-    .moving_edge = GES_EDGE_NONE,
-    .diff = (GstClockTimeDiff) snapping_distance,
-  };
-
-  data.root = root;
-  data.element = element;
-  data.edge = edge;
-  data.snapping = snapping_distance ? &snapping : NULL;
-  data.start_diff = edge == GES_EDGE_END ? 0 : offset;
-  data.inpoint_diff = edge == GES_EDGE_END ? 0 : offset;
-  data.duration_diff = edge == GES_EDGE_END ? offset : -offset;
-  data.ripple_time = GST_CLOCK_TIME_NONE;
-  neighbour_edge = data.edge == GES_EDGE_END ? GES_EDGE_START : GES_EDGE_END;
-
-  SET_TRIMMING_DATA (data, edge, offset);
-  g_node_traverse (root, G_PRE_ORDER, G_TRAVERSE_LEAFS, -1,
-      (GNodeTraverseFunc) find_neighbour, &data);
-
-  if (data.neighbours == NULL) {
-    GST_INFO ("%s doesn't have any direct neighbour on edge %s",
-        element->name, ges_edge_name (edge));
-
-    return timeline_tree_trim (root, element, 0, offset, edge,
-        snapping_distance);
+  GHashTable *edits = new_edit_table ();
+  GHashTable *moving = new_position_table ();
+  ElementEditMode mode;
+  SnappedPosition *snap = new_snapped_position (snapping_distance);
+
+  _REPLACE_TRACK_ELEMENT_WITH_PARENT (element);
+
+  /* if EDGE_END:
+   *   TRIM_END the element, and TRIM_START the neighbouring clips to the
+   *   end edge
+   * otherwise:
+   *   TRIM_START the element, and TRIM_END the neighbouring clips to the
+   *   start edge */
+
+  switch (edge) {
+    case GES_EDGE_END:
+      GST_INFO_OBJECT (element, "Rolling end with offset %"
+          G_GINT64_FORMAT, offset);
+      mode = EDIT_TRIM_END;
+      break;
+    case GES_EDGE_START:
+      GST_INFO_OBJECT (element, "Rolling start with offset %"
+          G_GINT64_FORMAT, offset);
+      mode = EDIT_TRIM_START;
+      break;
+    case GES_EDGE_NONE:
+      GST_WARNING_OBJECT (element, "Need to select an edge when rolling.");
+      goto done;
+    default:
+      GST_WARNING_OBJECT (element, "Edge not supported");
+      goto done;
   }
 
-  GST_INFO ("Trimming %" GES_FORMAT " %s to %" G_GINT64_FORMAT "",
-      GES_ARGS (data.element), ges_edge_name (edge), offset);
+  /* add edits */
+  if (!add_element_edit (edits, element, mode))
+    goto error;
 
-  if (!timeline_tree_can_move_element_from_data (root, &data))
+  /* first, find all the sources at the edge */
+  node = find_node (root, element);
+  if (!node) {
+    GST_ERROR_OBJECT (element, "Not being tracked");
     goto error;
+  }
 
+  data.element = element;
+  data.edge = (edge == GES_EDGE_END) ? GES_EDGE_END : GES_EDGE_START;
+  data.position = ELEMENT_EDGE_VALUE (element, data.edge);
+  data.sources = NULL;
 
-  if (snapping_distance) {
-    if (snapping.element) {
-      gint64 noffset =
-          GST_CLOCK_DIFF (ELEMENT_EDGE_VALUE (snapping.element, snapping.edge),
-          ELEMENT_EDGE_VALUE (snapping.moving_element, snapping.moving_edge));
+  g_node_traverse (node, G_IN_ORDER, G_TRAVERSE_LEAVES, -1,
+      (GNodeTraverseFunc) find_sources_at_position, &data);
 
-      GST_INFO ("Snapping %" GES_FORMAT " (%s) with %" GES_FORMAT
-          "%s %" G_GINT64_FORMAT " -- offset: %" G_GINT64_FORMAT
-          " (previous offset: %" G_GINT64_FORMAT ")",
-          GES_ARGS (snapping.moving_element),
-          snapping.moving_edge == GES_EDGE_END ? "end" : "start",
-          GES_ARGS (snapping.element),
-          snapping.edge == GES_EDGE_END ? "end" : "start",
-          ELEMENT_EDGE_VALUE (snapping.element, snapping.edge), noffset,
-          offset);
-      offset = noffset;
+  /* find elements that whose opposite edge touches the edge of the
+   * element and shares a track with one of the found sources */
+  data.edge = (edge == GES_EDGE_END) ? GES_EDGE_START : GES_EDGE_END;
+  data.neighbours = NULL;
 
-      SET_TRIMMING_DATA (data, edge, offset);
+  g_node_traverse (root, G_PRE_ORDER, G_TRAVERSE_LEAVES, -1,
+      (GNodeTraverseFunc) find_neighbour, &data);
 
-      if (!timeline_tree_can_move_element_from_data (root, &data)) {
-        GST_INFO ("Can not move object.");
-        goto error;
-      }
-    }
+  for (tmp = data.neighbours; tmp; tmp = tmp->next) {
+    GESTimelineElement *clip = tmp->data;
+    ElementEditMode opposite =
+        (mode == EDIT_TRIM_END) ? EDIT_TRIM_START : EDIT_TRIM_END;
+    if (!add_element_edit (edits, clip, opposite))
+      goto error;
   }
 
-  if (snapping_distance && snapping.element) {
-    ges_timeline_emit_snapping (root->data, element,
-        snapping.element,
-        snapping.element ? ELEMENT_EDGE_VALUE (snapping.element,
-            snapping.edge) : GST_CLOCK_TIME_NONE);
-  }
+  if (!timeline_tree_add_edited_to_moving (root, edits, moving))
+    goto error;
 
-  data.snapping = NULL;
-  SET_TRIMMING_DATA (data, neighbour_edge, offset);
-  for (tmp = data.neighbours; tmp; tmp = tmp->next) {
-    data.element = tmp->data;
+  /* snap */
+  if (!timeline_tree_snap (root, element, mode, &offset, moving, snap))
+    goto error;
 
-    GST_INFO ("Trimming %" GES_FORMAT " %s to %" G_GINT64_FORMAT "",
-        GES_ARGS (data.element), ges_edge_name (data.edge), offset);
-    if (!timeline_tree_can_move_element_from_data (root, &data)) {
-      GST_INFO ("Can not move object.");
-      goto error;
-    }
+  /* check and set edits using snapped values */
+  give_edits_same_offset (edits, offset, 0);
+  if (!timeline_tree_set_element_edit_values (root, edits))
+    goto error;
+
+  /* check overlaps */
+  set_moving_positions_from_edits (moving, edits);
+  if (!timeline_tree_can_move_elements (root, moving)) {
+    goto error;
   }
 
-  trim_simple (element, offset, edge, NULL);
-  for (tmp = data.neighbours; tmp; tmp = tmp->next)
-    trim_simple (tmp->data, offset, data.edge, NULL);
+  /* emit snapping now. Edits should only fail if a programming error
+   * occured */
+  if (snap)
+    ges_timeline_emit_snapping (root->data, snap->element, snap->snapped_to,
+        snap->snapped);
+
+  res = timeline_tree_perform_edits (root, edits);
 
 done:
-  timeline_update_duration (root->data);
+  g_hash_table_unref (edits);
+  g_hash_table_unref (moving);
   g_list_free (data.neighbours);
+  g_list_free (data.sources);
+  g_free (snap);
   return res;
 
 error:
@@ -1296,7 +2062,6 @@ create_transitions (GNode * node,
     return FALSE;
 
   timeline = GES_TIMELINE_ELEMENT_TIMELINE (node->data);
-  data.element = node->data;
   if (!GES_IS_SOURCE (node->data))
     return FALSE;
 
@@ -1314,8 +2079,8 @@ create_transitions (GNode * node,
   if (!ges_layer_get_auto_transition (layer))
     return FALSE;
 
-  g_node_traverse (g_node_get_root (node), G_IN_ORDER, G_TRAVERSE_LEAVES, -1,
-      (GNodeTraverseFunc) check_track_elements_overlaps_and_values, &data);
+  data.root = g_node_get_root (node);
+  check_all_overlaps_with_element (node, &data);
 
   if (data.overlaping_on_start)
     create_transition_if_needed (timeline,
index a65a19f0aff21257fa0472f94b0b36545d535cf2..17146b2cab0ac0963d5f70808b034751e2ad2055 100644 (file)
@@ -13,19 +13,18 @@ gboolean timeline_tree_can_move_element   (GNode *root,
                                            GESTimelineElement *element,
                                            guint32 priority,
                                            GstClockTime start,
-                                           GstClockTime duration,
-                                           GList *moving_track_elements);
+                                           GstClockTime duration);
 
 gboolean timeline_tree_ripple             (GNode *root,
+                                           GESTimelineElement *element,
                                            gint64 layer_priority_offset,
                                            GstClockTimeDiff offset,
-                                           GESTimelineElement *rippled_element,
-                                           GESEdge moving_edge,
+                                           GESEdge edge,
                                            GstClockTime snapping_distance);
 
 void ges_timeline_emit_snapping           (GESTimeline * timeline,
-                                           GESTimelineElement * elem1,
-                                           GESTimelineElement * elem2,
+                                           GESTrackElement * elem1,
+                                           GESTrackElement * elem2,
                                            GstClockTime snap_time);
 
 gboolean timeline_tree_trim               (GNode *root,
index 3d977ea26ff7fe12e9c427b577a3d35259ee46ee..4a9f418693a1a2d7ec10f032be270f2f1b8fac4b 100644 (file)
@@ -146,10 +146,6 @@ struct _GESTimelinePrivate
   GESTrackElement *last_snaped1;
   GESTrackElement *last_snaped2;
 
-  /* This variable is set to %TRUE when it makes sense to update the transitions,
-   * and %FALSE otherwize */
-  gboolean needs_transitions_update;
-
   GESTrack *auto_transition_track;
   GESTrack *new_track;
 
@@ -559,9 +555,9 @@ ges_timeline_class_init (GESTimelineClass * klass)
    * GESTimeline:snapping-distance:
    *
    * The distance (in nanoseconds) at which a #GESTimelineElement being
-   * moved within the timeline should snap to its neighbours. Note that
-   * such a neighbour includes any element in the timeline, including
-   * across separate layers. 0 means no snapping.
+   * moved within the timeline should snap one of its #GESSource-s with
+   * another #GESSource-s edge. See #GESEditMode for which edges can
+   * snap during an edit. 0 means no snapping.
    */
   properties[PROP_SNAPPING_DISTANCE] =
       g_param_spec_uint64 ("snapping-distance", "Snapping distance",
@@ -674,9 +670,14 @@ ges_timeline_class_init (GESTimelineClass * klass)
    * @position: The position where the two objects will snap to
    *
    * Will be emitted whenever an element's movement invokes a snapping
-   * event (usually by its controlling #GESClip being moved) because its
+   * event during an edit (usually of one of its ancestors) because its
    * start or end point lies within the #GESTimeline:snapping-distance of
    * another element's start or end point.
+   *
+   * See #GESEditMode to see what can snap during an edit.
+   *
+   * Note that only up to one snapping-started signal will be emitted per
+   * element edit within a timeline.
    */
   ges_timeline_signals[SNAPING_STARTED] =
       g_signal_new ("snapping-started", G_TYPE_FROM_CLASS (klass),
@@ -692,11 +693,10 @@ ges_timeline_class_init (GESTimelineClass * klass)
    * @position: The position where the two objects were to be snapped to
    *
    * Will be emitted whenever a snapping event ends. After a snap event
-   * has started (see #GESTimeline::snapping-started), it can end because
-   * the element whose movement created the snap event has since moved
-   * outside of the #GESTimeline:snapping-distance before its position was
-   * committed. It can also end because the element's movement was ended
-   * by a timeline being committed.
+   * has started (see #GESTimeline::snapping-started), it can later end
+   * because either another timeline edit has occurred (which may or may
+   * not have created a new snapping event), or because the timeline has
+   * been committed.
    */
   ges_timeline_signals[SNAPING_ENDED] =
       g_signal_new ("snapping-ended", G_TYPE_FROM_CLASS (klass),
@@ -805,7 +805,6 @@ ges_timeline_init (GESTimeline * self)
   self->priv->last_snap_ts = GST_CLOCK_TIME_NONE;
 
   priv->priv_tracks = NULL;
-  priv->needs_transitions_update = TRUE;
 
   priv->all_elements =
       g_hash_table_new_full (g_str_hash, g_str_equal, g_free, gst_object_unref);
@@ -1029,8 +1028,8 @@ _create_auto_transition_from_transitions (GESTimeline * timeline,
 }
 
 void
-ges_timeline_emit_snapping (GESTimeline * timeline, GESTimelineElement * elem1,
-    GESTimelineElement * elem2, GstClockTime snap_time)
+ges_timeline_emit_snapping (GESTimeline * timeline, GESTrackElement * elem1,
+    GESTrackElement * elem2, GstClockTime snap_time)
 {
   GESTimelinePrivate *priv = timeline->priv;
   GstClockTime last_snap_ts = timeline->priv->last_snap_ts;
@@ -1048,15 +1047,6 @@ ges_timeline_emit_snapping (GESTimeline * timeline, GESTimelineElement * elem1,
   }
 
   g_assert (elem1 != elem2);
-  if (GES_IS_CLIP (elem1)) {
-    g_assert (GES_CONTAINER_CHILDREN (elem1));
-    elem1 = GES_CONTAINER_CHILDREN (elem1)->data;
-  }
-
-  if (GES_IS_CLIP (elem2)) {
-    g_assert (GES_CONTAINER_CHILDREN (elem2));
-    elem2 = GES_CONTAINER_CHILDREN (elem2)->data;
-  }
 
   if (last_snap_ts != snap_time) {
     g_signal_emit (timeline, ges_timeline_signals[SNAPING_ENDED], 0,
@@ -1067,10 +1057,9 @@ ges_timeline_emit_snapping (GESTimeline * timeline, GESTimelineElement * elem1,
   }
 
   if (!GST_CLOCK_TIME_IS_VALID (timeline->priv->last_snap_ts)) {
-    priv->last_snaped1 = (GESTrackElement *) elem1;
-    priv->last_snaped2 = (GESTrackElement *) elem2;
+    priv->last_snaped1 = elem1;
+    priv->last_snaped2 = elem2;
     timeline->priv->last_snap_ts = snap_time;
-
     g_signal_emit (timeline, ges_timeline_signals[SNAPING_STARTED], 0,
         elem1, elem2, snap_time);
   }
@@ -1129,91 +1118,14 @@ done:
 }
 
 
-gboolean
-ges_timeline_trim_object_simple (GESTimeline * timeline,
-    GESTimelineElement * element, guint32 new_layer_priority,
-    GList * layers, GESEdge edge, guint64 position, gboolean snapping)
-{
-
-  return timeline_trim_object (timeline, element, new_layer_priority, layers,
-      edge, position);
-}
-
-gboolean
-timeline_ripple_object (GESTimeline * timeline, GESTimelineElement * obj,
-    gint new_layer_priority, GList * layers, GESEdge edge, guint64 position)
-{
-  gboolean res = TRUE;
-  guint64 new_duration;
-  GstClockTimeDiff diff;
-
-  switch (edge) {
-    case GES_EDGE_NONE:
-      GST_DEBUG ("Simply rippling");
-      diff = GST_CLOCK_DIFF (position, _START (obj));
-
-      timeline->priv->needs_transitions_update = FALSE;
-      res = timeline_tree_ripple (timeline->priv->tree,
-          (gint64) GES_TIMELINE_ELEMENT_LAYER_PRIORITY (obj) -
-          (gint64) new_layer_priority, diff, obj,
-          GES_EDGE_NONE, timeline->priv->snapping_distance);
-      timeline->priv->needs_transitions_update = TRUE;
-
-      break;
-    case GES_EDGE_END:
-      GST_DEBUG ("Rippling end");
-
-      timeline->priv->needs_transitions_update = FALSE;
-      new_duration =
-          CLAMP (GST_CLOCK_DIFF (obj->start, position), 0,
-          GST_CLOCK_TIME_IS_VALID (obj->
-              maxduration) ? GST_CLOCK_DIFF (obj->inpoint,
-              obj->maxduration) : GST_CLOCK_TIME_NONE);
-      res =
-          timeline_tree_ripple (timeline->priv->tree,
-          (gint64) GES_TIMELINE_ELEMENT_LAYER_PRIORITY (obj) -
-          (gint64) new_layer_priority, _DURATION (obj) - new_duration, obj,
-          GES_EDGE_END, timeline->priv->snapping_distance);
-      timeline->priv->needs_transitions_update = TRUE;
-
-      GST_DEBUG ("Done Rippling end");
-      break;
-    case GES_EDGE_START:
-      GST_INFO ("Ripple start doesn't make sense, trimming instead");
-      if (!timeline_trim_object (timeline, obj, -1, layers, edge, position))
-        goto error;
-      break;
-    default:
-      GST_DEBUG ("Can not ripple edge: %i", edge);
-
-      break;
-  }
-
-  return res;
-
-error:
-
-  return FALSE;
-}
-
-gboolean
-timeline_slide_object (GESTimeline * timeline, GESTrackElement * obj,
-    GList * layers, GESEdge edge, guint64 position)
-{
-
-  /* FIXME implement me! */
-  GST_FIXME_OBJECT (timeline, "Slide mode editing not implemented yet");
-
-  return FALSE;
-}
-
-static gboolean
-_trim_transition (GESTimeline * timeline, GESTimelineElement * element,
-    GESEdge edge, GstClockTime position)
+static gint
+_edit_auto_transition (GESTimeline * timeline, GESTimelineElement * element,
+    GList * layers, gint64 new_layer_priority, GESEditMode mode, GESEdge edge,
+    GstClockTime position)
 {
   GList *tmp;
-  GESLayer *layer = ges_timeline_get_layer (timeline,
-      GES_TIMELINE_ELEMENT_LAYER_PRIORITY (element));
+  guint32 layer_prio = ges_timeline_element_get_layer_priority (element);
+  GESLayer *layer = ges_timeline_get_layer (timeline, layer_prio);
 
   if (!ges_layer_get_auto_transition (layer)) {
     gst_object_unref (layer);
@@ -1222,86 +1134,83 @@ _trim_transition (GESTimeline * timeline, GESTimelineElement * element,
 
   gst_object_unref (layer);
   for (tmp = timeline->priv->auto_transitions; tmp; tmp = tmp->next) {
+    GESTimelineElement *replace;
     GESAutoTransition *auto_transition = tmp->data;
 
     if (GES_TIMELINE_ELEMENT (auto_transition->transition) == element ||
         GES_TIMELINE_ELEMENT (auto_transition->transition_clip) == element) {
-      /* Trimming an auto transition means trimming its neighboors */
-      if (!auto_transition->positioning) {
-        if (edge == GES_EDGE_END) {
-          ges_container_edit (GES_CONTAINER (auto_transition->previous_clip),
-              NULL, -1, GES_EDIT_MODE_TRIM, GES_EDGE_END, position);
-        } else {
-          ges_container_edit (GES_CONTAINER (auto_transition->next_clip),
-              NULL, -1, GES_EDIT_MODE_TRIM, GES_EDGE_START, position);
-        }
-
-        return TRUE;
+      if (auto_transition->positioning) {
+        GST_ERROR_OBJECT (element, "Trying to edit an auto-transition "
+            "whilst it is being positioned");
+        return FALSE;
+      }
+      if (new_layer_priority != layer_prio) {
+        GST_WARNING_OBJECT (element, "Cannot edit an auto-transition to a "
+            "new layer");
+        return FALSE;
+      }
+      if (mode != GES_EDIT_MODE_TRIM) {
+        GST_WARNING_OBJECT (element, "Cannot edit an auto-transition "
+            "under the edit mode %i", mode);
+        return FALSE;
       }
 
-      return FALSE;
-    }
-  }
-
-  return FALSE;
-}
+      if (edge == GES_EDGE_END)
+        replace = GES_TIMELINE_ELEMENT (auto_transition->previous_clip);
+      else
+        replace = GES_TIMELINE_ELEMENT (auto_transition->next_clip);
 
-gboolean
-timeline_trim_object (GESTimeline * timeline, GESTimelineElement * object,
-    guint32 new_layer_priority, GList * layers, GESEdge edge, guint64 position)
-{
-  if ((GES_IS_TRANSITION (object) || GES_IS_TRANSITION_CLIP (object)) &&
-      !ELEMENT_FLAG_IS_SET (object, GES_TIMELINE_ELEMENT_SET_SIMPLE)) {
-    return _trim_transition (timeline, object, edge, position);
+      GST_INFO_OBJECT (element, "Trimming clip %" GES_FORMAT " in place "
+          "of trimming the corresponding auto-transition", GES_ARGS (replace));
+      return ges_timeline_element_edit (replace, layers, -1, mode, edge,
+          position);
+    }
   }
 
-  return timeline_tree_trim (timeline->priv->tree,
-      GES_TIMELINE_ELEMENT (object), (gint64)
-      ges_timeline_element_get_layer_priority (GES_TIMELINE_ELEMENT (object)) -
-      (gint64) new_layer_priority,
-      edge == GES_EDGE_END ? GST_CLOCK_DIFF (position,
-          _START (object) + _DURATION (object)) : GST_CLOCK_DIFF (position,
-          GES_TIMELINE_ELEMENT_START (object)), edge,
-      timeline->priv->snapping_distance);
-}
-
-gboolean
-timeline_roll_object (GESTimeline * timeline, GESTimelineElement * element,
-    GList * layers, GESEdge edge, guint64 position)
-{
-  return timeline_tree_roll (timeline->priv->tree,
-      element,
-      (edge == GES_EDGE_END) ?
-      GST_CLOCK_DIFF (position, _END (element)) :
-      GST_CLOCK_DIFF (position, _START (element)),
-      edge, timeline->priv->snapping_distance);
-}
-
-gboolean
-timeline_move_object (GESTimeline * timeline, GESTimelineElement * object,
-    guint32 new_layer_priority, GList * layers, GESEdge edge, guint64 position)
-{
-  gboolean ret = FALSE;
-  GstClockTimeDiff offset = edge == GES_EDGE_END ?
-      GST_CLOCK_DIFF (position, _START (object) + _DURATION (object)) :
-      GST_CLOCK_DIFF (position, GES_TIMELINE_ELEMENT_START (object));
-
-  ret = timeline_tree_move (timeline->priv->tree,
-      GES_TIMELINE_ELEMENT (object), (gint64)
-      ges_timeline_element_get_layer_priority (GES_TIMELINE_ELEMENT (object)) -
-      (gint64) new_layer_priority, offset, edge,
-      timeline->priv->snapping_distance);
-
-  return ret;
+  return -1;
 }
 
 gboolean
-ges_timeline_move_object_simple (GESTimeline * timeline,
-    GESTimelineElement * element, GList * layers, GESEdge edge,
+ges_timeline_edit (GESTimeline * timeline, GESTimelineElement * element,
+    GList * layers, gint64 new_layer_priority, GESEditMode mode, GESEdge edge,
     guint64 position)
 {
-  return timeline_move_object (timeline, element,
-      ges_timeline_element_get_layer_priority (element), NULL, edge, position);
+  GstClockTimeDiff edge_diff = (edge == GES_EDGE_END ?
+      GST_CLOCK_DIFF (position, element->start + element->duration) :
+      GST_CLOCK_DIFF (position, element->start));
+  gint64 prio_diff = (gint64) ges_timeline_element_get_layer_priority (element)
+      - new_layer_priority;
+  gint res = -1;
+
+  if ((GES_IS_TRANSITION (element) || GES_IS_TRANSITION_CLIP (element)))
+    res = _edit_auto_transition (timeline, element, layers, new_layer_priority,
+        mode, edge, position);
+
+  if (res != -1)
+    return res;
+
+  switch (mode) {
+    case GES_EDIT_MODE_RIPPLE:
+      return timeline_tree_ripple (timeline->priv->tree, element, prio_diff,
+          edge_diff, edge, timeline->priv->snapping_distance);
+    case GES_EDIT_MODE_TRIM:
+      return timeline_tree_trim (timeline->priv->tree, element, prio_diff,
+          edge_diff, edge, timeline->priv->snapping_distance);
+    case GES_EDIT_MODE_NORMAL:
+      return timeline_tree_move (timeline->priv->tree, element, prio_diff,
+          edge_diff, edge, timeline->priv->snapping_distance);
+    case GES_EDIT_MODE_ROLL:
+      if (prio_diff != 0) {
+        GST_WARNING_OBJECT (element, "Cannot roll an element to a new layer");
+        return FALSE;
+      }
+      return timeline_tree_roll (timeline->priv->tree, element,
+          edge_diff, edge, timeline->priv->snapping_distance);
+    case GES_EDIT_MODE_SLIDE:
+      GST_ERROR_OBJECT (element, "Sliding not implemented.");
+      return FALSE;
+  }
+  return FALSE;
 }
 
 void
index 2d3f7c59d215f619df38da06ff6325c7383f12b2..cb98f21f3c8ae2d5df71ee665a3b870a58a213e1 100644 (file)
@@ -1199,10 +1199,11 @@ ges_track_add_element (GESTrack * track, GESTrackElement * object)
 
   timeline = track->priv->timeline;
   ges_timeline_element_set_timeline (el, timeline);
+  /* check that we haven't broken the timeline configuration by adding this
+   * element to the track */
   if (timeline
       && !timeline_tree_can_move_element (timeline_get_tree (timeline), el,
-          GES_TIMELINE_ELEMENT_LAYER_PRIORITY (el), el->start, el->duration,
-          NULL)) {
+          GES_TIMELINE_ELEMENT_LAYER_PRIORITY (el), el->start, el->duration)) {
     GST_WARNING_OBJECT (track,
         "Could not add the track element %" GES_FORMAT
         " to the track because it breaks the timeline " "configuration rules",
index f592c5bdf33baca866e9170fc0d7e1570fd7e3f0..db07365a58929414db0901dc6601d97475436895 100644 (file)
@@ -395,8 +395,7 @@ GST_START_TEST (test_single_layer_automatic_transition)
 
   transition = objects->next->next->data;
   assert_is_type (transition, GES_TYPE_TRANSITION_CLIP);
-  assert_equals_int (_START (transition), 500);
-  assert_equals_uint64 (_DURATION (transition), 750);
+  CHECK_OBJECT_PROPS (transition, 500, 0, 750);
   g_list_free_full (objects, gst_object_unref);
 
   fail_if (ges_timeline_element_set_start (src1, 250));
index 5538687de8e668ae0c2db3c2ef94ef7ee9546df5..24790a1ac412e8c326d0a1f742629080491fd4e6 100644 (file)
@@ -376,8 +376,6 @@ GST_START_TEST (test_snapping)
    */
   fail_unless (ges_timeline_element_set_inpoint (GES_TIMELINE_ELEMENT (clip2),
           5));
-  fail_unless (ges_timeline_element_roll_start (GES_TIMELINE_ELEMENT (clip2),
-          60));
   DEEP_CHECK (clip, 30, 5, 32);
   DEEP_CHECK (clip1, 20, 0, 10);
   DEEP_CHECK (clip2, 62, 5, 60);
@@ -912,9 +910,6 @@ GST_START_TEST (test_timeline_edition_mode)
   CHECK_OBJECT_PROPS (trackelement1, 25, 5, 47);
   CHECK_OBJECT_PROPS (trackelement2, 72, 10, 50);
 
-  fail_if (ges_container_edit (clip2, NULL, -1, GES_EDIT_MODE_ROLL,
-          GES_EDGE_START, 59));
-
   ges_deinit ();
 }
 
@@ -1010,8 +1005,6 @@ GST_START_TEST (test_groups)
 
   fail_if (ges_container_edit (GES_CONTAINER (c1), NULL, 2,
           GES_EDIT_MODE_RIPPLE, GES_EDGE_END, 40) == TRUE);
-  fail_if (ges_container_edit (GES_CONTAINER (c1), NULL, 2,
-          GES_EDIT_MODE_RIPPLE, GES_EDGE_END, 30) == TRUE);
   CHECK_CLIP (c, 10, 0, 10, 1);
   CHECK_CLIP (c1, 20, 0, 10, 2);
   CHECK_CLIP (c2, 30, 0, 10, 2);
index 9ff87186c87fc976a4e58ec5a66896e3d379f4f0..5534776b62ade30cf55e95246d6e8995e0401c5e 100644 (file)
@@ -504,14 +504,14 @@ class TestEditing(common.GESSimpleTimelineTest):
 
     def test_trim_start(self):
         clip = self.append_clip()
-        self.assertTrue(clip.edit([], -1, GES.EditMode.EDIT_NORMAL, GES.Edge.EDGE_NONE, 10))
+        self.assertTrue(clip.edit([], -1, GES.EditMode.EDIT_NORMAL, GES.Edge.EDGE_START, 10))
         self.assertTimelineTopology([
             [  # Unique layer
                 (GES.TestClip, 10, 10),
             ]
         ])
 
-        self.assertFalse(clip.edit([], -1, GES.EditMode.EDIT_TRIM, GES.Edge.EDGE_NONE, 0))
+        self.assertFalse(clip.edit([], -1, GES.EditMode.EDIT_TRIM, GES.Edge.EDGE_START, 0))
         self.assertTimelineTopology([
             [  # Unique layer
                 (GES.TestClip, 10, 10),
index 483ea10d333ce6338936f4308fb2e6a8b002f5b7..983e8dd1f90e2cf7c07e054089e20c76f69f2387 100644 (file)
@@ -13,11 +13,11 @@ check-last-sample, sinkpad-caps="video/x-raw", timecode-frame-number=100
 
 edit, element-name=clip, edit-mode=normal, position=1.0
 
-edit, element-name=clip, edit-mode=edit_trim, source-frame=60
+edit, element-name=clip, edit-mode=edit_trim, edge=start, source-frame=60
 edit, element-name=clip, position=0
 commit;
 check-last-sample, sinkpad-caps="video/x-raw", timecode-frame-number=60
 
-edit, element-name=clip, edit-mode=edit_trim, edit-edge=end, source-frame=120
+edit, element-name=clip, edit-mode=edit_trim, edge=start, source-frame=120
 check-ges-properties, element-name=clip, start=0.5
-stop
\ No newline at end of file
+stop