[pitivi] Port the timeline to Gtk+ and remove clutter dependency!



commit 91b7b3753861c9ec5be11acc3744d1ca9ac19a89
Author: Thibault Saunier <tsaunier gnome org>
Date:   Thu Dec 18 19:36:50 2014 +0100

    Port the timeline to Gtk+ and remove clutter dependency!
    
    Summary:
    With help from Mathieu Duponchelle <mathieu duonchelle opencreed com>
    for the keyframes
    
    Maniphest Tasks: T31
    
    Reviewers: Mathieu_Du
    
    Differential Revision: http://phabricator.freedesktop.org/D111

 pitivi/application.py         |    4 +-
 pitivi/check.py               |   10 +-
 pitivi/mainwindow.py          |   12 +-
 pitivi/project.py             |   15 +-
 pitivi/timeline/Makefile.am   |    3 +-
 pitivi/timeline/controls.py   |  180 -----
 pitivi/timeline/elements.py   | 1678 ++++++++++++++++-------------------------
 pitivi/timeline/layer.py      |  253 ++++++-
 pitivi/timeline/previewers.py |  346 +++------
 pitivi/timeline/ruler.py      |   19 +-
 pitivi/timeline/timeline.py   | 1523 ++++++++++++++++++++------------------
 pitivi/transitions.py         |    7 +-
 pitivi/undo/timeline.py       |    1 +
 pitivi/utils/pipeline.py      |   16 +-
 pitivi/utils/timeline.py      |   40 +-
 pitivi/utils/ui.py            |   90 ++-
 pitivi/utils/validate.py      |  147 ++++-
 pitivi/utils/widgets.py       |    8 +-
 pitivi/viewer.py              |   12 +-
 tests/test_utils.py           |    4 +-
 20 files changed, 2118 insertions(+), 2250 deletions(-)
---
diff --git a/pitivi/application.py b/pitivi/application.py
index ac66e1f..c2eb101 100644
--- a/pitivi/application.py
+++ b/pitivi/application.py
@@ -24,8 +24,6 @@
 import os
 import time
 
-from datetime import datetime
-
 from gi.repository import GObject
 from gi.repository import Gio
 from gi.repository import Gtk
@@ -231,6 +229,8 @@ class Pitivi(Gtk.Application, Loggable):
     def _setScenarioFile(self, uri):
         if 'PITIVI_SCENARIO_FILE' in os.environ:
             uri = quote_uri(os.environ['PITIVI_SCENARIO_FILE'])
+            if uri:
+                project_path = path_from_uri(uri)
         else:
             cache_dir = get_dir(os.path.join(xdg_cache_home(), "scenarios"))
             scenario_name = str(time.strftime("%Y%m%d-%H%M%S"))
diff --git a/pitivi/check.py b/pitivi/check.py
index 3cea5ec..a3d3627 100644
--- a/pitivi/check.py
+++ b/pitivi/check.py
@@ -177,7 +177,7 @@ class GstDependency(GIDependency):
         return list(module.version())
 
 
-class GtkOrClutterDependency(GIDependency):
+class GtkDependency(GIDependency):
 
     def _format_version(self, module):
         return [module.MAJOR_VERSION, module.MINOR_VERSION, module.MICRO_VERSION]
@@ -254,8 +254,6 @@ def initialize_modules():
     """
     from gi.repository import Gdk
     Gdk.init([])
-    from gi.repository import GtkClutter
-    GtkClutter.init([])
 
     import gi
     if not gi.version_info >= (3, 11):
@@ -287,13 +285,13 @@ Some of our dependencies have version numbers requirements; for those without
 a specific version requirement, they have the "None" value.
 """
 HARD_DEPENDENCIES = [CairoDependency("1.10.0"),
-                     GtkOrClutterDependency("Clutter", "1.12.0"),
                      GstDependency("Gst", "1.4.0"),
                      GstDependency("GES", "1.5.0.0"),
-                     GtkOrClutterDependency("Gtk", "3.10.0"),
+                     GtkDependency("Gtk", "3.10.0"),
                      ClassicDependency("numpy", None),
                      GIDependency("Gio", None),
-                     GstPluginDependency("opengl", "1.4.0")
+                     GstPluginDependency("opengl", "1.4.0"),
+                     ClassicDependency("matplotlib", None),
                      ]
 
 SOFT_DEPENDENCIES = \
diff --git a/pitivi/mainwindow.py b/pitivi/mainwindow.py
index 82f2750..95d58e5 100644
--- a/pitivi/mainwindow.py
+++ b/pitivi/mainwindow.py
@@ -48,7 +48,7 @@ from pitivi.transitions import TransitionsListWidget
 from pitivi.utils.loggable import Loggable
 from pitivi.utils.misc import show_user_manual, path_from_uri
 from pitivi.utils.ui import info_name, beautify_time_delta, SPACING, \
-    beautify_length
+    beautify_length, TIMELINE_CSS
 from pitivi.viewer import ViewerContainer
 
 
@@ -144,6 +144,7 @@ class PitiviMainWindow(Gtk.ApplicationWindow, Loggable):
         self.connect("destroy", self._destroyedCb)
 
         self.uimanager = Gtk.UIManager()
+        self.setupCss()
         self.builder_handler_ids = []
         self.builder = Gtk.Builder()
         self.add_accel_group(self.uimanager.get_accel_group())
@@ -167,6 +168,14 @@ class PitiviMainWindow(Gtk.ApplicationWindow, Loggable):
         pm.connect("project-closed", self._projectManagerProjectClosedCb)
         pm.connect("missing-uri", self._projectManagerMissingUriCb)
 
+    def setupCss(self):
+        self.css_provider = Gtk.CssProvider()
+        self.css_provider.load_from_data(TIMELINE_CSS.encode('UTF-8'))
+        screen = Gdk.Screen.get_default()
+        style_context = self.get_style_context()
+        style_context.add_provider_for_screen(screen, self.css_provider,
+                                              Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION)
+
     @staticmethod
     def createStockIcons():
         """
@@ -627,6 +636,7 @@ class PitiviMainWindow(Gtk.ApplicationWindow, Loggable):
 
         comments = ["",
                     "GES %s" % ".".join(map(str, GES.version())),
+                    "Gtk %s" % ".".join(map(str, (Gtk.MAJOR_VERSION, Gtk.MINOR_VERSION))),
                     "GStreamer %s" % ".".join(map(str, Gst.version()))]
         abt.set_comments("\n".join(comments))
 
diff --git a/pitivi/project.py b/pitivi/project.py
index 7430c2e..7ef75ad 100644
--- a/pitivi/project.py
+++ b/pitivi/project.py
@@ -787,8 +787,13 @@ class Project(Loggable, GES.Project):
         self._acodecsettings_cache = {}
         self._has_rendering_values = False
 
+        self.runner = None
+        self.monitor = None
+        self._scenario = None
+
     def _scenarioDoneCb(self, scenario):
-        self.pipeline.setForcePositionListener(False)
+        if self.pipeline is not None:
+            self.pipeline.setForcePositionListener(False)
 
     def setupValidateScenario(self):
         from gi.repository import GstValidate
@@ -1150,8 +1155,16 @@ class Project(Loggable, GES.Project):
         return self.list_assets(GES.UriClip)
 
     def release(self):
+        if self.runner:
+            self.runner.printf()
+
         if self.pipeline:
             self.pipeline.release()
+
+        if self.runner:
+            self.runner = None
+            self.monitor = None
+
         self.pipeline = None
         self.timeline = None
 
diff --git a/pitivi/timeline/Makefile.am b/pitivi/timeline/Makefile.am
index cf89178..b08f86a 100644
--- a/pitivi/timeline/Makefile.am
+++ b/pitivi/timeline/Makefile.am
@@ -6,8 +6,7 @@ timeline_PYTHON =                       \
        ruler.py        \
        timeline.py             \
        elements.py \
-       previewers.py \
-       controls.py
+       previewers.py
 
 clean-local:
        rm -rf *.pyc *.pyo
diff --git a/pitivi/timeline/elements.py b/pitivi/timeline/elements.py
index 18729d8..a91db08 100644
--- a/pitivi/timeline/elements.py
+++ b/pitivi/timeline/elements.py
@@ -25,1174 +25,778 @@ Convention throughout this file:
 Every GES element which name could be mistaken with a UI element
 is prefixed with a little b, example : bTimeline
 """
-
-import cairo
-import math
 import os
-from datetime import datetime
 
-import weakref
+from gi.repository import GES
+from gi.repository import Gtk
+from gi.repository import Gdk
+from gi.repository import GdkPixbuf
+from gi.repository import GstController
+from gi.repository import GObject
 
-from gi.repository import Clutter, Gtk, GtkClutter, GES, Gdk, Gst, GstController
-from pitivi.utils.timeline import Zoomable, EditingContext, SELECT, UNSELECT, SELECT_ADD, Selected
-from .previewers import AudioPreviewer, VideoPreviewer
+from pitivi.utils import ui
+from pitivi.utils import misc
+from pitivi import configure
+from pitivi.timeline import previewers
+from pitivi.utils.loggable import Loggable
+from pitivi.utils import timeline as timelineUtils
 
-import pitivi.configure as configure
-from pitivi.utils.ui import EXPANDED_SIZE, SPACING, KEYFRAME_SIZE, CONTROL_WIDTH
+from matplotlib.figure import Figure
+from matplotlib.backends.backend_gtk3cairo import FigureCanvasGTK3Cairo as FigureCanvas
+import numpy
 
-# Colors for keyframes and clips (RGBA)
-KEYFRAME_LINE_COLOR = (237, 212, 0, 255)  # "Tango" yellow
-KEYFRAME_NORMAL_COLOR = Clutter.Color.new(0, 0, 0, 200)
-KEYFRAME_SELECTED_COLOR = Clutter.Color.new(200, 200, 200, 200)
-CLIP_SELECTED_OVERLAY_COLOR = Clutter.Color.new(60, 60, 60, 100)
-GHOST_CLIP_COLOR = Clutter.Color.new(255, 255, 255, 50)
-TRANSITION_COLOR = Clutter.Color.new(35, 85, 125, 125)  # light blue
+KEYFRAME_LINE_COLOR = (237, 212, 0)  # "Tango" yellow
 
-BORDER_NORMAL_COLOR = Clutter.Color.new(100, 100, 100, 255)
-BORDER_SELECTED_COLOR = Clutter.Color.new(200, 200, 10, 255)
+CURSORS = {
+    GES.Edge.EDGE_START: Gdk.Cursor.new(Gdk.CursorType.LEFT_SIDE),
+    GES.Edge.EDGE_END: Gdk.Cursor.new(Gdk.CursorType.RIGHT_SIDE)
+}
 
 NORMAL_CURSOR = Gdk.Cursor.new(Gdk.CursorType.LEFT_PTR)
 DRAG_CURSOR = Gdk.Cursor.new(Gdk.CursorType.HAND1)
-DRAG_LEFT_HANDLEBAR_CURSOR = Gdk.Cursor.new(Gdk.CursorType.LEFT_SIDE)
-DRAG_RIGHT_HANDLEBAR_CURSOR = Gdk.Cursor.new(Gdk.CursorType.RIGHT_SIDE)
-
-
-class Ghostclip(Clutter.Actor):
-
-    """
-    The concept of a ghostclip is to represent future actions without
-    actually moving GESClips. They are created when the user wants
-    to change a clip of layer, and when the user does a drag and drop
-    from the media library.
-    """
-
-    def __init__(self, track_type, bElement=None):
-        Clutter.Actor.__init__(self)
-        self.track_type = track_type
-        self.bElement = bElement
-        self.set_background_color(GHOST_CLIP_COLOR)
-        self.props.visible = False
-        self.shouldCreateLayer = False
-
-    def setNbrLayers(self, nbrLayers):
-        self.nbrLayers = nbrLayers
-
-    def setWidth(self, width):
-        self.props.width = width
-
-    def update(self, priority, y, isControlledByBrother):
-        # Priority and y can be negative when dragging an asset to the ruler.
-        # Priority can also be negative when dragging a linked element.
-        self.priority = min(max(0, priority), self.nbrLayers)
-        y = max(0, y)
-
-        # Here we make it so the calculation is the same for audio and video.
-        if self.track_type == GES.TrackType.AUDIO and not isControlledByBrother:
-            y -= self.nbrLayers * (EXPANDED_SIZE + SPACING)
-
-        # And here we take into account the fact that the pointer might actually be
-        # on the other track element, meaning we have to offset it.
-        if isControlledByBrother:
-            if self.track_type == GES.TrackType.AUDIO:
-                y += self.nbrLayers * (EXPANDED_SIZE + SPACING)
-            else:
-                y -= self.nbrLayers * (EXPANDED_SIZE + SPACING)
-
-        # Would that be a new layer at the end or inserted ?
-        if self.priority == self.nbrLayers or y % (EXPANDED_SIZE + SPACING) < SPACING:
-            self.shouldCreateLayer = True
-            self.set_size(self.props.width, SPACING)
-            self.props.y = self.priority * (EXPANDED_SIZE + SPACING)
-            if self.track_type == GES.TrackType.AUDIO:
-                self.props.y += self.nbrLayers * (EXPANDED_SIZE + SPACING)
-            self.props.visible = True
-        else:
-            self.shouldCreateLayer = False
-            # No need to mockup on the same layer
-            if self.bElement and self.priority == self.bElement.get_parent().get_layer().get_priority():
-                self.props.visible = False
-            # We would be moving to an existing layer.
-            elif self.priority < self.nbrLayers:
-                self.set_size(self.props.width, EXPANDED_SIZE)
-                self.props.y = self.priority * \
-                    (EXPANDED_SIZE + SPACING) + SPACING
-                if self.track_type == GES.TrackType.AUDIO:
-                    self.props.y += self.nbrLayers * (EXPANDED_SIZE + SPACING)
-                self.props.visible = True
-
-    def getLayerForY(self, y):
-        if self.track_type == GES.TrackType.AUDIO:
-            y -= self.nbrLayers * (EXPANDED_SIZE + SPACING)
-        priority = int(y / (EXPANDED_SIZE + SPACING))
-
-        return priority
-
-
-class TrimHandle(Clutter.Texture):
-
-    def __init__(self, timelineElement, isLeft):
-        Clutter.Texture.__init__(self)
-
-        self.isLeft = isLeft
-        self.timelineElement = weakref.proxy(timelineElement)
-        self.dragAction = Clutter.DragAction()
-
-        self.set_from_file(
-            os.path.join(configure.get_pixmap_dir(), "trimbar-normal.png"))
-        self.set_size(-1, EXPANDED_SIZE)
-        self.hide()
-        self.set_reactive(True)
-
-        self.add_action(self.dragAction)
-        self.dragAction.connect("drag-begin", self._dragBeginCb)
-        self.dragAction.connect("drag-end", self._dragEndCb)
-        self.dragAction.connect("drag-progress", self._dragProgressCb)
-
-        self.connect("enter-event", self._enterEventCb)
-        self.connect("leave-event", self._leaveEventCb)
-
-        self.timelineElement.connect("enter-event", self._elementEnterEventCb)
-        self.timelineElement.connect("leave-event", self._elementLeaveEventCb)
-
-    def cleanup(self):
-        self.disconnect_by_func(self._enterEventCb)
-        self.disconnect_by_func(self._leaveEventCb)
-        self.timelineElement.disconnect_by_func(self._elementEnterEventCb)
-        self.timelineElement.disconnect_by_func(self._elementLeaveEventCb)
+
+
+class KeyframeCurve(FigureCanvas, Loggable):
+
+    __gsignals__ = {
+        # Signal our values changed, and a redraw will be needed
+        "plot-changed": (GObject.SIGNAL_RUN_LAST, None, ()),
+    }
+
+    def __init__(self, timeline, source):
+        figure = Figure()
+        FigureCanvas.__init__(self, figure)
+        Loggable.__init__(self)
+
+        self.__timeline = timeline
+        self.__source = source
+
+        # Curve values, basically separating source.get_values() timestamps
+        # and values.
+        self.__line_xs = []
+        self.__line_ys = []
+
+        # axisbg to None for transparency
+        self.__ax = figure.add_axes([0, 0, 1, 1], axisbg='None')
+        self.__ax.cla()
+
+        # FIXME: drawing a grid and ticks would be nice, but
+        # matplotlib is too slow for now.
+        self.__ax.grid(False)
+
+        self.__ax.tick_params(axis='both',
+                              which='both',
+                              bottom='off',
+                              top='off',
+                              right='off',
+                              left='off')
+
+        # This seems to also be necessary for transparency ..
+        figure.patch.set_visible(False)
+
+        # The actual Line2D object
+        self.__line = None
+
+        # The PathCollection as returned by scatter
+        self.__keyframes = None
+
+        sizes = [100]
+        colors = ['r']
+
+        self.__keyframes = self.__ax.scatter([], [], marker='o', s=sizes,
+                                             c=colors, zorder=2)
+        self.__line = self.__ax.plot([], [],
+                                     linewidth=2.0, zorder=1)[0]
+        self.__updatePlots()
+
+        # Drag and drop logic
+        self.__dragged = False
+        self.__offset = None
+        self.__handling_motion = False
+        self.connect("event", self._eventCb)
+
+        self.mpl_connect('button_press_event', self.__mplButtonPressEventCb)
+        self.mpl_connect(
+            'button_release_event', self.__mplButtonReleaseEventCb)
+        self.mpl_connect('motion_notify_event', self.__mplMotionEventCb)
+
+    # Private methods
+    def __updatePlots(self):
+        values = self.__source.get_all()
+
+        self.__line_xs = []
+        self.__line_ys = []
+        for value in values:
+            self.__line_xs.append(value.timestamp)
+            self.__line_ys.append(value.value)
+            self.__ax.set_xlim(self.__line_xs[0], self.__line_xs[-1])
+            self.__ax.set_ylim(0.0, 1.0)
+
+        arr = numpy.array((self.__line_xs, self.__line_ys))
+        arr = arr.transpose()
+        self.__keyframes.set_offsets(arr)
+        self.__line.set_xdata(self.__line_xs)
+        self.__line.set_ydata(self.__line_ys)
+        self.emit("plot-changed")
+
+    def __maybeCreateKeyframe(self, event):
+        result = self.__line.contains(event)
+        if result[0]:
+            self.__source.set(event.xdata, event.ydata)
+            self.__updatePlots()
 
     # Callbacks
+    def _eventCb(self, element, event):
+        if event.type == Gdk.EventType.LEAVE_NOTIFY:
+            cursor = NORMAL_CURSOR
+            self.__timeline.get_window().set_cursor(cursor)
+        elif event.type == Gdk.EventType.MOTION_NOTIFY:
+            # We need to do that here, because mpl's callbacks can't stop
+            # signal propagation.
+            if self.__handling_motion:
+                return True
+        return False
 
-    def _enterEventCb(self, unused_actor, unused_event):
-        self.timelineElement.set_reactive(False)
-        for elem in self.timelineElement.get_children():
-            elem.set_reactive(False)
-        self.set_reactive(True)
-
-        self.set_from_file(
-            os.path.join(configure.get_pixmap_dir(), "trimbar-focused.png"))
-        if self.isLeft:
-            self.timelineElement.timeline._container.embed.get_window().set_cursor(
-                DRAG_LEFT_HANDLEBAR_CURSOR)
-        else:
-            self.timelineElement.timeline._container.embed.get_window().set_cursor(
-                DRAG_RIGHT_HANDLEBAR_CURSOR)
-
-    def _leaveEventCb(self, unused_actor, event):
-        self.timelineElement.set_reactive(True)
-        children = self.timelineElement.get_children()
-
-        other_actor = self.timelineElement.timeline._container.stage.get_actor_at_pos(
-            Clutter.PickMode.ALL, event.x, event.y)
-        if other_actor not in children:
-            self.timelineElement.hideHandles()
-
-        for elem in children:
-            elem.set_reactive(True)
-        self.set_from_file(
-            os.path.join(configure.get_pixmap_dir(), "trimbar-normal.png"))
-        self.timelineElement.timeline._container.embed.get_window().set_cursor(
-            NORMAL_CURSOR)
-
-    def _elementEnterEventCb(self, unused_actor, unused_event):
-        self.show()
+    def __mplButtonPressEventCb(self, event):
+        result = self.__keyframes.contains(event)
+        if result[0]:
+            self.__handling_motion = True
+            self.__offset = \
+                self.__keyframes.get_offsets()[result[1]['ind'][0]][0]
 
-    def _elementLeaveEventCb(self, unused_actor, unused_event):
-        self.hide()
+    def __mplMotionEventCb(self, event):
+        if not self.props.visible:
+            return
 
-    def _dragBeginCb(self, unused_action, unused_actor, event_x, event_y, unused_modifiers):
-        self.dragBeginStartX = event_x
-        self.dragBeginStartY = event_y
-        elem = self.timelineElement.bElement.get_parent()
-        self.timelineElement.setDragged(True)
+        if self.__offset is not None:
+            self.__dragged = True
+            # Check that the mouse event still is in the figure boundaries
+            if event.ydata is not None and event.xdata is not None:
+                self.__source.unset(int(self.__offset))
+                self.__source.set(event.xdata, event.ydata)
+                self.__offset = event.xdata
+                self.__updatePlots()
 
-        if self.isLeft:
-            edge = GES.Edge.EDGE_START
-            self._dragBeginStart = self.timelineElement.bElement.get_parent(
-            ).get_start()
-        else:
-            edge = GES.Edge.EDGE_END
-            self._dragBeginStart = self.timelineElement.bElement.get_parent().get_duration() + \
-                self.timelineElement.bElement.get_parent().get_start()
-
-        self._context = EditingContext(elem,
-                                       self.timelineElement.timeline.bTimeline,
-                                       GES.EditMode.EDIT_TRIM,
-                                       edge,
-                                       None,
-                                       self.timelineElement.timeline._container.app.action_log)
-
-        self._context.connect("clip-trim", self.clipTrimCb)
-        self._context.connect("clip-trim-finished", self.clipTrimFinishedCb)
-
-    def _dragProgressCb(self, unused_action, unused_actor, delta_x, unused_delta_y):
-        # We can't use delta_x here because it fluctuates weirdly.
-        coords = self.dragAction.get_motion_coords()
-        delta_x = coords[0] - self.dragBeginStartX
-        new_start = self._dragBeginStart + Zoomable.pixelToNs(delta_x)
-
-        self._context.setMode(
-            self.timelineElement.timeline._container.getEditionMode(isAHandle=True))
-        self._context.editTo(
-            new_start, self.timelineElement.bElement.get_parent().get_layer().get_priority())
-        return False
+        cursor = NORMAL_CURSOR
+        result = self.__line.contains(event)
+        if result[0]:
+            cursor = DRAG_CURSOR
 
-    def _dragEndCb(self, unused_action, unused_actor, unused_event_x, unused_event_y, unused_modifiers):
-        self.timelineElement.setDragged(False)
-        self._context.finish()
+        self.__timeline.get_window().set_cursor(
+            cursor)
 
-        self.timelineElement.set_reactive(True)
-        for elem in self.timelineElement.get_children():
-            elem.set_reactive(True)
+    def __mplButtonReleaseEventCb(self, event):
+        self.__offset = None
+        self.__handling_motion = False
 
-        self.set_from_file(
-            os.path.join(configure.get_pixmap_dir(), "trimbar-normal.png"))
-        self.timelineElement.timeline._container.embed.get_window().set_cursor(
-            NORMAL_CURSOR)
+        if not self.__dragged:
+            self.__maybeCreateKeyframe(event)
+        self.__dragged = False
 
-    def clipTrimCb(self, unused_TrimStartContext, tl_obj, position):
-        # While a clip is being trimmed, ask the viewer to preview it
-        self.timelineElement.timeline._container.app.gui.viewer.clipTrimPreview(
-            tl_obj, position)
 
-    def clipTrimFinishedCb(self, unused_TrimStartContext):
-        # When a clip has finished trimming, tell the viewer to reset itself
-        self.timelineElement.timeline._container.app.gui.viewer.clipTrimPreviewFinished(
-        )
+class TimelineElement(Gtk.Layout, timelineUtils.Zoomable, Loggable):
 
+    def __init__(self, element, timeline):
+        super(TimelineElement, self).__init__()
+        timelineUtils.Zoomable.__init__(self)
+        Loggable.__init__(self)
 
-class TimelineElement(Clutter.Actor, Zoomable):
+        self.timeline = timeline
+        self._bElement = element
+        self._bElement.selected = timelineUtils.Selected()
+        self._bElement.selected.connect(
+            "selected-changed", self.__selectedChangedCb)
 
-    """
-    @ivar bElement: the backend element.
-    @type bElement: GES.TrackElement
-    @ivar timeline: the containing graphic timeline.
-    @type timeline: TimelineStage
-    """
+        self.__width = self.__height = 0
 
-    def __init__(self, bElement, timeline):
-        Zoomable.__init__(self)
-        Clutter.Actor.__init__(self)
+        # Needed for effect's keyframe toggling
+        self._bElement.ui_element = self
 
-        self.timeline = timeline
-        self.bElement = bElement
-        self.bElement.selected = Selected()
-        self.bElement.ui_element = weakref.proxy(self)
-        self.track_type = self.bElement.get_track_type()  # This won't change
-        self.isDragged = False
-        self.lines = []
-        self.keyframes = []
-        self.keyframesVisible = False
-        self.source = None
-        self.keyframedElement = None
-        self.rightHandle = None
-        self.isSelected = False
-        self.updating_keyframes = False
-        size = self.bElement.get_duration()
+        self.props.vexpand = True
 
-        self.background = self._createBackground()
-        self.background.set_position(1, 1)
-        self.add_child(self.background)
+        self.__previewer = self._getPreviewer()
+        if self.__previewer:
+            self.add(self.__previewer)
 
-        self.preview = self._createPreview()
-        self.add_child(self.preview)
+        self.__background = self._getBackground()
+        if self.__background:
+            self.add(self.__background)
 
-        self.border = self._createBorder()
-        self.add_child(self.border)
+        self.__keyframeCurve = None
+        self.show_all()
 
