pitivi r1243 - in trunk: . pitivi pitivi/elements pitivi/timeline pitivi/ui po tests



Author: edwardrv
Date: Fri Aug 29 16:45:42 2008
New Revision: 1243
URL: http://svn.gnome.org/viewvc/pitivi?rev=1243&view=rev

Log:
Merging Brandon Lewis SOC branch into trunk

Added:
   trunk/pitivi/elements/imagefreeze.py
   trunk/pitivi/ui/util.py
   trunk/tests/testHList.py
   trunk/tests/test_binary_search.py
   trunk/tests/testcomplex.py
   trunk/tests/testmagnets.py   (contents, props changed)
Modified:
   trunk/ChangeLog
   trunk/pitivi/Makefile.am
   trunk/pitivi/bin.py
   trunk/pitivi/discoverer.py
   trunk/pitivi/elements/Makefile.am
   trunk/pitivi/objectfactory.py
   trunk/pitivi/pitivi.py
   trunk/pitivi/playground.py
   trunk/pitivi/project.py
   trunk/pitivi/settings.py
   trunk/pitivi/timeline/composition.py
   trunk/pitivi/timeline/objects.py
   trunk/pitivi/timeline/source.py
   trunk/pitivi/timeline/timeline.py
   trunk/pitivi/ui/Makefile.am
   trunk/pitivi/ui/actions.xml
   trunk/pitivi/ui/complexinterface.py
   trunk/pitivi/ui/complexlayer.py
   trunk/pitivi/ui/complexsource.py
   trunk/pitivi/ui/complextimeline.py
   trunk/pitivi/ui/layerwidgets.py
   trunk/pitivi/ui/mainwindow.py
   trunk/pitivi/ui/ruler.py
   trunk/pitivi/ui/sourcefactories.py
   trunk/pitivi/ui/timeline.py
   trunk/pitivi/ui/timelineobjects.py
   trunk/pitivi/ui/viewer.py
   trunk/pitivi/utils.py
   trunk/po/ca.po
   trunk/po/de.po
   trunk/po/dz.po
   trunk/po/el.po
   trunk/po/en_GB.po
   trunk/po/es.po
   trunk/po/fi.po
   trunk/po/fr.po
   trunk/po/it.po
   trunk/po/lv.po
   trunk/po/nb.po
   trunk/po/oc.po
   trunk/po/pa.po
   trunk/po/pt.po
   trunk/po/pt_BR.po
   trunk/po/sv.po
   trunk/po/zh_CN.po
   trunk/tests/Makefile.am
   trunk/tests/common.py
   trunk/tests/runtests.py

Modified: trunk/pitivi/Makefile.am
==============================================================================
--- trunk/pitivi/Makefile.am	(original)
+++ trunk/pitivi/Makefile.am	Fri Aug 29 16:45:42 2008
@@ -8,28 +8,28 @@
 
 pitivi_PYTHON = \
 	__init__.py 	\
-	configure.py 	\
-	instance.py 	\
 	bin.py 		\
 	check.py 	\
+	configure.py 	\
 	discoverer.py 	\
 	dnd.py 		\
 	effects.py	\
+	instance.py 	\
 	objectfactory.py \
 	pitivi.py 	\
+	pitivigstutils.py \
 	playground.py 	\
+	plugincore.py	\
+	pluginmanager.py \
 	project.py 	\
+	projectsaver.py	\
+	serializable.py	\
 	settings.py 	\
-	sourcelist.py 	\
-	utils.py 	\
-	pitivigstutils.py \
 	signalgroup.py	\
+	sourcelist.py 	\
 	threads.py	\
 	thumbnailer.py	\
-	serializable.py	\
-	projectsaver.py	\
-	plugincore.py	\
-	pluginmanager.py
+	utils.py
 
 BUILT_SOURCES=configure.py
 

Modified: trunk/pitivi/bin.py
==============================================================================
--- trunk/pitivi/bin.py	(original)
+++ trunk/pitivi/bin.py	Fri Aug 29 16:45:42 2008
@@ -396,7 +396,7 @@
                           has_video = factory.is_video,
                           has_audio = factory.is_audio,
                           width = width, height = height,
-                          length = factory.length)
+                          length = factory.getDuration())
 
     def _addSource(self):
         self.add(self.source)

Modified: trunk/pitivi/discoverer.py
==============================================================================
--- trunk/pitivi/discoverer.py	(original)
+++ trunk/pitivi/discoverer.py	Fri Aug 29 16:45:42 2008
@@ -80,7 +80,6 @@
         self.current = None
         self.currentTags = []
         self.pipeline = None
-        self.thumbnailing = False
         self.thisdone = False
         self.prerolled = False
         self.nomorepads = False
@@ -89,6 +88,7 @@
         self.error = None # reason for error
         self.extrainfo = None # extra information about the error
         self.fakesink = None
+        self.isimage = False # Used to know if the file is an image
 
     def addFile(self, filename):
         """ queue a filename to be discovered """
@@ -153,7 +153,9 @@
             self.emit('not_media_file', self.current, self.error, self.extrainfo)
         elif self.currentfactory:
             self.currentfactory.addMediaTags(self.currentTags)
-            if not self.currentfactory.length:
+            if self.isimage:
+                self.currentfactory.setThumbnail(gst.uri_get_location(self.current))
+            if not self.currentfactory.getDuration() and not self.isimage:
                 self.emit('not_media_file', self.current,
                           _("Could not establish the duration of the file."),
                           _("This clip seems to be in a format which cannot be accessed in a random fashion."))
@@ -169,6 +171,7 @@
         self.nomorepads = False
         self.error = None
         self.extrainfo = None
+        self.isimage = False
 
         # restart an analysis if there's more...
         if self.queue:
@@ -211,6 +214,8 @@
         self.signalsid.append((dbin, dbin.connect("new-decoded-pad", self._newDecodedPadCb)))
         self.signalsid.append((dbin, dbin.connect("unknown-type", self._unknownTypeCb)))
         self.signalsid.append((dbin, dbin.connect("no-more-pads", self._noMorePadsCb)))
+        tfind = dbin.get_by_name("typefind")
+        self.signalsid.append((tfind, tfind.connect("have-type", self._typefindHaveTypeCb)))
         self.pipeline.add(source, dbin)
         source.link(dbin)
         gst.info("analysis pipeline created")
@@ -237,6 +242,10 @@
         # return False so we don't get called again
         return False
 
+    def _typefindHaveTypeCb(self, typefind, perc, caps):
+        if caps.to_string().startswith("image/"):
+            self.isimage = True
+
     def _busMessageCb(self, unused_bus, message):
         if self.thisdone:
             return
@@ -250,7 +259,7 @@
                     # Let's get the information from all the pads
                     self._getPadsInfo()
                     # Only go to PLAYING if we have an video stream to thumbnail
-                    if self.currentfactory and self.currentfactory.is_video:
+                    if self.currentfactory and self.currentfactory.is_video and not self.isimage:
                         gst.log("pipeline has gone to PAUSED, now pushing to PLAYING")
                         if self.pipeline.set_state(gst.STATE_PLAYING) == gst.STATE_CHANGE_FAILURE:
                             if not self.error:
@@ -290,7 +299,7 @@
 
     def _handleError(self, gerror, detail, unused_source):
         gst.warning("got an ERROR")
-        
+
         if not self.error:
             self.error = _("An internal error occured while analyzing this file : %s") % gerror.message
             self.extrainfo = detail
@@ -319,7 +328,7 @@
                     self.currentfactory.setAudioInfo(caps)
                 elif caps.to_string().startswith("video/x-raw") and not self.currentfactory.video_info:
                     self.currentfactory.setVideoInfo(caps)
-            if not self.currentfactory.length:
+            if not self.currentfactory.getDuration():
                 try:
                     length, format = pad.query_duration(gst.FORMAT_TIME)
                 except:

Modified: trunk/pitivi/elements/Makefile.am
==============================================================================
--- trunk/pitivi/elements/Makefile.am	(original)
+++ trunk/pitivi/elements/Makefile.am	Fri Aug 29 16:45:42 2008
@@ -2,6 +2,7 @@
 
 elements_PYTHON = \
 	__init__.py \
+	imagefreeze.py \
 	singledecodebin.py \
 	smartscale.py	\
 	thumbnailsink.py \

Added: trunk/pitivi/elements/imagefreeze.py
==============================================================================
--- (empty file)
+++ trunk/pitivi/elements/imagefreeze.py	Fri Aug 29 16:45:42 2008
@@ -0,0 +1,298 @@
+# PiTiVi , Non-linear video editor
+#
+#       pitivi/elements/singledecodebin.py
+#
+# Copyright (c) 2005, Edward Hervey <bilboed bilboed com>
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this program; if not, write to the
+# Free Software Foundation, Inc., 59 Temple Place - Suite 330,
+# Boston, MA 02111-1307, USA.
+
+"""
+Image-to-video element
+"""
+
+# Goal:
+#
+# We want to take a raw image source and output a continuous
+# video feed (by default [0,GST_CLOCK_TIME_NONE]) according to
+# the srcpad negotiated caps (i.e. proper timestamps)
+#
+# In the event of seeks, this is the element that will handle the seeks
+# and output the proper segments.
+#
+# for a given negotiated framerate R (in frames/second):
+# The outputted buffer timestamps will be X * 1/R
+# where X is an integer.
+# EXCEPT for accurate segment seeks where the first/last buffers will be
+# adjusted to the requested start/stop values
+
+import gobject
+import gst
+
+
+class ImageFreeze(gst.Element):
+
+    __gstdetails__ = (
+        "ImageFreeze plugin",
+        "imagefreeze.py",
+        "Outputs a video feed out an incoming frame",
+        "Edward Hervey <bilboed bilboed com>")
+
+    _srctemplate = gst.PadTemplate("src",
+                                   gst.PAD_SRC,
+                                   gst.PAD_ALWAYS,
+                                   gst.caps_new_any())
+    _sinktemplate = gst.PadTemplate("sink",
+                                   gst.PAD_SINK,
+                                   gst.PAD_ALWAYS,
+                                   gst.caps_new_any())
+    __gsttemplates__ = (_srctemplate, _sinktemplate)
+
+    def __init__(self, *args, **kwargs):
+        gst.Element.__init__(self, *args, **kwargs)
+        self.srcpad = gst.Pad(self._srctemplate)
+        self.srcpad.set_event_function(self._src_event)
+
+        self.sinkpad = gst.Pad(self._sinktemplate)
+        self.sinkpad.set_chain_function(self._sink_chain)
+        self.sinkpad.set_event_function(self._sink_event)
+        self.sinkpad.set_setcaps_function(self._sink_setcaps)
+
+        self.add_pad(self.srcpad)
+        self.add_pad(self.sinkpad)
+
+        self._reset()
+
+    def _reset(self):
+        gst.debug("resetting ourselves")
+        self._outputrate = None
+        self._srccaps = None
+        # number of outputted buffers
+        self._offset = 0
+        self._segment = gst.Segment()
+        self._segment.init(gst.FORMAT_TIME)
+        self._needsegment = True
+        self._bufferduration = 0
+        # this is the buffer we store and repeatedly output
+        self._buffer = None
+        # this will be set by our task
+        self.last_return = gst.FLOW_OK
+
+    def _sink_setcaps(self, pad, caps):
+        gst.debug("caps %s" % caps.to_string())
+        downcaps = self.srcpad.peer_get_caps().copy()
+        gst.debug("downcaps %s" % downcaps.to_string())
+
+        # methodology
+        # 1. We override any incoming framerate
+        ccaps = caps.make_writable()
+        for struct in ccaps:
+            if struct.has_key("framerate"):
+                try:
+                    del struct["framerate"]
+                except:
+                    gst.warning("Couldn't remove 'framerate' from %s" % struct.to_string())
+
+        # 2. we do the intersection of our incoming stripped caps
+        #    and the downstream caps
+        intersect = ccaps.intersect(downcaps)
+        if intersect.is_empty():
+            gst.warning("no negotiation possible !")
+            return False
+
+        # 3. for each candidate in the intersection, we try to set that
+        #    candidate downstream
+        for candidate in intersect:
+            gst.debug("Trying %s" % candidate.to_string())
+            if self.srcpad.peer_accept_caps(candidate):
+                gst.debug("accepted !")
+                # 4. When we have an accepted caps downstream, we store the negotiated
+                #    framerate and return
+                self._outputrate = candidate["framerate"]
+                self._bufferduration = gst.SECOND * self._outputrate.denom / self._outputrate.num
+                self._srccaps = candidate
+                return self.srcpad.set_caps(candidate)
+
+        # 5. If we can't find an accepted candidate, we return False
+        return False
+
+    def _src_event(self, pad, event):
+        # for the moment we just push it upstream
+        gst.debug("event %r" % event)
+        if event.type == gst.EVENT_SEEK:
+            rate,fmt,flags,startt,start,stopt,stop = event.parse_seek()
+            gst.debug("Handling seek event %r" % flags)
+            if flags & gst.SEEK_FLAG_FLUSH:
+                gst.debug("sending flush_start event")
+                self.srcpad.push_event(gst.event_new_flush_start())
+            self._segment.set_seek(*event.parse_seek())
+            gst.debug("_segment start:%s stop:%s" % (gst.TIME_ARGS(self._segment.start),
+                                                     gst.TIME_ARGS(self._segment.stop)))
+            # create a new initial seek
+            gst.debug("pausing task")
+            self.srcpad.pause_task()
+            gst.debug("task paused")
+            seek = gst.event_new_seek(1.0, gst.FORMAT_TIME, flags,
+                                      gst.SEEK_TYPE_NONE, 0,
+                                      gst.SEEK_TYPE_NONE, 0)
+            #return self.sinkpad.push_event(seek)
+            self._needsegment = True
+            if flags & gst.SEEK_FLAG_FLUSH:
+                self.srcpad.push_event(gst.event_new_flush_stop())
+            self.srcpad.start_task(self.our_task)
+            return True
+
+        return self.sinkpad.push_event(event)
+
+    def _sink_event(self, pad, event):
+        gst.debug("event %r" % event)
+        if event.type == gst.EVENT_NEWSEGMENT:
+            gst.debug("dropping new segment !")
+            return True
+        elif event.type == gst.EVENT_FLUSH_START:
+            self._reset()
+        return self.srcpad.push_event(event)
+
+    def _sink_chain(self, pad, buffer):
+        gst.debug("buffer %s %s" % (gst.TIME_ARGS(buffer.timestamp),
+                                    gst.TIME_ARGS(buffer.duration)))
+        if self._buffer != None:
+            gst.debug("already have a buffer ! Returning GST_FLOW_WRONG_STATE")
+            return gst.FLOW_WRONG_STATE
+
+        self._buffer = buffer
+        self.srcpad.start_task(self.our_task)
+        return gst.FLOW_WRONG_STATE
+
+    def our_task(self, something):
+        #this is where we repeatedly output our buffer
+        gst.debug("self:%r, something:%r" % (self, something))
+
+        gst.debug("needsegment: %r" % self._needsegment)
+        if self._needsegment:
+            gst.debug("Need to output a new segment")
+            segment = gst.event_new_new_segment(False,
+                                                self._segment.rate,
+                                                self._segment.format,
+                                                self._segment.start,
+                                                self._segment.stop,
+                                                self._segment.start)
+            self.srcpad.push_event(segment)
+            # calculate offset
+            # offset is int(segment.start / outputrate)
+            self._offset = int(self._segment.start * self._outputrate.num / self._outputrate.denom / gst.SECOND)
+            self._needsegment = False
+            gst.debug("Newsegment event pushed")
+
+        # new position
+        position = self._offset * gst.SECOND * self._outputrate.denom / self._outputrate.num
+        if self._segment.stop != -1 and position > self._segment.stop:
+            gst.debug("end of configured segment (position:%s / segment_stop:%s)" % (gst.TIME_ARGS(position),
+                                                                                     gst.TIME_ARGS(self._segment.stop)))
+            # end of stream/segment handling
+            if self._segment.flags & gst.SEEK_FLAG_SEGMENT:
+                # emit a gst.MESSAGE_SEGMENT_DONE
+                self.post_message(gst.message_new_segment_done(self, gst.FORMAT_TIME, self._segment.stop))
+            else:
+                self.srcpad.push_event(gst.event_new_eos())
+            self.last_return = gst.FLOW_WRONG_STATE
+            self.srcpad.pause_task()
+
+        # we need to update the caps here !
+        obuf = self._buffer.make_metadata_writable()
+        ok, nstart, nstop = self._segment.clip(gst.FORMAT_TIME,
+                                               position, position + self._bufferduration)
+        if ok:
+            obuf.timestamp = nstart
+            obuf.duration = nstop - nstart
+            obuf.caps = self._srccaps
+            gst.debug("Pushing out buffer %s" % gst.TIME_ARGS(obuf.timestamp))
+            self.last_return = self.srcpad.push(obuf)
+        self._offset += 1
+
+        if self.last_return != gst.FLOW_OK:
+            gst.debug("Pausing ourself, last_return : %s" % gst.flow_get_name(self.last_return))
+            self.srcpad.pause_task()
+
+    def do_change_state(self, transition):
+        if transition in [gst.STATE_CHANGE_READY_TO_PAUSED, gst.STATE_CHANGE_PAUSED_TO_READY]:
+            self._reset()
+        return gst.Element.do_change_state(self, transition)
+
+gobject.type_register(ImageFreeze)
+
+def dataprobe(pad, data):
+    if isinstance(data, gst.Buffer):
+        print "Buffer", gst.TIME_ARGS(data.timestamp), gst.TIME_ARGS(data.duration), data.caps.to_string()
+    else:
+        print "Event", data.type
+        if data.type == gst.EVENT_NEWSEGMENT:
+            print data.parse_new_segment()
+    return True
+
+def make_image_video_bin(location):
+    b = gst.Bin("image-video-bin-"+location)
+    src = gst.element_factory_make("filesrc")
+    src.props.location = location
+    src.props.blocksize = 1024 * 1024
+    dec = gst.element_factory_make("jpegdec")
+    vscale = gst.element_factory_make("videoscale")
+    freeze = ImageFreeze()
+    cfil = gst.element_factory_make("capsfilter")
+    cfil.props.caps = gst.Caps("video/x-raw-yuv,framerate=25/1")
+    p.add(src, dec, vscale, freeze, cfil)
+    gst.element_link_many(src, dec, vscale)
+    vscale.link(freeze, gst.Caps("video/x-raw-yuv,width=640,height=480"))
+    gst.element_link_many(freeze, cfil)
+
+    b.add_pad(gst.GhostPad("src", cfil.get_pad("src")))
+
+    return b
+
+def post_link(gnls, pad, q):
+    gnls.link(q)
+
+# filesrc ! jpegdec ! imagefreeze ! xvimagesink
+if __name__ == "__main__":
+    import sys
+    p = gst.Pipeline()
+
+    b = make_image_video_bin(sys.argv[1])
+    gnls = gst.element_factory_make("gnlsource")
+    gnls.add(b)
+
+    gnls.props.media_start = 5 * gst.SECOND
+    gnls.props.media_duration = 5 * gst.SECOND
+    gnls.props.duration = 5 * gst.SECOND
+
+    toverl = gst.element_factory_make("timeoverlay")
+    sink = gst.element_factory_make("xvimagesink")
+    sink.get_pad("sink").add_data_probe(dataprobe)
+
+    q = gst.element_factory_make("queue")
+
+    p.add(gnls, toverl, q, sink)
+
+    gst.element_link_many(q, toverl, sink)
+    #q.link(sink)
+
+    gnls.connect("pad-added", post_link, q)
+
+    ml = gobject.MainLoop()
+
+    p.set_state(gst.STATE_PLAYING)
+
+    ml.run()
+

Modified: trunk/pitivi/objectfactory.py
==============================================================================
--- trunk/pitivi/objectfactory.py	(original)
+++ trunk/pitivi/objectfactory.py	Fri Aug 29 16:45:42 2008
@@ -270,7 +270,35 @@
 
 gobject.type_register(ObjectFactory)
 
-class FileSourceFactory(ObjectFactory):
+class SourceFactory(ObjectFactory):
+    """
+    Provides sources usable in a timeline
+    """
+
+    __data_type__ = "source-factory"
+
+    def getDuration(self):
+        """
+        Returns the maximum duration of the source in nanoseconds
+
+        If the source doesn't have a maximum duration (like an image), subclasses
+        should implement this by returning 2**63 - 1 (MAX_LONG).
+        """
+        pass
+
+    def getDefaultDuration(self):
+        """
+        Returns the default duration of a file in nanoseconds,
+        this should be used when using sources initially.
+
+        Most sources will return the same as getDuration(), but can be overriden
+        for sources that have an infinite duration.
+        """
+        return self.getDuration()
+
+gobject.type_register(SourceFactory)
+
+class FileSourceFactory(SourceFactory):
     """
     Provides File sources useable in a timeline
     """
@@ -294,26 +322,33 @@
 
     def __init__(self, filename="", project=None, **kwargs):
         gst.info("filename:%s , project:%s" % (filename, project))
-        ObjectFactory.__init__(self, **kwargs)
+        SourceFactory.__init__(self, **kwargs)
         self.project = project
         self.name = filename
         self.displayname = os.path.basename(unquote(self.name))
         self.lastbinid = 0
-        self.length = 0
-        self.thumbnail = ""
-        self.thumbnails = []
+        self._length = 0
+        self._thumbnail = ""
+        self._thumbnails = []
         self.settings = None
 
+    ## SourceFactory implementation
+    def getDuration(self):
+        return self._length
+
     def do_set_property(self, property, value):
         if property.name == "length":
-            if self.length and self.length != value:
+            if self._length and self._length != value:
                 gst.warning("%s : Trying to set a new length (%s) different from previous one (%s)" % (self.name,
-                                                                                                       gst.TIME_ARGS(self.length),
+                                                                                                       gst.TIME_ARGS(self._length),
                                                                                                        gst.TIME_ARGS(value)))
-            self.length = value
+            self._length = value
         elif property.name == "thumbnail":
+            gst.debug("thumbnail : %s" % value)
             if os.path.isfile(value):
-                self.thumbnail = value
+                self._thumbnail = value
+            else:
+                gst.warning("Thumbnail path is invalid !")
         else:
             ObjectFactory.do_set_property(self, property, value)
 
@@ -380,6 +415,9 @@
         """ Sets the thumbnail filename of the element """
         self.set_property("thumbnail", thumbnail)
 
+    def getThumbnail(self):
+        return self._thumbnail
+
     def getExportSettings(self):
         """ Returns the ExportSettings corresponding to this source """
         if self.settings:
@@ -408,13 +446,13 @@
     def toDataFormat(self):
         ret = ObjectFactory.toDataFormat(self)
         ret["filename"] = self.name
-        ret["length"] = self.length
+        ret["length"] = self._length
         return ret
 
     def fromDataFormat(self, obj):
         ObjectFactory.fromDataFormat(self, obj)
         self.name = obj["filename"]
-        self.length = obj["length"]
+        self._length = obj["length"]
 
 
 class OperationFactory(ObjectFactory):

Modified: trunk/pitivi/pitivi.py
==============================================================================
--- trunk/pitivi/pitivi.py	(original)
+++ trunk/pitivi/pitivi.py	Fri Aug 29 16:45:42 2008
@@ -22,6 +22,7 @@
 """
 Main application
 """
+import os
 import gobject
 import gtk
 import gst
@@ -89,7 +90,7 @@
                        ( ))
         }
 
-    def __init__(self, use_ui=True, *args, **kwargs):
+    def __init__(self, args=[], use_ui=True):
         """
         initialize pitivi with the command line arguments
         """
@@ -108,7 +109,11 @@
                 % APPNAME)
         instance.PiTiVi = self
 
-        # TODO parse cmd line arguments
+        # FIXME: use gnu getopt or somethign of the sort
+        project_file = None
+        if len(args) > 1:
+            if os.path.exists(args[1]):
+                project_file = args[1]
 
         # get settings
         self.settings = GlobalSettings()
