~timo-jyrinki/ubuntu/trusty/pitivi/backport_utopic_fixes

« back to all changes in this revision

Viewing changes to pitivi/timeline/ruler.py

  • Committer: Package Import Robot
  • Author(s): Sebastian Dröge
  • Date: 2014-03-29 15:22:50 UTC
  • mto: (3.1.23 experimental)
  • mto: This revision was merged to the branch mainline in revision 44.
  • Revision ID: package-import@ubuntu.com-20140329152250-flg9onx416bqf3e3
Tags: upstream-0.93
Import upstream version 0.93

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# -*- coding: utf-8 -*-
1
2
# Pitivi video editor
2
3
#
3
4
#       pitivi/timeline/ruler.py
4
5
#
5
6
# Copyright (c) 2006, Edward Hervey <bilboed@bilboed.com>
 
7
# Copyright (c) 2014, Alex Băluț <alexandru.balut@gmail.com>
6
8
#
7
9
# This program is free software; you can redistribute it and/or
8
10
# modify it under the terms of the GNU Lesser General Public
19
21
# Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
20
22
# Boston, MA 02110-1301, USA.
21
23
 
22
 
"""
23
 
Widget for the complex view ruler
24
 
"""
25
24
import cairo
26
25
 
27
26
from gi.repository import Gtk
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
39
 
 
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)
43
 
 
44
 
 
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)
50
 
    else:
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
 
38
 
 
39
# A series of valid interval lengths in seconds.
 
40
SCALES = (0.5, 1, 2, 5, 10, 15, 30, 60, 120, 300, 600, 3600)
 
41
 
 
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))
 
46
 
 
47
# For displaying the times a bit to the right.
 
48
TIMES_LEFT_MARGIN_PIXELS = 3
 
49
 
 
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
 
 
55
NORMAL_FONT_SIZE = 13
 
56
SMALL_FONT_SIZE = 11
52
57
 
53
58
 
54
59
class ScaleRuler(Gtk.DrawingArea, Zoomable, Loggable):
 
60
    """
 
61
    Widget for displaying the ruler.
 
62
 
 
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.
 
65
    """
55
66
 
56
67
    __gsignals__ = {
57
68
        "button-press-event": "override",
62
73
                [GObject.TYPE_UINT64])
63
74
    }
64
75
 
65
 
    border = 0
66
 
    min_tick_spacing = 3
67
 
    scale = [0, 0, 0, 0.5, 1, 2, 5, 10, 15, 30, 60, 120, 300, 600, 3600]
68
 
    subdivide = ((1, 1.0), (2, 0.5), (10, .25))
69
 
 
70
 
    def __init__(self, instance, hadj):
 
76
    def __init__(self, timeline, hadj):
71
77
        Gtk.DrawingArea.__init__(self)
72
78
        Zoomable.__init__(self)
73
79
        Loggable.__init__(self)
78
84
        self.connect("focus-in-event", self._focusInCb)
79
85
        self.connect("focus-out-event", self._focusOutCb)
80
86
 
81
 
        self.app = instance
 
87
        self.timeline = timeline
 
88
        self._background_color = timeline.get_style_context().lookup_color('theme_bg_color')[1]
82
89
        self._seeker = Seeker()
83
90
        self.hadj = hadj
84
91
        hadj.connect("value-changed", self._hadjValueChangedCb)
96
103
 
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)
 
113
 
 
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))])
 
123
 
 
124
        self.scales = SCALES
107
125
 
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)
111
129
 
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)
115
133
 
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)
124
142
    def _maybeUpdate(self):
125
143
        self.queue_draw()
126
144
        self.callback_id = None
 
145
        self.callback_id_scroll = None
127
146
        return False
128
147
 
129
148
    def zoomChanged(self):
133
152
 
134
153
## timeline position changed method
135
154
 
136
 
    def timelinePositionChanged(self, value, unused_frame=None):
137
 
        self.position = value
 
155
    def setPipeline(self, pipeline):
 
156
        pipeline.connect('position', self.timelinePositionCb)
 
157
 
 
158
    def timelinePositionCb(self, unused_pipeline, position):
 
159
        self.position = position
138
160
        self.queue_draw()
139
161
 
140
162
## Gtk.Widget overrides
141
 
    def configureEventCb(self, widget, event, data=None):
142
 
        self.debug("Configuring, height %d, width %d",
143
 
            widget.get_allocated_width(), widget.get_allocated_height())
 
