[pitivi] Code is now clean and consistent, and logically split in different files



commit ddabf48882b6a6dfc1067fb885ef830c35b1f6c6
Author: Mathieu Duponchelle <mathieu duponchelle epitech eu>
Date:   Wed Apr 17 03:54:39 2013 +0200

    Code is now clean and consistent, and logically split in different files

 pitivi/timeline/controls.py   |  149 +++++++
 pitivi/timeline/elements.py   |  114 +++---
 pitivi/timeline/previewers.py |    7 +
 pitivi/timeline/timeline.py   |  853 ++++++++++++++++-------------------------
 pitivi/utils/ui.py            |    4 +
 pitivi/utils/widgets.py       |   65 ++++
 6 files changed, 623 insertions(+), 569 deletions(-)
---
diff --git a/pitivi/timeline/controls.py b/pitivi/timeline/controls.py
new file mode 100644
index 0000000..33468a6
--- /dev/null
+++ b/pitivi/timeline/controls.py
@@ -0,0 +1,149 @@
+from gi.repository import GtkClutter
+GtkClutter.init([])
+
+from gi.repository import Clutter, GObject
+
+from pitivi.utils.ui import EXPANDED_SIZE, SPACING, CONTROL_WIDTH
+
+from layer import VideoLayerControl, AudioLayerControl
+
+
+class ControlActor(GtkClutter.Actor):
+    def __init__(self, container, widget, layer):
+        GtkClutter.Actor.__init__(self)
+
+        self.layer = layer
+        self._container = container
+        self.widget = widget
+
+        self.get_widget().add(widget)
+        self.set_reactive(True)
+        self._setUpDragAndDrop()
+
+    def _getLayerForY(self, y):
+        if self.isAudio:
+            y -= self.nbrLayers * (EXPANDED_SIZE + SPACING)
+        priority = int(y / (EXPANDED_SIZE + SPACING))
+
+        return priority
+
+    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
+
+    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()
+
+
+class ControlContainer(Clutter.ScrollActor):
+    __gsignals__ = {
+        "selection-changed": (GObject.SignalFlags.RUN_LAST, None, (GObject.TYPE_PYOBJECT,),)
+    }
+
+    def __init__(self, timeline):
+        Clutter.ScrollActor.__init__(self)
+
+        self.timeline = timeline
+        self.controlActors = []
+        self.trackControls = []
+
+    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  # Don't put 1000 layers or this breaks !
+
+        if priority > target:
+            for layer in self.timeline.bTimeline.get_layers():
+                prio = layer.get_priority()
+                if target <= prio < priority:  # Python idiom, is that bad ?
+                    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:
+            control = VideoLayerControl(self, layer)
+
+        controlActor = ControlActor(self, control, layer)
+        controlActor.isAudio = isAudio
+        controlActor.layer = layer
+        controlActor.set_size(CONTROL_WIDTH, EXPANDED_SIZE + SPACING)
+
+        self.add_child(controlActor)
+        self.trackControls.append(control)
+        self.controlActors.append(controlActor)
+
+    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 addLayerControl(self, layer):
+        self.addTrackControl(layer, False)
+        self.addTrackControl(layer, True)
+        self._reorderLayerActors()
diff --git a/pitivi/timeline/elements.py b/pitivi/timeline/elements.py
index 63b3fbe..bba56fa 100644
--- a/pitivi/timeline/elements.py
+++ b/pitivi/timeline/elements.py
@@ -6,7 +6,7 @@ is prefixed with a little b, example : bTimeline
 
 import os
 
-from gi.repository import Clutter, Cogl, GES
+from gi.repository import Clutter, Cogl, GES, Gdk
 from pitivi.utils.timeline import Zoomable, EditingContext, Selection, SELECT, UNSELECT, Selected
 from previewers import VideoPreviewer, BORDER_WIDTH
 
@@ -18,6 +18,7 @@ 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
@@ -46,8 +47,10 @@ class RoundedRectangle(Clutter.Actor):
         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
@@ -114,18 +117,17 @@ class RoundedRectangle(Clutter.Actor):
 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.isLeft = isLeft
         self.isSelected = False
-
         self.timelineElement = timelineElement
+        self.dragAction = Clutter.DragAction()
 
+        self.set_from_file(os.path.join(configure.get_pixmap_dir(), "trimbar-normal.png"))
+        self.set_size(-1, EXPANDED_SIZE)
+        self.hide()
         self.set_reactive(True)
 
-        self.dragAction = Clutter.DragAction()
         self.add_action(self.dragAction)
 
         self.dragAction.connect("drag-begin", self._dragBeginCb)
@@ -134,6 +136,7 @@ class TrimHandle(Clutter.Texture):
 
         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)
@@ -145,6 +148,7 @@ class TrimHandle(Clutter.Texture):
         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))
@@ -170,9 +174,12 @@ class TrimHandle(Clutter.Texture):
         self.props.visible = isSelected
 
     def _dragBeginCb(self, action, actor, event_x, event_y, modifiers):
-        self.timelineElement.setDragged(True)
+        self.dragBeginStartX = event_x
+        self.dragBeginStartY = event_y
         elem = self.timelineElement.bElement.get_parent()
 
