pitivi r1243 - in trunk: . pitivi pitivi/elements pitivi/timeline pitivi/ui po tests
- From: edwardrv svn gnome org
- To: svn-commits-list gnome org
- Subject: pitivi r1243 - in trunk: . pitivi pitivi/elements pitivi/timeline pitivi/ui po tests
- Date: Fri, 29 Aug 2008 16:45:42 +0000 (UTC)
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]