163
    def configureEventCb(self, widget, unused_event, unused_data=None):
 
164
        width = widget.get_allocated_width()
 
165
        height = widget.get_allocated_height()
 
166
        self.debug("Configuring, height %d, width %d", width, height)
144
167
 
145
168
        # Destroy previous buffer
146
169
        if self.pixbuf is not None:
148
171
            self.pixbuf = None
149
172
 
150
173
        # Create a new buffer
151
 
        self.pixbuf = cairo.ImageSurface(cairo.FORMAT_ARGB32,
152
 
                widget.get_allocated_width(), widget.get_allocated_height())
 
174
        self.pixbuf = cairo.ImageSurface(cairo.FORMAT_ARGB32, width, height)
153
175
 
154
176
        return False
155
177
 
156
 
    def drawCb(self, widget, cr):
157
 
        if self.pixbuf is not None:
158
 
            db = self.pixbuf
159
 
 
160
 
            # Create cairo context with double buffer as is DESTINATION
161
 
            cc = cairo.Context(db)
162
 
 
163
 
            #draw everything
164
 
            self.drawBackground(cc)
165
 
            self.drawRuler(cc)
166
 
            self.drawPosition(cc)
167
 
            db.flush()
168
 
 
169
 
            cr.set_source_surface(self.pixbuf, 0.0, 0.0)
170
 
            cr.paint()
171
 
        else:
172
 
            self.info('No buffer to paint buffer')
 
178
    def drawCb(self, unused_widget, context):
 
179
        if self.pixbuf is None:
 
180
            self.info('No buffer to paint')
 
181
            return False
 
182
 
 
183
        pixbuf = self.pixbuf
 
184
 
 
185
        # Draw on a temporary context and then copy everything.
 
186
        drawing_context = cairo.Context(pixbuf)
 
187
        self.drawBackground(drawing_context)
 
188
        self.drawRuler(drawing_context)
 
189
        self.drawPosition(drawing_context)
 
190
        pixbuf.flush()
 
191
 
 
192
        context.set_source_surface(self.pixbuf, 0.0, 0.0)
 
193
        context.paint()
173
194
 
174
195
        return False
175
196
 
202
223
            # Control + scroll = zoom
203
224
            if event.scroll.direction == Gdk.ScrollDirection.UP:
204
225
                Zoomable.zoomIn()
205
 
                self.app.gui.timeline_ui.zoomed_fitted = False
 
226
                self.timeline.zoomed_fitted = False
206
227
            elif event.scroll.direction == Gdk.ScrollDirection.DOWN:
207
228
                Zoomable.zoomOut()
208
 
                self.app.gui.timeline_ui.zoomed_fitted = False
 
229
                self.timeline.zoomed_fitted = False
209
230
        else:
210
231
            # No modifier key held down, just scroll
211
232
            if (event.scroll.direction == Gdk.ScrollDirection.UP
212
233
            or event.scroll.direction == Gdk.ScrollDirection.LEFT):
213
 
                self.app.gui.timeline_ui.scroll_left()
 
234
                self.timeline.scroll_left()
214
235
            elif (event.scroll.direction == Gdk.ScrollDirection.DOWN
215
236
            or event.scroll.direction == Gdk.ScrollDirection.RIGHT):
216
 
                self.app.gui.timeline_ui.scroll_right()
 
237
                self.timeline.scroll_right()
217
238
 
218
239
    def setProjectFrameRate(self, rate):
219
240
        """
221
242
        """
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
227
246
 
228
247
## Drawing methods
229
248
 
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())
234
 
        cr.fill()
 
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)
 
255
        context.fill()
235
256
        offset = int(self.nsToPixel(Gst.CLOCK_TIME_NONE)) - self.pixbuf_offset
236
257
        if offset > 0:
237
 
            setCairoColor(cr, style.get_background_color(Gtk.StateFlags.ACTIVE))
238
 
            cr.rectangle(0, 0, int(offset), cr.get_target().get_height())
239
 
            cr.fill()
240
 
 
241
 
    def drawRuler(self, cr):
242
 
        # FIXME use system defaults
243
 
        cr.set_font_face(cairo.ToyFontFace("Cantarell"))
244
 
        cr.set_font_size(13)
245
 
        textwidth = cr.text_extents(time_to_string(0))[2]
246
 
 
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())
 
260
            context.fill()
 
261
 
 
262
    def drawRuler(self, context):
 