-        self.set_child_below_sibling(self.border, self.background)
+        # We set up the default mixing property right here, if a binding was
+        # already set (when loading a project), it will be added later
+        # and override that one.
+        self.__controlledProperty = self._getDefaultMixingProperty()
+        if self.__controlledProperty:
+            self.__createControlBinding(self._bElement)
 
-        self.marquee = self._createMarquee()
-        self.add_child(self.marquee)
+    # Public API
+    def setSize(self, width, height):
+        width = max(0, width)
+        self.set_size_request(width, height)
+
+        if self.__previewer:
+            self.__previewer.set_size_request(width, height)
+
+        if self.__background:
+            self.__background.set_size_request(width, height)
+
+        if self.__keyframeCurve:
+            self.__keyframeCurve.set_size_request(width, height)
+
+        self.__width = width
+        self.__height = height
+
+    def showKeyframes(self, effect, prop):
+        self.__controlledProperty = prop
+        self.__createControlBinding(effect)
+
+    # Private methods
+    def __createKeyframeCurve(self, binding):
+        source = binding.props.control_source
+        values = source.get_all()
+
+        if len(values) < 2:
+            source.unset_all()
+            val = float(self.__controlledProperty.default_value) / \
+                (self.__controlledProperty.maximum -
+                 self.__controlledProperty.minimum)
+            source.set(self._bElement.props.in_point, val)
+            source.set(
+                self._bElement.props.duration + self._bElement.props.in_point,
+                val)
+
+        if self.__keyframeCurve:
+            self.__keyframeCurve.disconnect_by_func(
+                self.__keyframePlotChangedCb)
+            self.remove(self.__keyframeCurve)
+
+        self.__keyframeCurve = KeyframeCurve(self.timeline, source)
+        self.__keyframeCurve.connect("plot-changed",
+                                     self.__keyframePlotChangedCb)
+        self.add(self.__keyframeCurve)
+        self.__keyframeCurve.set_size_request(self.__width, self.__height)
+        self.__keyframeCurve.props.visible = bool(self._bElement.selected)
+        self.queue_draw()
+
+    def __createControlBinding(self, element):
+        if self.__controlledProperty:
+            element.connect("control-binding-added",
+                            self.__controlBindingAddedCb)
+            binding = \
+                element.get_control_binding(self.__controlledProperty.name)
+
+            if binding:
+                self.__createKeyframeCurve(binding)
 
-        self._createHandles()
+            source = GstController.InterpolationControlSource()
+            source.props.mode = GstController.InterpolationMode.LINEAR
+            element.set_control_source(source,
+                                       self.__controlledProperty.name, "direct")
 
-        self._linesMarker = self._createMarker()
-        self._keyframesMarker = self._createMarker()
+    def __controlBindingAddedCb(self, unused_bElement, binding):
+        if binding.props.name == self.__controlledProperty.name:
+            self.__createKeyframeCurve(binding)
 
-        self._createGhostclip()
+    # Gtk implementation
+    def do_set_property(self, property_id, value, pspec):
+        Gtk.Layout.do_set_property(self, property_id, value, pspec)
 
-        self.update(True)
-        self.set_reactive(True)
+    def do_get_preferred_width(self):
+        wanted_width = max(
+            0, self.nsToPixel(self._bElement.props.duration) - TrimHandle.DEFAULT_WIDTH * 2)
 
-        self._createMixingKeyframes()
+        return wanted_width, wanted_width
 
-        self._connectToEvents()
+    def do_draw(self, cr):
+        self.propagate_draw(self.__background, cr)
 
-    def _valueChanged(self, source, value):
-        if self.updating_keyframes is True:
-            return
+        if self.__previewer:
+            self.propagate_draw(self.__previewer, cr)
 
-        self.updateKeyframes()
+        if self.__keyframeCurve and self._bElement.selected:
+            self.__keyframeCurve.draw()
+            self.propagate_draw(self.__keyframeCurve, cr)
 
-    def _valueAddedCb(self, source, value):
-        if self.updating_keyframes is True:
-            return
+    def do_show_all(self):
+        for child in self.get_children():
+            if bool(self._bElement.selected) or child != self.__keyframeCurve:
+                child.show_all()
 
-        self.updateKeyframes()
+        self.show()
 
-    def _valueRemovedCb(self, source, value):
-        if self.updating_keyframes is True:
-            return
+    # Callbacks
+    def __selectedChangedCb(self, unused_bElement, selected):
+        if self.__keyframeCurve:
+            self.__keyframeCurve.props.visible = selected
 
-        self.updateKeyframes()
+    def __keyframePlotChangedCb(self, unused_curve):
+        self.queue_draw()
 
-    # Public API
+    # Virtual methods
+    def _getPreviewer(self):
+        """
+        Should return a GtkWidget offering a representation of the
+        medium (waveforms for audio, thumbnails for video ..).
+        This previewer will be automatically scaled to the width and
+        height of the TimelineElement.
+        """
+        return None
 
-    def set_size(self, width, height, ease):
-        if ease:
-            self.save_easing_state()
-            self.set_easing_duration(600)
-            self.background.save_easing_state()
-            self.background.set_easing_duration(600)
-            self.border.save_easing_state()
-            self.border.set_easing_duration(600)
-            self.preview.save_easing_state()
-            self.preview.set_easing_duration(600)
-            if self.rightHandle:
-                self.rightHandle.save_easing_state()
-                self.rightHandle.set_easing_duration(600)
-
-        self.marquee.set_size(width, height)
-        self.background.props.width = max(width - 2, 1)
-        self.background.props.height = max(height - 2, 1)
-        self.border.props.width = width
-        self.border.props.height = height
-        self.props.width = width
-        self.props.height = height
-        self.preview.set_size(max(width - 2, 1), max(height - 2, 1))
-        if self.rightHandle:
-            self.rightHandle.set_position(
-                width - self.rightHandle.props.width, 0)
-
-        if ease:
-            self.background.restore_easing_state()
-            self.border.restore_easing_state()
-            self.preview.restore_easing_state()
-            if self.rightHandle:
-                self.rightHandle.restore_easing_state()
-            self.restore_easing_state()
-
-    def addKeyframe(self, value, timestamp):
-        self.timeline._container.app.action_log.begin("Add KeyFrame")
-        self.updating_keyframes = True
-        self.source.set(timestamp, value)
-        self.updateKeyframes()
-        self.timeline._container.app.action_log.commit()
-        self.updating_keyframes = False
-
-    def removeKeyframe(self, kf):
-        self.timeline._container.app.action_log.begin("Remove KeyFrame")
-        self.updating_keyframes = True
-        self.source.unset(kf.value.timestamp)
-        self.keyframes = sorted(
-            self.keyframes, key=lambda keyframe: keyframe.value.timestamp)
-        self.updateKeyframes()
-        self.timeline._container.app.action_log.commit()
-        self.updating_keyframes = False
-
-    def showKeyframes(self, element, propname, isDefault=False):
-        binding = element.get_control_binding(propname.name)
-        if not binding:
-            source = GstController.InterpolationControlSource()
-            source.props.mode = GstController.InterpolationMode.LINEAR
-            if not (element.set_control_source(source, propname.name, "direct")):
-                print("There was something like a problem captain")
-                return
-            binding = element.get_control_binding(propname.name)
+    def _getBackground(self):
+        """
+        Should return a GtkWidget with a unique background color.
+        """
+        return None
 
-        self.binding = binding
-        self.prop = propname
-        self.keyframedElement = element
-        self.source = self.binding.props.control_source
-        self.source.connect("value-added", self._valueAddedCb)
-        self.source.connect("value-removed", self._valueRemovedCb)
-        self.source.connect("value-changed", self._valueChanged)
+    def _getDefaultMixingProperty(self):
+        """
+        Should return a controllable GObject.ParamSpec allowing to mix
+        media on different layers.
+        """
+        return None
 
-        if isDefault:
-            self.default_prop = propname
-            self.default_element = element
 
-        self.keyframesVisible = True
+class TitleSource(TimelineElement):
 
-        self.updateKeyframes()
+    __gtype_name__ = "PitiviTitleSource"
 
-    def hideKeyframes(self):
-        for keyframe in self.keyframes:
-            self.remove_child(keyframe)
-        self.keyframes = []
+    def __init__(self, element, timeline):
+        super(TitleSource, self).__init__(element, timeline)
+        self.get_style_context().add_class("VideoUriSource")
 
-        self.keyframesVisible = False
+    def _getBackground(self):
+        return VideoBackground()
 
-        if self.isSelected:
-            self.showKeyframes(self.default_element, self.default_prop)
+    def do_get_preferred_height(self):
+        return ui.LAYER_HEIGHT / 2, ui.LAYER_HEIGHT
 
-        self.drawLines()
 
-    def setKeyframePosition(self, keyframe, value):
-        x = self.nsToPixel(
-            value.timestamp - self.bElement.props.in_point) - KEYFRAME_SIZE / 2
-        y = EXPANDED_SIZE - (value.value * EXPANDED_SIZE) - KEYFRAME_SIZE / 2
-        keyframe.set_position(x, y)
+class VideoBackground (Gtk.Box):
 
-    def drawLines(self, line=None):
-        for line_ in self.lines:
-            if line_ != line:
-                self.remove_child(line_)
+    def __init__(self):
+        super(VideoBackground, self).__init__(self)
+        self.get_style_context().add_class("VideoBackground")
 
-        if line:
-            self.lines = [line]
-        else:
-            self.lines = []
-
-        lastKeyframe = None
-        for keyframe in self.keyframes:
-            if lastKeyframe and (not line or lastKeyframe != line.previousKeyframe):
-                self._createLine(keyframe, lastKeyframe, None)
-            elif lastKeyframe:
-                self._createLine(keyframe, lastKeyframe, line)
-            lastKeyframe = keyframe
-
-    def updateKeyframes(self):
-        if not self.source:
-            return
 
-        updating = self.updating_keyframes
-        self.updating_keyframes = True
-        values = self.source.get_all()
-        if len(values) < 2 and self.bElement.props.duration > 0:
-            self.source.unset_all()
-            val = float(self.prop.default_value) / \
-                (self.prop.maximum - self.prop.minimum)
-            self.source.set(self.bElement.props.in_point, val)
-            self.source.set(
-                self.bElement.props.duration + self.bElement.props.in_point, val)
-
-        for keyframe in self.keyframes:
-            self.remove_child(keyframe)
-
-        self.keyframes = []
-        values = self.source.get_all()
-        values_count = len(values)
-        for i, value in enumerate(values):
-            has_changeable_time = i > 0 and i < values_count - 1
-            keyframe = self._createKeyframe(value, has_changeable_time)
-            self.keyframes.append(keyframe)
-
-        self.drawLines()
-        self.updating_keyframes = updating
-
-    def cleanup(self):
-        Zoomable.__del__(self)
-        self.disconnectFromEvents()
-
-    def disconnectFromEvents(self):
-        self.dragAction.disconnect_by_func(self._dragProgressCb)
-        self.dragAction.disconnect_by_func(self._dragBeginCb)
-        self.dragAction.disconnect_by_func(self._dragEndCb)
-        self.remove_action(self.dragAction)
-        self.bElement.selected.disconnect_by_func(self._selectedChangedCb)
-        self.bElement.disconnect_by_func(self._durationChangedCb)
-        self.bElement.disconnect_by_func(self._inpointChangedCb)
-        self.disconnect_by_func(self._clickedCb)
-
-    # private API
-
-    def _createMarker(self):
-        marker = Clutter.Actor()
-        self.add_child(marker)
-        return marker
-
-    def update(self, ease):
-        start = self.bElement.get_start()
-        duration = self.bElement.get_duration()
-        # The calculation of the duration assumes that the start is always
-        # int(pixels_float). In that case, the rounding can add up and a pixel
-        # might be lost if we ignore the start of the clip.
-        size = self.nsToPixel(start + duration) - self.nsToPixel(start)
-        # Avoid elements to become invisible.
-        size = max(size, 1)
-        self.set_size(size, EXPANDED_SIZE, ease)
-
-    def setDragged(self, dragged):
-        brother = self.timeline.findBrother(self.bElement)
-        if brother:
-            brother.isDragged = dragged
-        self.isDragged = dragged
-
-    def _createMixingKeyframes(self):
-        if self.track_type == GES.TrackType.VIDEO:
-            propname = "alpha"
-        else:
-            propname = "volume"
-
-        for spec in self.bElement.list_children_properties():
-            if spec.name == propname:
-                self.showKeyframes(self.bElement, spec, isDefault=True)
-
-        self.hideKeyframes()
-
-    def _createKeyframe(self, value, has_changeable_time):
-        keyframe = Keyframe(self, value, has_changeable_time)
-        self.insert_child_above(keyframe, self._keyframesMarker)
-        self.setKeyframePosition(keyframe, value)
-        return keyframe
-
-    def _createLine(self, keyframe, lastKeyframe, line):
-        if not line:
-            line = Line(self, keyframe, lastKeyframe)
-            self.lines.append(line)
-            self.insert_child_above(line, self._linesMarker)
-
-        adj = self.nsToPixel(
-            keyframe.value.timestamp - lastKeyframe.value.timestamp)
-        opp = (lastKeyframe.value.value - keyframe.value.value) * EXPANDED_SIZE
-        hyp = math.sqrt(adj ** 2 + opp ** 2)
-        if hyp < 1:
-            # line length would be less than one pixel
-            return
+class VideoSource(TimelineElement):
 
-        sinX = opp / hyp
-        line.props.width = hyp
-        line.props.height = KEYFRAME_SIZE
-        line.props.rotation_angle_z = math.degrees(math.asin(sinX))
-        line.props.x = self.nsToPixel(
-            lastKeyframe.value.timestamp - self.bElement.props.in_point)
-        line.props.y = EXPANDED_SIZE - \
-            (EXPANDED_SIZE * lastKeyframe.value.value) - KEYFRAME_SIZE / 2
-        line.canvas.invalidate()
-
-    def _createGhostclip(self):
-        pass
+    __gtype_name__ = "PitiviVideoSource"
 
-    def _createBorder(self):
-        border = Clutter.Actor()
-        border.bElement = self.bElement
-        border.set_background_color(BORDER_NORMAL_COLOR)
-        border.set_position(0, 0)
-        return border
+    def _getBackground(self):
+        return VideoBackground()
 
-    def _createBackground(self):
-        raise NotImplementedError()
+    def do_get_preferred_height(self):
+        return ui.LAYER_HEIGHT / 2, ui.LAYER_HEIGHT
 
-    def _createHandles(self):
-        pass
 
-    def _createPreview(self):
-        if isinstance(self.bElement, GES.AudioUriSource):
-            previewer = AudioPreviewer(self.bElement, self.timeline)
-            previewer.startLevelsDiscoveryWhenIdle()
-            return previewer
-        if isinstance(self.bElement, GES.VideoUriSource):
-            return VideoPreviewer(self.bElement, self.timeline)
-        # TODO: GES.AudioTransition, GES.VideoTransition, GES.ImageSource,
-        # GES.TitleSource
-        return Clutter.Actor()
-
-    def _createMarquee(self):
-        marquee = Clutter.Actor()
-        marquee.bElement = self.bElement
-        marquee.set_background_color(CLIP_SELECTED_OVERLAY_COLOR)
-        marquee.props.visible = False
-        return marquee
-
-    def _connectToEvents(self):
-        self.dragAction = Clutter.DragAction()
-        self.add_action(self.dragAction)
-        self.dragAction.connect("drag-progress", self._dragProgressCb)
-        self.dragAction.connect("drag-begin", self._dragBeginCb)
-        self.dragAction.connect("drag-end", self._dragEndCb)
-        self.bElement.selected.connect(
-            "selected-changed", self._selectedChangedCb)
-        self.bElement.connect("notify::duration", self._durationChangedCb)
-        self.bElement.connect("notify::in-point", self._inpointChangedCb)
-        # We gotta go low-level cause Clutter.ClickAction["clicked"]
-        # gets emitted after Clutter.DragAction["drag-begin"]
-        self.connect("button-press-event", self._clickedCb)
-
-    def _getLayerForY(self, y):
-        if self.track_type == GES.TrackType.AUDIO:
-            y -= self.nbrLayers * (EXPANDED_SIZE + SPACING)
-        priority = int(y / (EXPANDED_SIZE + SPACING))
-        return priority
-
-    # Interface (Zoomable)
-
-    def zoomChanged(self):
-        self.update(False)
-        if self.isSelected:
-            self.updateKeyframes()
+class VideoUriSource(VideoSource):
 
-    # Callbacks
+    __gtype_name__ = "PitiviUriVideoSource"
 
-    def _clickedCb(self, action, actor):
-        pass
+    def __init__(self, element, timeline):
+        super(VideoUriSource, self).__init__(element, timeline)
+        self.get_style_context().add_class("VideoUriSource")
 
-    def _dragBeginCb(self, action, actor, event_x, event_y, modifiers):
-        pass
+    def _getPreviewer(self):
+        previewer = previewers.VideoPreviewer(self._bElement)
+        previewer.get_style_context().add_class("VideoUriSource")
 
-    def _dragProgressCb(self, unused_action, unused_actor, unused_delta_x, unused_delta_y):
-        return False
+        return previewer
 
-    def _dragEndCb(self, action, actor, event_x, event_y, modifiers):
-        pass
+    def _getDefaultMixingProperty(self):
+        for spec in self._bElement.list_children_properties():
+            if spec.name == "alpha":
+                return spec
 
-    def _durationChangedCb(self, unused_element, unused_duration):
-        if self.keyframesVisible:
-            self.updateKeyframes()
 
-    def _inpointChangedCb(self, unused_element, unused_inpoint):
-        if self.keyframesVisible:
-            self.updateKeyframes()
+class AudioBackground (Gtk.Box):
 
-    def _selectedChangedCb(self, unused_selected, isSelected):
-        self.isSelected = isSelected
-        if not isSelected:
-            self.hideKeyframes()
-        self.marquee.props.visible = isSelected
-        color = BORDER_SELECTED_COLOR if isSelected else BORDER_NORMAL_COLOR
-        self.border.set_background_color(color)
+    def __init__(self):
+        super(AudioBackground, self).__init__(self)
+        self.get_style_context().add_class("AudioBackground")
 
 
-class Gradient(Clutter.Actor):
+class AudioUriSource(TimelineElement):
 
-    def __init__(self, rb, gb, bb, re, ge, be):
-        """
-        Creates a rectangle with a gradient. The first three parameters
-        are the gradient's RGB values at the top, the last three params
-        are the RGB values at the bottom.
-        """
-        Clutter.Actor.__init__(self)
-        self.canvas = Clutter.Canvas()
-        self.linear = cairo.LinearGradient(0, 0, 10, EXPANDED_SIZE)
-        self.linear.add_color_stop_rgb(0, rb / 255., gb / 255., bb / 255.)
-        self.linear.add_color_stop_rgb(1, re / 255., ge / 255., be / 255.)
-        self.canvas.set_size(10, EXPANDED_SIZE)
-        self.canvas.connect("draw", self._drawCb)
-        self.set_content(self.canvas)
-        self.canvas.invalidate()
-
-    def _drawCb(self, unused_canvas, cr, unused_width, unused_height):
-        cr.set_operator(cairo.OPERATOR_CLEAR)
-        cr.paint()
-        cr.set_operator(cairo.OPERATOR_OVER)
-        cr.set_source(self.linear)
-        cr.rectangle(0, 0, 10, EXPANDED_SIZE)
-        cr.fill()
-
-
-class Line(Clutter.Actor):
-
-    """
-    A cairo line used for keyframe curves.
-    """
-
-    def __init__(self, timelineElement, keyframe, lastKeyframe):
-        Clutter.Actor.__init__(self)
-        self.timelineElement = weakref.proxy(timelineElement)
-
-        self.canvas = Clutter.Canvas()
-        self.canvas.set_size(1000, KEYFRAME_SIZE)
-        self.canvas.connect("draw", self._drawCb)
-        self.set_content(self.canvas)
-        self.set_reactive(True)
-
-        self.gotDragged = False
-
-        self.dragAction = Clutter.DragAction()
-        self.add_action(self.dragAction)
-
-        self.dragAction.connect("drag-begin", self._dragBeginCb)
-        self.dragAction.connect("drag-end", self._dragEndCb)
-        self.dragAction.connect("drag-progress", self._dragProgressCb)
+    __gtype_name__ = "PitiviAudioUriSource"
 
-        self.connect("button-release-event", self._clickedCb)
-        self.connect("motion-event", self._motionEventCb)
-        self.connect("enter-event", self._enterEventCb)
-        self.connect("leave-event", self._leaveEventCb)
+    def __init__(self, element, timeline):
+        super(AudioUriSource, self).__init__(element, timeline)
+        self.get_style_context().add_class("AudioUriSource")
 
-        self.previousKeyframe = lastKeyframe
-        self.nextKeyframe = keyframe
+    def do_get_preferred_height(self):
+        return ui.LAYER_HEIGHT / 2, ui.LAYER_HEIGHT
 
-    def _drawCb(self, unused_canvas, cr, width, unused_height):
-        """
-        This is where we actually create the line segments for keyframe curves.
-        We draw multiple lines (one-third of the height each) to add a "shadow"
-        around the actual line segment to improve visibility.
-        """
-        cr.set_operator(cairo.OPERATOR_CLEAR)
-        cr.paint()
-        cr.set_operator(cairo.OPERATOR_OVER)
-
-        # The "height budget" to draw line components = the tallest
-        # component...
-        _max_height = KEYFRAME_SIZE
-
-        # While normally all three lines would have an equal height,
-        # I make the shadow lines be 1/2 (3px) instead of 1/3 (2px),
-        # while keeping their 1/3 position... this softens them up.
-
-        # Upper shadow/border:
-        cr.set_source_rgba(0, 0, 0, 0.5)  # 50% transparent black color
-        cr.move_to(0, _max_height / 3)
-        cr.line_to(width, _max_height / 3)
-        cr.set_line_width(_max_height / 3)  # Special case: fuzzy 3px
-        cr.stroke()
-        # Lower shadow/border:
-        cr.set_source_rgba(0, 0, 0, 0.5)  # 50% transparent black color
-        cr.move_to(0, _max_height * 2 / 3)
-        cr.line_to(width, _max_height * 2 / 3)
-        cr.set_line_width(_max_height / 3)  # Special case: fuzzy 3px
-        cr.stroke()
-        # Draw the actual line in the middle.
-        # Do it last, so that it gets drawn on top and remains sharp.
-        cr.set_source_rgba(*KEYFRAME_LINE_COLOR)
-        cr.move_to(0, _max_height / 2)
-        cr.line_to(width, _max_height / 2)
-        cr.set_line_width(_max_height / 3)
-        cr.stroke()
-
-    def transposeXY(self, x, y):
-        x -= self.timelineElement.props.x + CONTROL_WIDTH - \
-            self.timelineElement.timeline._scroll_point.x
-        x += Zoomable.nsToPixel(self.timelineElement.bElement.props.in_point)
-        y -= self.timelineElement.props.y
-        return x, y
-
-    def _ungrab(self):
-        self.timelineElement.set_reactive(True)
-        self.timelineElement.timeline._container.embed.get_window().set_cursor(
-            NORMAL_CURSOR)
-
-    def _clickedCb(self, unused_actor, event):
-        if self.gotDragged:
-            self.gotDragged = False
-            return
-        x, unused_y = self.transposeXY(event.x, event.y)
-        timestamp = Zoomable.pixelToNs(x)
-        value = self._valueAtTimestamp(timestamp)
-        self.timelineElement.addKeyframe(value, timestamp)
-
-    def _valueAtTimestamp(self, timestamp):
-        timestamp_left = self.previousKeyframe.value.timestamp
-        value_left = self.previousKeyframe.value.value
-        timestamp_right = self.nextKeyframe.value.timestamp
-        value_right = self.nextKeyframe.value.value
-        height = value_right - value_left
-        duration = timestamp_right - timestamp_left
-        value = value_right - (timestamp_right - timestamp) * height / duration
-        return max(0.0, min(value, 1.0))
-
-    def _enterEventCb(self, unused_actor, unused_event):
-        self.timelineElement.set_reactive(False)
-        self.timelineElement.timeline._container.embed.get_window().set_cursor(
-            DRAG_CURSOR)
-
-    def _leaveEventCb(self, unused_actor, unused_event):
-        self._ungrab()
-
-    def _motionEventCb(self, actor, event):
-        pass
+    def _getPreviewer(self):
+        previewer = previewers.AudioPreviewer(self._bElement)
+        previewer.get_style_context().add_class("AudioUriSource")
+        previewer.startLevelsDiscoveryWhenIdle()
 
-    def _dragBeginCb(self, unused_action, unused_actor, event_x, event_y, unused_modifiers):
-        self.timelineElement.timeline._container.app.action_log.begin(
-            "Dragging keyframe line")
-        self.dragBeginStartX = event_x
-        self.dragBeginStartY = event_y
-        self.origY = self.props.y
-        self.previousKeyframe.startDrag(event_x, event_y, self)
-        self.nextKeyframe.startDrag(event_x, event_y, self)
+        return previewer
 
-    def _dragProgressCb(self, unused_action, unused_actor, unused_delta_x, delta_y):
-        self.gotDragged = True
-        coords = self.dragAction.get_motion_coords()
-        delta_x = coords[0] - self.dragBeginStartX
-        delta_y = coords[1] - self.dragBeginStartY
+    def _getBackground(self):
+        return AudioBackground()
 
-        self.previousKeyframe.updateValue(0, delta_y)
-        self.nextKeyframe.updateValue(0, delta_y)
+    def _getDefaultMixingProperty(self):
+        for spec in self._bElement.list_children_properties():
+            if spec.name == "volume":
+                return spec
 
-        return False
 
-    def _dragEndCb(self, unused_action, unused_actor, unused_event_x, unused_event_y, unused_modifiers):
-        self.previousKeyframe.endDrag()
-        self.nextKeyframe.endDrag()
-        if self.timelineElement.timeline.getActorUnderPointer() != self:
-            self._ungrab()
-        self.timelineElement.timeline._container.app.action_log.commit()
+class TrimHandle(Gtk.EventBox, Loggable):
+
+    __gtype_name__ = "PitiviTrimHandle"
 