+        self.timelineElement.setDragged(True)
+
         if self.isLeft:
             edge = GES.Edge.EDGE_START
             self._dragBeginStart = self.timelineElement.bElement.get_parent().get_start()
@@ -188,14 +195,10 @@ class TrimHandle(Clutter.Texture):
                                        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())
@@ -204,9 +207,11 @@ class TrimHandle(Clutter.Texture):
     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))
 
@@ -222,31 +227,22 @@ class TimelineElement(Clutter.Actor, Zoomable):
         Clutter.Actor.__init__(self)
 
         self.timeline = timeline
-
         self.bElement = bElement
-
         self.bElement.selected = Selected()
-        self.bElement.selected.connect("selected-changed", self._selectedChangedCb)
+        self.track_type = self.bElement.get_track_type()  # This won't change
+        self.isDragged = False
+        size = self.bElement.get_duration()
 
         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.update(True)
         self.set_reactive(True)
+
         self._connectToEvents()
 
     # Public API
@@ -308,11 +304,12 @@ class TimelineElement(Clutter.Actor, Zoomable):
     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):
@@ -323,33 +320,36 @@ class TimelineElement(Clutter.Actor, Zoomable):
 
     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
 
+        self.add_child(self.marquee)
+
     def _connectToEvents(self):
-        # Click
+        self.dragAction = Clutter.DragAction()
+
+        self.add_action(self.dragAction)
+
+        self.dragAction.connect("drag-progress", self._dragProgressCb)
+        self.dragAction.connect("drag-begin", self._dragBeginCb)
+        self.dragAction.connect("drag-end", self._dragEndCb)
+        self.bElement.selected.connect("selected-changed", self._selectedChangedCb)
         # 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)
@@ -420,29 +420,35 @@ class ClipElement(TimelineElement):
 
     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.leftHandle.set_position(0, 0)
+
         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
@@ -451,29 +457,32 @@ class ClipElement(TimelineElement):
         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)
-
+        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
 
+        # We can also safely find if the object has a brother element
+        self.setDragged(True)
+        if self.brother:
+            self.brother.nbrLayers = self.nbrLayers
+
     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)
+        new_start = self._dragBeginStart + self.pixelToNs(delta_x)
 
         self.ghostclip.props.x = self.nsToPixel(self._dragBeginStart) + delta_x
         self.updateGhostclip(priority, y, False)
@@ -481,8 +490,6 @@ class ClipElement(TimelineElement):
             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:
@@ -493,19 +500,17 @@ class ClipElement(TimelineElement):
         coords = self.dragAction.get_motion_coords()
         delta_x = coords[0] - self.dragBeginStartX
         new_start = self._dragBeginStart + self.pixelToNs(delta_x)
-
         priority = self._getLayerForY(coords[1] + self.timeline._container.point.y)
         priority = min(priority, len(self.timeline.bTimeline.get_layers()))
+        priority = max(0, priority)
 
         self.timeline._snapEndedCb()
-
         self.setDragged(False)
 
         self.ghostclip.props.visible = False
         if self.brother:
             self.brother.ghostclip.props.visible = False
 
-        priority = max(0, priority)
         self._context.editTo(new_start, priority)
         self._context.finish()
 
@@ -521,9 +526,10 @@ class TransitionElement(TimelineElement):
     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
index 5c21e48..275bcbb 100644
--- a/pitivi/timeline/previewers.py
+++ b/pitivi/timeline/previewers.py
@@ -11,6 +11,13 @@ from pitivi.utils.ui import EXPANDED_SIZE, SPACING
 BORDER_WIDTH = 3  # For the timeline elements
 
 
+"""
+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 VideoPreviewer(Clutter.ScrollActor, Zoomable):
     def __init__(self, bElement, timeline):
         """
diff --git a/pitivi/timeline/timeline.py b/pitivi/timeline/timeline.py
index 411425e..bcb8148 100644
--- a/pitivi/timeline/timeline.py
+++ b/pitivi/timeline/timeline.py
@@ -6,12 +6,14 @@ 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 pitivi.utils.ui import EXPANDED_SIZE, SPACING, PLAYHEAD_WIDTH, CONTROL_WIDTH
+from pitivi.utils.widgets import ZoomBox
+
 from ruler import ScaleRuler
 from gettext import gettext as _
 from pitivi.utils.pipeline import Pipeline
-from layer import VideoLayerControl, AudioLayerControl
 from elements import ClipElement, TransitionElement