@@ -123,9 +128,12 @@
         self.effects = Magician()
 
         if self._use_ui:
+            self.uimanager = gtk.UIManager()
             # we're starting a GUI for the time being
             self.gui = mainwindow.PitiviMainWindow()
             self.gui.show()
+            if project_file:
+                self.loadProject(filepath=project_file)
 
     def do_closing_project(self, project):
         return True

Modified: trunk/pitivi/playground.py
==============================================================================
--- trunk/pitivi/playground.py	(original)
+++ trunk/pitivi/playground.py	Fri Aug 29 16:45:42 2008
@@ -141,7 +141,7 @@
 
         bus = pipeline.get_bus()
         bus.remove_signal_watch()
-        bus.set_sync_handler(None)
+        #bus.set_sync_handler(None)
 
         if pipeline.set_state(gst.STATE_READY) == gst.STATE_CHANGE_FAILURE:
             return False
@@ -206,13 +206,18 @@
         will be taken.
         """
         if isinstance(self.current, SmartTimelineBin):
-            # fast path
             return True
+        p = self.getTimeline()
+        if p:
+            self.switchToPipeline(p)
+            return True
+        return False
+
+    def getTimeline(self):
         for pipeline in self.pipelines:
             if isinstance(pipeline, SmartTimelineBin):
-                self.switchToPipeline(pipeline)
-                return True
-        return False
+                return pipeline
+        return None
 
     def setVideoSinkThread(self, vsinkthread):
         """ sets the video sink thread """

Modified: trunk/pitivi/project.py
==============================================================================
--- trunk/pitivi/project.py	(original)
+++ trunk/pitivi/project.py	Fri Aug 29 16:45:42 2008
@@ -116,7 +116,6 @@
         # FIXME : This should be discovered !
         saveformat = "pickle"
         if self.uri and file_is_project(self.uri):
-            self.timeline = Timeline(self)
             loader = ProjectSaver.newProjectSaver(saveformat)
             path = gst.uri_get_location(self.uri)
             fileobj = open(path, "r")
@@ -126,7 +125,8 @@
             except ProjectLoadError:
                 gst.error("Error while loading the project !!!")
                 return False
-            fileobj.close()
+            finally:
+                fileobj.close()
             self.format = saveformat
             self.urichanged = False
             return True
@@ -282,9 +282,12 @@
     def fromDataFormat(self, obj):
         Serializable.fromDataFormat(self, obj)
         self.name = obj["name"]
-        self.settings = to_object_from_data_type(obj["settings"])
-        self.sources = to_object_from_data_type(obj["sources"])
-        self.timeline = to_object_from_data_type(obj["timeline"])
+        # calling this makes sure settigns-changed signal is emitted
+        self.setSettings(to_object_from_data_type(obj["settings"]))
+        # these objects already exist, so we initialize them from file
+        # to make sure UI catches signals
+        self.sources.fromDataFormat(obj["sources"])
+        self.timeline.fromDataFormat(obj["timeline"])
 
 def uri_is_valid(uri):
     return gst.uri_get_protocol(uri) == "file"

Modified: trunk/pitivi/settings.py
==============================================================================
--- trunk/pitivi/settings.py	(original)
+++ trunk/pitivi/settings.py	Fri Aug 29 16:45:42 2008
@@ -81,7 +81,8 @@
         # reads some settings from environment variable
         #self.advancedModeEnabled =
         #get_bool_env("PITIVI_ADVANCED_MODE")
-        self.fileSupportEnabled = get_bool_env("PITIVI_FILE_SUPPORT")
+        #self.fileSupportEnabled = get_bool_env("PITIVI_FILE_SUPPORT")
+        self.fileSupportEnabled = True
         pass
 
     def get_local_plugin_path(self, autocreate=True):

Modified: trunk/pitivi/timeline/composition.py
==============================================================================
--- trunk/pitivi/timeline/composition.py	(original)
+++ trunk/pitivi/timeline/composition.py	Fri Aug 29 16:45:42 2008
@@ -27,7 +27,7 @@
 import gst
 
 from source import TimelineSource
-from objects import BrotherObjects, MEDIA_TYPE_AUDIO
+from objects import BrotherObjects
 from pitivi.serializable import to_object_from_data_type
 
 class Layer(BrotherObjects):
@@ -427,10 +427,10 @@
         my_add_sorted(self.sources[position-1], source)
 
         # add it to self.gnlobject
-        self.gnlobject.info("adding %s to our composition" % source.gnlobject)
+        self.gnlobject.info("adding %s to our composition" % source.gnlobject.props.name)
         self.gnlobject.add(source.gnlobject)
 
-        self.gnlobject.info("added source %s" % source.gnlobject)
+        self.gnlobject.info("added source %s" % source.gnlobject.props.name)
         gst.info("%s" % str(self.sources))
         self.emit('source-added', source)
 
@@ -447,7 +447,7 @@
         auto_linked : if True will add the brother (if any) of the given source
                 to the linked composition with the same parameters
         """
-        self.gnlobject.info("source %s , position:%d, self.sources:%s" %(source, position, self.sources))
+        self.gnlobject.info("source %s , position:%d, self.sources:%s" %(source.name, position, self.sources))
 
         # make sure object to add has valid start/duration
         if source.start == -1 or source.duration <= 0:
@@ -485,15 +485,15 @@
         gst.info("start=%s, position=%d, existorder=%d, sourcelength=%s" % (gst.TIME_ARGS(start),
                                                                             position,
                                                                             existorder,
-                                                                            gst.TIME_ARGS(source.factory.length)))
+                                                                            gst.TIME_ARGS(source.factory.getDuration())))
         # set the correct start/duration time
-        duration = source.factory.length
+        duration = source.factory.getDuration()
         source.setStartDurationTime(start, duration)
 
         # pushing following
         if push_following and not position in [-1, 0]:
             #print self.gnlobject, "pushing following", existorder, len(self.sources[position - 1][2])
-            self.shiftSources(source.factory.length, existorder, len(self.sources[position - 1][2]))
+            self.shiftSources(source.factory.getDuration(), existorder, len(self.sources[position - 1][2]))
 
         self.addSource(source, position, auto_linked=auto_linked)
 
@@ -769,7 +769,7 @@
             # return the settings of our only source
             return self.sources[0][2][0].getExportSettings()
         else:
-            if self.media_type == MEDIA_TYPE_AUDIO:
+            if self.isAudio():
                 return self._autoAudioSettings()
             else:
                 return self._autoVideoSettings()

Modified: trunk/pitivi/timeline/objects.py
==============================================================================
--- trunk/pitivi/timeline/objects.py	(original)
+++ trunk/pitivi/timeline/objects.py	Fri Aug 29 16:45:42 2008
@@ -313,8 +313,8 @@
                                  (gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT, ))
         }
 
-    def __init__(self, factory=None, start=-1, duration=-1,
-                 media_type=MEDIA_TYPE_NONE, name="", **kwargs):
+    def __init__(self, factory=None, start=gst.CLOCK_TIME_NONE,
+                 duration=0, media_type=MEDIA_TYPE_NONE, name="", **kwargs):
         BrotherObjects.__init__(self, **kwargs)
         self.name = name
         gst.log("new TimelineObject :%s %r" % (name, self))
@@ -349,18 +349,18 @@
             self.gnlobject.connect("notify::duration", self._startDurationChangedCb)
             self._setStartDurationTime(self.start, self.duration, True)
 
-    def _setStartDurationTime(self, start=-1, duration=-1, force=False):
+    def _setStartDurationTime(self, start=gst.CLOCK_TIME_NONE, duration=0, force=False):
         # really modify the start/duration time
         self.gnlobject.info("start:%s , duration:%s" %( gst.TIME_ARGS(start),
                                                         gst.TIME_ARGS(duration)))
-        if not duration == -1 and (not self.duration == duration or force):
+        if duration > 0 and (not self.duration == duration or force):
             self.duration = duration
             self.gnlobject.set_property("duration", long(duration))
-        if not start == -1 and (not self.start == start or force):
+        if not start == gst.CLOCK_TIME_NONE and (not self.start == start or force):
             self.start = start
             self.gnlobject.set_property("start", long(start))
 
-    def setStartDurationTime(self, start=-1, duration=-1):
+    def setStartDurationTime(self, start=gst.CLOCK_TIME_NONE, duration=0):
         """ sets the start and/or duration time """
         self._setStartDurationTime(start, duration)
         if self.linked:
@@ -372,22 +372,25 @@
         if not gnlobject == self.gnlobject:
             gst.warning("We're receiving signals from an object we dont' control (self.gnlobject:%r, gnlobject:%r)" % (self.gnlobject, gnlobject))
         self.gnlobject.debug("property:%s" % property.name)
-        start = -1
-        duration = -1
+        start = gst.CLOCK_TIME_NONE
+        duration = 0
         if property.name == "start":
             start = gnlobject.get_property("start")
+            gst.log("start: %s => %s" % (gst.TIME_ARGS(self.start),
+                                         gst.TIME_ARGS(start)))
             if start == self.start:
-                start = -1
+                start = gst.CLOCK_TIME_NONE
             else:
                 self.start = long(start)
         elif property.name == "duration":
             duration = gnlobject.get_property("duration")
+            gst.log("duration: %s => %s" % (gst.TIME_ARGS(self.duration),
+                                            gst.TIME_ARGS(duration)))
             if duration == self.duration:
-                duration = -1
+                duration = 0
             else:
                 self.gnlobject.debug("duration changed:%s" % gst.TIME_ARGS(duration))
                 self.duration = long(duration)
-        #if not start == -1 or not duration == -1:
         self.emit("start-duration-changed", self.start, self.duration)
 
 
@@ -425,3 +428,9 @@
             self._setFactory(obj)
         else:
             BrotherObjects.pendingObjectCreated(self, obj, field)
+
+    def isAudio(self):
+        return self.media_type == MEDIA_TYPE_AUDIO
+
+    def isVideo(self):
+        return self.media_type == MEDIA_TYPE_VIDEO

Modified: trunk/pitivi/timeline/source.py
==============================================================================
--- trunk/pitivi/timeline/source.py	(original)
+++ trunk/pitivi/timeline/source.py	Fri Aug 29 16:45:42 2008
@@ -31,15 +31,125 @@
 class TimelineSource(TimelineObject):
     """
     Base class for all sources (O input)
+
+    Save/Load properties:
+    * 'media-start' (int) : start position of the media
+    * 'media-duration' (int) : duration of the media
     """
+    __gsignals__ = {
+        "media-start-duration-changed" : ( gobject.SIGNAL_RUN_LAST,
+                                       gobject.TYPE_NONE,
+                                       (gobject.TYPE_UINT64, gobject.TYPE_UINT64))
+        }
+
 
     __data_type__ = "timeline-source"
 
-    # FIXME : media_start and media_duration should be in this class
 
-    def __init__(self, **kw):
+    def __init__(self, media_start=gst.CLOCK_TIME_NONE,
+                 media_duration=0, **kw):
+        self.media_start = media_start
+        self.media_duration = media_duration
         TimelineObject.__init__(self, **kw)
 
+    def _makeGnlObject(self):
+        gst.debug("Making a source for %r" % self)
+        if self.isAudio():
+            caps = gst.caps_from_string("audio/x-raw-int;audio/x-raw-float")
+            postfix = "audio"
+        elif self.isVideo():
+            caps = gst.caps_from_string("video/x-raw-yuv;video/x-raw-rgb")
+            postfix = "video"
+        else:
+            raise NameError, "media type is NONE !"
+
+        if self.factory:
+            self.factory.lastbinid = self.factory.lastbinid + 1
+            sourcename =  "source-" + self.name + "-" + postfix + str(self.factory.lastbinid)
+        else:
+            sourcename = "source-" + self.name + "-" + postfix
+        gnl = gst.element_factory_make("gnlsource", sourcename)
+
+        try:
+            gst.debug("calling makeGnlSourceContents()")
+            obj = self.makeGnlSourceContents()
+        except:
+            gst.debug("Failure in calling self.makeGnlSourceContents()")
+            return None
+        gnl.add(obj)
+
+        # set properties
+        gnl.set_property("media-duration", long(self.media_duration))
+        gnl.set_property("media-start", long(self.media_start))
+        gnl.set_property("caps", caps)
+        gnl.connect("notify::media-start", self._mediaStartDurationChangedCb)
+        gnl.connect("notify::media-duration", self._mediaStartDurationChangedCb)
+        return gnl
+
+    def makeGnlSourceContents(self):
+        """
+        Return the contents of the gnlsource.
+        Should be a single element (or bin).
+
+        Sub-classes not implementing this method will need to override
+        the _makeGnlObject() method.
+        """
+        raise NotImplementedError
+
+    def _setMediaStartDurationTime(self, start=gst.CLOCK_TIME_NONE,
+                                   duration=0):
+        gst.info("TimelineFileSource %s start:%s , duration:%s" % (
+            self,
+            gst.TIME_ARGS(start),
+            gst.TIME_ARGS(duration)))
+        gst.info("TimelineFileSource %s EXISTING start:%s , duration:%s" % (
+            self,
+            gst.TIME_ARGS(self.media_start),
+            gst.TIME_ARGS(self.media_duration)))
+        if duration > 0 and not self.media_duration == duration:
+            self.gnlobject.set_property("media-duration", long(duration))
+        if not start == gst.CLOCK_TIME_NONE and not self.media_start == start:
+            self.gnlobject.set_property("media-start", long(start))
+
+    def setMediaStartDurationTime(self, start=gst.CLOCK_TIME_NONE,
+                                  duration=0):
+        """ sets the media start/duration time """
+        if not start == gst.CLOCK_TIME_NONE and start < 0:
+            gst.warning("Can't set start values < 0 !")
+            return
+        if duration < 0:
+            gst.warning("Can't set durations < 0 !")
+            return
+        self._setMediaStartDurationTime(start, duration)
+        if self.linked and isinstance(self.linked, TimelineFileSource):
+            self.linked._setMediaStartDurationTime(start, duration)
+
+    def _mediaStartDurationChangedCb(self, gnlobject, property):
+        gst.log("%r %s %s" % (gnlobject, property, property.name))
+        mstart = None
+        mduration = None
+        if property.name == "media-start":
+            mstart = gnlobject.get_property("media-start")
+            gst.log("start: %s => %s" % (gst.TIME_ARGS(self.media_start),
+                                         gst.TIME_ARGS(mstart)))
+            if self.media_start == gst.CLOCK_TIME_NONE:
+                self.media_start = mstart
+            elif mstart == self.media_start:
+                mstart = None
+            else:
+                self.media_start = mstart
+        elif property.name == "media-duration":
+            mduration = gnlobject.get_property("media-duration")
+            gst.log("duration: %s => %s" % (gst.TIME_ARGS(self.media_duration),
+                                         gst.TIME_ARGS(mduration)))
+            if mduration == self.media_duration:
+                mduration = None
+            else:
+                self.media_duration = mduration
+        if not mstart == None or not mduration == None:
+            self.emit("media-start-duration-changed",
+                      self.media_start, self.media_duration)
+
 class TimelineBlankSource(TimelineSource):
     """
     Blank source for testing purposes.
@@ -49,23 +159,21 @@
     __requires_factory__ = False
 
     def __init__(self, **kw):
-        TimelineObject.__init__(self, **kw)
+        TimelineSource.__init__(self, **kw)
 
-    def _makeGnlObject(self):
-        if self.media_type == MEDIA_TYPE_AUDIO:
+    def makeGnlSourceContents(self):
+        if self.isAudio():
             # silent audiotestsrc
             src = gst.element_factory_make("audiotestsrc")
             src.set_property("volume", 0)
-        elif self.media_type == MEDIA_TYPE_VIDEO:
+        elif self.isVideo():
             # black videotestsrc
             src = gst.element_factory_make("videotestsrc")
             src.props.pattern = 2
         else:
             gst.error("Can only handle Audio OR Video sources")
-            return
-        gnl = gst.element_factory_make("gnlsource")
-        gnl.add(src)
-        return gnl
+            return None
+        return src
 
     def getExportSettings(self):
         return self.factory.getExportSettings()
@@ -75,62 +183,50 @@
     Seekable sources (mostly files)
 
     Save/Load properties:
-    * 'media-start' (int) : start position of the media
-    * 'media-duration' (int) : duration of the media
     * (optional) 'volume' (int) : volume of the audio
     """
-    __gsignals__ = {
-        "media-start-duration-changed" : ( gobject.SIGNAL_RUN_LAST,
-                                       gobject.TYPE_NONE,
-                                       (gobject.TYPE_UINT64, gobject.TYPE_UINT64))
-        }
-
     __data_type__ = "timeline-file-source"
 
-    def __init__(self, media_start=-1, media_duration=-1, **kw):
-        self.media_start = media_start
-        self.media_duration = media_duration
+    def __init__(self, **kw):
         TimelineSource.__init__(self, **kw)
 
     def _makeGnlObject(self):
-        gst.log("creating object")
-        if self.media_type == MEDIA_TYPE_AUDIO:
+        if self.media_start == gst.CLOCK_TIME_NONE:
+            self.media_start = 0
+        if self.media_duration == 0:
+            self.media_duration = self.factory.getDuration()
+
+        gnlobject = TimelineSource._makeGnlObject(self)
+        if gnlobject == None:
+            return None
+
+        # we override start/duration
+        gnlobject.set_property("duration", long(self.factory.getDuration()))
+        gnlobject.set_property("start", long(0))
+
+        return gnlobject
+
+    def makeGnlSourceContents(self):
+        if self.isAudio():
             caps = gst.caps_from_string("audio/x-raw-int;audio/x-raw-float")
-            postfix = "audio"
-        elif self.media_type == MEDIA_TYPE_VIDEO:
+        elif self.isVideo():
             caps = gst.caps_from_string("video/x-raw-yuv;video/x-raw-rgb")
-            postfix = "video"
         else:
             raise NameError, "media type is NONE !"
-        self.factory.lastbinid = self.factory.lastbinid + 1
-
-        gnlobject = gst.element_factory_make("gnlsource", "source-" + self.name + "-" + postfix + str(self.factory.lastbinid))
         self.decodebin = SingleDecodeBin(caps=caps, uri=self.factory.name)
-        if self.media_type == MEDIA_TYPE_AUDIO:
+        if self.isAudio():
             self.volumeElement = gst.element_factory_make("volume", "internal-volume")
-            self.audioconv = gst.element_factory_make("audioconvert", "fdsjkljf")
+            self.audioconv = gst.element_factory_make("audioconvert", "audioconv")
             self.volumeBin = gst.Bin("volumebin")
             self.volumeBin.add(self.decodebin, self.audioconv, self.volumeElement)
             self.audioconv.link(self.volumeElement)
             self.decodebin.connect('pad-added', self._decodebinPadAddedCb)
             self.decodebin.connect('pad-removed', self._decodebinPadRemovedCb)
-            gnlobject.add(self.volumeBin)
+            bin = self.volumeBin
         else:
-            gnlobject.add(self.decodebin)
-        gnlobject.set_property("caps", caps)
-        gnlobject.set_property("start", long(0))
-        gnlobject.set_property("duration", long(self.factory.length))
+            bin = self.decodebin
 
-        if self.media_start == -1:
-            self.media_start = 0
-        if self.media_duration == -1:
-            self.media_duration = self.factory.length
-        gnlobject.set_property("media-duration", long(self.media_duration))
-        gnlobject.set_property("media-start", long(self.media_start))
-        gnlobject.connect("notify::media-start", self._mediaStartDurationChangedCb)
-        gnlobject.connect("notify::media-duration", self._mediaStartDurationChangedCb)
-
-        return gnlobject
+        return bin
 
     def _decodebinPadAddedCb(self, unused_dbin, pad):
         pad.link(self.audioconv.get_pad("sink"))
@@ -154,9 +250,9 @@
         #FIXME: we need a volume-changed signal, so that UI updates
 
     def setVolume(self, level):
-        if self.media_type == MEDIA_TYPE_AUDIO:
+        if self.isAudio():
             self._setVolume(level)
-        else:
+        elif self.linked:
             self.linked._setVolume(level)
 
     def _makeBrother(self):
@@ -165,75 +261,28 @@
         # find out if the factory provides the other element type
         if self.media_type == MEDIA_TYPE_NONE:
             return None
-        if self.media_type == MEDIA_TYPE_VIDEO:
+        if self.isVideo():
             if not self.factory.is_audio:
                 return None
