~timo-jyrinki/ubuntu/trusty/pitivi/merge_debian_0.93-3

« back to all changes in this revision

Viewing changes to pitivi/utils/ui.py

  • Committer: Timo Jyrinki
  • Author(s): Sebastian Dröge
  • Date: 2014-04-05 13:28:16 UTC
  • mfrom: (1.5.8)
  • Revision ID: timo.jyrinki@canonical.com-20140405132816-wmv1rhbtmlxmx3ag
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/utils/ui.py
 
5
#
 
6
# Copyright (c) 2005, Edward Hervey <bilboed@bilboed.com>
 
7
# Copyright (c) 2012, Thibault Saunier <thibault.saunier@collabora.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
"""
 
25
UI utilities. This file contain the UI constants, and various functions and
 
26
classes that help with UI drawing around the application
 
27
"""
 
28
 
 
29
 
 
30
import cairo
 
31
import decimal
 
32
import os
 
33
import urllib
 
34
 
 
35
from gettext import ngettext, gettext as _
 
36
 
 
37
from gi.repository import Clutter
 
38
from gi.repository import Cogl
 
39
from gi.repository import GLib
 
40
from gi.repository import GES
 
41
from gi.repository import Gdk
 
42
from gi.repository import Gio
 
43
from gi.repository import Gst
 
44
from gi.repository import Gtk
 
45
from gi.repository.GstPbutils import DiscovererVideoInfo, DiscovererAudioInfo,\
 
46
    DiscovererStreamInfo, DiscovererSubtitleInfo, DiscovererInfo
 
47
 
 
48
from pitivi.utils.misc import path_from_uri
 
49
 
 
50
from pitivi.utils.loggable import doLog, ERROR
 
51
 
 
52
# ---------------------- Constants -------------------------------------------#
 
53
 
 
54
##
 
55
# UI pixels information constants
 
56
##
 
57
 
 
58
LAYER_HEIGHT_EXPANDED = 50
 
59
LAYER_HEIGHT_COLLAPSED = 15
 
60
TRACK_SPACING = 8
 
61
EXPANDED_SIZE = 65
 
62
CONTROL_WIDTH = 250
 
63
 
 
64
PADDING = 6
 
65
SPACING = 10
 
66
 
 
67
PLAYHEAD_WIDTH = 1
 
68
CANVAS_SPACING = 21
 
69
KEYFRAME_SIZE = 8
 
70
 
 
71
PLAYHEAD_COLOR = Clutter.Color.new(200, 0, 0, 255)
 
72
 
 
73
# Layer creation blocking time in s
 
74
LAYER_CREATION_BLOCK_TIME = 0.2
 
75
 
 
76
##
 
77
#   Drag'n drop constants
 
78
##
 
79
TYPE_TEXT_PLAIN = 24
 
80
TYPE_URI_LIST = 25
 
81
 
 
82
# FileSourceFactory (or subclasses)
 
83
TYPE_PITIVI_FILESOURCE = 26
 
84
 
 
85
# What objects to these correspond to ???
 
86
TYPE_PITIVI_EFFECT = 27
 
87
TYPE_PITIVI_AUDIO_EFFECT = 28
 
88
TYPE_PITIVI_VIDEO_EFFECT = 29
 
89
TYPE_PITIVI_AUDIO_TRANSITION = 30
 
90
TYPE_PITIVI_VIDEO_TRANSITION = 31
 
91
TYPE_PITIVI_LAYER_CONTROL = 32
 
92
 
 
93
FILE_TARGET_ENTRY = Gtk.TargetEntry.new("text/plain", Gtk.TargetFlags.OTHER_APP, TYPE_TEXT_PLAIN)
 
94
URI_TARGET_ENTRY = Gtk.TargetEntry.new("text/uri-list", 0, TYPE_URI_LIST)
 
95
FILESOURCE_TARGET_ENTRY = Gtk.TargetEntry.new("pitivi/file-source", 0, TYPE_PITIVI_FILESOURCE)
 
