[pitivi] Begins the integration of the new ClutterTimeline



commit 2b8b71fb20c433105312afbdf8558fe921b76838
Author: Mathieu Duponchelle <mathieu duponchelle epitech eu>
Date:   Mon Apr 15 04:48:28 2013 +0200

    Begins the integration of the new ClutterTimeline

 pitivi/mainwindow.py           |    2 +-
 pitivi/timeline/layer.py       |   12 +-
 pitivi/timeline/thumbnailer.py |  871 -------------
 pitivi/timeline/timeline.py    | 2751 ++++++++++++++++++++--------------------
 pitivi/timeline/track.py       |  858 -------------
 pitivi/utils/timeline.py       |   34 +
 6 files changed, 1450 insertions(+), 3078 deletions(-)
---
diff --git a/pitivi/mainwindow.py b/pitivi/mainwindow.py
index febb05e..0e86690 100644
--- a/pitivi/mainwindow.py
+++ b/pitivi/mainwindow.py
@@ -967,7 +967,7 @@ class PitiviMainWindow(Gtk.Window, Loggable):
         if project.pipeline is not None:
             project.pipeline.deactivatePositionListener()
 
-        self.timeline_ui.timeline = None
+        self.timeline_ui.bTimeline = None
         self.clipconfig.timeline = None
         return False
 
diff --git a/pitivi/timeline/layer.py b/pitivi/timeline/layer.py
index 965b4d1..b518998 100644
--- a/pitivi/timeline/layer.py
+++ b/pitivi/timeline/layer.py
@@ -86,7 +86,7 @@ class BaseLayerControl(Gtk.VBox, Loggable):
         self.name_entry.connect("focus-in-event", self._focusChangeCb, False)
         self.name_entry.connect("focus-out-event", self._focusChangeCb, True)
         self.name_entry.connect("button_press_event", self._buttonPressCb)
-        self.name_entry.drag_dest_unset()
+#        self.name_entry.drag_dest_unset()
         self.name_entry.set_sensitive(False)
 
         # 'Solo' toggle button
@@ -153,9 +153,9 @@ class BaseLayerControl(Gtk.VBox, Loggable):
         self.popup.show_all()
 
         # Drag and drop
-        self.drag_source_set(Gdk.ModifierType.BUTTON1_MASK,
-                             [LAYER_CONTROL_TARGET_ENTRY],
-                             Gdk.DragAction.MOVE)
+#        self.drag_source_set(Gdk.ModifierType.BUTTON1_MASK,
+#                             [LAYER_CONTROL_TARGET_ENTRY],
+#                             Gdk.DragAction.MOVE)
 
     def getSelected(self):
         return self._selected
@@ -197,7 +197,7 @@ class BaseLayerControl(Gtk.VBox, Loggable):
         """
         Look if user selected layer or wants popup menu
         """
-        self._app.gui.timeline_ui.controls.selectLayerControl(self)
+        self._app.selectLayerControl(self)
         if event.button == 3:
             self.popup.popup(None, None, None, None, event.button, event.time)
 
@@ -357,7 +357,7 @@ class TwoStateButton(Gtk.Button):
     def set_states(self, state1, state2):
         self.states = {True: state1, False: state2}
 
-    def  _clickedCb(self, widget):
+    def _clickedCb(self, widget):
         self._state = not self._state
 
         self.set_label(self.states[self._state])
diff --git a/pitivi/timeline/timeline.py b/pitivi/timeline/timeline.py
index d64303a..c510979 100644
--- a/pitivi/timeline/timeline.py
+++ b/pitivi/timeline/timeline.py
@@ -1,69 +1,41 @@
-# PiTiVi , Non-linear video editor
-#
-#       pitivi/timeline/timeline.py
-#
-# Copyright (c) 2005, Edward Hervey <bilboed bilboed com>
-# Copyright (c) 2009, Brandon Lewis <brandon_lewis berkeley edu>
-#
-# This program is free software; you can redistribute it and/or
-# modify it under the terms of the GNU Lesser General Public
-# License as published by the Free Software Foundation; either
-# version 2.1 of the License, or (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
-# Lesser General Public License for more details.
-#
-# You should have received a copy of the GNU Lesser General Public
-# License along with this program; if not, write to the
-# Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
-# Boston, MA 02110-1301, USA.
-
-"""
-    Main Timeline widgets
-"""
-
-import sys
-import time
-
-from gi.repository import Gtk
+from gi.repository import GtkClutter
+GtkClutter.init([])
 from gi.repository import Gst
 from gi.repository import GES
 from gi.repository import GObject
-from gi.repository import GooCanvas
 
-from gi.repository import Gdk
+import collections
+import hashlib
+import os
+import sqlite3
+import sys
+import xdg.BaseDirectory as xdg_dirs
+
+from gi.repository import Clutter, GObject, Gtk, Cogl
+
 from gi.repository import GLib
+from gi.repository import Gdk
+from gi.repository import GdkPixbuf
 
-from gettext import gettext as _
-from os.path import join
-
-from pitivi.timeline import ruler
-from pitivi.check import missing_soft_deps
-from pitivi.effects import AUDIO_EFFECT, VIDEO_EFFECT
-from pitivi.autoaligner import AlignmentProgressDialog
-from pitivi.utils.misc import quote_uri, print_ns
-from pitivi.utils.pipeline import PipelineError
-from pitivi.settings import GlobalSettings
+import pitivi.configure as configure
 
-from track import Track, TrackElement
-from layer import VideoLayerControl, AudioLayerControl
-from pitivi.utils.timeline import EditingContext, SELECT, Zoomable
+from pitivi.viewer import ViewerWidget
+
+from pitivi.utils.timeline import Zoomable, EditingContext, Selection, SELECT, UNSELECT, Selected
+
+from pitivi.settings import GlobalSettings
 
-from pitivi.dialogs.depsmanager import DepsManager
-from pitivi.dialogs.filelisterrordialog import FileListErrorDialog
 from pitivi.dialogs.prefs import PreferencesDialog
 
-from pitivi.utils.receiver import receiver, handler
-from pitivi.utils.loggable import Loggable
-from pitivi.utils.ui import SPACING, CANVAS_SPACING, \
-    TYPE_PITIVI_FILESOURCE, VIDEO_EFFECT_TARGET_ENTRY, Point, \
-    AUDIO_EFFECT_TARGET_ENTRY, EFFECT_TARGET_ENTRY, FILESOURCE_TARGET_ENTRY, TYPE_PITIVI_EFFECT, \
-    LAYER_CREATION_BLOCK_TIME, LAYER_CONTROL_TARGET_ENTRY
+from ruler import ScaleRuler
 
-# FIXME GES Port regression
-# from pitivi.utils.align import AutoAligner
+from datetime import datetime
+
+from gettext import gettext as _
+
+from pitivi.utils.pipeline import Pipeline
+
+from layer import VideoLayerControl, AudioLayerControl
 
 GlobalSettings.addConfigOption('edgeSnapDeadband',
     section="user-interface",
@@ -90,18 +62,13 @@ PreferencesDialog.addNumericPreference('imageClipLength',
     description=_("Default clip length (in miliseconds) of images when inserting on the timeline."),
     lower=1)
 
+# CONSTANTS
 
-# cursors to be used for resizing objects
-ARROW = Gdk.Cursor.new(Gdk.CursorType.ARROW)
-# TODO: replace this with custom cursor
-PLAYHEAD_CURSOR = Gdk.Cursor.new(Gdk.CursorType.SB_H_DOUBLE_ARROW)
+EXPANDED_SIZE = 65
+SPACING = 10
+CONTROL_WIDTH = 250
 
-# Drag and drop constants/tuples
-# FIXME, rethink the way we handle that as it is quite 'hacky'
-DND_EFFECT_LIST = [[VIDEO_EFFECT_TARGET_ENTRY.target, EFFECT_TARGET_ENTRY.target],
-                  [AUDIO_EFFECT_TARGET_ENTRY.target, EFFECT_TARGET_ENTRY.target]]
-VIDEO_EFFECT_LIST = [VIDEO_EFFECT_TARGET_ENTRY.target, EFFECT_TARGET_ENTRY.target],
-AUDIO_EFFECT_LIST = [AUDIO_EFFECT_TARGET_ENTRY.target, EFFECT_TARGET_ENTRY.target],
+BORDER_WIDTH = 3  # For the timeline elements
 
 # tooltip text for toolbar
 DELETE = _("Delete Selected")
@@ -166,890 +133,954 @@ ui = '''
 '''
 
 
-class TimelineCanvas(GooCanvas.Canvas, Zoomable, Loggable):
+"""
+Convention throughout this file:
+Every GES element which name could be mistaken with a UI element
+is prefixed with a little b, example : bTimeline
+"""
+
+
+class RoundedRectangle(Clutter.Actor):
     """
-        The GooCanvas widget representing the timeline
+    Custom actor used to draw a rectangle that can have rounded corners
     """
+    __gtype_name__ = 'RoundedRectangle'
 
-    __gtype_name__ = 'TimelineCanvas'
+    def __init__(self, width, height, arc, step,
+                 color=None, border_color=None, border_width=0):
+        """
+        Creates a new rounded rectangle
+        """
+        Clutter.Actor.__init__(self)
+        self.props.width = width
+        self.props.height = height
+        self._arc = arc
+        self._step = step
+        self._border_width = border_width
+        self._color = color
+        self._border_color = border_color
+
+    def do_paint(self):
+        # Set a rectangle for the clipping
+        Cogl.clip_push_rectangle(0, 0, self.props.width, self.props.height)
+
+        if self._border_color:
+            # draw the rectangle for the border which is the same size as the
+            # object
+            Cogl.path_round_rectangle(0, 0, self.props.width, self.props.height,
+                                      self._arc, self._step)
+            Cogl.path_round_rectangle(self._border_width, self._border_width,
+                                      self.props.width - self._border_width,
+                                      self.props.height - self._border_width,
+                                      self._arc, self._step)
+            Cogl.path_set_fill_rule(Cogl.PathFillRule.EVEN_ODD)
+            Cogl.path_close()
+
+            # set color to border color
+            Cogl.set_source_color(self._border_color)
+            Cogl.path_fill()
+
+        if self._color:
+            # draw the content with is the same size minus the width of the border
+            # finish the clip
+            Cogl.path_round_rectangle(self._border_width, self._border_width,
+                                      self.props.width - self._border_width,
+                                      self.props.height - self._border_width,
+                                      self._arc, self._step)
+            Cogl.path_close()
+
+            # set the color of the filled area
+            Cogl.set_source_color(self._color)
+            Cogl.path_fill()
+
+        Cogl.clip_pop()
+
+    def get_color(self):
+        return self._color
+
+    def set_color(self, color):
+        self._color = color
+        self.queue_redraw()
+
+    def get_border_width(self):
+        return self._border_width
+
+    def set_border_width(self, width):
+        self._border_width = width
+        self.queue_redraw()
+
+    def get_border_color(color):
+        return self._border_color
+
+    def set_border_color(self, color):
+        self._border_color = color
+        self.queue_redraw()
+
+
+class TrimHandle(Clutter.Texture):
+    def __init__(self, timelineElement, isLeft):
+        Clutter.Texture.__init__(self)
+        self.set_from_file(os.path.join(configure.get_pixmap_dir(), "trimbar-normal.png"))
+        self.isLeft = isLeft
+        self.set_size(-1, EXPANDED_SIZE)
+        self.hide()
 
-    _tracks = None
+        self.isSelected = False
 
-    def __init__(self, instance, timeline=None):
-        GooCanvas.Canvas.__init__(self)
-        Zoomable.__init__(self)
-        Loggable.__init__(self)
-        self.app = instance
-        self._tracks = []
-        self.height = CANVAS_SPACING
-
-        self._block_size_request = False
-        self.props.integer_layout = True
-        self.props.automatic_bounds = False
-        self.props.clear_background = False
-        self.get_root_item().set_simple_transform(0, 2.0, 1.0, 0)
-
-        self._createUI()
-        self._timeline = timeline
-        self.settings = instance.settings
-        self.connect("draw", self.drawCb)
-
-    def _createUI(self):
-        self._cursor = ARROW
-        root = self.get_root_item()
-        self.tracks = GooCanvas.CanvasGroup()
-        self.tracks.set_simple_transform(0, 0, 1.0, 0)
-        root.add_child(self.tracks, -1)
-        self._marquee = GooCanvas.CanvasRect(
-            parent=root,
-            stroke_color_rgba=0x33CCFF66,
-            fill_color_rgba=0x33CCFF66,
-            visibility=GooCanvas.CanvasItemVisibility.INVISIBLE)
-        self._playhead = GooCanvas.CanvasRect(
-            y=-10,
-            parent=root,
-            line_width=1,
-            fill_color_rgba=0x000000FF,
-            stroke_color_rgba=0xFFFFFFFF,
-            width=3)
-        self._snap_indicator = GooCanvas.CanvasRect(
-            parent=root, x=0, y=0, width=3, line_width=0.5,
-            fill_color_rgba=0x85c0e6FF,
-            stroke_color_rgba=0x294f95FF)
-        self.connect("size-allocate", self._size_allocate_cb)
-        root.connect("motion-notify-event", self._selectionDrag)
-        root.connect("button-press-event", self._selectionStart)
-        root.connect("button-release-event", self._selectionEnd)
-        self.connect("button-release-event", self._buttonReleasedCb)
-        # add some padding for the horizontal scrollbar
-        self.set_size_request(-1, self.height)
-
-    def from_event(self, event):
-        x, y = event.x, event.y
-        x += self.app.gui.timeline_ui.hadj.get_value()
-        return Point(*self.convert_from_pixels(x, y))
-
-    def setExpanded(self, track_element, expanded):
-        track_ui = None
-        for track in self._tracks:
-            if track.track == track_element:
-                track_ui = track
-                break
+        self.timelineElement = timelineElement
 
-        track_ui.setExpanded(expanded)
+        self.set_reactive(True)
 
-## sets the cursor as appropriate
+        self.dragAction = Clutter.DragAction()
+        self.add_action(self.dragAction)
 
-    def _mouseEnterCb(self, unused_item, unused_target, event):
-        event.window.set_cursor(self._cursor)
-        return True
+        self.dragAction.connect("drag-begin", self._dragBeginCb)
+        self.dragAction.connect("drag-end", self._dragEndCb)
+        self.dragAction.connect("drag-progress", self._dragProgressCb)
 
-    def drawCb(self, widget, cr):
-        allocation = self.get_allocation()
-        width = allocation.width
-        height = allocation.height
-        # draw the canvas background
-        # we must have props.clear_background set to False
-
-        # FIXME GObject Introspection -> move to Gtk.StyleContext
-        #self.style.apply_default_background(event.window,
-            #True,
-            #Gtk.StateType.ACTIVE,
-            #event.area,
-            #event.area.x, event.area.y,
-            #event.area.width, event.area.height)
-
-        #GooCanvas.Canvas.do_expose_event(self, event)
-
-## implements selection marquee
-
-    _selecting = False
-    _mousedown = None
-    _marquee = None
-    _got_motion_notify = False
-
-    def getItemsInArea(self, x1, y1, x2, y2):
-        '''
-        Permits to get the Non UI L{Track}/L{TrackElement} in a list of set
-        corresponding to the L{Track}/L{TrackElement} which are in the are
-
-        @param x1: The horizontal coordinate of the up left corner of the area
-        @type x1: An C{int}
-        @param y1: The vertical coordinate of the up left corner of the area
-        @type y1: An C{int}
-        @param x2: The horizontal coordinate of the down right area corner
-        @type x2: An C{int}
-        @param x2: The vertical coordinate of the down right corner of the area
-        @type x2: An C{int}
-
-        @returns: A list of L{Track}, L{TrackElement} tuples
-        '''
-        bounds = GooCanvas.CanvasBounds()
-        bounds.x1 = x1
-        bounds.x2 = x2
-        bounds.y1 = y1
-        bounds.y2 = y2
-        items = self.get_items_in_area(bounds, True, True, True)
-        if not items:
-            return [], []
-
-        tracks = set()
-        track_elements = set()
-
-        for item in items:
-            if isinstance(item, Track):
-                tracks.add(item.track)
-            elif isinstance(item, TrackElement):
-                track_elements.add(item.element)
-
-        return tracks, track_elements
-
-    def _normalize(self, p1, p2):
-        w, h = p1 - p2
-        w = abs(w)
-        h = abs(h)
-        x = min(p1[0], p2[0])
-        y = min(p1[1], p2[1])
-        return (x, y), (w, h)
-
-    def _get_adjustment(self, xadj=True, yadj=True):
-        return Point(self.app.gui.timeline_ui.hadj.get_value() * xadj,
-                     self.app.gui.timeline_ui.vadj.get_value() * yadj)
-
-    def _selectionDrag(self, item, target, event):
-        if self._selecting:
-            self._got_motion_notify = True
-            cur = self.from_event(event) - self._get_adjustment(True, False)
-            pos, size = self._normalize(self._mousedown, cur)
-            self._marquee.props.x, self._marquee.props.y = pos
-            self._marquee.props.width, self._marquee.props.height = size
-            return True
-        return False
+        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)
+        self.timelineElement.bElement.selected.connect("selected-changed", self._selectedChangedCb)
 
