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

« back to all changes in this revision

Viewing changes to pitivi/utils/widgets.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/utils/widgets.py
 
5
#
 
6
# Copyright (c) 2005, Edward Hervey <bilboed@bilboed.com>
 
7
#
 
8
# This program is free software; you can redistribute it and/or
 
9
# modify it under the terms of the GNU Lesser General Public
 
10
# License as published by the Free Software Foundation; either
 
11
# version 2.1 of the License, or (at your option) any later version.
 
12
#
 
13
# This program is distributed in the hope that it will be useful,
 
14
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
15
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 
16
# Lesser General Public License for more details.
 
17
#
 
18
# You should have received a copy of the GNU Lesser General Public
 
19
# License along with this program; if not, write to the
 
20
# Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
 
21
# Boston, MA 02110-1301, USA.
 
22
 
 
23
"""
 
24
A collection of helper classes and routines for:
 
25
    * dynamically creating user interfaces
 
26
    * Creating UI from GstElement-s
 
27
"""
 
28
 
 
29
import math
 
30
import os
 
31
import re
 
32
import sys
 
33
 
 
34
from gi.repository import Gtk
 
35
from gi.repository import Gdk
 
36
from gi.repository import Gst
 
37
from gi.repository import GES
 
38
from gi.repository import Pango
 
39
from gi.repository import GObject
 
40
 
 
41
from gettext import gettext as _
 
42
 
 
43
from pitivi.utils.loggable import Loggable
 
44
from pitivi.configure import get_ui_dir
 
45
from pitivi.utils.ui import beautify_length, \
 
46
    unpack_color, pack_color_32, pack_color_64, \
 
47
    time_to_string, SPACING, CONTROL_WIDTH
 
48
from pitivi.utils.timeline import Zoomable
 
49
 
 
50
 
 
51
ZOOM_FIT = _("Zoom Fit")
 
52
 
 
53
ZOOM_SLIDER_PADDING = SPACING * 4 / 5
 
54
 
 
55
 
 
56
class DynamicWidget(object):
 
57
 
 
58
    """An interface which provides a uniform way to get, set, and observe
 
59
    widget properties"""
 
60
    def __init__(self, default):
 
61
        self.default = default
 
62
 
 
63
    def connectValueChanged(self, callback, *args):
 
64
        raise NotImplementedError
 
65
 
 
66
    def setWidgetValue(self, value):
 
67
        raise NotImplementedError
 
68
 
 
69
    def getWidgetValue(self, value):
 
70
        raise NotImplementedError
 
71
 
 
72
    def getWidgetDefault(self):
 
73
        return self.default
 
74
 
 
75
    def setWidgetToDefault(self):
 
76
        if self.default is not None:
 
77
            self.setWidgetValue(self.default)
 
78
 
 
79
 
 
80
class DefaultWidget(Gtk.Label):
 
81
    """When all hope fails...."""
 
82
 
 
83
    def __init__(self, *unused, **unused_kwargs):
 
84
        Gtk.Label.__init__(self, _("Implement Me"))
 
85
 
 
86
 
 
87
class TextWidget(Gtk.HBox, DynamicWidget):
 
88
    """
 
89
    A Gtk.Entry which emits a "value-changed" signal only when its input is
 
90
    valid (matches the provided regex). If the input is invalid, a warning
 
91
    icon is displayed.
 
92
 
 
93
    You can also connect to the "activate" signal if you don't want to watch
 
94
    for live changes, but it will only be emitted if the input is valid when
 
95
    the user presses Enter.
 
96
    """
 
97
 
 
98
    __gtype_name__ = 'TextWidget'
 
99
    __gsignals__ = {
 
100
        "value-changed": (GObject.SignalFlags.RUN_LAST, None, (),),
 
101
        "activate": (GObject.SignalFlags.RUN_LAST, None, (),)
 
102
    }
 
103
 
 
104
    __INVALID__ = Gdk.Color(0xFFFF, 0, 0)
 
105
    __NORMAL__ = Gdk.Color(0, 0, 0)
 
106
 
 
107
    def __init__(self, matches=None, choices=None, default=None):
 
108
        if not default:
 
109
            # In the case of text widgets, a blank default is an empty string
 
110
            default = ""
 
111
 
 
112
        Gtk.HBox.__init__(self)
 
113
        DynamicWidget.__init__(self, default)
 
114
 
 
115
        self.set_border_width(0)
 
116
        self.set_spacing(0)
 
117
        if choices:
 
118
            self.combo = Gtk.ComboBoxText.new_with_entry()
 
119
            self.text = self.combo.get_child()
 
120
            self.combo.show()
 
121
            self.pack_start(self.combo, expand=True, fill=True, padding=0)
 
122
            for choice in choices:
 
123
                self.combo.append_text(choice)
 
124
        else:
 
125
            self.text = Gtk.Entry()
 
126
            self.text.show()
 
127
            self.pack_start(self.text, expand=True, fill=True, padding=0)
 
128
        self.matches = None
 
129
        self.last_valid = None
 
130
        self.valid = False
 
131
        self.send_signal = True
 
132
        self.text.connect("changed", self._textChanged)
 
133
        self.text.connect("activate", self._activateCb)
 
134
        if matches:
 
135
            if type(matches) is str:
 
136
                self.matches = re.compile(matches)
 
137
            else:
 
138
                self.matches = matches
 
139
            self._textChanged(None)
 
140
 
 
141
    def connectValueChanged(self, callback, *args):
 
142
        return self.connect("value-changed", callback, *args)
 
143
 
 
144
    def setWidgetValue(self, value, send_signal=True):
 
145
        self.send_signal = send_signal
 
146
        self.text.set_text(value)
 
147
 
 
148
    def getWidgetValue(self):
 
149
        if self.matches:
 
150
            return self.last_valid
 
151
        return self.text.get_text()
 
152
 
 
153
    def addChoices(self, choices):
 
154
        for choice in choices:
 
155
            self.combo.append_text(choice)
 
156
 
 
157
    def _textChanged(self, unused_widget):
 
158
        text = self.text.get_text()
 
159
        if self.matches:
 
160
            if self._filter(text):
 