96
EFFECT_TARGET_ENTRY = Gtk.TargetEntry.new("pitivi/effect", 0, TYPE_PITIVI_EFFECT)
 
97
AUDIO_EFFECT_TARGET_ENTRY = Gtk.TargetEntry.new("pitivi/audio-effect", 0, TYPE_PITIVI_AUDIO_EFFECT)
 
98
VIDEO_EFFECT_TARGET_ENTRY = Gtk.TargetEntry.new("pitivi/video-effect", 0, TYPE_PITIVI_VIDEO_EFFECT)
 
99
AUDIO_TRANSITION_TARGET_ENTRY = Gtk.TargetEntry.new("pitivi/audio-transition", 0, TYPE_PITIVI_AUDIO_TRANSITION)
 
100
VIDEO_TRANSITION_TARGET_ENTRY = Gtk.TargetEntry.new("pitivi/video-transition", 0, TYPE_PITIVI_VIDEO_TRANSITION)
 
101
LAYER_CONTROL_TARGET_ENTRY = Gtk.TargetEntry.new("pitivi/layer-control", 0, TYPE_PITIVI_LAYER_CONTROL)
 
102
 
 
103
 
 
104
def _get_settings(schema):
 
105
    if schema not in Gio.Settings.list_schemas():
 
106
        return None
 
107
    return Gio.Settings(schema=schema)
 
108
 
 
109
 
 
110
def _get_font(font_spec, default):
 
111
    raw_font = default
 
112
    settings = _get_settings("org.gnome.desktop.interface")
 
113
    if settings:
 
114
        if font_spec in settings.list_keys():
 
115
            raw_font = settings.get_string(font_spec)
 
116
    face = raw_font.rsplit(" ", 1)[0]
 
117
    return cairo.ToyFontFace(face)
 
118
 
 
119
NORMAL_FONT = _get_font("font-name", "Cantarell")
 
120
DOCUMENT_FONT = _get_font("document-font-name", "Sans")
 
121
MONOSPACE_FONT = _get_font("monospace-font-name", "Monospace")
 
122
 
 
123
 
 
124
# ---------------------- ARGB color helper-------------------------------------#
 
125
def pack_color_32(red, green, blue, alpha=0xFFFF):
 
126
    """Packs the specified 16bit color values in a 32bit RGBA value."""
 
127
    red = red >> 8
 
128
    green = green >> 8
 
129
    blue = blue >> 8
 
130
    alpha = alpha >> 8
 
131
    return (red << 24 | green << 16 | blue << 8 | alpha)
 
132
 
 
133
 
 
134
def pack_color_64(red, green, blue, alpha=0xFFFF):
 
135
    """Packs the specified 16bit color values in a 64bit RGBA value."""
 
136
    return (red << 48 | green << 32 | blue << 16 | alpha)
 
137
 
 
138
 
 
139
def unpack_color(value):
 
140
    """Unpacks the specified RGBA value into four 16bit color values.
 
141
 
 
142
    Args:
 
143
      value: A 32bit or 64bit RGBA value.
 
144
    """
 
145
    if not (value >> 32):
 
146
        return unpack_color_32(value)
 
147
    else:
 
148
        return unpack_color_64(value)
 
149
 
 
150
 
 
151
def unpack_color_32(value):
 
152
    """Unpacks the specified 32bit RGBA value into four 16bit color values."""
 
153
    red = (value >> 24) << 8
 
154
    green = ((value >> 16) & 0xFF) << 8
 
155
    blue = ((value >> 8) & 0xFF) << 8
 
156
    alpha = (value & 0xFF) << 8
 
157
    return red, green, blue, alpha
 
158
 
 
159
 
 
160
def unpack_color_64(value):
 
161
    """Unpacks the specified 64bit RGBA value into four 16bit color values."""
 