-    def _selectionStart(self, item, target, event):
-        self._selecting = True
-        self._marquee.props.visibility = GooCanvas.CanvasItemVisibility.VISIBLE
-        self._mousedown = self.from_event(event) + self._get_adjustment(False, True)
-        self._marquee.props.width = 0
-        self._marquee.props.height = 0
-        self.pointer_grab(self.get_root_item(), Gdk.EventMask.POINTER_MOTION_MASK |
-            Gdk.EventMask.BUTTON_RELEASE_MASK, self._cursor, event.time)
-        return True
+    #Callbacks
 
-    def _selectionEnd(self, item, target, event):
-        self.pointer_ungrab(self.get_root_item(), event.time)
-        self._selecting = False
-        self._marquee.props.visibility = GooCanvas.CanvasItemVisibility.INVISIBLE
-        if not self._got_motion_notify:
-            self._timeline.selection.setSelection([], 0)
-            self.app.current.seeker.seek(Zoomable.pixelToNs(event.x))
-        elif self._timeline is not None:
-            self._got_motion_notify = False
-            mode = 0
-            if event.get_state()[1] & Gdk.ModifierType.SHIFT_MASK:
-                mode = 1
-            if event.get_state()[1] & Gdk.ModifierType.CONTROL_MASK:
-                mode = 2
-            selected = self._elementsUnderMarquee()
-            self._timeline.selection.setSelection(self._elementsUnderMarquee(), mode)
-        return True
+    def _enterEventCb(self, actor, 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(Gdk.Cursor.new(Gdk.CursorType.LEFT_SIDE))
+        else:
+            
self.timelineElement.timeline._container.embed.get_window().set_cursor(Gdk.Cursor.new(Gdk.CursorType.RIGHT_SIDE))
 
-    def _elementsUnderMarquee(self):
-        items = self.get_items_in_area(self._marquee.get_bounds(), True, True, True)
-        if items:
-            return set((item.element for item in items if isinstance(item,
-                TrackElement) and item.bg in items))
-        return set()
+    def _leaveEventCb(self, actor, event):
+        self.timelineElement.set_reactive(True)
+        for elem in self.timelineElement.get_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(Gdk.Cursor.new(Gdk.CursorType.ARROW))
 
-## playhead implementation
+    def _elementEnterEventCb(self, actor, event):
+        self.show()
 
-    position = 0
+    def _elementLeaveEventCb(self, actor, event):
+        if not self.isSelected:
+            self.hide()
 
-    def timelinePositionChanged(self, position):
-        self.position = position
-        self._playhead.props.x = self.nsToPixel(position)
+    def _selectedChangedCb(self, selected, isSelected):
+        self.isSelected = isSelected
+        self.props.visible = isSelected
 
-    max_duration = 0
+    def _dragBeginCb(self, action, actor, event_x, event_y, modifiers):
+        self.timelineElement.setDragged(True)
+        elem = self.timelineElement.bElement.get_parent()
 
-    def setMaxDuration(self, duration):
-        self.max_duration = duration
-        self._request_size()
+        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()
 
-    def _request_size(self):
-        alloc = self.get_allocation()
-        self.set_bounds(0, 0, alloc.width, alloc.height)
-        self._playhead.props.height = max(0, self.height + SPACING)
+        self._context = EditingContext(elem,
+                                       self.timelineElement.timeline.bTimeline,
+                                       GES.EditMode.EDIT_TRIM,
+                                       edge,
+                                       set([]),
+                                       None)
 
-    def _size_allocate_cb(self, widget, allocation):
-        self._request_size()
+        self.dragBeginStartX = event_x
+        self.dragBeginStartY = event_y
 
-    def zoomChanged(self):
-        self.queue_draw()
+    def _dragProgressCb(self, action, actor, delta_x, 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)
 
-## snapping indicator
-    def _snapCb(self, unused_timeline, obj1, obj2, position):
+        self._context.editTo(new_start, 
self.timelineElement.bElement.get_parent().get_layer().get_priority())
+        return False
+
+    def _dragEndCb(self, action, actor, event_x, event_y, modifiers):
+        self.timelineElement.setDragged(False)
+        self._context.finish()
+        self.timelineElement.set_reactive(True)
+        for elem in self.timelineElement.get_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(Gdk.Cursor.new(Gdk.CursorType.ARROW))
+
+
+class TimelineElement(Clutter.Actor, Zoomable):
+    def __init__(self, bElement, track, timeline):
         """
-        Display or hide a snapping indicator line
+        @param bElement : the backend GES.TrackElement
+        @param track : the track to which the bElement belongs
+        @param timeline : the containing graphic timeline.
         """
-        if position == 0:
-            self._snapEndedCb()
-        else:
-            self.debug("Snapping indicator at %d" % position)
-            self._snap_indicator.props.x = Zoomable.nsToPixel(position)
-            self._snap_indicator.props.height = self.height
-            self._snap_indicator.props.visibility = GooCanvas.CanvasItemVisibility.VISIBLE
+        Zoomable.__init__(self)
+        Clutter.Actor.__init__(self)
 
-    def _snapEndedCb(self, *args):
-        self._snap_indicator.props.visibility = GooCanvas.CanvasItemVisibility.INVISIBLE
+        self.timeline = timeline
 
-    def _buttonReleasedCb(self, canvas, event):
-        # select clicked layer, if any
-        x, y = self.from_event(event) + self._get_adjustment(True, True)
-        self.app.gui.timeline_ui.controls.selectLayerControlForY(y)
+        self.bElement = bElement
 
-        # also hide snap indicator
-        self._snapEndedCb()
+        self.bElement.selected = Selected()
+        self.bElement.selected.connect("selected-changed", self._selectedChangedCb)
 
-## settings callbacks
-    def _setSettings(self):
-        self.zoomChanged()
+        self._createBackground(track)
 
-    settings = receiver(_setSettings)
+        self._createPreview()
 
-    @handler(settings, "edgeSnapDeadbandChanged")
-    def _edgeSnapDeadbandChangedCb(self, settings):
-        self.zoomChanged()
+        self._createBorder()
 
-## Timeline callbacks
+        self._createMarquee()
 
-    def setTimeline(self, timeline):
-        while self._tracks:
-            self._trackRemovedCb(None, 0)
+        self._createHandles()
 
-        if self._timeline is not None:
-            self._timeline.disconnect_by_func(self._trackAddedCb)
-            self._timeline.disconnect_by_func(self._trackRemovedCb)
-            self._timeline.disconnect_by_func(self._snapCb)
-            self._timeline.disconnect_by_func(self._snapEndedCb)
+        self._createGhostclip()
 
-        self._timeline = timeline
-        if self._timeline is not None:
-            for track in self._timeline.get_tracks():
-                self._trackAddedCb(None, track)
+        self.track_type = self.bElement.get_track_type()  # This won't change
 
-            self._timeline.connect("track-added", self._trackAddedCb)
-            self._timeline.connect("track-removed", self._trackRemovedCb)
-            self._timeline.connect("snapping-started", self._snapCb)
-            self._timeline.connect("snapping-ended", self._snapEndedCb)
+        self.isDragged = False
 
-        self.zoomChanged()
+        size = self.bElement.get_duration()
+        self.set_size(self.nsToPixel(size), EXPANDED_SIZE, False)
+        self.set_reactive(True)
+        self._connectToEvents()
 
-    def getTimeline(self):
-        return self._timeline
+    # Public API
 
-    timeline = property(getTimeline, setTimeline, None, "The timeline property")
+    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)
+            self.rightHandle.save_easing_state()
+            self.rightHandle.set_easing_duration(600)
 
-    def _trackAddedCb(self, timeline, track):
-        track = Track(self.app, track, self._timeline)
-        self._tracks.append(track)
-        track.set_canvas(self)
-        self.tracks.add_child(track, -1)
-        self.regroupTracks()
-
-    def _trackRemovedCb(self, unused_timeline, position):
-        track = self._tracks[position]
-        del self._tracks[position]
-        track.remove()
-        self.regroupTracks()
-
-    def regroupTracks(self):
-        """
-        Make it so we have a real differentiation between the Audio tracks
-        and video tracks
-        This method should be called each time a change happen in the timeline
-        """
-        height = 0
-        for i, track in enumerate(self._tracks):
-            track.set_simple_transform(0, height, 1, 0)
-            height += track.height
-        self.height = height
-        self._request_size()
+        self.marquee.set_size(width, height)
+        self.background.props.width = width
+        self.background.props.height = height
+        self.border.props.width = width
+        self.border.props.height = height
+        self.props.width = width
+        self.props.height = height
+        self.preview.set_size(width, height)
+        self.rightHandle.set_position(width - self.rightHandle.props.width, 0)
 
-    def updateTracks(self):
-        self.debug("Updating all TrackElements")
-        for track in self._tracks:
-            track.updateTrackElements()
+        if ease:
+            self.background.restore_easing_state()
+            self.border.restore_easing_state()
+            self.preview.restore_easing_state()
+            self.rightHandle.restore_easing_state()
+            self.restore_easing_state()
 
+    def updateGhostclip(self, priority, y, isControlledByBrother):
+        # Only tricky part of the code, can be called by the linked track element.
+        if priority < 0:
+            return
 
-class TimelineControls(Gtk.VBox, Loggable):
-    """
-    Holds and manages the LayerControlWidgets
-    """
+        # 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)
 
-    __gsignals__ = {
-        "selection-changed": (GObject.SignalFlags.RUN_LAST, None, (GObject.TYPE_PYOBJECT,),)
-    }
+        # 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 ?
+        if priority == self.nbrLayers:
+            self.ghostclip.set_size(self.props.width, SPACING)
+            self.ghostclip.props.y = priority * (EXPANDED_SIZE + SPACING)
+            if self.track_type == GES.TrackType.AUDIO:
+                self.ghostclip.props.y += self.nbrLayers * (EXPANDED_SIZE + SPACING)
+            self.ghostclip.props.visible = True
+        else:
+            # No need to mockup on the same layer
+            if priority == self.bElement.get_parent().get_layer().get_priority():
+                self.ghostclip.props.visible = False
+            # We would be moving to an existing layer.
+            elif priority < self.nbrLayers:
+                self.ghostclip.set_size(self.props.width, EXPANDED_SIZE)
+                self.ghostclip.props.y = priority * (EXPANDED_SIZE + SPACING) + SPACING
+                if self.track_type == GES.TrackType.AUDIO:
+                    self.ghostclip.props.y += self.nbrLayers * (EXPANDED_SIZE + SPACING)
+                self.ghostclip.props.visible = True
+
+    def update(self, ease):
+        size = self.bElement.get_duration()
+        self.set_size(self.nsToPixel(size), EXPANDED_SIZE, ease)
+
+    def setDragged(self, dragged):
+        brother = self.timeline.findBrother(self.bElement)
+        if brother:
+            brother.isDragged = dragged
+        self.isDragged = dragged
+
+    # Internal API
+
+    def _createGhostclip(self):
+        self.ghostclip = Clutter.Actor.new()
+        self.ghostclip.set_background_color(Clutter.Color.new(100, 100, 100, 50))
+        self.ghostclip.props.visible = False
+        self.timeline.add_child(self.ghostclip)
+
+    def _createBorder(self):
+        self.border = RoundedRectangle(0, 0, 5, 5)
+        color = Cogl.Color()
+        color.init_from_4ub(100, 100, 100, 255)
+        self.border.set_border_color(color)
+        self.border.set_border_width(3)
+
+        self.border.set_position(0, 0)
+        self.add_child(self.border)
+
+    def _createBackground(self, track):
+        self.background = RoundedRectangle(0, 0, 5, 5)
+        if track.type == GES.TrackType.AUDIO:
+            color = Cogl.Color()
+            color.init_from_4ub(70, 79, 118, 255)
+        else:
+            color = Cogl.Color()
+            color.init_from_4ub(225, 232, 238, 255)
+        self.background.set_color(color)
+        self.background.set_border_width(3)
+
+        self.background.set_position(0, 0)
+        self.add_child(self.background)
+
+    def _createHandles(self):
+        self.leftHandle = TrimHandle(self, True)
+        self.rightHandle = TrimHandle(self, False)
+        self.add_child(self.leftHandle)
+        self.add_child(self.rightHandle)
+        self.leftHandle.set_position(0, 0)
+
+    def _createPreview(self):
+        self.preview = get_preview_for_object(self.bElement)
+        self.add_child(self.preview)
+
+    def _createMarquee(self):
+        # TODO: difference between Actor.new() and Actor()?
+        self.marquee = Clutter.Actor()
+        self.marquee.set_background_color(Clutter.Color.new(60, 60, 60, 100))
+        self.add_child(self.marquee)
+        self.marquee.props.visible = False
+
+    def _connectToEvents(self):
+        # Click
+        # We gotta go low-level cause Clutter.ClickAction["clicked"]
+        # gets emitted after Clutter.DragAction["drag-begin"]
+        self.connect("button-press-event", self._clickedCb)
+
+        # Drag and drop.
+        action = Clutter.DragAction()
+        self.add_action(action)
+        action.connect("drag-progress", self._dragProgressCb)
+        action.connect("drag-begin", self._dragBeginCb)
+        action.connect("drag-end", self._dragEndCb)
+        self.dragAction = action
+
+    def _getLayerForY(self, y):
+        if self.bElement.get_track_type() == GES.TrackType.AUDIO:
+            y -= self.nbrLayers * (EXPANDED_SIZE + SPACING)
+        priority = int(y / (EXPANDED_SIZE + SPACING))
+        return priority
 