161
                self.last_valid = text
 
162
                if self.send_signal:
 
163
                    self.emit("value-changed")
 
164
                if not self.valid:
 
165
                    self.text.set_icon_from_icon_name(1, None)
 
166
                self.valid = True
 
167
            else:
 
168
                if self.valid:
 
169
                    self.text.set_icon_from_icon_name(1, "dialog-warning")
 
170
                self.valid = False
 
171
        elif self.send_signal:
 
172
            self.emit("value-changed")
 
173
 
 
174
        self.send_signal = True
 
175
 
 
176
    def _activateCb(self, unused_widget):
 
177
        """
 
178
        Similar to _textChanged, to account for the case where we connect to
 
179
        the "activate" signal instead of "text-changed".
 
180
 
 
181
        We don't need to set the icons or anything like that, as _textChanged
 
182
        does it already.
 
183
        """
 
184
        if self.matches and self.send_signal:
 
185
            self.emit("activate")
 
186
 
 
187
    def _filter(self, text):
 
188
        match = self.matches.match(text)
 
189
        if match is not None:
 
190
            return True
 
191
        return False
 
192
 
 
193
    def set_width_chars(self, width):
 
194
        """Allows setting the width of the text entry widget for compactness."""
 
195
        self.text.set_width_chars(width)
 
196
 
 
197
 
 
198
class NumericWidget(Gtk.HBox, DynamicWidget):
 
199
 
 
200
    """An horizontal Gtk.Scale and a Gtk.SpinButton which share an adjustment.
 
201
    The SpinButton is always displayed, while the Scale only appears if both
 
202
    lower and upper bounds are defined"""
 
203
 
 
204
    def __init__(self, upper=None, lower=None, default=None):
 
205
        Gtk.HBox.__init__(self)
 
206
        DynamicWidget.__init__(self, default)
 
207
 
 
208
        self.spacing = SPACING
 
209
        self.adjustment = Gtk.Adjustment()
 
210
        self.upper = upper
 
211
        self.lower = lower
 
212
        self._type = None
 
213
        if (lower is not None and upper is not None) and (lower > -5000 and upper < 5000):
 
214
            self.slider = Gtk.Scale.new(Gtk.Orientation.HORIZONTAL, self.adjustment)
 
215
            self.pack_start(self.slider, expand=True, fill=True, padding=0)
 
216
            self.slider.show()
 
217
            self.slider.props.draw_value = False
 
218
            # Abuse GTK3's progressbar "fill level" feature to provide
 
219
            # a visual indication of the default value on property sliders.
 
220
            if default is not None:
 
221
                self.slider.set_restrict_to_fill_level(False)
 
222
                self.slider.set_fill_level(float(default))
 
223
                self.slider.set_show_fill_level(True)
 
224
 
 
225
        if upper is None:
 
226
            upper = GObject.G_MAXDOUBLE
 
227
        if lower is None:
 
228
            lower = GObject.G_MINDOUBLE
 
229
        range = upper - lower
 
230
        self.adjustment.props.lower = lower
 
231
        self.adjustment.props.upper = upper
 
232
        self.spinner = Gtk.SpinButton(adjustment=self.adjustment)
 
233
        self.pack_end(self.spinner, fill=True, expand=not hasattr(self, 'slider'), padding=0)
 
234
        self.spinner.show()
 
235
 
 
236
    def connectValueChanged(self, callback, *args):
 
237
        self.adjustment.connect("value-changed", callback, *args)
 
238
 
 
239
    def getWidgetValue(self):
 
240
        if self._type:
 
241
            return self._type(self.adjustment.get_value())
 
242
 
 
243
        return self.adjustment.get_value()
 
244
 
 
245
    def setWidgetValue(self, value):
 
246
        type_ = type(value)
 
247
        if self._type is None:
 
248
            self._type = type_
 
249
 
 
250
        if type_ == int or type_ == long:
 
251
            minimum, maximum = (-sys.maxint, sys.maxint)
 
252
            step = 1.0
 
253
            page = 10.0
 
254
        elif type_ == float:
 
255
            minimum, maximum = (GObject.G_MINDOUBLE, GObject.G_MAXDOUBLE)
 
256
            step = 0.01
 
257
            page = 0.1
 
258
            self.spinner.props.digits = 2
 
259
        if self.lower is not None:
 
260
            minimum = self.lower
 
261
        if self.upper is not None:
 
262
            maximum = self.upper
 
263
        self.adjustment.configure(value, minimum, maximum, step, page, 0)
 
264
        self.spinner.set_adjustment(self.adjustment)
 
265
 
 
266
 
 
267
class TimeWidget(TextWidget, DynamicWidget):
 
268
    """
 
269
    A widget that contains a time in nanoseconds. Accepts timecode formats
 
270
    or a frame number (integer).
 
271
    """
 
272
    # The "frame number" match rule is ^([0-9]+)$ (with a + to require 1 digit)
 
273
    # The "timecode" rule is ^([0-9]:[0-5][0-9]:[0-5][0-9])\.[0-9][0-9][0-9]$"
 
274
    # Combining the two, we get:
 
275
    VALID_REGEX = re.compile("^([0-9]+)$|^([0-9]:)?([0-5][0-9]:[0-5][0-9])\.[0-9][0-9][0-9]$")
 
276
 
 
277
    __gtype_name__ = 'TimeWidget'
 
278
 
 
279
    def __init__(self, default=None):
 
280
        DynamicWidget.__init__(self, default)
 
281
        TextWidget.__init__(self, self.VALID_REGEX)
 
282
        TextWidget.set_width_chars(self, 10)
 
283
        self._framerate = None
 
284
 
 
285
    def getWidgetValue(self):
 
286
        timecode = TextWidget.getWidgetValue(self)
 
287
 
 
288
        if ":" in timecode:
 
289
            parts = timecode.split(":")
 
290
            if len(parts) == 2:
 
291
                hh = 0
 
292
                mm, end = parts
 
293
            else:
 
294
                hh, mm, end = parts
 
295
            ss, millis = end.split(".")
 
296
            nanosecs = int(hh) * 3.6 * 10e12 \
 