162
    red = (value >> 48) & 0xFFFF
 
163
    green = (value >> 32) & 0xFFFF
 
164
    blue = (value >> 16) & 0xFFFF
 
165
    alpha = value & 0xFFFF
 
166
    return red, green, blue, alpha
 
167
 
 
168
 
 
169
def unpack_cairo_pattern(value):
 
170
    """Transforms the specified RGBA value into a SolidPattern object."""
 
171
    red, green, blue, alpha = unpack_color(value)
 
172
    return cairo.SolidPattern(
 
173
        red / 65535.0,
 
174
        green / 65535.0,
 
175
        blue / 65535.0,
 
176
        alpha / 65535.0)
 
177
 
 
178
 
 
179
def unpack_cairo_gradient(value):
 
180
    """Creates a LinearGradient object out of the specified RGBA value."""
 
181
    red, green, blue, alpha = unpack_color(value)
 
182
    gradient = cairo.LinearGradient(0, 0, 0, 50)
 
183
    gradient.add_color_stop_rgba(
 
184
        1.0,
 
185
        red / 65535.0,
 
186
        green / 65535.0,
 
187
        blue / 65535.0,
 
188
        alpha / 65535.0)
 
189
    gradient.add_color_stop_rgba(
 
190
        0,
 
191
        (red / 65535.0) * 1.5,
 
192
        (green / 65535.0) * 1.5,
 
193
        (blue / 65535.0) * 1.5,
 
194
        alpha / 65535.0)
 
195
    return gradient
 
196
 
 
197
 
 
198
def hex_to_rgb(value):
 
199
    return tuple(float(int(value[i:i + 2], 16)) / 255.0 for i in range(0, 6, 2))
 
200
 
 
201
 
 
202
def create_cogl_color(red, green, blue, alpha):
 
203
    color = Cogl.Color()
 
204
    color.init_from_4ub(red, green, blue, alpha)
 
205
    return color
 
206
 
 
207
 
 
208
def set_cairo_color(context, color):
 
209
    if type(color) is Clutter.Color:
 
210
        color = (color.red, color.green, color.blue)
 
211
 
 
212
    if type(color) is Gdk.RGBA:
 
213
        cairo_color = (float(color.red), float(color.green), float(color.blue))
 
214
    elif type(color) is tuple:
 
215
        # Cairo's set_source_rgb function expects values from 0.0 to 1.0
 
216
        cairo_color = map(lambda x: max(0, min(1, x / 255.0)), color)
 
217
    else:
 
218
        raise Exception("Unexpected color parameter: %s, %s" % (type(color), color))
 
219
    context.set_source_rgb(*cairo_color)
 
220
 
 
221
 
 
222
def beautify_info(info):
 
223
    """
 
224
    Formats the specified info for display.
 
225
 
 
226
    @type info: L{DiscovererInfo}
 
227
    """
 
228
    ranks = {
 
229
        DiscovererVideoInfo: 0,
 
230
        DiscovererAudioInfo: 1,
 
231
        DiscovererStreamInfo: 2
 
232
    }
 
233
 
 
234
    def stream_sort_key(stream):
 
235
        try:
 
236
            return ranks[type(stream)]
 
237
        except KeyError:
 
238
            return len(ranks)
 
239
 
 
240
    info.get_stream_list().sort(key=stream_sort_key)
 
241
    nice_streams_txts = []
 
242
    for stream in info.get_stream_list():
 
243
        try:
 
244
            beautified_string = beautify_stream(stream)
 
245
        except NotImplementedError:
 
246
            doLog(ERROR, "Beautify", "None", "Cannot beautify %s", stream)
 
247
            continue
 
248
        if beautified_string:
 
249
            nice_streams_txts.append(beautified_string)
 
250
 
 
251
    return ("<b>" + path_from_uri(info.get_uri()) + "</b>\n" +
 
252
        "\n".join(nice_streams_txts))
 
253
 
 
254
 
 
255
def info_name(info):
 