-    def __init__(self, instance):
-        Gtk.VBox.__init__(self)
-        Loggable.__init__(self)
-        self.app = instance
-        self._layer_controls = {}
-        self._selected_layer = None
-        self._timeline = None
-        self.set_spacing(0)
-        self.separator_height = 0
-        self.type_map = {GES.TrackType.AUDIO: AudioLayerControl,
-                         GES.TrackType.VIDEO: VideoLayerControl}
-        self.connect("size-allocate", self._sizeAllocatedCb)
-        self.priority_block = sys.maxint
-        self.priority_block_time = time.time()
+    # Interface (Zoomable)
 
-        # drag'n' drop
-        self.connect("drag_drop", self._dragDropCb)
-        self.connect("drag_motion", self._dragMotionCb)
-        self.connect("drag_leave", self._dragLeaveCb)
-        self.drag_dest_set(Gtk.DestDefaults.ALL,
-                         [LAYER_CONTROL_TARGET_ENTRY], Gdk.DragAction.MOVE)
+    def zoomChanged(self):
+        self.update(True)
 
-    def _sizeAllocatedCb(self, widget, alloc):
-        if self.get_children():
-            self.separator_height = self.get_children()[0].getSeparatorHeight()
-        self.app.gui.timeline_ui._canvas.updateTracks()
+    # Callbacks
 
-## Timeline callbacks
+    def _clickedCb(self, action, actor):
+        #TODO : Let's be more specific, masks etc ..
+        self.timeline.selection.setToObj(self.bElement, SELECT)
 
-    def getTimeline(self):
-        return self._timeline
+    def _dragBeginCb(self, action, actor, event_x, event_y, modifiers):
+        self._context = EditingContext(self.bElement, self.timeline.bTimeline, GES.EditMode.EDIT_NORMAL, 
GES.Edge.EDGE_NONE, self.timeline.selection.getSelectedTrackElements(), None)
 
-    def setTimeline(self, timeline):
-        self.debug("Setting timeline %s", timeline)
+        # This can't change during a drag, so we can safely compute it now for drag events.
+        self.nbrLayers = len(self.timeline.bTimeline.get_layers())
+        # We can also safely find if the object has a brother element
+        self.setDragged(True)
+        self.brother = self.timeline.findBrother(self.bElement)
+        if self.brother:
+            self.brother.nbrLayers = self.nbrLayers
 
-        # remove old layer controls
-        for layer in self._layer_controls.copy():
-            self._layerRemovedCb(None, layer)
+        self._dragBeginStart = self.bElement.get_start()
+        self.dragBeginStartX = event_x
+        self.dragBeginStartY = event_y
 
-        if timeline:
-            for layer in timeline.get_layers():
-                self._layerAddedCb(None, layer)
+    def _dragProgressCb(self, action, actor, delta_x, 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
+        delta_y = coords[1] - self.dragBeginStartY
 
-            timeline.connect("layer-added", self._layerAddedCb)
-            timeline.connect("layer-removed", self._layerRemovedCb)
-            self.connect = True
+        y = coords[1] + self.timeline._container.point.y
 
-        elif self._timeline:
-            self._timeline.disconnect_by_func(self._layerAddedCb)
-            self._timeline.disconnect_by_func(self._layerRemovedCb)
+        priority = self._getLayerForY(y)
 
-        self._timeline = timeline
+        self.ghostclip.props.x = self.nsToPixel(self._dragBeginStart) + delta_x
+        self.updateGhostclip(priority, y, False)
+        if self.brother:
+            self.brother.ghostclip.props.x = self.nsToPixel(self._dragBeginStart) + delta_x
+            self.brother.updateGhostclip(priority, y, True)
 
-    timeline = property(getTimeline, setTimeline, None, "The timeline property")
+        new_start = self._dragBeginStart + self.pixelToNs(delta_x)
 
-    def _layerAddedCb(self, timeline, layer):
-        self.debug("Layer %s added", layer)
-        video_control = VideoLayerControl(self.app, layer)
-        audio_control = AudioLayerControl(self.app, layer)
+        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())
+        return False
 
-        map = {GES.TrackType.AUDIO: audio_control,
-               GES.TrackType.VIDEO: video_control}
-        self._layer_controls[layer] = map
+    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)
 
-        self.pack_start(video_control, False, False, 0)
-        self.pack_start(audio_control, False, False, 0)
+        priority = self._getLayerForY(coords[1] + self.timeline._container.point.y)
+        priority = min(priority, len(self.timeline.bTimeline.get_layers()))
 
-        audio_control.show()
-        video_control.show()
+        self.setDragged(False)
 
-        self._orderControls()
-        self._hideLastSeparator()
-        self._updatePopupMenus()
+        self.ghostclip.props.visible = False
+        if self.brother:
+            self.brother.ghostclip.props.visible = False
 
-    def _layerRemovedCb(self, timeline, layer):
-        audio_control = self._layer_controls[layer][GES.TrackType.AUDIO]
-        video_control = self._layer_controls[layer][GES.TrackType.VIDEO]
+        priority = max(0, priority)
+        self._context.editTo(new_start, priority)
+        self._context.finish()
 
-        self.remove(audio_control)
-        self.remove(video_control)
+    def _selectedChangedCb(self, selected, isSelected):
+        self.marquee.props.visible = isSelected
 
-        del self._layer_controls[layer]
-        self._hideLastSeparator()
-        self._updatePopupMenus()
 
-    def _orderControls(self):
-        # this works since every layer has audio and video
-        middle = len(self.get_children()) / 2
-        for child in self.get_children():
-            if isinstance(child, VideoLayerControl):
-                self.reorder_child(child, child.layer.get_priority())
-            elif isinstance(child, AudioLayerControl):
-                self.reorder_child(child, middle + child.layer.get_priority())
+class TimelineStage(Clutter.ScrollActor, Zoomable):
+    def __init__(self, container):
+        Clutter.ScrollActor.__init__(self)
+        Zoomable.__init__(self)
+        self.set_background_color(Clutter.Color.new(31, 30, 33, 255))
+        self.elements = []
+        self.selection = Selection()
+        self._createPlayhead()
+        self._container = container
+        self.lastPosition = 0
 
-    def _hideLastSeparator(self):
-        if self.get_children():
-            for child in self.get_children():
-                child.setSeparatorVisibility(True)
+    # Public API
 
-            self.get_children()[-1].setSeparatorVisibility(False)
+    def setPipeline(self, pipeline):
+        pipeline.connect('position', self._positionCb)
 
-    def _updatePopupMenus(self):
+    def setTimeline(self, bTimeline):
         """
-        Update sensitivity of menus
-
-        Should be called after _orderControls as it expects the controls
-        in ordered state
+        @param bTimeline : the backend GES.Timeline which we interface.
+        Does all the necessary connections.
         """
-        children = self.get_children()
+        self.bTimeline = bTimeline
 
-        # handle no layer case
-        if not children:
-            return
+        self.bTimeline.connect("track-added", self._trackAddedCb)
 
-        # handle one layer case
-        if len(children) == 2:
-            for child in children:
-                child.updateMenuSensitivity(-2)
-            return
+        for track in bTimeline.get_tracks():
+            self.connectTrack(track)
+        for layer in bTimeline.get_layers():
+            self._add_layer(layer)
+        self.bTimeline.connect("layer-added", self._layerAddedCb)
+        self.bTimeline.connect("layer-removed", self._layerRemovedCb)
+        self.zoomChanged()
 
-        # all other cases
-        last = None
-        index = 0
-        first = True
-        for child in children:
-            if type(child) == AudioLayerControl and first:
-                index = 0
-                last.updateMenuSensitivity(-1)
-                first = False
-
-            child.updateMenuSensitivity(index)
-            index += 1
-            last = child
-
-        last.updateMenuSensitivity(-1)
-
-    def getHeightOfLayer(self, track_type, layer):
-        if track_type == GES.TrackType.VIDEO:
-            return self._layer_controls[layer][GES.TrackType.VIDEO].getControlHeight()
-        else:
-            return self._layer_controls[layer][GES.TrackType.AUDIO].getControlHeight()
+    def connectTrack(self, track):
+        track.connect("track-element-added", self._trackElementAddedCb)
+        track.connect("track-element-removed", self._trackElementRemovedCb)
 
-    def getYOfLayer(self, track_type, layer):
+    #Stage was clicked with nothing under the pointer
+    def emptySelection(self):
+        """
+        Empty the current selection.
+        """
+        self.selection.setSelection(self.selection.getSelectedTrackElements(), UNSELECT)
+
+    def findBrother(self, element):
+        father = element.get_parent()
+        for elem in self.elements:
+            if elem.bElement.get_parent() == father and elem.bElement != element:
+                return elem
+        return None
+
+    #Internal API
+
+    def _positionCb(self, pipeline, position):
+        self.playhead.props.x = self.nsToPixel(position)
+        self._container._scrollToPlayhead()
+        self.lastPosition = position
+
+    def _updatePlayHead(self):
+        height = len(self.bTimeline.get_layers()) * (EXPANDED_SIZE + SPACING) * 2
+        self.playhead.set_size(2, height)
+
+    def _createPlayhead(self):
+        self.playhead = Clutter.Actor()
+        self.playhead.set_background_color(Clutter.Color.new(200, 0, 0, 255))
+        self.playhead.set_size(0, 0)
+        self.playhead.set_position(0, 0)
+        self.add_child(self.playhead)
+        self.playhead.set_easing_duration(0)
+        self.playhead.set_z_position(1)
+
+    def _addTimelineElement(self, track, bElement):
+        element = TimelineElement(bElement, track, self)
+        element.set_z_position(-1)
+
+        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.elements.append(element)
+
+        self._setElementY(element)
+
+        self.add_child(element)
+
+        self._setElementX(element)
+
+    def _removeTimelineElement(self, track, bElement):
+        bElement.disconnect_by_func("notify::start", self._elementStartChangedCb)
+        bElement.disconnect_by_func("notify::duration", self._elementDurationChangedCb)
+        bElement.disconnect_by_func("notify::in-point", self._elementInPointChangedCb)
+
+    def _setElementX(self, element, ease=True):
+        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()
+
+    # Crack, change that when we have retractable layers
+    def _setElementY(self, element):
+        element.save_easing_state()
         y = 0
-        for child in self.get_children():
-            if layer == child.layer and isinstance(child, self.type_map[track_type]):
-                return y
+        bElement = element.bElement
+        track_type = bElement.get_track_type()
 
-            y += child.getHeight()
-        return 0
+        if (track_type == GES.TrackType.AUDIO):
+            y = len(self.bTimeline.get_layers()) * (EXPANDED_SIZE + SPACING)
 
-    def getHeightOfTrack(self, track_type):
-        y = 0
-        for child in self.get_children():
-            if isinstance(child, self.type_map[track_type]):
-                y += child.getHeight()
+        y += bElement.get_parent().get_layer().get_priority() * (EXPANDED_SIZE + SPACING) + SPACING
 
-        return y - self.separator_height
+        element.props.y = y
+        element.restore_easing_state()
 
-    def getPriorityForY(self, y):
-        priority = -1
-        current = 0
+    def _redraw(self):
+        self.save_easing_state()
+        self.props.width = self.nsToPixel(self.bTimeline.get_duration()) + 250
+        for element in self.elements:
+            self._setElementX(element)
+        self.restore_easing_state()
+        self.playhead.props.x = self.nsToPixel(self.lastPosition)
 
-        # increment priority for each control we pass
-        for child in self.get_children():
-            if y <= current:
-                return self._limitPriority(priority)
+    # Interface overrides (Zoomable)
 
-            current += child.getHeight()
-            priority += 1
+    def zoomChanged(self):
+        self._redraw()
 
-        # another check if priority has been incremented but not returned
-        # because there were no more children
-        if y <= current:
-            return self._limitPriority(priority)
+    def _add_layer(self, layer):
+        for element in self.elements:
+            self._setElementY(element)
+        self.save_easing_state()
+        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.controls.addLayerControl(layer)
+        self._updatePlayHead()
 
-        return 0
+    # Callbacks
 
-    def _limitPriority(self, calculated):
-        priority = min(self._getLayerBlock(), calculated)
-        self._setLayerBlock(priority)
-        return priority
+    def _layerAddedCb(self, timeline, layer):
+        self._add_layer(layer)
+
+    def _layerRemovedCb(self, timeline, layer):
+        layer.disconnect_by_func(self._clipAddedCb)
+        layer.disconnect_by_func(self._clipRemovedCb)
+        self._updatePlayHead()
+
+    def _clipAddedCb(self, layer, clip):
+        clip.connect("child-added", self._elementAddedCb)
+        clip.connect("child-removed", self._elementRemovedCb)
+
+    def _clipRemovedCb(self, layer, clip):
+        clip.disconnect_by_func(self._elementAddedCb)
+        clip.disconnect_by_func(self._elementRemovedCb)
+
+    def _trackAddedCb(self, timeline, track):
+        self.connectTrack(track)
+
+    def _elementAddedCb(self, clip, bElement):
+        pass
+
+    def _elementRemovedCb(self):
+        pass
+
+    def _trackElementAddedCb(self, track, bElement):
+        print "trackelement added"
+        self._addTimelineElement(track, bElement)
 
-    def _setLayerBlock(self, n):
-        if self.priority_block != n:
-            self.debug("Blocking UI layer creation")
-            self.priority_block = n
-            self.priority_block_time = time.time()
+    def _trackElementRemovedCb(self, track, bElement):
+        self._removeTimelineElement(track, bElement)
 
-    def _getLayerBlock(self):
-        if time.time() - self.priority_block_time >= LAYER_CREATION_BLOCK_TIME:
-            return sys.maxint
+    def _elementPriorityChangedCb(self, bElement, priority, element):
+        self._setElementY(element)
+
+    def _elementStartChangedCb(self, bElement, start, element):
+        if element.isDragged:
+            self._setElementX(element, ease=False)
         else:
-            return self.priority_block
+            self._setElementX(element)
 
