[pitivi] Remove unused imports, and separate elements and previewers from the timeline code



commit 41248171a60f225cca3263e4a8359717a6e3a15c
Author: Mathieu Duponchelle <mathieu duponchelle epitech eu>
Date:   Wed Apr 17 02:31:06 2013 +0200

    Remove unused imports, and separate elements and previewers from the timeline code

 pitivi/timeline/elements.py   |  529 +++++++++++++++++++++++
 pitivi/timeline/previewers.py |  413 ++++++++++++++++++
 pitivi/timeline/timeline.py   |  951 +----------------------------------------
 pitivi/utils/ui.py            |    7 +-
 4 files changed, 951 insertions(+), 949 deletions(-)
---
diff --git a/pitivi/timeline/elements.py b/pitivi/timeline/elements.py
new file mode 100644
index 0000000..63b3fbe
--- /dev/null
+++ b/pitivi/timeline/elements.py
@@ -0,0 +1,529 @@
+"""
+Convention throughout this file:
+Every GES element which name could be mistaken with a UI element
+is prefixed with a little b, example : bTimeline
+"""
+
+import os
+
+from gi.repository import Clutter, Cogl, GES
+from pitivi.utils.timeline import Zoomable, EditingContext, Selection, SELECT, UNSELECT, Selected
+from previewers import VideoPreviewer, BORDER_WIDTH
+
+import pitivi.configure as configure
+from pitivi.utils.ui import EXPANDED_SIZE, SPACING
+
+
+def get_preview_for_object(bElement, timeline):
+    # Fixme special preview for transitions, titles
+    if not isinstance(bElement.get_parent(), GES.UriClip):
+        return Clutter.Actor()
+    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, timeline)
+    else:
+        return Clutter.Actor()
+
+
+class RoundedRectangle(Clutter.Actor):
+    """
+    Custom actor used to draw a rectangle that can have rounded corners
+    """
+    __gtype_name__ = 'RoundedRectangle'
+
+    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()
+
+        self.isSelected = False
+
+        self.timelineElement = timelineElement
+
+        self.set_reactive(True)
+
+        self.dragAction = Clutter.DragAction()
+        self.add_action(self.dragAction)
+
+        self.dragAction.connect("drag-begin", self._dragBeginCb)
+        self.dragAction.connect("drag-end", self._dragEndCb)
+        self.dragAction.connect("drag-progress", self._dragProgressCb)
+
+        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)
+
+    #Callbacks
+
+    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 _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))
+
+    def _elementEnterEventCb(self, actor, event):
+        self.show()
+
+    def _elementLeaveEventCb(self, actor, event):
+        if not self.isSelected:
+            self.hide()
+
+    def _selectedChangedCb(self, selected, isSelected):
+        self.isSelected = isSelected
+        self.props.visible = isSelected
+
+    def _dragBeginCb(self, action, actor, event_x, event_y, modifiers):
+        self.timelineElement.setDragged(True)
+        elem = self.timelineElement.bElement.get_parent()
+
+        if self.isLeft:
+            edge = GES.Edge.EDGE_START
+            self._dragBeginStart = self.timelineElement.bElement.get_parent().get_start()
+        else:
+            edge = GES.Edge.EDGE_END
+            self._dragBeginStart = self.timelineElement.bElement.get_parent().get_duration() + \
+                self.timelineElement.bElement.get_parent().get_start()
+
+        self._context = EditingContext(elem,
+                                       self.timelineElement.timeline.bTimeline,
+                                       GES.EditMode.EDIT_TRIM,
+                                       edge,
+                                       set([]),
+                                       None)
+
+        self.dragBeginStartX = event_x
+        self.dragBeginStartY = event_y
+
+    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)
+
+        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):
+        """
+        @param bElement : the backend GES.TrackElement
+        @param track : the track to which the bElement belongs
+        @param timeline : the containing graphic timeline.
+        """
+        Zoomable.__init__(self)
+        Clutter.Actor.__init__(self)
+
+        self.timeline = timeline
+
+        self.bElement = bElement
+
+        self.bElement.selected = Selected()
+        self.bElement.selected.connect("selected-changed", self._selectedChangedCb)
+
+        self._createBackground(track)
+
+        self._createPreview()
+
+        self._createBorder()
+
+        self._createMarquee()
+
+        self._createHandles()
+
+        self._createGhostclip()
+
+        self.track_type = self.bElement.get_track_type()  # This won't change
+
+        self.isDragged = False
+
+        size = self.bElement.get_duration()
+        self.set_size(self.nsToPixel(size), EXPANDED_SIZE, False)
+        self.set_reactive(True)
+        self._connectToEvents()
+
+    # Public API
+
+    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)
+            try:
+                self.rightHandle.save_easing_state()
+                self.rightHandle.set_easing_duration(600)
+            except AttributeError:  # Element doesnt't have handles
+                pass
+
+        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)
+        try:
+            self.rightHandle.set_position(width - self.rightHandle.props.width, 0)
+        except AttributeError:  # Element doesnt't have handles
+                pass
+
+        if ease:
+            self.background.restore_easing_state()
+            self.border.restore_easing_state()
+            self.preview.restore_easing_state()
+            try:
+                self.rightHandle.restore_easing_state()
+            except AttributeError:  # Element doesnt't have handles
+                pass
+            self.restore_easing_state()
+
+    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):
+        pass
+
+    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):
+        pass
+
+    def _createHandles(self):
+        pass
+
+    def _createPreview(self):
+        self.preview = get_preview_for_object(self.bElement, self.timeline)
+        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
+
+    # Interface (Zoomable)
+
+    def zoomChanged(self):
+        self.update(True)
+
+    # Callbacks
+
+    def _clickedCb(self, action, actor):
+        pass
+
+    def _dragBeginCb(self, action, actor, event_x, event_y, modifiers):
+        pass
+
+    def _dragProgressCb(self, action, actor, delta_x, delta_y):
+        return False
+
+    def _dragEndCb(self, action, actor, event_x, event_y, modifiers):
+        pass
+
+    def _selectedChangedCb(self, selected, isSelected):
+        self.marquee.props.visible = isSelected
+
+
+class ClipElement(TimelineElement):
+    def __init__(self, bElement, track, timeline):
+        TimelineElement.__init__(self, bElement, track, timeline)
+
+    # public API
+    def updateGhostclip(self, priority, y, isControlledByBrother):
+        # Only tricky part of the code, can be called by the linked track element.
+        if priority < 0:
+            return
+
+        # Here we make it so the calculation is the same for audio and video.
+        if self.track_type == GES.TrackType.AUDIO and not isControlledByBrother:
+            y -= self.nbrLayers * (EXPANDED_SIZE + SPACING)
+
+        # And here we take into account the fact that the pointer might actually be
+        # on the other track element, meaning we have to offset it.
+        if isControlledByBrother:
+            if self.track_type == GES.TrackType.AUDIO:
+                y += self.nbrLayers * (EXPANDED_SIZE + SPACING)
+            else:
+                y -= self.nbrLayers * (EXPANDED_SIZE + SPACING)
+
+        # Would that be a new layer ?
+        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
+
+    # private 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 _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 _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)
+
+    # Callbacks
+    def _clickedCb(self, action, actor):
+        #TODO : Let's be more specific, masks etc ..
+        self.timeline.selection.setToObj(self.bElement, SELECT)
+
+    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)
+
+        # 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
+
+        self._dragBeginStart = self.bElement.get_start()
+        self.dragBeginStartX = event_x
+        self.dragBeginStartY = event_y
+
+    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
+
+        y = coords[1] + self.timeline._container.point.y
+
+        priority = self._getLayerForY(y)
+
+        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)
+
+        new_start = self._dragBeginStart + self.pixelToNs(delta_x)
+
+        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
+
+    def _dragEndCb(self, action, actor, event_x, event_y, modifiers):
+        coords = self.dragAction.get_motion_coords()
+        delta_x = coords[0] - self.dragBeginStartX
+        new_start = self._dragBeginStart + self.pixelToNs(delta_x)
+
+        priority = self._getLayerForY(coords[1] + self.timeline._container.point.y)
+        priority = min(priority, len(self.timeline.bTimeline.get_layers()))
+
+        self.timeline._snapEndedCb()
+
+        self.setDragged(False)
+
+        self.ghostclip.props.visible = False
+        if self.brother:
+            self.brother.ghostclip.props.visible = False
+
+        priority = max(0, priority)
+        self._context.editTo(new_start, priority)
+        self._context.finish()
+
+    def _selectedChangedCb(self, selected, isSelected):
+        self.marquee.props.visible = isSelected
+
+
+class TransitionElement(TimelineElement):
+    def __init__(self, bElement, track, timeline):
+        TimelineElement.__init__(self, bElement, track, timeline)
+        self.isDragged = True
+
+    def _createBackground(self, track):
+        self.background = RoundedRectangle(0, 0, 5, 5)
+        color = Cogl.Color()
+        color.init_from_4ub(100, 100, 100, 125)
+        self.background.set_color(color)
+        self.background.set_border_width(3)
+
+        self.background.set_position(0, 0)
+        self.add_child(self.background)
diff --git a/pitivi/timeline/previewers.py b/pitivi/timeline/previewers.py
new file mode 100644
index 0000000..5c21e48
--- /dev/null
+++ b/pitivi/timeline/previewers.py
@@ -0,0 +1,413 @@
+import hashlib
+import os
+import sqlite3
+import sys
+import xdg.BaseDirectory as xdg_dirs
+
+from gi.repository import Clutter, Gst, GLib, GdkPixbuf, Cogl
+from pitivi.utils.timeline import Zoomable
+from pitivi.utils.ui import EXPANDED_SIZE, SPACING
+
+BORDER_WIDTH = 3  # For the timeline elements
+
+
+class VideoPreviewer(Clutter.ScrollActor, Zoomable):
+    def __init__(self, bElement, timeline):
+        """
+        @param bElement : the backend GES.TrackElement
+        @param track : the track to which the bElement belongs
+        @param timeline : the containing graphic timeline.
+        """
+        Zoomable.__init__(self)
+        Clutter.ScrollActor.__init__(self)
+
+        self.uri = bElement.props.uri
+
+        self.bElement = bElement
+        self.timeline = timeline
+
+        self.bElement.connect("notify::duration", self.duration_changed)
+        self.bElement.connect("notify::in-point", self._inpoint_changed_cb)
+        self.bElement.connect("notify::start", self.start_changed)
+
+        self.timeline.connect("scrolled", self._scroll_changed)
+
+        self.duration = self.bElement.props.duration
+
+        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.5 * Gst.SECOND)
+
+        # maps (quantized) times to Thumbnail objects
+        self.thumbs = {}
+
+        self.thumb_cache = ThumbnailCache(uri=self.uri)
+
+        self.wishlist = []
+
+        self._setupPipeline()
+
+        self._startThumbnailing()
+
+        self.callback_id = None
+
+    # Internal API
+
+    def _scroll_changed(self, unused):
+        self._updateWishlist()
+
+    def start_changed(self, unused_bElement, unused_value):
+        self._updateWishlist()
+
+    def _update(self, unused_msg_source=None):
+        if self.callback_id:
+            GLib.source_remove(self.callback_id)
+        self.callback_id = GLib.idle_add(self._addAllThumbnails, priority=GLib.PRIORITY_LOW)
+
+    def _setupPipeline(self):
+        """
+        Create the pipeline.
+
+        It has the form "playbin ! thumbnailsink" where thumbnailsink
+        is a Bin made out of "videorate ! capsfilter ! gdkpixbufsink"
+        """
+        # TODO: don't hardcode framerate
+        self.pipeline = Gst.parse_launch(
+            "uridecodebin uri={uri} ! "
+            "videoconvert ! "
+            "videorate ! "
+            "videoscale method=lanczos ! "
+            "capsfilter caps=video/x-raw,format=(string)RGBA,height=(int){height},"
+            "pixel-aspect-ratio=(fraction)1/1,framerate=2/1 ! "
+            "gdkpixbufsink name=gdkpixbufsink".format(uri=self.uri, height=self.thumb_height))
+
+        # get the gdkpixbufsink and the sinkpad
+        self.gdkpixbufsink = self.pipeline.get_by_name("gdkpixbufsink")
+        sinkpad = self.gdkpixbufsink.get_static_pad("sink")
+
+        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]
+            self.thumb_width = neg_caps["width"]
+        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")
+            # assume 16:9 aspect ratio
+            self.thumb_width = 16 * self.thumb_height / 9
+
+        # pop all messages from the bus so we won't be flooded with messages
+        # from the prerolling phase
+        while self.pipeline.get_bus().pop():
+            continue
+        # 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)
+
+    def _startThumbnailing(self):
+        self.queue = []
+        query_success, duration = self.pipeline.query_duration(Gst.Format.TIME)
+        if not query_success:
+            print("Could not determine the duration of the file {}".format(self.uri))
+            duration = self.duration
+        else:
+            self.duration = duration
+
+        current_time = 0
+        while current_time < duration:
+            self.queue.append(current_time)
+            current_time += self.thumb_period
+
+        self._create_next_thumb()
+
+    def _create_next_thumb(self):
+        if not self.queue:
+            # nothing left to do
+            self.thumb_cache.commit()
+            return
+        wish = self._get_wish()
+        if wish:
+            time = wish
+            self.queue.remove(wish)
+        else:
+            time = self.queue.pop(0)
+        # append the time to the end of the queue so that if this seek fails
+        # another try will be started later
+        self.queue.append(time)
+
+        self.pipeline.seek(1.0,
+            Gst.Format.TIME, Gst.SeekFlags.FLUSH | Gst.SeekFlags.ACCURATE,
+            Gst.SeekType.SET, time,
+            Gst.SeekType.NONE, -1)
+
+    def _addAllThumbnails(self):
+        self.remove_all_children()
+        self.thumbs = {}
+
+        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)
+
+        current_time = 0
+        while current_time < self.duration:
+            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
+            if current_time in self.thumb_cache:
+                gdkpixbuf = self.thumb_cache[current_time]
+                self.thumbs[current_time].set_from_gdkpixbuf(gdkpixbuf)
+            current_time += thumb_duration
+
+        self._updateWishlist()
+
+    def _inpoint_changed_cb(self, unused_bElement, unused_value):
+        position = Clutter.Point()
+        position.x = Zoomable.nsToPixel(self.bElement.props.in_point)
+        self.scroll_to_point(position)
+        self._updateWishlist()
+
+    def _updateWishlist(self):
+        """
+        Adds thumbnails for the whole clip.
+
+        Takes the zoom setting into account and removes potentially
+        existing thumbnails prior to adding the new ones.
+        """
+        self.wishlist = []
+
+        # 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)
+
+        element_left, element_right = self._get_visible_range()
+        # TODO: replace with a call to utils.misc.quantize:
+        element_left = (element_left // thumb_duration) * thumb_duration
+
+        current_time = element_left
+        while current_time < element_right:
+            if current_time not in self.thumb_cache:
+                self.wishlist.append(current_time)
+            current_time += thumb_duration
+
+    def _get_wish(self):
+        """Returns a wish that is also in the queue or None
+           if no such wish exists"""
+        while True:
+            if not self.wishlist:
+                return None
+            wish = self.wishlist.pop(0)
+            if wish in self.queue:
+                return wish
+
+    def _setThumbnail(self, time, thumbnail):
+        # TODO: is "time" guaranteed to be nanosecond precise?
+        # => __tim says: "that's how it should be"
+        # => also see gst-plugins-good/tests/icles/gdkpixbufsink-test
+        # => Daniel: It is *not* nanosecond precise when we remove the videorate
+        #            element from the pipeline
+        if time in self.queue:
+            self.queue.remove(time)
+
+        self.thumb_cache[time] = thumbnail
+
+        if time in self.thumbs:
+            self.thumbs[time].set_from_gdkpixbuf(thumbnail)
+
+    # Interface (Zoomable)
+
+    def zoomChanged(self):
+        self._update()
+
+    def _get_visible_range(self):
+        timeline_left, timeline_right = self._get_visible_timeline_range()
+        element_left = timeline_left - self.bElement.props.start + self.bElement.props.in_point
+        element_left = max(element_left, self.bElement.props.in_point)
+
+        element_right = timeline_right - self.bElement.props.start + self.bElement.props.in_point
+        element_right = min(element_right, self.bElement.props.in_point + self.bElement.props.duration)
+
+        return (element_left, element_right)
+
+    # TODO: move to Timeline or to utils
+    def _get_visible_timeline_range(self):
+        # determine the visible left edge of the timeline
+        # TODO: isn't there some easier way to get the scroll point of the ScrollActor?
+        # timeline_left = -(self.timeline.get_transform().xw - self.timeline.props.x)
+        timeline_left = self.timeline.get_scroll_point().x
+
+        # determine the width of the pipeline
+        # by intersecting the timeline's and the stage's allocation
+        timeline_allocation = self.timeline.props.allocation
+        stage_allocation = self.timeline.get_stage().props.allocation
+
+        timeline_rect = Clutter.Rect()
+        timeline_rect.init(timeline_allocation.x1,
+                           timeline_allocation.y1,
+                           timeline_allocation.x2 - timeline_allocation.x1,
+                           timeline_allocation.y2 - timeline_allocation.y1)
+
+        stage_rect = Clutter.Rect()
+        stage_rect.init(stage_allocation.x1,
+                        stage_allocation.y1,
+                        stage_allocation.x2 - stage_allocation.x1,
+                        stage_allocation.y2 - stage_allocation.y1)
+
+        has_intersection, intersection = timeline_rect.intersection(stage_rect)
+
+        if not has_intersection:
+            return (0, 0)
+
+        timeline_width = intersection.size.width
+
+        # determine the visible right edge of the timeline
+        timeline_right = timeline_left + timeline_width
+
+        # convert to nanoseconds
+        time_left = Zoomable.pixelToNs(timeline_left)
+        time_right = Zoomable.pixelToNs(timeline_right)
+
+        return (time_left, time_right)
+
+    # Callbacks
+
+    def bus_message_handler(self, unused_bus, message):
+        if message.type == Gst.MessageType.ELEMENT and \
+                message.src == self.gdkpixbufsink:
+            struct = message.get_structure()
+            struct_name = struct.get_name()
+            if struct_name == "preroll-pixbuf":
+                self._setThumbnail(struct.get_value("stream-time"), struct.get_value("pixbuf"))
+        elif message.type == Gst.MessageType.ASYNC_DONE:
+            self._create_next_thumb()
+        return Gst.BusSyncReply.PASS
+
+    def duration_changed(self, unused_bElement, unused_value):
+        new_duration = max(self.duration, self.bElement.props.duration)
+        if new_duration > self.duration:
+            self.duration = new_duration
+            self._update()
+
+
+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()
+        alpha = gdkpixbuf.get_has_alpha()
+        if alpha:
+            self.props.content.set_data(pixel_data, Cogl.PixelFormat.RGBA_8888, self.width, self.height, 
row_stride)
+        else:
+            self.props.content.set_data(pixel_data, Cogl.PixelFormat.RGB_888, self.width, self.height, 
row_stride)
+
+
+# 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
+        filehash = 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")), filehash)
+        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,\
+                          Jpeg BLOB 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
+
+    def __getitem__(self, key):
+        self.cur.execute("SELECT * FROM Thumbs WHERE Time = ?", (key,))
+        row = self.cur.fetchone()
+        if row:
+            jpeg = row[1]
+            loader = GdkPixbuf.PixbufLoader.new()
+            # TODO: what do to if any of the following calls fails?
+            loader.write(jpeg)
+            loader.close()
+            pixbuf = loader.get_pixbuf()
+            return pixbuf
+        raise KeyError(key)
+
+    def __setitem__(self, key, value):
+        success, jpeg = value.save_to_bufferv("jpeg", ["quality", None], ["90"])
+        if not success:
+            self.warning("JPEG compression failed")
+            return
+        blob = sqlite3.Binary(jpeg)
+        #Replace if the key already existed
+        self.cur.execute("DELETE FROM Thumbs WHERE  time=?", (key,))
+        self.cur.execute("INSERT INTO Thumbs VALUES (?,?)", (key, blob,))
+        #self.conn.commit()
+
+    def commit(self):
+        print("commit")
+        self.conn.commit()
diff --git a/pitivi/timeline/timeline.py b/pitivi/timeline/timeline.py
index 12d5999..411425e 100644
--- a/pitivi/timeline/timeline.py
+++ b/pitivi/timeline/timeline.py
@@ -1,40 +1,17 @@
 from gi.repository import GtkClutter
 GtkClutter.init([])
-from gi.repository import Gst
-from gi.repository import GES
-from gi.repository import GObject
 
-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
-
-import pitivi.configure as configure
-
-from pitivi.viewer import ViewerWidget
-
-from pitivi.utils.timeline import Zoomable, EditingContext, Selection, SELECT, UNSELECT, Selected
+from gi.repository import Gst, GES, GObject, Clutter, Gtk, GLib, Gdk
 
+from pitivi.utils.timeline import Zoomable, Selection, UNSELECT
 from pitivi.settings import GlobalSettings
-
 from pitivi.dialogs.prefs import PreferencesDialog
-
+from pitivi.utils.ui import EXPANDED_SIZE, SPACING
 from ruler import ScaleRuler
-
-from datetime import datetime
-
 from gettext import gettext as _
-
 from pitivi.utils.pipeline import Pipeline
-
 from layer import VideoLayerControl, AudioLayerControl
+from elements import ClipElement, TransitionElement
 
 GlobalSettings.addConfigOption('edgeSnapDeadband',
     section="user-interface",
@@ -63,12 +40,8 @@ PreferencesDialog.addNumericPreference('imageClipLength',
 
 # CONSTANTS
 
-EXPANDED_SIZE = 65
-SPACING = 10
 CONTROL_WIDTH = 250
 
-BORDER_WIDTH = 3  # For the timeline elements
-
 # tooltip text for toolbar
 DELETE = _("Delete Selected")
 SPLIT = _("Split clip at playhead position")
@@ -139,500 +112,6 @@ is prefixed with a little b, example : bTimeline
 """
 
 
-class RoundedRectangle(Clutter.Actor):
-    """
-    Custom actor used to draw a rectangle that can have rounded corners
-    """
-    __gtype_name__ = 'RoundedRectangle'
-
-    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()
-
-        self.isSelected = False
-
-        self.timelineElement = timelineElement
-
-        self.set_reactive(True)
-
-        self.dragAction = Clutter.DragAction()
-        self.add_action(self.dragAction)
-
-        self.dragAction.connect("drag-begin", self._dragBeginCb)
-        self.dragAction.connect("drag-end", self._dragEndCb)
-        self.dragAction.connect("drag-progress", self._dragProgressCb)
-
-        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)
-
-    #Callbacks
-
-    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 _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))
-
-    def _elementEnterEventCb(self, actor, event):
-        self.show()
-
-    def _elementLeaveEventCb(self, actor, event):
-        if not self.isSelected:
-            self.hide()
-
-    def _selectedChangedCb(self, selected, isSelected):
-        self.isSelected = isSelected
-        self.props.visible = isSelected
-
-    def _dragBeginCb(self, action, actor, event_x, event_y, modifiers):
-        self.timelineElement.setDragged(True)
-        elem = self.timelineElement.bElement.get_parent()
-
-        if self.isLeft:
-            edge = GES.Edge.EDGE_START
-            self._dragBeginStart = self.timelineElement.bElement.get_parent().get_start()
-        else:
-            edge = GES.Edge.EDGE_END
-            self._dragBeginStart = self.timelineElement.bElement.get_parent().get_duration() + \
-                self.timelineElement.bElement.get_parent().get_start()
-
-        self._context = EditingContext(elem,
-                                       self.timelineElement.timeline.bTimeline,
-                                       GES.EditMode.EDIT_TRIM,
-                                       edge,
-                                       set([]),
-                                       None)
-
-        self.dragBeginStartX = event_x
-        self.dragBeginStartY = event_y
-
-    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)
-
-        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):
-        """
-        @param bElement : the backend GES.TrackElement
-        @param track : the track to which the bElement belongs
-        @param timeline : the containing graphic timeline.
-        """
-        Zoomable.__init__(self)
-        Clutter.Actor.__init__(self)
-
-        self.timeline = timeline
-
-        self.bElement = bElement
-
-        self.bElement.selected = Selected()
-        self.bElement.selected.connect("selected-changed", self._selectedChangedCb)
-
-        self._createBackground(track)
-
-        self._createPreview()
-
-        self._createBorder()
-
-        self._createMarquee()
-
-        self._createHandles()
-
-        self._createGhostclip()
-
-        self.track_type = self.bElement.get_track_type()  # This won't change
-
-        self.isDragged = False
-
-        size = self.bElement.get_duration()
-        self.set_size(self.nsToPixel(size), EXPANDED_SIZE, False)
-        self.set_reactive(True)
-        self._connectToEvents()
-
-    # Public API
-
-    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)
-            try:
-                self.rightHandle.save_easing_state()
-                self.rightHandle.set_easing_duration(600)
-            except AttributeError:  # Element doesnt't have handles
-                pass
-
-        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)
-        try:
-            self.rightHandle.set_position(width - self.rightHandle.props.width, 0)
-        except AttributeError:  # Element doesnt't have handles
-                pass
-
-        if ease:
-            self.background.restore_easing_state()
-            self.border.restore_easing_state()
-            self.preview.restore_easing_state()
-            try:
-                self.rightHandle.restore_easing_state()
-            except AttributeError:  # Element doesnt't have handles
-                pass
-            self.restore_easing_state()
-
-    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):
-        pass
-
-    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):
-        pass
-
-    def _createHandles(self):
-        pass
-
-    def _createPreview(self):
-        self.preview = get_preview_for_object(self.bElement, self.timeline)
-        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
-
-    # Interface (Zoomable)
-
-    def zoomChanged(self):
-        self.update(True)
-
-    # Callbacks
-
-    def _clickedCb(self, action, actor):
-        pass
-
-    def _dragBeginCb(self, action, actor, event_x, event_y, modifiers):
-        pass
-
-    def _dragProgressCb(self, action, actor, delta_x, delta_y):
-        return False
-
-    def _dragEndCb(self, action, actor, event_x, event_y, modifiers):
-        pass
-
-    def _selectedChangedCb(self, selected, isSelected):
-        self.marquee.props.visible = isSelected
-
-
-class ClipElement(TimelineElement):
-    def __init__(self, bElement, track, timeline):
-        TimelineElement.__init__(self, bElement, track, timeline)
-
-    # public API
-    def updateGhostclip(self, priority, y, isControlledByBrother):
-        # Only tricky part of the code, can be called by the linked track element.
-        if priority < 0:
-            return
-
-        # Here we make it so the calculation is the same for audio and video.
-        if self.track_type == GES.TrackType.AUDIO and not isControlledByBrother:
-            y -= self.nbrLayers * (EXPANDED_SIZE + SPACING)
-
-        # And here we take into account the fact that the pointer might actually be
-        # on the other track element, meaning we have to offset it.
-        if isControlledByBrother:
-            if self.track_type == GES.TrackType.AUDIO:
-                y += self.nbrLayers * (EXPANDED_SIZE + SPACING)
-            else:
-                y -= self.nbrLayers * (EXPANDED_SIZE + SPACING)
-
-        # Would that be a new layer ?
-        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
-
-    # private 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 _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 _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)
-
-    # Callbacks
-    def _clickedCb(self, action, actor):
-        #TODO : Let's be more specific, masks etc ..
-        self.timeline.selection.setToObj(self.bElement, SELECT)
-
-    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)
-
-        # 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
-
-        self._dragBeginStart = self.bElement.get_start()
-        self.dragBeginStartX = event_x
-        self.dragBeginStartY = event_y
-
-    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
-
-        y = coords[1] + self.timeline._container.point.y
-
-        priority = self._getLayerForY(y)
-
-        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)
-
-        new_start = self._dragBeginStart + self.pixelToNs(delta_x)
-
-        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
-
-    def _dragEndCb(self, action, actor, event_x, event_y, modifiers):
-        coords = self.dragAction.get_motion_coords()
-        delta_x = coords[0] - self.dragBeginStartX
-        new_start = self._dragBeginStart + self.pixelToNs(delta_x)
-
-        priority = self._getLayerForY(coords[1] + self.timeline._container.point.y)
-        priority = min(priority, len(self.timeline.bTimeline.get_layers()))
-
-        self.timeline._snapEndedCb()
-
-        self.setDragged(False)
-
-        self.ghostclip.props.visible = False
-        if self.brother:
-            self.brother.ghostclip.props.visible = False
-
-        priority = max(0, priority)
-        self._context.editTo(new_start, priority)
-        self._context.finish()
-
-    def _selectedChangedCb(self, selected, isSelected):
-        self.marquee.props.visible = isSelected
-
-
-class TransitionElement(TimelineElement):
-    def __init__(self, bElement, track, timeline):
-        TimelineElement.__init__(self, bElement, track, timeline)
-
-    def _createBackground(self, track):
-        self.background = RoundedRectangle(0, 0, 5, 5)
-        color = Cogl.Color()
-        color.init_from_4ub(100, 100, 100, 125)
-        self.background.set_color(color)
-        self.background.set_border_width(3)
-
-        self.background.set_position(0, 0)
-        self.add_child(self.background)
-
-
 class TimelineStage(Clutter.ScrollActor, Zoomable):
     __gsignals__ = {
         'scrolled': (GObject.SIGNAL_RUN_FIRST, None, ())
@@ -1739,428 +1218,6 @@ class Timeline(Gtk.VBox, Zoomable):
         self.project.connect("asset-added", self._doAssetAddedCb, layer)
         self.project.create_asset("file://" + sys.argv[1], GES.UriClip)
 
-
-def get_preview_for_object(bElement, timeline):
-    # Fixme special preview for transitions, titles
-    if not isinstance(bElement.get_parent(), GES.UriClip):
-        return Clutter.Actor()
-    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, timeline)
-    else:
-        return Clutter.Actor()
-
-
-class VideoPreviewer(Clutter.ScrollActor, Zoomable):
-    def __init__(self, bElement, timeline):
-        """
-        @param bElement : the backend GES.TrackElement
-        @param track : the track to which the bElement belongs
-        @param timeline : the containing graphic timeline.
-        """
-        Zoomable.__init__(self)
-        Clutter.ScrollActor.__init__(self)
-
-        self.uri = bElement.props.uri
-
-        self.bElement = bElement
-        self.timeline = timeline
-
-        self.bElement.connect("notify::duration", self.duration_changed)
-        self.bElement.connect("notify::in-point", self._inpoint_changed_cb)
-        self.bElement.connect("notify::start", self.start_changed)
-
-        self.timeline.connect("scrolled", self._scroll_changed)
-
-        self.duration = self.bElement.props.duration
-
-        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.5 * Gst.SECOND)
-
-        # maps (quantized) times to Thumbnail objects
-        self.thumbs = {}
-
-        self.thumb_cache = ThumbnailCache(uri=self.uri)
-
-        self.wishlist = []
-
-        self._setupPipeline()
-
-        self._startThumbnailing()
-
-        self.callback_id = None
-
-    # Internal API
-
-    def _scroll_changed(self, unused):
-        self._updateWishlist()
-
-    def start_changed(self, unused_bElement, unused_value):
-        self._updateWishlist()
-
-    def _update(self, unused_msg_source=None):
-        if self.callback_id:
-            GLib.source_remove(self.callback_id)
-        self.callback_id = GLib.idle_add(self._addAllThumbnails, priority=GLib.PRIORITY_LOW)
-
-    def _setupPipeline(self):
-        """
-        Create the pipeline.
-
-        It has the form "playbin ! thumbnailsink" where thumbnailsink
-        is a Bin made out of "videorate ! capsfilter ! gdkpixbufsink"
-        """
-        # TODO: don't hardcode framerate
-        self.pipeline = Gst.parse_launch(
-            "uridecodebin uri={uri} ! "
-            "videoconvert ! "
-            "videorate ! "
-            "videoscale method=lanczos ! "
-            "capsfilter caps=video/x-raw,format=(string)RGBA,height=(int){height},"
-            "pixel-aspect-ratio=(fraction)1/1,framerate=2/1 ! "
-            "gdkpixbufsink name=gdkpixbufsink".format(uri=self.uri, height=self.thumb_height))
-
-        # get the gdkpixbufsink and the sinkpad
-        self.gdkpixbufsink = self.pipeline.get_by_name("gdkpixbufsink")
-        sinkpad = self.gdkpixbufsink.get_static_pad("sink")
-
-        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]
-            self.thumb_width = neg_caps["width"]
-        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")
-            # assume 16:9 aspect ratio
-            self.thumb_width = 16 * self.thumb_height / 9
-
-        # pop all messages from the bus so we won't be flooded with messages
-        # from the prerolling phase
-        while self.pipeline.get_bus().pop():
-            continue
-        # 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)
-
-    def _startThumbnailing(self):
-        self.queue = []
-        query_success, duration = self.pipeline.query_duration(Gst.Format.TIME)
-        if not query_success:
-            print("Could not determine the duration of the file {}".format(self.uri))
-            duration = self.duration
-        else:
-            self.duration = duration
-
-        current_time = 0
-        while current_time < duration:
-            self.queue.append(current_time)
-            current_time += self.thumb_period
-
-        self._create_next_thumb()
-
-    def _create_next_thumb(self):
-        if not self.queue:
-            # nothing left to do
-            self.thumb_cache.commit()
-            return
-        wish = self._get_wish()
-        if wish:
-            time = wish
-            self.queue.remove(wish)
-        else:
-            time = self.queue.pop(0)
-        # append the time to the end of the queue so that if this seek fails
-        # another try will be started later
-        self.queue.append(time)
-
-        self.pipeline.seek(1.0,
-            Gst.Format.TIME, Gst.SeekFlags.FLUSH | Gst.SeekFlags.ACCURATE,
-            Gst.SeekType.SET, time,
-            Gst.SeekType.NONE, -1)
-
-    def _addAllThumbnails(self):
-        self.remove_all_children()
-        self.thumbs = {}
-
-        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)
-
-        current_time = 0
-        while current_time < self.duration:
-            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
-            if current_time in self.thumb_cache:
-                gdkpixbuf = self.thumb_cache[current_time]
-                self.thumbs[current_time].set_from_gdkpixbuf(gdkpixbuf)
-            current_time += thumb_duration
-
-        self._updateWishlist()
-
-    def _inpoint_changed_cb(self, unused_bElement, unused_value):
-        position = Clutter.Point()
-        position.x = Zoomable.nsToPixel(self.bElement.props.in_point)
-        self.scroll_to_point(position)
-        self._updateWishlist()
-
-    def _updateWishlist(self):
-        """
-        Adds thumbnails for the whole clip.
-
-        Takes the zoom setting into account and removes potentially
-        existing thumbnails prior to adding the new ones.
-        """
-        self.wishlist = []
-
-        # 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)
-
-        element_left, element_right = self._get_visible_range()
-        # TODO: replace with a call to utils.misc.quantize:
-        element_left = (element_left // thumb_duration) * thumb_duration
-
-        current_time = element_left
-        while current_time < element_right:
-            if current_time not in self.thumb_cache:
-                self.wishlist.append(current_time)
-            current_time += thumb_duration
-
-    def _get_wish(self):
-        """Returns a wish that is also in the queue or None
-           if no such wish exists"""
-        while True:
-            if not self.wishlist:
-                return None
-            wish = self.wishlist.pop(0)
-            if wish in self.queue:
-                return wish
-
-    def _setThumbnail(self, time, thumbnail):
-        # TODO: is "time" guaranteed to be nanosecond precise?
-        # => __tim says: "that's how it should be"
-        # => also see gst-plugins-good/tests/icles/gdkpixbufsink-test
-        # => Daniel: It is *not* nanosecond precise when we remove the videorate
-        #            element from the pipeline
-        if time in self.queue:
-            self.queue.remove(time)
-
-        self.thumb_cache[time] = thumbnail
-
-        if time in self.thumbs:
-            self.thumbs[time].set_from_gdkpixbuf(thumbnail)
-
-    # Interface (Zoomable)
-
-    def zoomChanged(self):
-        self._update()
-
-    def _get_visible_range(self):
-        timeline_left, timeline_right = self._get_visible_timeline_range()
-        element_left = timeline_left - self.bElement.props.start + self.bElement.props.in_point
-        element_left = max(element_left, self.bElement.props.in_point)
-
-        element_right = timeline_right - self.bElement.props.start + self.bElement.props.in_point
-        element_right = min(element_right, self.bElement.props.in_point + self.bElement.props.duration)
-
-        return (element_left, element_right)
-
-    # TODO: move to Timeline or to utils
-    def _get_visible_timeline_range(self):
-        # determine the visible left edge of the timeline
-        # TODO: isn't there some easier way to get the scroll point of the ScrollActor?
-        # timeline_left = -(self.timeline.get_transform().xw - self.timeline.props.x)
-        timeline_left = self.timeline.get_scroll_point().x
-
-        # determine the width of the pipeline
-        # by intersecting the timeline's and the stage's allocation
-        timeline_allocation = self.timeline.props.allocation
-        stage_allocation = self.timeline.get_stage().props.allocation
-
-        timeline_rect = Clutter.Rect()
-        timeline_rect.init(timeline_allocation.x1,
-                           timeline_allocation.y1,
-                           timeline_allocation.x2 - timeline_allocation.x1,
-                           timeline_allocation.y2 - timeline_allocation.y1)
-
-        stage_rect = Clutter.Rect()
-        stage_rect.init(stage_allocation.x1,
-                        stage_allocation.y1,
-                        stage_allocation.x2 - stage_allocation.x1,
-                        stage_allocation.y2 - stage_allocation.y1)
-
-        has_intersection, intersection = timeline_rect.intersection(stage_rect)
-
-        if not has_intersection:
-            return (0, 0)
-
-        timeline_width = intersection.size.width
-
-        # determine the visible right edge of the timeline
-        timeline_right = timeline_left + timeline_width
-
-        # convert to nanoseconds
-        time_left = Zoomable.pixelToNs(timeline_left)
-        time_right = Zoomable.pixelToNs(timeline_right)
-
-        return (time_left, time_right)
-
-    # Callbacks
-
-    def bus_message_handler(self, unused_bus, message):
-        if message.type == Gst.MessageType.ELEMENT and \
-                message.src == self.gdkpixbufsink:
-            struct = message.get_structure()
-            struct_name = struct.get_name()
-            if struct_name == "preroll-pixbuf":
-                self._setThumbnail(struct.get_value("stream-time"), struct.get_value("pixbuf"))
-        elif message.type == Gst.MessageType.ASYNC_DONE:
-            self._create_next_thumb()
-        return Gst.BusSyncReply.PASS
-
-    def duration_changed(self, unused_bElement, unused_value):
-        new_duration = max(self.duration, self.bElement.props.duration)
-        if new_duration > self.duration:
-            self.duration = new_duration
-            self._update()
-
-
-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()
-        alpha = gdkpixbuf.get_has_alpha()
-        if alpha:
-            self.props.content.set_data(pixel_data, Cogl.PixelFormat.RGBA_8888, self.width, self.height, 
row_stride)
-        else:
-            self.props.content.set_data(pixel_data, Cogl.PixelFormat.RGB_888, self.width, self.height, 
row_stride)
-
-
-# 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
-        filehash = 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")), filehash)
-        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,\
-                          Jpeg BLOB 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
-
-    def __getitem__(self, key):
-        self.cur.execute("SELECT * FROM Thumbs WHERE Time = ?", (key,))
-        row = self.cur.fetchone()
-        if row:
-            jpeg = row[1]
-            loader = GdkPixbuf.PixbufLoader.new()
-            # TODO: what do to if any of the following calls fails?
-            loader.write(jpeg)
-            loader.close()
-            pixbuf = loader.get_pixbuf()
-            return pixbuf
-        raise KeyError(key)
-
-    def __setitem__(self, key, value):
-        success, jpeg = value.save_to_bufferv("jpeg", ["quality", None], ["90"])
-        if not success:
-            self.warning("JPEG compression failed")
-            return
-        blob = sqlite3.Binary(jpeg)
-        #Replace if the key already existed
-        self.cur.execute("DELETE FROM Thumbs WHERE  time=?", (key,))
-        self.cur.execute("INSERT INTO Thumbs VALUES (?,?)", (key, blob,))
-        #self.conn.commit()
-
-    def commit(self):
-        print("commit")
-        self.conn.commit()
-
 if __name__ == "__main__":
     # Basic argument handling, no need for getopt here
     if len(sys.argv) < 2:
diff --git a/pitivi/utils/ui.py b/pitivi/utils/ui.py
index 79e9e6e..8655051 100644
--- a/pitivi/utils/ui.py
+++ b/pitivi/utils/ui.py
@@ -52,9 +52,12 @@ LAYER_HEIGHT_EXPANDED = 50
 LAYER_HEIGHT_COLLAPSED = 15
 TRACK_SPACING = 8
 
-SPACING = 6
+EXPANDED_SIZE = 65
+
 PADDING = 6
 
+SPACING = 10
+
 CANVAS_SPACING = 21
 
 # Layer creation blocking time in s
@@ -197,7 +200,7 @@ def beautify_info(info):
 
 def info_name(info):
     """Return a human-readable filename (without the path and quoting)."""
-    if  isinstance(info, GES.Asset):
+    if isinstance(info, GES.Asset):
         filename = unquote(os.path.basename(info.get_id()))
     else:
         filename = unquote(os.path.basename(info.get_uri()))


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