+    DEFAULT_WIDTH = 5
 
-class KeyframeMenu(GtkClutter.Actor):
+    def __init__(self, clip, edge):
+        Gtk.EventBox.__init__(self)
+        Loggable.__init__(self)
 
-    def __init__(self, keyframe):
-        GtkClutter.Actor.__init__(self)
-        self.keyframe = keyframe
-        vbox = Gtk.Box()
-        vbox.set_orientation(Gtk.Orientation.VERTICAL)
+        self.clip = clip
+        self.get_style_context().add_class("Trimbar")
+        self.edge = edge
 
-        button = Gtk.Button()
-        button.set_label("Remove")
-        button.connect("clicked", self._removeClickedCb)
-        vbox.pack_start(button, False, False, 0)
+        self.connect("event", self._eventCb)
+        self.connect("notify::window", self._windowSetCb)
 
-        self.get_widget().add(vbox)
-        self.vbox = vbox
-        self.vbox.hide()
-        self.set_reactive(True)
+    def _windowSetCb(self, window, pspec):
+        self.props.window.set_cursor(CURSORS[self.edge])
 
-    def show(self):
-        GtkClutter.Actor.show(self)
-        self.vbox.show_all()
+    def do_show_all(self):
+        self.info("DO not do anythin on .show_all")
 
-    def hide(self):
-        GtkClutter.Actor.hide(self)
-        self.vbox.hide()
+    def _eventCb(self, element, event):
+        if event.type == Gdk.EventType.ENTER_NOTIFY:
+            self.clip.edit_mode = GES.EditMode.EDIT_TRIM
+            self.clip.dragging_edge = self.edge
+        elif event.type == Gdk.EventType.LEAVE_NOTIFY:
+            self.clip.dragging_edge = GES.Edge.EDGE_NONE
+            self.clip.edit_mode = None
 
-    def _removeClickedCb(self, unused_button):
-        self.keyframe.remove()
+        return False
 
+    def do_get_preferred_width(self):
+        return TrimHandle.DEFAULT_WIDTH, TrimHandle.DEFAULT_WIDTH
 
-class Keyframe(Clutter.Actor):
+    def do_draw(self, cr):
+        Gtk.EventBox.do_draw(self, cr)
+        Gdk.cairo_set_source_pixbuf(cr, GdkPixbuf.Pixbuf.new_from_file(os.path.join(
+                                    configure.get_pixmap_dir(), "trimbar-focused.png")), 10, 10)
 
-    """
-    @ivar has_changeable_time: if False, it means this is an edge keyframe.
-    @type has_changeable_time: bool
-    """
 
-    def __init__(self, timelineElement, value, has_changeable_time):
-        Clutter.Actor.__init__(self)
+class Clip(Gtk.EventBox, timelineUtils.Zoomable, Loggable):
 
-        self.value = value
-        self.timelineElement = weakref.proxy(timelineElement)
-        self.has_changeable_time = has_changeable_time
-        self.lastClick = datetime.now()
+    __gtype_name__ = "PitiviClip"
 
-        self.set_size(KEYFRAME_SIZE, KEYFRAME_SIZE)
-        self.set_background_color(KEYFRAME_NORMAL_COLOR)
+    def __init__(self, layer, bClip):
+        super(Clip, self).__init__()
+        timelineUtils.Zoomable.__init__(self)
+        Loggable.__init__(self)
 
-        self.dragAction = Clutter.DragAction()
-        self.add_action(self.dragAction)
+        self.handles = []
+        self.z_order = -1
+        self.layer = layer
+        self.timeline = layer.timeline
+        self.app = layer.app
 
-        self.dragAction.connect("drag-begin", self._dragBeginCb)
-        self.dragAction.connect("drag-end", self._dragEndCb)
-        self.dragAction.connect("drag-progress", self._dragProgressCb)
-        self.connect("enter-event", self._enterEventCb)
-        self.connect("leave-event", self._leaveEventCb)
-        self.connect("button-press-event", self._clickedCb)
+        self.bClip = bClip
+        self.bClip.ui = self
+        self.bClip.selected = timelineUtils.Selected()
 
-        self.createMenu()
-        self.dragProgressed = False
-        self.set_reactive(True)
+        self._audioSource = None
+        self._videoSource = None
 
-    def createMenu(self):
-        self.menu = KeyframeMenu(self)
-        self.timelineElement.timeline._container.stage.connect(
-            "button-press-event", self._stageClickedCb)
-        self.timelineElement.timeline.add_child(self.menu)
+        self._setupWidget()
 
-    def _unselect(self):
-        self.timelineElement.set_reactive(True)
-        self.set_background_color(KEYFRAME_NORMAL_COLOR)
-        self.timelineElement.timeline._container.embed.get_window().set_cursor(
-            NORMAL_CURSOR)
+        for child in self.bClip.get_children(False):
+            self._childAdded(self.bClip, child)
 
-    def remove(self):
-        # Can't remove edge keyframes !
-        if not self.has_changeable_time:
-            return
+        self._savePositionState()
+        self._connectWidgetSignals()
 
-        self.timelineElement.timeline.remove_child(self.menu)
-        self._unselect()
-        self.timelineElement.removeKeyframe(self)
-
-    def _stageClickedCb(self, stage, event):
-        actor = stage.get_actor_at_pos(
-            Clutter.PickMode.REACTIVE, event.x, event.y)
-        if actor != self.menu:
-            self.menu.hide()
-
-    def _clickedCb(self, unused_actor, event):
-        if (event.modifier_state & Clutter.ModifierType.CONTROL_MASK):
-            self.remove()
-        elif (datetime.now() - self.lastClick).total_seconds() < 0.5:
-            self.remove()
-
-        self.lastClick = datetime.now()
-
-    def _enterEventCb(self, unused_actor, unused_event):
-        self.timelineElement.set_reactive(False)
-        self.set_background_color(KEYFRAME_SELECTED_COLOR)
-        self.timelineElement.timeline._container.embed.get_window().set_cursor(
-            DRAG_CURSOR)
-
-    def _leaveEventCb(self, unused_actor, unused_event):
-        self._unselect()
-
-    def startDrag(self, event_x, event_y, line=None):
-        self.dragBeginStartX = event_x
-        self.dragBeginStartY = event_y
-        self.lastTs = self.value.timestamp
-        self.valueStart = self.value.value
-        self.tsStart = self.value.timestamp
-        self.duration = self.timelineElement.bElement.props.duration
-        self.inpoint = self.timelineElement.bElement.props.in_point
-        self.start = self.timelineElement.bElement.props.start
-        self.line = line
-
-    def endDrag(self):
-        if not self.dragProgressed and not self.line:
-            timeline = self.timelineElement.timeline
-            self.menu.set_position(
-                self.timelineElement.props.x + self.props.x + 10, self.timelineElement.props.y + 
self.props.y + 10)
-            self.menu.show()
-
-        self.line = None
-
-    def updateValue(self, delta_x, delta_y):
-        newTs = self.tsStart + Zoomable.pixelToNs(delta_x)
-        newValue = self.valueStart - (delta_y / EXPANDED_SIZE)
-
-        # Don't overlap first and last keyframes.
-        newTs = min(max(newTs, self.inpoint + 1),
-                    self.duration + self.inpoint - 1)
-
-        newValue = min(max(newValue, 0.0), 1.0)
-
-        if not self.has_changeable_time:
-            newTs = self.lastTs
-
-        updating = self.timelineElement.updating_keyframes
-        self.timelineElement.updating_keyframes = True
-        self.timelineElement.source.unset(self.lastTs)
-        if (self.timelineElement.source.set(newTs, newValue)):
-            self.value = Gst.TimedValue()
-            self.value.timestamp = newTs
-            self.value.value = newValue
-            self.lastTs = newTs
-
-            self.timelineElement.setKeyframePosition(self, self.value)
-            # Resort the keyframes list each time. Should be cheap as there should never be too much 
keyframes,
-            # if optimization is needed, check if resorting is needed, should
-            # not be in 99 % of the cases.
-            self.timelineElement.keyframes = sorted(
-                self.timelineElement.keyframes, key=lambda keyframe: keyframe.value.timestamp)
-            self.timelineElement.drawLines(self.line)
-            # This will update the viewer. nifty.
-            if not self.line:
-                self.timelineElement.timeline._container.seekInPosition(
-                    newTs + self.start)
-
-        self.timelineElement.updating_keyframes = updating
-
-    def _dragBeginCb(self, unused_action, unused_actor, event_x, event_y, unused_modifiers):
-        self.timelineElement.timeline._container.app.action_log.begin(
-            "Dragging keyframe")
-        self.dragProgressed = False
-        self.startDrag(event_x, event_y)
-
-    def _dragProgressCb(self, unused_action, unused_actor, delta_x, delta_y):
-        self.dragProgressed = True
-        coords = self.dragAction.get_motion_coords()
-        delta_x = coords[0] - self.dragBeginStartX
-        delta_y = coords[1] - self.dragBeginStartY
-        self.updateValue(delta_x, delta_y)
-        return False
+        self.edit_mode = None
+        self.dragging_edge = GES.Edge.EDGE_NONE
 
-    def _dragEndCb(self, unused_action, unused_actor, unused_event_x, unused_event_y, unused_modifiers):
-        self.endDrag()
-        if self.timelineElement.timeline.getActorUnderPointer() != self:
-            self._unselect()
-        self.timelineElement.timeline._container.app.action_log.commit()
+        self._connectGES()
+        self.get_accessible().set_name(self.bClip.get_name())
 
+    def do_get_preferred_width(self):
+        return self.nsToPixel(self.bClip.props.duration), self.nsToPixel(self.bClip.props.duration)
 
-class URISourceElement(TimelineElement):
+    def do_get_preferred_height(self):
+        parent = self.get_parent()
+        return parent.get_allocated_height(), parent.get_allocated_height()
 
-    def __init__(self, bElement, timeline):
-        TimelineElement.__init__(self, bElement, timeline)
-        self.gotDragged = False
+    def _savePositionState(self):
+        self._current_x = self.nsToPixel(self.bClip.props.start)
+        self._curent_width = self.nsToPixel(self.bClip.props.duration)
+        parent = self.get_parent()
+        if parent:
+            self._current_parent_height = self.get_parent(
+            ).get_allocated_height()
+        else:
+            self._current_parent_height = 0
+        self._current_parent = parent
 
-    # public API
+    def updatePosition(self):
+        parent = self.get_parent()
+        x = self.nsToPixel(self.bClip.props.start)
+        width = self.nsToPixel(self.bClip.props.duration)
+        parent_height = parent.get_allocated_height()
 
-    def hideHandles(self):
-        self.rightHandle.hide()
-        self.leftHandle.hide()
+        if x != self._current_x or \
+                width != self._curent_width \
+                or parent_height != self._current_parent_height or \
+                parent != self._current_parent:
 
-    # private API
+            self.layer.move(self, x, 0)
+            self.set_size_request(width, parent_height)
 
-    def _createGhostclip(self):
-        self.ghostclip = Ghostclip(self.track_type, self.bElement)
-        self.timeline.add_child(self.ghostclip)
+            elements = self._elements_container.get_children()
+            for child in elements:
+                child.setSize(width, parent_height / len(elements))
 
-    def _createHandles(self):
-        self.leftHandle = TrimHandle(self, True)
-        self.rightHandle = TrimHandle(self, False)
+            self._savePositionState()
 
-        self.leftHandle.set_position(0, 0)
+    def _setupWidget(self):
+        pass
 
-        self.add_child(self.leftHandle)
-        self.add_child(self.rightHandle)
+    def sendFakeEvent(self, event, event_widget):
+        if event.type == Gdk.EventType.BUTTON_RELEASE:
+            self._clickedCb(event_widget, event)
 
-    def _createBackground(self):
-        if self.track_type == GES.TrackType.AUDIO:
-            # Audio clips go from dark green to light green
-            # (27, 46, 14, 255) to (73, 108, 33, 255)
-            background = Gradient(27, 46, 14, 73, 108, 33)
-        else:
-            # Video clips go from almost black to gray
-            # (15, 15, 15, 255) to (45, 45, 45, 255)
-            background = Gradient(15, 15, 15, 45, 45, 45)
-        background.bElement = self.bElement
-        return background
+        self.timeline.sendFakeEvent(event, event_widget)
+
+    def do_draw(self, cr):
+        self.updatePosition()
+        Gtk.EventBox.do_draw(self, cr)
 
-    # Callbacks
     def _clickedCb(self, unused_action, unused_actor):
+        if self.timeline.got_dragged:
+            # If the timeline just got dragged and @self
+            # is the element initiating the mode,
+            # do not do anything when the button is
+            # released
+            self.timeline.got_dragged = False
+
+            return False
+
         # TODO : Let's be more specific, masks etc ..
-        mode = SELECT
-        if self.timeline._container._controlMask:
-            if not self.bElement.selected:
-                mode = SELECT_ADD
+        mode = timelineUtils.SELECT
+        if self.timeline.parent._controlMask:
+            if not self.get_state_flags() & Gtk.StateFlags.SELECTED:
+                mode = timelineUtils.SELECT_ADD
                 self.timeline.current_group.add(
-                    self.bElement.get_toplevel_parent())
+                    self.bClip.get_toplevel_parent())
             else:
                 self.timeline.current_group.remove(
-                    self.bElement.get_toplevel_parent())
-                mode = UNSELECT
-        elif not self.bElement.selected:
+                    self.bClip.get_toplevel_parent())
+                mode = timelineUtils.UNSELECT
+        elif not self.get_state_flags() & Gtk.StateFlags.SELECTED:
             GES.Container.ungroup(self.timeline.current_group, False)
             self.timeline.createSelectionGroup()
             self.timeline.current_group.add(
-                self.bElement.get_toplevel_parent())
-            self.timeline._container.gui.switchContextTab(self.bElement)
+                self.bClip.get_toplevel_parent())
+            self.timeline.parent.gui.switchContextTab(self.bClip)
 
-        children = self.bElement.get_toplevel_parent().get_children(True)
-        selection = [elem for elem in children if isinstance(elem, GES.Source)]
+        parent = self.bClip.get_parent()
+        if parent == self.timeline.current_group or parent is None:
+            selection = [self.bClip]
+        else:
+            while parent:
+                if parent.get_parent() == self.timeline.current_group:
+                    break
+                parent = parent.get_parent()
+
+            children = parent.get_children(True)
+            selection = [elem for elem in children if isinstance(elem, GES.SourceClip) or
+                         isinstance(elem, GES.TransitionClip)]
 
         self.timeline.selection.setSelection(selection, mode)
 
-        if self.keyframedElement:
-            self.showKeyframes(self.keyframedElement, self.prop)
+        # if self.keyframedElement:
+        #    self.showKeyframes(self.keyframedElement, self.prop)
 
         return False
 
-    def _dragBeginCb(self, action, actor, event_x, event_y, modifiers):
-        self.gotDragged = False
-        mode = self.timeline._container.getEditionMode()
-
-        # This can't change during a drag, so we can safely compute it now for
-        # drag events.
-        nbrLayers = len(self.timeline.bTimeline.get_layers())
-        self.brother = self.timeline.findBrother(self.bElement)
-        self._dragBeginStart = self.bElement.get_start()
-        self.dragBeginStartX = event_x
-        self.dragBeginStartY = event_y
-
-        self.nbrLayers = nbrLayers
-        self.ghostclip.setNbrLayers(nbrLayers)
-        self.ghostclip.setWidth(self.props.width)
-        if self.brother:
-            self.brother.ghostclip.setWidth(self.props.width)
-            self.brother.ghostclip.setNbrLayers(nbrLayers)
-
-        # We can also safely find if the object has a brother element
-        self.setDragged(True)
-
-    def _dragProgressCb(self, action, actor, delta_x, delta_y):
-        # We can't use delta_x here because it fluctuates weirdly.
-        if not self.gotDragged:
-            self.gotDragged = True
-            self._context = EditingContext(self.bElement,
-                                           self.timeline.bTimeline,
-                                           None,
-                                           GES.Edge.EDGE_NONE,
-                                           None,
-                                           self.timeline._container.app.action_log)
-
-        mode = self.timeline._container.getEditionMode()
-        self._context.setMode(mode)
-
-        coords = self.dragAction.get_motion_coords()
-        delta_x = coords[0] - self.dragBeginStartX
-        delta_y = coords[1] - self.dragBeginStartY
-        y = coords[1] + self.timeline._container.point.y
-        priority = self._getLayerForY(y)
-        new_start = self._dragBeginStart + self.pixelToNs(delta_x)
-
-        self.ghostclip.props.x = max(
-            0, self.nsToPixel(self._dragBeginStart) + delta_x)
-        self.ghostclip.update(priority, y, False)
-        if self.brother:
-            self.brother.ghostclip.props.x = max(
-                0, self.nsToPixel(self._dragBeginStart) + delta_x)
-            self.brother.ghostclip.update(priority, y, True)
-
-        if not self.ghostclip.props.visible:
-            self._context.editTo(
-                new_start, self.bElement.get_parent().get_layer().get_priority())
-        else:
-            self._context.editTo(
-                self._dragBeginStart, self.bElement.get_parent().get_layer().get_priority())
+    def _connectWidgetSignals(self):
+        self.connect("button-release-event", self._clickedCb)
+        self.connect("event", self._eventCb)
+
+    def _eventCb(self, element, event):
+        if event.type == Gdk.EventType.ENTER_NOTIFY:
+            ui.set_children_state_recurse(self, Gtk.StateFlags.PRELIGHT)
+            for handle in self.handles:
+                handle.show()
+        elif event.type == Gdk.EventType.LEAVE_NOTIFY:
+            ui.unset_children_state_recurse(self, Gtk.StateFlags.PRELIGHT)
+            for handle in self.handles:
+                handle.hide()
 
-        self.timeline._updateSize(self.ghostclip)
         return False
 
-    def _dragEndCb(self, action, actor, event_x, event_y, modifiers):
-        coords = self.dragAction.get_motion_coords()
-        delta_x = coords[0] - self.dragBeginStartX
-        new_start = self._dragBeginStart + self.pixelToNs(delta_x)
-        priority = self._getLayerForY(
-            coords[1] + self.timeline._container.point.y)
-        priority = min(priority, len(self.timeline.bTimeline.get_layers()))
-        priority = max(0, priority)
-
-        self.timeline._snapEndedCb()
-        self.setDragged(False)
-
-        self.ghostclip.props.visible = False
-        if self.brother:
-            self.brother.ghostclip.props.visible = False
-
-        if self.ghostclip.shouldCreateLayer:
-            self.timeline.createLayerForGhostClip(self.ghostclip)
-
-        if self.gotDragged:
-            self._context.editTo(new_start, priority)
-            self._context.finish()
-
-    def cleanup(self):
-        if self.preview and not type(self.preview) is Clutter.Actor:
-            self.preview.cleanup()
-        self.leftHandle.cleanup()
-        self.leftHandle = None
-        self.rightHandle.cleanup()
-        self.rightHandle = None
-        TimelineElement.cleanup(self)
-
-
-class TransitionElement(TimelineElement):
-
-    def __init__(self, bElement, timeline):
-        TimelineElement.__init__(self, bElement, timeline)
-        self.isDragged = True
-        self.set_reactive(True)
-
-    def _createBackground(self):
-        background = Clutter.Actor()
-        background.set_background_color(TRANSITION_COLOR)
-        return background
-
-    def _createBorder(self):
-        border = Clutter.Actor()
-        border.set_background_color(Clutter.Color.new(0, 0, 0, 0))
-        return border
-
-    def _selectedChangedCb(self, selected, isSelected):
-        TimelineElement._selectedChangedCb(self, selected, isSelected)
-
-        if isSelected:
-            self.timeline._container.app.gui.trans_list.activate(self.bElement)
+    def _startChangedCb(self, unused_clip, unused_pspec):
+        if self.get_parent() is None:
+            # FIXME Check why that happens at all (looks like a GTK bug)
+            return
+
+        self.layer.move(self, self.nsToPixel(self.bClip.props.start), 0)
+
+    def _durationChangedCb(self, unused_clip, unused_pspec):
+        parent = self.get_parent()
+        if parent:
+            duration = self.nsToPixel(self.bClip.props.duration)
+            parent_height = parent.get_allocated_height()
+            self.set_size_request(duration, parent_height)
+
+    def _layerChangedCb(self, bClip, unused_pspec):
+        bLayer = bClip.props.layer
+        if bLayer:
+            self.layer = bLayer.ui
+
+    def _childAdded(self, clip, child):
+        child.selected = timelineUtils.Selected()
+
+    def _childAddedCb(self, clip, child):
+        self._childAdded(clip, child)
+
+    def _childRemoved(self, clip, child):
+        pass
+
+    def _childRemovedCb(self, clip, child):
+        self._childRemoved(clip, child)
+
+    def _connectGES(self):
+        self.bClip.connect("notify::start", self._startChangedCb)
+        self.bClip.connect("notify::inpoint", self._startChangedCb)
+        self.bClip.connect("notify::duration", self._durationChangedCb)
+        self.bClip.connect("notify::layer", self._layerChangedCb)
+
+        self.bClip.connect_after("child-added", self._childAddedCb)
+        self.bClip.connect_after("child-removed", self._childRemovedCb)
+
+
+class SourceClip(Clip):
+    __gtype_name__ = "PitiviSourceClip"
+
+    def __init__(self, layer, bClip):
+        super(SourceClip, self).__init__(layer, bClip)
+
+    def _setupWidget(self):
+        self._vbox = Gtk.Box()
+        self._vbox.set_orientation(Gtk.Orientation.HORIZONTAL)
+        self.add(self._vbox)
+
+        self.leftHandle = TrimHandle(self, GES.Edge.EDGE_START)
+        self._vbox.pack_start(self.leftHandle, False, False, 0)
+
+        self._elements_container = Gtk.Paned.new(Gtk.Orientation.VERTICAL)
+        self._vbox.pack_start(self._elements_container, True, True, 0)
+
+        self.rightHandle = TrimHandle(self, GES.Edge.EDGE_END)
+        self._vbox.pack_end(self.rightHandle, False, False, 0)\
+
+        self.handles.append(self.leftHandle)
+        self.handles.append(self.rightHandle)
+
+        self.get_style_context().add_class("Clip")
+
+    def _childRemoved(self, clip, child):
+        if child.ui is not None:
+            self._elements_container.remove(child.ui)
+            child.ui = None
+
+
+class UriClip(SourceClip):
+    __gtype_name__ = "PitiviuriClip"
+
+    def __init__(self, layer, bClip):
+        super(UriClip, self).__init__(layer, bClip)
+
+        self.set_tooltip_markup(misc.filename_from_uri(bClip.get_uri()))
+
+    def _childAdded(self, clip, child):
+        if isinstance(child, GES.Source):
+            if child.get_track_type() == GES.TrackType.AUDIO:
+                self._audioSource = AudioUriSource(child, self.timeline)
+                child.ui = self._audioSource
+                self._elements_container.pack2(self._audioSource, True, False)
+                self._audioSource.set_visible(True)
+            elif child.get_track_type() == GES.TrackType.VIDEO:
+                self._videoSource = VideoUriSource(child, self.timeline)
+                child.ui = self._videoSource
+                self._elements_container.pack1(self._videoSource, True, False)
+                self._videoSource.set_visible(True)
         else:
-            self.timeline._container.app.gui.trans_list.deactivate()
+            child.ui = None
 
-    def _clickedCb(self, action, actor):
-        selection = {self.bElement}
-        self.timeline.selection.setSelection(selection, SELECT)
-        return False
+
+class TitleClip(SourceClip):
+    __gtype_name__ = "PitiviTitleClip"
+
+    def _childAdded(self, clip, child):
+        if isinstance(child, GES.Source):
+            if child.get_track_type() == GES.TrackType.VIDEO:
+                self._videoSource = VideoSource(child, self.timeline)
+                child.ui = self._videoSource
+                self._elements_container.pack1(self._videoSource, True, False)
+                self._videoSource.set_visible(True)
+        else:
+            child.ui = None
+
+
+class TransitionClip(Clip):
+
+    __gtype_name__ = "PitiviTransitionClip"
+
+    def __init__(self, layer, bClip):
+        super(TransitionClip, self).__init__(layer, bClip)
+        self.get_style_context().add_class("TransitionClip")
+        self.z_order = 0
+
+        for child in bClip.get_children(True):
+            child.selected = timelineUtils.Selected()
+        self.bClip.connect("child-added", self._childAddedCb)
+        self.selected = False
+        self.connect("state-flags-changed", self._selectedChangedCb)
+        self.connect("button-press-event", self._pressEventCb)
+
+        # In the case of TransitionClips, we are the only container
+        self._elements_container = self
+        self.set_tooltip_markup("<span foreground='blue'>%s</span>" %
+                                str(bClip.props.vtype.value_nick))
+
+    def _childAdded(self, clip, child):
+        child.selected = timelineUtils.Selected()
+
+        if isinstance(child, GES.VideoTransition):
+            self.z_order += 1
+
+    def do_draw(self, cr):
+        Clip.do_draw(self, cr)
+
+    def _selectedChangedCb(self, unused_widget, flags):
+        if not [c for c in self.bClip.get_children(True) if isinstance(c, GES.VideoTransition)]:
+            return
+
+        if flags & Gtk.StateFlags.SELECTED:
+            self.timeline.parent.app.gui.trans_list.activate(self.bClip)
+            self.selected = True
+        elif self.selected:
+            self.selected = False
+            self.timeline.parent.app.gui.trans_list.deactivate()
+
+    def _pressEventCb(self, unused_action, unused_widget):
+        selection = {self.bClip}
+        self.timeline.selection.setSelection(selection, timelineUtils.SELECT)
+        return True
+
+GES_TYPE_UI_TYPE = {
+    GES.UriClip.__gtype__: UriClip,
+    GES.TitleClip.__gtype__: TitleClip,
+    GES.TransitionClip.__gtype__: TransitionClip
+}
diff --git a/pitivi/timeline/layer.py b/pitivi/timeline/layer.py
index d9ba35f..4628f8a 100644
--- a/pitivi/timeline/layer.py
+++ b/pitivi/timeline/layer.py
@@ -21,18 +21,19 @@
 # Boston, MA 02110-1301, USA.
 
 from gi.repository import Gtk