256
    """
 
257
    Return a human-readable filename (without the path and quoting).
 
258
 
 
259
    @type info: L{GES.Asset} or L{DiscovererInfo}
 
260
    """
 
261
    if isinstance(info, GES.Asset):
 
262
        filename = urllib.unquote(os.path.basename(info.get_id()))
 
263
    elif isinstance(info, DiscovererInfo):
 
264
        filename = urllib.unquote(os.path.basename(info.get_uri()))
 
265
    else:
 
266
        raise Exception("Unsupported argument type: %s" % type(info))
 
267
    return GLib.markup_escape_text(filename)
 
268
 
 
269
 
 
270
def beautify_stream(stream):
 
271
    if type(stream) is DiscovererAudioInfo:
 
272
        templ = ngettext("<b>Audio:</b> %d channel at %d <i>Hz</i> (%d <i>bits</i>)",
 
273
                "<b>Audio:</b> %d channels at %d <i>Hz</i> (%d <i>bits</i>)",
 
274
                stream.get_channels())
 
275
        templ = templ % (stream.get_channels(), stream.get_sample_rate(),
 
276
            stream.get_depth())
 
277
        return templ
 
278
 
 
279
    elif type(stream) is DiscovererVideoInfo:
 
280
        par = stream.get_par_num() / stream.get_par_denom()
 
281
        if not stream.is_image():
 
282
            templ = _("<b>Video:</b> %d×%d <i>pixels</i> at %.3f <i>fps</i>")
 
283
            try:
 
284
                templ = templ % (par * stream.get_width(), stream.get_height(),
 
285
                    float(stream.get_framerate_num()) / stream.get_framerate_denom())
 
286
            except ZeroDivisionError:
 
287
                templ = templ % (par * stream.get_width(), stream.get_height(), 0)
 
288
        else:
 
289
            templ = _("<b>Image:</b> %d×%d <i>pixels</i>")
 
290
            templ = templ % (par * stream.get_width(), stream.get_height())
 
291
        return templ
 
292
 
 
293
    elif type(stream) is DiscovererSubtitleInfo:
 
294
        # Ignore subtitle streams
 
295
        return None
 
296
 
 
297
    elif type(stream) is DiscovererStreamInfo:
 
298
        caps = stream.get_caps().to_string()
 
299
        if caps in ("application/x-subtitle", "application/x-id3", "text"):
 
300
            # Ignore all audio ID3 tags and subtitle tracks, we don't show them
 
301
            return None
 
302
 
 
303
    raise NotImplementedError
 
304
 
 
305
 
 
306
def time_to_string(value):
 
307
    """
 
308
    Converts the given time in nanoseconds to a human readable string
 
309
 
 
310
    Format HH:MM:SS.XXX
 
311
    """
 
312
    if value == Gst.CLOCK_TIME_NONE:
 
313
        return "--:--:--.---"
 
314
    ms = value / Gst.MSECOND
 
315
    sec = ms / 1000
 
316
    ms = ms % 1000
 
317
    mins = sec / 60
 
318
    sec = sec % 60
 
319
    hours = mins / 60
 
320
    mins = mins % 60
 
321
    return "%01d:%02d:%02d.%03d" % (hours, mins, sec, ms)
 
322
 
 
323
 
 
324
def beautify_length(length):
 
325
    """
 
326
    Converts the given time in nanoseconds to a human readable string
 
327
    """
 
328
    sec = length / Gst.SECOND
 
329
    mins = sec / 60
 
330
    sec = sec % 60
 
331
    hours = mins / 60
 
332
    mins = mins % 60
 
333
 
 
334
    parts = []
 
335
    if hours:
 
336
        parts.append(ngettext("%d hour", "%d hours", hours) % hours)
 
337
 
 
338
    if mins:
 
339
        parts.append(ngettext("%d minute", "%d minutes", mins) % mins)
 