297
                + int(mm) * 6 * 10e10 \
 
298
                + int(ss) * 10e9 \
 
299
                + int(millis) * 10e6
 
300
            nanosecs = nanosecs / 10  # Compensate the 10 factor of e notation
 
301
        else:
 
302
            # We were given a frame number. Convert from the project framerate.
 
303
            frame_no = int(timecode)
 
304
            nanosecs = frame_no / float(self._framerate) * Gst.SECOND
 
305
        # The seeker won't like floating point nanoseconds!
 
306
        return int(nanosecs)
 
307
 
 
308
    def setWidgetValue(self, timeNanos, send_signal=True):
 
309
        timecode = time_to_string(timeNanos)
 
310
        if timecode.startswith("0:"):
 
311
            timecode = timecode[2:]
 
312
        TextWidget.setWidgetValue(self, timecode, send_signal=send_signal)
 
313
 
 
314
    def connectActivateEvent(self, activateCb):
 
315
        return self.connect("activate", activateCb)
 
316
 
 
317
    def connectFocusEvents(self, focusInCb, focusOutCb):
 
318
        fIn = self.text.connect("focus-in-event", focusInCb)
 
319
        fOut = self.text.connect("focus-out-event", focusOutCb)
 
320
 
 
321
        return [fIn, fOut]
 
322
 
 
323
    def setFramerate(self, framerate):
 
324
        self._framerate = framerate
 
325
 
 
326
 
 
327
class FractionWidget(TextWidget, DynamicWidget):
 
328
 
 
329
    """A Gtk.ComboBoxEntry """
 
330
 
 
331
    fraction_regex = re.compile(
 
332
        "^([0-9]*(\.[0-9]+)?)(([:/][0-9]*(\.[0-9]+)?)|M)?$")
 
333
    __gtype_name__ = 'FractionWidget'
 
334
 
 
335
    def __init__(self, range=None, presets=None, default=None):
 
336
        DynamicWidget.__init__(self, default)
 
337
 
 
338
        if range:
 
339
            flow = float(range.low)
 
340
            fhigh = float(range.high)
 
341
        else:
 
342
            flow = float("-Infinity")
 
343
            fhigh = float("Infinity")
 
344
        choices = []
 
345
        if presets:
 
346
            for preset in presets:
 
347
                if type(preset) is str:
 
348
                    strval = preset
 
349
                    preset = self._parseText(preset)
 
350
                else:
 
351
                    strval = "%g:%g" % (preset.num, preset.denom)
 
352
                fpreset = float(preset)
 
353
                if flow <= fpreset and fpreset <= fhigh:
 
354
                    choices.append(strval)
 
355
        self.low = flow
 
356
        self.high = fhigh
 
357
        TextWidget.__init__(self, self.fraction_regex, choices)
 
358
 
 
359
    def _filter(self, text):
 
360
        if TextWidget._filter(self, text):
 
361
            value = self._parseText(text)
 
362
            if self.low <= float(value) and float(value) <= self.high:
 
363
                return True
 
364
        return False
 
365
 
 
366
    def addPresets(self, presets):
 
367
        choices = []
 
368
        for preset in presets:
 
369
            if type(preset) is str:
 
370
                strval = preset
 
371
                preset = self._parseText(preset)
 
372
            else:
 
373
                strval = "%g:%g" % (preset.num, preset.denom)
 
374
            fpreset = float(preset)
 
375
            if self.low <= fpreset and fpreset <= self.high:
 
376
                choices.append(strval)
 
377
 
 
378
        self.addChoices(choices)
 
379
 
 
380
    def setWidgetValue(self, value):
 
381
        if type(value) is str:
 
382
            value = self._parseText(value)
 
383
        elif not hasattr(value, "denom"):
 
384
            value = Gst.Fraction(value)
 
385
        if (value.denom / 1001) == 1:
 
386
            text = "%gM" % (value.num / 1000)
 
387
        else:
 
388
            text = "%g:%g" % (value.num, value.denom)
 
389
 
 
390
        self.text.set_text(text)
 
391
 
 
392
    def getWidgetValue(self):
 
393
        if self.last_valid:
 
394
            return self._parseText(self.last_valid)
 
395
        return Gst.Fraction(1, 1)
 
396
 
 
397
    def _parseText(self, text):
 
398
        match = self.fraction_regex.match(text)
 
399
        groups = match.groups()
 
400
        num = 1.0
 
401
        denom = 1.0
 
402
        if groups[0]:
 
403
            num = float(groups[0])
 
404
        if groups[2]:
 
405
            if groups[2] == "M":
 
406
                num = num * 1000
 
407
                denom = 1001
 
408
            elif groups[2][1:]:
 
409
                denom = float(groups[2][1:])
 
410
        return Gst.Fraction(num, denom)
 
411
 
 
412
 
 
413
class ToggleWidget(Gtk.CheckButton, DynamicWidget):
 
414
 
 
415
    """A Gtk.CheckButton which supports the DynamicWidget interface."""
 
416
 
 
417
    def __init__(self, default=None):
 
418
        Gtk.CheckButton.__init__(self)
 
419
        DynamicWidget.__init__(self, default)
 
420
 
 
421
    def connectValueChanged(self, callback, *args):
 
422
        self.connect("toggled", callback, *args)
 
423
 
 
424
    def setWidgetValue(self, value):
 
425
        self.set_active(value)
 
426
 
 
427
    def getWidgetValue(self):
 
428
        return self.get_active()
 
429
 
 
430
 
 
431
class ChoiceWidget(Gtk.HBox, DynamicWidget):
 
432
 
 
433
    """Abstractly, represents a choice between a list of named values. The
 
434
    association between value names and values is arbitrary. The current
 
435
    implementation uses a Gtk.ComboBoxText for simplicity."""
 
436
 
 
437
    def __init__(self, choices, default=None):
 
438
        Gtk.HBox.__init__(self)
 
439
        DynamicWidget.__init__(self, default)
 
440
        self.choices = None
 
441
        self.values = None
 
442
        self.contents = Gtk.ComboBoxText()
 
443
        self.pack_start(self.contents, expand=True, fill=True, padding=0)
 