-    def soloLayer(self, layer):
-        """
-        Enable this layer and disable all others
-        """
-        for key, controls in self._layer_controls.iteritems():
-            controls[GES.TrackType.VIDEO].setSoloState(key == layer)
-            controls[GES.TrackType.AUDIO].setSoloState(key == layer)
+    def _elementDurationChangedCb(self, bElement, duration, element):
+        element.update(False)
 
-    def selectLayerControl(self, layer_control):
-        """
-        Select layer_control and unselect all other controls
-        """
-        layer = layer_control.layer
-        # if selected layer changed
-        if self._selected_layer != layer:
-            self._selected_layer = layer
-            self.emit("selection-changed", layer)
-
-        for key, controls in self._layer_controls.iteritems():
-            # selected widget not in this layer
-            if key != layer:
-                controls[GES.TrackType.VIDEO].selected = False
-                controls[GES.TrackType.AUDIO].selected = False
-            # selected widget in this layer
-            else:
-                if type(layer_control) is AudioLayerControl:
-                    controls[GES.TrackType.VIDEO].selected = False
-                    controls[GES.TrackType.AUDIO].selected = True
-                else:  # video
-                    controls[GES.TrackType.VIDEO].selected = True
-                    controls[GES.TrackType.AUDIO].selected = False
+    def _elementInPointChangedCb(self, bElement, inpoint, element):
+        self._setElementX(element, ease=False)
 
-    def getSelectedLayer(self):
-        return self._selected_layer
+    def _layerPriorityChangedCb(self, layer, priority):
+        self._redraw()
+
+
+def quit_(stage):
+    Gtk.main_quit()
+
+
+def quit2_(*args, **kwargs):
+    Gtk.main_quit()
 
-    def selectLayerControlForY(self, y):
-        """
-        Check if y is in the bounds of a layer control
-        """
-        current_y = 0
-        # count height
-        for child in self.get_children():
-            # calculate upper bound
-            next_y = current_y + child.getControlHeight()
-
-            # if y is in bounds, activate control and terminate
-            if y >= current_y and y <= next_y:
-                self.selectLayerControl(child)
-                return
-            # else check next control
-            else:
-                current_y += child.getHeight()
 
-    def _dragDropCb(self, widget, context, x, y, time):
+class ZoomBox(Gtk.HBox, Zoomable):
+    def __init__(self, timeline):
         """
-        Handles received drag data to reorder layers
+        This will hold the widgets responsible for zooming.
         """
-        widget = Gtk.drag_get_source_widget(context)
-        self._unhighlightSeparators()
+        Gtk.HBox.__init__(self)
+        Zoomable.__init__(self)
 
-        current = self.getControlIndex(widget)
-        index = self._getIndexForPosition(y, widget)
+        self.timeline = timeline
 
-        # if current control is before desired index move one place less
-        if current < index:
-            index -= 1
+        zoom_fit_btn = Gtk.Button()
+        zoom_fit_btn.set_relief(Gtk.ReliefStyle.NONE)
+        zoom_fit_btn.set_tooltip_text(ZOOM_FIT)
+        zoom_fit_icon = Gtk.Image()
+        zoom_fit_icon.set_from_stock(Gtk.STOCK_ZOOM_FIT, Gtk.IconSize.BUTTON)
+        zoom_fit_btn_hbox = Gtk.HBox()
+        zoom_fit_btn_hbox.pack_start(zoom_fit_icon, False, True, 0)
+        zoom_fit_btn_hbox.pack_start(Gtk.Label(_("Zoom")), False, True, 0)
+        zoom_fit_btn.add(zoom_fit_btn_hbox)
+        zoom_fit_btn.connect("clicked", self._zoomFitCb)
+        self.pack_start(zoom_fit_btn, False, True, 0)
 
-        self.moveControlWidget(widget, index)
+        # zooming slider
+        self._zoomAdjustment = Gtk.Adjustment()
+        self._zoomAdjustment.set_value(Zoomable.getCurrentZoomLevel())
+        self._zoomAdjustment.connect("value-changed", self._zoomAdjustmentChangedCb)
+        self._zoomAdjustment.props.lower = 0
+        self._zoomAdjustment.props.upper = Zoomable.zoom_steps
+        zoomslider = Gtk.Scale.new(Gtk.Orientation.HORIZONTAL, adjustment=self._zoomAdjustment)
+        zoomslider.props.draw_value = False
+        zoomslider.set_tooltip_text(_("Zoom Timeline"))
+        zoomslider.connect("scroll-event", self._zoomSliderScrollCb)
+        zoomslider.set_size_request(100, 0)  # At least 100px wide for precision
+        self.pack_start(zoomslider, True, True, 0)
 
-    def _dragLeaveCb(self, widget, context, timestamp):
-        self._unhighlightSeparators()
+        self.show_all()
 
-    def _dragMotionCb(self, widget, context, x, y, timestamp):
-        """
-        Highlight separator where control would go when dropping
-        """
-        index = self._getIndexForPosition(y, Gtk.drag_get_source_widget(context))
+        self._updateZoomSlider = True
 
-        self._unhighlightSeparators()
+    def _zoomAdjustmentChangedCb(self, adjustment):
+        # GTK crack
+        self._updateZoomSlider = False
+        Zoomable.setZoomLevel(int(adjustment.get_value()))
+        self.zoomed_fitted = False
+        self._updateZoomSlider = True
 
-        # control would go in first position
-        if index == 0:
-            pass
-        else:
-            self.get_children()[index - 1].setSeparatorHighlight(True)
+    def _zoomFitCb(self, button):
+        self.timeline.zoomFit()
 
-    def _unhighlightSeparators(self):
-        for child in self.get_children():
-            child.setSeparatorHighlight(False)
+    def _zoomSliderScrollCb(self, unused, event):
+        value = self._zoomAdjustment.get_value()
+        if event.direction in [Gdk.ScrollDirection.UP, Gdk.ScrollDirection.RIGHT]:
+            self._zoomAdjustment.set_value(value + 1)
+        elif event.direction in [Gdk.ScrollDirection.DOWN, Gdk.ScrollDirection.LEFT]:
+            self._zoomAdjustment.set_value(value - 1)
 
-    def _getIndexForPosition(self, y, widget):
-        """
-        Calculates the new index for a dragged layer
-        """
-        counter = 0
-        index = 0
-        last = None
+    def zoomChanged(self):
+        if self._updateZoomSlider:
+            self._zoomAdjustment.set_value(self.getCurrentZoomLevel())
 
-        # find new index
-        for child in self.get_children():
-            next = counter + child.getControlHeight()
 
-            # add height of last separator
-            if last:
-                next += last.getSeparatorHeight()
+class ControlActor(GtkClutter.Actor):
+    def __init__(self, container, widget, layer):
+        GtkClutter.Actor.__init__(self)
+        self.get_widget().add(widget)
+        self.set_reactive(True)
+        self.layer = layer
+        self._setUpDragAndDrop()
+        self._container = container
+        self.widget = widget
+
+    def _getLayerForY(self, y):
+        if self.isAudio:
+            y -= self.nbrLayers * (EXPANDED_SIZE + SPACING)
+        priority = int(y / (EXPANDED_SIZE + SPACING))
+        return priority
 
-            # check if current interval matches y
-            if y >= counter and y < next:
-                return self._limitPositionIndex(index, widget)
+    def _setUpDragAndDrop(self):
+        self.dragAction = Clutter.DragAction()
+        self.add_action(self.dragAction)
+        self.dragAction.connect("drag-begin", self._dragBeginCb)
+        self.dragAction.connect("drag-progress", self._dragProgressCb)
+        self.dragAction.connect("drag-end", self._dragEndCb)
+
+    def _dragBeginCb(self, action, actor, event_x, event_y, modifiers):
+        self.brother = self._container.getBrotherControl(self)
+        self.brother.raise_top()
+        self.raise_top()
+        self.nbrLayers = len(self._container.timeline.bTimeline.get_layers())
+        self._dragBeginStartX = event_x
+
+    def _dragProgressCb(self, action, actor, delta_x, delta_y):
+        y = self.dragAction.get_motion_coords()[1]
+        priority = self._getLayerForY(y)
+        lowerLimit = 0
+        if self.isAudio:
+            lowerLimit = self.nbrLayers * (EXPANDED_SIZE + SPACING)
+
+        if actor.props.y + delta_y > lowerLimit and priority < self.nbrLayers:
+            actor.move_by(0, delta_y)
+            self.brother.move_by(0, delta_y)
+
+        if self.layer.get_priority() != priority and priority >= 0 and priority < self.nbrLayers:
+            self._container.moveLayer(self, priority)
+        return False
 
-            # setup next iteration
-            counter = next
-            index += 1
-            last = child
+    def _dragEndCb(self, action, actor, event_x, event_y, modifiers):
+        priority = self._getLayerForY(event_y)
+        if self.layer.get_priority() != priority and priority >= 0 and priority < self.nbrLayers:
+            self._container.moveLayer(self, priority)
+        self._container._reorderLayerActors()
 
-        # return a limited index
-        return self._limitPositionIndex(index, widget)
 
-    def _limitPositionIndex(self, index, widget):
-        """
-        Limit the index depending on the type of widget
-        """
-        limit = len(self.get_children()) / 2
-        if type(widget) == AudioLayerControl:
-            return max(index, limit)
+class ControlContainer(Clutter.ScrollActor):
+    __gsignals__ = {
+        "selection-changed": (GObject.SignalFlags.RUN_LAST, None, (GObject.TYPE_PYOBJECT,),)
+    }
+
+    def __init__(self, timeline):
+        Clutter.ScrollActor.__init__(self)
+        self.controlActors = []
+        self.trackControls = []
+        self.timeline = timeline
+
+    def _setTrackControlPosition(self, control):
+        y = control.layer.get_priority() * (EXPANDED_SIZE + SPACING) + SPACING
+        if control.isAudio:
+            y += len(self.timeline.bTimeline.get_layers()) * (EXPANDED_SIZE + SPACING)
+        control.set_position(0, y)
+
+    def _reorderLayerActors(self):
+        for control in self.controlActors:
+            control.save_easing_state()
+            control.set_easing_mode(Clutter.AnimationMode.EASE_OUT_BACK)
+            self._setTrackControlPosition(control)
+            control.restore_easing_state()
+
+    def getBrotherControl(self, control):
+        for cont in self.controlActors:
+            if cont != control and cont.layer == control.layer:
+                return cont
+
+    def moveLayer(self, control, target):
+        movedLayer = control.layer
+        priority = movedLayer.get_priority()
+        self.timeline.bTimeline.enable_update(False)
+        movedLayer.props.priority = 999
+
+        if priority > target:
+            for layer in self.timeline.bTimeline.get_layers():
+                prio = layer.get_priority()
+                if target <= prio < priority:
+                    layer.props.priority = prio + 1
+        elif priority < target:
+            for layer in self.timeline.bTimeline.get_layers():
+                prio = layer.get_priority()
+                if priority < prio <= target:
+                    layer.props.priority = prio - 1
+        movedLayer.props.priority = target
+
+        self._reorderLayerActors()
+        self.timeline.bTimeline.enable_update(True)
+
+    def addTrackControl(self, layer, isAudio):
+        if isAudio:
+            control = AudioLayerControl(self, layer)
         else:
-            return min(index, limit)
+            control = VideoLayerControl(self, layer)
 
-    def moveControlWidget(self, control, index):
-        """
-        Moves control to the given index and cares for moving the linked layer
-        as well as updating separators
-        """
-        self.reorder_child(control, index)
-
-        # reorder linked audio/video layer
-        widget_type = type(control)
-        index = 0
-        for child in self.get_children():
-            # only set layer priority once
-            if type(child) == widget_type:
-                child.layer.set_priority(index)
-                index += 1
-
-        # order controls and update separators
-        self._orderControls()
-        self._hideLastSeparator()
-        self._updatePopupMenus()
-
-    def getControlIndex(self, control):
-        """
-        Returns an unique ID of a control
+        controlActor = ControlActor(self, control, layer)
+        controlActor.isAudio = isAudio
+        controlActor.layer = layer
+        controlActor.set_size(CONTROL_WIDTH, EXPANDED_SIZE + SPACING)
 
-        Used for drag and drop
-        """
-        counter = 0
-        for child in self.get_children():
-            if child == control:
-                return counter
+        self.add_child(controlActor)
+        self.trackControls.append(control)
+        self.controlActors.append(controlActor)
 
-            counter += 1
+    def selectLayerControl(self, layer_control):
+        for control in self.trackControls:
+            control.selected = False
+        layer_control.selected = True
+        self.props.height += (EXPANDED_SIZE + SPACING) * 2 + SPACING
 
-    def getControlFromId(self, id):
-        """
-        Returns the control for an ID
+    def addLayerControl(self, layer):
+        self.addTrackControl(layer, False)
+        self.addTrackControl(layer, True)
+        self._reorderLayerActors()
 
-        Used for drag and drop
-        """
-        counter = 0
-        for child in self.get_children():
-            if counter == id:
-                return child
 
-            counter += 1
+class Timeline(Gtk.VBox, Zoomable):
+    def __init__(self, instance, ui_manager):
+        gtksettings = Gtk.Settings.get_default()
+        gtksettings.set_property("gtk-application-prefer-dark-theme", True)
+        Zoomable.__init__(self)
+        Gtk.VBox.__init__(self)
+        GObject.threads_init()
 
+        self.ui_manager = ui_manager
+        self.app = instance
+        self._settings = self.app.settings
 
-class InfoStub(Gtk.HBox, Loggable):
-    """
-    Box used to display information on the current state of the timeline
-    """
+        self.embed = GtkClutter.Embed()
+        self.embed.show()
 
-    def __init__(self):
-        Gtk.HBox.__init__(self)
-        Loggable.__init__(self)
-        self.errors = []
-        self._scroll_pos_ns = 0
-        self._errorsmessage = _("One or more GStreamer errors occured!")
-        self._makeUI()
+        self.point = Clutter.Point()
+        self.point.x = 0
+        self.point.y = 0
 
-    def _makeUI(self):
-        self.set_spacing(SPACING)
-        self.erroricon = Gtk.Image.new_from_stock(Gtk.STOCK_DIALOG_WARNING,
-                                                  Gtk.IconSize.SMALL_TOOLBAR)
+        self.zoomBox = ZoomBox(self)
+#        self.pack_end(self.zoomBox, False, False, False)
 
-        self.pack_start(self.erroricon, False, True, 0)
+        self._packScrollbars(self)
 
-        self.infolabel = Gtk.Label(label=self._errorsmessage)
-        self.infolabel.set_alignment(0, 0.5)
+#        self.viewer = ViewerWidget()
 
-        self.questionbutton = Gtk.Button()
-        self.infoicon = Gtk.Image()
-        self.infoicon.set_from_stock(Gtk.STOCK_INFO, Gtk.IconSize.SMALL_TOOLBAR)
-        self.questionbutton.add(self.infoicon)
-        self.questionbutton.connect("clicked", self._questionButtonClickedCb)
+#        self.pack_end(self.viewer, False, False, False)
 