-from gi.repository import Gdk
 from gi.repository import GES
 from gi.repository import GObject
 
 from gettext import gettext as _
 
+from pitivi.timeline import elements
 from pitivi.utils.loggable import Loggable
-from pitivi.utils.ui import LAYER_CONTROL_TARGET_ENTRY
+from pitivi.utils import ui
+from pitivi.utils import timeline as timelineUtils
 
 
-# TODO GTK3 port to GtkGrid
 class BaseLayerControl(Gtk.Box, Loggable):
+
     """
     Base Layer control classes
     """
@@ -70,9 +71,6 @@ class BaseLayerControl(Gtk.Box, Loggable):
         self.eventbox.connect("button_press_event", self._buttonPressCb)
         self.pack_start(self.eventbox, True, True, 0)
 
-        self.sep = SpacedSeparator()
-        self.pack_start(self.sep, True, True, 0)
-
         icon_mapping = {GES.TrackType.AUDIO: "audio-x-generic",
                         GES.TrackType.VIDEO: "video-x-generic"}
 
@@ -157,9 +155,6 @@ class BaseLayerControl(Gtk.Box, Loggable):
         self.popup.show_all()
 
         # Drag and drop
-#        self.drag_source_set(Gdk.ModifierType.BUTTON1_MASK,
-#                             [LAYER_CONTROL_TARGET_ENTRY],
-#                             Gdk.DragAction.MOVE)
 
     def getSelected(self):
         return self._selected
@@ -198,7 +193,7 @@ class BaseLayerControl(Gtk.Box, Loggable):
         """
         Look if user selected layer or wants popup menu
         """
-        self._control_container.selectLayerControl(self)
+        # FIXME!! self._control_container.selectLayerControl(self)
         if event.button == 3:
             self.popup.popup(None, None, None, None, event.button, event.time)
 
@@ -222,22 +217,24 @@ class BaseLayerControl(Gtk.Box, Loggable):
 
     def _deleteLayerCb(self, unused_widget):
         self._app.action_log.begin("delete layer")
-        self._control_container.timeline.bTimeline.remove_layer(self.layer)
-        self._control_container.timeline.bTimeline.get_asset().pipeline.commit_timeline()
+        bLayer = self.layer.bLayer
+        bTimeline = bLayer.get_timeline()
+        bTimeline.remove_layer(bLayer)
+        bTimeline.get_asset().pipeline.commit_timeline()
         self._app.action_log.commit()
 
     def _moveLayerCb(self, unused_widget, step):
-        index = self.layer.get_priority()
+        index = self.layer.bLayer.get_priority()
         if abs(step) == 1:
             index += step
         elif step == -2:
             index = 0
         else:
-            index = len(self.layer.get_timeline().get_layers()) - 1
+            index = len(self.layer.bLayer.get_timeline().get_layers()) - 1
             # if audio, set last position
 
         self._app.moveLayer(self, index)
-#        self._app.timeline._container.app.gui.timeline_ui.controls.moveControlWidget(self, index)
+        # self._app.timeline.parent.app.gui.timeline_ui.controls.moveControlWidget(self, index)
 
     def getHeight(self):
         return self.get_allocation().height
@@ -390,7 +387,227 @@ class SpacedSeparator(Gtk.EventBox):
 
         self.box = Gtk.Box()
         self.box.set_orientation(Gtk.Orientation.VERTICAL)
-        self.box.add(Gtk.HSeparator())
-        self.box.set_border_width(6)
-
         self.add(self.box)
+
+        self.get_style_context().add_class("SpacedSeparator")
+        self.box.get_style_context().add_class("SpacedSeparator")
+
+
+class LayerControls(Gtk.Bin, Loggable):
+
+    __gtype_name__ = 'PitiviLayerControls'
+
+    def __init__(self, bLayer, app):
+        super(LayerControls, self).__init__()
+        Loggable.__init__(self)
+
+        ebox = Gtk.EventBox()
+        self.add(ebox)
+        self._hbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
+        ebox.add(self._hbox)
+        self.bLayer = bLayer
+        self.app = app
+
+        sep = SpacedSeparator()
+        self._hbox.pack_start(sep, False, False, 5)
+
+        self.video_control = VideoLayerControl(None, self, self.app)
+        self.video_control.set_visible(True)
+        self.video_control.props.width_request = ui.CONTROL_WIDTH
+        self.video_control.props.height_request = ui.LAYER_HEIGHT / 2
+        self._hbox.add(self.video_control)
+
+        self.audio_control = AudioLayerControl(None, self, self.app)
+        self.audio_control.set_visible(True)
+        self.audio_control.props.height_request = ui.LAYER_HEIGHT / 2
+        self.audio_control.props.width_request = ui.CONTROL_WIDTH
+        self._hbox.add(self.audio_control)
+
+        self._hbox.props.vexpand = False
+        self._hbox.props.width_request = ui.CONTROL_WIDTH
+        self.props.width_request = ui.CONTROL_WIDTH
+
+        sep = SpacedSeparator()
+        self._hbox.pack_start(sep, False, False, 5)
+
+
+class LayerLayout(Gtk.Layout, Loggable):
+    """
+    A GtkLayout that exclusivly container Clips.
+    This allows us to properly handle the z order of
+    """
+    __gtype_name__ = "PitiviLayerLayout"
+
+    def __init__(self, timeline):
+        super(LayerLayout, self).__init__()
+        Loggable.__init__(self)
+
+        self._children = []
+        self._changed = False
+        self.timeline = timeline
+
+        self.props.hexpand = True
+        self.get_style_context().add_class("LayerLayout")
+
+    def do_add(self, widget):
+        self._children.append(widget)
+        self._children.sort(key=lambda clip: clip.z_order)
+        Gtk.Layout.do_add(self, widget)
+        self._changed = True
+
+        for child in self._children:
+            if isinstance(child, elements.TransitionClip):
+                window = child.get_window()
+                if window is not None:
+                    window.raise_()
+
+    def do_remove(self, widget):
+        self._children.remove(widget)
+        self._changed = True
+        Gtk.Layout.do_remove(self, widget)
+
+    def put(self, child, x, y):
+        self._children.append(child)
+        self._children.sort(key=lambda clip: clip.z_order)
+        Gtk.Layout.put(self, child, x, y)
+        self._changed = True
+
+    def do_draw(self, cr):
+        if self._changed:
+            self._children.sort(key=lambda clip: clip.z_order)
+            for child in self._children:
+
+                if isinstance(child, elements.TransitionClip):
+                    window = child.get_window()
+                    window.raise_()
+            self._changed = False
+
+        self.props.width = timelineUtils.Zoomable.nsToPixel(self.timeline.bTimeline.props.duration) + 500
+        self.props.width_request = timelineUtils.Zoomable.nsToPixel(self.timeline.bTimeline.props.duration) 
+ 500
+
+        for child in self._children:
+            self.propagate_draw(child, cr)
+
+
+class Layer(Gtk.EventBox, timelineUtils.Zoomable, Loggable):
+
+    __gtype_name__ = "PitiviLayer"
+
+    __gsignals__ = {
+        "remove-me": (GObject.SignalFlags.RUN_LAST, None, (),)
+    }
+
+    def __init__(self, bLayer, timeline):
+        super(Layer, self).__init__()
+        Loggable.__init__(self)
+
+        self.bLayer = bLayer
+        self.bLayer.ui = self
+        self.timeline = timeline
+        self.app = timeline.app
+
+        self.bLayer.connect("clip-added", self._clipAddedCb)
+        self.bLayer.connect("clip-removed", self._clipRemovedCb)
+
+        # FIXME Make the layer height user setable with 'Paned'
+        self.props.height_request = ui.LAYER_HEIGHT
+        self.props.valign = Gtk.Align.START
+
+        self._layout = LayerLayout(self.timeline)
+        self.add(self._layout)
+
+        self.media_types = GES.TrackType(0)
+        for clip in bLayer.get_clips():
+            self._addClip(clip)
+
+        self.before_sep = None
+        self.after_sep = None
+
+    def _checkMediaTypes(self, bClip=None):
+        self.media_types = GES.TrackType(0)
+        bClips = self.bLayer.get_clips()
+
+        """
+        FIXME: That produces segfault in GES/GSequence
+        if not bClips:
+            self.emit("remove-me")
+            return
+        """
+
+        for bClip in bClips:
+            for child in bClip.get_children(False):
+                self.media_types |= child.get_track().props.track_type
+                if self.media_types == (GES.TrackType.AUDIO | GES.TrackType.VIDEO):
+                    break
+
+        if not (self.media_types & GES.TrackType.AUDIO) and not (self.media_types & GES.TrackType.VIDEO):
+            self.media_types = GES.TrackType.AUDIO | GES.TrackType.VIDEO
+
+        height = 0
+        if self.media_types & GES.TrackType.AUDIO:
+            height += ui.LAYER_HEIGHT / 2
+            self.bLayer.control_ui.audio_control.show()
+        else:
+            self.bLayer.control_ui.audio_control.hide()
+
+        if self.media_types & GES.TrackType.VIDEO:
+            self.bLayer.control_ui.video_control.show()
+            height += ui.LAYER_HEIGHT / 2
+        else:
+            self.bLayer.control_ui.video_control.hide()
+
+        self.props.height_request = height
+        self.bLayer.control_ui.props.height_request = height
+
+    def move(self, child, x, y):
+        self._layout.move(child, x, y)
+
+    def _childAddedCb(self, bClip, child):
+        self._checkMediaTypes()
+
+    def _childRemovedCb(self, bClip, child):
+        self._checkMediaTypes()
+
+    def _clipAddedCb(self, layer, bClip):
+        self._addClip(bClip)
+
+    def _addClip(self, bClip):
+        ui_type = elements.GES_TYPE_UI_TYPE.get(bClip.__gtype__, None)
+        if ui_type is None:
+            self.error("Implement UI for type %s?" % bClip.__gtype__)
+            return
+
+        if not hasattr(bClip, "ui") or bClip.ui is None:
+            clip = ui_type(self, bClip)
+        else:
+            clip = bClip.ui
+
+        self._layout.put(clip, self.nsToPixel(bClip.props.start), 0)
+        self.show_all()
+        bClip.connect_after("child-added", self._childAddedCb)
+        bClip.connect_after("child-removed", self._childRemovedCb)
+        self._checkMediaTypes()
+
+    def _clipRemovedCb(self, bLayer, bClip):
+        self._removeClip(bClip)
+
+    def _removeClip(self, bClip):
+        ui_type = elements.GES_TYPE_UI_TYPE.get(bClip.__gtype__, None)
+        if ui_type is None:
+            self.error("Implement UI for type %s?" % bClip.__gtype__)
+            return
+
+        self._layout.remove(bClip.ui)
+        if self.timeline.draggingElement is None:
+            bClip.ui = None
+
+        bClip.disconnect_by_func(self._childAddedCb)
+        bClip.disconnect_by_func(self._childRemovedCb)
+        self._checkMediaTypes(bClip)
+
+    def updatePosition(self):
+        for bClip in self.bLayer.get_clips():
+            bClip.ui.updatePosition()
+
+    def do_draw(self, cr):
+        Gtk.Box.do_draw(self, cr)
diff --git a/pitivi/timeline/previewers.py b/pitivi/timeline/previewers.py
index 40ccd0e..dddd71b 100644
--- a/pitivi/timeline/previewers.py
+++ b/pitivi/timeline/previewers.py
@@ -20,7 +20,6 @@
 # Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
 # Boston, MA 02110-1301, USA.
 
-from datetime import datetime, timedelta
 from random import randrange
 import cairo
 import numpy
@@ -28,13 +27,13 @@ import os
 import pickle
 import sqlite3
 
-from gi.repository import Clutter
-from gi.repository import Cogl
 from gi.repository import GES
 from gi.repository import GObject
 from gi.repository import GLib
 from gi.repository import GdkPixbuf
 from gi.repository import Gst
+from gi.repository import Gdk
+from gi.repository import Gtk
 
 # Our C module optimizing waveforms rendering
 try:
@@ -45,10 +44,9 @@ except ImportError:
 
 from pitivi.settings import get_dir, xdg_cache_home
 from pitivi.utils.loggable import Loggable
-from pitivi.utils.misc import binary_search, filename_from_uri, quantize, quote_uri, hash_file, format_ns
+from pitivi.utils.misc import binary_search, filename_from_uri, quantize, quote_uri, hash_file
 from pitivi.utils.system import CPUUsageTracker
 from pitivi.utils.timeline import Zoomable
-from pitivi.utils.ui import CONTROL_WIDTH
 from pitivi.utils.ui import EXPANDED_SIZE
 
 
@@ -58,7 +56,6 @@ WAVEFORMS_CPU_USAGE = 30
 THUMBNAILS_CPU_USAGE = 20
 
 THUMB_MARGIN_PX = 3
-WAVEFORM_UPDATE_INTERVAL = timedelta(microseconds=500000)
 # For the waveforms, ensures we always have a little extra surface when
 # scrolling while playing.
 MARGIN = 500
@@ -151,25 +148,24 @@ class PreviewGenerator(object):
         PreviewGenerator.__manager.addPipeline(self)
 
 
-class VideoPreviewer(Clutter.ScrollActor, PreviewGenerator, Zoomable, Loggable):
+class VideoPreviewer(Gtk.Layout, PreviewGenerator, Zoomable, Loggable):
 
     # We could define them in PreviewGenerator, but then for some reason they
     # are ignored.
     __gsignals__ = PREVIEW_GENERATOR_SIGNALS
 
-    def __init__(self, bElement, timeline):
+    def __init__(self, bElement):
         """
         @param bElement : the backend GES.TrackElement
         @param track : the track to which the bElement belongs
-        @param timeline : the containing graphic timeline.
         """
-        Clutter.ScrollActor.__init__(self)
+        super(VideoPreviewer, self).__init__()
         PreviewGenerator.__init__(self, GES.TrackType.VIDEO)
         Zoomable.__init__(self)
         Loggable.__init__(self)
 
         # Variables related to the timeline objects
-        self.timeline = timeline
+        self.timeline = bElement.get_parent().get_timeline().ui
         self.bElement = bElement
         # Guard against malformed URIs
         self.uri = quote_uri(bElement.props.uri)
@@ -178,8 +174,8 @@ class VideoPreviewer(Clutter.ScrollActor, PreviewGenerator, Zoomable, Loggable):
         # Variables related to thumbnailing
         self.wishlist = []
         self._thumb_cb_id = None
-        self._allAnimated = False
         self._running = False
+
         # We should have one thumbnail per thumb_period.
         # TODO: get this from the user settings
         self.thumb_period = int(0.5 * Gst.SECOND)
@@ -194,20 +190,24 @@ class VideoPreviewer(Clutter.ScrollActor, PreviewGenerator, Zoomable, Loggable):
         self.interval = 500  # Every 0.5 second, reevaluate the situation
 
         # Connect signals and fire things up
-        self.timeline.connect("scrolled", self._scrollCb)
+        self.timeline.hadj.connect("value-changed", self._scrollCb)
         self.bElement.connect("notify::duration", self._durationChangedCb)
         self.bElement.connect("notify::in-point", self._inpointChangedCb)
         self.bElement.connect("notify::start", self._startChangedCb)
 
         self.pipeline = None
+        self._needs_redraw = True
         self.becomeControlled()
 
+        self.connect("notify::height-request", self._heightChangedCb)
+
     # Internal API
-    def _update(self, unused_msg_source=None):
-        if self.thumb_width:
-            self._addVisibleThumbnails()
-            if self.wishlist:
-                self.becomeControlled()
+    def _force_redraw(self, unused_msg_source=None):
+        self._needs_redraw = True
+        if self.wishlist:
+            self.becomeControlled()
+
+        return
 
     def _setupPipeline(self):
         """
@@ -289,6 +289,10 @@ class VideoPreviewer(Clutter.ScrollActor, PreviewGenerator, Zoomable, Loggable):
             # removed from the timeline after the PreviewGeneratorManager
             # started this job.
             return
+
+        # self.props.width_request = 
self.nsToPixel(self.bElement.get_asset().get_filesource_asset().props.duration)
+        # self.props.width = self.nsToPixel(self.bElement.get_asset().get_filesource_asset().props.duration)
+
         self.debug(
             'Now generating thumbnails for: %s', filename_from_uri(self.uri))
         query_success, duration = self.pipeline.query_duration(Gst.Format.TIME)
@@ -303,10 +307,11 @@ class VideoPreviewer(Clutter.ScrollActor, PreviewGenerator, Zoomable, Loggable):
         self._checkCPU()
 
         if self.bElement.props.in_point != 0:
-            position = Clutter.Point()
-            position.x = Zoomable.nsToPixel(self.bElement.props.in_point)
-            self.scroll_to_point(position)
-        self._addVisibleThumbnails()
+            adj = self.get_hadjustment()
+            adj.props.page_size = 1.0
+            adj.props.value = Zoomable.nsToPixel(self.bElement.props.in_point)
+
+        # self._addVisibleThumbnails()
         # Save periodically to avoid the common situation where the user exits
         # the app before a long clip has been fully thumbnailed.
         # Spread timeouts between 30-80 secs to avoid concurrent disk writes.
@@ -353,8 +358,7 @@ class VideoPreviewer(Clutter.ScrollActor, PreviewGenerator, Zoomable, Loggable):
             return False  # Stop the timer
 
     def _get_thumb_duration(self):
-        thumb_duration_tmp = Zoomable.pixelToNs(
-            self.thumb_width + THUMB_MARGIN_PX)
+        thumb_duration_tmp = Zoomable.pixelToNs(self.thumb_width + THUMB_MARGIN_PX)
         # quantize thumb length to thumb_period
         thumb_duration = quantize(thumb_duration_tmp, self.thumb_period)
         # make sure that the thumb duration after the quantization isn't
@@ -364,35 +368,40 @@ class VideoPreviewer(Clutter.ScrollActor, PreviewGenerator, Zoomable, Loggable):
         # make sure that we don't show thumbnails more often than thumb_period
         return max(thumb_duration, self.thumb_period)
 
-    def _addVisibleThumbnails(self):
+    def _remove_all_children(self):
+        for child in self.get_children():
+            self.remove(child)
+
+    def _addVisibleThumbnails(self, rect):
         """
         Get the thumbnails to be displayed in the currently visible clip portion
         """
-        self.remove_all_children()
-        old_thumbs = self.thumbs
+        if self.thumb_width is None:
+            return False
+
         self.thumbs = {}
         self.wishlist = []
 
         thumb_duration = self._get_thumb_duration()
-        element_left, element_right = self._get_visible_range()
+
+        element_left = self.pixelToNs(rect.x) + self.bElement.props.in_point
+        element_right = element_left + self.pixelToNs(rect.width)
         element_left = quantize(element_left, thumb_duration)
 
         for current_time in range(element_left, element_right, thumb_duration):
             thumb = Thumbnail(self.thumb_width, self.thumb_height)
-            thumb.set_position(
-                Zoomable.nsToPixel(current_time), THUMB_MARGIN_PX)
-            self.add_child(thumb)
+            self.put(thumb, Zoomable.nsToPixel(current_time) - self.nsToPixel(self.bElement.props.in_point),
+                     (self.props.height_request - self.thumb_height) / 2)
+
             self.thumbs[current_time] = thumb
             if current_time in self.thumb_cache:
                 gdkpixbuf = self.thumb_cache[current_time]
-                if self._allAnimated or current_time not in old_thumbs:
-                    self.thumbs[
-                        current_time].set_from_gdkpixbuf_animated(gdkpixbuf)
-                else:
-                    self.thumbs[current_time].set_from_gdkpixbuf(gdkpixbuf)
+                self.thumbs[current_time].set_from_pixbuf(gdkpixbuf)
+                self.thumbs[current_time].set_visible(True)
             else:
                 self.wishlist.append(current_time)
-        self._allAnimated = False
+
+        return True
 
     def _get_wish(self):
         """
@@ -413,7 +422,6 @@ class VideoPreviewer(Clutter.ScrollActor, PreviewGenerator, Zoomable, Loggable):
         # => Daniel: It is *not* nanosecond precise when we remove the videorate
         #            element from the pipeline
         # => thiblahute: not the case with mpegts
-        original_time = time
         if time in self.thumbs:
             thumb = self.thumbs[time]
         else:
@@ -421,81 +429,19 @@ class VideoPreviewer(Clutter.ScrollActor, PreviewGenerator, Zoomable, Loggable):
             index = binary_search(sorted_times, time)
             time = sorted_times[index]
             thumb = self.thumbs[time]
-            if thumb.has_pixel_data:
-                # If this happens, it means the precision of the thumbnail
-                # generator is not good enough for the current thumbnail
-                # interval.
-                # We could consider shifting the thumbnails, but seems like
-                # too much trouble for something which does not happen in
-                # practice. My last words..
-                self.fixme("Thumbnail is already set for time: %s, %s",
-                           format_ns(time), format_ns(original_time))
-                return
-        thumb.set_from_gdkpixbuf_animated(pixbuf)
+
+        thumb.set_from_pixbuf(pixbuf)
         if time in self.queue:
             self.queue.remove(time)
         self.thumb_cache[time] = pixbuf
+        self._needs_redraw = True
+        self.queue_draw()
 
     # Interface (Zoomable)
 
     def zoomChanged(self):
-        self.remove_all_children()
-        self._allAnimated = True
-        self._update()
-
-    def _get_visible_range(self):
-        # Shortcut/convenience variables:
-        start = self.bElement.props.start
-        in_point = self.bElement.props.in_point
-        duration = self.bElement.props.duration
-        timeline_left, timeline_right = self._get_visible_timeline_range()
-
-        element_left = timeline_left - start + in_point
-        element_left = max(element_left, in_point)
-        element_right = timeline_right - start + in_point
-        element_right = min(element_right, in_point + duration)
-
-        return (element_left, element_right)
-
-    # TODO: move to Timeline or to utils
-    def _get_visible_timeline_range(self):
-        # determine the visible left edge of the timeline
-        # TODO: isn't there some easier way to get the scroll point of the ScrollActor?
-        # timeline_left = -(self.timeline.get_transform().xw - self.timeline.props.x)
-        timeline_left = self.timeline.get_scroll_point().x
-
-        # determine the width of the pipeline
-        # by intersecting the timeline's and the stage's allocation
-        timeline_allocation = self.timeline.props.allocation
-        stage_allocation = self.timeline.get_stage().props.allocation
-
-        timeline_rect = Clutter.Rect()
-        timeline_rect.init(timeline_allocation.x1,
-                           timeline_allocation.y1,
-                           timeline_allocation.x2 - timeline_allocation.x1,
-                           timeline_allocation.y2 - timeline_allocation.y1)
-
-        stage_rect = Clutter.Rect()
-        stage_rect.init(stage_allocation.x1,
-                        stage_allocation.y1,
-                        stage_allocation.x2 - stage_allocation.x1,
-                        stage_allocation.y2 - stage_allocation.y1)
-
-        has_intersection, intersection = timeline_rect.intersection(stage_rect)
-
-        if not has_intersection:
-            return (0, 0)
-
-        timeline_width = intersection.size.width
-
-        # determine the visible right edge of the timeline
-        timeline_right = timeline_left + timeline_width
-
-        # convert to nanoseconds
-        time_left = Zoomable.pixelToNs(timeline_left)
-        time_right = Zoomable.pixelToNs(timeline_right)
-
-        return (time_left, time_right)
+        self._remove_all_children()
+        self._force_redraw()
 
     # Callbacks
 
@@ -519,23 +465,25 @@ class VideoPreviewer(Clutter.ScrollActor, PreviewGenerator, Zoomable, Loggable):
             return True
         return False
 
+    def _heightChangedCb(self, unused_widget, unused_value):
+        self._remove_all_children()
+        self._force_redraw()
+
     def _scrollCb(self, unused):
-        self._update()
+        self._force_redraw()
 
     def _startChangedCb(self, unused_bElement, unused_value):
-        self._update()
+        self._force_redraw()
 
     def _inpointChangedCb(self, unused_bElement, unused_value):
-        position = Clutter.Point()
-        position.x = Zoomable.nsToPixel(self.bElement.props.in_point)
-        self.scroll_to_point(position)
-        self._update()
+        self.get_hadjustment().set_value(Zoomable.nsToPixel(self.bElement.props.in_point))
+        self._force_redraw()
 
     def _durationChangedCb(self, unused_bElement, unused_value):
         new_duration = max(self.duration, self.bElement.props.duration)
         if new_duration > self.duration:
             self.duration = new_duration
-            self._update()
+            self._force_redraw()
 
     def startGeneration(self):
         self._setupPipeline()
@@ -556,38 +504,23 @@ class VideoPreviewer(Clutter.ScrollActor, PreviewGenerator, Zoomable, Loggable):
         self.stopGeneration()
         Zoomable.__del__(self)
 
+    def do_draw(self, context):
+        clipped_rect = Gdk.cairo_get_clip_rectangle(context)[1]
+        if self._needs_redraw:
+            if self._addVisibleThumbnails(clipped_rect):
+                self._needs_redraw = False
 
-class Thumbnail(Clutter.Actor):
+        Gtk.Layout.do_draw(self, context)
+
+
+class Thumbnail(Gtk.Image):
 
     def __init__(self, width, height):
-        Clutter.Actor.__init__(self)
-        image = Clutter.Image.new()
-        self.props.content = image
+        super(Thumbnail, self).__init__()
         self.width = width
         self.height = height
