1# -*- coding: utf-8 -*- 2# Pitivi video editor 3# Copyright (c) 2010, Thibault Saunier <tsaunier@gnome.org> 4# Copyright (c) 2005, Edward Hervey <bilboed@bilboed.com> 5# 6# This program is free software; you can redistribute it and/or 7# modify it under the terms of the GNU Lesser General Public 8# License as published by the Free Software Foundation; either 9# version 2.1 of the License, or (at your option) any later version. 10# 11# This program is distributed in the hope that it will be useful, 12# but WITHOUT ANY WARRANTY; without even the implied warranty of 13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14# Lesser General Public License for more details. 15# 16# You should have received a copy of the GNU Lesser General Public 17# License along with this program; if not, write to the 18# Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, 19# Boston, MA 02110-1301, USA. 20"""Effects categorization and management. 21 22 There are different types of effects available: 23 _ Simple Audio/Video Effects 24 GStreamer elements that only apply to audio OR video 25 Only take the elements who have a straightforward meaning/action 26 _ Expanded Audio/Video Effects 27 These are the Gstreamer elements that don't have a easy meaning/action or 28 that are too cumbersome to use as such 29 _ Complex Audio/Video Effects 30""" 31import os 32import re 33from gettext import gettext as _ 34 35from gi.repository import Gdk 36from gi.repository import GdkPixbuf 37from gi.repository import GES 38from gi.repository import GLib 39from gi.repository import Gst 40from gi.repository import Gtk 41from gi.repository import Pango 42 43from pitivi.configure import get_pixmap_dir 44from pitivi.configure import get_ui_dir 45from pitivi.settings import GlobalSettings 46from pitivi.utils.loggable import Loggable 47from pitivi.utils.ui import EFFECT_TARGET_ENTRY 48from pitivi.utils.ui import SPACING 49from pitivi.utils.widgets import FractionWidget 50from pitivi.utils.widgets import GstElementSettingsWidget 51 52 53(VIDEO_EFFECT, AUDIO_EFFECT) = list(range(1, 3)) 54 55AUDIO_EFFECTS_CATEGORIES = () 56 57ALLOWED_ONLY_ONCE_EFFECTS = ['videoflip'] 58 59VIDEO_EFFECTS_CATEGORIES = ( 60 (_("Colors"), ( 61 # Mostly "serious" stuff that relates to correction/adjustments 62 # Fancier stuff goes into the "fancy" category 63 "cogcolorspace", "videobalance", "chromahold", "gamma", 64 "coloreffects", "exclusion", "burn", "dodge", "videomedian", 65 "frei0r-filter-color-distance", "frei0r-filter-threshold0r", 66 "frei0r-filter-contrast0r", "frei0r-filter-saturat0r", 67 "frei0r-filter-white-balance", "frei0r-filter-brightness", 68 "frei0r-filter-gamma", "frei0r-filter-invert0r", 69 "frei0r-filter-hueshift0r", "frei0r-filter-equaliz0r", 70 "frei0r-filter-bw0r", "frei0r-filter-glow", 71 "frei0r-filter-twolay0r", "frei0r-filter-3-point-color-balance", 72 "frei0r-filter-coloradj-rgb", "frei0r-filter-curves", 73 "frei0r-filter-levels", "frei0r-filter-primaries", 74 "frei0r-filter-sop-sat", "frei0r-filter-threelay0r", 75 "frei0r-filter-tint0r", 76 )), 77 (_("Compositing"), ( 78 "alpha", "alphacolor", "gdkpixbufoverlay", 79 "frei0r-filter-transparency", "frei0r-filter-mask0mate", 80 "frei0r-filter-alpha0ps", "frei0r-filter-alphagrad", 81 "frei0r-filter-alphaspot", "frei0r-filter-bluescreen0r", 82 "frei0r-filter-select0r", 83 )), 84 (_("Noise & blur"), ( 85 "gaussianblur", "diffuse", "dilate", "marble", "smooth", 86 "frei0r-filter-hqdn3d", "frei0r-filter-squareblur", 87 "frei0r-filter-sharpness", "frei0r-filter-edgeglow", 88 "frei0r-filter-facebl0r", 89 )), 90 (_("Analysis"), ( 91 "videoanalyse", "videodetect", "videomark", "revtv", 92 "navigationtest", "frei0r-filter-rgb-parade", 93 "frei0r-filter-r", "frei0r-filter-g", "frei0r-filter-b", 94 "frei0r-filter-vectorscope", "frei0r-filter-luminance", 95 "frei0r-filter-opencvfacedetect", "frei0r-filter-pr0be", 96 "frei0r-filter-pr0file", 97 )), 98 (_("Geometry"), ( 99 "cogscale", "aspectratiocrop", "cogdownsample", "videoscale", 100 "videocrop", "videoflip", "videobox", "gdkpixbufscale", 101 "kaleidoscope", "mirror", "pinch", "sphere", "square", "fisheye", 102 "stretch", "twirl", "waterriple", "rotate", "bulge", "circle", 103 "frei0r-filter-letterb0xed", "frei0r-filter-k-means-clustering", 104 "frei0r-filter-lens-correction", "frei0r-filter-defish0r", 105 "frei0r-filter-perspective", "frei0r-filter-c0rners", 106 "frei0r-filter-scale0tilt", "frei0r-filter-pixeliz0r", 107 "frei0r-filter-flippo", "frei0r-filter-3dflippo", 108 )), 109 (_("Fancy"), ( 110 "rippletv", "streaktv", "radioactv", "optv", "solarize", 111 "quarktv", "vertigotv", "shagadelictv", "warptv", "dicetv", 112 "agingtv", "edgetv", "bulge", "circle", "fisheye", "tunnel", 113 "kaleidoscope", "mirror", "pinch", "sphere", "square", 114 "stretch", "twirl", "waterripple", "glfiltersobel", "chromium", 115 "frei0r-filter-sobel", "frei0r-filter-cartoon", 116 "frei0r-filter-water", "frei0r-filter-nosync0r", 117 "frei0r-filter-k-means-clustering", "frei0r-filter-delay0r", 118 "frei0r-filter-distort0r", "frei0r-filter-light-graffiti", 119 "frei0r-filter-tehroxx0r", "frei0r-filter-vertigo", 120 )), 121 (_("Time"), ( 122 "videorate", "frei0r-filter-delay0r", "frei0r-filter-baltan", 123 "frei0r-filter-nervous", 124 )), 125) 126 127BLACKLISTED_EFFECTS = ["colorconvert", "coglogoinsert", "festival", 128 "alphacolor", "cogcolorspace", "videodetect", 129 "navigationtest", "videoanalyse", "volume"] 130 131BLACKLISTED_PLUGINS = [] 132 133HIDDEN_EFFECTS = [ 134 # Overlaying an image onto a video stream can already be done. 135 "gdkpixbufoverlay"] 136 137GlobalSettings.addConfigSection('effect-library') 138 139(COL_NAME_TEXT, 140 COL_DESC_TEXT, 141 COL_EFFECT_TYPE, 142 COL_EFFECT_CATEGORIES, 143 COL_ELEMENT_NAME, 144 COL_ICON) = list(range(6)) 145 146ICON_WIDTH = 48 + 2 * 6 # 48 pixels, plus a margin on each side 147 148 149class EffectInfo(object): 150 """Info for displaying and using an effect. 151 152 Attributes: 153 effect_name (str): The bin_description identifying the effect. 154 """ 155 156 def __init__(self, effect_name, media_type, categories, 157 human_name, description): 158 object.__init__(self) 159 self.effect_name = effect_name 160 self.media_type = media_type 161 self.categories = categories 162 self.description = description 163 self.human_name = human_name 164 165 @property 166 def icon(self): 167 pixdir = os.path.join(get_pixmap_dir(), "effects") 168 try: 169 # We can afford to scale the images here, the impact is negligible 170 icon = GdkPixbuf.Pixbuf.new_from_file_at_size( 171 os.path.join(pixdir, self.effect_name + ".png"), 172 ICON_WIDTH, ICON_WIDTH) 173 # An empty except clause is bad, but "gi._glib.GError" is not helpful. 174 except: 175 icon = GdkPixbuf.Pixbuf.new_from_file( 176 os.path.join(pixdir, "defaultthumbnail.svg")) 177 return icon 178 179 @property 180 def bin_description(self): 181 """Gets the bin description which defines this effect.""" 182 if self.effect_name.startswith("gl"): 183 return "glupload ! %s ! gldownload" % self.effect_name 184 else: 185 return self.effect_name 186 187 @staticmethod 188 def name_from_bin_description(bin_description): 189 """Gets the name of the effect defined by the `bin_description`.""" 190 if bin_description.startswith("glupload"): 191 return bin_description.split("!")[1].strip() 192 else: 193 return bin_description 194 195 def good_for_track_element(self, track_element): 196 """Checks the effect is compatible with the specified track element. 197 198 Args: 199 track_element (GES.TrackElement): The track element to check against. 200 201 Returns: 202 bool: Whether it makes sense to apply the effect to the track element. 203 """ 204 track_type = track_element.get_track_type() 205 if track_type == GES.TrackType.AUDIO: 206 return self.media_type == AUDIO_EFFECT 207 elif track_type == GES.TrackType.VIDEO: 208 return self.media_type == VIDEO_EFFECT 209 else: 210 return False 211 212 213class EffectsManager(Loggable): 214 """Keeps info about effects and their categories. 215 216 Attributes: 217 video_effects (List[Gst.ElementFactory]): The available video effects. 218 audio_effects (List[Gst.ElementFactory]): The available audio effects. 219 """ 220 221 def __init__(self): 222 Loggable.__init__(self) 223 self.video_effects = [] 224 self.audio_effects = [] 225 self.gl_effects = [] 226 self._effects = {} 227 228 useless_words = ["Video", "Audio", "audio", "effect", 229 _("Video"), _("Audio"), _("Audio").lower(), _("effect")] 230 uselessRe = re.compile(" |".join(useless_words)) 231 232 registry = Gst.Registry.get() 233 factories = registry.get_feature_list(Gst.ElementFactory) 234 longnames = set() 235 duplicate_longnames = set() 236 for factory in factories: 237 longname = factory.get_longname() 238 if longname in longnames: 239 duplicate_longnames.add(longname) 240 else: 241 longnames.add(longname) 242 for factory in factories: 243 klass = factory.get_klass() 244 name = factory.get_name() 245 if ("Effect" not in klass or 246 any(black in name for black in BLACKLISTED_PLUGINS)): 247 continue 248 249 media_type = None 250 if "Audio" in klass: 251 self.audio_effects.append(factory) 252 media_type = AUDIO_EFFECT 253 elif "Video" in klass: 254 self.video_effects.append(factory) 255 media_type = VIDEO_EFFECT 256 if not media_type: 257 HIDDEN_EFFECTS.append(name) 258 continue 259 260 longname = factory.get_longname() 261 if longname in duplicate_longnames: 262 # Workaround https://bugzilla.gnome.org/show_bug.cgi?id=760566 263 # Add name which identifies the element and is unique. 264 longname = "%s %s" % (longname, name) 265 human_name = uselessRe.sub("", longname).title() 266 effect = EffectInfo(name, 267 media_type, 268 categories=self._getEffectCategories(name), 269 human_name=human_name, 270 description=factory.get_description()) 271 self._effects[name] = effect 272 273 gl_element_factories = registry.get_feature_list_by_plugin("opengl") 274 self.gl_effects = [element_factory.get_name() 275 for element_factory in gl_element_factories] 276 if self.gl_effects: 277 # Checking whether the GL effects can be used 278 # by setting a pipeline with "gleffects" to PAUSED. 279 pipeline = Gst.parse_launch("videotestsrc ! glupload ! gleffects ! fakesink") 280 bus = pipeline.get_bus() 281 bus.add_signal_watch() 282 bus.connect("message", self._gl_pipeline_message_cb, pipeline) 283 assert pipeline.set_state(Gst.State.PAUSED) == Gst.StateChangeReturn.ASYNC 284 285 def _gl_pipeline_message_cb(self, bus, message, pipeline): 286 """Handles a `message` event on the pipeline for checking gl effects.""" 287 done = False 288 if message.type == Gst.MessageType.ASYNC_DONE: 289 self.debug("GL effects check pipeline successfully PAUSED") 290 done = True 291 elif message.type == Gst.MessageType.ERROR: 292 # The pipeline cannot be set to PAUSED. 293 error, detail = message.parse_error() 294 self.debug("Hiding the GL effects because: %s, %s", error, detail) 295 HIDDEN_EFFECTS.extend(self.gl_effects) 296 done = True 297 298 if done: 299 bus.remove_signal_watch() 300 bus.disconnect_by_func(self._gl_pipeline_message_cb) 301 pipeline.set_state(Gst.State.NULL) 302 303 def getInfo(self, bin_description): 304 """Gets the info for an effect which can be applied. 305 306 Args: 307 bin_description (str): The bin_description defining the effect. 308 309 Returns: 310 EffectInfo: The info corresponding to the name, or None. 311 """ 312 name = EffectInfo.name_from_bin_description(bin_description) 313 return self._effects.get(name) 314 315 def _getEffectCategories(self, effect_name): 316 """Gets the categories to which the specified effect belongs. 317 318 Args: 319 effect_name (str): The bin_description identifying the effect. 320 321 Returns: 322 List[str]: The categories which contain the effect. 323 """ 324 categories = [] 325 for category_name, effects in AUDIO_EFFECTS_CATEGORIES: 326 if effect_name in effects: 327 categories.append(category_name) 328 for category_name, effects in VIDEO_EFFECTS_CATEGORIES: 329 if effect_name in effects: 330 categories.append(category_name) 331 if not categories: 332 categories.append(_("Uncategorized")) 333 categories.insert(0, _("All effects")) 334 return categories 335 336 @property 337 def video_categories(self): 338 """Gets all video effect categories names.""" 339 return EffectsManager._getCategoriesNames(VIDEO_EFFECTS_CATEGORIES) 340 341 @property 342 def audio_categories(self): 343 """Gets all audio effect categories names.""" 344 return EffectsManager._getCategoriesNames(AUDIO_EFFECTS_CATEGORIES) 345 346 @staticmethod 347 def _getCategoriesNames(categories): 348 ret = [category_name for category_name, unused_effects in categories] 349 ret.sort() 350 ret.insert(0, _("All effects")) 351 if categories: 352 # Add Uncategorized only if there are other categories defined. 353 ret.append(_("Uncategorized")) 354 return ret 355 356 357# ----------------------- UI classes to manage effects -------------------------# 358 359 360class EffectListWidget(Gtk.Box, Loggable): 361 """Widget for listing effects.""" 362 363 def __init__(self, instance): 364 Gtk.Box.__init__(self) 365 Loggable.__init__(self) 366 367 self.app = instance 368 369 self._draggedItems = None 370 self._effectType = VIDEO_EFFECT 371 372 self.set_orientation(Gtk.Orientation.VERTICAL) 373 builder = Gtk.Builder() 374 builder.add_from_file(os.path.join(get_ui_dir(), "effectslibrary.ui")) 375 builder.connect_signals(self) 376 toolbar = builder.get_object("effectslibrary_toolbar") 377 toolbar.get_style_context().add_class(Gtk.STYLE_CLASS_INLINE_TOOLBAR) 378 self.video_togglebutton = builder.get_object("video_togglebutton") 379 self.audio_togglebutton = builder.get_object("audio_togglebutton") 380 self.categoriesWidget = builder.get_object("categories") 381 self.searchEntry = builder.get_object("search_entry") 382 383 # Store 384 self.storemodel = Gtk.ListStore( 385 str, str, int, object, str, GdkPixbuf.Pixbuf) 386 self.storemodel.set_sort_column_id( 387 COL_NAME_TEXT, Gtk.SortType.ASCENDING) 388 389 # Create the filter for searching the storemodel. 390 self.model_filter = self.storemodel.filter_new() 391 self.model_filter.set_visible_func(self._setRowVisible, data=None) 392 393 self.view = Gtk.TreeView(model=self.model_filter) 394 self.view.props.headers_visible = False 395 self.view.get_selection().set_mode(Gtk.SelectionMode.SINGLE) 396 397 icon_col = Gtk.TreeViewColumn() 398 icon_col.set_spacing(SPACING) 399 icon_col.set_sizing(Gtk.TreeViewColumnSizing.FIXED) 400 icon_col.props.fixed_width = ICON_WIDTH 401 icon_cell = Gtk.CellRendererPixbuf() 402 icon_cell.props.xpad = 6 403 icon_col.pack_start(icon_cell, True) 404 icon_col.add_attribute(icon_cell, "pixbuf", COL_ICON) 405 406 text_col = Gtk.TreeViewColumn() 407 text_col.set_expand(True) 408 text_col.set_spacing(SPACING) 409 text_col.set_sizing(Gtk.TreeViewColumnSizing.AUTOSIZE) 410 text_cell = Gtk.CellRendererText() 411 text_cell.props.yalign = 0.0 412 text_cell.props.xpad = 6 413 text_cell.set_property("ellipsize", Pango.EllipsizeMode.END) 414 text_col.pack_start(text_cell, True) 415 text_col.set_cell_data_func( 416 text_cell, self.viewDescriptionCellDataFunc, None) 417 418 self.view.append_column(icon_col) 419 self.view.append_column(text_col) 420 421 self.view.connect("query-tooltip", self._treeViewQueryTooltipCb) 422 self.view.props.has_tooltip = True 423 424 # Make the treeview a drag source which provides effects. 425 self.view.enable_model_drag_source( 426 Gdk.ModifierType.BUTTON1_MASK, [EFFECT_TARGET_ENTRY], Gdk.DragAction.COPY) 427 428 self.view.connect("button-press-event", self._buttonPressEventCb) 429 self.view.connect("select-cursor-row", self._enterPressEventCb) 430 self.view.connect("drag-data-get", self._dndDragDataGetCb) 431 432 scrollwin = Gtk.ScrolledWindow() 433 scrollwin.props.hscrollbar_policy = Gtk.PolicyType.NEVER 434 scrollwin.props.vscrollbar_policy = Gtk.PolicyType.AUTOMATIC 435 scrollwin.add(self.view) 436 437 self.pack_start(toolbar, False, False, 0) 438 self.pack_start(scrollwin, True, True, 0) 439 440 # Delay the loading of the available effects so the application 441 # starts faster. 442 GLib.idle_add(self._loadAvailableEffectsCb) 443 self.populate_categories_widget() 444 445 # Individually show the tab's widgets. 446 # If you use self.show_all(), the tab will steal focus on startup. 447 scrollwin.show_all() 448 toolbar.show_all() 449 450 def _treeViewQueryTooltipCb(self, view, x, y, keyboard_mode, tooltip): 451 is_row, x, y, model, path, tree_iter = view.get_tooltip_context( 452 x, y, keyboard_mode) 453 if not is_row: 454 return False 455 456 view.set_tooltip_row(tooltip, path) 457 tooltip.set_markup(self.formatDescription(model, tree_iter)) 458 return True 459 460 def viewDescriptionCellDataFunc(self, unused_column, cell, model, iter_, unused_data): 461 cell.props.markup = self.formatDescription(model, iter_) 462 463 def formatDescription(self, model, iter_): 464 name, element_name, desc = model.get(iter_, COL_NAME_TEXT, COL_ELEMENT_NAME, COL_DESC_TEXT) 465 escape = GLib.markup_escape_text 466 return "<b>%s</b>\n%s" % (escape(name), escape(desc)) 467 468 def _loadAvailableEffectsCb(self): 469 self._addFactories(self.app.effects.video_effects, VIDEO_EFFECT) 470 self._addFactories(self.app.effects.audio_effects, AUDIO_EFFECT) 471 return False 472 473 def _addFactories(self, elements, effectType): 474 for element in elements: 475 name = element.get_name() 476 if name in HIDDEN_EFFECTS: 477 continue 478 effect_info = self.app.effects.getInfo(name) 479 self.storemodel.append([effect_info.human_name, 480 effect_info.description, 481 effectType, 482 effect_info.categories, 483 name, 484 effect_info.icon]) 485 486 def populate_categories_widget(self): 487 self.categoriesWidget.get_model().clear() 488 icon_column = self.view.get_column(0) 489 490 if self._effectType is VIDEO_EFFECT: 491 for category in self.app.effects.video_categories: 492 self.categoriesWidget.append_text(category) 493 icon_column.props.visible = True 494 else: 495 for category in self.app.effects.audio_categories: 496 self.categoriesWidget.append_text(category) 497 icon_column.props.visible = False 498 499 self.categoriesWidget.set_active(0) 500 501 def _dndDragDataGetCb(self, unused_view, drag_context, selection_data, unused_info, unused_timestamp): 502 data = bytes(self.getSelectedEffect(), "UTF-8") 503 selection_data.set(drag_context.list_targets()[0], 0, data) 504 505 def _rowUnderMouseSelected(self, view, event): 506 result = view.get_path_at_pos(int(event.x), int(event.y)) 507 if result: 508 path = result[0] 509 selection = view.get_selection() 510 return selection.path_is_selected(path) and\ 511 selection.count_selected_rows() > 0 512 return False 513 514 def _enterPressEventCb(self, unused_view, unused_event=None): 515 self._addSelectedEffect() 516 517 def _buttonPressEventCb(self, view, event): 518 chain_up = True 519 520 if event.button == 3: 521 chain_up = False 522 elif event.type == getattr(Gdk.EventType, '2BUTTON_PRESS'): 523 self._addSelectedEffect() 524 else: 525 chain_up = not self._rowUnderMouseSelected(view, event) 526 527 if chain_up: 528 self._draggedItems = None 529 else: 530 self._draggedItems = self.getSelectedEffect() 531 532 Gtk.TreeView.do_button_press_event(view, event) 533 return True 534 535 def _addSelectedEffect(self): 536 """Adds the selected effect to the single selected clip, if any.""" 537 effect = self.getSelectedEffect() 538 effect_info = self.app.effects.getInfo(effect) 539 if not effect_info: 540 return 541 timeline = self.app.gui.timeline_ui.timeline 542 clip = timeline.selection.getSingleClip() 543 if not clip: 544 return 545 pipeline = timeline.ges_timeline.get_parent() 546 from pitivi.undo.timeline import CommitTimelineFinalizingAction 547 with self.app.action_log.started("add effect", 548 finalizing_action=CommitTimelineFinalizingAction(pipeline), 549 toplevel=True): 550 clip.ui.add_effect(effect_info) 551 552 def getSelectedEffect(self): 553 if self._draggedItems: 554 return self._draggedItems 555 model, rows = self.view.get_selection().get_selected_rows() 556 path = self.model_filter.convert_path_to_child_path(rows[0]) 557 return self.storemodel[path][COL_ELEMENT_NAME] 558 559 def _toggleViewTypeCb(self, widget): 560 """Switches the view mode between video and audio. 561 562 This makes the two togglebuttons behave like a group of radiobuttons. 563 """ 564 if widget is self.video_togglebutton: 565 self.audio_togglebutton.set_active(not widget.get_active()) 566 else: 567 assert widget is self.audio_togglebutton 568 self.video_togglebutton.set_active(not widget.get_active()) 569 570 if self.video_togglebutton.get_active(): 571 self._effectType = VIDEO_EFFECT 572 else: 573 self._effectType = AUDIO_EFFECT 574 self.populate_categories_widget() 575 self.model_filter.refilter() 576 577 def _categoryChangedCb(self, unused_combobox): 578 self.model_filter.refilter() 579 580 def _searchEntryChangedCb(self, unused_entry): 581 self.model_filter.refilter() 582 583 def _searchEntryIconClickedCb(self, entry, unused, unused1): 584 entry.set_text("") 585 586 def _setRowVisible(self, model, iter, unused_data): 587 if not self._effectType == model.get_value(iter, COL_EFFECT_TYPE): 588 return False 589 if model.get_value(iter, COL_EFFECT_CATEGORIES) is None: 590 return False 591 if self.categoriesWidget.get_active_text() not in model.get_value(iter, COL_EFFECT_CATEGORIES): 592 return False 593 text = self.searchEntry.get_text().lower() 594 return text in model.get_value(iter, COL_DESC_TEXT).lower() or\ 595 text in model.get_value(iter, COL_NAME_TEXT).lower() 596 597 598PROPS_TO_IGNORE = ['name', 'qos', 'silent', 'message', 'parent'] 599 600 601class EffectsPropertiesManager: 602 """Provides and caches UIs for editing effects. 603 604 Attributes: 605 app (Pitivi): The app. 606 """ 607 608 def __init__(self, app): 609 self.cache_dict = {} 610 self._current_element_values = {} 611 self.app = app 612 613 def getEffectConfigurationUI(self, effect): 614 """Gets a configuration UI element for the effect. 615 616 Args: 617 effect (Gst.Element): The effect for which we want the UI. 618 619 Returns: 620 GstElementSettingsWidget: A container for configuring the effect. 621 """ 622 if effect not in self.cache_dict: 623 # Here we should handle special effects configuration UI 624 effect_widget = GstElementSettingsWidget() 625 effect_widget.setElement(effect, ignore=PROPS_TO_IGNORE, 626 with_reset_button=True) 627 self.cache_dict[effect] = effect_widget 628 self._connectAllWidgetCallbacks(effect_widget, effect) 629 self._postConfiguration(effect, effect_widget) 630 631 for prop in effect.list_children_properties(): 632 value = effect.get_child_property(prop.name) 633 self._current_element_values[prop.name] = value 634 635 return self.cache_dict[effect] 636 637 def cleanCache(self, effect): 638 if effect in self.cache_dict: 639 return self.cache_dict.pop(effect) 640 641 def _postConfiguration(self, effect, effect_set_ui): 642 if 'aspectratiocrop' in effect.get_property("bin-description"): 643 for widget in effect_set_ui.get_children()[0].get_children(): 644 if isinstance(widget, FractionWidget): 645 widget.addPresets(["4:3", "5:4", "9:3", "16:9", "16:10"]) 646 647 def _connectAllWidgetCallbacks(self, effect_settings_widget, effect): 648 for prop, widget in effect_settings_widget.properties.items(): 649 widget.connectValueChanged(self._onValueChangedCb, widget, prop, effect) 650 651 def _onSetDefaultCb(self, unused_widget, effect_widget): 652 effect_widget.setWidgetToDefault() 653 654 def _onValueChangedCb(self, unused_widget, effect_widget, prop, effect): 655 value = effect_widget.getWidgetValue() 656 657 # FIXME Workaround in order to make aspectratiocrop working 658 if isinstance(value, Gst.Fraction): 659 value = Gst.Fraction(int(value.num), int(value.denom)) 660 661 if value != self._current_element_values.get(prop.name): 662 from pitivi.undo.timeline import CommitTimelineFinalizingAction 663 664 pipeline = self.app.project_manager.current_project.pipeline 665 with self.app.action_log.started("Effect property change", 666 finalizing_action=CommitTimelineFinalizingAction(pipeline), 667 toplevel=True): 668 effect.set_child_property(prop.name, value) 669 self._current_element_values[prop.name] = value 670