-            brother = TimelineFileSource(media_start=self.media_start, media_duration=self.media_duration,
-                                         factory=self.factory, start=self.start, duration=self.duration,
+            brother = TimelineFileSource(media_start=self.media_start,
+                                         media_duration=self.media_duration,
+                                         factory=self.factory, start=self.start,
+                                         duration=self.duration,
                                          media_type=MEDIA_TYPE_AUDIO,
                                          name=self.name + "-brother")
-        elif self.media_type == MEDIA_TYPE_AUDIO:
+        elif self.isAudio():
             if not self.factory.is_video:
                 return None
-            brother = TimelineFileSource(media_start=self.media_start, media_duration=self.media_duration,
-                                         factory=self.factory, start=self.start, duration=self.duration,
+            brother = TimelineFileSource(media_start=self.media_start,
+                                         media_duration=self.media_duration,
+                                         factory=self.factory, start=self.start,
+                                         duration=self.duration,
                                          media_type=MEDIA_TYPE_VIDEO,
                                          name=self.name + "-brother")
         else:
             brother = None
         return brother
 
-    def _setMediaStartDurationTime(self, start=-1, duration=-1):
-        gst.info("TimelineFileSource %s start:%s , duration:%s" % (
-            self,
-            gst.TIME_ARGS(start),
-            gst.TIME_ARGS(duration)))
-        gst.info("TimelineFileSource %s EXISTING start:%s , duration:%s" % (
-            self,
-            gst.TIME_ARGS(self.media_start),
-            gst.TIME_ARGS(self.media_duration)))
-        if not duration == -1 and not self.media_duration == duration:
-            self.gnlobject.set_property("media-duration", long(duration))
-        if not start == -1 and not self.media_start == start:
-            self.gnlobject.set_property("media-start", long(start))
-
-    def setMediaStartDurationTime(self, start=-1, duration=-1):
-        """ sets the media start/duration time """
-        if not start == -1 and start < 0:
-            gst.warning("Can't set start values < 0 !")
-            return
-        if not duration == -1 and duration <= 0:
-            gst.warning("Can't set durations <= 0 !")
-            return
-        self._setMediaStartDurationTime(start, duration)
-        if self.linked and isinstance(self.linked, TimelineFileSource):
-            self.linked._setMediaStartDurationTime(start, duration)
-
-    def _mediaStartDurationChangedCb(self, gnlobject, property):
-        gst.log("%r %s %s" % (gnlobject, property, property.name))
-        mstart = None
-        mduration = None
-        if property.name == "media-start":
-            mstart = gnlobject.get_property("media-start")
-            gst.log("%s %s" % (gst.TIME_ARGS(mstart),
-                               gst.TIME_ARGS(self.media_start)))
-            if self.media_start == -1:
-                self.media_start = mstart
-            elif mstart == self.media_start:
-                mstart = None
-            else:
-                self.media_start = mstart
-        elif property.name == "media-duration":
-            mduration = gnlobject.get_property("media-duration")
-            gst.log("%s %s" % (gst.TIME_ARGS(mduration),
-                               gst.TIME_ARGS(self.media_duration)))
-            if mduration == self.media_duration:
-                mduration = None
-            else:
-                self.media_duration = mduration
-        if not mstart == None or not mduration == None:
-            self.emit("media-start-duration-changed",
-                      self.media_start, self.media_duration)
 
     def getExportSettings(self):
         return self.factory.getExportSettings()
@@ -244,7 +293,7 @@
         ret = TimelineSource.toDataFormat(self)
         ret["media-start"] = self.media_start
         ret["media-duration"] = self.media_duration
-        if self.media_type == MEDIA_TYPE_AUDIO and hasattr(self, "volumeElement"):
+        if self.isAudio() and hasattr(self, "volumeElement"):
             ret["volume"] = self.volumeElement.get_property("volume")
         return ret
 
@@ -258,7 +307,6 @@
 
 gobject.type_register(TimelineFileSource)
 
-
 class TimelineLiveSource(TimelineSource):
     """
     Non-seekable sources (like cameras)

Modified: trunk/pitivi/timeline/timeline.py
==============================================================================
--- trunk/pitivi/timeline/timeline.py	(original)
+++ trunk/pitivi/timeline/timeline.py	Fri Aug 29 16:45:42 2008
@@ -113,6 +113,9 @@
             s.audiodepth = a.audiodepth
         return s
 
+    def getDuration(self):
+        return max(self.audiocomp.duration, self.videocomp.duration)
+
     # Serializable methods
 
     def toDataFormat(self):

Modified: trunk/pitivi/ui/Makefile.am
==============================================================================
--- trunk/pitivi/ui/Makefile.am	(original)
+++ trunk/pitivi/ui/Makefile.am	Fri Aug 29 16:45:42 2008
@@ -2,27 +2,28 @@
 
 ui_PYTHON =			\
 	__init__.py		\
+	complexinterface.py	\
 	complexlayer.py		\
 	complexsource.py	\
 	complextimeline.py	\
-	complexinterface.py	\
 	exportsettingswidget.py	\
 	filelisterrordialog.py	\
 	glade.py		\
 	gstwidget.py		\
+	infolayer.py		\
+	layerwidgets.py		\
 	mainwindow.py		\
+	pluginmanagerdialog.py	\
+	plumber.py		\
 	projectsettings.py	\
+	ruler.py		\
+	slider.py		\
 	sourcefactories.py	\
 	timeline.py		\
 	timelineobjects.py	\
-	viewer.py		\
-	ruler.py		\
-	layerwidgets.py		\
 	tracklayer.py		\
-	infolayer.py		\
-	plumber.py		\
-	slider.py	\
-	pluginmanagerdialog.py
+	util.py			\
+	viewer.py
 
 ui_DATA =				\
 	actions.xml			\

Modified: trunk/pitivi/ui/actions.xml
==============================================================================
--- trunk/pitivi/ui/actions.xml	(original)
+++ trunk/pitivi/ui/actions.xml	Fri Aug 29 16:45:42 2008
@@ -38,4 +38,6 @@
     <toolitem action="FullScreen" />
     <toolitem action="AdvancedView" />
   </toolbar>
+  <toolbar name="TimelineToolBar">
+  </toolbar>
 </ui>

Modified: trunk/pitivi/ui/complexinterface.py
==============================================================================
--- trunk/pitivi/ui/complexinterface.py	(original)
+++ trunk/pitivi/ui/complexinterface.py	Fri Aug 29 16:45:42 2008
@@ -27,10 +27,9 @@
 import gst
 
 #
-# Complex Timeline interfaces v1 (01 Feb 2006)
+# Complex Timeline interfaces v2 (01 Jul 2008)
 #
-#
-# ZoomableWidgetInterface
+# Zoomable
 # -----------------------
 # Interface for the Complex Timeline widgets for setting, getting,
 # distributing and modifying the zoom ratio and the size of the widget.
@@ -41,41 +40,49 @@
 # ex : 1.0 = 1 pixel for a second
 #
 # Methods:
+# . setZoomAdjustment(adj)
+# . getZoomAdjustment()
+# . setChildZoomAdjustment()
+# . zoomChanged()
 # . setZoomRatio(ratio)
 # . getZoomRatio(ratio)
 # . pixelToNs(pixels)
 # . nsToPixels(time)
-# . getPixelWidth()
-#
-#
 
-class ZoomableWidgetInterface:
+class Zoomable:
+    
+    zoomratio = 0
+    zoom_adj = None
+
+    def setZoomAdjustment(self, adjustment):
+        if self.zoom_adj:
+            self.zoom_adj.disconnect(self._zoom_changed_sigid)
+        self.zoom_adj = adjustment
+        if adjustment:
+            self._zoom_changed_sigid = adjustment.connect("value-changed",
+                self._zoom_changed_cb)
+            self.zoomratio = adjustment.get_value()
+            self.setChildZoomAdjustment(adjustment)
+            self.zoomChanged()
 
-    def getPixelWidth(self):
-        """
-        Returns the width in pixels corresponding to the duration of the object
-        """
-        dur = self.getDuration()
-        width = self.nsToPixel(dur)
-        gst.log("Got time %s, returning width : %d" % (gst.TIME_ARGS(dur), width))
-        return width
+    def getZoomAdjustment(self):
+        return self.zoom_adj
 
-    def getPixelPosition(self):
-        """
-        Returns the pixel offset of the widget in it's container, according to:
-        _ the start position of the object in it's timeline container,
-        _ and the set zoom ratio
-        """
-        start = self.getStartTime()
-        pos = self.nsToPixel(start)
-        gst.log("Got start time %s, returning offset %d" % (gst.TIME_ARGS(start), pos))
-        return pos
+    def _zoom_changed_cb(self, adjustment):
+        self.zoomratio = adjustment.get_value()
+        self.zoomChanged()
+
+    def getZoomRatio(self):
+        return self.zoomratio
+
+    def setZoomRatio(self, ratio):
+        self.zoom_adj.set_value(ratio)
 
     def pixelToNs(self, pixel):
         """
         Returns the pixel equivalent in nanoseconds according to the zoomratio
         """
-        return long(pixel * gst.SECOND / self.getZoomRatio())
+        return long(pixel * gst.SECOND / self.zoomratio)
 
     def nsToPixel(self, duration):
         """
@@ -84,53 +91,59 @@
         """
         if duration == gst.CLOCK_TIME_NONE:
             return 0
-        return int((float(duration) / gst.SECOND) * self.getZoomRatio())
+        return int((float(duration) / gst.SECOND) * self.zoomratio)
 
-    ## Methods to implement in subclasses
-
-    def getDuration(self):
-        """
-        Return the duration in nanoseconds of the object
-        To be implemented by subclasses
-        """
-        raise NotImplementedError
-
-    def getStartTime(self):
-        """
-        Return the start time in nanosecond of the object
-        To be implemented by subclasses
-        """
-        raise NotImplementedError
+    # Override in subclasses
 
     def zoomChanged(self):
-        raise NotImplementedError
+        pass
 
-    def durationChanged(self):
-        self.queue_resize()
+    def setChildZoomAdjustment(self, adj):
+        pass
 
-    def startChanged(self):
-        self.queue_resize()
+# ZoomableObject(Zoomable)
+# -----------------------
+# Interface for UI widgets which wrap PiTiVi timeline objects.
+#
+# Methods:
+# . setObject
+# . getObject
+# . startDurationChanged
+# . getPixelPosition
+# . getPixelWidth
+
+class ZoomableObject(Zoomable):
+
+    object = None
+    width = None
+    position = None
+
+    def setTimelineObject(self, object):
+        if self.object:
+            self.object.disconnect(self._start_duration_changed_sigid)
+        self.object = object
+        if object:
+            self.start_duration_changed_sigid = object.connect(
+                "start-duration-changed", self._start_duration_changed_cb)
+
+    def getTimelineObject(self):
+        return self.object
+
+    def _start_duration_changed(self, object, start, duration):
+        self.width = self.nsToPixel(duration)
+        self.position = self.nsToPixel(start)
+        self.startDurationChanged()
 
     def startDurationChanged(self):
-        gst.info("start/duration changed")
-        self.queue_resize()
+        """Overriden by subclasses"""
+        pass
 
-    def getZoomRatio(self):
-        # either the current object is the top-level object that contains the zoomratio
-        if hasattr(self, 'zoomratio'):
-            return self.zoomratio
-        # chain up to the parent
-        parent = self.parent
-        while not hasattr(parent, 'getZoomRatio'):
-            parent = parent.parent
-        return parent.getZoomRatio()
-
-    def setZoomRatio(self, zoomratio):
-        if hasattr(self, 'zoomratio'):
-            if self.zoomratio == zoomratio:
-                return
-            gst.debug("Changing zoomratio to %f" % zoomratio)
-            self.zoomratio = zoomratio
-            self.zoomChanged()
-        else:
-            self.parent.setZoomRatio(zoomratio)
+    def zoomChanged(self):
+        self._start_duration_changed(self.object, self.object.start,
+            self.object.duration)
+
+    def getPixelPosition(self):
+        return self.position
+
+    def getPixelWidth(self):
+        return self.width

Modified: trunk/pitivi/ui/complexlayer.py
==============================================================================
--- trunk/pitivi/ui/complexlayer.py	(original)
+++ trunk/pitivi/ui/complexlayer.py	Fri Aug 29 16:45:42 2008
@@ -57,35 +57,53 @@
 class LayerInfo:
     """ Information on a layer for the complex timeline widgets """
 
-    def __init__(self, composition, expanded=True):
+    def __init__(self, composition, sigid, expanded=True):
         """
         If currentHeight is None, it will be set to the given minimumHeight.
         """
         self.composition = composition
         self.expanded = expanded
+        self.sigid = sigid
 
 class LayerInfoList(gobject.GObject):
     """ List, on steroids, of the LayerInfo"""
 
     __gsignals__ = {
-        'layer-added' : ( gobject.SIGNAL_RUN_LAST,
-                          gobject.TYPE_NONE,
-                          ( gobject.TYPE_INT, ) ),
-        'layer-removed' : ( gobject.SIGNAL_RUN_LAST,
-                          gobject.TYPE_NONE,
-                          ( gobject.TYPE_INT, ) ),
-        }
+        'layer-added' : (
+            gobject.SIGNAL_RUN_LAST,
+            gobject.TYPE_NONE,
+            (gobject.TYPE_PYOBJECT, gobject.TYPE_INT, )
+        ),
+        'layer-removed' : (
+            gobject.SIGNAL_RUN_LAST,
+            gobject.TYPE_NONE,
+            (gobject.TYPE_INT, )
+        ),
+        'start-duration-changed' : (
+            gobject.SIGNAL_RUN_LAST,
+            gobject.TYPE_NONE,
+            ()
+        )
+    }
 
-    def __init__(self, timeline):
+    def __init__(self):
         gobject.GObject.__init__(self)
-        self.timeline = timeline
+        self.timeline = None
         self._list = []
-        self._fillList()
+
+    def setTimeline(self, timeline):
+        self._clear()
+        self.timeline = timeline
+        if self.timeline:
+            self._fillList()
 
     def _fillList(self):
         gst.debug("filling up LayerInfoList")
-        self.addComposition(self.timeline.videocomp)
         self.addComposition(self.timeline.audiocomp)
+        self.addComposition(self.timeline.videocomp)
+
+    def _start_duration_changed_cb(self, timeline, start, duration):
+        self.emit("start-duration-changed")
 
     def addComposition(self, composition, pos=-1):
         """
@@ -100,12 +118,14 @@
             expanded = False
         else:
             expanded = True
-        layer = LayerInfo(composition, expanded)
+        sigid = composition.connect("start-duration-changed",
+            self._start_duration_changed_cb)
+        layer = LayerInfo(composition, sigid, expanded)
         if pos == -1:
             self._list.append(layer)
         else:
             self._list.insert(pos, layer)
-        self.emit('layer-added', pos)
+        self.emit('layer-added', layer, pos)
         return layer
 
     def removeComposition(self, composition):
@@ -119,8 +139,14 @@
             return False
         position = self._list.index(layer)
         self._list.remove(layer)
+        layer.composition.disconnect(layer.sigid)
         self.emit('layer-removed', position)
 
+    def _clear(self):
+        while len(self._list):
+            layer = self._list[0]
+            self.removeComposition(layer.composition)
+
     def findCompositionLayerInfo(self, composition):
         """ Returns the LayerInfo corresponding to the given composition """
         for layer in self._list:

Modified: trunk/pitivi/ui/complexsource.py
==============================================================================
--- trunk/pitivi/ui/complexsource.py	(original)
+++ trunk/pitivi/ui/complexsource.py	Fri Aug 29 16:45:42 2008
@@ -49,8 +49,8 @@
         self.layerInfo = layerInfo
         self.source = source
         self.source.connect("start-duration-changed", self._startDurationChangedCb)
-        if self.source.factory.thumbnail:
-            self.thumbnailsurface = cairo.ImageSurface.create_from_png(self.source.factory.thumbnail)
+        if self.source.factory.getThumbnail():
+            self.thumbnailsurface = cairo.ImageSurface.create_from_png(self.source.factory.getThumbnail())
         else:
             self.thumbnailsurface = cairo.ImageSurface.create_from_png(os.path.join(get_pixmap_dir(), "pitivi-video.png"))
         self.pixmap = None

Modified: trunk/pitivi/ui/complextimeline.py
==============================================================================
--- trunk/pitivi/ui/complextimeline.py	(original)
+++ trunk/pitivi/ui/complextimeline.py	Fri Aug 29 16:45:42 2008
@@ -25,70 +25,616 @@
 
 import gtk
 import gst
-
+import cairo
 import pitivi.instance as instance
 
 from pitivi.bin import SmartTimelineBin
+from pitivi.timeline.source import TimelineFileSource
 from complexlayer import LayerInfoList
-from layerwidgets import TopLayer, CompositionLayer
-from complexinterface import ZoomableWidgetInterface
+import ruler
+from complexinterface import Zoomable
+import goocanvas
+from util import *
+import os.path
+from urllib import unquote
+from pitivi.timeline.objects import MEDIA_TYPE_AUDIO, MEDIA_TYPE_VIDEO
+from pitivi.utils import closest_item, argmax
+from gettext import gettext as _
+
+
+# default heights for composition layer objects
+VIDEO_TRACK_HEIGHT = 50
+AUDIO_TRACK_HEIGHT = 20
+
+# visual styles for sources in the UI
+VIDEO_SOURCE = (
+    goocanvas.Rect,
+    {
+        "height" : VIDEO_TRACK_HEIGHT, 
+        "fill_color_rgba" : 0x709fb899,
+        "line_width" : 0
+    },
+    {
+        "normal_color" : 0x709fb899,
+        "selected_color" : 0xa6cee3AA, 
+    }
+)
+AUDIO_SOURCE = (
+    goocanvas.Rect,
+    {
+        "height" : AUDIO_TRACK_HEIGHT, 
+        "fill_color_rgba" : 0x709fb899,
+        "line_width" : 0
+    },
+    {
+        "normal_color" : 0x709fb899,
+        "selected_color" : 0xa6cee3AA, 
+    }
+)
+
+# defines visual appearance for source resize handle
+DRAG_HANDLE = (
+    goocanvas.Rect,
+    {
+        "width" : 5,
+        "fill_color_rgba" : 0x00000022,
+        "line-width" : 0
+    },
+    {}
+)
+
+BACKGROUND = (
+    goocanvas.Rect,
+    {
+        "stroke_color" : "gray",
+        "fill_color" : "gray",
+    },
+    {}
+)
+
+RAZOR_LINE = (
+    goocanvas.Rect,
+    {
+        "line_width" : 0,
+        "fill_color" : "orange",
+        "width" : 1,
+    },
+    {}
+)
+
+SPACER = (
+    goocanvas.Polyline,
+    {
+        "stroke_color_rgba" : 0xFFFFFFFF,
+        "line_width" : 1,
+    },
+    {}
+)
+
+LABEL = (
+    goocanvas.Text,
+    {
+        "font" : "Sans 9",
+        "text" : "will be replaced",
+        "fill_color_rgba" : 0x000000FF,
+        "alignment" : pango.ALIGN_LEFT
+    },
+    {}
+)
+
+# the vsiual appearance for the selection marquee
+MARQUEE = (
+    goocanvas.Rect,
+    {
+        "stroke_color_rgba" : 0x33CCFF66,
+        "fill_color_rgba" : 0x33CCFF66,
+    },
+    {}
+)
+
+# cursors to be used for resizing objects
+LEFT_SIDE = gtk.gdk.Cursor(gtk.gdk.LEFT_SIDE)
+RIGHT_SIDE = gtk.gdk.Cursor(gtk.gdk.RIGHT_SIDE)
+ARROW = gtk.gdk.Cursor(gtk.gdk.ARROW)
+# TODO: replace this with custom cursor
+RAZOR_CURSOR = gtk.gdk.Cursor(gtk.gdk.XTERM)
+
+# default number of pixels to use for edge snaping
+DEADBAND = 5
+
+# tooltip text for toolbar
+DELETE = _("Delete Selected")
+RAZOR = _("Cut clip at mouse position")
+ZOOM_IN =  _("Zoom In")
+ZOOM_OUT =  _("Zoom Out")
+SELECT_BEFORE = ("Select all sources before selected")
+SELECT_AFTER = ("Select all after selected")
+
+# ui string for the complex timeline toolbar
+ui = '''
+<ui>
+    <toolbar name="TimelineToolBar">
+        <toolitem action="ZoomOut" />
+        <toolitem action="ZoomIn" />
+        <separator />
+        <toolitem action="Razor" />
+        <separator />
+        <toolitem action="DeleteObj" />
+        <toolitem action="SelectBefore" />
+        <toolitem action="SelectAfter" />
+    </toolbar>
+</ui>
+'''
+
+class ComplexTrack(SmartGroup, Zoomable):
+    __gtype_name__ = 'ComplexTrack'
+
+    def __init__(self, *args, **kwargs):
+        SmartGroup.__init__(self, *args, **kwargs)
+        self.widgets = {}
+        self.elements = {}
+        self.sig_ids = None
+        self.comp = None
+        self.object_style = None
+
+    def set_composition(self, comp):
+        if self.sig_ids:
+            for sig in self.sig_ids:
+                comp.disconnect(sig)
+        self.comp = comp
+        self.object_style = VIDEO_SOURCE
+        if comp:
+            added = comp.connect("source-added", self._objectAdded)
+            removed = comp.connect("source-removed", self._objectRemoved)
+            self.sig_ids = (added, removed)
+            if comp.media_type == MEDIA_TYPE_VIDEO:
+                self.object_style = VIDEO_SOURCE
+                self.height = VIDEO_TRACK_HEIGHT
+            else:
+                self.object_style = AUDIO_SOURCE
+                self.height = AUDIO_TRACK_HEIGHT
+
+    def _objectAdded(self, timeline, element):
+        w = ComplexTimelineObject(element, self.comp, self.object_style)
+        make_dragable(self.canvas, w, start=self._start_drag,
+            end=self._end_drag, moved=self._move_source_cb)
+        element.connect("start-duration-changed", self.start_duration_cb, w)
+        self.widgets[element] = w
+        self.elements[w] = element
+        element.set_data("widget", w)
+        self.start_duration_cb(element, element.start, element.duration, w)
+        self.add_child(w)
+        make_selectable(self.canvas, w.bg)
+        make_dragable(self.canvas, w.l_handle, 
+            start=self._start_drag, moved=self._trim_source_start_cb,
+            cursor=LEFT_SIDE)
+        make_dragable(self.canvas, w.r_handle, start=self._start_drag,
+            moved=self._trim_source_end_cb,
+            cursor=RIGHT_SIDE)
+
+    def _objectRemoved(self, timeline, element):
+        w = self.widgets[element]
+        self.remove_child(w)
+        w.comp = None
+        del self.widgets[element]
+        del self.elements[w]
+
+    def start_duration_cb(self, obj, start, duration, widget):
+        widget.props.width =  self.nsToPixel(duration)
+        self.set_child_pos(widget, (self.nsToPixel(start), 0))
+
+    def _start_drag(self, item):
+        item.raise_(None)
+        self._draging = True
+        #self.canvas.block_size_request(True)
+        self.canvas.update_editpoints()
+
+    def _end_drag(self, item):
+        self.canvas.block_size_request(False)
+
+    def _move_source_cb(self, item, pos):
+        element = item.element
+        element.setStartDurationTime(max(self.canvas.snap_obj_to_edit(element,
+            self.pixelToNs(pos[0])), 0))
+
+    def _trim_source_start_cb(self, item, pos):
+        element = item.element
+        cur_end = element.start + element.duration
+        # Invariant:
+        #  max(duration) = element.factory.getDuration()
+        #  start = end - duration
+        # Therefore
+        #  min(start) = end - element.factory.getDuration()
+        new_start =  max(0,
+            cur_end - element.factory.getDuration(), 
+            self.canvas.snap_time_to_edit(self.pixelToNs(pos[0])))
+        new_duration = cur_end - new_start
+        new_media_start = element.media_start + (new_start - element.media_start)
+        element.setStartDurationTime(new_start, new_duration)
+        #FIXME: only for sources
+        element.setMediaStartDurationTime(new_media_start, new_duration)
+
+    def _trim_source_end_cb(self, item, pos):
+        element = item.element
+        cur_start = element.start
+        new_end = min(cur_start + element.factory.getDuration(),
+            max(cur_start, 
+                self.canvas.snap_time_to_edit(
+                    self.pixelToNs(pos[0] + width(item)))))
+        new_duration = new_end - element.start
+
+        element.setStartDurationTime(gst.CLOCK_TIME_NONE, new_duration)
+        #FIXME: only for sources
+        element.setMediaStartDurationTime(gst.CLOCK_TIME_NONE, new_duration)
+
+    def zoomChanged(self):
+        """Force resize if zoom ratio changes"""
+        for child in self.elements:
+            element = self.elements[child]
+            start = element.start
+            duration = element.duration
+            self.start_duration_cb(self, start, duration, child)
+
+class ComplexTimelineObject(goocanvas.Group):
+
+    __gtype_name__ = 'ComplexTimelineObject'
+
+    x = gobject.property(type=float)
+    y = gobject.property(type=float)
+    height = gobject.property(type=float)
+    width = gobject.property(type=float)
+
+    def __init__(self, element, composition, style):
+        goocanvas.Group.__init__(self)
+        self.element = element
+        self.comp = composition
+        self.bg = make_item(style)
+        self.bg.element = element
+        self.bg.comp = composition
+        self.name = make_item(LABEL)
+        self.name.props.text = os.path.basename(unquote(
+            element.factory.name))
+        self.l_handle = self._make_handle(LEFT_SIDE)
+        self.r_handle = self._make_handle(RIGHT_SIDE)
+        self.spacer = make_item(SPACER)
+        self.children = [self.bg, self.name, self.l_handle, self.r_handle,
+            self.spacer]
+        for thing in self.children:
+            self.add_child(thing)
+        self.connect("notify::x", self.do_set_x)
+        self.connect("notify::y", self.do_set_y)
+        self.connect("notify::width", self.do_set_width)
+        self.connect("notify::height", self.do_set_height)
+        self.width = self.bg.props.width
+        self.height = self.bg.props.height
+
+    def _set_cursor(self, item, target, event, cursor):
+        window = event.window
+        # wtf ? no get_cursor?
+        #self._oldcursor = window.get_cursor()
+        window.set_cursor(cursor)
+        return True
+
+    def _make_handle(self, cursor):
+        ret = make_item(DRAG_HANDLE)
+        ret.element = self.element
+        ret.connect("enter-notify-event", self._set_cursor, cursor)
+        #ret.connect("leave-notify-event", self._set_cursor, ARROW)
+        return ret
+
+    def _size_spacer(self):
+        x = self.x + self.width
+        y = self.y + self.height
+        self.spacer.points = goocanvas.Points([(x, 0), (x, y)])
+        # clip text to object width
+        w = self.width - width(self.r_handle)
+        self.name.props.clip_path = "M%g,%g h%g v%g h-%g z" % (
+            self.x, self.y, w, self.height, w)
+
+    def do_set_x(self, *args):
+        x = self.x
+        self.bg.props.x = x
+        self.name.props.x = x + width(self.l_handle) + 2
+        self.l_handle.props.x = x
+        self.r_handle.props.x = x + self.width - width(self.r_handle)
+        self._size_spacer()
+
+    def do_set_y(self, *args):
+        y = self.y
+        self.bg.props.y = y
+        self.name.props.y = y + 2
+        self.l_handle.props.y = y
+        self.r_handle.props.y = y
+        self._size_spacer()
+
+    def do_set_width(self, *args):
+        self.bg.props.width = self.width
+        self.r_handle.props.x = self.x + self.width - width(self.r_handle)
+        self.name.props.width = self.width - (2 * width(self.l_handle) + 4)
+        self._size_spacer()
+
+    def do_set_height(self, *args):
+        height = self.height
+        self.bg.props.height = height
+        self.l_handle.props.height = height
+        self.r_handle.props.height = height
+        self._size_spacer()
 
-class CompositionLayers(gtk.VBox, ZoomableWidgetInterface):
+class CompositionLayers(goocanvas.Canvas, Zoomable):
     """ Souped-up VBox that contains the timeline's CompositionLayer """
 
-    def __init__(self, leftsizegroup, hadj, layerinfolist):
-        gtk.VBox.__init__(self)
-        self.modify_bg(gtk.STATE_NORMAL, gtk.gdk.Color(1,0,0))
-        self.leftSizeGroup = leftsizegroup
-        self.hadj = hadj
+    def __init__(self, layerinfolist):
+        goocanvas.Canvas.__init__(self)
+        self._selected_sources = []
+        self._editpoints = []
+        self._deadband = 0
+        self._timeline_position = 0
+
+        self._block_size_request = False
+        self.props.integer_layout = True
+        self.props.automatic_bounds = False
+
         self.layerInfoList = layerinfolist
         self.layerInfoList.connect('layer-added', self._layerAddedCb)
         self.layerInfoList.connect('layer-removed', self._layerRemovedCb)
-        self._createUI()
 
+        self._createUI()
+        self.connect("size_allocate", self._size_allocate)
+       
     def _createUI(self):
-        self.set_spacing(5)
-        self.set_border_width(2)
-        self.layers = []
-        for layerinfo in self.layerInfoList:
-            complayer = CompositionLayer(self.leftSizeGroup, self.hadj,
-                                         layerinfo)
-            self.layers.append(complayer)
-            self.pack_start(complayer, expand=False)
-
-
-    ## ZoomableWidgetInterface overrides
-
-    def getDuration(self):
-        return max([layer.getDuration() for layer in self.layers])
-
-    def getStartTime(self):
-        # the start time is always 0 (for display reason)
-        return 0
+        self._cursor = ARROW
 
-    def zoomChanged(self):
-        for layer in self.layers:
-            layer.zoomChanged()
+        self.layers = VList(canvas=self)
+        self.layers.connect("notify::width", self._request_size)
+        self.layers.connect("notify::height", self._request_size)
+
+        root = self.get_root_item()
+        root.add_child(self.layers)
+
+        root.connect("enter_notify_event", self._mouseEnterCb)
+        self._marquee = make_item(MARQUEE)
+        manage_selection(self, self._marquee, True, self._selection_changed_cb)
+
+        self._razor = make_item(RAZOR_LINE)
+        self._razor.props.visibility = goocanvas.ITEM_INVISIBLE
+        root.add_child(self._razor)
+
+## methods for dealing with updating the canvas size
+
+    def block_size_request(self, status):
+        self._block_size_request = status
+
+    def _size_allocate(self, unused_layout, allocation):
+        self._razor.props.height = allocation.height
+
+    def _request_size(self, unused_item, prop):
+        #TODO: figure out why this doesn't work... (wtf?!?)
+        if self._block_size_request:
+            return True
+        # we only update the bounds of the canvas by chunks of 100 pixels
+        # in width, otherwise we would always be redrawing the whole canvas.
+        # Make sure canvas is at least 800 pixels wide, and at least 100 pixels 
+        # wider than it actually needs to be.
+        w = max(800, ((int(self.layers.width + 100) / 100) + 1 ) * 100)
+        h = int(self.layers.height)
+        x1,y1,x2,y2 = self.get_bounds()
+        pw = abs(x2 - x1)
+        ph = abs(y2 - y1)
+        if not (w == pw and h == ph):
+            self.set_bounds(0, 0, w, h)
+        return True
+
+## code for keeping track of edit points, and snapping timestamps to the
+## nearest edit point. We do this here so we can keep track of edit points
+## for all layers/tracks.
+
+    def update_editpoints(self):
+        #FIXME: this might be more efficient if we used a binary sort tree,
+        # updated only when the timeline actually changes instead of before
+        # every drag operation. possibly concerned this could lead to a
+        # noticible lag on large timelines
+
+        # using a dictionary to silently filter out duplicate entries
+        # this list: it will screw up the edge-snaping algorithm
+        edges = {}
+        for layer in self.layerInfoList:
+            for obj in layer.composition.condensed:
+                # start/end of object both considered "edit points"
+                edges[obj.start] = None
+                edges[obj.start + obj.duration] = None
+        self._editpoints = edges.keys()
+        self._editpoints.sort()
+
+    def snap_time_to_edit(self, time):
+        res, diff = closest_item(self._editpoints, time)
+        if diff <= self._deadband:
+            return res
+        return time
+
+    def snap_time_to_playhead(self, time):
+        if abs(time - self._timeline_position)  <= self._deadband:
+            return self._timeline_position
+        return time
+
+    def snap_obj_to_edit(self, obj, time):
+        # need to find the closest edge to both the left and right sides of
+        # the object we are draging.
+        duration = obj.duration
+        left_res, left_diff = closest_item(self._editpoints, time)
+        right_res, right_diff = closest_item(self._editpoints, time + duration)
+        if left_diff <= right_diff:
+            res = left_res
+            diff = left_diff
+        else:
+            res = right_res - duration
+            diff = right_diff
+        if diff <= self._deadband:
+            return res
+        return time
+
+## mouse callbacks
+
+    def _mouseEnterCb(self, item, target, event):
+        event.window.set_cursor(self._cursor)
+        return True
+
+## Editing Operations
+
+    def deleteSelected(self, unused_action):
+        for obj in self._selected_sources:
+            if obj.comp:
+                obj.comp.removeSource(obj.element, remove_linked=True, 
+                    collapse_neighbours=False)
+        set_selection(self, set())
+        return True
+
+    def activateRazor(self, unused_action):
+        self._cursor = RAZOR_CURSOR
+        # we don't want mouse events passing through to the canvas items
+        # underneath, so we connect to the canvas's signals
+        self._razor_sigid = self.connect("button_press_event", 
+            self._razorClickedCb)
+        self._razor_motion_sigid = self.connect("motion_notify_event",
+            self._razorMovedCb)
+        self._razor.props.visibility = goocanvas.ITEM_VISIBLE
+        return True
+
+    def _razorMovedCb(self, canvas, event):
+        x, y = event_coords(self, event)
+        self._razor.props.x = self.nsToPixel(self.snap_time_to_playhead(
+            self.pixelToNs(x)))
+        return True
+
+    def _razorClickedCb(self, canvas, event):
+        self._cursor = ARROW
+        event.window.set_cursor(ARROW)
+        self.disconnect(self._razor_sigid)
+        self.disconnect(self._razor_motion_sigid)
+        self._razor.props.visibility = goocanvas.ITEM_INVISIBLE
+
+        # Find the topmost source under the mouse. This is tricky because not
+        # all objects in the timeline are ComplexTimelineObjects. Some of them
+        # are drag handles, for example. For now, only objects marked as
+        # selectable should be sources
+        x, y = event_coords(self, event)
+        items = self.get_items_at(x, y, True)
+        if not items:
+            return True
+        for item in items:
+            if item.get_data("selectable"):
+                parent = item.get_parent()
+                gst.log("attempting to split source at position %d" %  x)
+                self._splitSource(parent, self.snap_time_to_playhead(
+                    self.pixelToNs(x)))
+        return True
+
+    def _splitSource(self, obj, editpoint):
+        comp = obj.comp
+        element = obj.element
+
+        # we want to divide element in elementA, elementB at the
+        # edit point.
+        a_start = element.start
+        a_end = editpoint
+        b_start = editpoint
+        b_end = element.start + element.duration
+
+        # so far so good, but we need this expressed in the form
+        # start/duration.
+        a_dur = a_end - a_start
+        b_dur = b_end - b_start
+        if not (a_dur and b_dur):
+            gst.Log("cannot cut at existing edit point, aborting")
+            return
+
+        # and finally, we need the media-start/duration for both sources.
+        # in this case, media-start = media-duration, but this would not be
+        # true if timestretch were applied to either source. this is why I
+        # really think we should not have to care about media-start /duratoin
+        # here, and have a more abstract method for setting time stretch that
+        # would keep media start/duration in sync for sources that have it.
+        a_media_start = element.media_start
+        b_media_start = a_media_start + a_dur
+
+        # trim source a
+        element.setMediaStartDurationTime(a_media_start, a_dur)
+        element.setStartDurationTime(a_start, a_dur)
+
+        # add source b
+        # TODO: for linked sources, split linked and create brother
+        # TODO: handle other kinds of sources
+        new = TimelineFileSource(factory=element.factory,
+            media_type=comp.media_type)
+        new.setMediaStartDurationTime(b_media_start, b_dur)
+        new.setStartDurationTime(b_start, b_dur)
+        comp.addSource(new, 0, True)
+
+    def selectBeforeCurrent(self, unused_action):
+        pass
+
+    def selectAfterCurrent(self, unused_action):
+        ## helper function
+        #def source_pos(ui_obj):
+        #    return ui_obj.comp.getSimpleSourcePosition(ui_obj.element)
+
+        ## mapping from composition -> (source1, ... sourceN)
+        #comps = dict()
+        #for source in self._selected_sources:
+        #    if not source.comp in comps:
+        #        comps[source.comp] = []
+        #    comps[source.comp].append(source)
+
+        ## find the latest source in each compostion, and all sources which
+        ## occur after it. then select them.
+        #to_select = set()
+        #for comp, sources in comps.items():
+        #    # source positions start at 1, not 0.
+        #    latest = max((source_pos(source) for source in sources)) - 1
+        #    # widget is available in "widget" data member of object.
+        #    # we add the background of the widget, not the widget itself.
+        #    objs = [obj.get_data("widget").bg for obj in comp.condensed[latest:]]
+        #    to_select.update(set(objs))
+        #set_selection(self, to_select)
+        pass
+
+    def _selection_changed_cb(self, selected, deselected):
+        # TODO: filter this list for things other than sources, and put them
+        # into appropriate lists
+        for item in selected:
+            item.props.fill_color_rgba = item.get_data("selected_color")
+            parent = item.get_parent()
+            self._selected_sources.append(parent)
+        for item in deselected:
+            item.props.fill_color_rgba = item.get_data("normal_color")
+            parent = item.get_parent()
+            self._selected_sources.remove(parent)
 
     def timelinePositionChanged(self, value, frame):
+        self._timeline_position = value
+
+## Zoomable Override
+
+    def zoomChanged(self):
+        self._deadband = self.pixelToNs(DEADBAND)
+
+    def setChildZoomAdjustment(self, adj):
         for layer in self.layers:
-            layer.timelinePositionChanged(value, frame)
+            layer.setZoomAdjustment(adj)
 
-    ## LayerInfoList callbacks
+## LayerInfoList callbacks
 
-    def _layerAddedCb(self, layerInfoList, position):
-        complayer = CompositionLayer(self.leftSizeGroup, self.hadj,
-                                     layerInfoList[position])
-        self.layers.insert(position, complayer)
-        self.pack_start(complayer, expand=False)
-        self.reorder_child(complayer, position)
+    def _layerAddedCb(self, unused_infolist, layer, position):
+        track = ComplexTrack()
+        track.setZoomAdjustment(self.getZoomAdjustment())
+        track.set_composition(layer.composition)
+        track.set_canvas(self)
+        self.layers.insert_child(track, position)
+        self.set_bounds(0, 0, self.layers.width, self.layers.height)
+        self.set_size_request(int(self.layers.width), int(self.layers.height))
 
     def _layerRemovedCb(self, unused_layerInfoList, position):
-        # find the proper child
-        child = self.layers[position]
-        # remove it
-        self.remove(child)
-
+        child = self.layers.item_at(position)
+        self.layers.remove_child(child)
 #
 # Complex Timeline Design v2 (08 Feb 2006)
 #
@@ -99,93 +645,134 @@
 # ComplexTimelineWidget(gtk.VBox)
 # |  Top container
 # |
-# +--TopLayer (TimelineLayer (gtk.HBox))
-# |   |
-# |   +--TopLeftTimelineWidget(?)
-# |   |
-# |   +--ScaleRuler(gtk.Layout)
+# +--ScaleRuler(gtk.Layout)
 # |
 # +--gtk.ScrolledWindow
 #    |
-#    +--CompositionsLayer(gtk.VBox)
-#       |
-#       +--CompositionLayer(TimelineLayer(gtk.HBox))
-#          |
-#          +--InfoLayer(gtk.Expander)
-#          |
-#          +--TrackLayer(gtk.Layout)
+#    +--CompositionLayers(goocanas.Canvas)
+#    |  |
+#    |  +--ComplexTrack(SmartGroup)
+#    |
+#    +--Status Bar ??
+#
+
+class ComplexTimelineWidget(gtk.VBox):
 
-class ComplexTimelineWidget(gtk.VBox, ZoomableWidgetInterface):
+    # the screen width of the current unit
+    unit_width = 10 
+    # specific levels of zoom, in (multiplier, unit) pairs which 
+    # from zoomed out to zoomed in
+    zoom_levels = (1, 5, 10, 20, 50, 100, 150) 
 
-    def __init__(self, topwidget):
+    def __init__(self):
         gst.log("Creating ComplexTimelineWidget")
         gtk.VBox.__init__(self)
 
-        self.zoomratio = 10.0
-
-        self.hadj = topwidget.hadjustment
-        self.vadj = topwidget.vadjustment
-
+        self._zoom_adj = gtk.Adjustment()
+        self._zoom_adj.lower = self._computeZoomRatio(0)
+        self._zoom_adj.upper = self._computeZoomRatio(-1)
+        self._cur_zoom = 2
+        self._zoom_adj.set_value(self._computeZoomRatio(self._cur_zoom))
+ 
         # common LayerInfoList
-        self.layerInfoList = LayerInfoList(instance.PiTiVi.current.timeline)
-        instance.PiTiVi.playground.connect('position', self._playgroundPositionCb)
-        for layer in self.layerInfoList:
-            layer.composition.connect('start-duration-changed',
-                                      self._layerStartDurationChangedCb)
+        self.layerInfoList = LayerInfoList()
 
+        instance.PiTiVi.playground.connect('position',
+           self._playgroundPositionCb)
+        # project signals
+        instance.PiTiVi.connect("new-project-loading",
+            self._newProjectLoadingCb)
+        instance.PiTiVi.connect("new-project-failed",
+            self._newProjectFailedCb)
         self._createUI()
 
+        # force update of UI
+        self.layerInfoList.setTimeline(instance.PiTiVi.current.timeline)
+        self.layerInfoList.connect("start-duration-changed",
+            self._layerStartDurationChanged)
+
     def _createUI(self):
-        self.set_border_width(4)
         self.leftSizeGroup = gtk.SizeGroup(gtk.SIZE_GROUP_HORIZONTAL)
-
-        # top layer (TopLayer)
-        self.topLayer = TopLayer(self.leftSizeGroup, self.hadj)
-        # overriding topLayer's ZoomableWidgetInterface methods
-        self.topLayer.getDuration = self.getDuration
-        self.topLayer.getStartTime = self.getStartTime
-        self.topLayer.overrideZoomableWidgetInterfaceMethods()
-        self.pack_start(self.topLayer, expand=False)
+        self.hadj = gtk.Adjustment()
+        self.ruler = ruler.ScaleRuler(self.hadj)
+        self.ruler.setZoomAdjustment(self._zoom_adj)
+        self.ruler.set_size_request(0, 35)
+        self.ruler.set_border_width(2)
+        self.pack_start(self.ruler, expand=False, fill=True)
 
         # List of CompositionLayers
-        self.compositionLayers = CompositionLayers(self.leftSizeGroup,
-                                                   self.hadj, self.layerInfoList)
-
-        # ... in a scrolled window
-        self.scrolledWindow = gtk.ScrolledWindow()
-        self.scrolledWindow.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
-        self.scrolledWindow.add_with_viewport(self.compositionLayers)
+        self.compositionLayers = CompositionLayers(self.layerInfoList)
+        self.compositionLayers.setZoomAdjustment(self._zoom_adj)
+        self.scrolledWindow = gtk.ScrolledWindow(self.hadj)
+        self.scrolledWindow.set_policy(gtk.POLICY_ALWAYS, gtk.POLICY_AUTOMATIC)
+        self.scrolledWindow.add(self.compositionLayers)
+        #FIXME: remove padding between scrollbar and scrolled window
         self.pack_start(self.scrolledWindow, expand=True)
 
-    def _layerStartDurationChangedCb(self, unused_composition, unused_start,
-                                     unused_duration):
-        # Force resize of ruler
-        self.topLayer.startDurationChanged()
-
-    ## ZoomableWidgetInterface overrides
-    ## * we send everything to self.compositionLayers
-    ## * topLayer's function calls will also go there
-
-    def getDuration(self):
-        return self.compositionLayers.getDuration()
-
-    def getStartTime(self):
-        return self.compositionLayers.getStartTime()
-
-    def zoomChanged(self):
-        self.topLayer.rightWidget.zoomChanged()
-        self.compositionLayers.zoomChanged()
-
-
-    ## ToolBar callbacks
-
-    def toolBarZoomChangedCb(self, unused_toolbar, zoomratio):
-        self.setZoomRatio(zoomratio)
+        # toolbar actions
+        actions = (
+            ("ZoomIn", gtk.STOCK_ZOOM_IN, None, None, ZOOM_IN,
+                self._zoomInCb),
+            ("ZoomOut", gtk.STOCK_ZOOM_OUT, None, None, ZOOM_OUT, 
+                self._zoomOutCb),
+            ("DeleteObj", gtk.STOCK_DELETE, None, None, DELETE, 
+                self.compositionLayers.deleteSelected),
+            ("SelectBefore", gtk.STOCK_GOTO_FIRST, None, None, SELECT_BEFORE, 
+                self.compositionLayers.selectBeforeCurrent),
+            ("SelectAfter", gtk.STOCK_GOTO_LAST, None, None, SELECT_AFTER,
+                self.compositionLayers.selectAfterCurrent),
+            ("Razor", gtk.STOCK_CUT, None, None, RAZOR,
+                self.compositionLayers.activateRazor)
+        )
+        self.actiongroup = gtk.ActionGroup("complextimeline")
+        self.actiongroup.add_actions(actions)
+        self.actiongroup.set_visible(False)
+        instance.PiTiVi.uimanager.insert_action_group(self.actiongroup, 0)
+        instance.PiTiVi.uimanager.add_ui_from_string(ui)
+
+## Project callbacks
+
+    def _newProjectLoadingCb(self, unused_inst, project):
+        self.layerInfoList.setTimeline(project.timeline)
+
+    def _newProjectFailedCb(self, unused_inst, unused_reason, unused_uri):
+        self.layerInfoList.setTimeline(None)
+
+## layer callbacks
+
+    def _layerStartDurationChanged(self, layer):
+        self.ruler.startDurationChanged()
+
+## ToolBar callbacks
+
+    ## override show()/hide() methods to take care of actions
+    def show(self):
+        super(ComplexTimelineWidget, self).show()
+        self.actiongroup.set_visible(True)
+
+    def show_all(self):
+        super(ComplexTimelineWidget, self).show_all()
+        self.actiongroup.set_visible(True)
+
+    def hide(self):
+        self.actiongroup.set_visible(False)
+        super(ComplexTimelineWidget, self).hide()
+
+    def _computeZoomRatio(self, index):
+        return self.zoom_levels[index]
+
+    def _zoomInCb(self, unused_action):
+        self._cur_zoom = min(len(self.zoom_levels) - 1, self._cur_zoom + 1)
+        self._zoom_adj.set_value(self._computeZoomRatio(self._cur_zoom))
+
+    def _zoomOutCb(self, unused_action):
+        self._cur_zoom = max(0, self._cur_zoom - 1)
+        self._zoom_adj.set_value(self._computeZoomRatio(self._cur_zoom))
 
-    ## PlayGround timeline position callback
+## PlayGround timeline position callback
 
     def _playgroundPositionCb(self, unused_playground, smartbin, value):
         if isinstance(smartbin, SmartTimelineBin):
             # for the time being we only inform the ruler
-            self.topLayer.timelinePositionChanged(value, 0)
+            self.ruler.timelinePositionChanged(value, 0)
             self.compositionLayers.timelinePositionChanged(value, 0)

Modified: trunk/pitivi/ui/layerwidgets.py
==============================================================================
--- trunk/pitivi/ui/layerwidgets.py	(original)
+++ trunk/pitivi/ui/layerwidgets.py	Fri Aug 29 16:45:42 2008
@@ -33,7 +33,7 @@
 class TimelineToolBar(gtk.HBox):
 
     def __init__(self):
-        gtk.HBox.__init__(self, homogeneous=True)
+        gtk.HBox.__init__(self, homogeneous=False)
         self._addButtons()
 
     def _addButtons(self):
@@ -42,14 +42,15 @@
         image = gtk.image_new_from_stock(gtk.STOCK_ZOOM_IN,
                                          gtk.ICON_SIZE_SMALL_TOOLBAR)
         self.zoomInButton.set_image(image)
-        self.pack_start(self.zoomInButton, expand=False)
+        self.pack_start(self.zoomInButton, expand=False, fill=False)
         self.zoomInButton.connect('clicked', self._zoomClickedCb)
 
         self.zoomOutButton = gtk.Button(label="")
         self.zoomOutButton.set_image(gtk.image_new_from_stock(gtk.STOCK_ZOOM_OUT,
                                                               gtk.ICON_SIZE_SMALL_TOOLBAR))
-        self.pack_start(self.zoomOutButton, expand=False)
+        self.pack_start(self.zoomOutButton, expand=False, fill=False)
         self.zoomOutButton.connect('clicked', self._zoomClickedCb)
+        self._ratio = None
 
     def _zoomClickedCb(self, button):
         if button == self.zoomInButton:
@@ -62,6 +63,12 @@
             return
         self.setZoomRatio(ratio)
 
+    def getZoomRatio(self):
+        return self._ratio
+
+    def setZoomRatio(self, ratio):
+        self._ratio = ratio
+
 class TimelineLayer(gtk.HBox):
 
     leftWidgetClass = None

Modified: trunk/pitivi/ui/mainwindow.py
==============================================================================
--- trunk/pitivi/ui/mainwindow.py	(original)
+++ trunk/pitivi/ui/mainwindow.py	Fri Aug 29 16:45:42 2008
@@ -109,8 +109,6 @@
         if not isinstance(instance.PiTiVi.playground.current, SmartTimelineBin):
             return
         self.render_button.set_sensitive((duration > 0) and True or False)
-        if duration > 0 :
-            gobject.idle_add(self.timeline.simpleview._displayTimeline)
 
     def _currentPlaygroundChangedCb(self, playground, smartbin):
         if smartbin == playground.default:
@@ -123,7 +121,6 @@
                                                             self._timelineDurationChangedCb)
                 if smartbin.project.timeline.videocomp.duration > 0:
                     self.render_button.set_sensitive(True)
-                    gobject.idle_add(self.timeline.simpleview._displayTimeline)
                 else:
                     self.render_button.set_sensitive(False)
             else:
@@ -176,7 +173,7 @@
             ("File", None, _("_File")),
             ("Edit", None, _("_Edit")),
             ("View", None, _("_View")),
-            ("Help", None, _("_Help"))
+            ("Help", None, _("_Help")),
         ]
 
         self.toggleactions = [
@@ -209,18 +206,19 @@
                     action.set_sensitive(False)
             else:
                 action.set_sensitive(False)
-
-        self.uimanager = gtk.UIManager()
+        
+        self.uimanager = instance.PiTiVi.uimanager
         self.add_accel_group(self.uimanager.get_accel_group())
         self.uimanager.insert_action_group(self.actiongroup, 0)
-        self.uimanager.add_ui_from_file(os.path.join(os.path.dirname(os.path.abspath(__file__)), "actions.xml"))
+        self.uimanager.add_ui_from_file(os.path.join(os.path.dirname(
+            os.path.abspath(__file__)), "actions.xml"))
 
         self.connect_after("key-press-event", self._keyPressEventCb)
 
     def _createUi(self):
         """ Create the graphical interface """
         self.set_title("%s v%s" % (APPNAME, pitivi_version))
-        self.set_geometry_hints(min_width=800, min_height=600)
+        self.set_geometry_hints(min_width=800, min_height=480)
 
         self.connect("destroy", self._destroyCb)
 
@@ -240,9 +238,10 @@
 
         self.timeline = TimelineWidget()
         self.timeline.showSimpleView()
-        timelineframe = gtk.Frame()
-        timelineframe.add(self.timeline)
-        vpaned.pack2(timelineframe, resize=False, shrink=False)
+        # I honestly think it looks better without the frame
+        # timelineframe = gtk.Frame()
+        # timelineframe.add(self.timeline)
+        vpaned.pack2(self.timeline, resize=False, shrink=True)
 
         hpaned = gtk.HPaned()
         vpaned.pack1(hpaned, resize=True, shrink=False)
@@ -253,13 +252,20 @@
         # Viewer
         self.viewer = PitiviViewer()
 
-        instance.PiTiVi.playground.connect("current-changed", self._currentPlaygroundChangedCb)
+        instance.PiTiVi.playground.connect("current-changed", 
+            self._currentPlaygroundChangedCb)
 
         hpaned.pack1(self.sourcefactories, resize=False, shrink=False)
         hpaned.pack2(self.viewer, resize=True, shrink=False)
+        # FIXME: remove toolbar padding and shadow. In fullscreen mode, the
+        # toolbar buttons should be clickable with the mouse cursor at the
+        # very bottom of the screen.
+        vbox.pack_start(self.uimanager.get_widget("/TimelineToolBar"),
+            False)
 
         #application icon
-        self.set_icon_from_file(configure.get_global_pixmap_dir() + "/pitivi.png")
+        self.set_icon_from_file(configure.get_global_pixmap_dir() 
+            + "/pitivi.png")
 
     def toggleFullScreen(self):
         """ Toggle the fullscreen mode of the application """
@@ -270,7 +276,7 @@
             self.viewer.window.unfullscreen()
             self.isFullScreen = False
 
-    ## PlayGround callback
+## PlayGround callback
 
     def _errorMessageResponseCb(self, dialogbox, unused_response):
         dialogbox.hide()
@@ -278,27 +284,31 @@
         self.errorDialogBox = None
 
     def _playGroundErrorCb(self, unused_playground, error, detail):
+        # FIXME FIXME FIXME:
+        # _need_ an onobtrusive way to present gstreamer errors, 
+        # one that doesn't steel mouse/keyboard focus, one that
+        # makes some kind of sense to the user, and one that presents
+        # some ways of actually _dealing_ with the underlying problem:
+        # install a plugin, re-conform source to some other format, or
+        # maybe even disable playback of a problematic file.
         if self.errorDialogBox:
             return
         self.errorDialogBox = gtk.MessageDialog(None, gtk.DIALOG_MODAL,
-                                                gtk.MESSAGE_ERROR,
-                                                gtk.BUTTONS_OK,
-                                                None)
+            gtk.MESSAGE_ERROR, gtk.BUTTONS_OK, None)
         self.errorDialogBox.set_markup("<b>%s</b>" % error)
         self.errorDialogBox.connect("response", self._errorMessageResponseCb)
         if detail:
             self.errorDialogBox.format_secondary_text(detail)
         self.errorDialogBox.show()
 
