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., 59 Temple Place - Suite 330,
20
# Boston, MA 02111-1307, 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
39
# cursors to be used for resizing objects
40
ARROW = gtk.gdk.Cursor(gtk.gdk.ARROW)
41
# TODO: replace this with custom cursor
42
PLAYHEAD_CURSOR = gtk.gdk.Cursor(gtk.gdk.SB_H_DOUBLE_ARROW)
44
GlobalSettings.addConfigOption('edgeSnapDeadband',
45
section = "user-interface",
46
key = "edge-snap-deadband",
50
PreferencesDialog.addNumericPreference('edgeSnapDeadband',
51
section = _("Behavior"),
52
label = _("Snap Distance (pixels)"),
53
description = _("Threshold distance (in pixels) used for all snapping "
57
class PlayheadController(Controller, Zoomable):
59
_cursor = PLAYHEAD_CURSOR
62
def __init__(self, *args, **kwargs):
63
Controller.__init__(self, *args, **kwargs)
65
def set_pos(self, item, pos):
66
self._canvas.app.current.seeker.seek(
67
Zoomable.pixelToNs(pos[0]))
69
class TimelineCanvas(goocanvas.Canvas, Zoomable, Loggable):
71
__gtype_name__ = 'TimelineCanvas'
73
"scroll-event": "override",
74
"expose-event" : "override",
79
def __init__(self, instance, timeline=None):
80
goocanvas.Canvas.__init__(self)
81
Zoomable.__init__(self)
82
Loggable.__init__(self)
84
self._selected_sources = []
89
self._block_size_request = False
90
self.props.integer_layout = True
91
self.props.automatic_bounds = False
92
self.props.clear_background = False
93
self.get_root_item().set_simple_transform(0, 2.0, 1.0, 0)
96
self.timeline = timeline
97
self.settings = instance.settings
101
root = self.get_root_item()
102
self.tracks = goocanvas.Group()
103
self.tracks.set_simple_transform(0, KW_LABEL_Y_OVERFLOW, 1.0, 0)
104
root.add_child(self.tracks)
105
self._marquee = goocanvas.Rect(
107
stroke_pattern = unpack_cairo_pattern(0x33CCFF66),
108
fill_pattern = unpack_cairo_pattern(0x33CCFF66),
109
visibility = goocanvas.ITEM_INVISIBLE)
110
self._playhead = goocanvas.Rect(
114
fill_color_rgba=0x000000FF,
115
stroke_color_rgba=0xFFFFFFFF,
117
self._playhead_controller = PlayheadController(self._playhead)
118
root.connect("motion-notify-event", self._selectionDrag)
119
root.connect("button-press-event", self._selectionStart)
120
root.connect("button-release-event", self._selectionEnd)
121
height = (LAYER_HEIGHT_EXPANDED + TRACK_SPACING + LAYER_SPACING) * 2
122
# add some padding for the horizontal scrollbar
124
self.set_size_request(-1, height)
126
def from_event(self, event):
127
return Point(*self.convert_from_pixels(event.x, event.y))
129
def setExpanded(self, track_object, expanded):
131
for track in self._tracks:
132
if track.track == track_object:
136
track_ui.setExpanded(expanded)
138
def do_scroll_event(self, event):
139
if event.state & gtk.gdk.SHIFT_MASK:
140
# shift + scroll => vertical (up/down) scroll
141
if event.direction == gtk.gdk.SCROLL_LEFT:
142
event.direction = gtk.gdk.SCROLL_UP
143
elif event.direction == gtk.gdk.SCROLL_RIGHT:
144
event.direction = gtk.gdk.SCROLL_DOWN
145
event.state &= ~gtk.gdk.SHIFT_MASK
146
elif event.state & gtk.gdk.CONTROL_MASK:
147
# zoom + scroll => zooming (up: zoom in)
148
if event.direction == gtk.gdk.SCROLL_UP:
151
elif event.direction == gtk.gdk.SCROLL_DOWN:
156
if event.direction == gtk.gdk.SCROLL_UP:
157
event.direction = gtk.gdk.SCROLL_LEFT
158
elif event.direction == gtk.gdk.SCROLL_DOWN:
159
event.direction = gtk.gdk.SCROLL_RIGHT
160
return goocanvas.Canvas.do_scroll_event(self, event)
162
## sets the cursor as appropriate
164
def _mouseEnterCb(self, unused_item, unused_target, event):
165
event.window.set_cursor(self._cursor)
168
def do_expose_event(self, event):
169
allocation = self.get_allocation()
170
width = allocation.width
171
height = allocation.height
172
# draw the canvas background
173
# we must have props.clear_background set to False
175
self.style.apply_default_background(event.window,
179
event.area.x, event.area.y,
180
event.area.width, event.area.height)
183
for track in self._tracks[:-1]:
185
self.style.paint_box(event.window,
191
event.area.x - 5, y + 1,
192
event.area.width + 10, TRACK_SPACING - 2)
195
goocanvas.Canvas.do_expose_event(self, event)
197
## implements selection marquee
202
_got_motion_notify = False
204
def _normalize(self, p1, p2):
213
return (x, y), (w, h)
215
def _selectionDrag(self, item, target, event):
217
self._got_motion_notify = True
218
cur = self.from_event(event)
219
pos, size = self._normalize(self._mousedown, cur)
220
self._marquee.props.x, self._marquee.props.y = pos
221
self._marquee.props.width, self._marquee.props.height = size
225
def _selectionStart(self, item, target, event):
226
self._selecting = True
227
self._marquee.props.visibility = goocanvas.ITEM_VISIBLE
228
self._mousedown = self.from_event(event)
229
self._marquee.props.width = 0
230
self._marquee.props.height = 0
231
self.pointer_grab(self.get_root_item(), gtk.gdk.POINTER_MOTION_MASK |
232
gtk.gdk.BUTTON_RELEASE_MASK, self._cursor, event.time)
235
def _selectionEnd(self, item, target, event):
236
seeker = self.app.current.seeker
237
self.pointer_ungrab(self.get_root_item(), event.time)
238
self._selecting = False
239
self._marquee.props.visibility = goocanvas.ITEM_INVISIBLE
240
if not self._got_motion_notify:
241
self.timeline.setSelectionTo(set(), 0)
242
seeker.seek(Zoomable.pixelToNs(event.x))
244
self._got_motion_notify = False
246
if event.get_state() & gtk.gdk.SHIFT_MASK:
248
if event.get_state() & gtk.gdk.CONTROL_MASK:
250
self.timeline.setSelectionTo(self._objectsUnderMarquee(), mode)
253
def _objectsUnderMarquee(self):
254
items = self.get_items_in_area(self._marquee.get_bounds(), True, True,
257
return set((item.element for item in items if isinstance(item,
258
TrackObject) and item.bg in items))
261
## playhead implementation
265
def timelinePositionChanged(self, position):
266
self.position = position
267
self._playhead.props.x = self.nsToPixel(position)
271
def setMaxDuration(self, duration):
272
self.max_duration = duration
275
def _request_size(self):
276
alloc = self.get_allocation()
277
w = Zoomable.nsToPixel(self.max_duration)
278
h = max(self._height, alloc.height)
279
self.set_bounds(0, 0, w, h)
280
self._playhead.props.height = h + 10
282
def zoomChanged(self):
284
self.timeline.dead_band = self.pixelToNs(
285
self.settings.edgeSnapDeadband)
287
self.timelinePositionChanged(self.position)
289
## settings callbacks
291
def _setSettings(self):
294
settings = receiver(_setSettings)
296
@handler(settings, "edgeSnapDeadbandChanged")
297
def _edgeSnapDeadbandChangedCb(self, settings):
300
## Timeline callbacks
302
def _set_timeline(self):
304
self._trackRemoved(None, 0)
306
for track in self.timeline.tracks:
307
self._trackAdded(None, track)
310
timeline = receiver(_set_timeline)
312
@handler(timeline, "track-added")
313
def _trackAdded(self, timeline, track):
314
track = Track(self.app, track, self.timeline)
315
self._tracks.append(track)
316
track.set_canvas(self)
317
self.tracks.add_child(track)
320
@handler(timeline, "track-removed")
321
def _trackRemoved(self, unused_timeline, position):
322
track = self._tracks[position]
323
del self._tracks[position]
327
def regroupTracks(self):
329
for i, track in enumerate(self._tracks):
330
track.set_simple_transform(0, height, 1, 0)
331
height += track.height + TRACK_SPACING
332
self._height = height