35
34
from pitivi.utils.pipeline import Seeker
36
35
from pitivi.utils.timeline import Zoomable
37
36
from pitivi.utils.loggable import Loggable
38
from pitivi.utils.ui import time_to_string, beautify_length
40
# Color #393f3f stolen from the dark variant of Adwaita.
41
# There's *no way* to get the GTK3 theme's bg color there (it's always black)
42
RULER_BACKGROUND_COLOR = (57, 63, 63)
45
def setCairoColor(cr, color):
46
if type(color) is tuple:
47
# Cairo's set_source_rgb function expects values from 0.0 to 1.0
48
cairo_color = map(lambda x: max(0, min(1, x / 255.0)), color)
49
cr.set_source_rgb(*cairo_color)
51
cr.set_source_rgb(float(color.red), float(color.green), float(color.blue))
37
from pitivi.utils.ui import NORMAL_FONT, PLAYHEAD_COLOR, PLAYHEAD_WIDTH, set_cairo_color, time_to_string, beautify_length
39
# A series of valid interval lengths in seconds.
40
SCALES = (0.5, 1, 2, 5, 10, 15, 30, 60, 120, 300, 600, 3600)
42
# The minimum distance between adjacent ticks.
43
MIN_TICK_SPACING_PIXELS = 3
44
# (count per interval, height ratio) tuples determining how the ticks appear.
45
TICK_TYPES = ((1, 1.0), (2, 0.5), (10, .25))
47
# For displaying the times a bit to the right.
48
TIMES_LEFT_MARGIN_PIXELS = 3
50
# The minimum width for a frame to be displayed.
51
FRAME_MIN_WIDTH_PIXELS = 5
52
# How short it should be.
53
FRAME_HEIGHT_PIXELS = 5
54
59
class ScaleRuler(Gtk.DrawingArea, Zoomable, Loggable):
61
Widget for displaying the ruler.
63
Displays a series of consecutive intervals. For each interval its beginning
64
time is shown. If zoomed in enough, shows the frames in alternate colors.
57
68
"button-press-event": "override",
97
104
self.position = 0 # In nanoseconds
98
105
self.pressed = False
99
self.min_frame_spacing = 5.0
100
self.frame_height = 5.0
101
106
self.frame_rate = Gst.Fraction(1 / 1)
102
107
self.ns_per_frame = float(1 / self.frame_rate) * Gst.SECOND
103
108
self.connect('draw', self.drawCb)
104
109
self.connect('configure-event', self.configureEventCb)
105
110
self.callback_id = None
106
111
self.callback_id_scroll = None
112
self.set_size_request(0, 25)
114
style = self.get_style_context()
115
color_normal = style.get_color(Gtk.StateFlags.NORMAL)
116
color_insensitive = style.get_color(Gtk.StateFlags.INSENSITIVE)
117
self._color_normal = color_normal
118
self._color_dimmed = Gdk.RGBA(
119
*[(x * 3 + y * 2) / 5
120
for x, y in ((color_normal.red, color_insensitive.red),
121
(color_normal.green, color_insensitive.green),
122
(color_normal.blue, color_insensitive.blue))])
108
126
def _focusInCb(self, unused_widget, unused_arg):
109
127
self.log("Ruler has grabbed focus")
110
self.app.gui.timeline_ui.setActionsSensitivity(True)
128
self.timeline.setActionsSensitivity(True)
112
130
def _focusOutCb(self, unused_widget, unused_arg):
113
131
self.log("Ruler has lost focus")
114
self.app.gui.timeline_ui.setActionsSensitivity(False)
132
self.timeline.setActionsSensitivity(False)
116
def _hadjValueChangedCb(self, hadj):
134
def _hadjValueChangedCb(self, unused_arg):
117
135
self.pixbuf_offset = self.hadj.get_value()
118
136
if self.callback_id_scroll is not None:
119
137
GLib.source_remove(self.callback_id_scroll)
222
243
self.frame_rate = rate
223
244
self.ns_per_frame = float(1 / self.frame_rate) * Gst.SECOND
224
self.scale[0] = float(2 / rate)
225
self.scale[1] = float(5 / rate)
226
self.scale[2] = float(10 / rate)
245
self.scales = (float(2 / rate), float(5 / rate), float(10 / rate)) + SCALES
228
247
## Drawing methods
230
def drawBackground(self, cr):
249
def drawBackground(self, context):
231
250
style = self.get_style_context()
232
setCairoColor(cr, RULER_BACKGROUND_COLOR)
233
cr.rectangle(0, 0, cr.get_target().get_width(), cr.get_target().get_height())
251
set_cairo_color(context, self._background_color)
252
width = context.get_target().get_width()
253
height = context.get_target().get_height()
254
context.rectangle(0, 0, width, height)
235
256
offset = int(self.nsToPixel(Gst.CLOCK_TIME_NONE)) - self.pixbuf_offset
237
setCairoColor(cr, style.get_background_color(Gtk.StateFlags.ACTIVE))
238
cr.rectangle(0, 0, int(offset), cr.get_target().get_height())
241
def drawRuler(self, cr):
242
# FIXME use system defaults
243
cr.set_font_face(cairo.ToyFontFace("Cantarell"))
245
textwidth = cr.text_extents(time_to_string(0))[2]
247
for scale in self.scale:
248
spacing = Zoomable.zoomratio * scale
258
set_cairo_color(context, style.get_background_color(Gtk.StateFlags.ACTIVE))
259
context.rectangle(0, 0, int(offset), context.get_target().get_height())
262
def drawRuler(self, context):
263
context.set_font_face(NORMAL_FONT)
264
context.set_font_size(NORMAL_FONT_SIZE)
266
spacing, scale = self._getSpacing(context)
267
offset = self.pixbuf_offset % spacing
268
self.drawFrameBoundaries(context)
269
self.drawTicks(context, offset, spacing)
270
self.drawTimes(context, offset, spacing, scale)
272
def _getSpacing(self, context):
273
textwidth = context.text_extents(time_to_string(0))[2]
274
zoom = Zoomable.zoomratio
275
for scale in self.scales:
276
spacing = scale * zoom
249
277
if spacing >= textwidth * 1.5:
278
return spacing, scale
279
raise Exception("Failed to find an interval size for textwidth:%s, zoomratio:%s" % (textwidth, Zoomable.zoomratio))
281
def drawTicks(self, context, offset, spacing):
282
for count_per_interval, height_ratio in TICK_TYPES:
283
space = float(spacing) / count_per_interval
284
if space < MIN_TICK_SPACING_PIXELS:
252
offset = self.pixbuf_offset % spacing
253
self.drawFrameBoundaries(cr)
254
self.drawTicks(cr, offset, spacing, scale)
255
self.drawTimes(cr, offset, spacing, scale)
257
def drawTick(self, cr, paintpos, height):
286
paintpos = 0.5 - offset
287
set_cairo_color(context, self._color_normal)
288
while paintpos < context.get_target().get_width():
289
self._drawTick(context, paintpos, height_ratio)
292
def _drawTick(self, context, paintpos, height_ratio):
258
293
# We need to use 0.5 pixel offsets to get a sharp 1 px line in cairo
259
294
paintpos = int(paintpos - 0.5) + 0.5
260
height = int(cr.get_target().get_height() * (1 - height))
261
style = self.get_style_context()
262
setCairoColor(cr, style.get_color(Gtk.StateType.NORMAL))
264
cr.move_to(paintpos, height)
265
cr.line_to(paintpos, cr.get_target().get_height())
269
def drawTicks(self, cr, offset, spacing, scale):
270
for subdivide, height in self.subdivide:
271
spc = spacing / float(subdivide)
272
if spc < self.min_tick_spacing:
274
paintpos = -spacing + 0.5
275
paintpos += spacing - offset
276
while paintpos < cr.get_target().get_width():
277
self.drawTick(cr, paintpos, height)
280
def drawTimes(self, cr, offset, spacing, scale):
295
target_height = context.get_target().get_height()
296
y = int(target_height * (1 - height_ratio))
297
context.set_line_width(1)
298
context.move_to(paintpos, y)
299
context.line_to(paintpos, target_height)
303
def drawTimes(self, context, offset, spacing, scale):
281
304
# figure out what the optimal offset is
282
305
interval = long(Gst.SECOND * scale)
283
seconds = self.pixelToNs(self.pixbuf_offset)
284
paintpos = float(self.border) + 2
306
current_time = self.pixelToNs(self.pixbuf_offset)
307
paintpos = TIMES_LEFT_MARGIN_PIXELS
286
seconds = seconds - (seconds % interval) + interval
309
current_time = current_time - (current_time % interval) + interval
287
310
paintpos += spacing - offset
289
while paintpos < cr.get_target().get_width():
290
if paintpos < self.nsToPixel(Gst.CLOCK_TIME_NONE):
291
state = Gtk.StateType.ACTIVE
312
state = Gtk.StateFlags.NORMAL
313
style = self.get_style_context()
314
set_cairo_color(context, style.get_color(state))
315
y_bearing = context.text_extents("0")[1]
319
# Seven elements: h : mm : ss . mmm
320
# Using negative indices because the first element (hour)
321
# can have a variable length.
322
return x[:-10], x[-10], x[-9:-7], x[-7], x[-6:-4], x[-4], x[-3:]
324
previous = split(time_to_string(max(0, current_time - interval)))
325
width = context.get_target().get_width()
326
while paintpos < width:
327
context.move_to(int(paintpos), 1 - y_bearing)
328
current = split(time_to_string(long(current_time)))
329
self._drawTime(context, current, previous, millis)
332
current_time += interval
334
def _drawTime(self, context, current, previous, millis):
335
hour = int(current[0])
336
for index, (element, previous_element) in enumerate(zip(current, previous)):
337
if index <= 1 and not hour:
339
if index >= 5 and not millis:
341
if element == previous_element:
342
color = self._color_dimmed
293
state = Gtk.StateType.NORMAL
294
timevalue = time_to_string(long(seconds))
295
style = self.get_style_context()
296
setCairoColor(cr, style.get_color(state))
297
x_bearing, y_bearing = cr.text_extents("0")[:2]
298
cr.move_to(int(paintpos), 1 - y_bearing)
299
cr.show_text(timevalue)
344
color = self._color_normal
345
set_cairo_color(context, color)
346
# Display the millis with a smaller font
349
context.set_font_size(SMALL_FONT_SIZE)
350
context.show_text(element)
352
context.set_font_size(NORMAL_FONT_SIZE)
303
def drawFrameBoundaries(self, cr):
354
def drawFrameBoundaries(self, context):
305
356
Draw the alternating rectangles that represent the project frames at
306
357
high zoom levels. These are based on the framerate set in the project
307
358
settings, not the actual frames on a video codec level.
309
360
frame_width = self.nsToPixel(self.ns_per_frame)
310
if not frame_width >= self.min_frame_spacing:
361
if not frame_width >= FRAME_MIN_WIDTH_PIXELS:
313
364
offset = self.pixbuf_offset % frame_width
314
height = cr.get_target().get_height()
315
y = int(height - self.frame_height)
365
height = context.get_target().get_height()
366
y = int(height - FRAME_HEIGHT_PIXELS)
316
367
# INSENSITIVE is a dark shade of gray, but lacks contrast
317
368
# SELECTED will be bright blue and more visible to represent frames
318
369
style = self.get_style_context()