-        self.pack_start(self.infolabel, True, True, 0)
-        self.pack_start(self.questionbutton, False, True, 0)
+#        self.viewer.set_size_request(200, 200)
 
-    def addErrors(self, *args):
-        self.errors.append(args)
-        self.show()
+        stage = self.embed.get_stage()
+        stage.set_background_color(Clutter.Color.new(31, 30, 33, 255))
 
-    def _errorDialogBoxCloseCb(self, dialog):
-        dialog.destroy()
-
-    def _errorDialogBoxResponseCb(self, dialog, unused_response):
-        dialog.destroy()
-
-    def _questionButtonClickedCb(self, unused_button):
-        msgs = (_("Error List"),
-            _("The following errors have been reported:"))
-        # show error dialog
-        dbox = FileListErrorDialog(*msgs)
-        dbox.connect("close", self._errorDialogBoxCloseCb)
-        dbox.connect("response", self._errorDialogBoxResponseCb)
-        for reason, extra in self.errors:
-            dbox.addFailedFile(None, reason, extra)
-        dbox.show()
-        # reset error list
-        self.errors = []
-        self.hide()
+        self.stage = stage
 
-    def show(self):
-        self.log("showing")
-        self.show_all()
+        self.embed.connect("scroll-event", self._scrollEventCb)
 
+        self.stage.set_throttle_motion_events(True)
 
-class Timeline(Gtk.Table, Loggable, Zoomable):
-    """
-    Initiate and manage the timeline's user interface components.
+        stage.show()
 
-    This class is not to be confused with project.py's
-    "timeline" instance of GESTimeline.
-    """
+        widget = TimelineStage(self)
 
-    def __init__(self, instance, ui_manager):
-        Gtk.Table.__init__(self, rows=2, columns=1, homogeneous=False)
-        Loggable.__init__(self)
-        Zoomable.__init__(self)
-        self.log("Creating Timeline")
+        self.controls = ControlContainer(widget)
+        stage.add_child(self.controls)
+        self.controls.set_position(0, 0)
+        self.controls.set_z_position(2)
 
-        self._updateZoomSlider = True
-        self.ui_manager = ui_manager
-        self.app = instance
-        self._temp_elements = []
-        self._drag_started = False
-        self._factories = None
-        self._finish_drag = False
-        self._createUI()
-        self._framerate = Gst.Fraction(1, 1)
-        self._timeline = None
+        stage.add_child(widget)
+        widget.set_position(CONTROL_WIDTH, 0)
+        stage.connect("destroy", quit_)
+        stage.connect("button-press-event", self._clickedCb)
+        self.timeline = widget
 
-        self.zoomed_fitted = True
+        print self.timeline
 
-        # Timeline edition related fields
-        self._move_context = None
+        self.scrolled = 0
+
+        self._createActions()
 
-        self._project = None
         self._projectmanager = None
+        self._project = None
 
-        # The IDs of the various gobject signals we connect to
-        self._signal_ids = []
+        self.show_all()
 
-        self._settings = self.app.settings
-        self._settings.connect("edgeSnapDeadbandChanged",
-                self._snapDistanceChangedCb)
+#        self.ruler.hide()
 
-    def _createUI(self):
-        self.leftSizeGroup = Gtk.SizeGroup(Gtk.SizeGroupMode.HORIZONTAL)
-        self.props.row_spacing = 2
-        self.props.column_spacing = 2
-        self.hadj = Gtk.Adjustment()
-        self.vadj = Gtk.Adjustment()
+    def insertEnd(self, assets):
+        """
+        Add source at the end of the timeline
+        @type sources: An L{GES.TimelineSource}
+        @param x2: A list of sources to add to the timeline
+        """
+        self.app.action_log.begin("add clip")
+        # FIXME we should find the longets layer instead of adding it to the
+        # first one
+        # Handle the case of a blank project
+        layer = self._ensureLayer()[0]
+        self.bTimeline.enable_update(False)
+        for asset in assets:
+            if asset.is_image():
+                clip_duration = long(long(self._settings.imageClipLength) * Gst.SECOND / 1000)
+            else:
+                clip_duration = asset.get_duration()
 
-        # zooming slider's "zoom fit" button
-        zoom_controls_hbox = Gtk.HBox()
-        zoom_fit_btn = Gtk.Button()
-        zoom_fit_btn.set_relief(Gtk.ReliefStyle.NONE)
-        zoom_fit_btn.set_tooltip_text(ZOOM_FIT)
-        zoom_fit_icon = Gtk.Image()
-        zoom_fit_icon.set_from_stock(Gtk.STOCK_ZOOM_FIT, Gtk.IconSize.BUTTON)
-        zoom_fit_btn_hbox = Gtk.HBox()
-        zoom_fit_btn_hbox.pack_start(zoom_fit_icon, False, True, 0)
-        zoom_fit_btn_hbox.pack_start(Gtk.Label(_("Zoom")), False, True, 0)
-        zoom_fit_btn.add(zoom_fit_btn_hbox)
-        zoom_fit_btn.connect("clicked", self._zoomFitCb)
-        zoom_controls_hbox.pack_start(zoom_fit_btn, False, True, 0)
-        # zooming slider
-        self._zoomAdjustment = Gtk.Adjustment()
-        self._zoomAdjustment.set_value(Zoomable.getCurrentZoomLevel())
-        self._zoomAdjustment.connect("value-changed", self._zoomAdjustmentChangedCb)
-        self._zoomAdjustment.props.lower = 0
-        self._zoomAdjustment.props.upper = Zoomable.zoom_steps
-        zoomslider = Gtk.Scale.new(Gtk.Orientation.HORIZONTAL, adjustment=self._zoomAdjustment)
-        zoomslider.props.draw_value = False
-        zoomslider.set_tooltip_text(_("Zoom Timeline"))
-        zoomslider.connect("scroll-event", self._zoomSliderScrollCb)
-        zoomslider.set_size_request(100, 0)  # At least 100px wide for precision
-        zoom_controls_hbox.pack_start(zoomslider, True, True, 0)
-        self.attach(zoom_controls_hbox, 0, 1, 0, 1, yoptions=0, xoptions=Gtk.AttachOptions.FILL)
-
-        # controls for tracks and layers
-        self.controls = TimelineControls(self.app)
-        controlwindow = Gtk.Viewport(None, None)
-        controlwindow.add(self.controls)
-        controlwindow.set_size_request(-1, 1)
-        controlwindow.set_shadow_type(Gtk.ShadowType.OUT)
-        scrolledwindow = Gtk.ScrolledWindow()
-        scrolledwindow.add(controlwindow)
-        scrolledwindow.props.hscrollbar_policy = Gtk.PolicyType.NEVER
-        scrolledwindow.props.vscrollbar_policy = Gtk.PolicyType.ALWAYS
-        scrolledwindow.props.vadjustment = self.vadj
-        # We need ALWAYS policy for correct sizing, but we don't want the
-        # scrollbar to be visible. Yay gtk3!
-        scrollbar = scrolledwindow.get_vscrollbar()
+            print "added asset"
+            layer.add_asset(asset, self.bTimeline.props.duration,
+                0, clip_duration, 1.0, asset.get_supported_formats())
+        self.bTimeline.enable_update(True)
 
-        def scrollbar_show_cb(scrollbar):
-            scrollbar.hide()
+    def setProjectManager(self, projectmanager):
+        if self._projectmanager is not None:
+            self._projectmanager.disconnect_by_func(self._projectChangedCb)
 
-        scrollbar.connect("show", scrollbar_show_cb)
-        scrollbar.hide()
-        self.attach(scrolledwindow, 0, 1, 1, 2, xoptions=Gtk.AttachOptions.FILL)
+        self._projectmanager = projectmanager
+        if projectmanager is not None:
+            projectmanager.connect("new-project-created", self._projectCreatedCb)
+            projectmanager.connect("new-project-loaded", self._projectChangedCb)
 
-        # timeline ruler
-        self.ruler = ruler.ScaleRuler(self.app, self.hadj)
-        self.ruler.get_accessible().set_name("timeline ruler")  # used for dogtail
-        self.ruler.set_size_request(0, 25)
-        self.ruler.connect("key-press-event", self._keyPressEventCb)
-        rulerframe = Gtk.Frame()
-        rulerframe.set_shadow_type(Gtk.ShadowType.OUT)
-        rulerframe.add(self.ruler)
-        self.attach(rulerframe, 1, 2, 0, 1, yoptions=0)
-
-        # proportional timeline
-        self._canvas = TimelineCanvas(self.app)
-        self._canvas.get_accessible().set_name("timeline canvas")  # used for dogtail
-        self._root_item = self._canvas.get_root_item()
-        self.attach(self._canvas, 1, 2, 1, 2)
-
-        # scrollbar
-        self._hscrollbar = Gtk.HScrollbar(self.hadj)
-        self._vscrollbar = Gtk.VScrollbar(self.vadj)
-        self.attach(self._hscrollbar, 1, 2, 2, 3, yoptions=0)
-        self.attach(self._vscrollbar, 2, 3, 1, 2, xoptions=0)
-        self.hadj.connect("value-changed", self._updateScrollPosition)
-        self.vadj.connect("value-changed", self._updateScrollPosition)
+    def _ensureLayer(self):
+        """
+        Make sure we have a layer in our timeline
 
-        # error infostub
-        self.infostub = InfoStub()
-        self.attach(self.infostub, 1, 2, 4, 5, yoptions=0)
+        Returns: The number of layer present in self.timeline
+        """
+        layers = self.bTimeline.get_layers()
 
-        self.show_all()
-        self.infostub.hide()
+        if (len(layers) == 0):
+            layer = GES.Layer()
+            layer.props.auto_transition = True
+            self.bTimeline.add_layer(layer)
+            layers = [layer]
+
+        return layers
 