444
        self.setChoices(choices)
 
445
        self.contents.show()
 
446
        cell = self.contents.get_cells()[0]
 
447
        cell.props.ellipsize = Pango.EllipsizeMode.END
 
448
 
 
449
    def connectValueChanged(self, callback, *args):
 
450
        return self.contents.connect("changed", callback, *args)
 
451
 
 
452
    def setWidgetValue(self, value):
 
453
        try:
 
454
            self.contents.set_active(self.values.index(value))
 
455
        except ValueError:
 
456
            raise ValueError("%r not in %r" % (value, self.values))
 
457
 
 
458
    def getWidgetValue(self):
 
459
        return self.values[self.contents.get_active()]
 
460
 
 
461
    def setChoices(self, choices):
 
462
        self.choices = [choice[0] for choice in choices]
 
463
        self.values = [choice[1] for choice in choices]
 
464
        m = Gtk.ListStore(str)
 
465
        self.contents.set_model(m)
 
466
        for choice, value in choices:
 
467
            self.contents.append_text(_(choice))
 
468
        if len(choices) <= 1:
 
469
            self.contents.set_sensitive(False)
 
470
        else:
 
471
            self.contents.set_sensitive(True)
 
472
 
 
473
 
 
474
class PathWidget(Gtk.FileChooserButton, DynamicWidget):
 
475
 
 
476
    """A Gtk.FileChooserButton which supports the DynamicWidget interface."""
 
477
 
 
478
    __gtype_name__ = 'PathWidget'
 
479
 
 
480
    __gsignals__ = {
 
481
        "value-changed": (GObject.SignalFlags.RUN_LAST,
 
482
            None,
 
483
            ()),
 
484
    }
 
485
 
 
486
    def __init__(self, action=Gtk.FileChooserAction.OPEN, default=None):
 
487
        DynamicWidget.__init__(self, default)
 
488
        self.dialog = Gtk.FileChooserDialog(action=action)
 
489
        self.dialog.add_buttons(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, Gtk.STOCK_CLOSE, Gtk.ResponseType.CLOSE)
 
490
        self.dialog.set_default_response(Gtk.ResponseType.OK)
 
491
        Gtk.FileChooserButton.__init__(self, self.dialog)
 
492
        self.dialog.connect("response", self._responseCb)
 
493
        self.uri = ""
 
494
 
 
495
    def connectValueChanged(self, callback, *args):
 
496
        return self.connect("value-changed", callback, *args)
 
497
 
 
498
    def setWidgetValue(self, value):
 
499
        self.set_uri(value)
 
500
        self.uri = value
 
501
 
 
502
    def getWidgetValue(self):
 
503
        return self.uri
 
504
 
 
505
    def _responseCb(self, unused_dialog, response):
 
506
        if response == Gtk.ResponseType.CLOSE:
 
507
            self.uri = self.get_uri()
 
508
            self.emit("value-changed")
 
509
            self.dialog.hide()
 
510
 
 
511
 
 
512
class ColorWidget(Gtk.ColorButton, DynamicWidget):
 
513
 
 
514
    def __init__(self, value_type=str, default=None):
 
515
        Gtk.ColorButton.__init__(self)
 
516
        DynamicWidget.__init__(self, default)
 
517
        self.value_type = value_type
 
518
        self.set_use_alpha(True)
 
519
 
 
520
    def connectValueChanged(self, callback, *args):
 
521
        self.connect("color-set", callback, *args)
 
522
 
 
523
    def setWidgetValue(self, value):
 
524
        type_ = type(value)
 
525
        alpha = 0xFFFF
 
526
 
 
527
        if type_ is str:
 
528
            color = Gdk.Color(value)
 
529
        elif (type_ is int) or (type_ is long):
 
530
            red, green, blue, alpha = unpack_color(value)
 
531
            color = Gdk.Color(red, green, blue)
 
532
        elif type_ is Gdk.Color:
 
533
            color = value
 
534
        else:
 
535
            raise TypeError("%r is not something we can convert to a color" %
 
536
                value)
 
537
        self.set_color(color)
 
538
        self.set_alpha(alpha)
 
539
 
 
540
    def getWidgetValue(self):
 
541
        color = self.get_color()
 
542
        alpha = self.get_alpha()
 
543
        if self.value_type is int:
 
544
            return pack_color_32(color.red, color.green, color.blue, alpha)
 
545
        if self.value_type is long:
 
546
            return pack_color_64(color.red, color.green, color.blue, alpha)
 
547
        elif self.value_type is Gdk.Color:
 
548
            return color
 
549
        return color.to_string()
 
550
 
 
551
 
 
552
class FontWidget(Gtk.FontButton, DynamicWidget):
 
553
 
 
554
    def __init__(self, default=None):
 
555
        Gtk.FontButton.__init__(self)
 
556
        DynamicWidget.__init__(self, default)
 
557
        self.set_use_font(True)
 
558
 
 
559
    def connectValueChanged(self, callback, *args):
 
560
        self.connect("font-set", callback, *args)
 
561
 
 
562
    def setWidgetValue(self, font_name):
 
563
        self.set_font_name(font_name)
 
564
 
 
565
    def getWidgetValue(self):
 
566
        return self.get_font_name()
 
567
 
 
568
 
 
569
class GstElementSettingsWidget(Gtk.VBox, Loggable):
 
570
    """
 
571
    Widget to view/modify properties of a Gst.Element
 
572
    """
 
573
 
 
574
    def __init__(self, isControllable=True):
 
575
        Gtk.VBox.__init__(self)
 
576
        Loggable.__init__(self)
 
577
        self.element = None
 
578
        self.ignore = None
 
579
        self.properties = None
 
580
        self.buttons = {}
 
581
        self.isControllable = isControllable
 
582
 
 
583
    def resetKeyframeToggleButtons(self, widget=None):
 
584
        """
 
585
        Reset all the keyframe togglebuttons for all properties.
 
586
        If a property widget is specified, reset only its keyframe togglebutton.
 
587
        """
 
588
        if widget:
 
589
            # Use the dynamic widget (that has been provided as an argument)
 
590
            # to find which of the togglebuttons is the related one.
 
