1
# PiTiVi , Non-linear video editor
3
# pitivi/ui/timelineobjects.py
5
# Copyright (c) 2005, Edward Hervey <bilboed@bilboed.com>
7
# This program is free software; you can redistribute it and/or
8
# modify it under the terms of the GNU Lesser General Public
9
# License as published by the Free Software Foundation; either
10
# version 2.1 of the License, or (at your option) any later version.
12
# This program is distributed in the hope that it will be useful,
13
# but WITHOUT ANY WARRANTY; without even the implied warranty of
14
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
15
# Lesser General Public License for more details.
17
# You should have received a copy of the GNU Lesser General Public
18
# License along with this program; if not, write to the
19
# Free Software Foundation, Inc., 59 Temple Place - Suite 330,
20
# Boston, MA 02111-1307, USA.
23
Simple view timeline widgets
27
from urllib import unquote
32
import pitivi.instance as instance
33
from pitivi.timeline.source import TimelineFileSource, TimelineSource, TimelineBlankSource
34
from pitivi.timeline.effects import TimelineTransition
35
from pitivi.timeline.objects import MEDIA_TYPE_AUDIO, MEDIA_TYPE_VIDEO
36
from pitivi.configure import get_pixmap_dir
37
import pitivi.dnd as dnd
38
from pitivi.signalgroup import SignalGroup
40
from sourcefactories import beautify_length
41
from gettext import gettext as _
43
# Default width / height ratio for simple elements
44
DEFAULT_SIMPLE_SIZE_RATIO = 1.0 # default width / height ratio
46
# Default simple elements size
47
DEFAULT_SIMPLE_ELEMENT_WIDTH = 100
48
DEFAULT_SIMPLE_ELEMENT_HEIGHT = DEFAULT_SIMPLE_ELEMENT_WIDTH * DEFAULT_SIMPLE_SIZE_RATIO
50
# Default spacing between/above elements in simple timeline
51
DEFAULT_SIMPLE_SPACING = 10
53
# Simple Timeline's default values
54
DEFAULT_HEIGHT = DEFAULT_SIMPLE_ELEMENT_HEIGHT + 2 * DEFAULT_SIMPLE_SPACING
55
DEFAULT_WIDTH = 3 * DEFAULT_SIMPLE_SPACING # borders (2) + one holding place
56
MINIMUM_HEIGHT = DEFAULT_HEIGHT
57
MINIMUM_WIDTH = 3 * DEFAULT_HEIGHT
59
class SimpleTimeline(gtk.Layout):
60
""" Simple Timeline representation """
62
def __init__(self, **kw):
63
gobject.GObject.__init__(self, **kw)
65
self.hadjustment = self.get_property("hadjustment")
67
# timeline and top level compositions
68
self.timeline = instance.PiTiVi.current.timeline
69
self.condensed = self.timeline.videocomp.condensed
71
# TODO : connect signals for when the timeline changes
73
# widgets correspondance dictionnary
74
# MAPPING timelineobject => widget
78
# True when in editing mode
79
self._editingMode = False
80
self.editingWidget = SimpleEditingWidget()
81
self.editingWidget.connect("hide-me", self._editingWidgetHideMeCb)
83
# Connect to timeline. We must remove and reset the callbacks when
85
self.project_signals = SignalGroup()
86
self._connectToTimeline(instance.PiTiVi.current.timeline)
87
instance.PiTiVi.connect("new-project", self._newProjectCb)
90
self.width = int(DEFAULT_WIDTH)
91
self.height = int(DEFAULT_HEIGHT)
92
self.realWidth = 0 # displayed width of the layout
93
self.childheight = int(DEFAULT_SIMPLE_ELEMENT_HEIGHT)
94
self.set_size_request(int(MINIMUM_WIDTH), int(MINIMUM_HEIGHT))
95
self.set_property("width", int(DEFAULT_WIDTH))
96
self.set_property("height", int(DEFAULT_HEIGHT))
99
self.connect("expose-event", self._exposeEventCb)
100
self.connect("notify::width", self._widthChangedCb)
101
self.connect("size-allocate", self._sizeAllocateCb)
102
self.connect("realize", self._realizeCb)
105
self.drag_dest_set(gtk.DEST_DEFAULT_DROP | gtk.DEST_DEFAULT_MOTION,
106
[dnd.FILESOURCE_TUPLE],
108
self.connect("drag-data-received", self._dragDataReceivedCb)
109
self.connect("drag-leave", self._dragLeaveCb)
110
self.connect("drag-motion", self._dragMotionCb)
111
self.slotposition = -1
113
self.draggedelement = None
120
def _connectToTimeline(self, timeline):
121
self.timeline = timeline
122
self.condensed = self.timeline.videocomp.condensed
124
self.project_signals.connect(self.timeline.videocomp,
125
"condensed-list-changed",
126
None, self._condensedListChangedCb)
128
def _newProjectCb(self, unused_pitivi, project):
129
assert(instance.PiTiVi.current == project)
131
for widget in self.widgets.itervalues():
135
self._connectToTimeline(instance.PiTiVi.current.timeline)
138
## Timeline callbacks
140
def _condensedListChangedCb(self, unused_videocomp, clist):
141
""" add/remove the widgets """
142
gst.debug("condensed list changed in videocomp")
144
current = self.widgets.keys()
145
self.condensed = clist
147
new = [x for x in clist if not x in current]
148
removed = [x for x in current if not x in clist]
152
# add the widget to self.widget
153
gst.debug("Adding new element to the layout")
154
if isinstance(element, TimelineFileSource):
155
widget = SimpleSourceWidget(element)
156
widget.connect("delete-me", self._sourceDeleteMeCb, element)
157
widget.connect("edit-me", self._sourceEditMeCb, element)
158
widget.connect("drag-begin", self._sourceDragBeginCb, element)
159
widget.connect("drag-end", self._sourceDragEndCb, element)
161
widget = SimpleTransitionWidget(element)
162
self.widgets[element] = widget
163
self.put(widget, 0, 0)
167
for element in removed:
168
self.remove(self.widgets[element])
169
del self.widgets[element]
171
self._resizeChildrens()
176
def _getNearestSourceSlot(self, x):
178
returns the nearest file slot position available for the given position
179
Returns the value in condensed list position
180
Returns n , the element before which it should go
181
Return -1 if it's meant to go last
183
if not self.condensed or x < 0:
185
if x > self.width - DEFAULT_SIMPLE_SPACING:
188
pos = DEFAULT_SIMPLE_SPACING
190
# TODO Need to avoid getting position between source and transition
191
for source in self.condensed:
192
if isinstance(source, TimelineSource):
193
spacing = self.childheight
194
elif isinstance(source, TimelineTransition):
195
spacing = self.childheight / 2
197
# this shouldn't happen !! The condensed list only contains
198
# sources and/or transitions
200
if x <= pos + spacing / 2:
202
pos = pos + spacing + DEFAULT_SIMPLE_SPACING
206
def _getNearestSourceSlotPixels(self, x):
208
returns the nearest file slot position available for the given position
209
Returns the value in pixels
211
if not self.condensed or x < 0:
212
return DEFAULT_SIMPLE_SPACING
213
if x > self.width - DEFAULT_SIMPLE_SPACING:
214
return self.width - 2 * DEFAULT_SIMPLE_SPACING
216
pos = DEFAULT_SIMPLE_SPACING
217
# TODO Need to avoid getting position between source and transition
218
for source in self.condensed:
219
if isinstance(source, TimelineSource):
220
spacing = self.childheight
221
elif isinstance(source, TimelineTransition):
222
spacing = self.childheight / 2
224
# this shouldn't happen !! The condensed list only contains
225
# sources and/or transitions
227
if x <= pos + spacing / 2:
229
pos = pos + spacing + DEFAULT_SIMPLE_SPACING
235
def _drawDragSlot(self):
236
if self.slotposition == -1:
238
self.bin_window.draw_rectangle(self.style.black_gc, True,
239
self.slotposition, DEFAULT_SIMPLE_SPACING,
240
DEFAULT_SIMPLE_SPACING, self.childheight)
242
def _eraseDragSlot(self):
243
if self.slotposition == -1:
245
self.bin_window.draw_rectangle(self.style.white_gc, True,
246
self.slotposition, DEFAULT_SIMPLE_SPACING,
247
DEFAULT_SIMPLE_SPACING, self.childheight)
249
def _gotFileFactory(self, filefactory, x, unused_y):
250
""" got a filefactory at the given position """
252
self._eraseDragSlot()
253
self.slotposition = -1
254
if not filefactory or not filefactory.is_video:
256
pos = self._getNearestSourceSlot(x)
258
gst.debug("_got_filefactory pos : %d" % pos)
260
# we just add it here, the drawing will be done in the condensed_list
262
source = TimelineFileSource(factory=filefactory,
263
media_type=MEDIA_TYPE_VIDEO,
264
name=filefactory.name)
266
# ONLY FOR SIMPLE TIMELINE : if video-only, we link a blank audio object
267
if not filefactory.is_audio:
268
audiobrother = TimelineBlankSource(factory=filefactory,
269
media_type=MEDIA_TYPE_AUDIO,
270
name=filefactory.name)
271
source.setBrother(audiobrother)
274
self.timeline.videocomp.appendSource(source)
276
self.timeline.videocomp.insertSourceAfter(source, self.condensed[pos - 1])
278
self.timeline.videocomp.prependSource(source)
280
def _moveElement(self, element, x):
281
gst.debug("TimelineSource, move %s to x:%d" % (element, x))
283
self._eraseDragSlot()
284
self.slotposition = -1
285
pos = self._getNearestSourceSlot(x)
287
self.timeline.videocomp.moveSource(element, pos)
289
def _widthChangedCb(self, unused_layout, property):
290
if not property.name == "width":
292
self.width = self.get_property("width")
294
def _motionNotifyEventCb(self, layout, event):
298
## Drag and Drop callbacks
300
def _dragMotionCb(self, unused_layout, unused_context, x, unused_y,
302
# TODO show where the dragged item would go
303
pos = self._getNearestSourceSlotPixels(x + (self.hadjustment.get_value()))
304
rpos = self._getNearestSourceSlot(x + self.hadjustment.get_value())
305
gst.log("SimpleTimeline x:%d , source would go at %d" % (x, rpos))
306
if not pos == self.slotposition:
307
if not self.slotposition == -1:
308
# erase previous slot position
309
self._eraseDragSlot()
310
# draw new slot position
311
self.slotposition = pos
314
def _dragLeaveCb(self, unused_layout, unused_context, unused_timestamp):
315
gst.log("SimpleTimeline")
316
self._eraseDragSlot()
317
self.slotposition = -1
318
# TODO remove the drag emplacement
320
def _dragDataReceivedCb(self, unused_layout, context, x, y, selection,
321
targetType, timestamp):
322
gst.log("SimpleTimeline, targetType:%d, selection.data:%s" % (targetType, selection.data))
323
if targetType == dnd.TYPE_PITIVI_FILESOURCE:
326
context.finish(False, False, timestamp)
327
x = x + int(self.hadjustment.get_value())
328
if self.draggedelement:
329
self._moveElement(self.draggedelement, x)
331
self._gotFileFactory(instance.PiTiVi.current.sources[uri], x, y)
332
context.finish(True, False, timestamp)
333
instance.PiTiVi.playground.switchToTimeline()
338
def _realizeCb(self, unused_layout):
339
self.modify_bg(gtk.STATE_NORMAL, self.style.white)
341
def _areaIntersect(self, x, y, w, h, x2, y2, w2, h2):
342
""" returns True if the area intersects, else False """
343
# is zone to the left of zone2
344
z1 = gtk.gdk.Rectangle(x, y, w, h)
345
z2 = gtk.gdk.Rectangle(x2, y2, w2, h2)
352
def _exposeEventCb(self, unused_layout, event):
353
x, y, w, h = event.area
354
# redraw the slot rectangle if there's one
355
if not self.slotposition == -1:
356
if self._areaIntersect(x, y, w, h,
357
self.slotposition, DEFAULT_SIMPLE_SPACING,
358
DEFAULT_SIMPLE_SPACING, self.childheight):
359
self.bin_window.draw_rectangle(self.style.black_gc, True,
360
self.slotposition, DEFAULT_SIMPLE_SPACING,
361
DEFAULT_SIMPLE_SPACING, self.childheight)
365
def _sizeAllocateCb(self, unused_layout, allocation):
366
if not self.height == allocation.height:
367
self.height = allocation.height
368
self.childheight = self.height - 2 * DEFAULT_SIMPLE_SPACING
369
self._resizeChildrens()
370
self.realWidth = allocation.width
371
if self._editingMode:
372
self.editingWidget.set_size_request(self.realWidth - 20,
376
def _resizeChildrens(self):
377
# resize the childrens to self.height
378
# also need to move them to their correct position
379
# TODO : check if there already at the given position
380
# TODO : check if they already have the good size
381
if self._editingMode:
383
pos = 2 * DEFAULT_SIMPLE_SPACING
384
for source in self.condensed:
385
widget = self.widgets[source]
386
if isinstance(source, TimelineFileSource):
387
widget.set_size_request(self.childheight, self.childheight)
388
self.move(widget, pos, DEFAULT_SIMPLE_SPACING)
389
pos = pos + self.childheight + DEFAULT_SIMPLE_SPACING
390
elif isinstance(source, SimpleTransitionWidget):
391
widget.set_size_request(self.childheight / 2, self.childheight)
392
self.move(widget, pos, DEFAULT_SIMPLE_SPACING)
393
pos = pos + self.childheight + DEFAULT_SIMPLE_SPACING
394
newwidth = pos + DEFAULT_SIMPLE_SPACING
395
self.set_property("width", newwidth)
400
def _sourceDeleteMeCb(self, unused_widget, element):
401
# remove this element from the timeline
402
self.timeline.videocomp.removeSource(element, collapse_neighbours=True)
404
def _sourceEditMeCb(self, unused_widget, element):
405
self.switchToEditingMode(element)
407
def _sourceDragBeginCb(self, unused_widget, unused_context, element):
408
gst.log("Timeline drag beginning on %s" % element)
409
if self.draggedelement:
410
gst.error("We were already doing a DnD ???")
411
self.draggedelement = element
412
# this element is starting to be dragged
414
def _sourceDragEndCb(self, unused_widget, unused_context, element):
415
gst.log("Timeline drag ending on %s" % element)
416
if not self.draggedelement == element:
417
gst.error("The DnD that ended is not the one that started before ???")
418
self.draggedelement = None
419
# this element is no longer dragged
421
def _editingWidgetHideMeCb(self, unused_widget):
422
self.switchToNormalMode()
427
def _switchEditingMode(self, source, mode=True):
428
""" Switch editing mode for the given TimelineSource """
429
gst.log("source:%s , mode:%s" % (source, mode))
431
if self._editingMode == mode:
432
gst.warning("We were already in the correct editing mode : %s" % mode)
435
if mode and not source:
436
gst.warning("You need to specify a valid TimelineSource")
440
# switching TO editing mode
441
gst.log("Switching TO editing mode")
443
# 1. Hide all sources
444
for widget in self.widgets.itervalues():
448
self._editingMode = mode
450
# 2. Show editing widget
451
self.editingWidget.setSource(source)
452
self.put(self.editingWidget, 10, 10)
453
self.props.width = self.realWidth
454
self.editingWidget.set_size_request(self.realWidth - 20, self.height - 20)
455
self.editingWidget.show()
458
gst.log("Switching back to normal mode")
459
# switching FROM editing mode
461
# 1. Hide editing widget
462
self.editingWidget.hide()
463
self.remove(self.editingWidget)
465
self._editingMode = mode
467
# 2. Show all sources
468
for widget in self.widgets.itervalues():
469
self.put(widget, 0, 0)
471
self._resizeChildrens()
473
def switchToEditingMode(self, source):
474
""" Switch to Editing mode for the given TimelineSource """
475
self._switchEditingMode(source)
477
def switchToNormalMode(self):
478
""" Switch back to normal timeline mode """
479
self._switchEditingMode(None, False)
482
class SimpleEditingWidget(gtk.DrawingArea):
484
Widget for editing a source in the SimpleTimeline
488
"hide-me" : (gobject.SIGNAL_RUN_LAST,
494
gtk.DrawingArea.__init__(self)
496
self.add_events(gtk.gdk.BUTTON_RELEASE_MASK | gtk.gdk.BUTTON_PRESS_MASK)
497
self.connect("realize", self._realizeCb)
498
self.connect("expose-event", self._exposeEventCb)
499
self.connect("button-press-event", self._buttonPressEventCb)
503
self._popupMenu = gtk.Menu()
504
closeitem = gtk.MenuItem(_("Close"))
505
closeitem.connect("activate", self._closeMenuItemCb)
507
self._popupMenu.append(closeitem)
509
def setSource(self, source):
510
self._source = source
512
def _realizeCb(self, unused_widget):
514
self.gc = self.window.new_gc()
515
self.gc.set_background(self.style.black)
517
def _exposeEventCb(self, unused_widget, event):
518
x, y, w, h = event.area
519
gst.log("expose %s" % ([x,y,w,h]))
521
def _closeMenuItemCb(self, unused_menuitem):
524
def _buttonPressEventCb(self, unused_widget, event):
525
if event.button == 3:
526
self._popupMenu.popup(None, None, None, event.button,
531
class SimpleSourceWidget(gtk.DrawingArea):
533
Widget for representing a source in simple timeline view
534
Takes a TimelineFileSource
538
'delete-me' : (gobject.SIGNAL_RUN_LAST,
541
'edit-me' : (gobject.SIGNAL_RUN_LAST,
548
# TODO change the factory argument into a TimelineFileSource
549
def __init__(self, filesource):
550
gobject.GObject.__init__(self)
552
self.add_events(gtk.gdk.POINTER_MOTION_MASK | gtk.gdk.ENTER_NOTIFY_MASK
553
| gtk.gdk.LEAVE_NOTIFY_MASK | gtk.gdk.BUTTON_PRESS_MASK
554
| gtk.gdk.BUTTON_RELEASE_MASK) # enter, leave, pointer-motion
557
self.filesource = filesource
558
if self.filesource.factory.thumbnail:
559
self.thumbnail = gtk.gdk.pixbuf_new_from_file(self.filesource.factory.thumbnail)
561
self.thumbnail = gtk.gdk.pixbuf_new_from_file(os.path.join(get_pixmap_dir(), "pitivi-video.png"))
562
self.thratio = float(self.thumbnail.get_width()) / float(self.thumbnail.get_height())
564
self.namelayout = self.create_pango_layout(os.path.basename(unquote(self.filesource.factory.name)))
565
self.lengthlayout = self.create_pango_layout(beautify_length(self.filesource.factory.length))
567
self.connect("expose-event", self._exposeEventCb)
568
self.connect("realize", self._realizeCb)
569
self.connect("configure-event", self._configureEventCb)
570
self.connect("button-press-event", self._buttonPressCb)
573
self._popupMenu = gtk.Menu()
574
deleteitem = gtk.MenuItem(_("Remove"))
575
deleteitem.connect("activate", self._deleteMenuItemCb)
577
# temporarily deactivate editing for 0.10.3 release !
578
# edititem = gtk.MenuItem(_("Edit"))
579
# edititem.connect("activate", self._editMenuItemCb)
581
self._popupMenu.append(deleteitem)
582
# self._popupMenu.append(edititem)
585
self.drag_source_set(gtk.gdk.BUTTON1_MASK,
586
[dnd.URI_TUPLE, dnd.FILESOURCE_TUPLE],
588
self.connect("drag_data_get", self._dragDataGetCb)
590
if not self.filesource.factory.video_info_stream:
591
height = 64 * self.thumbnail.get_height() / self.thumbnail.get_width()
593
vi = self.filesource.factory.video_info_stream
594
height = 64 * vi.dar.denom / vi.dar.num
595
smallthumbnail = self.thumbnail.scale_simple(64, height, gtk.gdk.INTERP_BILINEAR)
597
self.drag_source_set_icon_pixbuf(smallthumbnail)
603
# actually do the drawing in the pixmap here
605
self.pixmap = gtk.gdk.Pixmap(self.window, self.width, self.height)
606
# background and border
607
self.pixmap.draw_rectangle(self.style.bg_gc[gtk.STATE_NORMAL], True,
608
0, 0, self.width, self.height)
609
self.pixmap.draw_rectangle(self.gc, False,
610
1, 1, self.width - 2, self.height - 2)
612
namewidth, nameheight = self.namelayout.get_pixel_size()
613
lengthwidth, lengthheight = self.lengthlayout.get_pixel_size()
615
# maximal space left for thumbnail
616
tw = self.width - 2 * self.border
617
th = self.height - 4 * self.border - nameheight - lengthheight
619
# try calculating the desired height using tw
621
sh = int(tw / self.thratio)
622
if sh > tw or sh > th:
623
# calculate the width using th
624
sw = int(th * self.thratio)
630
self.pixmap.draw_layout(self.gc, self.border, self.border, self.namelayout)
633
subpixbuf = self.thumbnail.scale_simple(sw, sh, gtk.gdk.INTERP_BILINEAR)
634
self.pixmap.draw_pixbuf(self.gc, subpixbuf, 0, 0,
635
(self.width - sw) / 2,
636
(self.height - sh) / 2,
640
self.pixmap.draw_layout(self.gc,
641
self.width - self.border - lengthwidth,
642
self.height - self.border - lengthheight,
646
def _configureEventCb(self, unused_layout, event):
647
self.width = event.width
648
self.height = event.height
649
self.border = event.width / 20
650
# draw background pixmap
655
def _realizeCb(self, unused_widget):
656
self.gc = self.window.new_gc()
657
self.gc.set_line_attributes(2, gtk.gdk.LINE_SOLID,
658
gtk.gdk.CAP_ROUND, gtk.gdk.JOIN_ROUND)
659
self.gc.set_background(self.style.white)
662
def _exposeEventCb(self, unused_widget, event):
663
x, y, w, h = event.area
664
self.window.draw_drawable(self.gc, self.pixmap,
668
def _deleteMenuItemCb(self, unused_menuitem):
669
self.emit('delete-me')
671
def _editMenuItemCb(self, unused_menuitem):
674
def _buttonPressCb(self, unused_widget, event):
675
gst.debug("button %d" % event.button)
676
if event.button == 3:
677
self._popupMenu.popup(None,None,None,event.button,event.time)
679
# FIXME: mark as being selected
684
def _dragDataGetCb(self, unused_widget, unused_context, selection,
685
targetType, unused_eventTime):
686
gst.info("TimelineSource data get, type:%d" % targetType)
687
if targetType in [dnd.TYPE_PITIVI_FILESOURCE, dnd.TYPE_URI_LIST]:
688
selection.set(selection.target, 8, self.filesource.factory.name)
691
class SimpleTransitionWidget(gtk.DrawingArea):
692
""" Widget for representing a transition in simple timeline view """
694
# Change to use a TimelineTransitionEffect
695
def __init__(self, transitionfactory):
696
gobject.GObject.__init__(self)
701
self.factory = transitionfactory
702
self.connect("expose-event", self._exposeEventCb)
703
self.connect("realize", self._realizeCb)
704
self.connect("configure-event", self._configureEventCb)
707
# actually do the drawing in the pixmap here
709
self.pixmap = gtk.gdk.Pixmap(self.window, self.width, self.height)
710
# background and border
711
self.pixmap.draw_rectangle(self.style.white_gc, True,
712
0, 0, self.width, self.height)
713
self.pixmap.draw_rectangle(self.gc, False,
714
1, 1, self.width - 2, self.height - 2)
717
def _configureEventCb(self, unused_layout, event):
718
self.width = event.width
719
self.height = event.height
720
# draw background pixmap
725
def _realizeCb(self, unused_widget):
726
self.gc = self.window.new_gc()
727
self.gc.set_line_attributes(2, gtk.gdk.LINE_SOLID,
728
gtk.gdk.CAP_ROUND, gtk.gdk.JOIN_ROUND)
729
self.gc.set_background(self.style.white)
732
def _exposeEventCb(self, unused_widget, event):
733
x, y, w, h = event.area
734
self.window.draw_drawable(self.gc, self.pixmap,