-        self.set_opacity(0)
-        self.set_size(self.width, self.height)
-        self.has_pixel_data = False
-
-    def set_from_gdkpixbuf(self, gdkpixbuf):
-        row_stride = gdkpixbuf.get_rowstride()
-        pixel_data = gdkpixbuf.get_pixels()
-        alpha = gdkpixbuf.get_has_alpha()
-        self.has_pixel_data = True
-        if alpha:
-            self.props.content.set_data(pixel_data, Cogl.PixelFormat.RGBA_8888,
-                                        self.width, self.height, row_stride)
-        else:
-            self.props.content.set_data(pixel_data, Cogl.PixelFormat.RGB_888,
-                                        self.width, self.height, row_stride)
-        self.set_opacity(255)
-
-    def set_from_gdkpixbuf_animated(self, gdkpixbuf):
-        self.save_easing_state()
-        self.set_easing_duration(750)
-        self.set_from_gdkpixbuf(gdkpixbuf)
-        self.restore_easing_state()
-
+        self.props.width_request = self.width
+        self.props.height_request = self.height
 
 caches = {}
 
@@ -756,7 +689,7 @@ class PipelineCpuAdapter(Loggable):
                     self.ready = False
 
 
-class AudioPreviewer(Clutter.Actor, PreviewGenerator, Zoomable, Loggable):
+class AudioPreviewer(Gtk.Layout, PreviewGenerator, Zoomable, Loggable):
 
     """
     Audio previewer based on the results from the "level" gstreamer element.
@@ -764,8 +697,8 @@ class AudioPreviewer(Clutter.Actor, PreviewGenerator, Zoomable, Loggable):
 
     __gsignals__ = PREVIEW_GENERATOR_SIGNALS
 
-    def __init__(self, bElement, timeline):
-        Clutter.Actor.__init__(self)
+    def __init__(self, bElement):
+        super(AudioPreviewer, self).__init__()
         PreviewGenerator.__init__(self, GES.TrackType.AUDIO)
         Zoomable.__init__(self)
         Loggable.__init__(self)
@@ -773,28 +706,27 @@ class AudioPreviewer(Clutter.Actor, PreviewGenerator, Zoomable, Loggable):
         self.pipeline = None
         self.discovered = False
         self.bElement = bElement
+        self.timeline = bElement.get_parent().get_timeline().ui
+
+        self.nSamples = self.bElement.get_parent().get_asset().get_duration() / 10000000
+        self._start = 0
+        self._end = 0
+        self._surface_x = 0
+
         # Guard against malformed URIs
         self._uri = quote_uri(bElement.props.uri)
-        self.timeline = timeline
-        self.actors = []
-
-        self.set_content_scaling_filters(
-            Clutter.ScalingFilter.NEAREST, Clutter.ScalingFilter.NEAREST)
-        self.canvas = Clutter.Canvas()
-        self.set_content(self.canvas)
-        self.width = 0
-        self._num_failures = 0
-        self.lastUpdate = None
 
-        self.current_geometry = (-1, -1)
+        self._num_failures = 0
 
         self.adapter = None
         self.surface = None
-        self.timeline.connect("scrolled", self._scrolledCb)
-        self.canvas.connect("draw", self._drawContentCb)
-        self.canvas.invalidate()
 
-        self._callback_id = 0
+        self._force_redraw = True
+
+        self.bElement.connect("notify::in-point", self._inpointChangedCb)
+
+    def _inpointChangedCb(self, unused_bElement, unused_value):
+        self._force_redraw = True
 
     def startLevelsDiscoveryWhenIdle(self):
         self.debug('Waiting for UI to become idle for: %s',
@@ -834,67 +766,10 @@ class AudioPreviewer(Clutter.Actor, PreviewGenerator, Zoomable, Loggable):
         self.becomeControlled()
 
     def set_size(self, unused_width, unused_height):
-        if self.discovered:
-            self._maybeUpdate()
+        self._force_redraw = True
 
     def zoomChanged(self):
-        self._maybeUpdate()
-
-    def _maybeUpdate(self):
-        if self.discovered:
-            self.log('Checking if the waveform for "%s" needs to be redrawn' %
-                     self._uri)
-            if self.lastUpdate is None or datetime.now() - self.lastUpdate > WAVEFORM_UPDATE_INTERVAL:
-                # Last update was long ago or never.
-                self._compute_geometry()
-            else:
-                if self._callback_id:
-                    GLib.source_remove(self._callback_id)
-                self._callback_id = GLib.timeout_add(
-                    500, self._compute_geometry)
-
-    def _compute_geometry(self):
-        self._callback_id = 0
-        self.log("Computing the clip's geometry for waveforms")
-        self.lastUpdate = datetime.now()
-        width_px = self.nsToPixel(self.bElement.props.duration)
-        if width_px <= 0:
-            return
-        start = self.timeline.get_scroll_point().x - self.nsToPixel(
-            self.bElement.props.start)
-        start = max(0, start)
-        # Take into account the timeline width, to avoid building
-        # huge clips when the timeline is zoomed in a lot.
-        timeline_width = self.timeline._container.get_allocation(
-        ).width - CONTROL_WIDTH
-        end = min(width_px,
-                  self.timeline.get_scroll_point().x + timeline_width + MARGIN)
-        self.width = int(end - start)
-        # We've been called at a moment where size was updated but not
-        # scroll_point.
-        if self.width < 0:
-            return
-
-        # We need to take duration and inpoint into account.
-        asset_duration = self.bElement.get_parent().get_asset().get_duration()
-        if self.bElement.props.duration:
-            nbSamples = self.nbSamples / \
-                (float(asset_duration) / float(self.bElement.props.duration))
-        else:
-            nbSamples = self.nbSamples
-        if self.bElement.props.in_point:
-            startOffsetSamples = self.nbSamples / \
-                (float(asset_duration) / float(self.bElement.props.in_point))
-        else:
-            startOffsetSamples = 0
-
-        self.start = int(start / width_px * nbSamples + startOffsetSamples)
-        self.end = int(end / width_px * nbSamples + startOffsetSamples)
-
-        self.canvas.set_size(self.width, EXPANDED_SIZE)
-        Clutter.Actor.set_size(self, self.width, EXPANDED_SIZE)
-        self.set_position(start, self.props.y)
-        self.canvas.invalidate()
+        self._force_redraw = True
 
     def _prepareSamples(self):
         # Let's go mono.
@@ -913,7 +788,6 @@ class AudioPreviewer(Clutter.Actor, PreviewGenerator, Zoomable, Loggable):
         self.discovered = True
         self.start = 0
         self.end = self.nbSamples
-        self._compute_geometry()
         if self.adapter:
             self.adapter.stop()
 
@@ -992,25 +866,38 @@ class AudioPreviewer(Clutter.Actor, PreviewGenerator, Zoomable, Loggable):
             return True
         return False
 
-    def _drawContentCb(self, unused_canvas, context, unused_surf_w, unused_surf_h):
-        context.set_operator(cairo.OPERATOR_CLEAR)
-        context.paint()
+    def _get_num_inpoint_samples(self):
+        if self.bElement.props.in_point:
+            asset_duration = self.bElement.get_asset().get_filesource_asset().get_duration()
+            return int(self.nbSamples / (float(asset_duration) / float(self.bElement.props.in_point)))
+
+        return 0
+
+    def do_draw(self, context):
         if not self.discovered:
             return
 
-        if self.surface:
-            self.surface.finish()
+        clipped_rect = Gdk.cairo_get_clip_rectangle(context)[1]
 
-        self.surface = renderer.fill_surface(
-            self.samples[self.start:self.end], int(self.width), int(EXPANDED_SIZE))
+        num_inpoint_samples = self._get_num_inpoint_samples()
+        start = int(self.pixelToNs(clipped_rect.x) / 10000000) + num_inpoint_samples
+        end = int((self.pixelToNs(clipped_rect.x) + self.pixelToNs(clipped_rect.width)) / 10000000) + 
num_inpoint_samples
+
+        if self._force_redraw or self._surface_x > clipped_rect.x or self._end < end:
+            self._start = start
+            end = int(min(self.nSamples, end + (self.pixelToNs(MARGIN) / 10000000)))
+            self._end = end
+            self._surface_x = clipped_rect.x
+            self.surface = renderer.fill_surface(self.samples[start:end],
+                                                 min(self.props.width_request - clipped_rect.x, 
clipped_rect.width + MARGIN),
+                                                 int(self.get_parent().get_allocation().height))
+
+            self._force_redraw = False
 
         context.set_operator(cairo.OPERATOR_OVER)
-        context.set_source_surface(self.surface, 0, 0)
+        context.set_source_surface(self.surface, self._surface_x, 0)
         context.paint()
 
-    def _scrolledCb(self, unused):
-        self._maybeUpdate()
-
     def startGeneration(self):
         self.pipeline.set_state(Gst.State.PLAYING)
         if self.adapter is not None:
@@ -1029,6 +916,5 @@ class AudioPreviewer(Clutter.Actor, PreviewGenerator, Zoomable, Loggable):
 
     def cleanup(self):
         self.stopGeneration()
-        self.canvas.disconnect_by_func(self._drawContentCb)
         self.timeline.disconnect_by_func(self._scrolledCb)
         Zoomable.__del__(self)
diff --git a/pitivi/timeline/ruler.py b/pitivi/timeline/ruler.py
index 9b8ca17..24df0b0 100644
--- a/pitivi/timeline/ruler.py
+++ b/pitivi/timeline/ruler.py
@@ -34,7 +34,7 @@ from gettext import gettext as _
 from pitivi.utils.pipeline import Seeker
 from pitivi.utils.timeline import Zoomable
 from pitivi.utils.loggable import Loggable
-from pitivi.utils.ui import NORMAL_FONT, PLAYHEAD_COLOR, PLAYHEAD_WIDTH, set_cairo_color, time_to_string, 
beautify_length
+from pitivi.utils.ui import NORMAL_FONT, PLAYHEAD_WIDTH, set_cairo_color, time_to_string, beautify_length
 
 
 HEIGHT = 25
@@ -113,7 +113,6 @@ class ScaleRuler(Gtk.DrawingArea, Zoomable, Loggable):
         self.connect('draw', self.drawCb)
         self.connect('configure-event', self.configureEventCb)
         self.callback_id = None
-        self.callback_id_scroll = None
         self.set_size_request(0, HEIGHT)
 
         style = self.get_style_context()
@@ -138,22 +137,12 @@ class ScaleRuler(Gtk.DrawingArea, Zoomable, Loggable):
 
     def _hadjValueChangedCb(self, unused_arg):
         self.pixbuf_offset = self.hadj.get_value()
-        if self.callback_id_scroll is not None:
-            GLib.source_remove(self.callback_id_scroll)
-        self.callback_id_scroll = GLib.timeout_add(100, self._maybeUpdate)
+        self.queue_draw()
 
 # Zoomable interface override
 
-    def _maybeUpdate(self):
-        self.queue_draw()
-        self.callback_id = None
-        self.callback_id_scroll = None
-        return False
-
     def zoomChanged(self):
-        if self.callback_id is not None:
-            GLib.source_remove(self.callback_id)
-        self.callback_id = GLib.timeout_add(100, self._maybeUpdate)
+        self.queue_draw()
 
 # Timeline position changed method
 
@@ -398,7 +387,7 @@ class ScaleRuler(Gtk.DrawingArea, Zoomable, Loggable):
         # without this the line appears blurry.
         xpos = self.nsToPixel(self.position) - self.pixbuf_offset + 0.5
         context.set_line_width(PLAYHEAD_WIDTH + 2)
-        set_cairo_color(context, PLAYHEAD_COLOR)
+        set_cairo_color(context, (255, 0, 0))
         context.move_to(xpos, 0)
         context.line_to(xpos, context.get_target().get_height())
         context.stroke()
diff --git a/pitivi/timeline/timeline.py b/pitivi/timeline/timeline.py
index d8ea45f..508eac0 100644
--- a/pitivi/timeline/timeline.py
+++ b/pitivi/timeline/timeline.py
@@ -24,28 +24,26 @@ import os
 
 from gettext import gettext as _
 
-from gi.repository import Clutter
 from gi.repository import GES
 from gi.repository import GLib
-from gi.repository import GObject
 from gi.repository import Gdk
 from gi.repository import Gst
 from gi.repository import Gtk
-from gi.repository import GtkClutter
 
+from pitivi.utils import ui
 from pitivi.autoaligner import AlignmentProgressDialog, AutoAligner
 from pitivi.configure import get_ui_dir
 from pitivi.dialogs.prefs import PreferencesDialog
 from pitivi.settings import GlobalSettings
-from pitivi.timeline.controls import ControlContainer
-from pitivi.timeline.elements import URISourceElement, TransitionElement, Ghostclip
 from pitivi.timeline.ruler import ScaleRuler
 from pitivi.utils.loggable import Loggable
-from pitivi.utils.pipeline import PipelineError
-from pitivi.utils.timeline import Zoomable, Selection, SELECT, TimelineError
-from pitivi.utils.ui import alter_style_class, EFFECT_TARGET_ENTRY, EXPANDED_SIZE, SPACING, PLAYHEAD_COLOR, 
PLAYHEAD_WIDTH, CONTROL_WIDTH
+from pitivi.utils.timeline import Zoomable, TimelineError
+from pitivi.utils.ui import alter_style_class, EXPANDED_SIZE, SPACING, CONTROL_WIDTH
 from pitivi.utils.widgets import ZoomBox
 
+from pitivi.timeline.elements import Clip
+from pitivi.utils import timeline as timelineUtils
+from pitivi.timeline.layer import SpacedSeparator, Layer, LayerControls
 
 GlobalSettings.addConfigOption('edgeSnapDeadband',
                                section="user-interface",
@@ -73,12 +71,6 @@ PreferencesDialog.addNumericPreference('imageClipLength',
                                            "Default clip length (in miliseconds) of images when inserting on 
the timeline."),
                                        lower=1)
 
-# Colors
-TIMELINE_BACKGROUND_COLOR = Clutter.Color.new(31, 30, 33, 255)
-SELECTION_MARQUEE_COLOR = Clutter.Color.new(100, 100, 100, 200)
-SNAPPING_INDICATOR_COLOR = Clutter.Color.new(50, 150, 200, 200)
-
-
 """
 Convention throughout this file:
 Every GES element which name could be mistaken with a UI element
@@ -86,61 +78,246 @@ is prefixed with a little b, example : bTimeline
 """
 
 
-class TimelineStage(Clutter.ScrollActor, Zoomable, Loggable):
+class VerticalBar(Gtk.DrawingArea, Loggable):
+    """
+    A simple vertical bar to be drawn on top of the timeline
+    """
+    __gtype_name__ = "PitiviVerticalBar"
+
+    def __init__(self, css_class):
+        super(VerticalBar, self).__init__()
+        Loggable.__init__(self)
+        self.get_style_context().add_class(css_class)
 
+    def do_get_preferred_width(self):
+        self.debug("Getting prefered height")
+        return ui.PLAYHEAD_WIDTH, ui.PLAYHEAD_WIDTH
+
+    def do_get_preferred_height(self):
+        self.debug("Getting prefered height")
+        return self.get_parent().get_allocated_height(), self.get_parent().get_allocated_height()
+
+
+class Marquee(Gtk.Box, Loggable):
     """
-    The timeline view showing the clips.
+    Marquee widget representing a selection area inside the timeline
+    it should be drawn on top of the timeline layout.
+
+    It provides an API that makes it easy to update its value directly
+    from Gdk.Event
     """
 
-    __gsignals__ = {
-        'scrolled': (GObject.SIGNAL_RUN_FIRST, None, ())
-    }
+    __gtype_name__ = "PitiviMarquee"
 
-    def __init__(self, container, settings):
-        Clutter.ScrollActor.__init__(self)
-        Zoomable.__init__(self)
+    def __init__(self, timeline):
+        """
+        @timeline: The #Timeline on which the marquee will
+                   be used
+        """
+        super(Marquee, self).__init__()
+        Loggable.__init__(self)
+
+        self._timeline = timeline
+        self.start_x = None
+        self.start_y = None
+        self.set_visible(False)
+
+        self.get_style_context().add_class("Marquee")
+
+    def hide(self):
+        self.start_x = None
+        self.start_y = None
+        self.props.height_request = -1
+        self.props.width_request = -1
+        self.set_visible(False)
+
+    def setStartPosition(self, event):
+        event_widget = self._timeline.get_event_widget(event)
+        x, y = event_widget.translate_coordinates(self._timeline, event.x, event.y)
+
+        self.start_x, self.start_y = self._timeline.adjustCoords(x=x, y=y)
+
+    def move(self, event):
+        event_widget = self._timeline.get_event_widget(event)
+
+        x, y = self._timeline.adjustCoords(coords=event_widget.translate_coordinates(self._timeline, 
event.x, event.y))
+
+        start_x = min(x, self.start_x)
+        start_y = min(y, self.start_y)
+
+        self.get_parent().move(self, start_x, start_y)
+        self.props.width_request = abs(self.start_x - x)
+        self.props.height_request = abs(self.start_y - y)
+        self.set_visible(True)
+
+    def findSelected(self):
+        x, y = self._timeline.layout.child_get(self, "x", "y")
+        res = []
+
+        w = self.props.width_request
+        for layer in self._timeline.bTimeline.get_layers():
+            intersects, unused_rect = Gdk.rectangle_intersect(layer.ui.get_allocation(), 
self.get_allocation())
+
+            if not intersects:
+                continue
+
+            for clip in layer.get_clips():
+                if self.contains(clip, x, w):
+                    toplevel = clip.get_toplevel_parent()
+                    if isinstance(toplevel, GES.Group) and toplevel != self._timeline.current_group:
+                        res.extend([c for c in clip.get_toplevel_parent().get_children(True)
+                                    if isinstance(c, GES.Clip)])
+                    else:
+                        res.append(clip)
+
+        self.debug("Selected clips: %s" % res)
+
+        return tuple(set(res))
+
+    def contains(self, clip, marquee_start, marquee_width):
+        if clip.ui is None:
+            return False
+
+        child_start = clip.ui.get_parent().child_get(clip.ui, "x")[0]
+        child_end = child_start + clip.ui.get_allocation().width
+
+        marquee_end = marquee_start + marquee_width
+
+        if child_start <= marquee_start <= child_end:
+            return True
+
+        if child_start <= marquee_end <= child_end:
+            return True
+
+        if marquee_start <= child_start and marquee_end >= child_end:
+            return True
+
+        return False
+
+
+class Timeline(Gtk.EventBox, timelineUtils.Zoomable, Loggable):
+    """
+    The main timeline Widget, it contains the representation of the GESTimeline
+    without any extra widgets.
+    """
+
+    __gtype_name__ = "PitiviTimeline"
+
+    def __init__(self, container, app):
+        super(Timeline, self).__init__()
+
+        timelineUtils.Zoomable.__init__(self)
         Loggable.__init__(self)
+
+        self._main_hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
+        self.add(self._main_hbox)
+
+        self.layout = Gtk.Layout()
+        self.hadj = self.layout.get_hadjustment()
+        self.vadj = self.layout.get_vadjustment()
+
+        self.__layers_controls_vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
+
+        # Stuff the layers controls in a viewport so it can be scrolled.
+        viewport = Gtk.Viewport(vadjustment=self.vadj)
+        viewport.add(self.__layers_controls_vbox)
+
+        # Make sure the viewport has no border or other decorations.
+        viewport_style = viewport.get_style_context()
+        for css_class in viewport_style.list_classes():
+            viewport_style.remove_class(css_class)
+        self._main_hbox.pack_start(viewport, False, False, 0)
+
+        self._main_hbox.pack_start(self.layout, False, True, 0)
+        self.get_style_context().add_class("Timeline")
+
+        self.__layers_vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
+        self.__layers_vbox.props.width_request = self.get_allocated_width()
+        self.__layers_vbox.props.height_request = self.get_allocated_height()
+        self.layout.put(self.__layers_vbox, 0, 0)
+
         self.bTimeline = None
+        self.__last_position = 0
+        self.selection = timelineUtils.Selection()
+
+        self._layers = []
+        self.parent = container
+        self.app = app
+        self.__snap_position = 0
         self._project = None
+
+        self.current_group = None
         self.createSelectionGroup()
 
-        self._container = container
-        self.allowSeek = True
-        self._settings = settings
-        self.elements = []
-        self.ghostClips = []
-        self.selection = Selection()
-        self._scroll_point = Clutter.Point()
-        self.lastPosition = 0  # Saved for redrawing when paused
-        self.mouse = self._peekMouse()
-
-        # The markers are used for placing clips at the right depth.
-        # The first marker added as a child is the furthest and
-        # the latest added marker is the closest to the viewer.
-
-        # All the audio, video, image, title clips are placed above this
-        # marker.
-        self._clips_marker = Clutter.Actor()
-        self.add_child(self._clips_marker)
-        # All the transition clips are placed above this marker.
-        self._transitions_marker = Clutter.Actor()
-        self.add_child(self._transitions_marker)
-
-        # Add the playhead later so it appears on top of all the clips.
-        self.playhead = self._createPlayhead()
-        self.add_child(self.playhead)
-
-        self._snap_indicator = self._createSnapIndicator()
-        self.add_child(self._snap_indicator)
-
-        # Add the drag and drop marquee so it appears on top of the playhead.
-        self.marquee = self._setUpDragAndDrop()
-        self.add_child(self.marquee)
-        self.drawMarquee = False
+        self.__playhead = VerticalBar("PlayHead")
+        self.__playhead.show()
+        self.layout.put(self.__playhead, self.nsToPixel(self.__last_position), 0)
 
-    # Public API
+        self.__snap_bar = VerticalBar("SnapBar")
+        self.layout.put(self.__snap_bar, 0, 0)
+
+        self.__allow_seek = True
+
+        self.__setupTimelineEdition()
+        self.__setUpDragAndDrop()
+        self.__setupSelectionMarquee()
+
+        self.__button_pressed = False
+
+        # Setup our Gtk.Widget properties
+        self.add_events(Gdk.EventType.BUTTON_PRESS | Gdk.EventType.BUTTON_RELEASE)
+        self.connect("scroll-event", self.__scrollEventCb)
+        self.connect("button-press-event", self.__buttonPressEventCb)
+        self.connect("button-release-event", self.__buttonReleaseEventCb)
+        self.connect("motion-notify-event", self.__motionNotifyEventCb)
+        self.connect("drag-motion", self.__dragMotionCb)
+        self.connect("drag-leave", self.__dragLeaveCb)
+        self.connect("drag-drop", self.__dragDropCb)
+        self.connect("drag-data-received", self.__dragDataReceivedCb)
+
+        self.props.expand = True
+        self.get_accessible().set_name("timeline canvas")
+        self.__fake_event_widget = None
+
+    def sendFakeEvent(self, event, event_widget):
+        # Member usefull for testsing
+        self.__fake_event_widget = event_widget
+
+        self.info("Faking %s" % event)
+        if event.type == Gdk.EventType.BUTTON_PRESS:
+            self.__buttonPressEventCb(self, event)
+        elif event.type == Gdk.EventType.BUTTON_RELEASE:
+            self.__buttonReleaseEventCb(self, event)
+        elif event.type == Gdk.EventType.MOTION_NOTIFY:
+            self.__motionNotifyEventCb(self, event)
+
+        self.__fake_event_widget = None
+
+    def get_event_widget(self, event):
+        if self.__fake_event_widget:
+            return self.__fake_event_widget
+
+        return Gtk.get_event_widget(event)
+
+    def __get_event_widget(self, event):
+        if self.__fake_event_widget:
+            return self.__fake_event_widget
+
+        return Gtk.get_event_widget(event)
+
+    @property
+    def allowSeek(self):
+        return self.__allow_seek
+
+    @allowSeek.setter
+    def allowSeek(self, value):
+        self.debug("Setting AllowSeek to %s" % value)
+        self.__allow_seek = value
 
     def createSelectionGroup(self):
+        if self.current_group:
+            GES.Container.ungroup(self.current_group, False)
+
         self.current_group = GES.Group()
         self.current_group.props.serialize = False
 
@@ -156,506 +333,606 @@ class TimelineStage(Clutter.ScrollActor, Zoomable, Loggable):
             bTimeline = None
 
         if self.bTimeline is not None:
-            self.bTimeline.disconnect_by_func(self._trackAddedCb)
-            self.bTimeline.disconnect_by_func(self._trackRemovedCb)
+            self.bTimeline.disconnect_by_func(self._durationChangedCb)
             self.bTimeline.disconnect_by_func(self._layerAddedCb)
             self.bTimeline.disconnect_by_func(self._layerRemovedCb)
             self.bTimeline.disconnect_by_func(self._snapCb)
             self.bTimeline.disconnect_by_func(self._snapEndedCb)
-            for track in self.bTimeline.get_tracks():
-                self._trackRemovedCb(self.bTimeline, track)
             for layer in self.bTimeline.get_layers():
                 self._layerRemovedCb(self.bTimeline, layer)
 
+            self.bTimeline.ui = None
+
         self.bTimeline = bTimeline
 
         if bTimeline is None:
             return
 
-        for track in bTimeline.get_tracks():
-            self._connectTrack(track)
         for layer in bTimeline.get_layers():
-            self._add_layer(layer)
+            self._addLayer(layer)
 
-        self.bTimeline.connect("track-added", self._trackAddedCb)
-        self.bTimeline.connect("track-removed", self._trackRemovedCb)
+        self.bTimeline.connect("notify::duration", self._durationChangedCb)
         self.bTimeline.connect("layer-added", self._layerAddedCb)
         self.bTimeline.connect("layer-removed", self._layerRemovedCb)
         self.bTimeline.connect("snapping-started", self._snapCb)
         self.bTimeline.connect("snapping-ended", self._snapEndedCb)
+        self.bTimeline.ui = self
 
-        self.zoomChanged()
+        self.queue_draw()
 
-    def findBrother(self, element):
-        """
-        Iterate over ui_elements to get the URI source with the same parent clip
-        @param element: the ui_element for which we want to find the sibling.
-        """
-        father = element.get_parent()
-        for elem in self.elements:
-            if elem.bElement.get_parent() == father and elem.bElement != element:
-                return elem
-        return None
+    def _durationChangedCb(self, bTimeline, pspec):
+        self.queue_draw()
 
-    def createLayerForGhostClip(self, ghostclip):
-        """
-        Creates a layer and moves subsequent layers down, if any.
+    def scrollToPlayhead(self,):
+        if self.__button_pressed or self.parent.ruler.pressed:
+            self.__button_pressed = False
+            return
 