591
            self.log("Resetting one keyframe button")
 
592
            for togglebutton in self.keyframeToggleButtons.keys():
 
593
                if self.keyframeToggleButtons[togglebutton] is widget:
 
594
                    # The dynamic widget matches the one
 
595
                    # related to the current to the current togglebutton
 
596
                    togglebutton.set_label("◇")
 
597
                    self._setKeyframeToggleButtonState(togglebutton, False)
 
598
                    break  # Stop searching
 
599
        else:
 
600
            self.log("Resetting all keyframe buttons")
 
601
            for togglebutton in self.keyframeToggleButtons.keys():
 
602
                togglebutton.set_label("◇")
 
603
                self._setKeyframeToggleButtonState(togglebutton, False)
 
604
 
 
605
        effect = self.element
 
606
        for track_element in effect.get_parent().get_children(False):
 
607
            if hasattr(track_element, "ui_element"):
 
608
                track_element.ui_element.hideKeyframes()
 
609
 
 
610
    def setElement(self, element, properties={}, ignore=['name'],
 
611
                   default_btn=False, use_element_props=False):
 
612
        """
 
613
        Set given element on Widget, with optional properties
 
614
        """
 
615
        self.info("element: %s, use properties: %s", element, properties)
 
616
        self.element = element
 
617
        self.ignore = ignore
 
618
        self.properties = {}
 
619
        self._addWidgets(properties, default_btn, use_element_props)
 
620
 
 
621
    def _addWidgets(self, properties, default_btn, use_element_props):
 
622
        """
 
623
        Prepare a gtk table containing the property widgets of an element.
 
624
        Each property is on a separate row of the table.
 
625
        A row is typically a label followed by the widget and a reset button.
 
626
 
 
627
        If there are no properties, returns a table containing the label
 
628
        "No properties."
 
629
        """
 
630
        self.bindings = {}
 
631
        self.keyframeToggleButtons = {}
 
632
        is_effect = False
 
633
        if isinstance(self.element, GES.Effect):
 
634
            is_effect = True
 
635
            props = [prop for prop in self.element.list_children_properties() if not prop.name in self.ignore]
 
636
        else:
 
637
            props = [prop for prop in GObject.list_properties(self.element) if not prop.name in self.ignore]
 
638
        if not props:
 
639
            table = Gtk.Table(n_rows=1, n_columns=1)
 
640
            widget = Gtk.Label(label=_("No properties."))
 
641
            widget.set_sensitive(False)
 
642
            table.attach(widget, 0, 1, 0, 1, yoptions=Gtk.AttachOptions.FILL)
 
643
            self.pack_start(table, expand=True, fill=True, padding=0)
 
644
            self.show_all()
 
645
            return
 
646
 
 
647
        if default_btn:
 
648
            table = Gtk.Table(n_rows=len(props), n_columns=4)
 
649
        else:
 
650
            table = Gtk.Table(n_rows=len(props), n_columns=3)
 
651
 
 
652
        table.set_row_spacings(SPACING)
 
653
        table.set_col_spacings(SPACING)
 
654
        table.set_border_width(SPACING)
 
655
 
 
656
        y = 0
 
657
        for prop in props:
 
658
            # We do not know how to work with GObjects, so blacklist
 
659
            # them to avoid noise in the UI
 
660
            if (not prop.flags & GObject.PARAM_WRITABLE or
 
661
              not prop.flags & GObject.PARAM_READABLE or
 
662
              GObject.type_is_a(prop.value_type, GObject.Object)):
 
663
                continue
 
664
 
 
665
            if is_effect:
 
666
                result, prop_value = self.element.get_child_property(prop.name)
 
667
                if result is False:
 
668
                    self.debug("Could not get value for property: %s", prop.name)
 
669
            else:
 
670
                if use_element_props:
 
671
                    prop_value = self.element.get_property(prop.name)
 
672
                else:
 
673
                    prop_value = properties.get(prop.name)
 
674
 
 
675
            widget = self._makePropertyWidget(prop, prop_value)
 
676
            if isinstance(widget, ToggleWidget):
 
677
                widget.set_label(prop.nick)
 
678
                table.attach(widget, 0, 2, y, y + 1, yoptions=Gtk.AttachOptions.FILL)
 
679
            else:
 
680
                label = Gtk.Label(label=prop.nick + ":")
 
681
                label.set_alignment(0.0, 0.5)
 
682
                table.attach(label, 0, 1, y, y + 1, xoptions=Gtk.AttachOptions.FILL, yoptions=Gtk.AttachOptions.FILL)
 
683
                table.attach(widget, 1, 2, y, y + 1, yoptions=Gtk.AttachOptions.FILL)
 
684
 
 
685
            if not isinstance(widget, ToggleWidget) and not isinstance(widget, ChoiceWidget) and self.isControllable:
 
686
                button = self._getKeyframeToggleButton(prop)
 
687
                self.keyframeToggleButtons[button] = widget
 
688
                table.attach(button, 3, 4, y, y + 1, xoptions=Gtk.AttachOptions.FILL, yoptions=Gtk.AttachOptions.FILL)
 
689
 
 
690
            if hasattr(prop, 'blurb'):
 
691
                widget.set_tooltip_text(prop.blurb)
 
692
 
 
693
            self.properties[prop] = widget
 
694
 
 
695
            # The "reset to default" button associated with this property
 
696
            if default_btn:
 
697
                widget.propName = prop.name.split("-")[0]
 
698
                name = prop.name
 
699
 
 
700
                if self.isControllable:
 
701
                    # If this element is controlled, the value means nothing anymore.
 
702
                    binding = self.element.get_control_binding(prop.name)
 
703
                    if binding:
 
704
                        widget.set_sensitive(False)
 
705
                        self.bindings[widget] = binding
 
706
                button = self._getResetToDefaultValueButton(prop, widget)
 
707
                table.attach(button, 2, 3, y, y + 1, xoptions=Gtk.AttachOptions.FILL, yoptions=Gtk.AttachOptions.FILL)
 
708
                self.buttons[button] = widget
 
709
 
 
710
            self.element.connect('notify::' + prop.name, self._propertyChangedCb, widget)
 