-
-    ## Project source list callbacks
+## Project source list callbacks
 
     def _sourcesFileAddedCb(self, unused_sources, unused_factory):
-        if len(self.sourcefactories.sourcelist.storemodel) == 1 and not len(instance.PiTiVi.current.timeline.videocomp):
-            self.timeline.simpleview._displayTimeline(False)
-
+        #if (len(self.sourcefactories.sourcelist.storemodel) == 1 
+        #    and not len(instance.PiTiVi.current.timeline.videocomp):
+        pass
 
-    ## UI Callbacks
+## UI Callbacks
 
     def _destroyCb(self, unused_widget, data=None):
         instance.PiTiVi.shutdown()
@@ -308,7 +318,7 @@
         if gtk.gdk.keyval_name(event.keyval) in ['f', 'F', 'F11']:
             self.toggleFullScreen()
 
-    ## Toolbar/Menu actions callback
+## Toolbar/Menu actions callback
 
     def _newProjectMenuCb(self, unused_action):
         instance.PiTiVi.newBlankProject()
@@ -396,7 +406,7 @@
     def _pluginManagerCb(self, unused_action):
         PluginManagerDialog(instance.PiTiVi.plugin_manager)
 
-    ## PiTiVi main object callbacks
+## PiTiVi main object callbacks
 
     def _newProjectLoadedCb(self, pitivi, project):
         gst.log("A NEW project is loaded, update the UI!")