-        @param ghostclip: the ghostclip that was dropped, needing a new layer.
-        @type ghostclip: L{Ghostclip}
-        @rtype: L{GES.Layer}
-        """
-        layers = self.bTimeline.get_layers()
-        if ghostclip.priority < len(layers):
-            for layer in layers:
-                if layer.get_priority() >= ghostclip.priority:
-                    layer.props.priority += 1
+        self.hadj.set_value(self.nsToPixel(self.__last_position) -
+                            (self.layout.get_allocation().width / 2))
 
-        layer = self.bTimeline.append_layer()
-        layer.props.priority = ghostclip.priority
-        self._project.pipeline.commit_timeline()
-        self._container.controls._reorderLayerActors()
-        return layer
-
-    # Drag and drop from the medialibrary, handled by "ghost" (temporary) clips.
-    # We create those when drag data is received, reset them after conversion.
-    # This avoids bugs when dragging in and out of the timeline
-
-    def resetGhostClips(self):
-        self.ghostClips = []
-
-    def addGhostClip(self, asset, unused_x, unused_y):
-        ghostVideo = None
-        if asset.get_supported_formats() & GES.TrackType.VIDEO:
-            ghostVideo = self._createGhostclip(GES.TrackType.VIDEO, asset)
-        ghostAudio = None
-        if asset.get_supported_formats() & GES.TrackType.AUDIO:
-            ghostAudio = self._createGhostclip(GES.TrackType.AUDIO, asset)
-        self.ghostClips.append([ghostVideo, ghostAudio])
-
-    def updateGhostClips(self, x, y):
-        """
-        This is called for each drag-motion.
-        """
-        priority = int(y / (EXPANDED_SIZE + SPACING))
-        for ghostCouple in self.ghostClips:
-            for ghostclip in ghostCouple:
-                if ghostclip:
-                    ghostclip.update(priority, y, False)
-                    if x >= 0:
-                        ghostclip.props.x = x
-                        self._updateSize(ghostclip)
-
-    def convertGhostClips(self):
+    def _positionCb(self, unused_pipeline, position):
+        if self.__last_position == position:
+            return
+
+        self.__last_position = position
+        self.scrollToPlayhead()
+        self.layout.move(self.__playhead, max(0, self.nsToPixel(self.__last_position)), 0)
+
+    # snapping indicator
+    def _snapCb(self, unused_timeline, unused_obj1, unused_obj2, position):
         """
-        This is called at drag-drop
+        Display or hide a snapping indicator line
         """
-        placement = 0
-        layer = None
-        for ghostVideo, ghostAudio in self.ghostClips:
-            ghostclip = ghostVideo or ghostAudio
+        self.layout.move(self.__snap_bar, self.nsToPixel(position), 0)
+        self.__snap_bar.show()
+        self.__snap_position = position
+        self.debug("-> Snap START!")
 
-            if layer is None:
-                layer = self._getLayerForGhostClip(ghostclip)
+    def hideSnapBar(self):
+        self.debug("-> Force hiding snap bar")
+        self.__snap_position = 0
+        self.__snap_bar.hide()
 
-            if ghostclip.asset.is_image():
-                clip_duration = self._settings.imageClipLength * \
-                    Gst.SECOND / 1000.0
-            else:
-                clip_duration = ghostclip.asset.get_duration()
-
-            if not placement:
-                placement = Zoomable.pixelToNs(ghostclip.props.x)
-            self._container.app.action_log.begin("add clip")
-            layer.add_asset(ghostclip.asset,
-                            placement,
-                            0,
-                            clip_duration,
-                            ghostclip.asset.get_supported_formats())
-            self._container.app.action_log.commit()
-            placement += clip_duration
-        self._project.pipeline.commit_timeline()
+    def _snapEndedCb(self, *unused_args):
+        self.hideSnapBar()
+
+    # Gtk.Widget virtual methods implementation
+    def do_get_preferred_height(self):
+        natural_height = max(1, len(self._layers)) * (ui.LAYER_HEIGHT + 20)
+
+        return ui.LAYER_HEIGHT, natural_height
+
+    def do_draw(self, cr):
+        if self.bTimeline:
+            width = self._computeTheoricalWidth()
+            if self.draggingElement:
+                width = max(width, self.layout.props.width)
+
+            self.layout.set_size(width, len(self.bTimeline.get_layers()) * 200)
+
+        Gtk.EventBox.do_draw(self, cr)
+
+        self.__drawSnapIndicator(cr)
+        self.__drawPlayHead(cr)
 
-    def _getLayerForGhostClip(self, ghostclip):
+        self.layout.propagate_draw(self.__marquee, cr)
+
+    def __drawSnapIndicator(self, cr):
+        if self.__snap_position > 0:
+            self.__snap_bar.props.height_request = self.layout.props.height
+            self.__snap_bar.props.width_request = ui.SNAPBAR_WIDTH
+
+            self.layout.propagate_draw(self.__snap_bar, cr)
+        else:
+            self.__snap_bar.hide()
+
+    def __drawPlayHead(self, cr):
+        self.__playhead.props.height_request = self.layout.props.height
+        self.__playhead.props.width_request = ui.PLAYHEAD_WIDTH
+
+        self.layout.propagate_draw(self.__playhead, cr)
+
+    # ------------- #
+    # util methods  #
+    # ------------- #
+    def _computeTheoricalWidth(self):
+        if self.bTimeline is None:
+            return 100
+
+        return self.nsToPixel(self.bTimeline.props.duration)
+
+    def _getParentOfType(self, widget, _type):
         """
-        Return the layer on which the specified ghostclip should be added.
+        Get a clip from a child widget, if the widget is a child of the clip
         """
-        if ghostclip.shouldCreateLayer:
-            return self.createLayerForGhostClip(ghostclip)
-        for layer in self.bTimeline.get_layers():
-            if layer.get_priority() == ghostclip.priority:
-                return layer
-        raise TimelineError()
-
-    def removeGhostClips(self):
+        if isinstance(widget, _type):
+            return widget
+
+        parent = widget.get_parent()
+        while parent is not None and parent != self:
+            parent = parent.get_parent()
+
+            if isinstance(parent, _type):
+                return parent
+        return None
+
+    def adjustCoords(self, coords=None, x=None, y=None):
         """
-        This is called at drag-leave. We don't empty the list on purpose.
+        Adjust coordinates passed as parametter that are raw
+        coordinates from the whole timeline into sensible
+        coordinates inside the visible area of the timeline.
         """
-        for ghostCouple in self.ghostClips:
-            for ghostclip in ghostCouple:
-                if ghostclip and ghostclip.get_parent():
-                    self.remove_child(ghostclip)
-        self._project.pipeline.commit_timeline()
+        if coords:
+            x = coords[0]
+            y = coords[1]
 
-    def getActorUnderPointer(self):
-        return self.mouse.get_pointer_actor()
+        if x is not None:
+            x += self.hadj.props.value
+            x -= ui.CONTROL_WIDTH
 
-    # Internal API
+        if y is not None:
+            y += self.vadj.props.value
+
+            if x is None:
+                return y
+        else:
+            return x
+
+        return x, y
 
-    def _elementIsInLasso(self, element, x1, y1, x2, y2):
-        xE1 = element.props.x
-        xE2 = element.props.x + element.props.width
-        yE1 = element.props.y
-        yE2 = element.props.y + element.props.height
+    # Gtk events management
+    def __scrollEventCb(self, unused_widget, event):
+        res, delta_x, delta_y = event.get_scroll_deltas()
+        if not res:
+            return False
 
-        return self._segmentsOverlap((x1, x2), (xE1, xE2)) and self._segmentsOverlap((y1, y2), (yE1, yE2))
+        event_widget = self.get_event_widget(event)
+        x, y = event_widget.translate_coordinates(self, event.x, event.y)
+        if event.get_state() & Gdk.ModifierType.SHIFT_MASK:
+            if delta_y > 0:
+                self.parent.scroll_down()
+            elif delta_y < 0:
+                self.parent.scroll_up()
+        elif event.get_state() & Gdk.ModifierType.CONTROL_MASK:
+            if delta_y > 0:
+                timelineUtils.Zoomable.zoomOut()
+                self.queue_draw()
+            elif delta_y < 0:
+                timelineUtils.Zoomable.zoomIn()
+                self.queue_draw()
+        return False
 
-    def _segmentsOverlap(self, a, b):
-        x = max(a[0], b[0])
-        y = min(a[1], b[1])
-        return x < y
+    def __buttonPressEventCb(self, unused_widget, event):
+        event_widget = self.get_event_widget(event)
 
-    def _translateToTimelineContext(self, event):
-        event.x -= CONTROL_WIDTH
-        event.x += self._scroll_point.x
-        event.y += self._scroll_point.y
+        self.debug("PRESSED %s" % event)
+        self.__button_pressed = True
 
-        delta_x = event.x - self.dragBeginStartX
-        delta_y = event.y - self.dragBeginStartY
+        res, button = event.get_button()
+        if res and button == 1:
+            self.draggingElement = self._getParentOfType(event_widget, Clip)
+            self.debug("Dragging element is %s" % self.draggingElement)
+            if self.draggingElement is not None:
+                self.__drag_start_x = event.x
 
-        newX = self.dragBeginStartX
-        newY = self.dragBeginStartY
+            else:
+                self.__marquee.setStartPosition(event)
 
-        # This is needed when you start to click and go left or up.
+        return False
 
-        if delta_x < 0:
-            newX = event.x
-            delta_x = abs(delta_x)
+    def __buttonReleaseEventCb(self, unused_widget, event):
+        if self.draggingElement:
+            self.dragEnd()
+        else:
+            self._selectUnderMarquee()
 
-        if delta_y < 0:
-            newY = event.y
-            delta_y = abs(delta_y)
+        if self.allowSeek:
+            event_widget = self.get_event_widget(event)
+            x, unused_y = event_widget.translate_coordinates(self, event.x, event.y)
+            x -= CONTROL_WIDTH
+            x += self.hadj.get_value()
 
-        return newX, newY, delta_x, delta_y
+            position = self.pixelToNs(x)
+            self._project.seeker.seek(position)
 
-    def _setUpDragAndDrop(self):
-        self.set_reactive(True)
+        self.allowSeek = True
+        self._snapEndedCb()
 
-        self._container.stage.connect("button-press-event", self._dragBeginCb)
-        self._container.stage.connect("motion-event", self._dragProgressCb)
-        self._container.stage.connect("button-release-event", self._dragEndCb)
-        self._container.gui.connect("button-release-event", self._dragEndCb)
+        return False
 
-        marquee = Clutter.Actor()
-        marquee.set_background_color(SELECTION_MARQUEE_COLOR)
-        marquee.hide()
-        return marquee
+    def __motionNotifyEventCb(self, unused_widget, event):
+        if self.draggingElement:
+            state = event.get_state()
 
-    @staticmethod
-    def _peekMouse():
-        manager = Clutter.DeviceManager.get_default()
-        for device in manager.peek_devices():
-            if device.props.device_type == Clutter.InputDeviceType.POINTER_DEVICE and device.props.enabled 
is True:
-                return device
+            if isinstance(state, tuple):
+                state = state[1]
 
-    def _createGhostclip(self, trackType, asset):
-        ghostclip = Ghostclip(trackType)
-        ghostclip.asset = asset
-        ghostclip.setNbrLayers(len(self.bTimeline.get_layers()))
+            if not state & Gdk.ModifierType.BUTTON1_MASK:
+                self.dragEnd()
+                return False
 
-        if asset.is_image():
-            clip_duration = self._settings.imageClipLength * \
-                Gst.SECOND / 1000.0
-        else:
-            clip_duration = asset.get_duration()
+            self.__dragUpdate(self.get_event_widget(event), event.x, event.y)
+            self.got_dragged = True
+        elif self.__marquee.start_x:
+            self.__marquee.move(event)
 
-        ghostclip.setWidth(Zoomable.nsToPixel(clip_duration))
-        self.add_child(ghostclip)
-        return ghostclip
+        return False
 
-    def _connectTrack(self, track):
-        for trackelement in track.get_elements():
-            self._trackElementAddedCb(track, trackelement)
-        track.connect("track-element-added", self._trackElementAddedCb)
-        track.connect("track-element-removed", self._trackElementRemovedCb)
+    def _selectUnderMarquee(self):
+        if self.__marquee.props.width_request > 0:
+            clips = self.__marquee.findSelected()
 
-    def _disconnectTrack(self, track):
-        track.disconnect_by_func(self._trackElementAddedCb)
-        track.disconnect_by_func(self._trackElementRemovedCb)
+            if clips:
+                self.createSelectionGroup()
 
-    def _positionCb(self, unused_pipeline, position):
-        self._movePlayhead(position)
-        self._container._scrollToPlayhead()
-        self.lastPosition = position
-
-    def _updatePlayHead(self):
-        height = len(self.bTimeline.get_layers()) * \
-            (EXPANDED_SIZE + SPACING) * 2
-        self.playhead.set_size(PLAYHEAD_WIDTH, height)
-        self._movePlayhead(self.lastPosition)
-
-    def _movePlayhead(self, position):
-        self.playhead.props.x = self.nsToPixel(position)
-
-    @staticmethod
-    def _createPlayhead():
-        playhead = Clutter.Actor()
-        playhead.set_background_color(PLAYHEAD_COLOR)
-        playhead.set_size(0, 0)
-        playhead.set_position(0, 0)
-        playhead.set_easing_duration(0)
-        return playhead
-
-    @staticmethod
-    def _createSnapIndicator():
-        indicator = Clutter.Actor()
-        indicator.set_background_color(SNAPPING_INDICATOR_COLOR)
-        indicator.props.visible = False
-        indicator.props.width = 3
-        indicator.props.y = 0
-        return indicator
-
-    def _addTimelineElement(self, track, bElement):
-        if isinstance(bElement, GES.Effect):
-            return
+                for clip in clips:
+                    self.current_group.add(clip.get_toplevel_parent())
 
-        if isinstance(bElement, GES.Transition):
-            element = TransitionElement(bElement, self)
-            marker = self._transitions_marker
-        elif isinstance(bElement, GES.Source):
-            element = URISourceElement(bElement, self)
-            marker = self._clips_marker
+                self.selection.setSelection(clips, timelineUtils.SELECT)
+            else:
+                self.selection.setSelection([], timelineUtils.SELECT)
         else:
-            self.warning("Unknown element: %s", bElement)
-            return
+            only_transitions = not bool([selected for selected in self.selection.selected
+                                         if not isinstance(selected, GES.TransitionClip)])
+            if not only_transitions:
+                self.selection.setSelection([], timelineUtils.SELECT)
 
-        bElement.connect("notify::start", self._elementStartChangedCb, element)
-        bElement.connect(
-            "notify::duration", self._elementDurationChangedCb, element)
-        bElement.connect(
-            "notify::in-point", self._elementInPointChangedCb, element)
-        bElement.connect(
-            "notify::priority", self._elementPriorityChangedCb, element)
+        self.__marquee.hide()
 
-        self.elements.append(element)
+    def updatePosition(self):
+        for layer in self._layers:
+            layer.updatePosition()
 
-        self._setElementX(element, ease=True)
-        self._setElementY(element)
+        self.queue_draw()
 
-        self.insert_child_above(element, marker)
+    def __setupSelectionMarquee(self):
+        self.__marquee = Marquee(self)
+        self.layout.put(self.__marquee, 0, 0)
 
-    def _removeTimelineElement(self, unused_track, bElement):
-        if isinstance(bElement, GES.Effect):
-            return
-        bElement.disconnect_by_func(self._elementStartChangedCb)
-        bElement.disconnect_by_func(self._elementDurationChangedCb)
-        bElement.disconnect_by_func(self._elementInPointChangedCb)
-        bElement.disconnect_by_func(self._elementPriorityChangedCb)
-
-        element = self._getElement(bElement)
-        if not element:
-            raise TimelineError("Missing element for: " + bElement)
-        element.cleanup()
-        self.elements.remove(element)
-        self.remove_child(element)
-        self.selection.setSelection(set([]), SELECT)
-
-    def _getElement(self, bElement):
-        for element in self.elements:
-            if element.bElement == bElement:
-                return element
-        return None
+    # drag and drop
+    def __setUpDragAndDrop(self):
+        self.got_dragged = False
+        self.dropHighlight = False
+        self.dropOccured = False
+        self.dropDataReady = False
+        self.dropData = None
+        self._createdClips = False
+        self.isDraggedClip = False
+        self._lastClipOnLeave = None
 
-    def _setElementX(self, element, ease=False):
-        if ease:
-            element.save_easing_state()
-            element.set_easing_duration(600)
-        element.props.x = self.nsToPixel(element.bElement.get_start())
-        if ease:
-            element.restore_easing_state()
-
-    # FIXME, change that when we have retractable layers
-    def _setElementY(self, element):
-        bElement = element.bElement
-        track_type = bElement.get_track_type()
-
-        y = 0
-        if track_type == GES.TrackType.AUDIO:
-            y = len(self.bTimeline.get_layers()) * (EXPANDED_SIZE + SPACING)
-        y += bElement.get_parent().get_layer().get_priority() * \
-            (EXPANDED_SIZE + SPACING) + SPACING
-
-        element.save_easing_state()
-        element.props.y = y
-        element.restore_easing_state()
-
-    def _updateSize(self, ghostclip=None):
-        self.save_easing_state()
-        self.set_easing_duration(0)
-        self.props.width = self.nsToPixel(
-            self.bTimeline.get_duration()) + CONTROL_WIDTH
-        if ghostclip is not None:
-            ghostEnd = ghostclip.props.x + \
-                ghostclip.props.width + CONTROL_WIDTH
-            self.props.width = max(ghostEnd, self.props.width)
-        self.props.height = (len(self.bTimeline.get_layers()) + 1) * \
-            (EXPANDED_SIZE + SPACING) * 2 + SPACING
-        self.restore_easing_state()
-        self._container.vadj.props.upper = self.props.height
-        self._container.updateHScrollAdjustments()
-
-    def _redraw(self):
-        self._updateSize()
-
-        self.save_easing_state()
-        for element in self.elements:
-            self._setElementX(element)
-            self._setElementY(element)
-        self.restore_easing_state()
-
-        self._updatePlayHead()
-
-    def _remove_layer(self, layer):
-        self._container.controls.removeLayerControl(layer)
-        self._redraw()
-
-    def _add_layer(self, layer):
-        self._redraw()
-        self._container.controls.addLayerControl(layer)
-
-    # Interface overrides
-
-    # Zoomable Override
+        # To be able to receive effects dragged on clips.
+        self.drag_dest_set(0, [ui.EFFECT_TARGET_ENTRY], Gdk.DragAction.COPY)
+        # To be able to receive assets dragged from the media library.
+        self.drag_dest_add_uri_targets()
 
-    def zoomChanged(self):
-        self._redraw()
+    def createClip(self, x, y):
+        if self.isDraggedClip and self._createdClips is False:
 
-    # Clutter Override
+            # From the media library
+            placement = 0
+            for uri in self.dropData:
+                asset = self.app.gui.medialibrary.getAssetForUri(uri)
+                if asset is None:
+                    break
 
-    # TODO: remove self._scroll_point and get_scroll_point as soon as the Clutter API
-    # offers a way to query a ScrollActor for its current scroll point
-    def scroll_to_point(self, point):
-        Clutter.ScrollActor.scroll_to_point(self, point)
-        self._scroll_point = point.copy()
-        self.emit("scrolled")
+                if asset.is_image():
+                    clip_duration = self._settings.imageClipLength * \
+                        Gst.SECOND / 1000.0
+                else:
+                    clip_duration = asset.get_duration()
 
-    def get_scroll_point(self):
-        return self._scroll_point
+                layer, on_sep = self.__getLayerAt(y)
+                if not placement:
+                    placement = self.pixelToNs(x)
 
-    # Callbacks
+                self.app.action_log.begin("add clip")
+                bClip = layer.add_asset(asset,
+                                        placement,
+                                        0,
+                                        clip_duration,
+                                        asset.get_supported_formats())
+                self.app.action_log.commit()
 
-    def _dragBeginCb(self, unused_actor, event):
-        self.drawMarquee = self.getActorUnderPointer() == self
-        if not self.drawMarquee:
-            return
+                self.draggingElement = bClip.ui
+                self._createdClips = True
 
-        if self.current_group:
-            GES.Container.ungroup(self.current_group, False)
-            self.createSelectionGroup()
+                return True
 
-        self.dragBeginStartX = event.x - CONTROL_WIDTH + self._scroll_point.x
-        self.dragBeginStartY = event.y + self._scroll_point.y
-        self.marquee.set_size(0, 0)
-        self.marquee.set_position(event.x - CONTROL_WIDTH, event.y)
-        self.marquee.show()
+        return False
 
-    def _dragProgressCb(self, unused_actor, event):
-        if not self.drawMarquee:
-            return False
+    def __dragMotionCb(self, unused_widget, context, x, y, timestamp):
 
-        x, y, width, height = self._translateToTimelineContext(event)
+        target = self.drag_dest_find_target(context, None)
+        if not self.dropDataReady:
+            # We don't know yet the details of what's being dragged.
+            # Ask for the details.
+            self.drag_get_data(context, target, timestamp)
+            Gdk.drag_status(context, 0, timestamp)
+        else:
+            if not self.createClip(x, y):
+                self.__dragUpdate(self, x, y)
 
-        self.marquee.set_position(x, y)
-        self.marquee.set_size(width, height)
+            Gdk.drag_status(context, Gdk.DragAction.COPY, timestamp)
+            if not self.dropHighlight:
+                self.drag_highlight()
+                self.dropHighlight = True
+        return True
 
-        return False
+    def __dragLeaveCb(self, unused_widget, unused_context, unused_timestamp):
+        if self.draggingElement:
+            self._lastClipOnLeave = (self.draggingElement.bClip.get_layer(), self.draggingElement.bClip)
+            self.draggingElement.bClip.get_layer().remove_clip(self.draggingElement.bClip)
+            self._createdClips = False
 
-    def _dragEndCb(self, unused_actor, event):
-        if not self.drawMarquee:
-            return
-        self.drawMarquee = False
+    def __dragDropCb(self, unused_widget, context, x, y, timestamp):
+        # Same as in insertEnd: this value changes during insertion, snapshot
+        # it
+        zoom_was_fitted = self.parent.zoomed_fitted
 
-        x, y, width, height = self._translateToTimelineContext(event)
-        elements = self._getElementsInRegion(x, y, width, height)
-        self.createSelectionGroup()
-        for element in elements:
-            self.current_group.add(element)
-        selection = [child for child in self.current_group.get_children(True)
-                     if isinstance(child, GES.Source)]
-        self.selection.setSelection(selection, SELECT)
-        self.marquee.hide()
-
-    def _getElementsInRegion(self, x, y, width, height):
-        elements = set()
-        for element in self.elements:
-            if self._elementIsInLasso(element, x, y, x + width, y + height):
-                elements.add(element.bElement.get_toplevel_parent())
-        return elements
+        target = self.drag_dest_find_target(context, None)
+        if target.name() == "text/uri-list":
+            self.debug("Got list of URIs")
+            if self._lastClipOnLeave:
+                self.dropData = None
+                self.dropDataReady = False
 
-    # snapping indicator
-    def _snapCb(self, unused_timeline, unused_obj1, unused_obj2, position):
-        """
-        Display or hide a snapping indicator line
-        """
-        if position == 0:
-            self._snapEndedCb()
+                layer, clip = self._lastClipOnLeave
+                layer.add_clip(clip)
+
+                if zoom_was_fitted:
+                    self.parent._setBestZoomRatio()
+
+                self.dragEnd()
+        elif target.name() == "pitivi/effect":
+            self.fixme("TODO Implement effect support")
+
+        return True
+
+    def __dragDataReceivedCb(self, unused_widget,
+                             drag_context, unused_x,
+                             unused_y, selection_data, unused_info, timestamp):
+        dragging_effect = selection_data.get_data_type().name() == "pitivi/effect"
+        if not self.dropDataReady:
+            self._lastClipOnLeave = None
+            if dragging_effect:
+                # Dragging an effect from the Effect Library.
+                factory_name = str(selection_data.get_data(), "UTF-8")
+                self.dropData = factory_name
+                self.dropDataReady = True
+            elif selection_data.get_length() > 0:
+                # Dragging assets from the Media Library.
+                # if not self.dropOccured:
+                #    self.timeline.resetGhostClips()
+                self.dropData = selection_data.get_uris()
+                self.dropDataReady = True
+
+        if self.dropOccured:
+            # The data was requested by the drop handler.
+            self.dropOccured = False
+            drag_context.finish(True, False, timestamp)
         else:
-            height = len(self.bTimeline.get_layers()) * \
-                (EXPANDED_SIZE + SPACING) * 2
-            self._snap_indicator.props.height = height
-            self._snap_indicator.props.x = Zoomable.nsToPixel(position)
-            self._snap_indicator.props.visible = True
+            # The data was requested by the move handler.
+            self.isDraggedClip = not dragging_effect
+            self._createdClips = False
+            self.debug("Data received")
 
-    def _snapEndedCb(self, *unused_args):
-        self._snap_indicator.props.visible = False
+    # Handle layers
+    def _layerAddedCb(self, timeline, bLayer):
+        self._addLayer(bLayer)
+
+    def _addLayer(self, bLayer):
+        layer_widget = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
+
+        bLayer.control_ui = LayerControls(bLayer, self.app)
+        bLayer.ui = Layer(bLayer, self)
 
-    def _layerAddedCb(self, unused_timeline, layer):
-        self._add_layer(layer)
+        bLayer.ui.before_sep = SpacedSeparator()
+        layer_widget.pack_start(bLayer.ui.before_sep, False, False, 5)
+
+        self._layers.append(bLayer.ui)
+        layer_widget.pack_start(bLayer.ui, True, True, 0)
+
+        bLayer.ui.after_sep = SpacedSeparator()
+        layer_widget.pack_start(bLayer.ui.after_sep, False, False, 5)
+
+        self.__layers_vbox.pack_start(layer_widget, True, True, 0)
+        self.__layers_controls_vbox.pack_start(bLayer.control_ui, False, False, 0)
+
+        bLayer.ui.connect("remove-me", self._removeLayerCb)
+
+        self.show_all()
+
+    def _removeLayerCb(self, layer):
+        self.bTimeline.remove_layer(layer.bLayer)
+
+    def _removeLayer(self, bLayer):
+        self.info("Removing layer: %s" % bLayer.props.priority)
+        self.__layers_vbox.remove(bLayer.ui.get_parent())
+        self.__layers_controls_vbox.remove(bLayer.control_ui)
+
+        self._layers.remove(bLayer.ui)
+        bLayer.ui = None
+        bLayer.control_ui = None
+
+        self._layers.sort(key=lambda layer: layer.bLayer.props.priority)
+        i = 0
+        self.debug("Reseting layers priorities")
+        for layer in self._layers:
+            layer.bLayer.props.priority = i
+
+            self.__layers_vbox.child_set_property(layer.get_parent(),
+                                                  "position",
+                                                  layer.bLayer.props.priority)
+
+            self.__layers_controls_vbox.child_set_property(layer.bLayer.control_ui,
+                                                           "position",
+                                                           layer.bLayer.props.priority)
+
+            i += 1
 
     def _layerRemovedCb(self, unused_timeline, layer):
-        # FIXME : really remove layer ^^
-        for lyr in self.bTimeline.get_layers():
-            if lyr.props.priority > layer.props.priority:
-                lyr.props.priority -= 1
-        self._remove_layer(layer)
-        self._updatePlayHead()
-
-    def _trackAddedCb(self, unused_timeline, track):
-        self._connectTrack(track)
-        self._container.app.project_manager.current_project.update_restriction_caps(
-        )
+        self._removeLayer(layer)
+
+    # Interface Zoomable
+    def zoomChanged(self):
+        self.debug("Zoom changed")
+        self.updatePosition()
+        self.layout.move(self.__playhead, self.nsToPixel(self.__last_position), 0)
+        self.queue_draw()
+
+    # Edition handling
+    def __setupTimelineEdition(self):
+        self.draggingElement = None
+        self.__editing_context = None
+        self.__got_dragged = False
+        self.__drag_start_x = 0
+        self.__on_separators = []
+        self._on_layer = None
+
+    def __getLayerAt(self, y, bLayer=None):
+        if y < 20 or not self.bTimeline.get_layers():
+            try:
+                bLayer = self.bTimeline.get_layers()[0]
+            except IndexError:
+                bLayer = self.bTimeline.append_layer()
+
+            self.debug("Returning very first layer")
+            return bLayer, [bLayer.ui.before_sep]
+
+        layers = self.bTimeline.get_layers()
+        rect = Gdk.Rectangle()
+        rect.x = 0
+        rect.y = y
+        rect.height = 1
+        rect.width = 1
+        for i in range(len(layers)):
+            layer = layers[i]
+            layer_alloc = layer.ui.get_allocation()
+
+            if Gdk.rectangle_intersect(rect, layer_alloc)[0] is True:
+                return layer, []
+
+            separators = [layer.ui.after_sep]
+            sep_rectangle = Gdk.Rectangle()
+            sep_rectangle.x = 0
+            sep_rectangle.y = layer_alloc.y + layer_alloc.height
+            try:
+                sep_rectangle.height = layers[i + 1].ui.get_allocation().y - \
+                    layer_alloc.y - layer_alloc.height
+                separators.append(layers[i + 1].ui.before_sep)
+            except IndexError:
+                sep_rectangle.height += ui.LAYER_HEIGHT
+
+            if sep_rectangle.y <= rect.y <= sep_rectangle.y + sep_rectangle.height:
+                self.debug("Returning layer %s, separators: %s" % (layer, separators))
+                return layer, separators
+
+        self.debug("Returning very last layer")
+
+        return layers[-1], [layers[-1].ui.after_sep]
+
+    def __setHoverSeparators(self):
+        for sep in self.__on_separators:
+            ui.set_children_state_recurse(sep, Gtk.StateFlags.PRELIGHT)
+
+    def __unsetHoverSeparators(self):
+        for sep in self.__on_separators:
+            ui.unset_children_state_recurse(sep, Gtk.StateFlags.PRELIGHT)
+
+    def __dragUpdate(self, event_widget, x, y):
+        if self.__got_dragged is False:
+            self.__got_dragged = True
+            self.allowSeek = False
+            self.__editing_context = timelineUtils.EditingContext(self.draggingElement.bClip,
+                                                                  self.bTimeline,
+                                                                  self.draggingElement.edit_mode,
+                                                                  self.draggingElement.dragging_edge,
+                                                                  None,
+                                                                  self.app.action_log)
+
+        x, y = event_widget.translate_coordinates(self, x, y)
+        x -= ui.CONTROL_WIDTH
+        x += self.hadj.get_value()
+        y += self.vadj.get_value()
+
+        mode = self.get_parent().getEditionMode(isAHandle=self.__editing_context.edge != GES.Edge.EDGE_NONE)
+        self.__editing_context.setMode(mode)
+
+        if self.__editing_context.edge is GES.Edge.EDGE_END:
+            position = self.pixelToNs(x)
+        else:
+            position = self.pixelToNs(x - self.__drag_start_x)
+
+        self.__unsetHoverSeparators()
+
+        self._on_layer, self.__on_separators = self.__getLayerAt(y,
+                                                                 self.draggingElement.bClip.get_layer())
+
+        priority = self._on_layer.props.priority
+        if self.__on_separators:
+            self.__setHoverSeparators()
+
+        self.__editing_context.editTo(position, priority)
+
+    def createLayer(self, priority):
+        self.info("Creating layer %s" % priority)
+        new_bLayer = GES.Layer.new()
+        new_bLayer.props.priority = priority
+        self.bTimeline.add_layer(new_bLayer)
 
-    def _trackRemovedCb(self, unused_timeline, track):
-        self._disconnectTrack(track)
-        for element in track.get_elements():
-            self._removeTimelineElement(track, element)
+        bLayers = self.bTimeline.get_layers()
+        if priority < len(bLayers):
+            for bLayer in bLayers:
+                if bLayer == new_bLayer:
+                    continue
 
-    def _trackElementAddedCb(self, track, bElement):
-        self._updateSize()
-        self._addTimelineElement(track, bElement)
+                if bLayer.get_priority() >= priority:
+                    bLayer.props.priority += 1
+                    self.__layers_vbox.child_set_property(bLayer.ui.get_parent(),
+                                                          "position",
+                                                          bLayer.props.priority)
 
-    def _trackElementRemovedCb(self, track, bElement):
-        self._removeTimelineElement(track, bElement)
+                    self.__layers_controls_vbox.child_set_property(bLayer.control_ui,
+                                                                   "position",
+                                                                   bLayer.props.priority)
+
+        self.__layers_vbox.child_set_property(new_bLayer.ui.get_parent(),
+                                              "position",
+                                              new_bLayer.props.priority)
+
+        self.__layers_controls_vbox.child_set_property(new_bLayer.control_ui,
+                                                       "position",
+                                                       new_bLayer.props.priority)
+
+        return new_bLayer
+
+    def dragEnd(self):
+        if self.draggingElement is not None and self.__got_dragged:
+            self.debug("DONE dargging %s" % self.draggingElement)
+            self._snapEndedCb()
 
-    def _elementPriorityChangedCb(self, unused_bElement, unused_priority, element):
-        self._setElementY(element)
+            if self.__on_separators:
+                priority = self._on_layer.props.priority
+                if self.__on_separators[0] == self._on_layer.ui.after_sep:
+                    priority = self._on_layer.props.priority + 1
 
-    def _elementStartChangedCb(self, unused_bElement, unused_start, element):
-        self._updateSize()
-        self.allowSeek = False
-        self._setElementX(element)
+                self.createLayer(max(0, priority))
+                self._onSeparatorStartTime = None
+                self.__editing_context.editTo(self.__editing_context.new_position, priority)
+            self.layout.props.width = self._computeTheoricalWidth()
 
-    def _elementDurationChangedCb(self, unused_bElement, unused_duration, element):
-        self._updateSize()
-        self.allowSeek = False
-        element.update(ease=False)
+            self.__editing_context.finish()
 
-    def _elementInPointChangedCb(self, unused_bElement, unused_inpoint, element):
-        self.allowSeek = False
-        self._setElementX(element)
+        self.draggingElement = None
+        self.__got_dragged = False
+        self.__editing_context = None
+        self.hideSnapBar()
 
-    def _layerPriorityChangedCb(self, unused_layer, unused_priority):
-        self._redraw()
+        self.__unsetHoverSeparators()
+        self.__on_separators = []
+
+        self.queue_draw()
 
 
 class TimelineContainer(Gtk.Grid, Zoomable, Loggable):
@@ -671,8 +948,6 @@ class TimelineContainer(Gtk.Grid, Zoomable, Loggable):
 
         # Allows stealing focus from other GTK widgets, prevent accidents:
         self.props.can_focus = True
-        self.connect("focus-in-event", self._focusInCb)
-        self.connect("focus-out-event", self._focusOutCb)
 
         self.gui = gui
         self.ui_manager = ui_manager
@@ -688,12 +963,6 @@ class TimelineContainer(Gtk.Grid, Zoomable, Loggable):
         self._createActions()
         self._createUi()
 
-        self.dropHighlight = False
-        self.dropOccured = False
-        self.dropDataReady = False
-        self.dropData = None
-        self._setUpDragAndDrop()
-
         self._settings.connect("edgeSnapDeadbandChanged",
                                self._snapDistanceChangedCb)
 
@@ -763,36 +1032,10 @@ class TimelineContainer(Gtk.Grid, Zoomable, Loggable):
             projectmanager.connect(
                 "new-project-loaded", self._projectChangedCb)
 
-    def updateHScrollAdjustments(self):
-        """
-        Recalculate the horizontal scrollbar depending on the timeline duration.
-        """
-        timeline_ui_width = self.embed.get_allocation().width
-        if self.bTimeline is None:
-            contents_size = 0
-        else:
-            contents_size = Zoomable.nsToPixel(self.bTimeline.props.duration)
-
-        # Provide some space for clip insertion at the end
-        end_padding = CONTROL_WIDTH * 2
-
-        self.hadj.props.lower = 0
-        self.hadj.props.upper = contents_size + end_padding
-        self.hadj.props.page_size = timeline_ui_width
-        self.hadj.props.page_increment = contents_size * 0.9
-        self.hadj.props.step_increment = contents_size * 0.1
-
-        if contents_size <= timeline_ui_width:
-            # We're zoomed out completely, re-enable automatic zoom fitting
-            # when adding new clips.
-            self.log("Setting 'zoomed_fitted' to True")
-            self.zoomed_fitted = True
-        else:
-            self.log("Setting 'zoomed_fitted' to False")
-            self.zoomed_fitted = False
-
     def zoomFit(self):
-        self._hscrollbar.set_value(0)
+        # self._hscrollbar.set_value(0)
+        self.app.write_action("set-zoom-fit", {"not-mandatory-action-type": True})
+
         self._setBestZoomRatio(allow_zoom_in=True)
 
     def scrollToPixel(self, x):
@@ -802,10 +1045,6 @@ class TimelineContainer(Gtk.Grid, Zoomable, Loggable):
         else:
             self._scrollToPixel(x)
 
-    def seekInPosition(self, position):
-        self.pressed = True
-        self._seeker.seek(position)
-
     def setProject(self, project):
         self._project = project
         if self._project:
@@ -847,48 +1086,25 @@ class TimelineContainer(Gtk.Grid, Zoomable, Loggable):
     # Internal API
 
     def _createUi(self):
-        self.embed = GtkClutter.Embed()
-        self.embed.get_accessible().set_name("timeline canvas")  # for dogtail
-        self.stage = self.embed.get_stage()
-
-        self.timeline = TimelineStage(self, self._settings)
-        self.controls = ControlContainer(self.app, self.timeline)
         self.zoomBox = ZoomBox(self)
         self._shiftMask = False
         self._controlMask = False
 
-        self.stage.set_background_color(TIMELINE_BACKGROUND_COLOR)
-        self.timeline.set_position(CONTROL_WIDTH, 0)
-        self.controls.set_position(0, 0)
+        # self.stage.set_background_color(TIMELINE_BACKGROUND_COLOR)
+        # self.timeline.set_position(CONTROL_WIDTH, 0)
+        # self.controls.set_position(0, 0)
 
-        self.stage.add_child(self.controls)
-        self.stage.add_child(self.timeline)
-
-        self.timeline.connect("button-press-event", self._timelineClickedCb)
-        self.timeline.connect(
-            "button-release-event", self._timelineClickReleasedCb)
-        # FIXME: Connect to the stage of the embed instead, see
-        # https://bugzilla.gnome.org/show_bug.cgi?id=697522
-        self.embed.connect("scroll-event", self._scrollEventCb)
-
-        self.connect("key-press-event", self._keyPressEventCb)
-        self.connect("key-release-event", self._keyReleaseEventCb)
-
-        self.point = Clutter.Point()
-        self.point.x = 0
-        self.point.y = 0
+        # self.stage.add_child(self.controls)
+        # self.stage.add_child(self.timeline)
 
         self.scrolled = 0
 
         self.zoomed_fitted = True
-        self.pressed = False
-
-        self.hadj = Gtk.Adjustment()
-        self.vadj = Gtk.Adjustment()
-        self.hadj.connect("value-changed", self._updateScrollPosition)
-        self.vadj.connect("value-changed", self._updateScrollPosition)
-        self.vadj.props.lower = 0
-        self.vadj.props.page_size = 250
+
+        self.timeline = Timeline(self, self.app)
+        self.hadj = self.timeline.layout.get_hadjustment()
+        self.vadj = self.timeline.layout.get_vadjustment()
+
         self._vscrollbar = Gtk.VScrollbar(adjustment=self.vadj)
         self._hscrollbar = Gtk.HScrollbar(adjustment=self.hadj)
 
@@ -926,7 +1142,7 @@ class TimelineContainer(Gtk.Grid, Zoomable, Loggable):
 
         self.attach(self.zoomBox, 0, 0, 1, 1)
         self.attach(self.ruler, 1, 0, 1, 1)
-        self.attach(self.embed, 0, 1, 2, 1)
+        self.attach(self.timeline, 0, 1, 2, 1)
         self.attach(self._vscrollbar, 2, 1, 1, 1)
         self.attach(self._hscrollbar, 1, 2, 1, 1)
         self.attach(toolbar, 3, 1, 1, 1)
@@ -944,28 +1160,14 @@ class TimelineContainer(Gtk.Grid, Zoomable, Loggable):
 
     def disableKeyboardAndMouseEvents(self):
         """
