1
# PiTiVi , Non-linear video editor
5
# Copyright (c) 2006, 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., 51 Franklin St, Fifth Floor,
20
# Boston, MA 02110-1301, USA.
23
Widget for the complex view ruler
30
from pitivi.ui.zoominterface import Zoomable
31
from pitivi.log.loggable import Loggable
32
from pitivi.utils import time_to_string
35
class ScaleRuler(gtk.DrawingArea, Zoomable, Loggable):
38
"expose-event": "override",
39
"button-press-event": "override",
40
"button-release-event": "override",
41
"motion-notify-event": "override",
42
"scroll-event": "override",
43
"seek": (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,
44
[gobject.TYPE_UINT64])
49
scale = [0, 0, 0, 0.5, 1, 2, 5, 10, 15, 30, 60, 120, 300, 600, 3600]
50
subdivide = ((1, 1.0), (2, 0.5), (10, .25))
52
def __init__(self, instance, hadj):
53
gtk.DrawingArea.__init__(self)
54
Zoomable.__init__(self)
55
Loggable.__init__(self)
56
self.log("Creating new ScaleRule")
57
self.add_events(gtk.gdk.POINTER_MOTION_MASK |
58
gtk.gdk.BUTTON_PRESS_MASK | gtk.gdk.BUTTON_RELEASE_MASK)
60
hadj.connect("value-changed", self._hadjValueChangedCb)
62
# double-buffering properties
64
# all values are in pixels
65
self.pixmap_offset = 0
66
self.pixmap_visible_width = 0
67
self.pixmap_allocated_width = 0
68
self.pixmap_old_allocated_width = -1
69
# This is the number of visible_width we allocate for the pixmap
70
self.pixmap_multiples = 2
72
# position is in nanoseconds
75
self.shaded_duration = gst.CLOCK_TIME_NONE
76
self.max_duration = gst.CLOCK_TIME_NONE
77
self.min_frame_spacing = 5.0
78
self.frame_height = 5.0
79
self.frame_rate = gst.Fraction(1 / 1)
81
self.need_update = True
83
def _hadjValueChangedCb(self, hadj):
84
self.pixmap_offset = self.hadj.get_value()
85
self.need_update = True
88
## Zoomable interface override
90
def zoomChanged(self):
91
self.need_update = True
94
## timeline position changed method
96
def timelinePositionChanged(self, value, unused_frame=None):
97
self.debug("value : %r", value)
98
ppos = max(self.nsToPixel(self.position) - 1, 0)
100
npos = max(self.nsToPixel(self.position) - 1, 0)
101
self._hadjValueChangedCb(self.hadj)
102
height = self.get_allocation().height
103
self.window.invalidate_rect((ppos, 0, 2, height), True)
104
self.window.invalidate_rect((npos, 0, 2, height), True)
106
## gtk.Widget overrides
108
def do_expose_event(self, event):
109
self.debug("exposing ScaleRuler %s", list(event.area))
110
x, y, width, height = event.area
111
# if (x < self.pixmap_offset) or (x+width > self.pixmap_offset + self.pixmap_allocated_width):
112
# self.debug("exposing outside boundaries !")
113
# self.pixmap_offset = max(0, x + (width / 2) - (self.pixmap_allocated_width / 2))
114
# self.debug("offset is now %d", self.pixmap_offset)
116
# width = self.pixmap_allocated_width
120
self.need_update = False
122
# double buffering power !
123
self.window.draw_drawable(
124
self.style.fg_gc[gtk.STATE_NORMAL],
129
context = self.window.cairo_create()
130
self.drawPosition(context, self.get_allocation())
133
def do_button_press_event(self, event):
134
self.debug("button pressed at x:%d", event.x)
135
if self.getShadedDuration() <= 0:
136
self.debug("no timeline to seek on, ignoring")
139
cur = self.pixelToNs(event.x + self.pixmap_offset)
143
def do_button_release_event(self, event):
144
self.debug("button released at x:%d", event.x)
148
def do_motion_notify_event(self, event):
149
self.debug("motion at event.x %d", event.x)
152
cur = self.pixelToNs(event.x + self.pixmap_offset)
156
def do_scroll_event(self, event):
157
if event.direction == gtk.gdk.SCROLL_UP:
159
elif event.direction == gtk.gdk.SCROLL_DOWN:
161
# TODO: seek timeline back/forward
162
elif event.direction == gtk.gdk.SCROLL_LEFT:
164
elif event.direction == gtk.gdk.SCROLL_RIGHT:
169
def _seekerSeekCb(self, seeker, position, format):
170
# clamping values within acceptable range
171
duration = self.getShadedDuration()
172
if duration in (0, gst.CLOCK_TIME_NONE):
174
if position > duration:
175
position = duration - (1 * gst.MSECOND)
179
self.emit('seek', position)
183
def _doSeek(self, value, format=gst.FORMAT_TIME, on_idle=False):
184
self.app.current.seeker.seek(value, format, on_idle)
189
""" (re)create the buffered drawable for the Widget """
190
# we can't create the pixmap if we're not realized
191
if not self.flags() & gtk.REALIZED:
194
# We want to benefit from double-buffering (so as not to recreate the
195
# ruler graphics all the time) yet we don't want to allocate insanely
196
# big pixmaps (which would result in big memory usage, or even not being
197
# able to allocate such a big pixmap).
199
# We therefore create a pixmap with a width of 2 times the maximum viewable
200
# width (allocation.width)
202
allocation = self.get_allocation()
204
if (allocation.width != self.pixmap_old_allocated_width):
207
self.pixmap = gtk.gdk.Pixmap(self.window, allocation.width,
209
self.pixmap_old_allocated_width = allocation.width
211
self.drawBackground(allocation)
212
self.drawRuler(allocation)
214
def setProjectFrameRate(self, rate):
215
self.frame_rate = rate
217
# set the lowest scale based on project framerate
218
self.scale[0] = float(2 / rate)
219
self.scale[1] = float(5 / rate)
220
self.scale[2] = float(10 / rate)
222
def setShadedDuration(self, duration):
223
self.info("start/duration changed")
225
self.shaded_duration = duration
227
if duration < self.position:
228
position = duration - gst.NSECOND
230
position = self.position
231
self.need_update = True
234
def getShadedDuration(self):
235
return self.shaded_duration
237
def getShadedDurationWidth(self):
238
return self.nsToPixel(self.getShadedDuration())
240
def setMaxDuration(self, duration):
241
self.max_duration = duration
243
def drawBackground(self, allocation):
244
self.pixmap.draw_rectangle(
245
self.style.bg_gc[gtk.STATE_NORMAL],
248
allocation.width, allocation.height)
250
offset = int(Zoomable.nsToPixel(self.getShadedDuration())) - self.pixmap_offset
252
self.pixmap.draw_rectangle(
253
self.style.bg_gc[gtk.STATE_ACTIVE],
257
int(allocation.height))
259
def drawRuler(self, allocation):
260
layout = self.create_pango_layout(time_to_string(0))
261
textwidth, textheight = layout.get_pixel_size()
263
for scale in self.scale:
264
spacing = Zoomable.zoomratio * scale
265
if spacing >= textwidth * 1.5:
268
offset = self.pixmap_offset % spacing
270
zoomRatio = self.zoomratio
271
self.drawFrameBoundaries(allocation)
272
self.drawTicks(allocation, offset, spacing, scale)
273
self.drawTimes(allocation, offset, spacing, scale, layout)
275
def drawTick(self, allocation, paintpos, height):
276
paintpos = int(paintpos)
277
height = allocation.height - int(allocation.height * height)
278
self.pixmap.draw_line(
279
self.style.fg_gc[gtk.STATE_NORMAL],
280
paintpos, height, paintpos,
283
def drawTicks(self, allocation, offset, spacing, scale):
284
for subdivide, height in self.subdivide:
285
spc = spacing / float(subdivide)
286
dur = scale / float(subdivide)
287
if spc < self.min_tick_spacing:
289
paintpos = -spacing + 0.5
290
paintpos += spacing - offset
291
while paintpos < allocation.width:
292
self.drawTick(allocation, paintpos, height)
295
def drawTimes(self, allocation, offset, spacing, scale, layout):
296
# figure out what the optimal offset is
297
interval = long(gst.SECOND * scale)
298
seconds = self.pixelToNs(self.pixmap_offset)
299
paintpos = float(self.border) + 2
301
seconds = seconds - (seconds % interval) + interval
302
paintpos += spacing - offset
303
shaded = self.getShadedDurationWidth()
305
while paintpos < allocation.width:
306
timevalue = time_to_string(long(seconds))
307
layout.set_text(timevalue)
308
if paintpos < shaded:
309
state = gtk.STATE_ACTIVE
311
state = gtk.STATE_NORMAL
312
self.pixmap.draw_layout(
313
self.style.fg_gc[state],
314
int(paintpos), 0, layout)
318
def drawFrameBoundaries(self, allocation):
319
ns_per_frame = float(1 / self.frame_rate) * gst.SECOND
320
frame_width = self.nsToPixel(ns_per_frame)
321
if frame_width >= self.min_frame_spacing:
322
offset = self.pixmap_offset % frame_width
323
paintpos = -frame_width + 0.5
324
height = allocation.height
325
y = int(height - self.frame_height)
326
states = [gtk.STATE_ACTIVE, gtk.STATE_PRELIGHT]
327
paintpos += frame_width - offset
328
frame_num = int(paintpos // frame_width) % 2
329
while paintpos < allocation.width:
330
self.pixmap.draw_rectangle(
331
self.style.bg_gc[states[frame_num]],
333
int(paintpos), y, frame_width, height)
334
frame_num = (frame_num + 1) % 2
335
paintpos += frame_width
337
def drawPosition(self, context, allocation):
338
if self.getShadedDuration() <= 0:
340
# a simple RED line will do for now
341
xpos = self.nsToPixel(self.position) + self.border -\
344
context.set_line_width(1.5)
345
context.set_source_rgb(1.0, 0, 0)
347
context.move_to(xpos, 0)
348
context.line_to(xpos, allocation.height)