+from controls import ControlContainer
 
 GlobalSettings.addConfigOption('edgeSnapDeadband',
     section="user-interface",
@@ -38,10 +40,6 @@ PreferencesDialog.addNumericPreference('imageClipLength',
     description=_("Default clip length (in miliseconds) of images when inserting on the timeline."),
     lower=1)
 
-# CONSTANTS
-
-CONTROL_WIDTH = 250
-
 # tooltip text for toolbar
 DELETE = _("Delete Selected")
 SPLIT = _("Split clip at playhead position")
@@ -120,15 +118,18 @@ class TimelineStage(Clutter.ScrollActor, Zoomable):
     def __init__(self, container):
         Clutter.ScrollActor.__init__(self)
         Zoomable.__init__(self)
+
         self.bTimeline = None
-        self.set_background_color(Clutter.Color.new(31, 30, 33, 255))
+        self._container = container
         self.elements = []
         self.selection = Selection()
+        self._scroll_point = Clutter.Point()
+        self.lastPosition = 0  # Saved for redrawing when paused
+
+        self.set_background_color(Clutter.Color.new(31, 30, 33, 255))
+
         self._createPlayhead()
         self._createSnapIndicator()
-        self._container = container
-        self.lastPosition = 0
-        self._scroll_point = Clutter.Point()
 
     # Public API
 
@@ -165,11 +166,8 @@ class TimelineStage(Clutter.ScrollActor, Zoomable):
 
         self.zoomChanged()
 
-    #Stage was clicked with nothing under the pointer
+    # 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):
@@ -179,7 +177,7 @@ class TimelineStage(Clutter.ScrollActor, Zoomable):
                 return elem
         return None
 
-    #Internal API
+    # Internal API
 
     def _connectTrack(self, track):
         track.connect("track-element-added", self._trackElementAddedCb)
@@ -191,28 +189,33 @@ class TimelineStage(Clutter.ScrollActor, Zoomable):
 
     def _positionCb(self, pipeline, position):
         self.playhead.props.x = self.nsToPixel(position)
+        # FIXME this currently sucks
         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)
+        self.playhead.set_size(PLAYHEAD_WIDTH, 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)
 
+        self.add_child(self.playhead)
+
     def _createSnapIndicator(self):
         self._snap_indicator = Clutter.Actor()
+
         self._snap_indicator.set_background_color(Clutter.Color.new(0, 0, 250, 200))
         self._snap_indicator.props.visible = False
         self._snap_indicator.props.width = 3
         self._snap_indicator.props.y = 0
+
         self.add_child(self._snap_indicator)
 
     def _addTimelineElement(self, track, bElement):
@@ -230,19 +233,20 @@ class TimelineStage(Clutter.ScrollActor, Zoomable):
 
         self.elements.append(element)
 
+        self._setElementX(element)
         self._setElementY(element)
 
         self.add_child(element)
 
-        self._setElementX(element)
-
     def _removeTimelineElement(self, track, bElement):
         bElement.disconnect_by_func(self._elementStartChangedCb)
         bElement.disconnect_by_func(self._elementDurationChangedCb)
         bElement.disconnect_by_func(self._elementInPointChangedCb)
+
         for element in self.elements:
             if element.bElement == bElement:
                 break
+
         self.elements.remove(element)
         self.remove_child(element)
 
@@ -254,18 +258,17 @@ class TimelineStage(Clutter.ScrollActor, Zoomable):
         if ease:
             element.restore_easing_state()
 
-    # Crack, change that when we have retractable layers
+    # FIXME, change that when we have retractable layers
     def _setElementY(self, element):
-        element.save_easing_state()
-        y = 0
         bElement = element.bElement
         track_type = bElement.get_track_type()
 
+        y = 0
         if (track_type == GES.TrackType.AUDIO):
             y = len(self.bTimeline.get_layers()) * (EXPANDED_SIZE + SPACING)
-
         y += bElement.get_parent().get_layer().get_priority() * (EXPANDED_SIZE + SPACING) + SPACING
 
+        element.save_easing_state()
         element.props.y = y
         element.restore_easing_state()
 
@@ -274,31 +277,39 @@ class TimelineStage(Clutter.ScrollActor, Zoomable):
         self.set_easing_duration(0)
         self.props.width = self.nsToPixel(self.bTimeline.get_duration()) + 250
         self.restore_easing_state()
+
         self._container.updateHScrollAdjustments()
 
     def _redraw(self):
         self._updateSize()
+
         self.save_easing_state()
         for element in self.elements:
             self._setElementX(element)
         self.restore_easing_state()
-        self.playhead.props.x = self.nsToPixel(self.lastPosition)
-
-    # Interface overrides (Zoomable)
 
-    def zoomChanged(self):
-        self._redraw()
+        self.playhead.props.x = self.nsToPixel(self.lastPosition)
 
     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()
 
+    # Interface overrides
+
+    # Zoomable Override
+
+    def zoomChanged(self):
+        self._redraw()
+
     # Clutter Override
 
     # TODO: remove self._scroll_point and get_scroll_point as soon as the Clutter API
@@ -321,9 +332,10 @@ class TimelineStage(Clutter.ScrollActor, Zoomable):
         if position == 0:
             self._snapEndedCb()
         else:
-            self._snap_indicator.props.x = Zoomable.nsToPixel(position)
             height = len(self.bTimeline.get_layers()) * (EXPANDED_SIZE + SPACING) * 2
+
             self._snap_indicator.props.height = height
+            self._snap_indicator.props.x = Zoomable.nsToPixel(position)
             self._snap_indicator.props.visible = True
 
     def _snapEndedCb(self, *args):