-        # toolbar actions
+    def _createActions(self):
         actions = (
             ("ZoomIn", Gtk.STOCK_ZOOM_IN, None,
             "<Control>plus", ZOOM_IN, self._zoomInCb),
@@ -1121,435 +1152,234 @@ class Timeline(Gtk.Table, Loggable, Zoomable):
 
         self.ui_manager.add_ui_from_string(ui)
 
-        # drag and drop
-
-        self._canvas.drag_dest_set(Gtk.DestDefaults.MOTION | Gtk.DestDefaults.HIGHLIGHT,
-                                   [FILESOURCE_TARGET_ENTRY, EFFECT_TARGET_ENTRY],
-                                   Gdk.DragAction.COPY)
-
-        self._canvas.drag_dest_add_text_targets()
-
-        self._canvas.connect("drag-leave", self._dragLeaveCb)
-        self._canvas.connect("drag-drop", self._dragDropCb)
-        self._canvas.connect("drag-motion", self._dragMotionCb)
-        self._canvas.connect("key-press-event", self._keyPressEventCb)
-        self._canvas.connect("scroll-event", self._scrollEventCb)
-
-## Event callbacks
-    def _keyPressEventCb(self, unused_widget, event):
-        kv = event.keyval
-        self.debug("kv:%r", kv)
-        if kv not in [Gdk.KEY_Left, Gdk.KEY_Right]:
-            return False
-        mod = event.get_state()
-        try:
-            if mod & Gdk.ModifierType.CONTROL_MASK:
-                now = self._project.pipeline.getPosition()
-                ltime, rtime = self._project.timeline.edges.closest(now)
-
-            if kv == Gdk.KEY_Left:
-                if mod & Gdk.ModifierType.SHIFT_MASK:
-                    self._seeker.seekRelative(0 - Gst.SECOND)
-                elif mod & Gdk.ModifierType.CONTROL_MASK:
-                    self._seeker.seek(ltime + 1)
-                else:
-                    self.app.current.pipeline.stepFrame(self._framerate, -1)
-            elif kv == Gdk.KEY_Right:
-                if mod & Gdk.ModifierType.SHIFT_MASK:
-                    self._seeker.seekRelative(Gst.SECOND)
-                elif mod & Gdk.ModifierType.CONTROL_MASK:
-                    self._seeker.seek(rtime + 1)
-                else:
-                    self.app.current.pipeline.stepFrame(self._framerate, 1)
-        finally:
-            return True
+    def _packScrollbars(self, vbox):
+        self.hadj = Gtk.Adjustment()
+        self.vadj = Gtk.Adjustment()
+        self.hadj.connect("value-changed", self._updateScrollPosition)
+        self.vadj.connect("value-changed", self._updateScrollPosition)
 
-## Drag and Drop callbacks
-
-    def _dragMotionCb(self, unused, context, x, y, timestamp):
-        # Set up the initial data when we first initiate the drag operation
-        if not self._drag_started:
-            self._drag_started = True
-        elif context.list_targets() not in DND_EFFECT_LIST and self.app.gui.medialibrary.dragged:
-            if not self._temp_elements:
-                self._create_temp_source(x, y)
-
-            # Let some time for TrackElement-s to be created
-            if self._temp_elements:
-                focus = self._temp_elements[0]
-                if self._move_context is None:
-                    self._move_context = EditingContext(focus,
-                            self.timeline, GES.EditMode.EDIT_NORMAL, GES.Edge.EDGE_NONE,
-                            set(self._temp_elements[1:]), self.app.settings)
-
-                self._move_temp_source(x, y)
-        return True
+        self._vscrollbar = Gtk.VScrollbar(self.vadj)
 
-    def _dragLeaveCb(self, unused_layout, context, unused_tstamp):
-        """
-        This occurs when the user leaves the canvas area during a drag,
-        or when the item being dragged has been dropped.
+        def scrollbar_show_cb(scrollbar):
+            scrollbar.hide()
 
-        Since we always get a "drag-dropped" signal right after "drag-leave",
-        we wait 75 ms to see if a drop happens and if we need to cleanup or not.
-        """
-        self.debug("Drag leave")
-        self._canvas.handler_block_by_func(self._dragMotionCb)
-        GLib.timeout_add(75, self._dragCleanUp, context)
+#        self._vscrollbar.connect("show", scrollbar_show_cb)
 
-    def _dragCleanUp(self, context):
-        """
-        If the user drags outside the timeline,
-        remove the temporary objects we had created during the drap operation.
-        """
-        # Clean up only if clip was not dropped already
-        if self._drag_started:
-            self.debug("Drag cleanup")
-            self._drag_started = False
-            self._factories = []
-            if context.list_targets() not in DND_EFFECT_LIST:
-                self.timeline.enable_update(True)
-                self.debug("Need to cleanup %d elements" % len(self._temp_elements))
-                for obj in self._temp_elements:
-                    layer = obj.get_layer()
-                    layer.remove_clip(obj)
-                self._temp_elements = []
-                self._move_context = None
-
-            self.debug("Drag cleanup ended")
-        self._canvas.handler_unblock_by_func(self._dragMotionCb)
-        return False
+        self._hscrollBar = Gtk.HScrollbar(self.hadj)
+        vbox.pack_end(self._hscrollBar, False, True, False)
 
-    def _dragDropCb(self, widget, context, x, y, timestamp):
-        # Resetting _drag_started will tell _dragCleanUp to not do anything
-        self._drag_started = False
-        self.debug("Drag drop")
-
-        if self.app.gui.medialibrary.dragged:
-            self._canvas.drag_unhighlight()
-            self.app.action_log.begin("add clip")
-            if self._move_context is not None:
-                self._move_context.finish()
-                self._move_context = None
-            self.app.action_log.commit()
-            # The temporary objects and factories that we had created
-            # in _dragMotionCb are now kept for good.
-            # Clear the temporary references to objects, as they are real now.
-            self._temp_elements = []
-            self._factories = []
-            #context.drop_finish(True, timestamp)
-        else:
-            if self.app.current.timeline.props.duration == 0:
-                return False
-            clips = self._getClipsUnderMouse(x, y)
-            if clips:
-                # FIXME make a util function to add effects
-                # instead of copy/pasting it from cliproperties
-                bin_desc = self.app.gui.effectlist.getSelectedItems()
-                media_type = self.app.effects.getFactoryFromName(bin_desc).media_type
-
-                # Trying to apply effect only on the first object of the selection
-                clip = clips[0]
-
-                # Checking that this effect can be applied on this track object
-                # Which means, it has the corresponding media_type
-                for track_element in clip.get_children():
-                    track_type = track_element.get_track_type()
-                    if track_type == GES.TrackType.AUDIO and media_type == AUDIO_EFFECT or \
-                            track_type == GES.TrackType.VIDEO and media_type == VIDEO_EFFECT:
-                        #Actually add the effect
-                        self.app.action_log.begin("add effect")
-                        effect = GES.Effect.new(bin_description=bin_desc)
-                        clip.add(effect)
-                        self.app.gui.clipconfig.effect_expander.updateAll()
-                        self.app.action_log.commit()
-                        self._factories = None
-                        self._seeker.flush()
+        self.ruler = ScaleRuler(self, self.hadj)
+        self.ruler.setProjectFrameRate(24.)
 
-                        self.timeline.selection.setSelection(clips, SELECT)
-                        break
-        Gtk.drag_finish(context, True, False, timestamp)
-        return True
+        self.ruler.set_size_request(0, 25)
+        self.ruler.hide()
 
-    def _getClipsUnderMouse(self, x, y):
-        clips = []
-        items_in_area = self._canvas.getItemsInArea(x, y, x + 1, y + 1)
+        self.vadj.props.lower = 0
+        self.vadj.props.upper = 500
+        self.vadj.props.page_size = 250
 
-        track_elements = [obj for obj in items_in_area[1]]
-        for track_element in track_elements:
-            clips.append(track_element.get_parent())
+        hbox = Gtk.HBox()
+        hbox.set_size_request(-1, 500)
+        hbox.pack_start(self.embed, True, True, True)
+        hbox.pack_start(self._vscrollbar, False, True, False)
 
-        return clips
+        vbox.pack_end(hbox, True, True, True)
 
-    def _showSaveScreenshotDialog(self):
-        """
-        Show a filechooser dialog asking the user where to save the snapshot
-        and what file type to use.
+        hbox = Gtk.HBox()
+        self.zoomBox.set_size_request(CONTROL_WIDTH, -1)
+        hbox.pack_start(self.zoomBox, False, True, False)
+        hbox.pack_start(self.ruler, True, True, True)
 
-        Returns a list containing the full path and the mimetype if successful,
-        returns none otherwise.
-        """
-        chooser = Gtk.FileChooserDialog(_("Save As..."), self.app.gui,
-            action=Gtk.FileChooserAction.SAVE,
-            buttons=(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
-            Gtk.STOCK_SAVE, Gtk.ResponseType.OK))
-        chooser.set_icon_name("pitivi")
-        chooser.set_select_multiple(False)
-        chooser.set_current_name(_("Untitled"))
-        chooser.props.do_overwrite_confirmation = True
-        formats = {_("PNG image"): ["image/png", ("png",)],
-            _("JPEG image"): ["image/jpeg", ("jpg", "jpeg")]}
-        for format in formats:
-            filt = Gtk.FileFilter()
-            filt.set_name(format)
-            filt.add_mime_type(formats.get(format)[0])
-            chooser.add_filter(filt)
-        response = chooser.run()
-        if response == Gtk.ResponseType.OK:
-            chosen_format = formats.get(chooser.get_filter().get_name())
-            chosen_ext = chosen_format[1][0]
-            chosen_mime = chosen_format[0]
-            uri = join(chooser.get_current_folder(), chooser.get_filename())
-            ret = [uri + "." + chosen_ext, chosen_mime]
-        else:
-            ret = None
-        chooser.destroy()
-        return ret
+        vbox.pack_end(hbox, False, True, False)
 
-    def _ensureLayer(self):
-        """
-        Make sure we have a layer in our timeline
+    def _updateScrollPosition(self, 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)
 
-        Returns: The number of layer present in self.timeline
+    def zoomChanged(self):
+        self.updateHScrollAdjustments()
+
+    def updateHScrollAdjustments(self):
+        """
+        Recalculate the horizontal scrollbar depending on the timeline duration.
         """
-        layers = self.timeline.get_layers()
+        timeline_ui_width = self.embed.get_allocation().width
+#        controls_width = self.controls.get_allocation().width
+#        scrollbar_width = self._vscrollbar.get_allocation().width
+        controls_width = 0
+        scrollbar_width = 0
+        contents_size = Zoomable.nsToPixel(self.bTimeline.props.duration)
 
-        if (len(layers) == 0):
-            layer = GES.Layer()
-            layer.props.auto_transition = True
-            self.timeline.add_layer(layer)
-            layers = [layer]
+        widgets_width = controls_width + scrollbar_width
+        end_padding = 250  # Provide some space for clip insertion at the end
 
-        return layers
+        self.hadj.props.lower = 0
+        self.hadj.props.upper = contents_size + widgets_width + 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
 
-    def purgeObject(self, uri):
-        """Remove all instances of a clip from the timeline."""
-        quoted_uri = quote_uri(uri)
-        layers = self.timeline.get_layers()
-        for layer in layers:
-            for clip in layer.get_clips():
-                if hasattr(clip, "get_uri"):
-                    if quote_uri(clip.get_uri()) == quoted_uri:
-                        layer.remove_clip(clip)
-                else:
-                    # TimelineStandardTransition and the like don't have URIs
-                    # GES will remove those transitions automatically.
-                    self.debug("Not removing %s from timeline as it has no URI" % clip)
-
-    def _create_temp_source(self, x, y):
-        """
-        Create temporary clips to be displayed on the timeline during a
-        drag-and-drop operation.
-        """
-        layer = self._ensureLayer()[0]
-        start = 0
+        if contents_size + widgets_width <= 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
 
-        for asset in self.app.gui.medialibrary.getSelectedAssets():
-            if asset.is_image():
-                clip_duration = long(long(self._settings.imageClipLength) * Gst.SECOND / 1000)
-            else:
-                clip_duration = asset.get_duration()
+    def run(self):
+        self.testTimeline(self.timeline)
+        GLib.io_add_watch(sys.stdin, GLib.IO_IN, quit2_)
+        Gtk.main()
 
-            source = layer.add_asset(asset, start, 0,
-                clip_duration, asset.get_supported_formats())
+    def _setBestZoomRatio(self):
+        """
+        Set the zoom level so that the entire timeline is in view.
+        """
+        ruler_width = self.ruler.get_allocation().width
+        # Add Gst.SECOND - 1 to the timeline duration to make sure the
+        # last second of the timeline will be in view.
+        duration = self.timeline.bTimeline.get_duration()
+        if duration == 0:
+#            self.debug("The timeline duration is 0, impossible to calculate zoom")
+            return
 
-            self._temp_elements.insert(0, source)
-            start += asset.get_duration()
+        timeline_duration = duration + Gst.SECOND - 1
+        timeline_duration_s = int(timeline_duration / Gst.SECOND)
 
-    def _move_temp_source(self, x, y):
-        x = self.hadj.props.value + x
-        y = self.vadj.props.value + y
+        #self.debug("duration: %s, timeline duration: %s" % (print_ns(duration),
+    #       print_ns(timeline_duration)))
 
-        priority = self.controls.getPriorityForY(y)
+        ideal_zoom_ratio = float(ruler_width) / timeline_duration_s
+        nearest_zoom_level = Zoomable.computeZoomLevel(ideal_zoom_ratio)
+        #self.debug("Ideal zoom: %s, nearest_zoom_level %s", ideal_zoom_ratio, nearest_zoom_level)
+        Zoomable.setZoomLevel(nearest_zoom_level)
+        #self.timeline.props.snapping_distance = \
+        #    Zoomable.pixelToNs(self.app.settings.edgeSnapDeadband)
 
-        delta = Zoomable.pixelToNs(x)
-        self._move_context.editTo(delta, priority)
+        # Only do this at the very end, after updating the other widgets.
+        #self.log("Setting 'zoomed_fitted' to True")
+        self.zoomed_fitted = True
 
-## Zooming and Scrolling
+    def zoomFit(self):
+        self._hscrollBar.set_value(0)
+        self._setBestZoomRatio()
 
-    def _scrollEventCb(self, canvas, event):
-        if event.get_state() & Gdk.ModifierType.SHIFT_MASK:
-            # shift + scroll => vertical (up/down) scroll
-            if event.direction == Gdk.ScrollDirection.UP:
-                self.scroll_up()
-            elif event.direction == Gdk.ScrollDirection.DOWN:
-                self.scroll_down()
-            event.props.state &= ~Gdk.SHIFT_MASK
-        elif event.get_state() & Gdk.ModifierType.CONTROL_MASK:
-            # zoom + scroll => zooming (up: zoom in)
-            if event.direction == Gdk.ScrollDirection.UP:
-                Zoomable.zoomIn()
-                self.log("Setting 'zoomed_fitted' to False")
-                self.zoomed_fitted = False
-                return True
-            elif event.direction == Gdk.ScrollDirection.DOWN:
-                Zoomable.zoomOut()
-                self.log("Setting 'zoomed_fitted' to False")
-                self.zoomed_fitted = False
-                return True
-            return False
+    def scrollToPosition(self, position):
+        if position > self.hadj.props.upper:
+            # we can't perform the scroll because the canvas needs to be
+            # updated
+            GLib.idle_add(self._scrollToPosition, position)
         else:
-            if event.direction == Gdk.ScrollDirection.UP:
-                self.scroll_left()
-            elif event.direction == Gdk.ScrollDirection.DOWN:
-                self.scroll_right()
-        return True
+            self._scrollToPosition(position)
 
-    def scroll_left(self):
-        self._hscrollbar.set_value(self._hscrollbar.get_value() -
+    def _scrollLeft(self):
+        self._hscrollBar.set_value(self._hscrollBar.get_value() -
             self.hadj.props.page_size ** (2.0 / 3.0))
 
-    def scroll_right(self):
-        self._hscrollbar.set_value(self._hscrollbar.get_value() +
+    def _scrollRight(self):
+        self._hscrollBar.set_value(self._hscrollBar.get_value() +
             self.hadj.props.page_size ** (2.0 / 3.0))
 
-    def scroll_up(self):
+    def _scrollUp(self):
         self._vscrollbar.set_value(self._vscrollbar.get_value() -
             self.vadj.props.page_size ** (2.0 / 3.0))
 
-    def scroll_down(self):
+    def _scrollDown(self):
         self._vscrollbar.set_value(self._vscrollbar.get_value() +
             self.vadj.props.page_size ** (2.0 / 3.0))
 
-    def unsureVadjHeight(self):
-        self._scroll_pos_ns = Zoomable.pixelToNs(self.hadj.get_value())
-        self._root_item.set_simple_transform(0 - self.hadj.get_value(),
-            0 - self.vadj.get_value(), 1.0, 0)
-
-    def _updateScrollPosition(self, adjustment):
-        self.unsureVadjHeight()
-
-    def _zoomAdjustmentChangedCb(self, adjustment):
-        # GTK crack
-        self._updateZoomSlider = False
-        Zoomable.setZoomLevel(int(adjustment.get_value()))
-        self.log("Setting 'zoomed_fitted' to False")
-        self.zoomed_fitted = False
-        self._updateZoomSlider = True
-
-    def _zoomSliderScrollCb(self, unused_widget, event):
-        value = self._zoomAdjustment.get_value()
-        if event.direction in [Gdk.ScrollDirection.UP, Gdk.ScrollDirection.RIGHT]:
-            self._zoomAdjustment.set_value(value + 1)
-        elif event.direction in [Gdk.ScrollDirection.DOWN, Gdk.ScrollDirection.LEFT]:
-            self._zoomAdjustment.set_value(value - 1)
-
-    def zoomChanged(self):
-        if self._updateZoomSlider:
-            self._zoomAdjustment.set_value(self.getCurrentZoomLevel())
-
-        if self._settings and self._timeline:
-            # zoomChanged might be called various times before the UI is ready
-            self._timeline.props.snapping_distance = \
-                Zoomable.pixelToNs(self._settings.edgeSnapDeadband)
-
-        # the new scroll position should preserve the current horizontal
-        # position of the playhead in the window
-        cur_playhead_offset = self._canvas._playhead.props.x - self.hadj.props.value
-        try:
-            position = self.app.current.pipeline.getPosition()
-        except PipelineError:
-            position = 0
-        new_pos = Zoomable.nsToPixel(position) - cur_playhead_offset
-
-        # Update the position of the playhead's line on the canvas
-        # This does not actually change the timeline position
-        self._canvas._playhead.props.x = Zoomable.nsToPixel(position)
-
-        self.updateHScrollAdjustments()
-        self.scrollToPosition(new_pos)
-        self.ruler.queue_resize()
-        self.ruler.queue_draw()
-
-    def positionChangedCb(self, seeker, position):
-        self.ruler.timelinePositionChanged(position)
-        self._canvas.timelinePositionChanged(position)
-        if self.app.current.pipeline.getState() == Gst.State.PLAYING:
-            self.scrollToPlayhead()
+    def _scrollToPosition(self, position):
+        self._hscrollBar.set_value(position)
+        return False
 
-    def scrollToPlayhead(self):
-        """
-        If the current position is out of the view bounds, then scroll
-        as close to the center of the view as possible or as close as the
-        timeline canvas allows.
-        """
-        canvas_size = self._canvas.get_allocation().width
-        new_pos = Zoomable.nsToPixel(self.app.current.pipeline.getPosition())
+    def _scrollToPlayhead(self):
+        canvas_size = self.embed.get_allocation().width - CONTROL_WIDTH
+        new_pos = self.timeline.playhead.props.x
         scroll_pos = self.hadj.get_value()
-        if (new_pos > scroll_pos + canvas_size) or (new_pos < scroll_pos):
-            self.scrollToPosition(min(new_pos - canvas_size / 6,
-                                      self.hadj.props.upper - canvas_size - 1))
+        self.scrollToPosition(min(new_pos - canvas_size / 2,
+                                  self.hadj.props.upper - canvas_size - 1))
+
+    def goToPoint(self, timeline):
+        point = Clutter.Point()
+        point.x = 1000
+        point.y = 0
+        timeline.scroll_to_point(point)
         return False
 
-    def scrollToPosition(self, position):
-        if position > self.hadj.props.upper:
-            # we can't perform the scroll because the canvas needs to be
-            # updated
-            GLib.idle_add(self._scrollToPosition, position)
-        else:
-            self._scrollToPosition(position)
+    def addClipToLayer(self, layer, asset, start, duration, inpoint):
+        layer.add_asset(asset, start * Gst.SECOND, 0, duration * Gst.SECOND, 1.0, 
asset.get_supported_formats())
+
+    def handle_message(self, bus, message):
+        if message.type == Gst.MessageType.ELEMENT:
+            if message.has_name('prepare-window-handle'):
+                Gdk.threads_enter()
+                self.sink = message.src
+                self.sink.set_window_handle(self.viewer.window_xid)
+                self.sink.expose()
+                Gdk.threads_leave()
+            elif message.type == Gst.MessageType.STATE_CHANGED:
+                prev, new, pending = message.parse_state_changed()
+        return True
 
-    def _scrollToPosition(self, position):
-        self._hscrollbar.set_value(position)
+    def _clickedCb(self, stage, event):
+        actor = self.stage.get_actor_at_pos(Clutter.PickMode.REACTIVE, event.x, event.y)
+        if actor == stage:
+            self.timeline.emptySelection()
+
+    def doSeek(self):
+        #self.pipeline.simple_seek(3000000000)
         return False
 
-    def _snapDistanceChangedCb(self, settings):
-        if self._timeline:
-            self._timeline.props.snapping_distance = \
-                Zoomable.pixelToNs(settings.edgeSnapDeadband)
+    def togglePlayback(self, button):
+        self.pipeline.togglePlayback()
 
-    def _setBestZoomRatio(self):
-        """
-        Set the zoom level so that the entire timeline is in view.
+    def _renderingSettingsChangedCb(self, project, item, value):
         """
-        ruler_width = self.ruler.get_allocation().width
-        # Add Gst.SECOND - 1 to the timeline duration to make sure the
-        # last second of the timeline will be in view.
-        duration = self.timeline.get_duration()
-        if duration == 0:
-            self.debug("The timeline duration is 0, impossible to calculate zoom")
-            return
-
-        timeline_duration = duration + Gst.SECOND - 1
-        timeline_duration_s = int(timeline_duration / Gst.SECOND)
+        Called when any Project metadata changes, we filter out the one
+        we are interested in.
 
-        self.debug("duration: %s, timeline duration: %s" % (print_ns(duration),
-           print_ns(timeline_duration)))
+        if @item is None, it mean we called it ourself, and want to force
+        getting the project videorate value
+        """
+        if item == "videorate" or item is None:
+            if value is None:
+                value = project.videorate
+            self._framerate = value
+            self.ruler.setProjectFrameRate(self._framerate)
 
-        ideal_zoom_ratio = float(ruler_width) / timeline_duration_s
-        nearest_zoom_level = Zoomable.computeZoomLevel(ideal_zoom_ratio)
-        self.debug("Ideal zoom: %s, nearest_zoom_level %s", ideal_zoom_ratio, nearest_zoom_level)
-        Zoomable.setZoomLevel(nearest_zoom_level)
-        self.timeline.props.snapping_distance = \
-            Zoomable.pixelToNs(self.app.settings.edgeSnapDeadband)
+    def _doAssetAddedCb(self, project, asset, layer):
+        self.addClipToLayer(layer, asset, 2, 10, 5)
+        self.addClipToLayer(layer, asset, 15, 10, 5)
 
-        # Only do this at the very end, after updating the other widgets.
-        self.log("Setting 'zoomed_fitted' to True")
-        self.zoomed_fitted = True
+        self.pipeline = Pipeline()
+        self.pipeline.add_timeline(layer.get_timeline())
 
-## Project callbacks
+        self.bus = self.pipeline.get_bus()
+        self.bus.add_signal_watch()
+        self.bus.connect("message", self.handle_message)
+        self.playButton.connect("clicked", self.togglePlayback)
+        #self.pipeline.togglePlayback()
+        self.pipeline.activatePositionListener(interval=30)
+        self.timeline.setPipeline(self.pipeline)
+        GObject.timeout_add(1000, self.doSeek)
+        Zoomable.setZoomLevel(50)
 
     def _projectChangedCb(self, app, project, unused_fully_loaded):
         """
         When a project is loaded, we connect to its pipeline
         """
-        self.debug("Project changed")
+#        self.debug("Project changed")
 
         if project:
-            self.debug("Project is not None, connecting to its pipeline")
+#            self.debug("Project is not None, connecting to its pipeline")
             self._seeker = self._project.seeker
             self._pipeline = self._project.pipeline
-            self._pipeline.connect("position", self.positionChangedCb)
+#            self._pipeline.connect("position", self.positionChangedCb)
             self.ruler.setProjectFrameRate(self._project.videorate)
             self.ruler.zoomChanged()
             self._renderingSettingsChangedCb(self._project, None, None)
@@ -1560,13 +1390,13 @@ class Timeline(Gtk.Table, Loggable, Zoomable):
         """
         When a project is created, we connect to it timeline
         """
-        self.debug("Setting project %s", project)
+#        self.debug("Setting project %s", project)
         if self._project:
             self._project.disconnect_by_func(self._renderingSettingsChangedCb)
-            try:
-                self._pipeline.disconnect_by_func(self.positionChangedCb)
-            except TypeError:
-                pass  # We were not connected no problem
+            #try:
+            #    self._pipeline.disconnect_by_func(self.positionChangedCb)
+            #except TypeError:
+            #    pass  # We were not connected no problem
 
             self._pipeline = None
             self._seeker = None
@@ -1577,139 +1407,6 @@ class Timeline(Gtk.Table, Loggable, Zoomable):
                                   self._renderingSettingsChangedCb)
             self.setTimeline(project.timeline)
 