@@ -437,7 +447,7 @@
         dialog.destroy()
         self.set_sensitive(True)
 
-    ## PiTiVi current project callbacks
+## PiTiVi current project callbacks
 
     def _confirmOverwriteCb(self, unused_project, uri):
         message = _("Do you wish to overwrite existing file \"%s\"?") %\

Modified: trunk/pitivi/ui/ruler.py
==============================================================================
--- trunk/pitivi/ui/ruler.py	(original)
+++ trunk/pitivi/ui/ruler.py	Fri Aug 29 16:45:42 2008
@@ -27,9 +27,10 @@
 import gtk
 import gst
 import pitivi.instance as instance
-from complexinterface import ZoomableWidgetInterface
+from complexinterface import Zoomable
+from pitivi.utils import time_to_string
 
-class ScaleRuler(gtk.Layout, ZoomableWidgetInterface):
+class ScaleRuler(gtk.Layout, Zoomable):
 
     __gsignals__ = {
         "expose-event":"override",
@@ -40,12 +41,14 @@
         "motion-notify-event":"override",
         }
 
-    border = 5
+    border = 0
+    min_tick_spacing = 3
 
     def __init__(self, hadj):
         gst.log("Creating new ScaleRule")
         gtk.Layout.__init__(self)
-        self.add_events(gtk.gdk.POINTER_MOTION_MASK | gtk.gdk.BUTTON_PRESS_MASK | gtk.gdk.BUTTON_RELEASE_MASK)
+        self.add_events(gtk.gdk.POINTER_MOTION_MASK |
+            gtk.gdk.BUTTON_PRESS_MASK | gtk.gdk.BUTTON_RELEASE_MASK)
         self.set_hadjustment(hadj)
         self.pixmap = None
         # position is in nanoseconds
@@ -55,28 +58,25 @@
         self.currentlySeeking = False
         self.pressed = False
 
-    ## ZoomableWidgetInterface methods are handled by the container (LayerStack)
-    ## Except for ZoomChanged
+## Zoomable interface override
 
     def zoomChanged(self):
+        self.queue_resize()
         self.doPixmap()
         self.queue_draw()
 
-    def getPixelWidth(self):
-        return ZoomableWidgetInterface.getPixelWidth(self) + 2 * self.border
-
-
-    ## timeline position changed method
+## timeline position changed method
 
     def timelinePositionChanged(self, value, unused_frame):
-        previous = self.position
+        ppos = max(self.nsToPixel(self.position) - 1, 0)
         self.position = value
-        self.queue_draw_area(max(self.nsToPixel(min(value, previous)) - 5, 0),
-                             0,
-                             self.nsToPixel(max(value, previous)) + 5,
-                             self.get_allocation().height)
+        npos = max(self.nsToPixel(self.position) - 1, 0)
+
+        height = self.get_allocation().height
+        self.queue_draw_area(ppos, 0, 2, height)
+        self.queue_draw_area(npos, 0, 2, height)
 
-    ## gtk.Widget overrides
+## gtk.Widget overrides
 
     def do_size_allocate(self, allocation):
         gst.debug("ScaleRuler got %s" % list(allocation))
@@ -96,9 +96,10 @@
         gst.debug("exposing ScaleRuler %s" % list(event.area))
         x, y, width, height = event.area
         # double buffering power !
-        self.bin_window.draw_drawable(self.style.fg_gc[gtk.STATE_NORMAL],
-                                      self.pixmap,
-                                      x, y, x, y, width, height)
+        self.bin_window.draw_drawable(
+            self.style.fg_gc[gtk.STATE_NORMAL],
+            self.pixmap,
+            x, y, x, y, width, height)
         # draw the position
         context = self.bin_window.cairo_create()
         self.drawPosition(context, self.get_allocation())
@@ -126,13 +127,13 @@
             self._doSeek(cur)
         return False
 
-    ## Seeking methods
+## Seeking methods
 
     def _seekTimeoutCb(self):
         gst.debug("timeout")
-        self.currentlySeeking = False
         if not self.position == self.requested_time:
             self._doSeek(self.requested_time)
+            self.currentlySeeking = False
 
     def _doSeek(self, value, format=gst.FORMAT_TIME):
         gst.debug("seeking to %s" % gst.TIME_ARGS (value))
@@ -144,7 +145,7 @@
         elif format == gst.FORMAT_TIME:
             self.requested_time = value
 
-    ## Drawing methods
+## Drawing methods
 
     def doPixmap(self):
         """ (re)create the buffered drawable for the Widget """
@@ -154,10 +155,12 @@
         allocation = self.get_allocation()
         lwidth, lheight = self.get_size()
         allocation.width = max(allocation.width, lwidth)
-        gst.debug("Creating pixmap(self.window, width:%d, height:%d)" % (allocation.width, allocation.height))
+        gst.debug("Creating pixmap(self.window, width:%d, height:%d)" 
+            % (allocation.width, allocation.height))
         if self.pixmap:
             del self.pixmap
-        self.pixmap = gtk.gdk.Pixmap(self.bin_window, allocation.width, allocation.height)
+        self.pixmap = gtk.gdk.Pixmap(self.bin_window, allocation.width, 
+            allocation.height)
         context = self.pixmap.cairo_create()
         self.drawBackground(context, allocation)
         self.drawRuler(context, allocation)
@@ -168,105 +171,96 @@
         self.drawBackground(context, rect)
         self.drawRuler(context, rect)
 
+    def getDuration(self):
+        return instance.PiTiVi.current.timeline.getDuration()
+
+    def getPixelWidth(self):
+        return self.nsToPixel(self.getDuration())
+
+    def getPixelPosition(self):
+        return 0
+
     def drawBackground(self, context, allocation):
         context.save()
 
         context.set_source_rgb(0.5, 0.5, 0.5)
-        context.rectangle(0, 0,
-                          allocation.width, allocation.height)
+        context.rectangle(0, 0, allocation.width, allocation.height)
         context.fill()
         context.stroke()
 
         if self.getDuration() > 0:
             context.set_source_rgb(0.8, 0.8, 0.8)
-            context.rectangle(0, 0,
-                              self.getPixelWidth(), allocation.height)
+            context.rectangle(0, 0, self.getPixelWidth(), allocation.height)
             context.fill()
             context.stroke()
 
         context.restore()
 
-    def drawRuler(self, context, allocation):
-        context.save()
+    def startDurationChanged(self):
+        gst.info("start/duration changed")
+        self.queue_resize()
 
-        zoomRatio = self.getZoomRatio()
+    def drawRuler(self, context, allocation):
+        # there are 4 lengths of tick mark:
+        # full height: largest increments, 1 minute
+        # 3/4 height: 10 seconds
+        # 1/2 height: 1 second
+        # 1/4 height: 1/10 second (might later be changed to 1 frame in
+        #   project framerate)
+
+        # At the highest level of magnification, all ticmarks are visible. At
+        # the lowest, only the full height tic marks are visible. The
+        # appearance of text is dependent on the spacing between tics: text
+        # only appears when there is enough space between tics for it to be
+        # readable.
 
-        paintpos = float(self.border) + 0.5
-        seconds = 0
-        secspertic = 1
-
-        timeprint = 0
-        ticspertime = 1
-
-        # FIXME : this should be beautified (instead of all the if/elif/else)
-        if zoomRatio < 0.05:
-            #Smallest tic is 10 minutes
-            secspertic = 600
-            if zoomRatio < 0.006:
-                ticspertime = 24
-            elif zoomRatio < 0.0125:
-                ticspertime = 12
-            elif zoomRatio < 0.025:
-                ticspertime = 6
-            else:
-                ticspertime = 3
-        elif zoomRatio < 0.5:
-            #Smallest tic is 1 minute
-            secspertic = 60
-            if zoomRatio < 0.25:
-                ticspertime = 10
-            else:
-                ticspertime = 5
-        elif zoomRatio < 3:
-            #Smallest tic is 10 seconds
-            secspertic = 10
-            if zoomRatio < 1:
-                ticspertime = 12
-            else:
-                ticspertime = 6
-        else:
-            #Smallest tic is 1 second
-            if zoomRatio < 5:
-                ticspertime = 20
-            elif zoomRatio < 10:
-                ticspertime = 10
-            elif zoomRatio < 20:
-                ticspertime = 5
-            elif zoomRatio < 40:
-                ticspertime = 2
+        def textSize(text):
+            return context.text_extents(text)[2:4]
 
-        while paintpos < allocation.width:
+        def drawTick(paintpos, height):
             context.move_to(paintpos, 0)
+            context.line_to(paintpos, allocation.height * height)
 
-            if seconds % 600 == 0:
-                context.line_to(paintpos, allocation.height)
-            elif seconds % 60 == 0:
-                context.line_to(paintpos, allocation.height * 3 / 4)
-            elif seconds % 10 == 0:
-                context.line_to(paintpos, allocation.height / 2)
-            else:
-                context.line_to(paintpos, allocation.height / 4)
-
-            if timeprint == 0:
-                # draw the text position
-                hours = int(seconds / 3600)
-                mins = seconds % 3600 / 60
-                secs = seconds % 60
-                time = "%02d:%02d:%02d" % (hours, mins, secs)
-                txtwidth, txtheight = context.text_extents(time)[2:4]
-                context.move_to( paintpos - txtwidth / 2.0,
-                                 allocation.height - 2 )
-                context.show_text( time )
-                timeprint = ticspertime
-            timeprint -= 1
-
-            paintpos += zoomRatio * secspertic
-            seconds += secspertic
-
-        #Since drawing is done in batch we can't use different styles
-        context.set_line_width(1)
-        context.set_source_rgb(0, 0, 0)
+        def drawText(paintpos, time, txtwidth, txtheight):
+            # draw the text position
+            time = time_to_string(time)
+            context.move_to( paintpos - txtwidth / 2.0,
+                             allocation.height - 2 )
+            context.show_text( time )
+
+        def drawTicks(interval, height):
+            paintpos = float(self.border) + 0.5
+            spacing = zoomRatio * interval
+            if spacing >= self.min_tick_spacing:
+                while paintpos < allocation.width:
+                    drawTick(paintpos, height)
+                    paintpos += zoomRatio * interval
+
+        def drawTimes(interval):
+            # figure out what the optimal offset is
+            paintpos = float(self.border) + 0.5
+            seconds = 0
+            spacing = zoomRatio * interval
+            textwidth, textheight = textSize(time_to_string(0))
+            if spacing > textwidth:
+                while paintpos < allocation.width:
+                    timevalue = long(seconds * gst.SECOND)
+                    drawText(paintpos, timevalue, textwidth, textheight)
+                    paintpos += spacing
+                    seconds += interval
 
+        context.save()
+        zoomRatio = self.getZoomRatio()
+        # looks better largest tick doesn't run into the text label
+        interval_sizes = ((60, 0.80), (10, 0.75), (1, 0.5), (0.1, 0.25))
+        for interval, height in interval_sizes:
+            drawTicks(interval, height)
+            drawTimes(interval)
+
+        #set a slightly thicker line. This forces anti-aliasing, and gives the
+        #a softer appearance
+        context.set_line_width(1.1)
+        context.set_source_rgb(0.4, 0.4, 0.4)
         context.stroke()
         context.restore()
 
@@ -274,8 +268,9 @@
         if self.getDuration() <= 0:
             return
         # a simple RED line will do for now
-        xpos = self.nsToPixel(self.position) + self.border + 0.5
+        xpos = self.nsToPixel(self.position) + self.border
         context.save()
+        context.set_line_width(1.5)
         context.set_source_rgb(1.0, 0, 0)
 
         context.move_to(xpos, 0)

Modified: trunk/pitivi/ui/sourcefactories.py
==============================================================================
--- trunk/pitivi/ui/sourcefactories.py	(original)
+++ trunk/pitivi/ui/sourcefactories.py	Fri Aug 29 16:45:42 2008
@@ -337,8 +337,9 @@
 
     def _addFactory(self, factory):
         try:
-            pixbuf = gtk.gdk.pixbuf_new_from_file(factory.thumbnail)
+            pixbuf = gtk.gdk.pixbuf_new_from_file(factory.getThumbnail())
         except:
+            gst.error("Failure to create thumbnail from file %s" % factory.getThumbnail())
             if factory.is_video:
                 thumbnail = self.videofilepixbuf
             elif factory.is_audio:
@@ -355,7 +356,7 @@
                                 factory.getPrettyInfo(),
                                 factory,
                                 factory.name,
-                                "<b>%s</b>" % beautify_length(factory.length)])
+                                factory.getDuration() and "<b>%s</b>" % beautify_length(factory.getDuration()) or ""])
         self._displayTreeView()
 
     # sourcelist callbacks

Modified: trunk/pitivi/ui/timeline.py
==============================================================================
--- trunk/pitivi/ui/timeline.py	(original)
+++ trunk/pitivi/ui/timeline.py	Fri Aug 29 16:45:42 2008
@@ -33,8 +33,10 @@
 
 import pitivi.instance as instance
 import pitivi.dnd as dnd
+from pitivi.timeline.source import TimelineFileSource, TimelineBlankSource
+from pitivi.timeline.objects import MEDIA_TYPE_AUDIO, MEDIA_TYPE_VIDEO
 
-from timelineobjects import SimpleTimeline
+from timelineobjects import SimpleTimelineWidget
 from complextimeline import ComplexTimelineWidget
 
 class TimelineWidget(gtk.VBox):
@@ -45,19 +47,18 @@
         gtk.VBox.__init__(self)
         self._createUi()
 
+        # drag and drop
+        self.drag_dest_set(gtk.DEST_DEFAULT_DROP | gtk.DEST_DEFAULT_MOTION, 
+            [dnd.FILESOURCE_TUPLE],
+            gtk.gdk.ACTION_COPY)
+        self.connect("drag-data-received", self._dragDataReceivedCb)
+        self.connect("drag-leave", self._dragLeaveCb)
+        self.connect("drag-motion", self._dragMotionCb)
+
     def _createUi(self):
         """ draw the GUI """
-        self.hadjustment = gtk.Adjustment()
-        self.vadjustment = gtk.Adjustment()
-
-        self.simpleview = SimpleTimelineContentWidget(self)
-        self.complexview = ComplexTimelineWidget(self)
-
-        self.simpleview.connect("scroll-event", self._simpleScrollCb)
-        self.complexview.connect("scroll-event", self._simpleScrollCb)
-
-        self.hscroll = gtk.HScrollbar(self.hadjustment)
-        self.pack_end(self.hscroll, expand=False)
+        self.simpleview = SimpleTimelineWidget()
+        self.complexview = ComplexTimelineWidget()
 
     def showSimpleView(self):
         """ Show the simple timeline """
@@ -79,94 +80,56 @@
         gst.debug("state:%s" % event.state)
         self.hscroll.emit("scroll-event", event)
 
-class SimpleTimelineContentWidget(gtk.HBox):
-    """ Widget for Simple Timeline content display """
-    def __init__(self, twidget):
-        """ init """
-        self.twidget = twidget
-        gtk.HBox.__init__(self)
-        self._createUi()
-        self.show_all()
+## Drag and Drop callbacks
 
-    def _createUi(self):
-        """ draw the GUI """
-
-        # (A) real simple timeline
-        self.timeline = SimpleTimeline(hadjustment = self.twidget.hadjustment)
-        self.layoutframe = gtk.Frame()
-        self.layoutframe.add(self.timeline)
-
-
-        # (B) Explanatory message label
-        self.messageframe = gtk.Frame()
-        self.messageframe.set_shadow_type(gtk.SHADOW_ETCHED_IN)
-        self.messageframe.show()
-
-        self.textbox = gtk.EventBox()
-        self.textbox.modify_bg(gtk.STATE_NORMAL, gtk.gdk.color_parse('white'))
-        self.textbox.add_events(gtk.gdk.ENTER_NOTIFY_MASK)
-        self.textbox.show()
-        self.messageframe.add(self.textbox)
-
-        txtlabel = gtk.Label()
-        txtlabel.set_padding(10, 10)
-        txtlabel.set_line_wrap(True)
-        txtlabel.set_line_wrap_mode(pango.WRAP_WORD)
-        txtlabel.set_justify(gtk.JUSTIFY_CENTER)
-        txtlabel.set_markup(
-            _("<span size='x-large'>Add clips to the timeline by dragging them here.</span>"))
-        self.textbox.add(txtlabel)
-        self.txtlabel = txtlabel
-
-        self.pack_start(self.messageframe, expand=True, fill=True)
-        self.reorder_child(self.messageframe, 0)
-        self.motionSigId = self.textbox.connect("drag-motion", self._dragMotionCb)
-        self.textbox.drag_dest_set(gtk.DEST_DEFAULT_DROP | gtk.DEST_DEFAULT_MOTION,
-                                   [dnd.URI_TUPLE, dnd.FILE_TUPLE],
-                                   gtk.gdk.ACTION_COPY)
-
-        self.showingTimeline = False
-        self._displayTimeline()
-
-    def _dragMotionCb(self, unused_layout, unused_context, unused_x, unused_y,
-                      unused_timestamp):
-        gst.log("motion...")
-        self.showingTimeline = False
-        gobject.idle_add(self._displayTimeline)
-
-    def _dragLeaveCb(self, unused_layout, unused_context, unused_timestamp):
-        gst.log("leave...")
-        if len(instance.PiTiVi.current.timeline.videocomp):
+    def _gotFileFactory(self, filefactory, x, y):
+        """ got a filefactory at the given position """
+        # remove the slot
+        if not filefactory or not filefactory.is_video:
             return
-        self.showingTimeline = True
-        gobject.idle_add(self._displayTimeline, False)
+        #pos_ = self.items.point_to_index(pixel_coords(self, (x, y)))
+        pos_ = 0
+        gst.debug("_got_filefactory pos : %d" % pos_)
+        # we just add it here, the drawing will be done in the condensed_list
+        # callback
+        source = TimelineFileSource(factory=filefactory,
+            media_type=MEDIA_TYPE_VIDEO,
+            name=filefactory.name)
+
+        # ONLY FOR SIMPLE TIMELINE : if video-only, we link a blank audio object
+        if not filefactory.is_audio:
+            audiobrother = TimelineBlankSource(factory=filefactory,
+                media_type=MEDIA_TYPE_AUDIO, name=filefactory.name)
+            source.setBrother(audiobrother)
+
+        timeline = instance.PiTiVi.current.timeline
+        if pos_ == -1:
+            timeline.videocomp.appendSource(source)
+        elif pos_:
+            timeline.videocomp.insertSourceAfter(source,
+                self.condensed[pos_ - 1])
+        else:
+            timeline.videocomp.prependSource(source)
 