@@ -333,30 +345,15 @@ class TimelineStage(Clutter.ScrollActor, Zoomable):
         self._add_layer(layer)
 
     def _layerRemovedCb(self, timeline, layer):
-        layer.disconnect_by_func(self._clipAddedCb)
-        layer.disconnect_by_func(self._clipRemovedCb)
+        # FIXME : really remove layer ^^
         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 _trackRemovedCb(self, timeline, track):
         self._disconnectTrack(track)
 
-    def _elementAddedCb(self, clip, bElement):
-        pass
-
-    def _elementRemovedCb(self):
-        pass
-
     def _trackElementAddedCb(self, track, bElement):
         self._updateSize()
         self._addTimelineElement(track, bElement)
@@ -369,6 +366,7 @@ class TimelineStage(Clutter.ScrollActor, Zoomable):
 
     def _elementStartChangedCb(self, bElement, start, element):
         self._updateSize()
+
         if element.isDragged:
             self._setElementX(element, ease=False)
         else:
@@ -385,6 +383,7 @@ class TimelineStage(Clutter.ScrollActor, Zoomable):
         self._redraw()
 
 
+# This is for running standalone
 def quit_(stage):
     Gtk.main_quit()
 
@@ -393,255 +392,35 @@ def quit2_(*args, **kwargs):
     Gtk.main_quit()
 
 
-class ZoomBox(Gtk.HBox, Zoomable):
-    def __init__(self, timeline):
-        """
-        This will hold the widgets responsible for zooming.
-        """
-        Gtk.HBox.__init__(self)
-        Zoomable.__init__(self)
-
-        self.timeline = timeline
-
-        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)
-
-        # 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)
-
-        self.show_all()
-
-        self._updateZoomSlider = True
-
-    def _zoomAdjustmentChangedCb(self, adjustment):
-        # GTK crack
-        self._updateZoomSlider = False
-        Zoomable.setZoomLevel(int(adjustment.get_value()))
-        self.zoomed_fitted = False
-        self._updateZoomSlider = True
-
-    def _zoomFitCb(self, button):
-        self.timeline.zoomFit()
-
-    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 zoomChanged(self):
-        if self._updateZoomSlider:
-            self._zoomAdjustment.set_value(self.getCurrentZoomLevel())
-
-
-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
-
-    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
-
-    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()
-
-
-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:
-            control = VideoLayerControl(self, layer)
-
-        controlActor = ControlActor(self, control, layer)
-        controlActor.isAudio = isAudio
-        controlActor.layer = layer
-        controlActor.set_size(CONTROL_WIDTH, EXPANDED_SIZE + SPACING)
-
-        self.add_child(controlActor)
-        self.trackControls.append(control)
-        self.controlActors.append(controlActor)
-
-    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 addLayerControl(self, layer):
-        self.addTrackControl(layer, False)
-        self.addTrackControl(layer, True)
-        self._reorderLayerActors()
-
-
 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
 
-        self.embed = GtkClutter.Embed()
-        self.embed.show()
-
-        self.point = Clutter.Point()
-        self.point.x = 0
-        self.point.y = 0
-
-        self.zoomBox = ZoomBox(self)
-
-        self._packScrollbars(self)
-
-        stage = self.embed.get_stage()
-        stage.set_background_color(Clutter.Color.new(31, 30, 33, 255))
-
-        self.stage = stage
-
-        self.embed.connect("scroll-event", self._scrollEventCb)
-
-        self.stage.set_throttle_motion_events(True)
-
-        stage.show()
-
-        widget = TimelineStage(self)
-
-        self.controls = ControlContainer(widget)
-        stage.add_child(self.controls)
-        self.controls.set_position(0, 0)
-        self.controls.set_z_position(2)
-
-        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.scrolled = 0
-
-        self._createActions()
-
         self._projectmanager = None
         self._project = None
 
+        self._createUi()
+        self._createActions()
+
         self._settings.connect("edgeSnapDeadbandChanged",
                 self._snapDistanceChangedCb)
 
+        # Standalone
+        if __name__ == "__main__":
+            gtksettings = Gtk.Settings.get_default()
+            gtksettings.set_property("gtk-application-prefer-dark-theme", True)
+
         self.show_all()
 