-        A safety measure to prevent interacting with the Clutter timeline
-        during render (no, setting GtkClutterEmbed as insensitive won't work,
-        neither will using handler_block_by_func, nor connecting to the "event"
-        signals because they won't block the children and other widgets).
+        A safety measure to prevent interacting with the timeline
         """
         self.info("Blocking timeline mouse and keyboard signals")
-        self.stage.connect("captured-event", self._ignoreAllEventsCb)
+        self.timeline.connect("event", self._ignoreAllEventsCb)
 
     def _ignoreAllEventsCb(self, *unused_args):
         return True
 
-    def _setUpDragAndDrop(self):
-        # To be able to receive effects dragged on clips.
-        self.drag_dest_set(0, [EFFECT_TARGET_ENTRY], Gdk.DragAction.COPY)
-        # To be able to receive assets dragged from the media library.
-        self.drag_dest_add_uri_targets()
-
-        self.connect('drag-motion', self._dragMotionCb)
-        self.connect('drag-data-received', self._dragDataReceivedCb)
-        self.connect('drag-drop', self._dragDropCb)
-        self.connect('drag-leave', self._dragLeaveCb)
-
     def _getLayers(self):
         """
         Make sure we have at least one layer in our timeline.
@@ -1069,17 +1271,6 @@ class TimelineContainer(Gtk.Grid, Zoomable, Loggable):
         self.playhead_actions.add_actions(playhead_actions)
         self.ui_manager.insert_action_group(self.playhead_actions, -1)
 
-    def _updateScrollPosition(self, unused_adjustment):
-        self._scroll_pos_ns = Zoomable.pixelToNs(self.hadj.get_value())
-        point = Clutter.Point()
-        point.x = self.hadj.get_value()
-        point.y = self.vadj.get_value()
-        self.point = point
-
-        self.timeline.scroll_to_point(point)
-        point.x = 0
-        self.controls.scroll_to_point(point)
-
     def _setBestZoomRatio(self, allow_zoom_in=False):
         """
         Set the zoom level so that the entire timeline is in view.
@@ -1145,28 +1336,18 @@ class TimelineContainer(Gtk.Grid, Zoomable, Loggable):
                 (x, self.hadj.props.lower))
 
         if self._project and self._project.pipeline.getState() != Gst.State.PLAYING:
-            self.timeline.save_easing_state()
-            self.timeline.set_easing_duration(600)
+            self.error("FIXME What should be done here?")
 
         self._hscrollbar.set_value(x)
         if self._project and self._project.pipeline.getState() != Gst.State.PLAYING:
-            self.timeline.restore_easing_state()
+            self.error("FIXME What should be done here?")
+
+        self.timeline.updatePosition()
+        self.timeline.queue_draw()
         return False
 
-    def _scrollToPlayhead(self):
-        if self.ruler.pressed or self.pressed:
-            self.pressed = False
-            return
-        canvas_width = self.embed.get_allocation().width - CONTROL_WIDTH
-        try:
-            new_pos = Zoomable.nsToPixel(self._project.pipeline.getPosition())
-        except PipelineError as e:
-            self.info("Pipeline error: %s", e)
-            return
-        except AttributeError:  # Standalone, no pipeline.
-            return
-        playhead_pos_centered = new_pos - canvas_width / 2
-        self.scrollToPixel(max(0, playhead_pos_centered))
+    def scrollToPlayhead(self):
+        self.timeline.scrollToPlayhead()
 
     def _deleteSelected(self, unused_action):
         if self.bTimeline:
@@ -1174,6 +1355,8 @@ class TimelineContainer(Gtk.Grid, Zoomable, Loggable):
 
             for clip in self.timeline.selection:
                 layer = clip.get_layer()
+                if isinstance(clip, GES.TransitionClip):
+                    continue
                 layer.remove_clip(clip)
 
             self._project.pipeline.commit_timeline()
@@ -1195,7 +1378,32 @@ class TimelineContainer(Gtk.Grid, Zoomable, Loggable):
                     containers.add(toplevel)
 
             for container in containers:
-                GES.Container.ungroup(container, False)
+                was_clip = isinstance(container, GES.Clip)
+                clips = GES.Container.ungroup(container, False)
+                if not was_clip:
+                    continue
+
+                new_layers = {}
+                for clip in clips:
+                    if isinstance(clip, GES.Clip):
+                        all_audio = True
+                        for child in clip.get_children(True):
+                            if child.get_track_type() != GES.TrackType.AUDIO:
+                                all_audio = False
+                                break
+
+                        if not all_audio:
+                            self.debug("Not all audio, not moving anything to a new layer")
+
+                            continue
+
+                        new_layer = new_layers.get(clip.get_layer().get_priority(), None)
+                        if not new_layer:
+                            new_layer = self.timeline.createLayer(clip.get_layer().get_priority() + 1)
+                            new_layers[clip.get_layer().get_priority()] = new_layer
+                        self.info("Moving audio audio clip %s to new layer %s" % (clip, new_layer))
+                        clip.move_to_layer(new_layer)
+
                 self._project.pipeline.commit_timeline()
 
             self.timeline.createSelectionGroup()
@@ -1219,7 +1427,8 @@ class TimelineContainer(Gtk.Grid, Zoomable, Loggable):
                     containers.add(toplevel)
 
             if containers:
-                group = GES.Container.group(list(containers))
+                GES.Container.group(list(containers))
+
             self.timeline.createSelectionGroup()
 
             self._project.pipeline.commit_timeline()
@@ -1261,6 +1470,7 @@ class TimelineContainer(Gtk.Grid, Zoomable, Loggable):
             for track in self.bTimeline.get_tracks():
                 self._splitElements(track.get_elements())
 
+        self.timeline.hideSnapBar()
         self._project.pipeline.commit_timeline()
 
     def _splitElements(self, elements):
@@ -1288,7 +1498,7 @@ class TimelineContainer(Gtk.Grid, Zoomable, Loggable):
         for obj in selected:
             keyframe_exists = False
             position = self._project.pipeline.getPosition()
-            position_in_obj = (position - obj.start) + obj.in_point
+            position_in_obj = (position - obj.props.start) + obj.props.in_point
             interpolators = obj.getInterpolators()
             for value in interpolators:
                 interpolator = obj.getInterpolator(value)
@@ -1320,11 +1530,9 @@ class TimelineContainer(Gtk.Grid, Zoomable, Loggable):
             self.bTimeline.set_snapping_distance(
                 Zoomable.pixelToNs(self._settings.edgeSnapDeadband))
 
-        self.updateHScrollAdjustments()
-
-    # Callbacks
+    # Gtk widget virtual methods
 
-    def _keyPressEventCb(self, unused_widget, event):
+    def do_key_press_event(self, event):
         # This is used both for changing the selection modes and for affecting
         # the seek keyboard shortcuts further below
         if event.keyval == Gdk.KEY_Shift_L:
@@ -1345,33 +1553,25 @@ class TimelineContainer(Gtk.Grid, Zoomable, Loggable):
             else:
                 self._project.pipeline.stepFrame(self._framerate, 1)
 
-    def _keyReleaseEventCb(self, unused_widget, event):
+    def do_key_release_event(self, event):
         if event.keyval == Gdk.KEY_Shift_L:
             self._shiftMask = False
         elif event.keyval == Gdk.KEY_Control_L:
             self._controlMask = False
 
-    def _focusInCb(self, unused_widget, unused_arg):
+    def do_focus_in_event(self, unused_event):
         self.log("Timeline has grabbed focus")
         self.setActionsSensitivity(True)
 
-    def _focusOutCb(self, unused_widget, unused_arg):
+    def do_focus_out_event(self, unused_event):
         self.log("Timeline has lost focus")
         self.setActionsSensitivity(False)
 
-    def _timelineClickedCb(self, unused_timeline, unused_event):
+    def __buttonPressCb(self, unused_event):
         self.pressed = True
         self.grab_focus()  # Prevent other widgets from being confused
 
-    def _timelineClickReleasedCb(self, unused_timeline, event):
-        if self.app and self.timeline.allowSeek is True:
-            position = self.pixelToNs(
-                event.x - CONTROL_WIDTH + self.timeline._scroll_point.x)
-            self._seeker.seek(position)
-
-        self.timeline.allowSeek = True
-        self.timeline._snapEndedCb()
-
+    # Callbacks
     def _renderingSettingsChangedCb(self, project, item, value):
         """
         Called when any Project metadata changes, we filter out the one
@@ -1445,26 +1645,6 @@ class TimelineContainer(Gtk.Grid, Zoomable, Loggable):
     def _zoomFitCb(self, unused_action):
         self.zoomFit()
 
-    def _scrollEventCb(self, unused_embed, event):
-        unused_res, delta_x, delta_y = event.get_scroll_deltas()
-        if event.state & Gdk.ModifierType.CONTROL_MASK:
-            if delta_y < 0:
-                Zoomable.zoomIn()
-            elif delta_y > 0:
-                Zoomable.zoomOut()
-            self._scrollToPlayhead()
-        elif event.state & Gdk.ModifierType.SHIFT_MASK:
-            if delta_y > 0:
-                self.scroll_down()
-            elif delta_y < 0:
-                self.scroll_up()
-        else:
-            if delta_y > 0:
-                self.scroll_right()
-            elif delta_y < 0:
-                self.scroll_left()
-        self.scrolled += 1
-
     def _selectionChangedCb(self, selection):
         """
         The selected clips on the timeline canvas have changed with the
@@ -1486,96 +1666,3 @@ class TimelineContainer(Gtk.Grid, Zoomable, Loggable):
             self.info("Automatic ripple deactivated")
             self._autoripple_active = False
         self._settings.timelineAutoRipple = self._autoripple_active
-
-    # drag and drop
-
-    def _dragMotionCb(self, widget, context, x, y, timestamp):
-        target = widget.drag_dest_find_target(context, None)
-        if not self.dropDataReady:
-            # We don't know yet the details of what's being dragged.
-            # Ask for the details.
-            widget.drag_get_data(context, target, timestamp)
-            Gdk.drag_status(context, 0, timestamp)
-        else:
-            if self.isDraggedClip:
-                # From the media library
-                x, y = self.transposeXY(x, y)
-                if not self.timeline.ghostClips:
-                    for uri in self.dropData:
-                        asset = self.app.gui.medialibrary.getAssetForUri(uri)
-                        if asset is None:
-                            self.isDraggedClip = False
-                            break
-                        self.timeline.addGhostClip(asset, x, y)
-                self.timeline.updateGhostClips(x, y)
-
-            Gdk.drag_status(context, Gdk.DragAction.COPY, timestamp)
-            if not self.dropHighlight:
-                widget.drag_highlight()
-                self.dropHighlight = True
-        return True
-
-    def _dragLeaveCb(self, widget, unused_context, unused_timestamp):
-        if self.dropHighlight:
-            widget.drag_unhighlight()
-            self.dropHighlight = False
-        # Cleanup because the user might have moved the mouse cursor outside
-        # and abandon this widget.
-        self.dropDataReady = False
-        self.timeline.removeGhostClips()
-
-    def _dragDropCb(self, widget, context, x, y, timestamp):
-        # Same as in insertEnd: this value changes during insertion, snapshot
-        # it
-        zoom_was_fitted = self.zoomed_fitted
-
-        target = widget.drag_dest_find_target(context, None)
-        y -= self.ruler.get_allocation().height
-        if target.name() == "text/uri-list":
-            self.dropOccured = True
-            widget.drag_get_data(context, target, timestamp)
-            if self.isDraggedClip:
-                self.timeline.convertGhostClips()
-                self.timeline.resetGhostClips()
-                self.dropData = None
-                self.dropDataReady = False
-                if zoom_was_fitted:
-                    self._setBestZoomRatio()
-                else:
-                    x, y = self.transposeXY(x, y)
-                    # Add a margin (up to 50px) on the left, this prevents
-                    # disorientation & clarifies to users where the clip starts
-                    margin = min(x, 50)
-                    self.scrollToPixel(x - margin)
-        elif target.name() == "pitivi/effect":
-            actor = self.stage.get_actor_at_pos(
-                Clutter.PickMode.REACTIVE, x, y)
-            bElement = actor.bElement
-            clip = bElement.get_parent()
-            factory_name = self.dropData
-            self.app.gui.clipconfig.effect_expander.addEffectToClip(
-                clip, factory_name)
-        return True
-
-    def _dragDataReceivedCb(self, widget, drag_context, unused_x, unused_y, selection_data, unused_info, 
timestamp):
-        dragging_effect = selection_data.get_data_type().name() == "pitivi/effect"
-        if not self.dropDataReady:
-            if dragging_effect:
-                # Dragging an effect from the Effect Library.
-                factory_name = str(selection_data.get_data(), "UTF-8")
-                self.dropData = factory_name
-                self.dropDataReady = True
-            elif selection_data.get_length() > 0:
-                # Dragging assets from the Media Library.
-                if not self.dropOccured:
-                    self.timeline.resetGhostClips()
-                self.dropData = selection_data.get_uris()
-                self.dropDataReady = True
-
-        if self.dropOccured:
-            # The data was requested by the drop handler.
-            self.dropOccured = False
-            drag_context.finish(True, False, timestamp)
-        else:
-            # The data was requested by the move handler.
-            self.isDraggedClip = not dragging_effect
diff --git a/pitivi/transitions.py b/pitivi/transitions.py
index 86e9bbc..ec4fe23 100644
--- a/pitivi/transitions.py
+++ b/pitivi/transitions.py
@@ -173,11 +173,10 @@ class TransitionsListWidget(Gtk.Box, Loggable):
         else:
             self.props_widgets.set_sensitive(True)
 
-        clip_asset = self.element.get_parent()
-        clip_asset.set_asset(transition_asset)
+        self.element.set_asset(transition_asset)
         self.app.write_action("element-set-asset", {
             "asset-id": transition_asset.get_id(),
-            "element-name": clip_asset.get_name()})
+            "element-name": self.element.get_name()})
         self.app.project_manager.current_project.seeker.flush(True)
 
         return True
@@ -289,7 +288,7 @@ class TransitionsListWidget(Gtk.Box, Loggable):
         self.element.connect("notify::border", self._borderChangedCb)
         self.element.connect("notify::invert", self._invertChangedCb)
         self.element.connect("notify::type", self._transitionTypeChangedCb)
-        transition_asset = element.get_parent().get_asset()
+        transition_asset = element.get_asset()
         if transition_asset.get_id() == "crossfade":
             self.props_widgets.set_sensitive(False)
         else:
diff --git a/pitivi/undo/timeline.py b/pitivi/undo/timeline.py
index 6efb029..465d06f 100644
--- a/pitivi/undo/timeline.py
+++ b/pitivi/undo/timeline.py
@@ -20,6 +20,7 @@
 # Boston, MA 02110-1301, USA.
 
 from gi.repository import Gst
+from gi.repository import GstController
 from gi.repository import GES
 from gi.repository import GObject
 
diff --git a/pitivi/utils/pipeline.py b/pitivi/utils/pipeline.py
index 300636a..0de95d6 100644
--- a/pitivi/utils/pipeline.py
+++ b/pitivi/utils/pipeline.py
@@ -183,7 +183,7 @@ class SimplePipeline(GObject.Object, Loggable):
         self._bus.add_signal_watch()
         self._bus.connect("message", self._busMessageCb)
         self._listening = False  # for the position handler
-        self._listeningInterval = 300  # default 300ms
+        self._listeningInterval = 50  # default 300ms
         self._listeningSigId = 0
         self._duration = Gst.CLOCK_TIME_NONE
         self._last_position = int(0 * Gst.SECOND)
@@ -343,7 +343,7 @@ class SimplePipeline(GObject.Object, Loggable):
         self._duration = dur
         return dur
 
-    def activatePositionListener(self, interval=500):
+    def activatePositionListener(self, interval=50):
         """
         Activate the position listener.
 
@@ -433,9 +433,9 @@ class SimplePipeline(GObject.Object, Loggable):
         self.debug("position: %s", format_ns(position))
 
         # clamp between [0, duration]
-        position = max(0, min(position, self.getDuration()) - 1)
+        position = max(0, min(position, self.getDuration()))
 
-        res = self._pipeline.seek(1.0, Gst.Format.TIME, Gst.SeekFlags.FLUSH,
+        res = self._pipeline.seek(1.0, Gst.Format.TIME, Gst.SeekFlags.FLUSH | Gst.SeekFlags.ACCURATE,
                                   Gst.SeekType.SET, position,
                                   Gst.SeekType.NONE, -1)
         self._addWaitingForAsyncDoneTimeout()
@@ -516,6 +516,14 @@ class SimplePipeline(GObject.Object, Loggable):
         else:
             self.log("%s [%r]", message.type, message.src)
 
+    @property
+    def _waiting_for_async_done(self):
+        return self.__waiting_for_async_done
+
+    @_waiting_for_async_done.setter
+    def _waiting_for_async_done(self, value):
+        self.__waiting_for_async_done = value
+
     def _recover(self):
         if self._attempted_recoveries > MAX_RECOVERIES:
             self.emit("died")
diff --git a/pitivi/utils/timeline.py b/pitivi/utils/timeline.py
index a4d3dea..854396c 100644
--- a/pitivi/utils/timeline.py
+++ b/pitivi/utils/timeline.py
@@ -23,6 +23,10 @@
 from gi.repository import GES
 from gi.repository import GObject
 from gi.repository import Gst
+from gi.repository import Gtk
+
+from pitivi.utils.loggable import Loggable
+from pitivi.utils import ui
 
 
 # Selection modes
@@ -78,7 +82,7 @@ class Selected(GObject.Object):
     selected = property(getSelected, setSelected)
 
 
-class Selection(GObject.Object):
+class Selection(GObject.Object, Loggable):
 
     """
     A collection of L{GES.Clip}.
@@ -96,6 +100,7 @@ class Selection(GObject.Object):
 
     def __init__(self):
         GObject.Object.__init__(self)
+        Loggable.__init__(self)
         self.selected = set()
 
     def setToObj(self, obj, mode):
@@ -142,9 +147,12 @@ class Selection(GObject.Object):
 
         for obj in old_selection - self.selected:
             for element in obj.get_children(False):
+                ui.unset_children_state_recurse(obj.ui, Gtk.StateFlags.SELECTED)
                 if not isinstance(element, GES.BaseEffect) and not isinstance(element, GES.TextOverlay):
                     element.selected.selected = False
+
         for obj in self.selected - old_selection:
+            ui.set_children_state_recurse(obj.ui, Gtk.StateFlags.SELECTED)
             for element in obj.get_children(False):
                 if not isinstance(element, GES.BaseEffect) and not isinstance(element, GES.TextOverlay):
                     element.selected.selected = True
@@ -193,7 +201,7 @@ class Selection(GObject.Object):
 # -----------------------------------------------------------------------------#
 # Timeline edition modes helper                         #
 
-class EditingContext(GObject.Object):
+class EditingContext(GObject.Object, Loggable):
 
     """
         Encapsulates interactive editing.
@@ -201,11 +209,6 @@ class EditingContext(GObject.Object):
         This is the main class for interactive edition.
     """
 
-    __gsignals__ = {
-        "clip-trim": (GObject.SIGNAL_RUN_LAST, None, (GES.Clip, int)),
-        "clip-trim-finished": (GObject.SIGNAL_RUN_LAST, None, ()),
-    }
-
     def __init__(self, focus, timeline, mode, edge, unused_settings, action_log):
         """
         @param focus: the Clip or TrackElement which is to be the
@@ -230,6 +233,7 @@ class EditingContext(GObject.Object):
         @returns: An instance of L{pitivi.utils.timeline.EditingContext}
         """
         GObject.Object.__init__(self)
+        Loggable.__init__(self)
         if isinstance(focus, GES.TrackElement):
             self.focus = focus.get_parent()
         else:
@@ -253,7 +257,7 @@ class EditingContext(GObject.Object):
     def finish(self):
         self.action_log.commit()
         self.timeline.get_asset().pipeline.commit_timeline()
-        self.emit("clip-trim-finished")
+        self.timeline.ui.app.gui.viewer.clipTrimPreviewFinished()
 
     def setMode(self, mode):
         """Set the current editing mode.
@@ -271,8 +275,7 @@ class EditingContext(GObject.Object):
         self.new_position = position
         self.new_priority = priority
 
-        res = self.focus.edit(
-            [], priority, self.mode, self.edge, int(position))
+        res = self.focus.edit([], priority, self.mode, self.edge, int(position))
         self.action_log.app.write_action("edit-container", {
             "container-name": self.focus.get_name(),
             "position": float(position / Gst.SECOND),
@@ -282,9 +285,10 @@ class EditingContext(GObject.Object):
 
         if res and self.mode == GES.EditMode.EDIT_TRIM:
             if self.edge == GES.Edge.EDGE_START:
-                self.emit("clip-trim", self.focus, self.focus.props.in_point)
+                self.timeline.ui.app.gui.viewer.clipTrimPreview(self.focus, self.focus.props.in_point)
             elif self.edge == GES.Edge.EDGE_END:
-                self.emit("clip-trim", self.focus, self.focus.props.duration)
+                self.timeline.ui.app.gui.viewer.clipTrimPreview(self.focus,
+                                                                self.focus.props.duration + 
self.focus.props.in_point)
 
 
 # -------------------------- Interfaces ----------------------------------------#
@@ -406,6 +410,18 @@ class Zoomable(object):
         return int((float(duration) / Gst.SECOND) * cls.zoomratio)
 
     @classmethod
+    def nsToPixelAccurate(cls, duration):
+        """
+        Returns the pixel equivalent of the given duration, according to the
+        set zoom ratio
+        """
+        # Here, a long time ago (206f3a05), a pissed programmer said:
+        # DIE YOU CUNTMUNCH CLOCK_TIME_NONE UBER STUPIDITY OF CRACK BINDINGS !!
+        if duration == Gst.CLOCK_TIME_NONE:
+            return 0
+        return ((float(duration) / Gst.SECOND) * cls.zoomratio)
+
+    @classmethod
     def _zoomChanged(cls):
         for inst in cls._instances:
             inst.zoomChanged()
diff --git a/pitivi/utils/ui.py b/pitivi/utils/ui.py
index 3dbb189..a03952c 100644
--- a/pitivi/utils/ui.py
+++ b/pitivi/utils/ui.py
@@ -35,7 +35,6 @@ import urllib.error
 
 from gettext import ngettext, gettext as _
 
-from gi.repository import Clutter
 from gi.repository import GLib
 from gi.repository import GES
 from gi.repository import Gdk
@@ -47,21 +46,22 @@ from gi.repository.GstPbutils import DiscovererVideoInfo, DiscovererAudioInfo,\
 
 from pitivi.utils.loggable import doLog, ERROR
 from pitivi.utils.misc import path_from_uri
+from pitivi.configure import get_pixmap_dir
 
 
 # Dimensions in pixels
 TRACK_SPACING = 8
 EXPANDED_SIZE = 65
-CONTROL_WIDTH = 250
+CONTROL_WIDTH = 300
 
 PADDING = 6
 SPACING = 10
 
 PLAYHEAD_WIDTH = 1
+SNAPBAR_WIDTH = 5
 CANVAS_SPACING = 21
 KEYFRAME_SIZE = 8
-
-PLAYHEAD_COLOR = Clutter.Color.new(200, 0, 0, 255)
+LAYER_HEIGHT = 130
 
 # Layer creation blocking time in s
 LAYER_CREATION_BLOCK_TIME = 0.2
@@ -97,9 +97,72 @@ NORMAL_FONT = _get_font("font-name", "Cantarell")
 DOCUMENT_FONT = _get_font("document-font-name", "Sans")
 MONOSPACE_FONT = _get_font("monospace-font-name", "Monospace")
 
+TIMELINE_CSS = """
+    .AudioBackground {
+        background-color: #496c21;
+    }
+
+    .VideoBackground {
+        background-color: #2d2d2d;
+    }
+
+    .AudioBackground:selected {
+        background-color: #1b2e0e;
+    }
+
+    .VideoBackground:selected {
+        background-color: #0f0f0f;
+    }
+
+    .Trimbar {
+         background-image: url('%(trimbar_normal)s');
+    }
+
+    .Trimbar:first-child {
+        border-radius: 5px 0 0 5px;
+    }
+
+    .Trimbar:last-child {
+        border-radius: 0 5px 5px 0;
+    }
+
+    .Trimbar:hover {
+         background-image: url('%(trimbar_focused)s');
+    }
+
+    .PlayHead {
+         background-color: red;
+    }
+
+    .Clip {
+    /* TODO */
+    }
+
+    .SnapBar {
+        background-color: rgb(127, 153, 204);
+    }
+
+    .TransitionClip {
+         background-color: rgba(127, 153, 204, 0.5);
+    }
+
+    .TransitionClip:selected {
+         background-color: rgba(127, 200, 204, 0.7);
+    }
+
+    .SpacedSeparator:hover {
+         background-color: rgba(127, 153, 204, 0.5);
+    }
+
+    .Marquee {
+         background-color: rgba(224, 224, 224, 0.7);
+    }
 
+""" % ({'trimbar_normal': os.path.join(get_pixmap_dir(), "trimbar-normal.png"),
+        'trimbar_focused': os.path.join(get_pixmap_dir(), "trimbar-focused.png")})
 # ---------------------- ARGB color helper-------------------------------------#
 
+
 def argb_to_gdk_rgba(color_int):
     return Gdk.RGBA(color_int / 256 ** 2 % 256 / 255.,
                     color_int / 256 ** 1 % 256 / 255.,
@@ -164,9 +227,6 @@ def hex_to_rgb(value):
 
 
 def set_cairo_color(context, color):
-    if type(color) is Clutter.Color:
-        color = (color.red, color.green, color.blue)
-
     if type(color) is Gdk.RGBA:
         cairo_color = (float(color.red), float(color.green), float(color.blue))
     elif type(color) is tuple:
@@ -401,6 +461,22 @@ def alter_style_class(style_class, target_widget, css_style):
         css_provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION)
 
 
+def set_children_state_recurse(widget, state):
+    widget.set_state_flags(state, False)
+    for child in widget.get_children():
+        child.set_state_flags(state, False)
+        if isinstance(child, Gtk.Container):
+            set_children_state_recurse(child, state)
+
+
+def unset_children_state_recurse(widget, state):
+    widget.unset_state_flags(state)
+    for child in widget.get_children():
+        child.unset_state_flags(state)
+        if isinstance(child, Gtk.Container):
+            unset_children_state_recurse(child, state)
+
+
 # ----------------------- encoding datas --------------------------------------- #
 # FIXME This should into a special file
 frame_rates = model((str, object), (
diff --git a/pitivi/utils/validate.py b/pitivi/utils/validate.py
index 7b8575e..3583082 100644
--- a/pitivi/utils/validate.py
+++ b/pitivi/utils/validate.py
@@ -19,17 +19,146 @@
 # Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
 # Boston, MA 02110-1301, USA.
 import sys
+
+from gi.repository import Gst
 from gi.repository import GES
+from gi.repository import Gdk
+from gi.repository import GLib
+
+from pitivi.utils import ui
+from pitivi.utils import timeline as timelineUtils
+
+try:
+    from gi.repository import GstValidate
+except ImportError:
+    GstValidate = None
 
 has_validate = False
 
 
 def stop(scenario, action):
-    sys.stdout.write("STOP action, not doing anything in pitivi")
-    sys.stdout.flush()
+    if action.structure.get_boolean("force")[0]:
+        timeline = scenario.pipeline.props.timeline
+        project = timeline.get_asset()
+
+        if project:
+            project.setModificationState(False)
+            GstValidate.print_action(action, "Force quiting, ignoring any"
+                                     " changes in the project")
+        timeline.ui.app.shutdown()
+
+        return 1
+
+    GstValidate.print_action(action, "not doing anything in pitivi")
+
+    return 1
+
+
+def editContainer(scenario, action):
+    # edit-container, edge=(string)edge_end, position=(double)2.2340325289999998, 
edit-mode=(string)edit_trim, container-name=(string)uriclip0, new-layer-priority=(int)-1;
+    timeline = scenario.pipeline.props.timeline
+    container = timeline.get_element(action.structure["container-name"])
+
+    try:
+        res, edge = GstValidate.utils_enum_from_str(GES.Edge, action.structure["edge"])
+        if not res:
+            edge = GES.Edge.EDGE_NONE
+        else:
+            edge = GES.Edge(edge)
+    except KeyError:
+        edge = GES.Edge.EDGE_NONE
+
+    res, position = GstValidate.action_get_clocktime(scenario, action, "position")
+    layer_prio = action.structure["new-layer-priority"]
+
+    if res is False:
+        return 0
+
+    container_ui = container.ui
+
+    y = 21
+    if container.get_layer().get_priority() != layer_prio:
+        try:
+            layer = timeline.get_layers()[layer_prio]
+            y = layer.ui.get_allocation().y - container_ui.translate_coordinates(timeline.ui, 0, 0)[1]
+            if y < 0:
+                y += 21
+            else:
+                y -= 21
+        except IndexError:
+            if layer_prio == -1:
+                y = -5
+            else:
+                layer = timeline.get_layers()[-1]
+                alloc = layer.ui.get_allocation()
+                y = alloc.y + alloc.height + 10 - container_ui.translate_coordinates(timeline.ui, 0, 0)[1]
+
+    if not hasattr(scenario, "dragging") or scenario.dragging is False:
+        if isinstance(container, GES.SourceClip):
+            if edge == GES.Edge.EDGE_START:
+                container.ui.leftHandle._eventCb(Gdk.Event.new(Gdk.Event.ENTER_NOTIFY))
+            elif edge == GES.Edge.EDGE_END:
+                container.ui.leftHandle._eventCb(Gdk.Event.new(Gdk.Event.ENTER_NOTIFY))
+
+        scenario.dragging = True
+        event = Gdk.EventButton.new(Gdk.EventType.BUTTON_PRESS)
+        event.button = 1
+        event.y = y
+        container.ui.sendFakeEvent(event, container.ui)
+
+    event = Gdk.EventMotion.new(Gdk.EventType.MOTION_NOTIFY)
+    event.button = 1
+    event.x = timelineUtils.Zoomable.nsToPixelAccurate(position) - 
container_ui.translate_coordinates(timeline.ui, 0, 0)[0] + ui.CONTROL_WIDTH
+    event.y = y
+    event.state = Gdk.ModifierType.BUTTON1_MASK
+    container.ui.sendFakeEvent(event, container.ui)
+
+    GstValidate.print_action(action, "Editing %s to %s in %s mode, edge: %s "
+          "with new layer prio: %d\n" % (action.structure["container-name"],
+                                         Gst.TIME_ARGS(position),
+                                         timeline.ui.draggingElement.edit_mode,
+                                         timeline.ui.draggingElement.dragging_edge,
+                                         layer_prio))
+
+    next_action = scenario.get_next_action()
+    if next_action is None or next_action.type != "edit-container":
+        scenario.dragging = False
+        event = Gdk.EventButton.new(Gdk.EventType.BUTTON_RELEASE)
+        event.button = 1
+        event.x = timelineUtils.Zoomable.nsToPixelAccurate(position)
+        event.y = y
+        container.ui.sendFakeEvent(event, container.ui)
+
+        if isinstance(container, GES.SourceClip):
+            if edge == GES.Edge.EDGE_START:
+                container.ui.leftHandle._eventCb(Gdk.Event.new(Gdk.Event.LEAVE_NOTIFY))
+            if edge == GES.Edge.EDGE_END:
+                container.ui.leftHandle._eventCb(Gdk.Event.new(Gdk.Event.LEAVE_NOTIFY))
+
+        if container.get_layer().get_priority() != layer_prio:
+            scenario.report_simple(GLib.quark_from_string("scenario::execution-error"),
+                                   "Resulting clip priority: %s"
+                                   " is not the same as the wanted one: %s"
+                                   % (container.get_layer().get_priority(),
+                                      layer_prio))
+
     return 1
 
 
+def splitClip(scenario, action):
+    timeline = scenario.pipeline.props.timeline.ui
+    timeline.parent._splitCb(None)
+
+    return True
+
+
+def setZoomFit(scenario, action):
+    timeline = scenario.pipeline.props.timeline.ui
+    timeline.parent.zoomFit()
+
+    return True
+
+
 def init():
     global has_validate
     try:
@@ -40,5 +169,19 @@ def init():
                                          stop, None,
                                          "Pitivi override for the stop action",
                                          GstValidate.ActionTypeFlags.NONE)
+
+        GstValidate.register_action_type("edit-container", "pitivi",
+                                         editContainer, None,
+                                         "Start dragging a clip in the timeline",
+                                         GstValidate.ActionTypeFlags.NONE)
+
+        GstValidate.register_action_type("split-clip", "pitivi",
+                                         splitClip, None,
+                                         "Split a clip",
+                                         GstValidate.ActionTypeFlags.NONE)
+        GstValidate.register_action_type("set-zoom-fit", "pitivi",
+                                         setZoomFit, None,
+                                         "Split a clip",
+                                         GstValidate.ActionTypeFlags.NO_EXECUTION_NOT_FATAL)
     except ImportError:
         has_validate = False
diff --git a/pitivi/utils/widgets.py b/pitivi/utils/widgets.py
index c4ea608..645ebf8 100644
--- a/pitivi/utils/widgets.py
+++ b/pitivi/utils/widgets.py
@@ -233,7 +233,6 @@ class NumericWidget(Gtk.Box, DynamicWidget):
             upper = GObject.G_MAXDOUBLE
         if lower is None:
             lower = GObject.G_MINDOUBLE
-        range = upper - lower
         self.adjustment.props.lower = lower
         self.adjustment.props.upper = upper
         self.spinner = Gtk.SpinButton(adjustment=self.adjustment)
@@ -1028,6 +1027,7 @@ class ZoomBox(Gtk.Grid, Zoomable):
         Gtk.Grid.__init__(self)
         Zoomable.__init__(self)
 
+        self._manual_set = False
         self.timeline = timeline
 
         zoom_fit_btn = Gtk.Button()
@@ -1073,7 +1073,9 @@ class ZoomBox(Gtk.Grid, Zoomable):
 
     def _zoomAdjustmentChangedCb(self, adjustment):
         Zoomable.setZoomLevel(adjustment.get_value())
-        self.timeline._scrollToPlayhead()
+
+        if self._manual_set is False:
+            self.timeline.scrollToPlayhead()
 
     def _zoomFitCb(self, unused_button):
         self.timeline.zoomFit()
@@ -1096,7 +1098,9 @@ class ZoomBox(Gtk.Grid, Zoomable):
     def zoomChanged(self):
         zoomLevel = self.getCurrentZoomLevel()
         if int(self._zoomAdjustment.get_value()) != zoomLevel:
+            self._manual_set = True
             self._zoomAdjustment.set_value(zoomLevel)
+            self._manual_set = False
 
     def _sliderTooltipCb(self, unused_slider, unused_x, unused_y, unused_keyboard_mode, tooltip):
         # We assume the width of the ruler is exactly the width of the
diff --git a/pitivi/viewer.py b/pitivi/viewer.py
index 2f0a427..5fa035d 100644
--- a/pitivi/viewer.py
+++ b/pitivi/viewer.py
@@ -19,9 +19,7 @@
 # Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
 # Boston, MA 02110-1301, USA.
 
-from gi.repository import Clutter
 from gi.repository import Gtk
-from gi.repository import GtkClutter
 from gi.repository import Gdk
 from gi.repository import Gst
 from gi.repository import GObject
@@ -415,22 +413,22 @@ class ViewerContainer(Gtk.Box, Loggable):
         """
         self.timecode_entry.setWidgetValue(position, False)
 
-    def clipTrimPreview(self, tl_obj, position):
+    def clipTrimPreview(self, clip, position):
         """
         While a clip is being trimmed, show a live preview of it.
         """
-        if isinstance(tl_obj, GES.TitleClip) or tl_obj.props.is_image or not hasattr(tl_obj, "get_uri"):
+        if isinstance(clip, GES.TitleClip) or clip.props.is_image or not hasattr(clip, "get_uri"):
             self.log(
-                "%s is an image or has no URI, so not previewing trim" % tl_obj)
+                "%s is an image or has no URI, so not previewing trim" % clip)
             return False
 
-        clip_uri = tl_obj.props.uri
+        clip_uri = clip.props.uri
         cur_time = time()
         if self.pipeline == self.app.project_manager.current_project.pipeline:
             self.debug("Creating temporary pipeline for clip %s, position %s",
                        clip_uri, format_ns(position))
             self._oldTimelinePos = self.pipeline.getPosition()
-            self.setPipeline(AssetPipeline(tl_obj))
+            self.setPipeline(AssetPipeline(clip))
             self._lastClipTrimTime = cur_time
 
         if (cur_time - self._lastClipTrimTime) > 0.2 and self.pipeline.getState() == Gst.State.PAUSED:
diff --git a/tests/test_utils.py b/tests/test_utils.py
index 9ecfb16..b2b2029 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -70,11 +70,11 @@ class TestDependencyChecks(TestCase):
         gi_dep.check()
         self.assertFalse(gi_dep.satisfied)
 
-        gi_dep = GtkOrClutterDependency("Gtk", "3.0.0")
+        gi_dep = GtkDependency("Gtk", "3.0.0")
         gi_dep.check()
         self.assertTrue(gi_dep.satisfied)
 
-        gi_dep = GtkOrClutterDependency("Gtk", "9.9.9")
+        gi_dep = GtkDependency("Gtk", "9.9.9")
         gi_dep.check()
         self.assertFalse(gi_dep.satisfied)
 


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