~ubuntu-branches/ubuntu/trusty/pitivi/trusty

« 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-04-05 15:28:16 UTC
  • mfrom: (6.1.13 sid)
  • Revision ID: package-import@ubuntu.com-20140405152816-6lijoax4cngiz5j5
Tags: 0.93-3
* debian/control:
  + Depend on python-gi (>= 3.10), older versions do not work
    with pitivi (Closes: #732813).
  + Add missing dependency on gir1.2-clutter-gst-2.0 (Closes: #743692).
  + Add suggests on gir1.2-notify-0.7 and gir1.2-gnomedesktop-3.0.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# -*- coding: utf-8 -*-
 
2
# Pitivi video editor
 
3
#
 
4
#       pitivi/timeline/ruler.py
 
5
#
 
6
# Copyright (c) 2006, Edward Hervey <bilboed@bilboed.com>
 
7
# Copyright (c) 2014, Alex Băluț <alexandru.balut@gmail.com>
 
8
#
 
9
# This program is free software; you can redistribute it and/or
 
10
# modify it under the terms of the GNU Lesser General Public
 
11
# License as published by the Free Software Foundation; either
 
12
# version 2.1 of the License, or (at your option) any later version.
 
13
#
 
14
# This program is distributed in the hope that it will be useful,
 
15
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
16
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 
17
# Lesser General Public License for more details.
 
18
#
 
19
# You should have received a copy of the GNU Lesser General Public
 
20
# License along with this program; if not, write to the
 
21
# Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
 
22
# Boston, MA 02110-1301, USA.
 
23
 
 
24
import cairo
 
25
 
 
26
from gi.repository import Gtk
 
27
from gi.repository import Gdk
 
28
from gi.repository import Gst
 
29
from gi.repository import GLib
 
30
from gi.repository import GObject
 
31
 
 
32
from gettext import gettext as _
 
33
 
 
34
from pitivi.utils.pipeline import Seeker
 
35
from pitivi.utils.timeline import Zoomable
 
36
from pitivi.utils.loggable import Loggable
 
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
 
57
 
 
58
 
 
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
    """
 
66
 
 
67
    __gsignals__ = {
 
68
        "button-press-event": "override",
 
69
        "button-release-event": "override",
 
70
        "motion-notify-event": "override",
 
71
        "scroll-event": "override",
 
72
        "seek": (GObject.SignalFlags.RUN_LAST, None,
 
73
                [GObject.TYPE_UINT64])
 
74
    }
 
75
 
 
76
    def __init__(self, timeline, hadj):
 
77
        Gtk.DrawingArea.__init__(self)
 
78
        Zoomable.__init__(self)
 
79
        Loggable.__init__(self)
 
80
        self.log("Creating new ScaleRuler")
 
81
 
 
82
        # Allows stealing focus from other GTK widgets, prevent accidents:
 
83
        self.props.can_focus = True
 
84
        self.connect("focus-in-event", self._focusInCb)
 
85
        self.connect("focus-out-event", self._focusOutCb)
 
86
 
 
87
        self.timeline = timeline
 
88
        self._background_color = timeline.get_style_context().lookup_color('theme_bg_color')[1]
 
89
        self._seeker = Seeker()
 
90
        self.hadj = hadj
 
91
        hadj.connect("value-changed", self._hadjValueChangedCb)
 
92
        self.add_events(Gdk.EventMask.POINTER_MOTION_MASK |
 
93
            Gdk.EventMask.BUTTON_PRESS_MASK | Gdk.EventMask.BUTTON_RELEASE_MASK |
 
94
            Gdk.EventMask.SCROLL_MASK)
 
95
 
 
96
        self.pixbuf = None
 
97
 
 
98
        # all values are in pixels
 
99
        self.pixbuf_offset = 0
 
100
        self.pixbuf_offset_painted = 0
 
101
        # This is the number of width we allocate for the pixbuf
 
102
        self.pixbuf_multiples = 4
 
103
 
 
104
        self.position = 0  # In nanoseconds
 
105
        self.pressed = False
 
106
        self.frame_rate = Gst.Fraction(1 / 1)
 
107
        self.ns_per_frame = float(1 / self.frame_rate) * Gst.SECOND
 
108
        self.connect('draw', self.drawCb)
 
109
        self.connect('configure-event', self.configureEventCb)
 
110
        self.callback_id = None
 
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
 
125
 
 
126
    def _focusInCb(self, unused_widget, unused_arg):
 
127
        self.log("Ruler has grabbed focus")
 
128
        self.timeline.setActionsSensitivity(True)
 
129
 
 
130
    def _focusOutCb(self, unused_widget, unused_arg):
 
131
        self.log("Ruler has lost focus")
 
132
        self.timeline.setActionsSensitivity(False)
 
133
 
 
134
    def _hadjValueChangedCb(self, unused_arg):
 
135
        self.pixbuf_offset = self.hadj.get_value()
 
136
        if self.callback_id_scroll is not None:
 
137
            GLib.source_remove(self.callback_id_scroll)
 
138
        self.callback_id_scroll = GLib.timeout_add(100, self._maybeUpdate)
 
139
 
 
140
## Zoomable interface override
 
141
 
 
142
    def _maybeUpdate(self):
 
143
        self.queue_draw()
 
144
        self.callback_id = None
 
145
        self.callback_id_scroll = None
 
146
        return False
 
147
 
 
148
    def zoomChanged(self):
 
149
        if self.callback_id is not None:
 
150
            GLib.source_remove(self.callback_id)
 
151
        self.callback_id = GLib.timeout_add(100, self._maybeUpdate)
 
152
 
 
153
## timeline position changed method
 
154
 
 
155
    def setPipeline(self, pipeline):
 
156
        pipeline.connect('position', self.timelinePositionCb)
 
157
 
 
158
    def timelinePositionCb(self, unused_pipeline, position):
 
159
        self.position = position
 
160
        self.queue_draw()
 
161
 
 
162
## Gtk.Widget overrides
 
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)
 
167
 
 
168
        # Destroy previous buffer
 
169
        if self.pixbuf is not None:
 
170
            self.pixbuf.finish()
 
171
            self.pixbuf = None
 
172
 
 
173
        # Create a new buffer
 
174
        self.pixbuf = cairo.ImageSurface(cairo.FORMAT_ARGB32, width, height)
 
175
 
 
176
        return False
 
177
 
 
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()
 
194
 
 
195
        return False
 
196
 
 
197
    def do_button_press_event(self, event):
 
198
        self.debug("button pressed at x:%d", event.x)
 
199
        self.pressed = True
 
200
        position = self.pixelToNs(event.x + self.pixbuf_offset)
 
201
        self._seeker.seek(position, on_idle=True)
 
202
        return True
 
203
 
 
204
    def do_button_release_event(self, event):
 
205
        self.debug("button released at x:%d", event.x)
 
206
        self.grab_focus()  # Prevent other widgets from being confused
 
207
        self.pressed = False
 
208
        return False
 
209
 
 
210
    def do_motion_notify_event(self, event):
 
211
        position = self.pixelToNs(event.x + self.pixbuf_offset)
 
212
        if self.pressed:
 
213
            self.debug("motion at event.x %d", event.x)
 
214
            self._seeker.seek(position, on_idle=True)
 
215
 
 
216
        human_time = beautify_length(position)
 
217
        cur_frame = int(position / self.ns_per_frame) + 1
 
218
        self.set_tooltip_text(human_time + "\n" + _("Frame #%d" % cur_frame))
 
219
        return False
 
220
 
 
221
    def do_scroll_event(self, event):
 
222
        if event.scroll.state & Gdk.ModifierType.CONTROL_MASK:
 
223
            # Control + scroll = zoom
 
224
            if event.scroll.direction == Gdk.ScrollDirection.UP:
 
225
                Zoomable.zoomIn()
 
226
                self.timeline.zoomed_fitted = False
 
227
            elif event.scroll.direction == Gdk.ScrollDirection.DOWN:
 
228
                Zoomable.zoomOut()
 
229
                self.timeline.zoomed_fitted = False
 
230
        else:
 
231
            # No modifier key held down, just scroll
 
232
            if (event.scroll.direction == Gdk.ScrollDirection.UP
 
233
            or event.scroll.direction == Gdk.ScrollDirection.LEFT):
 
234
                self.timeline.scroll_left()
 
235
            elif (event.scroll.direction == Gdk.ScrollDirection.DOWN
 
236
            or event.scroll.direction == Gdk.ScrollDirection.RIGHT):
 
237
                self.timeline.scroll_right()
 
238
 
 
239
    def setProjectFrameRate(self, rate):
 
240
        """
 
241
        Set the lowest scale based on project framerate
 
242
        """
 
243
        self.frame_rate = rate
 
244
        self.ns_per_frame = float(1 / self.frame_rate) * Gst.SECOND
 
245
        self.scales = (float(2 / rate), float(5 / rate), float(10 / rate)) + SCALES
 
246
 
 
247
## Drawing methods
 
248
 
 
249
    def drawBackground(self, context):
 
250
        style = self.get_style_context()
 
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()
 
256
        offset = int(self.nsToPixel(Gst.CLOCK_TIME_NONE)) - self.pixbuf_offset
 
257
        if offset > 0:
 
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
 
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:
 
285
                break
 
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):
 
293
        # We need to use 0.5 pixel offsets to get a sharp 1 px line in cairo
 
294
        paintpos = int(paintpos - 0.5) + 0.5
 
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):
 
304
        # figure out what the optimal offset is
 
305
        interval = long(Gst.SECOND * scale)
 
306
        current_time = self.pixelToNs(self.pixbuf_offset)
 
307
        paintpos = TIMES_LEFT_MARGIN_PIXELS
 
308
        if offset > 0:
 
309
            current_time = current_time - (current_time % interval) + interval
 
310
            paintpos += spacing - offset
 
311
 
 
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
 
343
            else:
 
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)
 
353
 
 
354
    def drawFrameBoundaries(self, context):
 
355
        """
 
356
        Draw the alternating rectangles that represent the project frames at
 
357
        high zoom levels. These are based on the framerate set in the project
 
358
        settings, not the actual frames on a video codec level.
 
359
        """
 
360
        frame_width = self.nsToPixel(self.ns_per_frame)
 
361
        if not frame_width >= FRAME_MIN_WIDTH_PIXELS:
 
362
            return
 
363
 
 
364
        offset = self.pixbuf_offset % frame_width
 
365
        height = context.get_target().get_height()
 
366
        y = int(height - FRAME_HEIGHT_PIXELS)
 
367
        # INSENSITIVE is a dark shade of gray, but lacks contrast
 
368
        # SELECTED will be bright blue and more visible to represent frames
 
369
        style = self.get_style_context()
 
370
        states = [style.get_background_color(Gtk.StateFlags.ACTIVE),
 
371
                  style.get_background_color(Gtk.StateFlags.SELECTED)]
 
372
 
 
373
        frame_num = int(self.pixelToNs(self.pixbuf_offset) * float(self.frame_rate) / Gst.SECOND)
 
374
        paintpos = self.pixbuf_offset - offset
 
375
        max_pos = context.get_target().get_width() + self.pixbuf_offset
 
376
        while paintpos < max_pos:
 
377
            paintpos = self.nsToPixel(1 / float(self.frame_rate) * Gst.SECOND * frame_num)
 
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()
 
381
            frame_num += 1
 
382
 
 
383
    def drawPosition(self, context):
 
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)
 
389
        context.move_to(xpos, 0)
 
390
        context.line_to(xpos, context.get_target().get_height())
 
391
        context.stroke()