+    # Public API
+
     def insertEnd(self, assets):
         """
         Add source at the end of the timeline
@@ -649,11 +428,14 @@ class Timeline(Gtk.VBox, Zoomable):
         @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 isinstance(asset, GES.TitleClip):
                 clip_duration = asset.get_duration()
@@ -662,13 +444,13 @@ class Timeline(Gtk.VBox, Zoomable):
             else:
                 clip_duration = asset.get_duration()
 
-            print "added asset"
             if not isinstance(asset, GES.TitleClip):
                 layer.add_asset(asset, self.bTimeline.props.duration,
                                 0, clip_duration, 1.0, asset.get_supported_formats())
             else:
                 asset.set_start(self.bTimeline.props.duration)
                 layer.add_clip(asset)
+
         self.bTimeline.enable_update(True)
 
     def setProjectManager(self, projectmanager):
@@ -676,10 +458,81 @@ class Timeline(Gtk.VBox, Zoomable):
             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 updateHScrollAdjustments(self):
+        """
+        Recalculate the horizontal scrollbar depending on the timeline duration.
+        """
+        timeline_ui_width = self.embed.get_allocation().width
+        controls_width = 0
+        scrollbar_width = 0
+        contents_size = Zoomable.nsToPixel(self.bTimeline.props.duration)
+
+        widgets_width = controls_width + scrollbar_width
+        end_padding = CONTROL_WIDTH + 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.zoomed_fitted = True
+
+    def zoomFit(self):
+        self._hscrollBar.set_value(0)
+        self._setBestZoomRatio()
+
+    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 setTimeline(self, bTimeline):
+        self.bTimeline = bTimeline
+        self.timeline.selection.connect("selection-changed", self._selectionChangedCb)
+        self.timeline.setTimeline(bTimeline)
+
+    # Internal API
+
+    def _createUi(self):
+        self.embed = GtkClutter.Embed()
+        self.stage = self.embed.get_stage()
+        self.timeline = TimelineStage(self)
+        self.controls = ControlContainer(self.timeline)
+        self.zoomBox = ZoomBox(self)
+
+        self.stage.set_background_color(Clutter.Color.new(31, 30, 33, 255))
+        self.timeline.set_position(CONTROL_WIDTH, 0)
+        self.controls.set_position(0, 0)
+        self.controls.set_z_position(2)
+
+        self.stage.add_child(self.controls)
+        self.stage.add_child(self.timeline)
+
+        self.stage.connect("destroy", quit_)
+        self.stage.connect("button-press-event", self._clickedCb)
+        self.embed.connect("scroll-event", self._scrollEventCb)
+
+        self.point = Clutter.Point()
+        self.point.x = 0
+        self.point.y = 0
+
+        self.scrolled = 0
+
+        self._packScrollbars(self)
+        self.stage.show()
+
     def _ensureLayer(self):
         """
         Make sure we have a layer in our timeline
@@ -688,7 +541,7 @@ class Timeline(Gtk.VBox, Zoomable):
         """
         layers = self.bTimeline.get_layers()
 
-        if (len(layers) == 0):
+        if not layers:
             layer = GES.Layer()
             layer.props.auto_transition = True
             self.bTimeline.add_layer(layer)
@@ -724,28 +577,28 @@ class Timeline(Gtk.VBox, Zoomable):
 
         selection_actions = (
             ("DeleteObj", Gtk.STOCK_DELETE, None,
-            "Delete", DELETE, self.deleteSelected),
+            "Delete", DELETE, self._deleteSelected),
 
             ("UngroupObj", "pitivi-ungroup", _("Ungroup"),
-            "<Shift><Control>G", UNGROUP, self.ungroupSelected),
+            "<Shift><Control>G", UNGROUP, self._ungroupSelected),
 
             # Translators: This is an action, the title of a button
             ("GroupObj", "pitivi-group", _("Group"),
-            "<Control>G", GROUP, self.groupSelected),
+            "<Control>G", GROUP, self._groupSelected),
 
             ("AlignObj", "pitivi-align", _("Align"),
-            "<Shift><Control>A", ALIGN, self.alignSelected),
+            "<Shift><Control>A", ALIGN, self._alignSelected),
         )
 
         playhead_actions = (
             ("PlayPause", Gtk.STOCK_MEDIA_PLAY, None,
-            "space", _("Start Playback"), self.playPause),
+            "space", _("Start Playback"), self._playPause),
 
             ("Split", "pitivi-split", _("Split"),
-            "S", SPLIT, self.split),
+            "S", SPLIT, self._split),
 
             ("Keyframe", "pitivi-keyframe", _("Add a Keyframe"),
-            "K", KEYFRAME, self.keyframe),
+            "K", KEYFRAME, self._keyframe),
 
             ("Prevkeyframe", None, _("_Previous Keyframe"),
             "comma", PREVKEYFRAME, self._previousKeyframeCb),
@@ -755,14 +608,15 @@ class Timeline(Gtk.VBox, Zoomable):
         )
 
         actiongroup = Gtk.ActionGroup("timelinepermanent")
+        self.selection_actions = Gtk.ActionGroup("timelineselection")
+        self.playhead_actions = Gtk.ActionGroup("timelineplayhead")
+
         actiongroup.add_actions(actions)
-        self.ui_manager.insert_action_group(actiongroup, 0)
 
-        self.selection_actions = Gtk.ActionGroup("timelineselection")
+        self.ui_manager.insert_action_group(actiongroup, 0)
         self.selection_actions.add_actions(selection_actions)
         self.selection_actions.set_sensitive(False)
         self.ui_manager.insert_action_group(self.selection_actions, -1)
-        self.playhead_actions = Gtk.ActionGroup("timelineplayhead")
         self.playhead_actions.add_actions(playhead_actions)
         self.ui_manager.insert_action_group(self.playhead_actions, -1)
 
@@ -771,17 +625,18 @@ class Timeline(Gtk.VBox, Zoomable):
     def _packScrollbars(self, vbox):
         self.hadj = Gtk.Adjustment()
         self.vadj = Gtk.Adjustment()
+        self._vscrollbar = Gtk.VScrollbar(self.vadj)
+        self._hscrollBar = Gtk.HScrollbar(self.hadj)
+        self.ruler = ScaleRuler(self, self.hadj)
+
+        hbox = Gtk.HBox()
+
         self.hadj.connect("value-changed", self._updateScrollPosition)
         self.vadj.connect("value-changed", self._updateScrollPosition)
 
-        self._vscrollbar = Gtk.VScrollbar(self.vadj)
-
-        self._hscrollBar = Gtk.HScrollbar(self.hadj)
         vbox.pack_end(self._hscrollBar, False, True, False)
 
-        self.ruler = ScaleRuler(self, self.hadj)
         self.ruler.setProjectFrameRate(24.)
-
         self.ruler.set_size_request(0, 25)
         self.ruler.hide()
 
@@ -789,7 +644,6 @@ class Timeline(Gtk.VBox, Zoomable):
         self.vadj.props.upper = 500
         self.vadj.props.page_size = 250
 
-        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)
@@ -797,7 +651,9 @@ class Timeline(Gtk.VBox, Zoomable):
         vbox.pack_end(hbox, True, True, True)
 
         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)
 
@@ -809,48 +665,11 @@ class Timeline(Gtk.VBox, Zoomable):
         point.x = self.hadj.get_value()
         point.y = self.vadj.get_value()
         self.point = point
+
         self.timeline.scroll_to_point(point)
         point.x = 0
         self.controls.scroll_to_point(point)
 
-    def zoomChanged(self):
-        if self._settings and self.bTimeline:
-            # zoomChanged might be called various times before the UI is ready
-            self.bTimeline.props.snapping_distance = \
-                Zoomable.pixelToNs(self._settings.edgeSnapDeadband)
-        self.updateHScrollAdjustments()
-
-    def updateHScrollAdjustments(self):
-        """
-        Recalculate the horizontal scrollbar depending on the timeline duration.
-        """
-        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)
-
-        widgets_width = controls_width + scrollbar_width
-        end_padding = 500  # 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 run(self):
-        self.testTimeline(self.timeline)
-        GLib.io_add_watch(sys.stdin, GLib.IO_IN, quit2_)
-        Gtk.main()
-
     def _setBestZoomRatio(self):
         """
         Set the zoom level so that the entire timeline is in view.