711
 
 
712
            y += 1
 
713
 
 
714
        self.pack_start(table, expand=True, fill=True, padding=0)
 
715
        self.show_all()
 
716
 
 
717
    def _propertyChangedCb(self, unused_element, pspec, widget):
 
718
        widget.setWidgetValue(self.element.get_property(pspec.name))
 
719
 
 
720
    def _getKeyframeToggleButton(self, prop):
 
721
        button = Gtk.ToggleButton()
 
722
        button.set_label("◇")
 
723
        button.props.focus_on_click = False  # Avoid the ugly selection outline
 
724
        button.set_tooltip_text(_("Show keyframes for this value"))
 
725
        button.connect('toggled', self._showKeyframesToggledCb, prop)
 
726
        return button
 
727
 
 
728
    def _getResetToDefaultValueButton(self, unused_prop, widget):
 
729
        icon = Gtk.Image()
 
730
        icon.set_from_icon_name("edit-clear-all-symbolic", Gtk.IconSize.MENU)
 
731
        button = Gtk.Button()
 
732
        button.add(icon)
 
733
        button.set_tooltip_text(_("Reset to default value"))
 
734
        button.set_relief(Gtk.ReliefStyle.NONE)
 
735
        button.connect('clicked', self._defaultBtnClickedCb, widget)
 
736
        return button
 
737
 
 
738
    def _setKeyframeToggleButtonState(self, button, active_state):
 
739
        """
 
740
        This is meant for programmatically (un)pushing the provided keyframe
 
741
        togglebutton, without triggering its signals.
 
742
        """
 
743
        self.log("Manually resetting the UI state of %s" % button)
 
744
        button.handler_block_by_func(self._showKeyframesToggledCb)
 
745
        button.set_active(active_state)
 
746
        button.handler_unblock_by_func(self._showKeyframesToggledCb)
 
747
 
 
748
    def _showKeyframesToggledCb(self, button, prop):
 
749
        self.log("keyframes togglebutton clicked for %s" % prop)
 
750
        active = button.get_active()
 
751
        # Disable the related dynamic gst property widget
 
752
        widget = self.keyframeToggleButtons[button]
 
753
        widget.set_sensitive(False)
 
754
        # Now change the state of the *other* togglebuttons.
 
755
        for togglebutton in self.keyframeToggleButtons.keys():
 
756
            if togglebutton != button:
 
757
                # Don't use set_active directly on the buttons; doing so will
 
758
                # fire off signals that will toggle the others/confuse the UI
 
759
                self._setKeyframeToggleButtonState(togglebutton, False)
 
760
        # We always set this label, since the only way to *deactivate* keyframes
 
761
        # (not just hide them temporarily) is to use the separate reset button.
 
762
        button.set_label("◆")
 
763
 
 
764
        effect = self.element
 
765
        track_type = effect.get_track_type()
 
766
        for track_element in effect.get_parent().get_children(False):
 
767
            if active and hasattr(track_element, "ui_element") and track_type == track_element.get_track_type():
 
768
                track_element.ui_element.showKeyframes(effect, prop)
 
769
                binding = self.element.get_control_binding(prop.name)
 
770
                self.bindings[widget] = binding
 
771
            elif hasattr(track_element, "ui_element") and track_type == track_element.get_track_type():
 
772
                track_element.ui_element.hideKeyframes()
 
773
 
 
774
    def _defaultBtnClickedCb(self, unused_button, widget):
 
775
        try:
 
776
            binding = self.bindings[widget]
 
777
        except KeyError:
 
778
            binding = None
 
779
        if binding:
 
780
            effect = self.element
 
781
            track_type = effect.get_track_type()
 
782
            for track_element in effect.get_parent().get_children(False):
 
783
                if hasattr(track_element, "ui_element") and track_type == track_element.get_track_type():
 
784
                    binding.props.control_source.unset_all()
 
785
                    track_element.ui_element.updateKeyframes()
 
786
 
 
787
        widget.set_sensitive(True)
 
788
        widget.setWidgetToDefault()
 
789
        self.resetKeyframeToggleButtons(widget)
 
790
 
 
791
    def getSettings(self, with_default=False):
 
792
        """
 
793
        returns the dictionnary of propertyname/propertyvalue
 
794
        """
 
795
        d = {}
 
796
        for prop, widget in self.properties.iteritems():
 
797
            if not prop.flags & GObject.PARAM_WRITABLE:
 
798
                continue
 
799
            if isinstance(widget, DefaultWidget):
 
800
                continue
 
801
            value = widget.getWidgetValue()
 
802
            if value is not None and (value != prop.default_value or with_default):
 
803
                d[prop.name] = value
 
804
        return d
 
805
 
 
806
    def _makePropertyWidget(self, prop, value=None):
 
807
        """ Creates a Widget for the specified element property """
 
808
        type_name = GObject.type_name(prop.value_type.fundamental)
 
809
        if type_name == "gchararray":
 
810
            widget = TextWidget(default=prop.default_value)
 
811
        elif type_name in ['guint64', 'gint64', 'guint', 'gint', 'gfloat', 'gulong', 'gdouble']:
 
812
            maximum, minimum = None, None
 
813
            if hasattr(prop, "minimum"):
 
814
                minimum = prop.minimum
 
815
            if hasattr(prop, "maximum"):
 
816
                maximum = prop.maximum
 
817
            widget = NumericWidget(default=prop.default_value, upper=maximum, lower=minimum)
 
818
        elif type_name == "gboolean":
 
819
            widget = ToggleWidget(default=prop.default_value)
 
820
        elif type_name == "GEnum":
 
821
            choices = []
 
822
            for key, val in prop.enum_class.__enum_values__.iteritems():
 
823
                choices.append([val.value_name, int(val)])
 
824
            widget = ChoiceWidget(choices, default=prop.default_value)
 
825
        elif type_name == "GstFraction":
 
826
            widget = FractionWidget(None, presets=["0:1"], default=prop.default_value)
 
827
        else:
 
828
            # TODO: implement widgets for: GBoxed, GFlags
 
829
            self.fixme("Unsupported property type: %s", type_name)
 