263
        context.set_font_face(NORMAL_FONT)
 
264
        context.set_font_size(NORMAL_FONT_SIZE)
 
265
 
 
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)
 
271
 
 
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))
 
280
 
 
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:
250
285
                break
251
 
 
252
 
        offset = self.pixbuf_offset % spacing
253
 
        self.drawFrameBoundaries(cr)
254
 
        self.drawTicks(cr, offset, spacing, scale)
255
 
        self.drawTimes(cr, offset, spacing, scale)
256
 
 
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)
 
290
                paintpos += space
 
291
 
 
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))
263
 
        cr.set_line_width(1)
264
 
        cr.move_to(paintpos, height)
265
 
        cr.line_to(paintpos, cr.get_target().get_height())
266
 
        cr.close_path()
267
 
        cr.stroke()
268
 
 
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:
273
 
                break
274
 
            paintpos = -spacing + 0.5
275
 
            paintpos += spacing - offset
276
 
            while paintpos < cr.get_target().get_width():
277
 
                self.drawTick(cr, paintpos, height)
278
 
                paintpos += spc
279
 
 
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)
 
300
        context.close_path()
 
301
        context.stroke()
 
302
 
 
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
285
308
        if offset > 0:
286
 
            seconds = seconds - (seconds % interval) + interval
 
309
            current_time = current_time - (current_time % interval) + interval
287
310
            paintpos += spacing - offset
288
311
 
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]
 
316
        millis = scale < 1
 
317
 
 
318
        def split(x):
 
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:]
 
323
 
 
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)
 
330
            previous = current
 
331
            paintpos += spacing
 
332
            current_time += interval
 
333
 
 
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:
 
338
                continue
 
339
            if index >= 5 and not millis:
 
340
                break
 
341
            if element == previous_element:
 
342
                color = self._color_dimmed
292
343
            else:
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)
300
 
            paintpos += spacing
301
 
            seconds += interval
 
344
                color = self._color_normal
 
345
            set_cairo_color(context, color)
 
346
            # Display the millis with a smaller font
 
347
            small = index >= 5
 
348
            if small:
 
349
                context.set_font_size(SMALL_FONT_SIZE)
 
350
            context.show_text(element)
 
351
            if small:
 
352
                context.set_font_size(NORMAL_FONT_SIZE)
302
353
 
303
 
    def drawFrameBoundaries(self, cr):
 
354
    def drawFrameBoundaries(self, context):
304
355
        """
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.
308
359
        """
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:
311
362
            return
312
363
 
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()
321
372
 
322
373
        frame_num = int(self.pixelToNs(self.pixbuf_offset) * float(self.frame_rate) / Gst.SECOND)
323
374
        paintpos = self.pixbuf_offset - offset
324
 
        max_pos = cr.get_target().get_width() + self.pixbuf_offset
 
375
        max_pos = context.get_target().get_width() + self.pixbuf_offset
325
376
        while paintpos < max_pos:
326
377
            paintpos = self.nsToPixel(1 / float(self.frame_rate) * Gst.SECOND * frame_num)
327
 
            setCairoColor(cr, states[(frame_num + 1) % 2])
328
 
            cr.rectangle(0.5 + paintpos - self.pixbuf_offset, y, frame_width, height)
329
 
            cr.fill()
 
378
            set_cairo_color(context, states[(frame_num + 1) % 2])
 
379
            context.rectangle(0.5 + paintpos - self.pixbuf_offset, y, frame_width, height)
 
380
            context.fill()
330
381
            frame_num += 1
331
382
 
332
383
    def drawPosition(self, context):
333
 
        # a simple RED line will do for now
334
 
        xpos = self.nsToPixel(self.position) + self.border - self.pixbuf_offset
335
 
        context.save()
336
 
        context.set_line_width(1.5)
337
 
        context.set_source_rgb(1.0, 0, 0)
 
384
        # Add 0.5 so that the line center is at the middle of the pixel,
 
385
        # without this the line appears blurry.
 
386
        xpos = self.nsToPixel(self.position) - self.pixbuf_offset + 0.5
 
387
        context.set_line_width(PLAYHEAD_WIDTH + 2)
 
388
        set_cairo_color(context, PLAYHEAD_COLOR)
338
389
        context.move_to(xpos, 0)
339
390
        context.line_to(xpos, context.get_target().get_height())
340
391
        context.stroke()
341
 
        context.restore()