@@ -860,38 +679,20 @@ class Timeline(Gtk.VBox, Zoomable):
         # 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
 
         timeline_duration = duration + Gst.SECOND - 1
         timeline_duration_s = int(timeline_duration / Gst.SECOND)
 
-        #self.debug("duration: %s, timeline duration: %s" % (print_ns(duration),
-    #       print_ns(timeline_duration)))
-
         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)
+        self.timeline.bTimeline.props.snapping_distance = \
+            Zoomable.pixelToNs(self.app.settings.edgeSnapDeadband)
 
         # Only do this at the very end, after updating the other widgets.
-        #self.log("Setting 'zoomed_fitted' to True")
         self.zoomed_fitted = True
 
-    def zoomFit(self):
-        self._hscrollBar.set_value(0)
-        self._setBestZoomRatio()
-
-    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 _scrollLeft(self):
         self._hscrollBar.set_value(self._hscrollBar.get_value() -
             self.hadj.props.page_size ** (2.0 / 3.0))
@@ -916,177 +717,49 @@ class Timeline(Gtk.VBox, Zoomable):
         canvas_size = self.embed.get_allocation().width - CONTROL_WIDTH
         new_pos = self.timeline.playhead.props.x
         scroll_pos = self.hadj.get_value()
+
         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 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 _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 togglePlayback(self, button):
-        self.pipeline.togglePlayback()
-
-    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)
-
-    def _doAssetAddedCb(self, project, asset, layer):
-        self.addClipToLayer(layer, asset, 2, 10, 5)
-        self.addClipToLayer(layer, asset, 15, 10, 5)
-
-        self.pipeline = Pipeline()
-        self.pipeline.add_timeline(layer.get_timeline())
-
-        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 _snapDistanceChangedCb(self, settings):
-        if self.bTimeline:
-            self.bTimeline.props.snapping_distance = \
-                Zoomable.pixelToNs(settings.edgeSnapDeadband)
-
-    def _projectChangedCb(self, app, project, unused_fully_loaded):
-        """
-        When a project is loaded, we connect to its pipeline
-        """
-#        self.debug("Project changed")
-
-        if project:
-#            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.ruler.setProjectFrameRate(self._project.videorate)
-            self.ruler.zoomChanged()
-            self._renderingSettingsChangedCb(self._project, None, None)
-
-            self._setBestZoomRatio()
-
-    def _projectCreatedCb(self, app, project):
-        """
-        When a project is created, we connect to it timeline
-        """
-#        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
-
-            self._pipeline = None
-            self._seeker = None
-
-        self._project = project
-        if self._project:
-            self._project.connect("rendering-settings-changed",
-                                  self._renderingSettingsChangedCb)
-            self.setTimeline(project.timeline)
-
-    def _zoomInCb(self, unused_action):
-        # This only handles the button callbacks (from the menus),
-        # not keyboard shortcuts or the zoom slider!
-        Zoomable.zoomIn()
-        self.log("Setting 'zoomed_fitted' to False")
-        self.zoomed_fitted = False
-
-    def _zoomOutCb(self, unused_action):
-        # This only handles the button callbacks (from the menus),
-        # not keyboard shortcuts or the zoom slider!
-        Zoomable.zoomOut()
-        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):
+    def _deleteSelected(self, unused_action):
         if self.timeline:
             self.app.action_log.begin("delete clip")