830
            widget = DefaultWidget()
 
831
 
 
832
        if value is None:
 
833
            value = prop.default_value
 
834
        if value is not None and not isinstance(widget, DefaultWidget):
 
835
            widget.setWidgetValue(value)
 
836
 
 
837
        return widget
 
838
 
 
839
 
 
840
class GstElementSettingsDialog(Loggable):
 
841
    """
 
842
    Dialog window for viewing/modifying properties of a Gst.Element
 
843
    """
 
844
 
 
845
    def __init__(self, elementfactory, properties, parent_window=None, isControllable=True):
 
846
        Loggable.__init__(self)
 
847
        self.debug("factory: %s, properties: %s", elementfactory, properties)
 
848
 
 
849
        self.builder = Gtk.Builder()
 
850
        self.builder.add_from_file(os.path.join(get_ui_dir(), "elementsettingsdialog.ui"))
 
851
        self.builder.connect_signals(self)
 
852
        self.ok_btn = self.builder.get_object("okbutton1")
 
853
 
 
854
        self.window = self.builder.get_object("dialog1")
 
855
        self.elementsettings = GstElementSettingsWidget(isControllable)
 
856
        self.builder.get_object("viewport1").add(self.elementsettings)
 
857
 
 
858
        self.factory = elementfactory
 
859
        self.element = self.factory.create("elementsettings")
 
860
        if not self.element:
 
861
            self.warning("Couldn't create element from factory %s", self.factory)
 
862
        self.properties = properties
 
863
        self._fillWindow()
 
864
 
 
865
        # Try to avoid scrolling, whenever possible.
 
866
        screen_height = self.window.get_screen().get_height()
 
867
        contents_height = self.elementsettings.size_request().height
 
868
        maximum_contents_height = max(500, 0.7 * screen_height)
 
869
        if contents_height < maximum_contents_height:
 
870
            # The height of the content is small enough, disable the scrollbars.
 
871
            default_height = -1
 
872
            scrolledwindow = self.builder.get_object("scrolledwindow1")
 
873
            scrolledwindow.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.NEVER)
 
874
            scrolledwindow.set_shadow_type(Gtk.ShadowType.NONE)
 
875
        else:
 
876
            # If we need to scroll, set a reasonable height for the window.
 
877
            default_height = 600
 
878
        self.window.set_default_size(400, default_height)
 
879
 
 
880
        if parent_window:
 
881
            self.window.set_transient_for(parent_window)
 
882
        self.window.show()
 
883
 
 
884
    def _fillWindow(self):
 
885
        # set title and frame label
 
886
        self.window.set_title(_("Properties for %s") % self.factory.get_longname())
 
887
        self.elementsettings.setElement(self.element, self.properties)
 
888
 
 
889
    def getSettings(self):
 
890
        """ returns the property/value dictionnary of the selected settings """
 
891
        return self.elementsettings.getSettings()
 
892
 
 
893
    def _resetValuesClickedCb(self, unused_button):
 
894
        self.resetAll()
 
895
 
 
896
    def resetAll(self):
 
897
        for prop, widget in self.elementsettings.properties.iteritems():
 
898
            widget.setWidgetToDefault()
 
899
 
 
900
 
 
901
class BaseTabs(Gtk.Notebook):
 
902
    def __init__(self, app, hide_hpaned=False):
 
903
        """ initialize """
 
904
        Gtk.Notebook.__init__(self)
 
905
        self.set_border_width(SPACING)
 
906
 
 
907
        self.connect("create-window", self._createWindowCb)
 
908
        self._hide_hpaned = hide_hpaned
 
909
        self.app = app
 
910
        self._createUi()
 
911
 
 
912
    def _createUi(self):
 
913
        """ set up the gui """
 
914
        settings = self.get_settings()
 
915
        settings.props.gtk_dnd_drag_threshold = 1
 
916
        self.set_tab_pos(Gtk.PositionType.TOP)
 
917
 
 
918
    def append_page(self, child, label):
 
919
        Gtk.Notebook.append_page(self, child, label)
 
920
        self._set_child_properties(child, label)
 
921
        child.show()
 
922
        label.show()
 
923
 
 
924
    def _set_child_properties(self, child, label):
 
925
        self.child_set_property(child, "detachable", True)
 
926
        self.child_set_property(child, "tab-expand", False)
 
927
        self.child_set_property(child, "tab-fill", True)
 
928
        label.props.xalign = 0.0
 
929
 
 
930
    def _detachedComponentWindowDestroyCb(self, window, child,
 
931
            original_position, label):
 
932
        notebook = window.get_child()
 
933
        position = notebook.child_get_property(child, "position")
 
934
        notebook.remove_page(position)
 
935
        label = Gtk.Label(label=label)
 
936
        self.insert_page(child, label, original_position)
 
937
        self._set_child_properties(child, label)
 
938
        self.child_set_property(child, "detachable", True)
 
939
 
 
940
        if self._hide_hpaned:
 
941
            self._showSecondHpanedInMainWindow()
 
942
 
 
943
    def _createWindowCb(self, unused_from_notebook, child, x, y):
 
944
        original_position = self.child_get_property(child, "position")
 
945
        label = self.child_get_property(child, "tab-label")
 
946
        window = Gtk.Window()
 
947
        window.set_type_hint(Gdk.WindowTypeHint.UTILITY)
 
948
 
 
949
        window.set_title(label)
 
950
        window.set_default_size(600, 400)
 
951
        window.connect("destroy", self._detachedComponentWindowDestroyCb,
 
952
                child, original_position, label)
 
953
        notebook = Gtk.Notebook()
 
954
        notebook.props.show_tabs = False
 
955
        window.add(notebook)
 
956
 
 
957
        window.show_all()
 
958
        # set_uposition is deprecated but what should I use instead?
 
959
        window.set_uposition(x, y)
 
960
 
 
961
        if self._hide_hpaned:
 
962
            self._hideSecondHpanedInMainWindow()
 
963
 
 
964
        return notebook
 
965
 
 
966
    def _hideSecondHpanedInMainWindow(self):
 
967
        self.app.gui.mainhpaned.remove(self.app.gui.secondhpaned)
 
