def test_dependent_properties(self): """Checks dependent properties updating is handled correctly.""" mainloop = common.create_main_loop() app = common.create_pitivi() app.project_manager.new_blank_project() manager = EffectsPropertiesManager(app) called = False def set_child_property(prop_name, value): nonlocal called called = True self.assertEqual(prop_name, "aspect-ratio") GES.Effect.set_child_property(effect, prop_name, value) # When setting the aspect-ratio property, and the stars align, # the effect also changes the left/right properties. # Here we simulate the updating of the dependent properties. GES.Effect.set_child_property(effect, "left", 100) GES.Effect.set_child_property(effect, "right", 100) effect = GES.Effect.new("aspectratiocrop") effect.set_child_property = set_child_property effect_widget = manager.getEffectConfigurationUI(effect) widgets = {prop.name: widget for prop, widget in effect_widget.properties.items()} # Simulate the user choosing an aspect-ratio. widgets["aspect-ratio"].setWidgetValue(Gst.Fraction(4, 3)) mainloop.run(until_empty=True) self.assertTrue(called)
def test_dependent_properties(self): """Checks dependent properties updating is handled correctly.""" mainloop = common.create_main_loop() app = common.create_pitivi() app.project_manager.new_blank_project() manager = EffectsPropertiesManager(app) called = False def set_child_property(prop_name, value): nonlocal called called = True self.assertEqual(prop_name, "aspect-ratio") GES.Effect.set_child_property(effect, prop_name, value) # When setting the aspect-ratio property, and the stars align, # the effect also changes the left/right properties. # Here we simulate the updating of the dependent properties. GES.Effect.set_child_property(effect, "left", 100) GES.Effect.set_child_property(effect, "right", 100) effect = GES.Effect.new("aspectratiocrop") effect.set_child_property = set_child_property effect_widget = manager.getEffectConfigurationUI(effect) widgets = { prop.name: widget for prop, widget in effect_widget.properties.items() } # Simulate the user choosing an aspect-ratio. widgets["aspect-ratio"].setWidgetValue(Gst.Fraction(4, 3)) mainloop.run(until_empty=True) self.assertTrue(called)
class EffectProperties(Gtk.Expander, Loggable): """Widget for viewing a list of effects and configuring them. Attributes: app (Pitivi): The app. """ # pylint: disable=too-many-statements def __init__(self, app, clip_properties): Gtk.Expander.__init__(self) self.set_expanded(True) self.set_label(_("Effects")) Loggable.__init__(self) # Global variables related to effects self.app = app self._project = None self._selection = None self.selected_effects = [] self.clips = [] self._effect_config_ui = None self.effects_properties_manager = EffectsPropertiesManager(app) self.clip_properties = clip_properties # The toolbar that will go between the list of effects and properties buttons_box = Gtk.ButtonBox() buttons_box.set_halign(Gtk.Align.END) buttons_box.set_margin_end(SPACING) buttons_box.props.margin_top = SPACING / 2 remove_effect_button = Gtk.Button() remove_icon = Gtk.Image.new_from_icon_name("list-remove-symbolic", Gtk.IconSize.BUTTON) remove_effect_button.set_image(remove_icon) remove_effect_button.set_always_show_image(True) remove_effect_button.set_label(_("Remove effect")) buttons_box.pack_start(remove_effect_button, expand=False, fill=False, padding=0) # We need to specify Gtk.TreeDragSource because otherwise we are hitting # bug https://bugzilla.gnome.org/show_bug.cgi?id=730740. class EffectsListStore(Gtk.ListStore, Gtk.TreeDragSource): """Just a work around!""" # pylint: disable=non-parent-init-called def __init__(self, *args): Gtk.ListStore.__init__(self, *args) # Set the source index on the storemodel directly, # to avoid issues with the selection_data API. # FIXME: Work around # https://bugzilla.gnome.org/show_bug.cgi?id=737587 self.source_index = None def do_drag_data_get(self, path, unused_selection_data): self.source_index = path.get_indices()[0] self.storemodel = EffectsListStore(bool, str, str, str, str, object) self.treeview = Gtk.TreeView(model=self.storemodel) self.treeview.set_property("has_tooltip", True) self.treeview.set_headers_visible(False) self.treeview.props.margin_top = SPACING self.treeview.props.margin_left = SPACING # Without this, the treeview hides the border of its parent. # I should file a bug about this. self.treeview.props.margin_right = 1 activated_cell = Gtk.CellRendererToggle() activated_cell.props.xalign = 0 activated_cell.props.xpad = 0 activated_cell.connect("toggled", self._effectActiveToggleCb) self.treeview.insert_column_with_attributes(-1, _("Active"), activated_cell, active=COL_ACTIVATED) type_col = Gtk.TreeViewColumn(_("Type")) type_col.set_spacing(SPACING) type_col.set_sizing(Gtk.TreeViewColumnSizing.AUTOSIZE) type_cell = Gtk.CellRendererText() type_cell.props.xpad = PADDING type_col.pack_start(type_cell, expand=True) type_col.add_attribute(type_cell, "text", COL_TYPE) self.treeview.append_column(type_col) name_col = Gtk.TreeViewColumn(_("Effect name")) name_col.set_spacing(SPACING) name_cell = Gtk.CellRendererText() name_cell.props.xpad = PADDING name_cell.set_property("ellipsize", Pango.EllipsizeMode.END) name_col.pack_start(name_cell, expand=True) name_col.add_attribute(name_cell, "text", COL_NAME_TEXT) self.treeview.append_column(name_col) # Allow the treeview to accept EFFECT_TARGET_ENTRY when drag&dropping. self.treeview.enable_model_drag_dest([EFFECT_TARGET_ENTRY], Gdk.DragAction.COPY) # Enable reordering by drag&drop. self.treeview.enable_model_drag_source(Gdk.ModifierType.BUTTON1_MASK, [EFFECT_TARGET_ENTRY], Gdk.DragAction.MOVE) self.treeview_selection = self.treeview.get_selection() self.treeview_selection.set_mode(Gtk.SelectionMode.SINGLE) self._infobar = clip_properties.createInfoBar( _("Select a clip on the timeline to configure its associated effects" )) self._infobar.show_all() # Prepare the main container widgets and lay out everything self._vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) self._vbox.pack_start(self.treeview, expand=False, fill=False, padding=0) self._vbox.pack_start(buttons_box, expand=False, fill=False, padding=0) separator = Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL) separator.set_margin_top(SPACING) separator.set_margin_left(SPACING) separator.set_margin_right(SPACING) self._vbox.pack_start(separator, expand=False, fill=False, padding=0) self._vbox.show_all() self.add(self._vbox) self.hide() effects_actions_group = Gio.SimpleActionGroup() self.treeview.insert_action_group("clipproperties-effects", effects_actions_group) buttons_box.insert_action_group("clipproperties-effects", effects_actions_group) self.app.shortcuts.register_group("clipproperties-effects", _("Clip Effects"), position=60) self.remove_effect_action = Gio.SimpleAction.new("remove-effect", None) self.remove_effect_action.connect("activate", self._removeEffectCb) effects_actions_group.add_action(self.remove_effect_action) self.app.shortcuts.add("clipproperties-effects.remove-effect", ["Delete"], _("Remove the selected effect")) self.remove_effect_action.set_enabled(False) remove_effect_button.set_action_name( "clipproperties-effects.remove-effect") # Connect all the widget signals self.treeview_selection.connect("changed", self._treeviewSelectionChangedCb) self.treeview.connect("drag-motion", self._dragMotionCb) self.treeview.connect("drag-leave", self._dragLeaveCb) self.treeview.connect("drag-data-received", self._dragDataReceivedCb) self.treeview.connect("query-tooltip", self._treeViewQueryTooltipCb) self.app.project_manager.connect_after("new-project-loaded", self._newProjectLoadedCb) self.connect('notify::expanded', self._expandedCb) def _newProjectLoadedCb(self, unused_app, project): if self._selection is not None: self._selection.disconnect_by_func(self._selectionChangedCb) self._selection = None self._project = project if project: self._selection = project.ges_timeline.ui.selection self._selection.connect('selection-changed', self._selectionChangedCb) self.selected_effects = self._selection.getSelectedEffects() self.__updateAll() def _selectionChangedCb(self, selection): for clip in self.clips: clip.disconnect_by_func(self._trackElementAddedCb) clip.disconnect_by_func(self._trackElementRemovedCb) self.selected_effects = selection.getSelectedEffects() if selection: self.clips = list(selection.selected) for clip in self.clips: clip.connect("child-added", self._trackElementAddedCb) clip.connect("child-removed", self._trackElementRemovedCb) self.show() else: self.clips = [] self.hide() self.__updateAll() def _trackElementAddedCb(self, unused_clip, track_element): if isinstance(track_element, GES.BaseEffect): selec = self._selection.getSelectedEffects() self.selected_effects = selec self.__updateAll() for path, row in enumerate(self.storemodel): if row[COL_TRACK_EFFECT] == track_element: self.treeview_selection.select_path(path) break def _trackElementRemovedCb(self, unused_clip, track_element): if isinstance(track_element, GES.BaseEffect): selec = self._selection.getSelectedEffects() self.selected_effects = selec self.__updateAll() def _removeEffectCb(self, unused_action, unused_param): selected = self.treeview_selection.get_selected() if not selected[1]: # Cannot remove nothing, return effect = self.storemodel.get_value(selected[1], COL_TRACK_EFFECT) self._removeEffect(effect) def _removeEffect(self, effect): pipeline = self._project.pipeline with self.app.action_log.started( "remove effect", CommitTimelineFinalizingAction(pipeline)): self.__remove_configuration_widget() self.effects_properties_manager.cleanCache(effect) effect.get_parent().remove(effect) self._updateTreeview() def addEffectToClip(self, clip, factory_name, priority=None): """Adds the specified effect if it can be applied to the clip.""" if factory_name in ALLOWED_ONLY_ONCE_EFFECTS: for effect in clip.find_track_elements(None, GES.TrackType.VIDEO, GES.BaseEffect): for elem in effect.get_nleobject().iterate_recurse(): if elem.get_factory().get_name() == factory_name: self.error( "Not adding %s as it would be duplicate" " and this is not allowed.", factory_name) # TODO Let the user know about why it did not work. return effect model = self.treeview.get_model() media_type = self.app.effects.getInfo(factory_name).media_type for track_element in clip.get_children(False): track_type = track_element.get_track_type() if track_type == GES.TrackType.AUDIO and media_type == AUDIO_EFFECT or \ track_type == GES.TrackType.VIDEO and media_type == VIDEO_EFFECT: # Actually add the effect pipeline = self._project.pipeline with self.app.action_log.started( "add effect", CommitTimelineFinalizingAction(pipeline)): effect = GES.Effect.new(bin_description=factory_name) clip.add(effect) if priority is not None and priority < len(model): clip.set_top_effect_priority(effect, priority) break return None def addEffectToCurrentSelection(self, factory_name): """Adds an effect to the current selection. Args: factory_name (str): The name of the GstElementFactory for creating the effect. """ if not self.clips or len(self.clips) > 1: return clip = self.clips[0] # Checking that this effect can be applied on this track object # Which means, it has the corresponding media_type self.addEffectToClip(clip, factory_name) # pylint: disable=too-many-arguments def _dragMotionCb(self, unused_tree_view, unused_drag_context, unused_x, unused_y, unused_timestamp): self.debug( "Something is being dragged in the clip properties' effects list") self.drag_highlight() def _dragLeaveCb(self, unused_tree_view, unused_drag_context, unused_timestamp): self.info( "The item being dragged has left the clip properties' effects list" ) self.drag_unhighlight() # pylint: disable=too-many-arguments def _dragDataReceivedCb(self, treeview, drag_context, x, y, selection_data, unused_info, timestamp): if not self.clips or len(self.clips) > 1: # Indicate that a drop will not be accepted. Gdk.drag_status(drag_context, 0, timestamp) return clip = self.clips[0] dest_row = treeview.get_dest_row_at_pos(x, y) if drag_context.get_suggested_action() == Gdk.DragAction.COPY: # An effect dragged probably from the effects list. factory_name = str(selection_data.get_data(), "UTF-8") drop_index = self.__get_new_effect_index(dest_row) self.addEffectToClip(clip, factory_name, drop_index) elif drag_context.get_suggested_action() == Gdk.DragAction.MOVE: # An effect dragged from the same treeview to change its position. # Source source_index, drop_index = self.__get_move_indexes( dest_row, treeview.get_model()) self.__move_effect(clip, source_index, drop_index) drag_context.finish(True, False, timestamp) # pylint: disable=no-self-use def __get_new_effect_index(self, dest_row): # Target if dest_row: drop_path, drop_pos = dest_row drop_index = drop_path.get_indices()[0] if drop_pos != Gtk.TreeViewDropPosition.BEFORE: drop_index += 1 else: # This should happen when dragging after the last row. drop_index = None return drop_index def __get_move_indexes(self, dest_row, model): source_index = self.storemodel.source_index self.storemodel.source_index = None # Target if dest_row: drop_path, drop_pos = dest_row drop_index = drop_path.get_indices()[0] drop_index = self.calculateEffectPriority(source_index, drop_index, drop_pos) else: # This should happen when dragging after the last row. drop_index = len(model) - 1 drop_pos = Gtk.TreeViewDropPosition.INTO_OR_BEFORE return source_index, drop_index def __move_effect(self, clip, source_index, drop_index): if source_index == drop_index: # Noop. return # The paths are different. effects = clip.get_top_effects() effect = effects[source_index] pipeline = self._project.ges_timeline.get_parent() with self.app.action_log.started( "move effect", CommitTimelineFinalizingAction(pipeline)): clip.set_top_effect_priority(effect, drop_index) new_path = Gtk.TreePath.new() new_path.append_index(drop_index) self.__updateAll(path=new_path) @staticmethod def calculateEffectPriority(source_index, drop_index, drop_pos): """Calculates where the effect from source_index will end up.""" if drop_pos in (Gtk.TreeViewDropPosition.INTO_OR_BEFORE, Gtk.TreeViewDropPosition.INTO_OR_AFTER): return drop_index if drop_pos == Gtk.TreeViewDropPosition.BEFORE: if source_index < drop_index: return drop_index - 1 elif drop_pos == Gtk.TreeViewDropPosition.AFTER: if source_index > drop_index: return drop_index + 1 return drop_index def _effectActiveToggleCb(self, cellrenderertoggle, path): _iter = self.storemodel.get_iter(path) tck_effect = self.storemodel.get_value(_iter, COL_TRACK_EFFECT) with self.app.action_log.started("change active state"): tck_effect.props.active = not tck_effect.props.active cellrenderertoggle.set_active(tck_effect.is_active()) self._updateTreeview() self._project.ges_timeline.commit() def _expandedCb(self, unused_expander, unused_params): self.__updateAll() def _treeViewQueryTooltipCb(self, view, x, y, keyboard_mode, tooltip): is_row, x, y, unused_model, path, tree_iter = view.get_tooltip_context( x, y, keyboard_mode) if not is_row: return False view.set_tooltip_row(tooltip, path) description = self.storemodel.get_value(tree_iter, COL_DESC_TEXT) bin_description = self.storemodel.get_value(tree_iter, COL_BIN_DESCRIPTION_TEXT) tooltip.set_text("%s\n%s" % (bin_description, description)) return True def __updateAll(self, path=None): if len(self.clips) == 1: self.show() self._infobar.hide() self._updateTreeview() if path: self.treeview_selection.select_path(path) else: self.hide() self.__remove_configuration_widget() self.storemodel.clear() self._infobar.show() def _updateTreeview(self): self.storemodel.clear() clip = self.clips[0] for effect in clip.get_top_effects(): if effect.props.bin_description in HIDDEN_EFFECTS: continue effect_info = self.app.effects.getInfo( effect.props.bin_description) to_append = [effect.props.active] track_type = effect.get_track_type() if track_type == GES.TrackType.AUDIO: to_append.append("Audio") elif track_type == GES.TrackType.VIDEO: to_append.append("Video") to_append.append(effect.props.bin_description) to_append.append(effect_info.human_name) to_append.append(effect_info.description) to_append.append(effect) self.storemodel.append(to_append) self._vbox.set_visible(len(self.storemodel) > 0) def _treeviewSelectionChangedCb(self, unused_treeview): selection_is_emtpy = self.treeview_selection.count_selected_rows() == 0 self.remove_effect_action.set_enabled(not selection_is_emtpy) self._updateEffectConfigUi() def _updateEffectConfigUi(self): model, tree_iter = self.treeview_selection.get_selected() if tree_iter: effect = model.get_value(tree_iter, COL_TRACK_EFFECT) self._showEffectConfigurationWidget(effect) else: self.__remove_configuration_widget() def __remove_configuration_widget(self): if not self._effect_config_ui: # Nothing to remove. return self._effect_config_ui.deactivate_keyframe_toggle_buttons() self._vbox.remove(self._effect_config_ui) self._effect_config_ui = None def _showEffectConfigurationWidget(self, effect): self.__remove_configuration_widget() self._effect_config_ui = self.effects_properties_manager.getEffectConfigurationUI( effect) if not self._effect_config_ui: return self._effect_config_ui.show() self._effect_config_ui.show_all() self._vbox.add(self._effect_config_ui)
class EffectProperties(Gtk.Expander, Loggable): """ Widget for viewing a list of effects and configuring them. @type app: C{Pitivi} @type effects_properties_manager: C{EffectsPropertiesManager} """ # pylint: disable=too-many-statements def __init__(self, app, clip_properties): Gtk.Expander.__init__(self) self.set_expanded(True) self.set_label(_("Effects")) Loggable.__init__(self) # Global variables related to effects self.app = app self._project = None self._selection = None self.selected_effects = [] self.clips = [] self._effect_config_ui = None self.effects_properties_manager = EffectsPropertiesManager(app) self.clip_properties = clip_properties # The toolbar that will go between the list of effects and properties buttons_box = Gtk.ButtonBox() buttons_box.set_halign(Gtk.Align.END) buttons_box.set_margin_end(SPACING) buttons_box.props.margin_top = SPACING / 2 remove_effect_button = Gtk.Button() remove_icon = Gtk.Image.new_from_icon_name("list-remove-symbolic", Gtk.IconSize.BUTTON) remove_effect_button.set_image(remove_icon) remove_effect_button.set_always_show_image(True) remove_effect_button.set_label(_("Remove effect")) buttons_box.pack_start(remove_effect_button, expand=False, fill=False, padding=0) # We need to specify Gtk.TreeDragSource because otherwise we are hitting # bug https://bugzilla.gnome.org/show_bug.cgi?id=730740. class EffectsListStore(Gtk.ListStore, Gtk.TreeDragSource): """ Just a work around! """ # pylint: disable=non-parent-init-called def __init__(self, *args): Gtk.ListStore.__init__(self, *args) # Simply set the source index on the storemodrel directly # to avoid issues with the selection_data API # FIXME: Work around # https://bugzilla.gnome.org/show_bug.cgi?id=737587 self.source_index = None def do_drag_data_get(self, path, unused_selection_data): self.source_index = path.get_indices()[0] self.storemodel = EffectsListStore(bool, str, str, str, str, object) self.treeview = Gtk.TreeView(model=self.storemodel) self.treeview.set_property("has_tooltip", True) self.treeview.set_headers_visible(False) self.treeview.props.margin_top = SPACING self.treeview.props.margin_left = SPACING # Without this, the treeview hides the border of its parent. # I should file a bug about this. self.treeview.props.margin_right = 1 activated_cell = Gtk.CellRendererToggle() activated_cell.props.xalign = 0 activated_cell.props.xpad = 0 activated_cell.connect("toggled", self._effectActiveToggleCb) self.treeview.insert_column_with_attributes(-1, _("Active"), activated_cell, active=COL_ACTIVATED) type_col = Gtk.TreeViewColumn(_("Type")) type_col.set_spacing(SPACING) type_col.set_sizing(Gtk.TreeViewColumnSizing.AUTOSIZE) type_cell = Gtk.CellRendererText() type_cell.props.xpad = PADDING type_col.pack_start(type_cell, expand=True) type_col.add_attribute(type_cell, "text", COL_TYPE) self.treeview.append_column(type_col) name_col = Gtk.TreeViewColumn(_("Effect name")) name_col.set_spacing(SPACING) name_cell = Gtk.CellRendererText() name_cell.props.xpad = PADDING name_cell.set_property("ellipsize", Pango.EllipsizeMode.END) name_col.pack_start(name_cell, expand=True) name_col.add_attribute(name_cell, "text", COL_NAME_TEXT) self.treeview.append_column(name_col) # Allow the treeview to accept EFFECT_TARGET_ENTRY when drag&dropping. self.treeview.enable_model_drag_dest([EFFECT_TARGET_ENTRY], Gdk.DragAction.COPY) # Enable reordering by drag&drop. self.treeview.enable_model_drag_source(Gdk.ModifierType.BUTTON1_MASK, [EFFECT_TARGET_ENTRY], Gdk.DragAction.MOVE) self.treeview_selection = self.treeview.get_selection() self.treeview_selection.set_mode(Gtk.SelectionMode.SINGLE) self._infobar = clip_properties.createInfoBar( _("Select a clip on the timeline to configure its associated effects")) self._infobar.show_all() # Prepare the main container widgets and lay out everything self._vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) self._vbox.pack_start(self.treeview, expand=False, fill=False, padding=0) self._vbox.pack_start(buttons_box, expand=False, fill=False, padding=0) separator = Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL) separator.set_margin_top(SPACING) separator.set_margin_left(SPACING) separator.set_margin_right(SPACING) self._vbox.pack_start(separator, expand=False, fill=False, padding=0) self._vbox.show_all() self.add(self._vbox) self.hide() effects_actions_group = Gio.SimpleActionGroup() self.treeview.insert_action_group("clipproperties_effects", effects_actions_group) buttons_box.insert_action_group("clipproperties_effects", effects_actions_group) self.remove_effect_action = Gio.SimpleAction.new("remove_effect", None) self.remove_effect_action.connect("activate", self._removeEffectCb) effects_actions_group.add_action(self.remove_effect_action) self.app.add_accelerator("Delete", "clipproperties_effects.remove_effect", None) self.remove_effect_action.set_enabled(False) remove_effect_button.set_action_name("clipproperties_effects.remove_effect") # Connect all the widget signals self.treeview_selection.connect("changed", self._treeviewSelectionChangedCb) self.treeview.connect("drag-motion", self._dragMotionCb) self.treeview.connect("drag-leave", self._dragLeaveCb) self.treeview.connect("drag-data-received", self._dragDataReceivedCb) self.treeview.connect("query-tooltip", self._treeViewQueryTooltipCb) self.app.project_manager.connect( "new-project-loaded", self._newProjectLoadedCb) self.connect('notify::expanded', self._expandedCb) def _newProjectLoadedCb(self, unused_app, project): if self._selection is not None: self._selection.disconnect_by_func(self._selectionChangedCb) self._selection = None self._project = project if project: self._selection = project.timeline.ui.selection self._selection.connect('selection-changed', self._selectionChangedCb) self.selected_effects = self._selection.getSelectedEffects() self.__updateAll() def _selectionChangedCb(self, selection): for clip in self.clips: clip.disconnect_by_func(self._trackElementAddedCb) clip.disconnect_by_func(self._trackElementRemovedCb) self.selected_effects = selection.getSelectedEffects() if selection: self.clips = list(selection.selected) for clip in self.clips: clip.connect("child-added", self._trackElementAddedCb) clip.connect("child-removed", self._trackElementRemovedCb) self.show() else: self.clips = [] self.hide() self.__updateAll() def _trackElementAddedCb(self, unused_clip, track_element): if isinstance(track_element, GES.BaseEffect): selec = self._selection.getSelectedEffects() self.selected_effects = selec self.__updateAll() def _trackElementRemovedCb(self, unused_clip, track_element): if isinstance(track_element, GES.BaseEffect): selec = self._selection.getSelectedEffects() self.selected_effects = selec self.__updateAll() def _removeEffectCb(self, unused_action, unused_param): selected = self.treeview_selection.get_selected() if not selected[1]: # Cannot remove nothing, return effect = self.storemodel.get_value(selected[1], COL_TRACK_EFFECT) self._removeEffect(effect) def _removeEffect(self, effect): self.app.action_log.begin("remove effect") self.__remove_configuration_widget() self.effects_properties_manager.cleanCache(effect) effect.get_parent().remove(effect) self._project.timeline.commit() self.app.action_log.commit() self._updateTreeview() def addEffectToClip(self, clip, factory_name, priority=None): """Adds the specified effect if it can be applied to the clip.""" model = self.treeview.get_model() media_type = self.app.effects.getInfo(factory_name).media_type for track_element in clip.get_children(False): track_type = track_element.get_track_type() if track_type == GES.TrackType.AUDIO and media_type == AUDIO_EFFECT or \ track_type == GES.TrackType.VIDEO and media_type == VIDEO_EFFECT: # Actually add the effect self.app.action_log.begin("add effect") effect = GES.Effect.new(bin_description=factory_name) clip.add(effect) if priority is not None and priority < len(model): clip.set_top_effect_priority(effect, priority) self._project.timeline.commit() self.app.action_log.commit() self.__updateAll() break def addEffectToCurrentSelection(self, factory_name): """ Add an effect to the current selection Args: factory_name (str): The name of the GstElementFactory to creaye the effect from. """ if not self.clips or len(self.clips) > 1: return clip = self.clips[0] # Checking that this effect can be applied on this track object # Which means, it has the corresponding media_type self.addEffectToClip(clip, factory_name) # pylint: disable=too-many-arguments def _dragMotionCb(self, unused_tree_view, unused_drag_context, unused_x, unused_y, unused_timestamp): self.debug( "Something is being dragged in the clip properties' effects list") self.drag_highlight() def _dragLeaveCb(self, unused_tree_view, unused_drag_context, unused_timestamp): self.info( "The item being dragged has left the clip properties' effects list") self.drag_unhighlight() # pylint: disable=too-many-arguments def _dragDataReceivedCb(self, treeview, drag_context, x, y, selection_data, unused_info, timestamp): if not self.clips or len(self.clips) > 1: # Indicate that a drop will not be accepted. Gdk.drag_status(drag_context, 0, timestamp) return clip = self.clips[0] dest_row = treeview.get_dest_row_at_pos(x, y) if drag_context.get_suggested_action() == Gdk.DragAction.COPY: # An effect dragged probably from the effects list. factory_name = str(selection_data.get_data(), "UTF-8") drop_index = self.__get_new_effect_index(dest_row) self.addEffectToClip(clip, factory_name, drop_index) elif drag_context.get_suggested_action() == Gdk.DragAction.MOVE: # An effect dragged from the same treeview to change its position. # Source source_index, drop_index = self.__get_move_indexes( dest_row, treeview.get_model()) self.__move_effect(clip, source_index, drop_index) drag_context.finish(True, False, timestamp) # pylint: disable=no-self-use def __get_new_effect_index(self, dest_row): # Target if dest_row: drop_path, drop_pos = dest_row drop_index = drop_path.get_indices()[0] if drop_pos != Gtk.TreeViewDropPosition.BEFORE: drop_index += 1 else: # This should happen when dragging after the last row. drop_index = None return drop_index def __get_move_indexes(self, dest_row, model): source_index = self.storemodel.source_index self.storemodel.source_index = None # Target if dest_row: drop_path, drop_pos = dest_row drop_index = drop_path.get_indices()[0] drop_index = self.calculateEffectPriority( source_index, drop_index, drop_pos) else: # This should happen when dragging after the last row. drop_index = len(model) - 1 drop_pos = Gtk.TreeViewDropPosition.INTO_OR_BEFORE return source_index, drop_index def __move_effect(self, clip, source_index, drop_index): if source_index == drop_index: # Noop. return # The paths are different. effects = clip.get_top_effects() effect = effects[source_index] self.app.action_log.begin("move effect") clip.set_top_effect_priority(effect, drop_index) self._project.timeline.commit() self.app.action_log.commit() self._project.pipeline.flushSeek() new_path = Gtk.TreePath.new() new_path.append_index(drop_index) self.__updateAll(path=new_path) @staticmethod def calculateEffectPriority(source_index, drop_index, drop_pos): """ Return where the effect from source_index will end up """ if drop_pos in (Gtk.TreeViewDropPosition.INTO_OR_BEFORE, Gtk.TreeViewDropPosition.INTO_OR_AFTER): return drop_index if drop_pos == Gtk.TreeViewDropPosition.BEFORE: if source_index < drop_index: return drop_index - 1 elif drop_pos == Gtk.TreeViewDropPosition.AFTER: if source_index > drop_index: return drop_index + 1 return drop_index def _effectActiveToggleCb(self, cellrenderertoggle, path): _iter = self.storemodel.get_iter(path) tck_effect = self.storemodel.get_value(_iter, COL_TRACK_EFFECT) self.app.action_log.begin("change active state") tck_effect.set_active(not tck_effect.is_active()) cellrenderertoggle.set_active(tck_effect.is_active()) self._updateTreeview() self._project.timeline.commit() self.app.action_log.commit() def _expandedCb(self, unused_expander, unused_params): self.__updateAll() def _treeViewQueryTooltipCb(self, view, x, y, keyboard_mode, tooltip): is_row, x, y, unused_model, path, tree_iter = view.get_tooltip_context( x, y, keyboard_mode) if not is_row: return False view.set_tooltip_row(tooltip, path) description = self.storemodel.get_value(tree_iter, COL_DESC_TEXT) bin_description = self.storemodel.get_value( tree_iter, COL_BIN_DESCRIPTION_TEXT) tooltip.set_text("%s\n%s" % (bin_description, description)) return True def __updateAll(self, path=None): if len(self.clips) == 1: self.show() self._infobar.hide() self._updateTreeview() if path: self.treeview_selection.select_path(path) else: self.hide() self.__remove_configuration_widget() self.storemodel.clear() self._infobar.show() def _updateTreeview(self): self.storemodel.clear() clip = self.clips[0] for effect in clip.get_top_effects(): if effect.props.bin_description in HIDDEN_EFFECTS: continue effect_info = self.app.effects.getInfo(effect.props.bin_description) to_append = [effect.props.active] track_type = effect.get_track_type() if track_type == GES.TrackType.AUDIO: to_append.append("Audio") elif track_type == GES.TrackType.VIDEO: to_append.append("Video") to_append.append(effect.props.bin_description) to_append.append(effect_info.human_name) to_append.append(effect_info.description) to_append.append(effect) self.storemodel.append(to_append) self._vbox.set_visible(len(self.storemodel) > 0) def _treeviewSelectionChangedCb(self, unused_treeview): selection_is_emtpy = self.treeview_selection.count_selected_rows() == 0 self.remove_effect_action.set_enabled(not selection_is_emtpy) self._updateEffectConfigUi() def _updateEffectConfigUi(self): model, tree_iter = self.treeview_selection.get_selected() if tree_iter: effect = model.get_value(tree_iter, COL_TRACK_EFFECT) self._showEffectConfigurationWidget(effect) else: self.__remove_configuration_widget() def __remove_configuration_widget(self): if not self._effect_config_ui: # Nothing to remove. return self._effect_config_ui.deactivate_keyframe_toggle_buttons() self._vbox.remove(self._effect_config_ui) self._effect_config_ui = None def _showEffectConfigurationWidget(self, effect): self.__remove_configuration_widget() self._effect_config_ui = self.effects_properties_manager.getEffectConfigurationUI( effect) if not self._effect_config_ui: return self._effect_config_ui.show() self._effect_config_ui.show_all() self._vbox.add(self._effect_config_ui)
class EffectProperties(Gtk.Expander, Loggable): """Widget for viewing a list of effects and configuring them. Attributes: app (Pitivi): The app. clip (GES.Clip): The clip being configured. """ # pylint: disable=too-many-statements def __init__(self, app, clip_properties): Gtk.Expander.__init__(self) self.set_expanded(True) self.set_label(_("Effects")) Loggable.__init__(self) # Global variables related to effects self.app = app self._project = None self._selection = None self.clip = None self._effect_config_ui = None self.effects_properties_manager = EffectsPropertiesManager(app) setup_custom_effect_widgets(self.effects_properties_manager) self.clip_properties = clip_properties no_effect_label = Gtk.Label( _("To apply an effect to the clip, drag it from the Effect Library.")) no_effect_label.set_line_wrap(True) self.no_effect_infobar = Gtk.InfoBar() fix_infobar(self.no_effect_infobar) self.no_effect_infobar.props.message_type = Gtk.MessageType.OTHER self.no_effect_infobar.get_content_area().add(no_effect_label) # The toolbar that will go between the list of effects and properties buttons_box = Gtk.ButtonBox() buttons_box.set_halign(Gtk.Align.END) buttons_box.set_margin_end(SPACING) buttons_box.props.margin_top = SPACING / 2 remove_effect_button = Gtk.Button() remove_icon = Gtk.Image.new_from_icon_name("list-remove-symbolic", Gtk.IconSize.BUTTON) remove_effect_button.set_image(remove_icon) remove_effect_button.set_always_show_image(True) remove_effect_button.set_label(_("Remove effect")) buttons_box.pack_start(remove_effect_button, expand=False, fill=False, padding=0) # We need to specify Gtk.TreeDragSource because otherwise we are hitting # bug https://bugzilla.gnome.org/show_bug.cgi?id=730740. class EffectsListStore(Gtk.ListStore, Gtk.TreeDragSource): """Just a work around!""" # pylint: disable=non-parent-init-called def __init__(self, *args): Gtk.ListStore.__init__(self, *args) # Set the source index on the storemodel directly, # to avoid issues with the selection_data API. # FIXME: Work around # https://bugzilla.gnome.org/show_bug.cgi?id=737587 self.source_index = None def do_drag_data_get(self, path, unused_selection_data): self.source_index = path.get_indices()[0] self.storemodel = EffectsListStore(bool, str, str, str, str, object) self.treeview = Gtk.TreeView(model=self.storemodel) self.treeview.set_property("has_tooltip", True) self.treeview.set_headers_visible(False) self.treeview.props.margin_top = SPACING self.treeview.props.margin_left = SPACING # Without this, the treeview hides the border of its parent. # I should file a bug about this. self.treeview.props.margin_right = 1 activated_cell = Gtk.CellRendererToggle() activated_cell.props.xalign = 0 activated_cell.props.xpad = 0 activated_cell.connect("toggled", self._effectActiveToggleCb) self.treeview.insert_column_with_attributes(-1, _("Active"), activated_cell, active=COL_ACTIVATED) type_col = Gtk.TreeViewColumn(_("Type")) type_col.set_spacing(SPACING) type_col.set_sizing(Gtk.TreeViewColumnSizing.AUTOSIZE) type_cell = Gtk.CellRendererText() type_cell.props.xpad = PADDING type_col.pack_start(type_cell, expand=True) type_col.add_attribute(type_cell, "text", COL_TYPE) self.treeview.append_column(type_col) name_col = Gtk.TreeViewColumn(_("Effect name")) name_col.set_spacing(SPACING) name_cell = Gtk.CellRendererText() name_cell.props.xpad = PADDING name_cell.set_property("ellipsize", Pango.EllipsizeMode.END) name_col.pack_start(name_cell, expand=True) name_col.add_attribute(name_cell, "text", COL_NAME_TEXT) self.treeview.append_column(name_col) # Allow the entire expander to accept EFFECT_TARGET_ENTRY when # drag&dropping. self.drag_dest_set(Gtk.DestDefaults.DROP, [EFFECT_TARGET_ENTRY], Gdk.DragAction.COPY) # Allow also the treeview to accept EFFECT_TARGET_ENTRY when # drag&dropping so the effect can be dragged at a specific position. self.treeview.enable_model_drag_dest([EFFECT_TARGET_ENTRY], Gdk.DragAction.COPY) # Enable reordering by drag&drop. self.treeview.enable_model_drag_source(Gdk.ModifierType.BUTTON1_MASK, [EFFECT_TARGET_ENTRY], Gdk.DragAction.MOVE) self.treeview_selection = self.treeview.get_selection() self.treeview_selection.set_mode(Gtk.SelectionMode.SINGLE) self._infobar = clip_properties.createInfoBar( _("Select a clip on the timeline to configure its associated effects")) self._infobar.show_all() # Prepare the main container widgets and lay out everything self._expander_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) self._vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) self._vbox.pack_start(self.treeview, expand=False, fill=False, padding=0) self._vbox.pack_start(buttons_box, expand=False, fill=False, padding=0) separator = Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL) separator.set_margin_top(SPACING) separator.set_margin_left(SPACING) separator.set_margin_right(SPACING) self._vbox.pack_start(separator, expand=False, fill=False, padding=0) self._vbox.show_all() self._expander_box.pack_start(self.no_effect_infobar, expand=False, fill=False, padding=0) self._expander_box.pack_start(self._vbox, expand=False, fill=False, padding=0) self._expander_box.show_all() self.add(self._expander_box) self.hide() effects_actions_group = Gio.SimpleActionGroup() self.treeview.insert_action_group("clipproperties-effects", effects_actions_group) buttons_box.insert_action_group("clipproperties-effects", effects_actions_group) self.app.shortcuts.register_group("clipproperties-effects", _("Clip Effects"), position=60) self.remove_effect_action = Gio.SimpleAction.new("remove-effect", None) self.remove_effect_action.connect("activate", self._removeEffectCb) effects_actions_group.add_action(self.remove_effect_action) self.app.shortcuts.add("clipproperties-effects.remove-effect", ["Delete"], _("Remove the selected effect")) self.remove_effect_action.set_enabled(False) remove_effect_button.set_action_name("clipproperties-effects.remove-effect") # Connect all the widget signals self.treeview_selection.connect("changed", self._treeviewSelectionChangedCb) self.connect("drag-motion", self._drag_motion_cb) self.connect("drag-leave", self._drag_leave_cb) self.connect("drag-data-received", self._drag_data_received_cb) self.treeview.connect("drag-motion", self._drag_motion_cb) self.treeview.connect("drag-leave", self._drag_leave_cb) self.treeview.connect("drag-data-received", self._drag_data_received_cb) self.treeview.connect("query-tooltip", self._treeViewQueryTooltipCb) self.app.project_manager.connect_after( "new-project-loaded", self._newProjectLoadedCb) self.connect('notify::expanded', self._expandedCb) def _newProjectLoadedCb(self, unused_app, project): if self._selection is not None: self._selection.disconnect_by_func(self._selectionChangedCb) self._selection = None self._project = project if project: self._selection = project.ges_timeline.ui.selection self._selection.connect('selection-changed', self._selectionChangedCb) self.__updateAll() def _selectionChangedCb(self, selection): if self.clip: self.clip.disconnect_by_func(self._trackElementAddedCb) self.clip.disconnect_by_func(self._trackElementRemovedCb) for track_element in self.clip.get_children(recursive=True): if isinstance(track_element, GES.BaseEffect): self._disconnect_from_track_element(track_element) clips = list(selection.selected) self.clip = clips[0] if len(clips) == 1 else None if self.clip: self.clip.connect("child-added", self._trackElementAddedCb) self.clip.connect("child-removed", self._trackElementRemovedCb) for track_element in self.clip.get_children(recursive=True): if isinstance(track_element, GES.BaseEffect): self._connect_to_track_element(track_element) self.__updateAll() def _trackElementAddedCb(self, unused_clip, track_element): if isinstance(track_element, GES.BaseEffect): self._connect_to_track_element(track_element) self.__updateAll() for path, row in enumerate(self.storemodel): if row[COL_TRACK_EFFECT] == track_element: self.treeview_selection.select_path(path) break def _connect_to_track_element(self, track_element): track_element.connect("notify::active", self._notify_active_cb) track_element.connect("notify::priority", self._notify_priority_cb) def _disconnect_from_track_element(self, track_element): track_element.disconnect_by_func(self._notify_active_cb) track_element.disconnect_by_func(self._notify_priority_cb) def _notify_active_cb(self, unused_track_element, unused_param_spec): self._updateTreeview() def _notify_priority_cb(self, unused_track_element, unused_param_spec): self._updateTreeview() def _trackElementRemovedCb(self, unused_clip, track_element): if isinstance(track_element, GES.BaseEffect): self._disconnect_from_track_element(track_element) self.__updateAll() def _removeEffectCb(self, unused_action, unused_param): selected = self.treeview_selection.get_selected() if not selected[1]: # Cannot remove nothing, return effect = self.storemodel.get_value(selected[1], COL_TRACK_EFFECT) selection_path = self.storemodel.get_path(selected[1]) # Preserve selection in the tree view. next_selection_index = selection_path.get_indices()[0] effect_count = self.storemodel.iter_n_children() if effect_count - 1 == next_selection_index: next_selection_index -= 1 self._removeEffect(effect) if next_selection_index >= 0: self.treeview_selection.select_path(next_selection_index) def _removeEffect(self, effect): pipeline = self._project.pipeline with self.app.action_log.started("remove effect", finalizing_action=CommitTimelineFinalizingAction(pipeline), toplevel=True): self.__remove_configuration_widget() self.effects_properties_manager.cleanCache(effect) effect.get_parent().remove(effect) def _drag_motion_cb(self, unused_widget, unused_drag_context, unused_x, unused_y, unused_timestamp): """Highlights some widgets to indicate it can receive drag&drop.""" self.debug( "Something is being dragged in the clip properties' effects list") self.no_effect_infobar.drag_highlight() # It would be nicer to highlight only the treeview, but # it does not seem to have a visible effect. self._vbox.drag_highlight() def _drag_leave_cb(self, unused_widget, unused_drag_context, unused_timestamp): """Unhighlights the widgets which can receive drag&drop.""" self.debug( "The item being dragged has left the clip properties' effects list") self.no_effect_infobar.drag_unhighlight() self._vbox.drag_unhighlight() # pylint: disable=too-many-arguments def _drag_data_received_cb(self, widget, drag_context, x, y, selection_data, unused_info, timestamp): if not self.clip: # Indicate that a drop will not be accepted. Gdk.drag_status(drag_context, 0, timestamp) return dest_row = self.treeview.get_dest_row_at_pos(x, y) if drag_context.get_suggested_action() == Gdk.DragAction.COPY: # An effect dragged probably from the effects list. factory_name = str(selection_data.get_data(), "UTF-8") if widget is self.treeview: drop_index = self.__get_new_effect_index(dest_row) else: drop_index = len(self.storemodel) self.debug("Effect dragged at position %s", drop_index) effect_info = self.app.effects.getInfo(factory_name) pipeline = self._project.pipeline with self.app.action_log.started("add effect", finalizing_action=CommitTimelineFinalizingAction(pipeline), toplevel=True): effect = self.clip.ui.add_effect(effect_info) if effect: self.clip.set_top_effect_index(effect, drop_index) elif drag_context.get_suggested_action() == Gdk.DragAction.MOVE: # An effect dragged from the same treeview to change its position. # Source source_index, drop_index = self.__get_move_indexes( dest_row, self.treeview.get_model()) self.__move_effect(self.clip, source_index, drop_index) drag_context.finish(True, False, timestamp) # pylint: disable=no-self-use def __get_new_effect_index(self, dest_row): # Target if dest_row: drop_path, drop_pos = dest_row drop_index = drop_path.get_indices()[0] if drop_pos != Gtk.TreeViewDropPosition.BEFORE: drop_index += 1 else: # This should happen when dragging after the last row. drop_index = None return drop_index def __get_move_indexes(self, dest_row, model): source_index = self.storemodel.source_index self.storemodel.source_index = None # Target if dest_row: drop_path, drop_pos = dest_row drop_index = drop_path.get_indices()[0] drop_index = self.calculateEffectPriority( source_index, drop_index, drop_pos) else: # This should happen when dragging after the last row. drop_index = len(model) - 1 return source_index, drop_index def __move_effect(self, clip, source_index, drop_index): if source_index == drop_index: # Noop. return # The paths are different. effects = clip.get_top_effects() effect = effects[source_index] pipeline = self._project.ges_timeline.get_parent() with self.app.action_log.started("move effect", finalizing_action=CommitTimelineFinalizingAction(pipeline), toplevel=True): clip.set_top_effect_index(effect, drop_index) new_path = Gtk.TreePath.new() new_path.append_index(drop_index) self.__updateAll(path=new_path) @staticmethod def calculateEffectPriority(source_index, drop_index, drop_pos): """Calculates where the effect from source_index will end up.""" if drop_pos in (Gtk.TreeViewDropPosition.INTO_OR_BEFORE, Gtk.TreeViewDropPosition.INTO_OR_AFTER): return drop_index if drop_pos == Gtk.TreeViewDropPosition.BEFORE: if source_index < drop_index: return drop_index - 1 elif drop_pos == Gtk.TreeViewDropPosition.AFTER: if source_index > drop_index: return drop_index + 1 return drop_index def _effectActiveToggleCb(self, cellrenderertoggle, path): _iter = self.storemodel.get_iter(path) effect = self.storemodel.get_value(_iter, COL_TRACK_EFFECT) pipeline = self._project.ges_timeline.get_parent() with self.app.action_log.started("change active state", finalizing_action=CommitTimelineFinalizingAction(pipeline), toplevel=True): effect.props.active = not effect.props.active # This is not strictly necessary, but makes sure # the UI reflects the current status. cellrenderertoggle.set_active(effect.is_active()) def _expandedCb(self, unused_expander, unused_params): self.__updateAll() def _treeViewQueryTooltipCb(self, view, x, y, keyboard_mode, tooltip): is_row, x, y, unused_model, path, tree_iter = view.get_tooltip_context( x, y, keyboard_mode) if not is_row: return False view.set_tooltip_row(tooltip, path) description = self.storemodel.get_value(tree_iter, COL_DESC_TEXT) bin_description = self.storemodel.get_value( tree_iter, COL_BIN_DESCRIPTION_TEXT) tooltip.set_text("%s\n%s" % (bin_description, description)) return True def __updateAll(self, path=None): if self.clip: self.show() self._infobar.hide() self._updateTreeview() if path: self.treeview_selection.select_path(path) else: self.hide() self.__remove_configuration_widget() self.storemodel.clear() self._infobar.show() def _updateTreeview(self): self.storemodel.clear() for effect in self.clip.get_top_effects(): if effect.props.bin_description in HIDDEN_EFFECTS: continue effect_info = self.app.effects.getInfo(effect.props.bin_description) to_append = [effect.props.active] track_type = effect.get_track_type() if track_type == GES.TrackType.AUDIO: to_append.append("Audio") elif track_type == GES.TrackType.VIDEO: to_append.append("Video") to_append.append(effect.props.bin_description) to_append.append(effect_info.human_name) to_append.append(effect_info.description) to_append.append(effect) self.storemodel.append(to_append) has_effects = len(self.storemodel) > 0 self.no_effect_infobar.set_visible(not has_effects) self._vbox.set_visible(has_effects) def _treeviewSelectionChangedCb(self, unused_treeview): selection_is_emtpy = self.treeview_selection.count_selected_rows() == 0 self.remove_effect_action.set_enabled(not selection_is_emtpy) self._updateEffectConfigUi() def _updateEffectConfigUi(self): model, tree_iter = self.treeview_selection.get_selected() if tree_iter: effect = model.get_value(tree_iter, COL_TRACK_EFFECT) self._showEffectConfigurationWidget(effect) else: self.__remove_configuration_widget() def __remove_configuration_widget(self): if not self._effect_config_ui: # Nothing to remove. return self._effect_config_ui.deactivate_keyframe_toggle_buttons() self._vbox.remove(self._effect_config_ui) self._effect_config_ui = None def _showEffectConfigurationWidget(self, effect): self.__remove_configuration_widget() self._effect_config_ui = self.effects_properties_manager.getEffectConfigurationUI( effect) if not self._effect_config_ui: return self._effect_config_ui.show() self._effect_config_ui.show_all() self._vbox.add(self._effect_config_ui)