-    def _displayTimeline(self, displayed=True):
-        if displayed:
-            if self.showingTimeline:
-                return
-            gst.debug("displaying timeline")
-            self.remove(self.messageframe)
-            self.txtlabel.hide()
-            self.textbox.disconnect(self.motionSigId)
-            self.motionSigId = None
-            self.pack_start(self.layoutframe)
-            self.reorder_child(self.layoutframe, 0)
-            self.layoutframe.show_all()
-            self.dragLeaveSigId = self.timeline.connect("drag-leave", self._dragLeaveCb)
-            self.showingTimeline = True
+    def _dragMotionCb(self, unused_layout, unused_context, x, y, timestamp):
+        #TODO: temporarily add source to timeline, and put it in drag mode
+        # so user can see where it will go
+        gst.info("SimpleTimeline x:%d , source would go at %d" % (x, 0))
+
+    def _dragLeaveCb(self, unused_layout, unused_context, unused_tstamp):
+        gst.info("SimpleTimeline")
+        #TODO: remove temp source from timeline
+
+    def _dragDataReceivedCb(self, unused_layout, context, x, y, 
+        selection, targetType, timestamp):
+        gst.log("SimpleTimeline, targetType:%d, selection.data:%s" % 
+            (targetType, selection.data))
+        if targetType == dnd.TYPE_PITIVI_FILESOURCE:
+            uri = selection.data
         else:
-            if not self.showingTimeline:
-                return
-            # only hide if there's nothing left in the timeline
-            if not len(instance.PiTiVi.current.timeline.videocomp):
-                gst.debug("hiding timeline")
-                self.timeline.disconnect(self.dragLeaveSigId)
-                self.dragLeaveSigId = None
-                self.remove(self.layoutframe)
-                self.layoutframe.hide()
-                self.pack_start(self.messageframe)
-                self.reorder_child(self.messageframe, 0)
-                self.txtlabel.show()
-                self.motionSigId = self.textbox.connect("drag-motion", self._dragMotionCb)
-                self.showingTimeline = False
+            context.finish(False, False, timestamp)
+        self._gotFileFactory(instance.PiTiVi.current.sources[uri], x, y)
+        context.finish(True, False, timestamp)
+        instance.PiTiVi.playground.switchToTimeline()
+
+

Modified: trunk/pitivi/ui/timelineobjects.py
==============================================================================
--- trunk/pitivi/ui/timelineobjects.py	(original)
+++ trunk/pitivi/ui/timelineobjects.py	Fri Aug 29 16:45:42 2008
@@ -42,11 +42,15 @@
 from sourcefactories import beautify_length
 from gettext import gettext as _
 
+import goocanvas
+from util import *
+from pitivi.utils import time_to_string
+
 # Default width / height ratio for simple elements
 DEFAULT_SIMPLE_SIZE_RATIO = 1.50 # default height / width ratio
 
 # Default simple elements size
-DEFAULT_SIMPLE_ELEMENT_WIDTH = 150
+DEFAULT_SIMPLE_ELEMENT_WIDTH = 100
 DEFAULT_SIMPLE_ELEMENT_HEIGHT = DEFAULT_SIMPLE_ELEMENT_WIDTH * DEFAULT_SIMPLE_SIZE_RATIO
 
 # Default spacing between/above elements in simple timeline
@@ -58,34 +62,23 @@
 MINIMUM_HEIGHT = DEFAULT_HEIGHT
 MINIMUM_WIDTH = 3 * MINIMUM_HEIGHT
 
-def time_to_string(value):
-    if value == -1:
-        return "--:--:--.---"
-    ms = value / gst.MSECOND
-    sec = ms / 1000
-    ms = ms % 1000
-    mins = sec / 60
-    sec = sec % 60
-    hours = mins / 60
-    return "%02d:%02d:%02d.%03d" % (hours, mins, sec, ms)
-
-class SimpleTimeline(gtk.Layout):
-    """ Simple Timeline representation """
-
-    def __init__(self, **kw):
-        gobject.GObject.__init__(self, **kw)
-
-        self.hadjustment = self.get_property("hadjustment")
-
-        # timeline and top level compositions
-        self.timeline = instance.PiTiVi.current.timeline
-        self.condensed = self.timeline.videocomp.condensed
-
-        # TODO : connect signals for when the timeline changes
-
-        # widgets correspondance dictionnary
-        # MAPPING timelineobject => widget
-        self.widgets = {}
+class SimpleTimelineWidget(gtk.HBox):
+    """Contains the editing widget as well as a gtk.ScrolledWindow containing
+    the simple timeline canvas. Handles showing/hiding the editing widget and
+    canvas."""
+
+    __gtype_name__ = 'SimpleTimelineWidget'
+
+    def __init__(self, *args, **kwargs):
+        gtk.HBox.__init__(self, *args, **kwargs)
+        timeline = SimpleTimelineCanvas()
+        timeline.connect("edit-me", self._editMeCb)
+
+        self.content = gtk.ScrolledWindow()
+        self.content.set_policy(gtk.POLICY_ALWAYS, gtk.POLICY_NEVER)
+        self.content.add(timeline)
+        #add other objects here
+        self.add(self.content)
 
         # edit-mode
         # True when in editing mode
@@ -93,433 +86,256 @@
         self.editingWidget = SimpleEditingWidget()
         self.editingWidget.connect("hide-me", self._editingWidgetHideMeCb)
 
-        # Connect to timeline.  We must remove and reset the callbacks when
-        # changing project.
-        self.project_signals = SignalGroup()
-        # FIXME: do we need this? or will the newproject sginal implicitly
-        # handle this???
-        self._connectToTimeline(instance.PiTiVi.current.timeline)
-        instance.PiTiVi.connect("new-project-loaded",
-            self._newProjectLoadedCb)
         instance.PiTiVi.connect("project-closed", self._projectClosedCb)
-        instance.PiTiVi.connect("new-project-loading",
-            self._newProjectLoadingCb)
         instance.PiTiVi.connect("new-project-failed",
             self._newProjectFailedCb)
 
-        # size
-        self.width = int(DEFAULT_WIDTH)
-        self.height = int(DEFAULT_HEIGHT)
-        self.realWidth = 0 # displayed width of the layout
-        self.childheight = int(DEFAULT_SIMPLE_ELEMENT_HEIGHT)
-        self.childwidth = int(DEFAULT_SIMPLE_ELEMENT_WIDTH)
-        self.set_size_request(int(MINIMUM_WIDTH), int(MINIMUM_HEIGHT))
-        self.set_property("width", int(DEFAULT_WIDTH))
-        self.set_property("height", int(DEFAULT_HEIGHT))
-
-        # event callbacks
-        self.connect("expose-event", self._exposeEventCb)
-        self.connect("notify::width", self._widthChangedCb)
-        self.connect("size-allocate", self._sizeAllocateCb)
-        self.connect("realize", self._realizeCb)
-
-        # drag and drop
-        self.drag_dest_set(gtk.DEST_DEFAULT_DROP | gtk.DEST_DEFAULT_MOTION,
-                           [dnd.FILESOURCE_TUPLE],
-                           gtk.gdk.ACTION_COPY)
-        self.connect("drag-data-received", self._dragDataReceivedCb)
-        self.connect("drag-leave", self._dragLeaveCb)
-        self.connect("drag-motion", self._dragMotionCb)
-        self.slotposition = -1
+    def _editMeCb(self, unused_timeline, element):
+        self._switchEditingMode(element)
 
-        self.draggedelement = None
-
-        self.show_all()
-
-
-    ## Project callbacks
-
-    def _connectToTimeline(self, timeline):
-        self.timeline = timeline
-        self.condensed = self.timeline.videocomp.condensed
-        self.project_signals.connect(self.timeline.videocomp,
-                                     "condensed-list-changed",
-                                     None, self._condensedListChangedCb)
-
-    def _newProjectLoadingCb(self, unused_inst, unused_project):
-        gst.log("...")
-
-    def _newProjectLoadedCb(self, unused_inst, project):
-        gst.log("...")
-        assert(instance.PiTiVi.current == project)
-        # now we connect to the new project, so we can receive any
-        # signals that might be emitted while the project is loading
-        self._connectToTimeline(project.timeline)
-        # TODO: display final state of project now that loading has
-        # completed. this callback doesn't do do much else
-
-        # LOAD THE TIMELINE !!!
-        self._condensedListChangedCb(None, self.timeline.videocomp.condensed)
+    def _editingWidgetHideMeCb(self, unused_widget):
+        self.switchToNormalMode()
+        # switch back to timeline in playground !
+        instance.PiTiVi.playground.switchToTimeline()
 
     def _newProjectFailedCb(self, unused_inst, unused_reason, unused_uri):
-        # oops the project failed to load
-        self._clearTimeline()
-
-    def _clearTimeline(self):
         self.switchToNormalMode()
-        self.project_signals.disconnectAll()
-        for widget in self.widgets.itervalues():
-            self.remove(widget)
-        self.widgets = {}
 
     def _projectClosedCb(self, unused_pitivi, unused_project):
-        self._clearTimeline()
-
-    ## Timeline callbacks
-
-    def _condensedListChangedCb(self, unused_videocomp, clist):
-        """ add/remove the widgets """
-        gst.debug("condensed list changed in videocomp")
-
-        current = self.widgets.keys()
-        self.condensed = clist
-
-        new = [x for x in clist if not x in current]
-        removed = [x for x in current if not x in clist]
-
-        # new elements
-        for element in new:
-            # add the widget to self.widget
-            gst.debug("Adding new element to the layout")
-            if isinstance(element, TimelineFileSource):
-                widget = SimpleSourceWidget(element)
-                widget.connect("delete-me", self._sourceDeleteMeCb, element)
-                widget.connect("edit-me", self._sourceEditMeCb, element)
-                widget.connect("drag-begin", self._sourceDragBeginCb, element)
-                widget.connect("drag-end", self._sourceDragEndCb, element)
-            else:
-                widget = SimpleTransitionWidget(element)
-            self.widgets[element] = widget
-            self.put(widget, 0, 0)
-            widget.show()
-
-        # removed elements
-        for element in removed:
-            self.remove(self.widgets[element])
-            del self.widgets[element]
-
-        self._resizeChildrens()
-
-
-    ## Utility methods
-
-    def _getNearestSourceSlot(self, x):
-        """
-        returns the nearest file slot position available for the given position
-        Returns the value in condensed list position
-        Returns n , the element before which it should go
-        Return -1 if it's meant to go last
-        """
-        if not self.condensed or x < 0:
-            return 0
-        if x > self.width - DEFAULT_SIMPLE_SPACING:
-            return -1
-
-        pos = DEFAULT_SIMPLE_SPACING
-        order = 0
-        # TODO Need to avoid getting position between source and transition
-        for source in self.condensed:
-            if isinstance(source, TimelineSource):
-                spacing = self.childwidth
-            elif isinstance(source, TimelineTransition):
-                spacing = self.childwidth / 2
-            else:
-                # this shouldn't happen !! The condensed list only contains
-                # sources and/or transitions
-                pass
-            if x <= pos + spacing / 2:
-                return order
-            pos = pos + spacing + DEFAULT_SIMPLE_SPACING
-            order = order + 1
-        return -1
-
-    def _getNearestSourceSlotPixels(self, x):
-        """
-        returns the nearest file slot position available for the given position
-        Returns the value in pixels
-        """
-        if not self.condensed or x < 0:
-            return DEFAULT_SIMPLE_SPACING
-        if x > self.width - DEFAULT_SIMPLE_SPACING:
-            return self.width - 2 * DEFAULT_SIMPLE_SPACING
-
-        pos = DEFAULT_SIMPLE_SPACING
-        # TODO Need to avoid getting position between source and transition
-        for source in self.condensed:
-            if isinstance(source, TimelineSource):
-                spacing = self.childwidth
-            elif isinstance(source, TimelineTransition):
-                spacing = self.childwidth / 2
-            else:
-                # this shouldn't happen !! The condensed list only contains
-                # sources and/or transitions
-                pass
-            if x <= pos + spacing / 2:
-                return pos
-            pos = pos + spacing + DEFAULT_SIMPLE_SPACING
-        return pos
-
+        self.switchToNormalMode()
 
-    ## Drawing
+    def _switchEditingMode(self, source, mode=True):
+        """ Switch editing mode for the given TimelineSource """
+        gst.log("source:%s , mode:%s" % (source, mode))
 
-    def _drawDragSlot(self):
-        if self.slotposition == -1:
+        if self._editingMode == mode:
+            gst.warning("We were already in correct editing mode : %s" % 
+                mode)
             return
-        self.bin_window.draw_rectangle(self.style.black_gc, True,
-                                       self.slotposition, DEFAULT_SIMPLE_SPACING,
-                                       DEFAULT_SIMPLE_SPACING, self.childheight)
 
-    def _eraseDragSlot(self):
-        if self.slotposition == -1:
-            return
-        self.bin_window.draw_rectangle(self.style.white_gc, True,
-                                       self.slotposition, DEFAULT_SIMPLE_SPACING,
-                                       DEFAULT_SIMPLE_SPACING, self.childheight)
-
-    def _gotFileFactory(self, filefactory, x, unused_y):
-        """ got a filefactory at the given position """
-        # remove the slot
-        self._eraseDragSlot()
-        self.slotposition = -1
-        if not filefactory or not filefactory.is_video:
+        if mode and not source:
+            gst.warning("You need to specify a valid TimelineSource")
             return
-        pos = self._getNearestSourceSlot(x)
 
-        gst.debug("_got_filefactory pos : %d" % pos)
-
-        # we just add it here, the drawing will be done in the condensed_list
-        # callback
-        source = TimelineFileSource(factory=filefactory,
-                                    media_type=MEDIA_TYPE_VIDEO,
-                                    name=filefactory.name)
-
-        # ONLY FOR SIMPLE TIMELINE : if video-only, we link a blank audio object
-        if not filefactory.is_audio:
-            audiobrother = TimelineBlankSource(factory=filefactory,
-                                               media_type=MEDIA_TYPE_AUDIO,
-                                               name=filefactory.name)
-            source.setBrother(audiobrother)
-
-        if pos == -1:
-            self.timeline.videocomp.appendSource(source)
-        elif pos:
-            self.timeline.videocomp.insertSourceAfter(source, self.condensed[pos - 1])
-        else:
-            self.timeline.videocomp.prependSource(source)
+        if mode:
+            # switching TO editing mode
+            gst.log("Switching TO editing mode")
 
-    def _moveElement(self, element, x):
-        gst.debug("TimelineSource, move %s to x:%d" % (element, x))
-        # remove the slot
-        self._eraseDragSlot()
-        self.slotposition = -1
-        pos = self._getNearestSourceSlot(x)
+            # 1. Hide all sources
+            self.remove(self.content)
+            self.content.hide()
+            self._editingMode = mode
 
-        self.timeline.videocomp.moveSource(element, pos)
+            # 2. Show editing widget
+            self.editingWidget.setSource(source)
+            self.add(self.editingWidget)
+            self.editingWidget.show_all()
 
-    def _widthChangedCb(self, unused_layout, property):
-        if not property.name == "width":
-            return
-        self.width = self.get_property("width")
+        else:
+            gst.log("Switching back to normal mode")
+            # switching FROM editing mode
 
-    def _motionNotifyEventCb(self, layout, event):
-        pass
+            # 1. Hide editing widget
+            self.remove(self.editingWidget)
+            self.editingWidget.hide()
+            self._editingMode = mode
 
+            # 2. Show all sources
+            self.add(self.content)
+            self.content.show_all()
 
-    ## Drag and Drop callbacks
+    def switchToEditingMode(self, source):
+        """ Switch to Editing mode for the given TimelineSource """
+        self._switchEditingMode(source)
 
-    def _dragMotionCb(self, unused_layout, unused_context, x, unused_y,
-                      unused_timestamp):
-        # TODO show where the dragged item would go
-        pos = self._getNearestSourceSlotPixels(x + (self.hadjustment.get_value()))
-        rpos = self._getNearestSourceSlot(x + self.hadjustment.get_value())
-        gst.log("SimpleTimeline x:%d , source would go at %d" % (x, rpos))
-        if not pos == self.slotposition:
-            if not self.slotposition == -1:
-                # erase previous slot position
-                self._eraseDragSlot()
-            # draw new slot position
-            self.slotposition = pos
-            self._drawDragSlot()
-
-    def _dragLeaveCb(self, unused_layout, unused_context, unused_timestamp):
-        gst.log("SimpleTimeline")
-        self._eraseDragSlot()
-        self.slotposition = -1
-        # TODO remove the drag emplacement
-
-    def _dragDataReceivedCb(self, unused_layout, context, x, y, selection,
-                            targetType, timestamp):
-        gst.log("SimpleTimeline, targetType:%d, selection.data:%s" % (targetType, selection.data))
-        if targetType == dnd.TYPE_PITIVI_FILESOURCE:
-            uri = selection.data
-        else:
-            context.finish(False, False, timestamp)
-        x = x + int(self.hadjustment.get_value())
-        if self.draggedelement:
-            self._moveElement(self.draggedelement, x)
-        else:
-            self._gotFileFactory(instance.PiTiVi.current.sources[uri], x, y)
-        context.finish(True, False, timestamp)
-        instance.PiTiVi.playground.switchToTimeline()
+    def switchToNormalMode(self):
+        """ Switch back to normal timeline mode """
+        self._switchEditingMode(None, False)
 
 
-    ## Drawing
+class TimelineList(HList):
+    """A dynamically re-orderable group of items which knows about pitivi
+    timeline objects. Connects only to the video composition of the
+    timeline"""
+    __gtype_name__ = 'TimelineList'
 
-    def _realizeCb(self, unused_layout):
-        self.modify_bg(gtk.STATE_NORMAL, self.style.white)
+    __gsignals__ = {
+        'edit-me' : (gobject.SIGNAL_RUN_LAST,
+                     gobject.TYPE_NONE,
+                     (gobject.TYPE_PYOBJECT,))
+        }
 
-    def _areaIntersect(self, x, y, w, h, x2, y2, w2, h2):
-        """ returns True if the area intersects, else False """
-        # is zone to the left of zone2
-        z1 = gtk.gdk.Rectangle(x, y, w, h)
-        z2 = gtk.gdk.Rectangle(x2, y2, w2, h2)
-        r = z1.intersect(z2)
-        a, b, c, d = r
-        if a or b or c or d:
-            return True
-        return False
+    def __init__(self, timeline, *args, **kwargs):
+        HList.__init__(self, *args, **kwargs)
+        self.sig_ids = None
+        self.timeline = None
+        self.set_timeline(timeline)
+        self.reorderable = True
+        self.widgets = {}
+        self.elements = {}
 
-    def _exposeEventCb(self, unused_layout, event):
-        x, y, w, h = event.area
-        # redraw the slot rectangle if there's one
-        if not self.slotposition == -1:
-            if self._areaIntersect(x, y, w, h,
-                                   self.slotposition, DEFAULT_SIMPLE_SPACING,
-                                   DEFAULT_SIMPLE_SPACING, self.childheight):
-                self.bin_window.draw_rectangle(self.style.black_gc, True,
-                                               self.slotposition, DEFAULT_SIMPLE_SPACING,
-                                               DEFAULT_SIMPLE_SPACING, self.childheight)
+    def set_timeline(self, timeline):
+        self.remove_all()
+        if self.timeline:
+            for sig in self.sig_ids:
+                self.timeline.videocomp.disconnect(sig)
+            self.sig_ids = None
+        self.timeline = timeline
+        if timeline:
+            #TODO: connect transition callbacks here
+            changed = timeline.videocomp.connect("condensed-list-changed", 
+                self._condensedListChangedCb)
+            added = timeline.videocomp.connect("source-added",
+                self._sourceAddedCb)
+            removed = timeline.videocomp.connect("source-removed",
+                self._sourceRemovedCb)
+            self.sig_ids = (changed, added, removed)
+            self._condensedListChangedCb(None, timeline.videocomp.condensed)
+
+    # overriding from parent
+    def swap(self, a, b):
+        #TODO: make this code handle transitions.
+        element_a = self.elements[a]
+        element_b = self.elements[b]
+        index_a = self.index(a)
+        index_b = self.index(b)
+
+        #FIXME: are both of these calls necessary? or do we just need to be
+        # smarter about figuring which source to move in front of the other.
+        # in any case, it seems to work.
+        self.timeline.videocomp.moveSource(element_a, index_b, True, True)
+        self.timeline.videocomp.moveSource(element_b, index_a, True, True)
 
-        return False
+    def _condensedListChangedCb(self, unused_videocomp, clist):
+        """ add/remove the widgets """
+        gst.debug("condensed list changed in videocomp")
+        order = [self.index(self.widgets[e]) for e in clist]
+        self.reorder(order)
 
-    def _sizeAllocateCb(self, unused_layout, allocation):
-        if not self.height == allocation.height:
-            self.height = allocation.height
-            self.childheight = self.height - 2 * DEFAULT_SIMPLE_SPACING
-            self.childwidth = int(self.height / DEFAULT_SIMPLE_SIZE_RATIO)
-            self._resizeChildrens()
-        self.realWidth = allocation.width
-        if self._editingMode:
-            self.editingWidget.set_size_request(self.realWidth - 20,
-                                                self.height - 20)
-
-    def _resizeChildrens(self):
-        # resize the childrens to self.height
-        # also need to move them to their correct position
-        # TODO : check if there already at the given position
-        # TODO : check if they already have the good size
-        if self._editingMode:
-            return
-        pos = 2 * DEFAULT_SIMPLE_SPACING
-        for source in self.condensed:
-            widget = self.widgets[source]
-            if isinstance(source, TimelineFileSource):
-                widget.set_size_request(self.childwidth, self.childheight)
-                self.move(widget, pos, DEFAULT_SIMPLE_SPACING)
-                pos = pos + self.childwidth + DEFAULT_SIMPLE_SPACING
-            elif isinstance(source, SimpleTransitionWidget):
-                widget.set_size_request(self.childheight / 2, self.childheight)
-                self.move(widget, pos, DEFAULT_SIMPLE_SPACING)
-                pos = pos + self.childwidth + DEFAULT_SIMPLE_SPACING
-        newwidth = pos + DEFAULT_SIMPLE_SPACING
-        self.set_property("width", newwidth)
+    def _sourceAddedCb(self, timeline, element):
+        gst.debug("Adding new element to the layout")
+        if isinstance(element, TimelineFileSource):
+            widget = SimpleSourceWidget(element)
+            widget.connect("delete-me", self._sourceDeleteMeCb, element)
+            widget.connect("edit-me", self._sourceEditMeCb, element)
+            item = goocanvas.Widget(widget=widget)
+            item.props.width = DEFAULT_SIMPLE_ELEMENT_WIDTH
+            item.props.height = DEFAULT_SIMPLE_ELEMENT_HEIGHT
+            background = goocanvas.Rect(fill_color="gray",
+                stroke_color="gray",
+                width=DEFAULT_SIMPLE_ELEMENT_WIDTH,
+                height=DEFAULT_SIMPLE_ELEMENT_HEIGHT)
+            item = group(background, item)
+        else:
+            #TODO: implement this
+            raise Exception("Not Implemented")
+        self.widgets[element] = item
+        self.elements[item] = element
+        self.add_child(self.widgets[element])
+
+    def _sourceRemovedCb(self, timeline, element):
+        gst.debug("Removing element")
+        self.remove_child(self.widgets[element])
+        del self.elements[self.widgets[element]]
+        del self.widgets[element]
+
+    def remove_all(self):
+        HList.remove_all(self)
+        self.elements = {}
+        self.widgets = {}
 
-    ## Child callbacks
+## Child callbacks
 
     def _sourceDeleteMeCb(self, unused_widget, element):
         # remove this element from the timeline