968
        self.app.gui.secondhpaned.remove(self.app.gui.projecttabs)
 
969
        self.app.gui.secondhpaned.remove(self.app.gui.propertiestabs)
 
970
        self.app.gui.mainhpaned.pack1(self.app.gui.projecttabs, resize=True,
 
971
                                      shrink=False)
 
972
 
 
973
    def _showSecondHpanedInMainWindow(self):
 
974
        self.app.gui.mainhpaned.remove(self.app.gui.projecttabs)
 
975
        self.app.gui.secondhpaned.pack1(self.app.gui.projecttabs,
 
976
                                        resize=True, shrink=False)
 
977
        self.app.gui.secondhpaned.pack2(self.app.gui.propertiestabs,
 
978
                                        resize=True, shrink=False)
 
979
        self.app.gui.mainhpaned.pack1(self.app.gui.secondhpaned,
 
980
                                      resize=True, shrink=False)
 
981
 
 
982
 
 
983
class ZoomBox(Gtk.HBox, Zoomable):
 
984
    """
 
985
    Container holding the widgets for zooming.
 
986
 
 
987
    @type timeline: TimelineContainer
 
988
    """
 
989
 
 
990
    def __init__(self, timeline):
 
991
        Gtk.HBox.__init__(self)
 
992
        Zoomable.__init__(self)
 
993
 
 
994
        self.timeline = timeline
 
995
 
 
996
        zoom_fit_btn = Gtk.Button()
 
997
        zoom_fit_btn.set_relief(Gtk.ReliefStyle.NONE)
 
998
        zoom_fit_btn.set_tooltip_text(ZOOM_FIT)
 
999
        zoom_fit_icon = Gtk.Image.new_from_icon_name("zoom-best-fit", Gtk.IconSize.BUTTON)
 
1000
        zoom_fit_btn_hbox = Gtk.HBox()
 
1001
        zoom_fit_btn_hbox.pack_start(zoom_fit_icon, expand=False, fill=True, padding=0)
 
1002
        zoom_fit_btn_hbox.pack_start(Gtk.Label(label=_("Zoom")), expand=False, fill=True, padding=0)
 
1003
        zoom_fit_btn.add(zoom_fit_btn_hbox)
 
1004
        zoom_fit_btn.connect("clicked", self._zoomFitCb)
 
1005
 
 
1006
        self.pack_start(zoom_fit_btn, expand=False, fill=True, padding=0)
 
1007
 
 
1008
        # zooming slider
 
1009
        self._zoomAdjustment = Gtk.Adjustment()
 
1010
        self._zoomAdjustment.props.lower = 0
 
1011
        self._zoomAdjustment.props.upper = Zoomable.zoom_steps
 
1012
        zoomslider = Gtk.Scale.new(Gtk.Orientation.HORIZONTAL, adjustment=self._zoomAdjustment)
 
1013
        # Setting _zoomAdjustment's value must be done after we create the
 
1014
        # zoom slider, otherwise the slider remains at the leftmost position.
 
1015
        self._zoomAdjustment.set_value(Zoomable.getCurrentZoomLevel())
 
1016
        zoomslider.props.draw_value = False
 
1017
        zoomslider.connect("scroll-event", self._zoomSliderScrollCb)
 
1018
        zoomslider.connect("value-changed", self._zoomAdjustmentChangedCb)
 
1019
        zoomslider.connect("query-tooltip", self._sliderTooltipCb)
 
1020
        zoomslider.set_has_tooltip(True)
 
1021
        zoomslider.set_size_request(100, 0)  # At least 100px wide for precision
 
1022
        self.pack_start(zoomslider, expand=True, fill=True, padding=ZOOM_SLIDER_PADDING)
 
1023
 
 
1024
        self.set_size_request(CONTROL_WIDTH, -1)
 
1025
        self.show_all()
 
1026
 
 
1027
    def _zoomAdjustmentChangedCb(self, adjustment):
 
1028
        Zoomable.setZoomLevel(adjustment.get_value())
 
1029
 
 
1030
    def _zoomFitCb(self, unused_button):
 
1031
        self.timeline.zoomFit()
 
1032
 
 
1033
    def _zoomSliderScrollCb(self, unused, event):
 
1034
        delta = 0
 
1035
        if event.direction in [Gdk.ScrollDirection.UP, Gdk.ScrollDirection.RIGHT]:
 
1036
            delta = 1
 
1037
        elif event.direction in [Gdk.ScrollDirection.DOWN, Gdk.ScrollDirection.LEFT]:
 
1038
            delta = -1
 
1039
        elif event.direction in [Gdk.ScrollDirection.SMOOTH]:
 
1040
            unused_res, delta_x, delta_y = event.get_scroll_deltas()
 
1041
            if delta_x:
 
1042
                delta = math.copysign(1, delta_x)
 
1043
            elif delta_y:
 
1044
                delta = math.copysign(1, -delta_y)
 
1045
        if delta:
 
1046
            Zoomable.setZoomLevel(Zoomable.getCurrentZoomLevel() + delta)
 
1047
 
 
1048
    def zoomChanged(self):
 
1049
        zoomLevel = self.getCurrentZoomLevel()
 
1050
        if int(self._zoomAdjustment.get_value()) != zoomLevel:
 
1051
            self._zoomAdjustment.set_value(zoomLevel)
 
1052
 
 
1053
    def _sliderTooltipCb(self, unused_slider, unused_x, unused_y, unused_keyboard_mode, tooltip):
 
1054
        # We assume the width of the ruler is exactly the width of the timeline.
 
1055
        width_px = self.timeline.ruler.get_allocated_width()
 
1056
        timeline_width_ns = Zoomable.pixelToNs(width_px)
 
1057
        if timeline_width_ns >= Gst.SECOND:
 
1058
            # Translators: %s represents a duration, for example "10 minutes"
 
1059
            tip = _("%s displayed") % beautify_length(timeline_width_ns)
 
1060
        else:
 
1061
            # Translators: This is a tooltip
 
1062
            tip = _("%d nanoseconds displayed, because we can") % timeline_width_ns
 
1063
        tooltip.set_text(tip)
 
1064
        return True