+
             #FIXME GES port: Handle unlocked TrackElement-s
             for clip in self.timeline.selection:
                 layer = clip.get_layer()
                 layer.remove_clip(clip)
+
             self.app.action_log.commit()
 
-    def ungroupSelected(self, unused_action):
+    def _ungroupSelected(self, unused_action):
         if self.timeline:
-            self.debug("Ungouping selected clips %s" % self.timeline.selection)
             self.timeline.enable_update(False)
             self.app.action_log.begin("ungroup")
+
             for clip in self.timeline.selection:
                 clip.ungroup(False)
+
             self.timeline.enable_update(True)
             self.app.action_log.commit()
 
-    def groupSelected(self, unused_action):
+    def _groupSelected(self, unused_action):
         if self.timeline:
-            self.debug("Gouping selected clips %s" % self.timeline.selection)
             self.timeline.enable_update(False)
             self.app.action_log.begin("group")
+
             GES.Container.group(self.timeline.selection)
+
             self.app.action_log.commit()
             self.timeline.enable_update(True)
 
-    def alignSelected(self, unused_action):
+    def _alignSelected(self, unused_action):
         if "NumPy" in missing_soft_deps:
             DepsManager(self.app)
 
         elif self.timeline:
             progress_dialog = AlignmentProgressDialog(self.app)
+
             progress_dialog.window.show()
             self.app.action_log.begin("align")
             self.timeline.enable_update(False)
@@ -1097,14 +770,16 @@ class Timeline(Gtk.VBox, Zoomable):
                 progress_dialog.window.destroy()
 
             pmeter = self.timeline.alignSelection(alignedCb)
+
             pmeter.addWatcher(progress_dialog.updatePosition)
 
-    def split(self, action):
+    def _split(self, action):
         """
         Split clips at the current playhead position, regardless of selections.
         """
         self.bTimeline.enable_update(False)
         position = self.app.current.pipeline.getPosition()
+
         for track in self.bTimeline.get_tracks():
             for element in track.get_elements():
                 start = element.get_start()
@@ -1112,15 +787,17 @@ class Timeline(Gtk.VBox, Zoomable):
                 if start < position and end > position:
                     clip = element.get_parent()
                     clip.split(position)
+
         self.bTimeline.enable_update(True)
 
-    def keyframe(self, action):
+    def _keyframe(self, action):
         """
         Add or remove a keyframe at the current position of the selected clip.
 
         FIXME GES: this method is currently not used anywhere
         """
         selected = self.timeline.selection.getSelectedTrackElements()
+
         for obj in selected:
             keyframe_exists = False
             position = self.app.current.pipeline.getPosition()
@@ -1140,6 +817,111 @@ class Timeline(Gtk.VBox, Zoomable):
                     interpolator.newKeyframe(position_in_obj)
                     self.app.action_log.commit()
 
+    def _playPause(self, unused_action):
+        self.app.current.pipeline.togglePlayback()
+
+    # Interface
+
+    # Zoomable
+
+    def zoomChanged(self):
+        if self._settings and self.bTimeline:
+            # zoomChanged might be called various times before the UI is ready
+            self.bTimeline.props.snapping_distance = \
+                Zoomable.pixelToNs(self._settings.edgeSnapDeadband)
+
+        self.updateHScrollAdjustments()
+
+    # Callbacks
+
+    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 _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)
+
+    def _snapDistanceChangedCb(self, settings):
+        if self.bTimeline:
+            self.bTimeline.props.snapping_distance = \
+                Zoomable.pixelToNs(settings.edgeSnapDeadband)
+
+    def _projectChangedCb(self, app, project, unused_fully_loaded):
+        """
+        When a project is loaded, we connect to its pipeline
+        """
+
+        if project:
+            self._seeker = self._project.seeker
+            self.timeline.setPipeline(self._project.pipeline)
+
+            self.ruler.setProjectFrameRate(self._project.videorate)
+            self.ruler.zoomChanged()
+
+            self._renderingSettingsChangedCb(self._project, None, None)
+            self._setBestZoomRatio()
+
+    def _projectCreatedCb(self, app, project):
+        """
+        When a project is created, we connect to it timeline
+        """
+        if self._project:
+            self._project.disconnect_by_func(self._renderingSettingsChangedCb)
+            try:
+                self.timeline.pipeline.disconnect_by_func(self.timeline.positionCb)
+            except AttributeError:
+                pass
+            except TypeError:
+                pass  # We were not connected no problem
+
+            self.timeline.pipeline = None
+            self._seeker = None
+
+        self._project = project
+        if self._project:
+            self._project.connect("rendering-settings-changed",
+                                  self._renderingSettingsChangedCb)
+            self.setTimeline(project.timeline)
+
+    def _zoomInCb(self, unused_action):
+        # This only handles the button callbacks (from the menus),
+        # not keyboard shortcuts or the zoom slider!
+        Zoomable.zoomIn()
+        self.log("Setting 'zoomed_fitted' to False")
+        self.zoomed_fitted = False
+
+    def _zoomOutCb(self, unused_action):
+        # This only handles the button callbacks (from the menus),
+        # not keyboard shortcuts or the zoom slider!
+        Zoomable.zoomOut()
+        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 _previousKeyframeCb(self, action):
         position = self.app.current.pipeline.getPosition()
         prev_kf = self.timeline.getPrevKeyframe(position)