-        self.timeline.videocomp.removeSource(element, collapse_neighbours=True)
-
+        self.timeline.videocomp.removeSource(element, 
+            collapse_neighbours=True)
+#
     def _sourceEditMeCb(self, unused_widget, element):
-        self.switchToEditingMode(element)
+        self.emit("edit-me", element)
 
-    def _sourceDragBeginCb(self, unused_widget, unused_context, element):
-        gst.log("Timeline drag beginning on %s" % element)
-        if self.draggedelement:
-            gst.error("We were already doing a DnD ???")
-        self.draggedelement = element
-        # this element is starting to be dragged
-
-    def _sourceDragEndCb(self, unused_widget, unused_context, element):
-        gst.log("Timeline drag ending on %s" % element)
-        if not self.draggedelement == element:
-            gst.error("The DnD that ended is not the one that started before ???")
-        self.draggedelement = None
-        # this element is no longer dragged
+class SimpleTimelineCanvas(goocanvas.Canvas):
+    """goocanvas.Canvas derivative which contains all the widgets used in the
+    simple timeline that should be scrolled together. It handles application event
+    like loading/saving, and external drag-and-drop events for adding objects 
+    to the canvas"""
 
-    def _editingWidgetHideMeCb(self, unused_widget):
-        self.switchToNormalMode()
-        # switch back to timeline in playground !
-        instance.PiTiVi.playground.switchToTimeline()
+    __gtype_name__ = 'SimpleTimeline'
 
+    __gsignals__ = {
+        'edit-me' : (gobject.SIGNAL_RUN_LAST,
+                     gobject.TYPE_NONE,
+                     (gobject.TYPE_PYOBJECT,))
+        }
 
 
-    ## Editing mode
+    def __init__(self, *args, **kwargs):
+        goocanvas.Canvas.__init__(self, *args, **kwargs)
+        self.props.automatic_bounds = False
 
-    def _switchEditingMode(self, source, mode=True):
-        """ Switch editing mode for the given TimelineSource """
-        gst.log("source:%s , mode:%s" % (source, mode))
+        # timeline and top level compositions
+        self.timeline = instance.PiTiVi.current.timeline
 
-        if self._editingMode == mode:
-            gst.warning("We were already in the correct editing mode : %s" % mode)
-            return
+        self.root = self.get_root_item()
+        self.items = TimelineList(self.timeline, self, spacing=10)
+        self.root.add_child(self.items)
+        self.items.connect("edit-me", self._editMeCb)
+
+        self.left = None
+        self.l_thresh = None
+        self.right = None
+        self.r_thresh = None
+        self.initial = None
 
-        if mode and not source:
-            gst.warning("You need to specify a valid TimelineSource")
-            return
 
-        if mode:
-            # switching TO editing mode
-            gst.log("Switching TO editing mode")
+        self.scale = 1.0
+        self.set_size_request(int(MINIMUM_WIDTH), int(MINIMUM_HEIGHT))
 
-            # 1. Hide all sources
-            for widget in self.widgets.itervalues():
-                widget.hide()
-                self.remove(widget)
+        instance.PiTiVi.connect("new-project-loaded",
+            self._newProjectLoadedCb)
+        instance.PiTiVi.connect("project-closed", self._projectClosedCb)
+        instance.PiTiVi.connect("new-project-loading",
+            self._newProjectLoadingCb)
+        instance.PiTiVi.connect("new-project-failed",
+            self._newProjectFailedCb)
 
-            self._editingMode = mode
+        # set a reasonable minimum size which will avoid grahics glitch
+        self.set_bounds(0, 0, DEFAULT_SIMPLE_ELEMENT_WIDTH,
+            DEFAULT_SIMPLE_ELEMENT_HEIGHT)
+
+    def _request_size(self, item, prop):
+        # no need to set size, just set the bounds
+        self.set_bounds(0, 0, self.items.width, self.items.height)
+        return True
 
-            # 2. Show editing widget
-            self.editingWidget.setSource(source)
-            self.put(self.editingWidget, 10, 10)
-            self.props.width = self.realWidth
-            self.editingWidget.set_size_request(self.realWidth - 20, self.height - 20)
-            self.editingWidget.show()
+    def _size_allocate(self, unused_layout, allocation):
+        x1, y1, x2, y2 = self.get_bounds()
+        height = y2 - y1
+
+        if height > 0:
+            self.scale = allocation.height / height
+            self.set_scale(self.scale)
+        return True
 
-        else:
-            gst.log("Switching back to normal mode")
-            # switching FROM editing mode
+## Project callbacks
 
-            # 1. Hide editing widget
-            self.editingWidget.hide()
-            self.remove(self.editingWidget)
+    def _newProjectLoadingCb(self, unused_inst, project):
+        #now we connect to the new project, so we can receive any
+        self.items.set_timeline(project.timeline)
 
-            self._editingMode = mode
+    def _newProjectLoadedCb(self, unused_inst, project):
+        assert(instance.PiTiVi.current == project)
 
-            # 2. Show all sources
-            for widget in self.widgets.itervalues():
-                self.put(widget, 0, 0)
-                widget.show()
-            self._resizeChildrens()
+    def _newProjectFailedCb(self, unused_inst, unused_reason, unused_uri):
+        self.items.set_timeline(None)
 
-    def switchToEditingMode(self, source):
-        """ Switch to Editing mode for the given TimelineSource """
-        self._switchEditingMode(source)
+    def _projectClosedCb(self, unused_pitivi, unused_project):
+        self.items.set_timeline(None)
 
-    def switchToNormalMode(self):
-        """ Switch back to normal timeline mode """
-        self._switchEditingMode(None, False)
+## Editing mode
 
+    def _editMeCb(self, timeline, element):
+        self.emit("edit-me", element)
 
 class SimpleEditingWidget(gtk.EventBox):
     """
@@ -706,10 +522,10 @@
         else:
             gst.warning("got pixbuf for a non-handled timestamp")
 
-    def _updateTextFields(self, start=-1, duration=-1):
-        if not start == -1:
+    def _updateTextFields(self, start=gst.CLOCK_TIME_NONE, duration=0):
+        if not start == gst.CLOCK_TIME_NONE:
             self.startPos.props.label = time_to_string(start)
-        if not start == -1 and not duration == -1:
+        if not start == gst.CLOCK_TIME_NONE and not duration == 0:
             self.endPos.props.label = time_to_string(start + duration)
 
     def _updateThumbnails(self):
@@ -768,7 +584,7 @@
             gst.log("end frame rewind")
             duration = self._mediaDuration - gst.SECOND
 
-        duration_max = self._source.factory.length - self._mediaStart
+        duration_max = self._source.factory.getDuration() - self._mediaStart
         duration = min(duration, duration_max)
 
         self._mediaDuration = duration
@@ -788,8 +604,6 @@
             self._mediaDuration, gst.FORMAT_TIME)
 
     def _updateStartDuration(self):
-        print (time_to_string(self._mediaStart),
-            time_to_string(self._mediaDuration))
         self._updateThumbnails()
         self._updateTextFields(self._mediaStart, self._mediaDuration)
         self._adjustControls()
@@ -805,7 +619,7 @@
             self.startRewindButton.set_sensitive(True)
 
         end = self._mediaDuration + self._mediaStart
-        assert end <= self._source.factory.length 
+        assert end <= self._source.factory.getDuration()
 
         if (self._mediaStart + gst.SECOND) >= end:
             self.startAdvanceButton.set_sensitive(False)
@@ -817,7 +631,7 @@
         else:
             self.endRewindButton.set_sensitive(True)
 
-        if end >= self._source.factory.length:
+        if end >= self._source.factory.getDuration():
             self.endAdvanceButton.set_sensitive(False)
         else:
             self.endAdvanceButton.set_sensitive(True)
@@ -1027,7 +841,7 @@
         self._update = True
         self.queue_draw()
 
-class SimpleSourceWidget(gtk.EventBox):
+class SimpleSourceWidget(gtk.HBox):
     """
     Widget for representing a source in simple timeline view
     Takes a TimelineFileSource
@@ -1046,7 +860,7 @@
 
     def __init__(self, filesource):
         """Represents filesource in the simple timeline."""
-        gtk.EventBox.__init__(self)
+        gtk.HBox.__init__(self)
 
         #TODO: create a separate thumbnailer for previewing effects
         self.filesource = filesource
@@ -1077,16 +891,20 @@
         self._popupMenu.append(deleteitem)
         self._popupMenu.append(edititem)
 
+        # Don't need this anymore
+
         # drag and drop
-        self.drag_source_set(gtk.gdk.BUTTON1_MASK,
-                             [dnd.URI_TUPLE, dnd.FILESOURCE_TUPLE],
-                             gtk.gdk.ACTION_COPY)
-        self.connect("drag_data_get", self._dragDataGetCb)
+        #self.drag_source_set(gtk.gdk.BUTTON1_MASK,
+        #                     [dnd.URI_TUPLE, dnd.FILESOURCE_TUPLE],
+        #                     gtk.gdk.ACTION_COPY)
+        #self.connect("drag_data_get", self._dragDataGetCb)
 
     def _createUI(self):
         # basic widget properties
         # TODO: randomly assign this color
-        #self.color = self.get_colormap().alloc_color("green")
+        #self.csdf
+        
+        lor = self.get_colormap().alloc_color("green")
         #self.modify_bg(gtk.STATE_NORMAL, self.color)
 
         # control decorations
@@ -1120,7 +938,7 @@
         editing.pack_start(edit, False, True)
         self.duration = gtk.Label()
         self.duration.set_markup("<small>%s</small>" %
-                beautify_length(self.filesource.factory.length))
+                beautify_length(self.filesource.factory.getDuration()))
         editing.pack_end(self.duration, False, False)
         edit.connect("clicked", self._editMenuItemCb)
 
@@ -1176,7 +994,7 @@
             vi = self.filesource.factory.video_info_stream
             height = 64 * vi.dar.denom / vi.dar.num
         smallthumb = pixbuf.scale_simple(64, height, gtk.gdk.INTERP_BILINEAR)
-        self.drag_source_set_icon_pixbuf(smallthumb)
+        #self.drag_source_set_icon_pixbuf(smallthumb)
 
     def _mediaStartDurationChangedCb(self, unused_source, start, duration):
         self._updateThumbnails()

Added: trunk/pitivi/ui/util.py
==============================================================================
--- (empty file)
+++ trunk/pitivi/ui/util.py	Fri Aug 29 16:45:42 2008
@@ -0,0 +1,689 @@
+#Copyright (C) 2008 Brandon J. Lewis
+#
+#License:
+#
+#    This library is free software; you can redistribute it and/or
+#    modify it under the terms of the GNU Lesser General Public
+#    License as published by the Free Software Foundation; either
+#    version 2 of the License, or (at your option) any later version.
+#
+#    This package is distributed in the hope that it will be useful,
+#    but WITHOUT ANY WARRANTY; without even the implied warranty of
+#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+#    Lesser General Public License for more details.
+#
+#    You should have received a copy of the GNU Lesser General Public
+#    License along with this package; if not, write to the Free Software
+#    Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
+#
+#On Debian systems, the complete text of the GNU Lesser General
+#Public License can be found in `/usr/share/common-licenses/LGPL'.
+
+import gobject
+import goocanvas
+import gtk
+import pango
+import pangocairo
+from pitivi.utils import closest_item
+
+## GooCanvas Convenience Functions
+
+def null_true(*args):
+    return True
+
+def null_false(*args):
+    return False
+
+def printall(*args):
+    print args
+
+def event_coords(canvas, event):
+    """returns the coordinates of an event"""
+    return canvas.convert_from_pixels(canvas.props.scale_x * event.x, 
+        canvas.props.scale_y * event.y)
+
+def pixel_coords(canvas, point):
+    return canvas.convert_from_pixels(canvas.props.scale_x * point[0], 
+        canvas.props.scale_y * point[1])
+
+def point_difference(p1, p2):
+    """Returns the 2-dvector difference p1 - p2"""
+    p1_x, p1_y = p1
+    p2_x, p2_y = p2
+    return (p1_x - p2_x, p1_y - p2_y)
+
+def point_sum(p1, p2):
+    """Returns the 2d vector sum p1 + p2"""
+    p1_x, p1_y = p1
+    p2_x, p2_y = p2
+    return (p1_x + p2_x, p1_y + p2_y)
+
+def point_mul(factor, point):
+    """Returns a scalar multiple factor * point"""
+    return tuple(factor * v for v in point)
+
+def pos(item):
+    """Returns a tuple x, y representing the position of the 
+    supplied goocanvas Item"""
+    return item.props.x, item.props.y
+
+def pos_change_cb(item, prop, callback, data):
+    """Used internally, don't call this function"""
+    callback(pos(item), item, *data)
+
+def size_change_cb(item, prop, callback):
+    """Used internally, don't call this function"""
+    callback(size(item))
+
+def pos_change(item, callback, *data):
+    """Connects the callback to the x and y property notificaitons.
+    Do not call this function again without calling unlink_pos_change()
+    first"""
+    item.set_data("x_sig_hdl", item.connect("notify::x", pos_change_cb,
+        callback, data))
+    item.set_data("y_sig_hdl", item.connect("notify::y", pos_change_cb,
+        callback, data))
+
+def unlink_pos_change(item):
+    """Disconnects signal handlers after calling pos_change()"""
+    item.disconnect(item.get_data("x_sig_hdl"))
+    item.disconnect(item.get_data("y_sig_hdl"))
+
+def size(item):
+    """Returns the tuple (<width>, <height>) of item"""
+    return item.props.width, item.props.height
+
+def size_change(item, callback):
+    """Connects the callback to the width, height property notifications.
+    """
+    item.set_data("w_sig_hdl", item.connect("notify::width", 
+        size_change_cb, callback))
+    item.set_data("h_sig_hdl", item.connect("notify::height", 
+        size_change_cb, callback))
+
+def unlink_size_change(item):
+    item.disconnect(item.get_data("w_sig_hdl"))
+    item.disconnect(item.get_data("h_sig_hdl"))
+
+def set_pos(item, pos):
+    """Sets the position of item given pos, a tuple of (<x>, <y>)"""
+    item.props.x, item.props.y = pos
+
+def set_size(item, size):
+    """Sets the size of the item given size, a tuple of 
+    (<width>, <height>)"""
+    item.props.width, item.props.height = size
+
+def width(item):
+    return item.props.width
+
+def height(item):
+    return item.props.height
+
+def left(item):
+    return item.props.x
+
+def right(item):
+    return item.props.x + item.props.width
+
+def center(item):
+    return point_sum(pos(item), point_mul(0.5, size(item)))
+
+def magnetize(obj, coord, magnets, deadband):
+    # remember that objects have two ends
+    left_res, left_diff = closest_item(magnets, coord)
+    right_res, right_diff = closest_item(magnets, coord + width(obj))
+
+    if left_diff <= right_diff:
+        res = left_res
+        diff = left_diff
+    else:
+        res = right_res - width(obj)
+        diff = right_diff
+    if diff <= deadband:
+        return res
+    # otherwise, return x
+    return coord
+
+def make_item(factory):
+    """Create a new goocanvas item given factory, a tuple of 
+    * <class> - the class to create
+    * <properties> - initial properties to set, such as color
+    * <data> - initial data to set
+    """
+    klass, properties, data = factory
+    ret = klass(**properties)
+    for key, value in data.items():
+        ret.set_data(key, value)
+    return ret
+
+def group(*items):
+    """Wrap all the canvas items in items in a smartgroup and return the
+    resulting smartgroup. The item's current position is the offset
+    within the smartgroup"""
+    ret = SmartGroup()
+    
+    for item in items:
+        ret.add_child(item, pos(item))
+    
+    return ret
+
+# these are callbacks for implementing "dragable object features
+def drag_start(item, target, event, canvas, start_cb, transform, cursor):
+    """A callback which starts the drag operation of a dragable 
+    object"""
+    mask = (gtk.gdk.BUTTON_PRESS_MASK | gtk.gdk.BUTTON_RELEASE_MASK 
+        | gtk.gdk.POINTER_MOTION_MASK  | gtk.gdk.POINTER_MOTION_HINT_MASK 
+        | gtk.gdk.ENTER_NOTIFY_MASK | gtk.gdk.LEAVE_NOTIFY_MASK)
+    canvas.pointer_grab(item, mask, cursor, event.time)
+    item.set_data("dragging", True)
+    if start_cb:
+        if start_cb(item):
+            drag_end(item, target, event, canvas, None)
+    if transform:
+        coords = transform(event_coords(canvas, event))
+    else:
+        coords = event_coords(canvas, event)
+    item.set_data("pendown", point_difference(pos(item), coords))
+    return True
+
+def drag_end(item, target, event, canvas, end_cb):
+    """A callback which ends the drag operation of a dragable object"""
+    item.set_data("dragging", False)
+    canvas.pointer_ungrab(item, event.time)
+    if end_cb:
+        end_cb(item)
+    return True
+
+def drag_move(item, target, event, canvas, transform, move_cb):
+    """A callback which handles updating the position during a drag
+    operation"""
+    if item.get_data("dragging"):
+        pos = point_sum(item.get_data("pendown"), 
+            event_coords(canvas, event))
+        if transform:
+            move_cb(item, transform(pos))
+            return True
+        move_cb(item, pos)
+        return True
+    return False
+
+def make_dragable(canvas, item, transform=None, start=None, end=None, 
+    moved=set_pos, cursor=None):
+    """Make item dragable with respect to the canvas. Call this 
+    after make_selectable, or it will prevent the latter from working.
+
+        - canvas : the goocanvas.Canvas that contains item
+        - item : the item which will become dragable
+        - transform : callback which preforms arbitrary transformation
+            on mouse coordinates, or None
+        - start : callback to prepare object for draging, or None.
+            if start() returns True, drag will be aborted and end()
+            will not be called.
+        - end : callback to clean up after draging, or None
+        - moved : what to do with coordinates after transform() is called,
+            default is set_pos(item, coords)
+    """
+    item.set_data("dragging", False)
+    dwn = item.connect("button_press_event", drag_start, canvas, start, 
+        transform, cursor)
+    up = item.connect("button_release_event", drag_end, canvas, end)
+    mv = item.connect("motion_notify_event", drag_move, canvas, transform,
+        moved)
+    item.set_data("drag_sigids", (up, dwn, mv))
+
+def unmake_dragable(item):
+    signals = item.get_data("drag_sigids")
+    if signals:
+        for sig in signals:
+            item.disconnect(sig)
+
+def make_resizable(canvas, item, transform=None, start=None, stop=None, 
+    moved=None):
+    pass
+
+def unmake_resizable(item):
+    pass
+
+def normalize_rect(mouse_down, cur_pos):
+    """Given two points, representing the upper left and bottom right 
+    corners of a rectangle (the order is irrelevant), return the tuple
+    ((x,y), (width, height))"""
+    w, h = point_difference(cur_pos, mouse_down)
+    x, y = mouse_down
+
+    if w < 0:
+        w = abs(w)
+        x -= w
+    if h < 0:
+        h = abs(h)
+        y -= h
+
+    return (x, y), (w, h)
+
+def object_select_cb(item, target, event, canvas, changed_cb):
+    prev = canvas.get_data("selected_objects")
+    if item in prev:
+        return
+    if (event.state & gtk.gdk.SHIFT_MASK):
+        prev.add(item)
+        changed_cb(prev, set())
+    else:
+        selected = set()
+        selected.add(item)
+        canvas.set_data("selected_objects", selected)
+        changed_cb(selected, prev)
+    return False
+
+def make_selectable(canvas, object):
+    """Make the object selectable with respect to canvas. This means
+    that the item will be included in the current selection, and that
+    clicking the object will select it. Must be called before 
+    make_dragable, as it will block the action of this handler"""
+    object.set_data("selectable", True)
+    object.connect("button_press_event", object_select_cb, canvas,
+        canvas.get_data("selection_callback"))
+
+def delete_from_selection(canvas, item):
+    selected = canvas.get_data("selected_objects")
+    set_selection(canvas, selected - set([item]))
+
+def set_selection(canvas, new):
+    prev = canvas.get_data("selected_objects")
+    deselected = prev - new
+    canvas.set_data("selected_objects", new)
+    canvas.get_data("selection_callback")(new, deselected)
+
+def objects_under_marquee(event, canvas, overlap):
+    pos, size = normalize_rect(canvas.mouse_down, event_coords(
+        canvas, event))
+    bounds = goocanvas.Bounds(*(pos + point_sum(pos, size)))
+    selected = canvas.get_items_in_area(bounds, True, overlap, 
+        True)
+    if selected:
+        return set((found for found in selected if 
+            found.get_data("selectable")))
+    return set()
+
+def selection_start(item, target, event, canvas, marquee):
+    root = canvas.get_root_item()
+    root.add_child(marquee)
+    cursor = event_coords(canvas, event)
+    set_pos(marquee, cursor)
+    canvas.selecting = True
+    canvas.mouse_down = cursor
+    set_pos(marquee, cursor) 
+    set_size(marquee, (0, 0))
+    return True
+
+def selection_end(item, target, event, canvas, marquee, overlap, changed_cb):
+    canvas.selecting = False
+    marquee.remove()
+    prev = canvas.get_data("selected_objects")
+    selected = objects_under_marquee(event, canvas, overlap)
+    canvas.set_data("selected_objects", selected)
+    if changed_cb:
+        changed_cb(selected, prev.difference(selected))
+    return True
+
+def selection_drag(item, target, event, canvas, marquee):
+    if canvas.selecting:
+        pos_, size_ = normalize_rect(canvas.mouse_down, 
+            event_coords(canvas, event))
+        set_size(marquee, size_)
+        set_pos(marquee, pos_)
+        return True
+    return False
+
+
+def manage_selection(canvas, marquee, overlap, changed_cb=None):
+    """Keep track of the current selection in canvas, including
+    * providing a rectangular selection marquee
+    * tracking specific canvas objects
+    Note: objects must be made selectable by calling make_selectable()
+    on the object before they will be reported by any selection changes
+    - overlap: True if you want items that merely intersect the 
+        data field to be considered selected.
+    - marquee: a goocanvas.Rectangle() to be used as the selection 
+        marquee (really, any canvas item with x, y, width, height 
+        properties). This object should not already be added to the
+        canvas.
+    - changed_cb: a callback with signature (selected, deselected)
+      """
+
+    canvas.selecting = False
+    canvas.mouse_down = None
+    canvas.set_data("selected_objects", set())
+    canvas.set_data("selection_callback", changed_cb)
+    root = canvas.get_root_item()
+    root.connect("button_press_event", selection_start, canvas, marquee)
+    root.connect("button_release_event", selection_end, canvas, marquee, overlap, changed_cb)
+    root.connect("motion_notify_event", selection_drag, canvas, marquee)
+
+class SmartGroup(goocanvas.Group):
+    """Extends goocanvas.Group() with 
+    through gobject properties x, y, and width/height"""
+    __gtype_name__ = 'SmartGroup'
+
+    x = gobject.property(type=float, default=0)
+    y = gobject.property(type=float, default=0)
+    width = gobject.property(type=float, default=0)
+    height = gobject.property(type=float, default=0)
+
+    def __init__(self, canvas=None, background=None, *args, **kwargs):
+        goocanvas.Group.__init__(self, *args, **kwargs)
+        self.children = {}
+        self.signals = {}
+        self.connect("notify::x", self.move_x_children)
+        self.connect("notify::y", self.move_y_children)
+        self.set_canvas(canvas)
+        self.background = None
+        self.set_background(background)
+
+    def set_background(self, bg):
+        if self.background:
+            self.background.remove()
+            goocanvas.Group.add_child(self, bg, 0)
+        self.background = bg
+        #TODO: move background beneath lowest item
+
+    def set_canvas(self, canvas):
+        self.canvas = canvas
+
+    def move_x_children(self, object, prop):
+        if self.background:
+            self.background.props.x = self.x
+        for child, (x, y) in self.children.items():
+            child.set_property('x', self.x + x)
+
+    def move_y_children(self, object, prop):
+        if self.background:
+            self.background.props.y = self.y
+        for child, (x, y) in self.children.items():
+            child.set_property('y', self.y + y)
+
+    def update_width(self, obj, prop):
+        def compute(c, p):
+            return (c.get_property('width') + p[0])
+        widths = (compute(c, p) for c, p in self.children.items())
+        self.width = max(widths) if len(self.children) else float(0)
+        if self.background:
+            self.background.props.width = self.width
+
+    def update_height(self, obj, prop):
+        def compute(c, p):
+            return (c.get_property('height') + p[1])
+        heights = (compute(c, p) for c, p in self.children.items())
+        self.height = max(heights) if len(self.children) else float(0)
+        if self.background:
+            self.background.props.height = self.height
+
+    def set_child_pos(self, child, pos_):
+        set_pos(child, point_sum(pos(self), pos_))
+        self.children[child] = pos_
+
+    def add_child(self, child, p=None):
+        goocanvas.Group.add_child(self, child)
+        cw = child.connect("notify::width", self.update_width)
+        ch = child.connect("notify::height", self.update_height)
+        self.signals[child] = (cw, ch)
+        if not p:
+            self.children[child] = pos(child)
+        else:
+            self.set_child_pos(child, p)
+        self.update_width(None, None)
+        self.update_height(None, None)
+
+    def remove_child(self, child):
+        goocanvas.Group.remove_child(self, child)
+        for s in self.signals[child]:
+            child.disconnect(s)
+        del self.children[child]
+        self.update_width(None, None)
+        self.update_height(None, None)
+
+class Text(goocanvas.ItemSimple, goocanvas.Item):
+    '''A replacement for the stock goocanvas.Text widget, which
+    doesn't have a height property, and the width property doesn't do
+    quite what you'd expect it might. To set where the text should
+    wrap, we provide this wrap_width, property. The width, height
+    property clip the text appropriately.'''
+
+    __gtype_name__ = 'SmartText'
+
+    alignment = gobject.property(type=int)
+    font = gobject.property(type=str)
+    font_desc = gobject.property(type=gobject.TYPE_PYOBJECT,default=None)
+    height = gobject.property(type=float)
+    justification = gobject.property(type=int)
+    text = gobject.property(type=str, default="")
+    use_markup = gobject.property(type=bool, default=False)
+    width = gobject.property(type=float)
+    wrap_width = gobject.property(type=float)
+    x = gobject.property(type=float)
+    y = gobject.property(type=float)
+
+    def __init__(self, *args, **kwargs):
+        super(Text, self).__init__(*args, **kwargs)
+        self.connect("notify::text", self.do_set_text)
+        self.connect("notify::font", self.do_set_font)
+
+    def do_simple_create_path(self, cr):
+        context = pangocairo.CairoContext(cr)
+        cr.move_to(self.x, self.y)
+        layout = context.create_layout()
+        layout.set_alignment(self.alignment)
+        layout.set_font_description(self.font_desc)
+        if not self.use_markup:
+            layout.set_text(self.text)
+        else:
+            layout.set_markup(self.text)
+        context.show_layout(layout)
+
+    @gobject.property
+    def layout(self):
+        return self._layout
+
+    def do_set_font(self, *args):
+        self.font_desc = pango.FontDescription(self.font)
+        self.changed(True)
+
+    def do_set_text(self, *args):
+        self.changed(True)
+ 
+class List(SmartGroup):
+    __gytpe_name__ = 'List'
+
+    spacing = gobject.property(type=float, default=5.0)
+    reorderable = gobject.property(type=bool, default=False)
+
+    def __len__(self):
+        return len(self.order)
+
+    def __iter__(self):
+        return self.order.__iter__()
+
+    def __init__(self, *args, **kwargs):
+        SmartGroup.__init__(self, *args, **kwargs)
+        self.cur_pos = 0
+        self.order = []
+        if kwargs.has_key("spacing"):
+            self.spacing = kwargs["spacing"]
+        self.draging = None
+        self.right = None
+        self.left = None
+        self.initial = None
+        self.l_thresh = None
+        self.r_thresh = None
+        self.connect("notify::spacing", self._set_spacing)
+        self.connect("notify::reorderable", self._set_reorderable)
+    
+    def _set_spacing(self, unused_object, unused_property):
+        self.tidy()
+
+    def _set_reorderable(self, unused_object, unused_property):
+        if self.reorderable:
+            for child in self.order:
+                self.make_reorderable(child)
+        else:
+            for child in self.order:
+                self.unmake_reorderable(child)
+    
+    def end(self, child):
+        return self.position(child) + self.dimension(child)
+
+    def tidy(self):
+        cur = 0
+        i = 0
+        for child in self.order:
+            self.set_child_pos(child, self.cur(cur))
+            child.set_data("index", i)
+            cur += self.spacing + self.dimension(child)
+            i += 1
+        self.cur_pos = cur
+        if self.draging:
+            self._set_drag_thresholds()
+    
+    def item_at(self, index):
+        return self.order[index]
+
+    def index(self, child):
+        return child.get_data("index")
+
+    def point_to_index(self, point):
+        x, y = point
+        bounds = goocanvas.Bounds(x, y, x, y)
+        items = self.canvas.get_items_in_area(bounds, True, True, True)
+        if items:
+            return [i for i in items if i.get_data("index")][0]
+        return None
+
+    def _reorder(self, new_order):
+        order = []
+        for index in new_order:
+            order.append(self.order[index])
+        self.order = order
+
+    def reorder(self, new_order):
+        self._reorder(new_order)
+        self.tidy()
+
+    def _child_drag_start(self, child):
+        child.raise_(None)
+        self.draging = child
+        self.dwidth = self.dimension(child)
+        self._set_drag_thresholds()
+        return True
+
+    def _set_drag_thresholds(self):
+        index = self.draging.get_data("index")
+        self.left = None
+        self.right = None
+        if index > 0:
+            self.left = self.order[index - 1]
+            self.l_thresh = (self.end(self.left) - 0.5 * self.dimension(self.left)
+                + self.spacing)
+        if index < len(self.order) - 1:
+            self.right = self.order[index + 1]
+            self.r_thresh = (self.position(self.right) + 0.5 * self.dimension(self.right)
+                - self.dimension(self.draging) + self.spacing)
+
+    def _child_drag_end(self, child):
+        self.left = None
+        self.right = None
+        self.initial = None
+        self.draging = None
+        self.tidy()
+        return True
+
+    def swap(self, a, b):
+        a_index = a.get_data("index")
+        b_index = b.get_data("index")
+        self.order[a_index] = b
+        self.order[b_index] = a
+        a.set_data("index", b_index)
+        b.set_data("index", a_index)
+        self.tidy()
+        return True
+
+    def _child_drag(self, pos_):
+        coord = self.coord(pos_)
+        coord = (min(self.dimension(self) - self.dimension(self.draging),  max(0, coord)))
+        if self.left:
+            if coord <= self.l_thresh:
+               self.swap(self.draging, self.left)
+        if self.right:
+            if coord >= self.r_thresh:
+               self.swap(self.draging, self.right)
+        return self.cur(coord)
+
+    def remove_child(self, child):
+        SmartGroup.remove_child(self, child)
+        self.order.remove(child)
+        if self.reorderable:
+            self.unmake_reorderable(child)
+        self.tidy()
+
+    def remove_all(self):
+        while len(self.order):
+            self.remove_child(self.order[0])
+    
+    def make_reorderable(self, child):
+        make_dragable(self.canvas, child, self._child_drag,
+            self._child_drag_start, self._child_drag_end)
+
+    def unmake_reorderable(self, child):
+        unmake_dragable(child)
+
+    def add_child(self, child):
+        SmartGroup.add_child(self, child, self.cur(self.cur_pos))
+        self.cur_pos += self.spacing + self.dimension(child)
+        self.order.append(child)
+        child.set_data("index", len(self.order) - 1)
+        if self.reorderable:
+            self.make_reorderable(child)
+
+    def add(self, child):
+        self.add_child(child)
+
+    def insert_child(self, child, index):
+        SmartGroup.add_child(self, child, self.cur(self.cur_pos))
+        self.order.insert(index, child)
+        self.tidy()
+
+class VList(List):
+    __gtype_name__ = 'VList'
+
+    def __init__(self, *args, **kwargs):
+        List.__init__(self, *args, **kwargs)
+    
+    def cur(self, value):
+        return (0, value)
+
+    def coord(self, point):
+        return point[1]
+
+    def position(self, child):
+        return child.props.y
+
+    def dimension(self, child):
+        return child.props.height
+
+class HList(List):
+    __gtype_name__ = 'HList'
+
+    def __init__(self, *args, **kwargs):
+        List.__init__(self, *args, **kwargs)
+
+    def coord(self, point):
+        return point[0]
+
+    def cur(self, value):
+        return (value, 0)
+
+    def position(self, child):
+        return child.props.x
+
+    def dimension(self, child):
+        return child.props.width
+

