1
# PiTiVi , Non-linear video editor
3
# pitivi/ui/timelinecanvas.py
5
# Copyright (c) 2009, Brandon Lewis <brandon_lewis@berkeley.edu>
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., 51 Franklin St, Fifth Floor,
20
# Boston, MA 02110-1301, USA.
24
from gettext import gettext as _
26
from pitivi.log.loggable import Loggable
27
from pitivi.receiver import receiver, handler
28
from pitivi.ui.track import Track
29
from pitivi.ui.trackobject import TrackObject
30
from pitivi.ui.point import Point
31
from pitivi.ui.zoominterface import Zoomable
32
from pitivi.settings import GlobalSettings
33
from pitivi.ui.prefs import PreferencesDialog
34
from pitivi.ui.common import TRACK_SPACING, unpack_cairo_pattern, \
35
LAYER_HEIGHT_EXPANDED, LAYER_SPACING
36
from pitivi.ui.controller import Controller
37
from pitivi.ui.curve import KW_LABEL_Y_OVERFLOW
38
from pitivi.ui.common import SPACING
40
# cursors to be used for resizing objects
41
ARROW = gtk.gdk.Cursor(gtk.gdk.ARROW)
42
# TODO: replace this with custom cursor
43
PLAYHEAD_CURSOR = gtk.gdk.Cursor(gtk.gdk.SB_H_DOUBLE_ARROW)
45
GlobalSettings.addConfigOption('edgeSnapDeadband',
46
section="user-interface",
47
key="edge-snap-deadband",
51
PreferencesDialog.addNumericPreference('edgeSnapDeadband',
52
section=_("Behavior"),
53
label=_("Snap distance"),
54
description=_("Threshold (in pixels) at which two clips will snap together "
55
"when dragging or trimming."),
59
class PlayheadController(Controller, Zoomable):
61
_cursor = PLAYHEAD_CURSOR
63
def __init__(self, *args, **kwargs):
64
Controller.__init__(self, *args, **kwargs)
66
def set_pos(self, item, pos):
68
x += self._hadj.get_value()
69
self._canvas.app.current.seeker.seek(Zoomable.pixelToNs(x))
72
class TimelineCanvas(goocanvas.Canvas, Zoomable, Loggable):
74
__gtype_name__ = 'TimelineCanvas'
76
"expose-event": "override",
81
def __init__(self, instance, timeline=None):
82
goocanvas.Canvas.__init__(self)
83
Zoomable.__init__(self)
84
Loggable.__init__(self)
86
self._selected_sources = []
91
self._block_size_request = False
92
self.props.integer_layout = True
93
self.props.automatic_bounds = False
94
self.props.clear_background = False
95
self.get_root_item().set_simple_transform(0, 2.0, 1.0, 0)
98
self.timeline = timeline
99
self.settings = instance.settings
103
root = self.get_root_item()
104
self.tracks = goocanvas.Group()
105
self.tracks.set_simple_transform(0, KW_LABEL_Y_OVERFLOW, 1.0, 0)
106
root.add_child(self.tracks)
107
self._marquee = goocanvas.Rect(
109
stroke_pattern=unpack_cairo_pattern(0x33CCFF66),
110
fill_pattern=unpack_cairo_pattern(0x33CCFF66),
111
visibility=goocanvas.ITEM_INVISIBLE)
112
self._playhead = goocanvas.Rect(
116
fill_color_rgba=0x000000FF,
117
stroke_color_rgba=0xFFFFFFFF,
119
self._playhead_controller = PlayheadController(self._playhead)
120
self.connect("size-allocate", self._size_allocate_cb)
121
root.connect("motion-notify-event", self._selectionDrag)
122
root.connect("button-press-event", self._selectionStart)
123
root.connect("button-release-event", self._selectionEnd)
124
self.height = (LAYER_HEIGHT_EXPANDED + TRACK_SPACING +
126
# add some padding for the horizontal scrollbar
128
self.set_size_request(-1, self.height)
130
def from_event(self, event):
131
x, y = event.x, event.y
132
x += self.app.gui.timeline.hadj.get_value()
133
return Point(*self.convert_from_pixels(x, y))
135
def setExpanded(self, track_object, expanded):
137
for track in self._tracks:
138
if track.track == track_object:
142
track_ui.setExpanded(expanded)
144
## sets the cursor as appropriate
146
def _mouseEnterCb(self, unused_item, unused_target, event):
147
event.window.set_cursor(self._cursor)
150
def do_expose_event(self, event):
151
allocation = self.get_allocation()
152
width = allocation.width
153
height = allocation.height
154
# draw the canvas background
155
# we must have props.clear_background set to False
157
self.style.apply_default_background(event.window,
161
event.area.x, event.area.y,
162
event.area.width, event.area.height)
164
goocanvas.Canvas.do_expose_event(self, event)
166
## implements selection marquee
171
_got_motion_notify = False
173
def getItemsInArea(self, x1, y1, x2, y2):
175
Permits to get the Non UI L{Track}/L{TrackObject} in a list of set
176
corresponding to the L{Track}/L{TrackObject} which are in the are
178
@param x1: The horizontal coordinate of the up left corner of the area
180
@param y1: The vertical coordinate of the up left corner of the area
182
@param x2: The horizontal coordinate of the down right corner of the
185
@param x2: The vertical coordinate of the down right corner of the area
188
@returns: A list of L{Track}, L{TrackObject} tuples
190
items = self.get_items_in_area(goocanvas.Bounds(x1, y1, x2, y2), True,
196
track_objects = set()
199
if isinstance(item, Track):
200
tracks.add(item.track)
201
elif isinstance(item, TrackObject):
202
track_objects.add(item.element)
204
return tracks, track_objects
206
def _normalize(self, p1, p2, adjust=0):
217
return (x, y), (w, h)
219
def _selectionDrag(self, item, target, event):
221
self._got_motion_notify = True
222
cur = self.from_event(event)
223
pos, size = self._normalize(self._mousedown, cur,
224
self.app.gui.timeline.hadj.get_value())
225
self._marquee.props.x, self._marquee.props.y = pos
226
self._marquee.props.width, self._marquee.props.height = size
230
def _selectionStart(self, item, target, event):
231
self._selecting = True
232
self._marquee.props.visibility = goocanvas.ITEM_VISIBLE
233
self._mousedown = self.from_event(event)
234
self._marquee.props.width = 0
235
self._marquee.props.height = 0
236
self.pointer_grab(self.get_root_item(), gtk.gdk.POINTER_MOTION_MASK |
237
gtk.gdk.BUTTON_RELEASE_MASK, self._cursor, event.time)
240
def _selectionEnd(self, item, target, event):
241
seeker = self.app.current.seeker
242
self.pointer_ungrab(self.get_root_item(), event.time)
243
self._selecting = False
244
self._marquee.props.visibility = goocanvas.ITEM_INVISIBLE
245
if not self._got_motion_notify:
246
self.timeline.setSelectionTo(set(), 0)
247
seeker.seek(Zoomable.pixelToNs(event.x))
249
self._got_motion_notify = False
251
if event.get_state() & gtk.gdk.SHIFT_MASK:
253
if event.get_state() & gtk.gdk.CONTROL_MASK:
255
self.timeline.setSelectionTo(self._objectsUnderMarquee(), mode)
258
def _objectsUnderMarquee(self):
259
items = self.get_items_in_area(self._marquee.get_bounds(), True, True,
262
return set((item.element for item in items if isinstance(item,
263
TrackObject) and item.bg in items))
266
## playhead implementation
270
def timelinePositionChanged(self, position):
271
self.position = position
272
self._playhead.props.x = self.nsToPixel(position)
276
def setMaxDuration(self, duration):
277
self.max_duration = duration
280
def _request_size(self):
281
alloc = self.get_allocation()
282
self.set_bounds(0, 0, alloc.width, alloc.height)
283
self._playhead.props.height = (self.height + SPACING)
285
def _size_allocate_cb(self, widget, allocation):
288
def zoomChanged(self):
291
self.timeline.dead_band = self.pixelToNs(
292
self.settings.edgeSnapDeadband)
293
self.timelinePositionChanged(self.position)
295
## settings callbacks
297
def _setSettings(self):
300
settings = receiver(_setSettings)
302
@handler(settings, "edgeSnapDeadbandChanged")
303
def _edgeSnapDeadbandChangedCb(self, settings):
306
## Timeline callbacks
308
def _set_timeline(self):
310
self._trackRemoved(None, 0)
312
for track in self.timeline.tracks:
313
self._trackAdded(None, track)
316
timeline = receiver(_set_timeline)
318
@handler(timeline, "track-added")
319
def _trackAdded(self, timeline, track):
320
track = Track(self.app, track, self.timeline)
321
self._tracks.append(track)
322
track.set_canvas(self)
323
self.tracks.add_child(track)
326
@handler(timeline, "track-removed")
327
def _trackRemoved(self, unused_timeline, position):
328
track = self._tracks[position]
329
del self._tracks[position]
333
def regroupTracks(self):
335
for i, track in enumerate(self._tracks):
336
track.set_simple_transform(0, height, 1, 0)
337
height += track.height + TRACK_SPACING