-    def setProjectManager(self, projectmanager):
-        if self._projectmanager is not None:
-            self._projectmanager.disconnect_by_func(self._projectChangedCb)
-
-        self._projectmanager = projectmanager
-        if projectmanager is not None:
-            projectmanager.connect("new-project-created", self._projectCreatedCb)
-            projectmanager.connect("new-project-loaded", self._projectChangedCb)
-
-    def _renderingSettingsChangedCb(self, project, item, value):
-        """
-        Called when any Project metadata changes, we filter out the one
-        we are interested in.
-
-        if @item is None, it mean we called it ourself, and want to force
-        getting the project videorate value
-        """
-        if item == "videorate" or item is None:
-            if value is None:
-                value = project.videorate
-            self._framerate = value
-            self.ruler.setProjectFrameRate(self._framerate)
-
-## Timeline callbacks
-
-    def setTimeline(self, timeline):
-        self.debug("Setting timeline %s", timeline)
-
-        self.delTimeline()
-        self.controls.timeline = timeline
-        self._timeline = timeline
-
-        if timeline:
-            # Connecting to timeline gobject signals
-            self._signal_ids.append(timeline.connect("layer-added",
-                    self._layerAddedCb))
-            self._signal_ids.append(timeline.connect("layer-removed",
-                    self._layerRemovedCb))
-            self._signal_ids.append(timeline.connect("notify::update",
-                    self._timelineUpdateChangedCb))
-            self._signal_ids.append(timeline.connect("notify::duration",
-                    self._timelineDurationChangedCb))
-
-            # The Selection object of _timeline inherits signallable
-            # We will be able to disconnect it with disconnect_by_func
-            self._timeline.selection.connect("selection-changed", self._selectionChangedCb)
-
-            # Make sure to set the current layer in use
-            self._layerAddedCb(None, None)
-            self._timeline.props.snapping_distance = \
-                Zoomable.pixelToNs(self._settings.edgeSnapDeadband)
-
-        self._canvas.setTimeline(timeline)
-        self._canvas.zoomChanged()
-
-    def getTimeline(self):
-        return self._timeline
-
-    def delTimeline(self):
-        # Disconnect signals
-        for sigid in self._signal_ids:
-            self._timeline.disconnect(sigid)
-        self._signal_ids = []
-        if hasattr(self._timeline, "selection"):
-            self._timeline.selection.disconnect_by_func(self._selectionChangedCb)
-
-        #Remove references to the ges timeline
-        self._timeline = None
-        self.controls.timeline = None
-
-    timeline = property(getTimeline, setTimeline, delTimeline, "The GESTimeline")
-
-    def _timelineDurationChangedCb(self, timeline, unused_duration):
-        self.updateHScrollAdjustments()
-
-    def _timelineUpdateChangedCb(self, unused, unsued2=None):
-        if self.zoomed_fitted is True:
-            self._setBestZoomRatio()
-
-    def _layerAddedCb(self, unused_layer, unused_user_data):
-        self.updateVScrollAdjustments()
-
-    def _layerRemovedCb(self, unused_layer, unused_user_data):
-        self.updateVScrollAdjustments()
-
-    def updateVScrollAdjustments(self):
-        """
-        Recalculate the vertical scrollbar depending on the number of layer in
-        the timeline.
-        """
-        self.vadj.props.upper = self.controls.get_allocation().height + 50
-
-    def updateHScrollAdjustments(self):
-        """
-        Recalculate the horizontal scrollbar depending on the timeline duration.
-        """
-        timeline_ui_width = self.get_allocation().width
-        controls_width = self.controls.get_allocation().width
-        scrollbar_width = self._vscrollbar.get_allocation().width
-        contents_size = Zoomable.nsToPixel(self.app.current.timeline.props.duration)
-
-        widgets_width = controls_width + scrollbar_width
-        end_padding = 250  # Provide some space for clip insertion at the end
-
-        self.hadj.props.lower = 0
-        self.hadj.props.upper = contents_size + widgets_width + 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 + widgets_width <= 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
-
-    def _selectionChangedCb(self, selection):
-        """
-        The selected clips on the timeline canvas have changed with the
-        "selection-changed" signal.
-
-        This is where you apply global UI changes, unlike individual
-        track elements' "selected-changed" signal from the Selected class.
-        """
-        if selection:
-            self.selection_actions.set_sensitive(True)
-        else:
-            self.selection_actions.set_sensitive(False)
-
-## ToolBar callbacks
-    def _zoomFitCb(self, unused, unsued2=None):
-        self._setBestZoomRatio()
-
     def _zoomInCb(self, unused_action):
         # This only handles the button callbacks (from the menus),
         # not keyboard shortcuts or the zoom slider!
@@ -1724,6 +1421,18 @@ class Timeline(Gtk.Table, Loggable, Zoomable):
         self.log("Setting 'zoomed_fitted' to False")
         self.zoomed_fitted = False
 
+    def _zoomFitCb(self, unused, unsued2=None):
+        self._setBestZoomRatio()
+
+    def _screenshotCb(self, unused_action):
+        """
+        Export a snapshot of the current frame as an image file.
+        """
+        foo = self._showSaveScreenshotDialog()
+        if foo:
+            path, mime = foo[0], foo[1]
+            self._project.pipeline.save_thumbnail(-1, -1, mime, path)
+
     def deleteSelected(self, unused_action):
         if self.timeline:
             self.app.action_log.begin("delete clip")
@@ -1774,16 +1483,16 @@ class Timeline(Gtk.Table, Loggable, Zoomable):
         """
         Split clips at the current playhead position, regardless of selections.
         """
-        self.timeline.enable_update(False)
+        self.bTimeline.enable_update(False)
         position = self.app.current.pipeline.getPosition()
-        for track in self.timeline.get_tracks():
+        for track in self.bTimeline.get_tracks():
             for element in track.get_elements():
                 start = element.get_start()
                 end = start + element.get_duration()
                 if start < position and end > position:
                     clip = element.get_parent()
                     clip.split(position)
-        self.timeline.enable_update(True)
+        self.bTimeline.enable_update(True)
 
     def keyframe(self, action):
         """
@@ -1811,9 +1520,6 @@ class Timeline(Gtk.Table, Loggable, Zoomable):
                     interpolator.newKeyframe(position_in_obj)
                     self.app.action_log.commit()
 
-    def playPause(self, unused_action):
-        self.app.current.pipeline.togglePlayback()
-
     def _previousKeyframeCb(self, action):
         position = self.app.current.pipeline.getPosition()
         prev_kf = self.timeline.getPrevKeyframe(position)
@@ -1828,33 +1534,394 @@ class Timeline(Gtk.Table, Loggable, Zoomable):
             self._seeker.seek(next_kf)
             self.scrollToPlayhead()
 
-    def _screenshotCb(self, unused_action):
+    def playPause(self, unused_action):
+        self.app.current.pipeline.togglePlayback()
+
+    def setTimeline(self, bTimeline):
+        self.bTimeline = bTimeline
+        self.timeline.setTimeline(bTimeline)
+
+    def _scrollEventCb(self, embed, event):
+        # FIXME : see https://bugzilla.gnome.org/show_bug.cgi?id=697522
+        deltas = event.get_scroll_deltas()
+        if event.state & Gdk.ModifierType.CONTROL_MASK:
+            if deltas[2] < 0:
+                Zoomable.zoomIn()
+            elif deltas[2] > 0:
+                Zoomable.zoomOut()
+            self._scrollToPlayhead()
+        elif event.state & Gdk.ModifierType.SHIFT_MASK:
+            if deltas[2] > 0:
+                self._scrollDown()
+            elif deltas[2] < 0:
+                self._scrollUp()
+        else:
+            if deltas[2] > 0:
+                self._scrollRight()
+            elif deltas[2] < 0:
+                self._scrollLeft()
+        self.scrolled += 1
+
+    def testTimeline(self, timeline):
+        timeline.set_easing_duration(600)
+
+        Gst.init([])
+        GES.init()
+
+        self.project = GES.Project(uri=None, extractable_type=GES.Timeline)
+
+        bTimeline = GES.Timeline()
+        bTimeline.add_track(GES.Track.audio_raw_new())
+        bTimeline.add_track(GES.Track.video_raw_new())
+
+        timeline.setTimeline(bTimeline)
+
+        layer = GES.Layer()
+        bTimeline.add_layer(layer)
+
+        self.bTimeline = bTimeline
+
+        self.project.connect("asset-added", self._doAssetAddedCb, layer)
+        self.project.create_asset("file://" + sys.argv[1], GES.UriClip)
+
+
+def get_preview_for_object(bElement):
+    track_type = bElement.get_track_type()
+    if track_type == GES.TrackType.AUDIO:
+        # FIXME: RandomAccessAudioPreviewer doesn't work yet
+        # previewers[key] = RandomAccessAudioPreviewer(instance, uri)
+        # TODO: return waveform previewer
+        return Clutter.Actor()
+    elif track_type == GES.TrackType.VIDEO:
+        if bElement.get_parent().is_image():
+            # TODO: return still image previewer
+            return Clutter.Actor()
+        else:
+            return VideoPreviewer(bElement)
+    else:
+        return Clutter.Actor()
+
+
+class VideoPreviewer(Clutter.Actor, Zoomable):
+    def __init__(self, bElement):
         """