Modified: trunk/pitivi/ui/viewer.py
==============================================================================
--- trunk/pitivi/ui/viewer.py	(original)
+++ trunk/pitivi/ui/viewer.py	Fri Aug 29 16:45:42 2008
@@ -34,17 +34,7 @@
 from pitivi.signalgroup import SignalGroup
 
 from gettext import gettext as _
-
-def time_to_string(value):
-    if value == -1:
-        return "--:--:--.---"
-    ms = value / gst.MSECOND
-    sec = ms / 1000
-    ms = ms % 1000
-    mins = sec / 60
-    sec = sec % 60
-    hours = mins / 60
-    return "%02d:%02d:%02d.%03d" % (hours, mins, sec, ms)
+from pitivi.utils import time_to_string
 
 class PitiviViewer(gtk.VBox):
     """ Pitivi's viewer widget with controls """
@@ -74,7 +64,7 @@
         # signal for timeline duration changes : (composition, sigid)
         self._timelineDurationChangedSigId = (None, None)
 
-        self._addTimelineToPlayground()
+
 
     def _connectToProject(self, project):
         """Connect signal handlers to a project.
@@ -87,6 +77,10 @@
                                      None, self._tmpIsReadyCb)
         self.project_signals.connect(project, "settings-changed",
                                      None, self._settingsChangedCb)
+        # we should add the timeline to the playground here, so
+        # that the new timeline bin will be added to the
+        # playground when the project loads
+        self._addTimelineToPlayground()
 
     def _createUi(self):
         """ Creates the Viewer GUI """
@@ -352,8 +346,14 @@
         self._connectToProject(project)
 
     def _addTimelineToPlayground(self):
-        instance.PiTiVi.playground.addPipeline(instance.PiTiVi.current.getBin())
-
+        # remove old timeline before proceeding
+        pg = instance.PiTiVi.playground
+        timeline = pg.getTimeline()
+        if timeline:
+            pg.switchToDefault()
+            pg.removePipeline(timeline)
+        # add current timeline
+        pg.addPipeline(instance.PiTiVi.current.getBin())
 
     ## Control gtk.Button callbacks
 
@@ -404,7 +404,7 @@
                 self._timelineDurationChangedSigId = (smartbin.project.timeline.videocomp,
                                                       sigid)
             else:
-                self.posadjust.upper = float(smartbin.factory.length)
+                self.posadjust.upper = float(smartbin.factory.getDuration())
                 if not self._timelineDurationChangedSigId == (None, None):
                     obj, sigid = self._timelineDurationChangedSigId
                     obj.disconnect(sigid)

Modified: trunk/pitivi/utils.py
==============================================================================
--- trunk/pitivi/utils.py	(original)
+++ trunk/pitivi/utils.py	Fri Aug 29 16:45:42 2008
@@ -22,7 +22,18 @@
 
 # set of utility functions
 
-import gst
+import gst, bisect
+
+def time_to_string(value):
+    if value == gst.CLOCK_TIME_NONE:
+        return "--:--:--.---"
+    ms = value / gst.MSECOND
+    sec = ms / 1000
+    ms = ms % 1000
+    mins = sec / 60
+    sec = sec % 60
+    hours = mins / 60
+    return "%02d:%02d:%02d.%03d" % (hours, mins, sec, ms)
 
 def bin_contains(bin, element):
     """ Returns True if the bin contains the given element, the search is recursive """
@@ -36,3 +47,58 @@
         if isinstance(elt, gst.Bin) and bin_contains(elt, element):
             return True
     return False
+
+# Python re-implementation of binary search algorithm found here:
+# http://en.wikipedia.org/wiki/Binary_search
+#
+# This is the iterative version without the early termination branch, which
+# also tells us the element of A that are nearest to Value, if the element we
+# want is not found. This is useful for implementing edge snaping in the UI,
+# where we repeatedly search through a list of control points for the one
+# closes to the cursor. Because we don't care whether the cursor position
+# matches the list, this function returns the index of the lement closest to
+# value in the array.
+
+def binary_search(A, value):
+    low = 0
+    high = len(A)
+    while (low < high): 
+        mid = (low + high)/2
+        if (A[mid] < value):
+            low = mid + 1
+        else:
+            #can't be high = mid-1: here A[mid] >= value,
+            #so high can't be < mid if A[mid] == value
+            high = mid; 
+    return low
+
+# Returns the element of seq nearest to item, and the difference between them
+
+def closest_item(seq, item):
+    index = bisect.bisect(seq, item)
+    if index >= len(seq):
+        index = len(seq) - 1
+    res = seq[index]
+    diff = abs(res - item)
+
+    # binary_search returns largest element closest to item.
+    # if there is a smaller element...
+    if index - 1 >= 0:
+        res_a = seq[index - 1]
+        # ...and it is closer to the pointer...
+        diff_a = abs(res_a - item)
+        if diff_a < diff:
+            # ...use it instead.
+            res = res_a
+            diff = diff_a
+    return res, diff
+
+def argmax(func, seq):
+    """return the element of seq that gives max(map(func, seq))"""
+    def compare(a, b):
+        if a[0] > b[0]:
+            return a
+        return b
+    # using a generator expression here should save memory
+    objs = ((func(x), x) for x in seq)
+    return reduce(compare, objs)[1]

Modified: trunk/tests/Makefile.am
==============================================================================
--- trunk/tests/Makefile.am	(original)
+++ trunk/tests/Makefile.am	Fri Aug 29 16:45:42 2008
@@ -1,10 +1,13 @@
 tests = \
+	testHList.py			\
 	test_basic.py			\
+	test_binary_search.py		\
 	test_file_load_save.py		\
 	test_timeline_composition.py	\
 	test_timeline_objects.py	\
 	test_serializable.py		\
-	test_timeline_source.py
+	test_timeline_source.py		\
+	testcomplex.py
 
 EXTRA_DIST = $(tests) runtests.py common.py
 

Modified: trunk/tests/common.py
==============================================================================
--- trunk/tests/common.py	(original)
+++ trunk/tests/common.py	Fri Aug 29 16:45:42 2008
@@ -123,3 +123,5 @@
         TestObjectFactory.__init__(self, *args, **kwargs)
         self.length = duration
 
+    def getDuration(self):
+        return self.length

Modified: trunk/tests/runtests.py
==============================================================================
--- trunk/tests/runtests.py	(original)
+++ trunk/tests/runtests.py	Fri Aug 29 16:45:42 2008
@@ -3,7 +3,7 @@
 import sys
 import unittest
 
-SKIP_FILES = ['common', 'runtests']
+SKIP_FILES = ['common', 'runtests', 'testcomplex', 'testHList', 'testmagets']
 
 def gettestnames(which):
     if not which:

Added: trunk/tests/testHList.py
==============================================================================
--- (empty file)
+++ trunk/tests/testHList.py	Fri Aug 29 16:45:42 2008
@@ -0,0 +1,82 @@
+import gobject
+gobject.threads_init()
+import pygtk
+pygtk.require("2.0")
+import gtk
+import goocanvas
+from itertools import cycle
+from util import *
+
+LABELS = "one two three four five six seven".split()
+
+box_a = (
+    goocanvas.Rect,
+    {
+        "width" : 50,
+        "height" : 30, 
+        "stroke_color" : "black",
+        "fill_color_rgba" : 0x556633FF
+    },
+    {}
+)
+box_b = (
+    goocanvas.Rect,
+    {
+        "width" : 75,
+        "height" : 30,
+        "stroke_color" : "black",
+        "fill_color_rgba" : 0x663333FF,
+    },
+    {}
+)
+
+box = cycle((box_a, box_b))
+
+label = (
+    Text,
+    {
+        "font" : "Sans 9",
+        "text" : "will be replaced",
+        "fill_color_rgba" : 0x66AA66FF,
+        "anchor" : gtk.ANCHOR_CENTER
+    },
+    {}
+)
+
+def null_true(*args):
+    return True
+
+def null_false(*args):
+    return False
+
+def make_box(text):
+    b = make_item(box.next())
+    t = make_item(label)
+    t.props.text = text
+    set_pos(t, center(b))
+    return group(b, t)
+
+def make_widget(text):
+    b = gtk.Label(text)
+    d = gtk.EventBox()
+    d.add(b)
+    e = goocanvas.Rect(width=75, height=50, visibility=False)
+    return group(goocanvas.Widget(widget=d, width=75,
+        height=50), e)
+
+c = goocanvas.Canvas()
+t = HList(canvas=c)
+c.get_root_item().add_child(t)
+for word in LABELS:
+    t.add(make_box(word))
+t.reorderable = True
+s = gtk.ScrolledWindow()
+s.set_policy(gtk.POLICY_ALWAYS, gtk.POLICY_NEVER)
+s.add(c)
+w = gtk.Window()
+w.add(s)
+w.show_all()
+w.connect("destroy", gtk.main_quit)
+gtk.main()
+
+

Added: trunk/tests/test_binary_search.py
==============================================================================
--- (empty file)
+++ trunk/tests/test_binary_search.py	Fri Aug 29 16:45:42 2008
@@ -0,0 +1,44 @@
+import unittest
+import pitivi
+from pitivi.pitivi import Pitivi
+from pitivi.utils import binary_search
+
+class BasicTest(unittest.TestCase):
+    """
+    Basic test to create the proper creation of the Pitivi object
+    """
+
+    def testBinarySearch(self):
+        # binary_search always returns an index, so we do the comparison here
+        def found(A, result, value):
+            if ((result < len(A)) and (A[result] == value)):
+                return result
+            else:
+                return False
+
+        for offset in xrange(1, 5):
+            for length in xrange(1, 2049, 300):
+                A = [i * offset for i in xrange(0, length)]
+                
+## check negative hits
+ 
+                # search value too low
+                # error if value is found
+                # if search returns non-negative index, fail
+                value = A[0] - 1
+                self.assertFalse(found(A, binary_search(A, value), value))
+ 
+                # search value too high
+                # error if value is found
+                # if search returns non-negative index, fail
+                value = A[-1] + 1
+                self.assertFalse(found(A, binary_search(A, value), value))
+ 
+## check positive hits
+                for i, a in enumerate(A):
+                    # error if value is NOT found
+                    # if search does not return correct value, fail
+                    self.assertEquals(binary_search(A, A[i]), i)
+
+if __name__ == "__main__":
+    unittest.main()

Added: trunk/tests/testcomplex.py
==============================================================================
--- (empty file)
+++ trunk/tests/testcomplex.py	Fri Aug 29 16:45:42 2008
@@ -0,0 +1,98 @@
+import gobject
+gobject.threads_init()
+import gst
+import pygtk
+pygtk.require("2.0")
+import gtk
+import goocanvas
+import sys
+import os
+from itertools import cycle
+from util import *
+
+
+root = os.path.abspath(os.path.curdir)
+print root
+if not root in sys.path:
+    sys.path.insert(0, root)
+
+from complextimeline import ComplexTrack
+from pitivi.timeline.objects import MEDIA_TYPE_VIDEO
+
+SOURCES = (
+    ("source1", 300 * gst.SECOND),
+    ("source2", 200 * gst.SECOND),
+    ("source3", 10 * gst.SECOND),
+)
+
+class TestComposition(gobject.GObject):
+    __gtype_name__ = "TestComposition"
+    __gsignals__ = {
+        "source-added" : (gobject.SIGNAL_RUN_LAST,
+            gobject.TYPE_NONE,
+            (gobject.TYPE_PYOBJECT, )),
+        "source-removed" : (gobject.SIGNAL_RUN_LAST,
+            gobject.TYPE_NONE,
+            (gobject.TYPE_PYOBJECT, )),
+    }
+    
+    def __init__(self, *args, **kwargs):
+        gobject.GObject.__init__(self, *args, **kwargs)
+        self.media_type = MEDIA_TYPE_VIDEO
+
+    def addSource(self, source, position):
+        self.emit("source-added", source)
+
+    def removeSource(self, source):
+        self.emit("source-removed", source)
+
+class TestTimelineObject(gobject.GObject):
+    __gtype_name__ = "TestObject"
+    __gsignals__ = {
+        "start-duration-changed" : (gobject.SIGNAL_RUN_LAST,
+            gobject.TYPE_NONE,
+            (gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT, )),
+    }
+
+    class Factory:
+        name=None
+
+    def __init__(self, name, start, duration):
+        gobject.GObject.__init__(self)
+        self.start = start
+        self.duration = duration
+        self.factory = self.Factory()
+        self.factory.name=name
+
+    def setStartDurationTime(self, start=-1, duration=-1):
+        if start != -1:
+            self.start = start
+        if duration != -1:
+            self.duration = duration
+        self.emit("start-duration-changed", self.start, self.duration)
+
+c = goocanvas.Canvas()
+t = ComplexTrack(c)
+model = TestComposition()
+t.set_composition(model)
+c.get_root_item().add_child(t)
+cur = long(0)
+for name, duration in SOURCES:
+    model.addSource(TestTimelineObject(name, cur, duration), None)
+    cur += duration
+print t.width
+c.set_size_request(int(t.width), int(t.height))
+s = gtk.ScrolledWindow()
+s.set_policy(gtk.POLICY_ALWAYS, gtk.POLICY_NEVER)
+s.add(c)
+z = gtk.HScale(t.get_zoom_adjustment())
+b = gtk.VBox()
+b.pack_start(s, True, True)
+b.pack_start(z, False, False)
+w = gtk.Window()
+w.add(b)
+w.show_all()
+w.connect("destroy", gtk.main_quit)
+gtk.main()
+
+

Added: trunk/tests/testmagnets.py
==============================================================================
--- (empty file)
+++ trunk/tests/testmagnets.py	Fri Aug 29 16:45:42 2008
@@ -0,0 +1,51 @@
+import sys, os, gtk, goocanvas
+
+root = os.path.abspath(os.path.curdir)
+print root
+if not root in sys.path:
+    sys.path.insert(0, root)
+
+from pitivi.ui.util import *
+from pitivi.utils import binary_search
+
+RECT = (
+    goocanvas.Rect,
+    {
+        "width" : 50,
+        "height" : 50,
+        "fill-color" : "blue"
+    },
+    {}
+)
+
+LINE = (
+    goocanvas.Rect,
+    {
+        "width" : 1,
+        "height" : 50,
+        "line-width" : 0.5
+    },
+    {}
+)
+magnets = [0, 100, 230, 500, 600]
+deadband = 7
+
+def transform(pos):
+    x, y = pos
+    global magnets, deadband, i
+    return (magnetize(i, x, magnets, deadband), 0)
+
+c = goocanvas.Canvas()
+c.set_bounds(0, 0, 700, 100)
+i = make_item(RECT)
+c.get_root_item().add_child(i)
+make_dragable(c, i, transform=transform)
+for m in magnets:
+    l = make_item(LINE)
+    l.props.x = m
+    c.get_root_item().add_child(l)
+w = gtk.Window()
+w.connect("destroy", gtk.main_quit)
+w.add(c)
+w.show_all()
+gtk.main()



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