@@ -1154,27 +936,6 @@ class Timeline(Gtk.VBox, Zoomable):
             self._seeker.seek(next_kf)
             self.scrollToPlayhead()
 
-    def playPause(self, unused_action):
-        self.app.current.pipeline.togglePlayback()
-
-    def setTimeline(self, bTimeline):
-        self.bTimeline = bTimeline
-        self.timeline.selection.connect("selection-changed", self._selectionChangedCb)
-        self.timeline.setTimeline(bTimeline)
-
-    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)
-
     def _scrollEventCb(self, embed, event):
         # FIXME : see https://bugzilla.gnome.org/show_bug.cgi?id=697522
         deltas = event.get_scroll_deltas()
@@ -1196,6 +957,34 @@ class Timeline(Gtk.VBox, Zoomable):
                 self._scrollLeft()
         self.scrolled += 1
 
+    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)
+
+    # Standalone
+
+    # Standalone public API
+
+    def run(self):
+        self.testTimeline(self.timeline)
+        GLib.io_add_watch(sys.stdin, GLib.IO_IN, quit2_)
+        Gtk.main()
+
+    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 togglePlayback(self, button):
+        self.pipeline.togglePlayback()
+
     def testTimeline(self, timeline):
         timeline.set_easing_duration(600)
 
@@ -1218,6 +1007,40 @@ class Timeline(Gtk.VBox, Zoomable):
         self.project.connect("asset-added", self._doAssetAddedCb, layer)
         self.project.create_asset("file://" + sys.argv[1], GES.UriClip)
 
+    # Standalone internal API
+
+    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
+
+    # Standalone callbacks
+
+    def _doAssetAddedCb(self, project, asset, layer):
+        self.addClipToLayer(layer, asset, 2, 10, 5)
+        self.addClipToLayer(layer, asset, 15, 10, 5)
+
+        self.pipeline = Pipeline()
+        self.pipeline.add_timeline(layer.get_timeline())
+
+        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)
+
 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 8655051..a9f5f6e 100644
--- a/pitivi/utils/ui.py
+++ b/pitivi/utils/ui.py
@@ -58,8 +58,12 @@ PADDING = 6
 
 SPACING = 10
 
+PLAYHEAD_WIDTH = 2
+
 CANVAS_SPACING = 21
 
+CONTROL_WIDTH = 250
+
 # Layer creation blocking time in s
 LAYER_CREATION_BLOCK_TIME = 0.2
 
diff --git a/pitivi/utils/widgets.py b/pitivi/utils/widgets.py
index 2fa6a24..9261fc0 100644
--- a/pitivi/utils/widgets.py
+++ b/pitivi/utils/widgets.py
@@ -42,6 +42,9 @@ from pitivi.utils.loggable import Loggable
 from pitivi.configure import get_ui_dir
 from pitivi.utils.ui import unpack_color, pack_color_32, pack_color_64, \
     time_to_string, SPACING
+from pitivi.utils.timeline import Zoomable
+
+ZOOM_FIT = _("Zoom Fit")
 
 
 class DynamicWidget(object):
@@ -1079,3 +1082,65 @@ class BaseTabs(Gtk.Notebook):
                                         resize=True, shrink=False)
         self.app.gui.mainhpaned.pack1(self.app.gui.secondhpaned,
                                       resize=True, shrink=False)
+
+
+class ZoomBox(Gtk.HBox, Zoomable):
+    def __init__(self, timeline):
+        """
+        This will hold the widgets responsible for zooming.
+        """
+        Gtk.HBox.__init__(self)
+        Zoomable.__init__(self)
+
+        self.timeline = timeline
+
+        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)
+
+        # 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)
+
+        self.show_all()
+
+        self._updateZoomSlider = True
+
+    def _zoomAdjustmentChangedCb(self, adjustment):
+        # GTK crack
+        self._updateZoomSlider = False
+        Zoomable.setZoomLevel(int(adjustment.get_value()))
+        self.zoomed_fitted = False
+        self._updateZoomSlider = True
+
+    def _zoomFitCb(self, button):
+        self.timeline.zoomFit()
+
+    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 zoomChanged(self):
+        if self._updateZoomSlider:
+            self._zoomAdjustment.set_value(self.getCurrentZoomLevel())


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