1
# -*- coding: utf-8 -*-
4
# pitivi/utils/widgets.py
6
# Copyright (c) 2005, Edward Hervey <bilboed@bilboed.com>
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.
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.
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.
24
A collection of helper classes and routines for:
25
* dynamically creating user interfaces
26
* Creating UI from GstElement-s
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
41
from gettext import gettext as _
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
51
ZOOM_FIT = _("Zoom Fit")
53
ZOOM_SLIDER_PADDING = SPACING * 4 / 5
56
class DynamicWidget(object):
58
"""An interface which provides a uniform way to get, set, and observe
60
def __init__(self, default):
61
self.default = default
63
def connectValueChanged(self, callback, *args):
64
raise NotImplementedError
66
def setWidgetValue(self, value):
67
raise NotImplementedError
69
def getWidgetValue(self, value):
70
raise NotImplementedError
72
def getWidgetDefault(self):
75
def setWidgetToDefault(self):
76
if self.default is not None:
77
self.setWidgetValue(self.default)
80
class DefaultWidget(Gtk.Label):
81
"""When all hope fails...."""
83
def __init__(self, *unused, **unused_kwargs):
84
Gtk.Label.__init__(self, _("Implement Me"))
87
class TextWidget(Gtk.HBox, DynamicWidget):
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
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.
98
__gtype_name__ = 'TextWidget'
100
"value-changed": (GObject.SignalFlags.RUN_LAST, None, (),),
101
"activate": (GObject.SignalFlags.RUN_LAST, None, (),)
104
__INVALID__ = Gdk.Color(0xFFFF, 0, 0)
105
__NORMAL__ = Gdk.Color(0, 0, 0)
107
def __init__(self, matches=None, choices=None, default=None):
109
# In the case of text widgets, a blank default is an empty string
112
Gtk.HBox.__init__(self)
113
DynamicWidget.__init__(self, default)
115
self.set_border_width(0)
118
self.combo = Gtk.ComboBoxText.new_with_entry()
119
self.text = self.combo.get_child()
121
self.pack_start(self.combo, expand=True, fill=True, padding=0)
122
for choice in choices:
123
self.combo.append_text(choice)
125
self.text = Gtk.Entry()
127
self.pack_start(self.text, expand=True, fill=True, padding=0)
129
self.last_valid = None
131
self.send_signal = True
132
self.text.connect("changed", self._textChanged)
133
self.text.connect("activate", self._activateCb)
135
if type(matches) is str:
136
self.matches = re.compile(matches)
138
self.matches = matches
139
self._textChanged(None)
141
def connectValueChanged(self, callback, *args):
142
return self.connect("value-changed", callback, *args)
144
def setWidgetValue(self, value, send_signal=True):
145
self.send_signal = send_signal
146
self.text.set_text(value)
148
def getWidgetValue(self):
150
return self.last_valid
151
return self.text.get_text()
153
def addChoices(self, choices):
154
for choice in choices:
155
self.combo.append_text(choice)
157
def _textChanged(self, unused_widget):
158
text = self.text.get_text()
160
if self._filter(text):
161
self.last_valid = text
163
self.emit("value-changed")
165
self.text.set_icon_from_icon_name(1, None)
169
self.text.set_icon_from_icon_name(1, "dialog-warning")
171
elif self.send_signal:
172
self.emit("value-changed")
174
self.send_signal = True
176
def _activateCb(self, unused_widget):
178
Similar to _textChanged, to account for the case where we connect to
179
the "activate" signal instead of "text-changed".
181
We don't need to set the icons or anything like that, as _textChanged
184
if self.matches and self.send_signal:
185
self.emit("activate")
187
def _filter(self, text):
188
match = self.matches.match(text)
189
if match is not None:
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)
198
class NumericWidget(Gtk.HBox, DynamicWidget):
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"""
204
def __init__(self, upper=None, lower=None, default=None):
205
Gtk.HBox.__init__(self)
206
DynamicWidget.__init__(self, default)
208
self.spacing = SPACING
209
self.adjustment = Gtk.Adjustment()
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)
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)
226
upper = GObject.G_MAXDOUBLE
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)
236
def connectValueChanged(self, callback, *args):
237
self.adjustment.connect("value-changed", callback, *args)
239
def getWidgetValue(self):
241
return self._type(self.adjustment.get_value())
243
return self.adjustment.get_value()
245
def setWidgetValue(self, value):
247
if self._type is None:
250
if type_ == int or type_ == long:
251
minimum, maximum = (-sys.maxint, sys.maxint)
255
minimum, maximum = (GObject.G_MINDOUBLE, GObject.G_MAXDOUBLE)
258
self.spinner.props.digits = 2
259
if self.lower is not None:
261
if self.upper is not None:
263
self.adjustment.configure(value, minimum, maximum, step, page, 0)
264
self.spinner.set_adjustment(self.adjustment)
267
class TimeWidget(TextWidget, DynamicWidget):
269
A widget that contains a time in nanoseconds. Accepts timecode formats
270
or a frame number (integer).
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]$")
277
__gtype_name__ = 'TimeWidget'
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
285
def getWidgetValue(self):
286
timecode = TextWidget.getWidgetValue(self)
289
parts = timecode.split(":")
295
ss, millis = end.split(".")
296
nanosecs = int(hh) * 3.6 * 10e12 \
297
+ int(mm) * 6 * 10e10 \
300
nanosecs = nanosecs / 10 # Compensate the 10 factor of e notation
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!
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)
314
def connectActivateEvent(self, activateCb):
315
return self.connect("activate", activateCb)
317
def connectFocusEvents(self, focusInCb, focusOutCb):
318
fIn = self.text.connect("focus-in-event", focusInCb)
319
fOut = self.text.connect("focus-out-event", focusOutCb)
323
def setFramerate(self, framerate):
324
self._framerate = framerate
327
class FractionWidget(TextWidget, DynamicWidget):
329
"""A Gtk.ComboBoxEntry """
331
fraction_regex = re.compile(
332
"^([0-9]*(\.[0-9]+)?)(([:/][0-9]*(\.[0-9]+)?)|M)?$")
333
__gtype_name__ = 'FractionWidget'
335
def __init__(self, range=None, presets=None, default=None):
336
DynamicWidget.__init__(self, default)
339
flow = float(range.low)
340
fhigh = float(range.high)
342
flow = float("-Infinity")
343
fhigh = float("Infinity")
346
for preset in presets:
347
if type(preset) is str:
349
preset = self._parseText(preset)
351
strval = "%g:%g" % (preset.num, preset.denom)
352
fpreset = float(preset)
353
if flow <= fpreset and fpreset <= fhigh:
354
choices.append(strval)
357
TextWidget.__init__(self, self.fraction_regex, choices)
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:
366
def addPresets(self, presets):
368
for preset in presets:
369
if type(preset) is str:
371
preset = self._parseText(preset)
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)
378
self.addChoices(choices)
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)
388
text = "%g:%g" % (value.num, value.denom)
390
self.text.set_text(text)
392
def getWidgetValue(self):
394
return self._parseText(self.last_valid)
395
return Gst.Fraction(1, 1)
397
def _parseText(self, text):
398
match = self.fraction_regex.match(text)
399
groups = match.groups()
403
num = float(groups[0])
409
denom = float(groups[2][1:])
410
return Gst.Fraction(num, denom)
413
class ToggleWidget(Gtk.CheckButton, DynamicWidget):
415
"""A Gtk.CheckButton which supports the DynamicWidget interface."""
417
def __init__(self, default=None):
418
Gtk.CheckButton.__init__(self)
419
DynamicWidget.__init__(self, default)
421
def connectValueChanged(self, callback, *args):
422
self.connect("toggled", callback, *args)
424
def setWidgetValue(self, value):
425
self.set_active(value)
427
def getWidgetValue(self):
428
return self.get_active()
431
class ChoiceWidget(Gtk.HBox, DynamicWidget):
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."""
437
def __init__(self, choices, default=None):
438
Gtk.HBox.__init__(self)
439
DynamicWidget.__init__(self, default)
442
self.contents = Gtk.ComboBoxText()
443
self.pack_start(self.contents, expand=True, fill=True, padding=0)
444
self.setChoices(choices)
446
cell = self.contents.get_cells()[0]
447
cell.props.ellipsize = Pango.EllipsizeMode.END
449
def connectValueChanged(self, callback, *args):
450
return self.contents.connect("changed", callback, *args)
452
def setWidgetValue(self, value):
454
self.contents.set_active(self.values.index(value))
456
raise ValueError("%r not in %r" % (value, self.values))
458
def getWidgetValue(self):
459
return self.values[self.contents.get_active()]
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)
471
self.contents.set_sensitive(True)
474
class PathWidget(Gtk.FileChooserButton, DynamicWidget):
476
"""A Gtk.FileChooserButton which supports the DynamicWidget interface."""
478
__gtype_name__ = 'PathWidget'
481
"value-changed": (GObject.SignalFlags.RUN_LAST,
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)
495
def connectValueChanged(self, callback, *args):
496
return self.connect("value-changed", callback, *args)
498
def setWidgetValue(self, value):
502
def getWidgetValue(self):
505
def _responseCb(self, unused_dialog, response):
506
if response == Gtk.ResponseType.CLOSE:
507
self.uri = self.get_uri()
508
self.emit("value-changed")
512
class ColorWidget(Gtk.ColorButton, DynamicWidget):
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)
520
def connectValueChanged(self, callback, *args):
521
self.connect("color-set", callback, *args)
523
def setWidgetValue(self, value):
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:
535
raise TypeError("%r is not something we can convert to a color" %
537
self.set_color(color)
538
self.set_alpha(alpha)
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:
549
return color.to_string()
552
class FontWidget(Gtk.FontButton, DynamicWidget):
554
def __init__(self, default=None):
555
Gtk.FontButton.__init__(self)
556
DynamicWidget.__init__(self, default)
557
self.set_use_font(True)
559
def connectValueChanged(self, callback, *args):
560
self.connect("font-set", callback, *args)
562
def setWidgetValue(self, font_name):
563
self.set_font_name(font_name)
565
def getWidgetValue(self):
566
return self.get_font_name()
569
class GstElementSettingsWidget(Gtk.VBox, Loggable):
571
Widget to view/modify properties of a Gst.Element
574
def __init__(self, isControllable=True):
575
Gtk.VBox.__init__(self)
576
Loggable.__init__(self)
579
self.properties = None
581
self.isControllable = isControllable
583
def resetKeyframeToggleButtons(self, widget=None):
585
Reset all the keyframe togglebuttons for all properties.
586
If a property widget is specified, reset only its keyframe togglebutton.
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
600
self.log("Resetting all keyframe buttons")
601
for togglebutton in self.keyframeToggleButtons.keys():
602
togglebutton.set_label("◇")
603
self._setKeyframeToggleButtonState(togglebutton, False)
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()
610
def setElement(self, element, properties={}, ignore=['name'],
611
default_btn=False, use_element_props=False):
613
Set given element on Widget, with optional properties
615
self.info("element: %s, use properties: %s", element, properties)
616
self.element = element
619
self._addWidgets(properties, default_btn, use_element_props)
621
def _addWidgets(self, properties, default_btn, use_element_props):
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.
627
If there are no properties, returns a table containing the label
631
self.keyframeToggleButtons = {}
633
if isinstance(self.element, GES.Effect):
635
props = [prop for prop in self.element.list_children_properties() if not prop.name in self.ignore]
637
props = [prop for prop in GObject.list_properties(self.element) if not prop.name in self.ignore]
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)
648
table = Gtk.Table(n_rows=len(props), n_columns=4)
650
table = Gtk.Table(n_rows=len(props), n_columns=3)
652
table.set_row_spacings(SPACING)
653
table.set_col_spacings(SPACING)
654
table.set_border_width(SPACING)
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)):
666
result, prop_value = self.element.get_child_property(prop.name)
668
self.debug("Could not get value for property: %s", prop.name)
670
if use_element_props:
671
prop_value = self.element.get_property(prop.name)
673
prop_value = properties.get(prop.name)
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)
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)
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)
690
if hasattr(prop, 'blurb'):
691
widget.set_tooltip_text(prop.blurb)
693
self.properties[prop] = widget
695
# The "reset to default" button associated with this property
697
widget.propName = prop.name.split("-")[0]
700
if self.isControllable:
701
# If this element is controlled, the value means nothing anymore.
702
binding = self.element.get_control_binding(prop.name)
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
710
self.element.connect('notify::' + prop.name, self._propertyChangedCb, widget)
714
self.pack_start(table, expand=True, fill=True, padding=0)
717
def _propertyChangedCb(self, unused_element, pspec, widget):
718
widget.setWidgetValue(self.element.get_property(pspec.name))
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)
728
def _getResetToDefaultValueButton(self, unused_prop, widget):
730
icon.set_from_icon_name("edit-clear-all-symbolic", Gtk.IconSize.MENU)
731
button = Gtk.Button()
733
button.set_tooltip_text(_("Reset to default value"))
734
button.set_relief(Gtk.ReliefStyle.NONE)
735
button.connect('clicked', self._defaultBtnClickedCb, widget)
738
def _setKeyframeToggleButtonState(self, button, active_state):
740
This is meant for programmatically (un)pushing the provided keyframe
741
togglebutton, without triggering its signals.
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)
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("◆")
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()
774
def _defaultBtnClickedCb(self, unused_button, widget):
776
binding = self.bindings[widget]
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()
787
widget.set_sensitive(True)
788
widget.setWidgetToDefault()
789
self.resetKeyframeToggleButtons(widget)
791
def getSettings(self, with_default=False):
793
returns the dictionnary of propertyname/propertyvalue
796
for prop, widget in self.properties.iteritems():
797
if not prop.flags & GObject.PARAM_WRITABLE:
799
if isinstance(widget, DefaultWidget):
801
value = widget.getWidgetValue()
802
if value is not None and (value != prop.default_value or with_default):
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":
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)
828
# TODO: implement widgets for: GBoxed, GFlags
829
self.fixme("Unsupported property type: %s", type_name)
830
widget = DefaultWidget()
833
value = prop.default_value
834
if value is not None and not isinstance(widget, DefaultWidget):
835
widget.setWidgetValue(value)
840
class GstElementSettingsDialog(Loggable):
842
Dialog window for viewing/modifying properties of a Gst.Element
845
def __init__(self, elementfactory, properties, parent_window=None, isControllable=True):
846
Loggable.__init__(self)
847
self.debug("factory: %s, properties: %s", elementfactory, properties)
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")
854
self.window = self.builder.get_object("dialog1")
855
self.elementsettings = GstElementSettingsWidget(isControllable)
856
self.builder.get_object("viewport1").add(self.elementsettings)
858
self.factory = elementfactory
859
self.element = self.factory.create("elementsettings")
861
self.warning("Couldn't create element from factory %s", self.factory)
862
self.properties = properties
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.
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)
876
# If we need to scroll, set a reasonable height for the window.
878
self.window.set_default_size(400, default_height)
881
self.window.set_transient_for(parent_window)
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)
889
def getSettings(self):
890
""" returns the property/value dictionnary of the selected settings """
891
return self.elementsettings.getSettings()
893
def _resetValuesClickedCb(self, unused_button):
897
for prop, widget in self.elementsettings.properties.iteritems():
898
widget.setWidgetToDefault()
901
class BaseTabs(Gtk.Notebook):
902
def __init__(self, app, hide_hpaned=False):
904
Gtk.Notebook.__init__(self)
905
self.set_border_width(SPACING)
907
self.connect("create-window", self._createWindowCb)
908
self._hide_hpaned = hide_hpaned
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)
918
def append_page(self, child, label):
919
Gtk.Notebook.append_page(self, child, label)
920
self._set_child_properties(child, label)
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
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)
940
if self._hide_hpaned:
941
self._showSecondHpanedInMainWindow()
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)
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
958
# set_uposition is deprecated but what should I use instead?
959
window.set_uposition(x, y)
961
if self._hide_hpaned:
962
self._hideSecondHpanedInMainWindow()
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,
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)
983
class ZoomBox(Gtk.HBox, Zoomable):
985
Container holding the widgets for zooming.
987
@type timeline: TimelineContainer
990
def __init__(self, timeline):
991
Gtk.HBox.__init__(self)
992
Zoomable.__init__(self)
994
self.timeline = timeline
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)
1006
self.pack_start(zoom_fit_btn, expand=False, fill=True, padding=0)
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)
1024
self.set_size_request(CONTROL_WIDTH, -1)
1027
def _zoomAdjustmentChangedCb(self, adjustment):
1028
Zoomable.setZoomLevel(adjustment.get_value())
1030
def _zoomFitCb(self, unused_button):
1031
self.timeline.zoomFit()
1033
def _zoomSliderScrollCb(self, unused, event):
1035
if event.direction in [Gdk.ScrollDirection.UP, Gdk.ScrollDirection.RIGHT]:
1037
elif event.direction in [Gdk.ScrollDirection.DOWN, Gdk.ScrollDirection.LEFT]:
1039
elif event.direction in [Gdk.ScrollDirection.SMOOTH]:
1040
unused_res, delta_x, delta_y = event.get_scroll_deltas()
1042
delta = math.copysign(1, delta_x)
1044
delta = math.copysign(1, -delta_y)
1046
Zoomable.setZoomLevel(Zoomable.getCurrentZoomLevel() + delta)
1048
def zoomChanged(self):
1049
zoomLevel = self.getCurrentZoomLevel()
1050
if int(self._zoomAdjustment.get_value()) != zoomLevel:
1051
self._zoomAdjustment.set_value(zoomLevel)
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)
1061
# Translators: This is a tooltip
1062
tip = _("%d nanoseconds displayed, because we can") % timeline_width_ns
1063
tooltip.set_text(tip)