340
 
 
341
    if not hours and sec:
 
342
        parts.append(ngettext("%d second", "%d seconds", sec) % sec)
 
343
 
 
344
    return ", ".join(parts)
 
345
 
 
346
 
 
347
def beautify_time_delta(seconds):
 
348
    """
 
349
    Converts the given time in seconds to a human-readable estimate.
 
350
 
 
351
    This is intended for "Unsaved changes" and "Backup file found" dialogs.
 
352
    """
 
353
    mins = seconds / 60
 
354
    sec = int(seconds % 60)
 
355
    hours = mins / 60
 
356
    mins = int(mins % 60)
 
357
    days = int(hours / 24)
 
358
    hours = int(hours % 24)
 
359
 
 
360
    parts = []
 
361
    if days > 0:
 
362
        parts.append(ngettext("%d day", "%d days", days) % days)
 
363
    if hours > 0:
 
364
        parts.append(ngettext("%d hour", "%d hours", hours) % hours)
 
365
 
 
366
    if days == 0 and mins > 0:
 
367
        parts.append(ngettext("%d minute", "%d minutes", mins) % mins)
 
368
 
 
369
    if hours == 0 and mins < 2 and sec:
 
370
        parts.append(ngettext("%d second", "%d seconds", sec) % sec)
 
371
 
 
372
    return ", ".join(parts)
 
373
 
 
374
 
 
375
def beautify_ETA(length):
 
376
    """
 
377
    Converts the given time in nanoseconds to a fuzzy estimate,
 
378
    intended for progress ETAs, not to indicate a clip's duration.
 
379
    """
 
380
    sec = length / Gst.SECOND
 
381
    mins = sec / 60
 
382
    sec = int(sec % 60)
 
383
    hours = int(mins / 60)
 
384
    mins = int(mins % 60)
 
385
 
 
386
    parts = []
 
387
    if hours > 0:
 
388
        parts.append(ngettext("%d hour", "%d hours", hours) % hours)
 
389
 
 
390
    if mins > 0:
 
391
        parts.append(ngettext("%d minute", "%d minutes", mins) % mins)
 
392
 
 
393
    if hours == 0 and mins < 2 and sec:
 
394
        parts.append(ngettext("%d second", "%d seconds", sec) % sec)
 
395
    return ", ".join(parts)
 
396
 
 
397
 
 
398
#--------------------- Gtk widget helpers ------------------------------------#
 
399
def model(columns, data):
 
400
    ret = Gtk.ListStore(*columns)
 
401
    for datum in data:
 
402
        ret.append(datum)
 
403
    return ret
 
404
 
 
405
 
 
406
def set_combo_value(combo, value, default_index=-1):
 
407
    model = combo.props.model
 
408
    for i, row in enumerate(model):
 
409
        if row[1] == value:
 
410
            combo.set_active(i)
 
411
            return
 
412
    combo.set_active(default_index)
 
413
 
 
414
 
 
415
def get_combo_value(combo):
 
416
    active = combo.get_active()
 
417
    return combo.props.model[active][1]
 
418
 
 
419
 
 
420
def get_value_from_model(model, key):
 
421
    """
 
422
    For a given key, search a gtk ListStore and return the value as a string.
 
423
 
 
424
    If not found and the key is a gst fraction, return a beautified form.
 
425
    """
 
426
    for row in model:
 
427
        if row[1] == key:
 
428
            return str(row[0])
 
429
    if isinstance(key, Gst.Fraction):
 
430
        return "%.3f" % decimal.Decimal(float(key.num) / key.denom)
 
431
    return str(key)
 
432
 
 
433
 
 
434
def alter_style_class(style_class, target_widget, css_style):
 
435
    css_provider = Gtk.CssProvider()
 
436
    toolbar_css = "%s { %s }" % (style_class, css_style)
 
437
    css_provider.load_from_data(toolbar_css.encode('UTF-8'))
 