-        Export a snapshot of the current frame as an image file.
+        @param bElement : the backend GES.TrackElement
+        @param track : the track to which the bElement belongs
+        @param timeline : the containing graphic timeline.
         """
-        foo = self._showSaveScreenshotDialog()
-        if foo:
-            path, mime = foo[0], foo[1]
-            self._project.pipeline.save_thumbnail(-1, -1, mime, path)
+        Zoomable.__init__(self)
+        Clutter.Actor.__init__(self)
 
-    def insertEnd(self, assets):
+        self.uri = bElement.props.uri
+
+        self.layoutManager = Clutter.BinLayout()
+        self.set_layout_manager(self.layoutManager)
+
+        self.bElement = bElement
+
+#        self.bElement.connect("notify::duration", self.element_changed)
+#        self.bElement.connect("notify::in-point", self.element_changed)
+
+        self.duration = self.bElement.get_duration()
+        self.in_point = self.bElement.get_inpoint()
+
+        self.thumb_margin = BORDER_WIDTH
+        self.thumb_height = EXPANDED_SIZE - 2 * self.thumb_margin
+        # self.thumb_width will be set by self._setupPipeline()
+
+        # TODO: read this property from the settings
+        self.thumb_period = long(0.1 * Gst.SECOND)
+
+        # maps (quantized) times to Thumbnail objects
+        self.thumbs = {}
+
+        self.thumb_cache = ThumbnailCache(uri=self.uri)
+
+        self.queue = []
+
+        self.waiting_timestamp = None
+
+        self._setupPipeline()
+
+        self.callback_id = None
+
+    # Internal API
+
+    def _setupPipeline(self):
         """
-        Add source at the end of the timeline
-        @type sources: An L{GES.TimelineSource}
-        @param x2: A list of sources to add to the timeline
+        Create the pipeline.
+
+        It has the form "playbin ! thumbnailsink" where thumbnailsink
+        is a Bin made out of "capsfilter ! gdkpixbufsink"
         """
-        self.app.action_log.begin("add clip")
-        # FIXME we should find the longets layer instead of adding it to the
-        # first one
-        # Handle the case of a blank project
-        layer = self._ensureLayer()[0]
-        self.timeline.enable_update(False)
-        for asset in assets:
-            if asset.is_image():
-                clip_duration = long(long(self._settings.imageClipLength) * Gst.SECOND / 1000)
+        self.pipeline = Gst.ElementFactory.make("playbin", None)
+        self.pipeline.props.uri = self.uri
+        self.pipeline.props.flags = 1  # Only render video
+
+        # Set up the thumbnailsink
+        thumbnailsink = Gst.parse_bin_from_description("capsfilter 
caps=video/x-raw,format=(string)RGB,pixel-aspect-ratio=(fraction)1/1 ! gdkpixbufsink name=gdkpixbufsink", 
True)
+
+        # get the gdkpixbufsink and the automatically created ghostpad
+        self.gdkpixbufsink = thumbnailsink.get_by_name("gdkpixbufsink")
+        sinkpad = thumbnailsink.get_static_pad("sink")
+
+        # Connect the playbin and the thumbnailsink
+        self.pipeline.props.video_sink = thumbnailsink
+
+        # add a message handler that listens for the created pixbufs
+        self.pipeline.get_bus().add_signal_watch()
+        self.pipeline.get_bus().connect("message", self.bus_message_handler)
+
+        self.pipeline.set_state(Gst.State.PAUSED)
+        # Wait for the pipeline to be prerolled so we can check the width
+        # that the thumbnails will have and set the aspect ratio accordingly
+        # as well as getting the framerate of the video:
+        change_return = self.pipeline.get_state(Gst.CLOCK_TIME_NONE)
+        if Gst.StateChangeReturn.SUCCESS == change_return[0]:
+            neg_caps = sinkpad.get_current_caps()[0]
+            video_width = neg_caps["width"]
+            video_height = neg_caps["height"]
+            self.thumb_width = video_width * self.thumb_height / video_height
+        else:
+            # the pipeline couldn't be prerolled so we can't determine the
+            # correct values. Set sane defaults (this should never happen)
+            self.warning("Couldn't preroll the pipeline")
+            self.thumb_width = 16 * self.thumb_height / 9  # assume 16:9 aspect ratio
+
+    def _addThumbnails(self):
+        """
+        Adds thumbnails for the whole clip.
+
+        Takes the zoom setting into account and removes potentially
+        existing thumbnails prior to adding the new ones.
+        """
+        # TODO: check if duration or zoomratio really changed?
+        self.remove_all_children()
+        self.thumbs = {}
+
+        # calculate unquantized length of a thumb in nano seconds
+        thumb_duration_tmp = Zoomable.pixelToNs(self.thumb_width + self.thumb_margin)
+
+        # quantize thumb length to thumb_period
+        # TODO: replace with a call to utils.misc.quantize:
+        thumb_duration = (thumb_duration_tmp // self.thumb_period) * self.thumb_period
+        # make sure that the thumb duration after the quantization isn't smaller than before
+        if thumb_duration < thumb_duration_tmp:
+            thumb_duration += self.thumb_period
+
+        # make sure that we don't show thumbnails more often than thumb_period
+        thumb_duration = max(thumb_duration, self.thumb_period)
+
+        number_of_thumbs = self.duration / thumb_duration
+
+        current_time = 0
+        # +1 because wa want to draw the rightmost thumbnail even if it will be clipped
+        for i in range(0, number_of_thumbs + 1):
+            thumb = Thumbnail(self.thumb_width, self.thumb_height)
+            thumb.set_position(Zoomable.nsToPixel(current_time), self.thumb_margin)
+            self.add_child(thumb)
+            self.thumbs[current_time] = thumb
+            self._thumbForTime(current_time)
+            current_time += thumb_duration
+
+    def _thumbForTime(self, time):
+        if time in self.thumb_cache:
+            gdkpixbuf = self.thumb_cache[time]
+            self.thumbs[time].set_from_gdkpixbuf(gdkpixbuf)
+        else:
+            self._requestThumbnail(time)
+
+    def _requestThumbnail(self, time):
+        """Queue a thumbnail request for the given time"""
+        if time not in self.queue:  # and len(self._queue) <= self.max_requests:
+            if self.queue:
+                self.queue.append(time)
             else:
-                clip_duration = asset.get_duration()
+                self.queue.append(time)
+                self._nextThumbnail()
+
+    def _nextThumbnail(self):
+        """Notifies the preview object that the pipeline is ready to process
+        the next thumbnail in the queue. This should always be called from the
+        main application thread."""
+        if self.queue:
+            if not self._startThumbnail(self.queue[0]):
+                self.queue.pop(0)
+                self._nextThumbnail()
+        return False
+
+    def _startThumbnail(self, time):
+        self.waiting_timestamp = time
+        return self.pipeline.seek(1.0,
+            Gst.Format.TIME, Gst.SeekFlags.FLUSH | Gst.SeekFlags.ACCURATE,
+            Gst.SeekType.SET, time,
+            Gst.SeekType.NONE, -1)
+
+    def _finishThumbnail(self, gdkpixbuf, time):
+        """Notifies the preview object that the a new thumbnail is ready to be
+        cached. This should be called by subclasses when they have finished
+        processing the thumbnail for the current segment. This function should
+        always be called from the main thread of the application."""
+        waiting = self.waiting_timestamp
+        self.waiting_timestamp = None
+
+        if time != waiting:
+            time = waiting
+
+        thumbnail = gdkpixbuf.scale_simple(self.thumb_width, self.thumb_height, 3)
+
+        self.thumb_cache[time] = thumbnail
+
+        if time in self.thumbs:
+            self.thumbs[time].set_from_gdkpixbuf(thumbnail)
+        #self.emit("update", time)
+
+        if time in self.queue:
+            self.queue.remove(time)
+        self._nextThumbnail()
+        return False
+
+    # Interface (Zoomable)
+
+    def _maybeUpdate(self):
+        self._addThumbnails()
+        self.callback_id = None
+        return False
+
+    def zoomChanged(self):
+        if self.callback_id is not None:
+            GObject.source_remove(self.callback_id)
+        self.callback_id = GObject.timeout_add(100, self._maybeUpdate)
+
+    # Callbacks
+
+    def bus_message_handler(self, unused_bus, message):
+        if message.type == Gst.MessageType.ELEMENT and \
+                message.src == self.gdkpixbufsink:
+            struct = message.get_structure()
+
+            # TODO: does struct.get_name() work?
+            #if struct.get_name() == "pixbuf":
+
+            # TODO: there exists no value named "timestamp"
+            #self._finishThumbnail(struct.get_value("pixbuf"), struct.get_value("timestamp"))
+            GLib.idle_add(self._finishThumbnail, struct.get_value("pixbuf"),
+                    struct.get_value("timestamp"))
+        return Gst.BusSyncReply.PASS
+
+    #bElement = receiver()
+
+    # handler(bElement, "notify::duration")
+    # handler(bElement, "notify::in-point")
+    def element_changed(self, unused_bElement, unused_start_duration):
+        self.duration = self.bElement.get_duration()
+        self.in_point = self.bElement.get_inpoint()
+        GLib.idle_add(self._addThumbnails)
+
+
+class Thumbnail(Clutter.Actor):
+
+    def __init__(self, width, height):
+        Clutter.Actor.__init__(self)
+        image = Clutter.Image.new()
+        self.props.content = image
+        self.width = width
+        self.height = height
+        self.set_background_color(Clutter.Color.new(0, 100, 150, 100))
+        self.set_size(self.width, self.height)
+
+    def set_from_gdkpixbuf(self, gdkpixbuf):
+        row_stride = gdkpixbuf.get_rowstride()
+        pixel_data = gdkpixbuf.get_pixels()
+        # Cogl.PixelFormat.RGB_888 := 2
+        self.props.content.set_data(pixel_data, Cogl.PixelFormat.RGB_888, self.width, self.height, 
row_stride)
+
+
+# TODO: replace with utils.misc.hash_file
+def hash_file(uri):
+    """Hashes the first 256KB of the specified file"""
+    sha256 = hashlib.sha256()
+    with open(uri, "rb") as file:
+        for _ in range(1024):
+            chunk = file.read(256)
+            if not chunk:
+                break
+            sha256.update(chunk)
+    return sha256.hexdigest()
+
+# TODO: remove eventually
+autocreate = True
+
+
+# TODO: replace with pitivi.settings.get_dir
+def get_dir(path, autocreate=True):
+    if autocreate and not os.path.exists(path):
+        os.makedirs(path)
+    return path
+
+
+class ThumbnailCache(object):
+
+    """Caches thumbnails by key using LRU policy, implemented with heapq.
+
+    Uses a two stage caching mechanism. A limited number of elements are
+    held in memory, the rest is being cached on disk using an sqlite db."""
+
+    def __init__(self, uri):
+        object.__init__(self)
+        # TODO: replace with utils.misc.hash_file
+        self.hash = hash_file(Gst.uri_get_location(uri))
+        # TODO: replace with pitivi.settings.xdg_cache_home()
+        cache_dir = get_dir(os.path.join(xdg_dirs.xdg_cache_home, "pitivi"), autocreate)
+        dbfile = os.path.join(get_dir(os.path.join(cache_dir, "thumbs")), self.hash)
+        self.conn = sqlite3.connect(dbfile)
+        self.cur = self.conn.cursor()
+        self.cur.execute("CREATE TABLE IF NOT EXISTS Thumbs (Time INTEGER NOT NULL PRIMARY KEY,\
+            Data BLOB NOT NULL, Width INTEGER NOT NULL, Height INTEGER NOT NULL, Stride INTEGER NOT NULL)")
+
+    def __contains__(self, key):
+        # check if item is present in on disk cache
+        self.cur.execute("SELECT Time FROM Thumbs WHERE Time = ?", (key,))
+        if self.cur.fetchone():
+            return True
+        return False
 
-            layer.add_asset(asset, self.timeline.props.duration,
-                0, clip_duration, asset.get_supported_formats())
-        self.timeline.enable_update(True)
+    def __getitem__(self, key):
+        self.cur.execute("SELECT * FROM Thumbs WHERE Time = ?", (key,))
+        row = self.cur.fetchone()
+        if row:
+            pixbuf = GdkPixbuf.Pixbuf.new_from_data(row[1],
+                                                    GdkPixbuf.Colorspace.RGB,
+                                                    False,
+                                                    8,
+                                                    row[2],
+                                                    row[3],
+                                                    row[4],
+                                                    None,
+                                                    None)
+            return pixbuf
+        raise KeyError(key)
+
+    def __setitem__(self, key, value):
+        blob = sqlite3.Binary(bytearray(value.get_pixels()))
+        #Replace if the key already existed
+        self.cur.execute("DELETE FROM Thumbs WHERE  time=?", (key,))
+        self.cur.execute("INSERT INTO Thumbs VALUES (?,?,?,?,?)", (key, blob, value.get_width(), 
value.get_height(), value.get_rowstride()))
+        self.conn.commit()
+
+if __name__ == "__main__":
+    # Basic argument handling, no need for getopt here
+    if len(sys.argv) < 2:
+        print "Supply a uri as argument"
+        sys.exit()
+
+    print "Starting stupid demo, using uri as a new clip, with start = 2, duration = 25 and inpoint = 5."
+    print "Use ipython if you want to interact with the timeline in a more interesting way"
+    print "ipython ; %gui gtk3 ; %run timeline.py ; help yourself"
+
+    window = Gtk.Window()
+    widget = Timeline()
+    window.add(widget)
+    window.maximize()
+    window.show_all()
+    widget.run()
diff --git a/pitivi/utils/timeline.py b/pitivi/utils/timeline.py
index 2b5b5db..1c4d9e0 100644
--- a/pitivi/utils/timeline.py
+++ b/pitivi/utils/timeline.py
@@ -49,6 +49,40 @@ class TimelineError(Exception):
     pass
 
 
+class Selected(Signallable):
+    """
+    A simple class that let us emit a selected-changed signal
+    when needed, and that can be check directly to know if the
+    object is selected or not.
+
+    This is meant only for individual elements, do not confuse this with
+    utils.timeline's "Selection" class.
+    """
+
+    __signals__ = {
+        "selected-changed": []}
+
+    def __init__(self):
+        self._selected = False
+        self.movable = True
+
+    def __nonzero__(self):
+        """
+        checking a Selected object is the same as checking its _selected
+        property
+        """
+        return self._selected
+
+    def getSelected(self):
+        return self._selected
+
+    def setSelected(self, selected):
+        self._selected = selected
+        self.emit("selected-changed", selected)
+
+    selected = property(getSelected, setSelected)
+
+
 class Selection(Signallable):
     """
     A collection of L{GES.Clip}.


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