438
    style_context = target_widget.get_style_context()
 
439
    style_context.add_provider(css_provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION)
 
440
 
 
441
 
 
442
#------------------------ encoding datas ----------------------------------------#
 
443
# FIXME This should into a special file
 
444
frame_rates = model((str, object), (
 
445
    # Translators: fps is for frames per second
 
446
    (_("%d fps") % 12, Gst.Fraction(12.0, 1.0)),
 
447
    (_("%d fps") % 15, Gst.Fraction(15.0, 1.0)),
 
448
    (_("%d fps") % 20, Gst.Fraction(20.0, 1.0)),
 
449
    (_("%.3f fps") % 23.976, Gst.Fraction(24000.0, 1001.0)),
 
450
    (_("%d fps") % 24, Gst.Fraction(24.0, 1.0)),
 
451
    (_("%d fps") % 25, Gst.Fraction(25.0, 1.0)),
 
452
    (_("%.2f fps") % 29.97, Gst.Fraction(30000.0, 1001.0)),
 
453
    (_("%d fps") % 30, Gst.Fraction(30.0, 1.0)),
 
454
    (_("%d fps") % 50, Gst.Fraction(50.0, 1.0)),
 
455
    (_("%.2f fps") % 59.94, Gst.Fraction(60000.0, 1001.0)),
 
456
    (_("%d fps") % 60, Gst.Fraction(60.0, 1.0)),
 
457
    (_("%d fps") % 120, Gst.Fraction(120.0, 1.0)),
 
458
))
 
459
 
 
460
audio_rates = model((str, int), (
 
461
    (_("%d kHz") % 8, 8000),
 
462
    (_("%d kHz") % 11, 11025),
 
463
    (_("%d kHz") % 22, 22050),
 
464
    (_("%.1f kHz") % 44.1, 44100),
 
465
    (_("%d kHz") % 48, 48000),
 
466
    (_("%d kHz") % 96, 96000)))
 
467
 
 
468
audio_channels = model((str, int), (
 
469
    (_("6 Channels (5.1)"), 6),
 
470
    (_("4 Channels (4.0)"), 4),
 
471
    (_("Stereo"), 2),
 
472
    (_("Mono"), 1)))
 
473
 
 
474
# FIXME: are we sure the following tables correct?
 
475
 
 
476
pixel_aspect_ratios = model((str, object), (
 
477
    (_("Square"), Gst.Fraction(1, 1)),
 
478
    (_("480p"), Gst.Fraction(10, 11)),
 
479
    (_("480i"), Gst.Fraction(8, 9)),
 
480
    (_("480p Wide"), Gst.Fraction(40, 33)),
 
481
    (_("480i Wide"), Gst.Fraction(32, 27)),
 
482
    (_("576p"), Gst.Fraction(12, 11)),
 
483
    (_("576i"), Gst.Fraction(16, 15)),
 
484
    (_("576p Wide"), Gst.Fraction(16, 11)),
 
485
    (_("576i Wide"), Gst.Fraction(64, 45)),
 
486
))
 
487
 
 
488
display_aspect_ratios = model((str, object), (
 
489
    (_("Standard (4:3)"), Gst.Fraction(4, 3)),
 
490
    (_("DV (15:11)"), Gst.Fraction(15, 11)),
 
491
    (_("DV Widescreen (16:9)"), Gst.Fraction(16, 9)),
 
492
    (_("Cinema (1.37)"), Gst.Fraction(11, 8)),
 
493
    (_("Cinema (1.66)"), Gst.Fraction(166, 100)),
 
494
    (_("Cinema (1.85)"), Gst.Fraction(185, 100)),
 
495
    (_("Anamorphic (2.35)"), Gst.Fraction(235, 100)),
 
496
    (_("Anamorphic (2.39)"), Gst.Fraction(239, 100)),
 
497
    (_("Anamorphic (2.4)"), Gst.Fraction(24, 10)),
 
498
))