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.get_effect_configuration_ui(effect) widgets = {prop.name: widget for prop, widget in effect_widget.properties.items()} # Simulate the user choosing an aspect-ratio. widgets["aspect-ratio"].set_widget_value(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)
def _register_alpha_widget(self, widgets): """Sets up an EffectsPropertiesManager instance to create custom effect UI.""" self.alpha_effect = GES.Effect.new("alpha") self.prop_name = "black-sensitivity" _, _, self.prop = self.alpha_effect.lookup_child(self.prop_name) self.effects_prop_manager = EffectsPropertiesManager(self.app) self.effects_prop_manager.connect("create-widget", self.create_alpha_widget_cb, widgets) self.element_settings_widget = GstElementSettingsWidget(self.alpha_effect, PROPS_TO_IGNORE) self.effects_prop_manager.emit("create-widget", self.element_settings_widget, self.alpha_effect) self.effects_prop_manager._connect_all_widget_callbacks(self.element_settings_widget, self.alpha_effect) self.effects_prop_manager._post_configuration(self.alpha_effect, self.element_settings_widget)
def __init__(self, app): Gtk.Expander.__init__(self) self.set_expanded(True) self.set_label(_("Effects")) Loggable.__init__(self) self.app = app self.clip = None self.effects_properties_manager = EffectsPropertiesManager(app) setup_custom_effect_widgets(self.effects_properties_manager) self.drag_lines_pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_size( os.path.join(get_pixmap_dir(), "grip-lines-solid.svg"), 15, 15) self.expander_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) self.effects_listbox = Gtk.ListBox() placeholder_label = Gtk.Label( _("To apply an effect to the clip, drag it from the Effect Library " "or use the button below.")) placeholder_label.set_line_wrap(True) placeholder_label.show() self.effects_listbox.set_placeholder(placeholder_label) # Add effect popover button self.effect_popover = EffectsPopover(app) self.add_effect_button = Gtk.MenuButton(_("Add Effect")) self.add_effect_button.set_popover(self.effect_popover) self.add_effect_button.props.halign = Gtk.Align.CENTER self.drag_dest_set(Gtk.DestDefaults.DROP, [EFFECT_TARGET_ENTRY], Gdk.DragAction.COPY) self.expander_box.pack_start(self.effects_listbox, False, False, 0) self.expander_box.pack_start(self.add_effect_button, False, False, PADDING) self.add(self.expander_box) # Connect all the widget signals 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.add_effect_button.connect("toggled", self._add_effect_button_cb) self.show_all()
def __init__(self, app): Gtk.ScrolledWindow.__init__(self) Loggable.__init__(self) self.app = app self.settings = app.settings self._project = None self.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC) viewport = Gtk.Viewport() viewport.show() self.add(viewport) vbox = Gtk.VBox() vbox.set_spacing(SPACING) vbox.show() viewport.add(vbox) self.infobar_box = Gtk.VBox() self.infobar_box.show() vbox.pack_start(self.infobar_box, False, True, 0) # Transformation boxed DISABLED # self.transformation_expander = TransformationProperties(instance, instance.action_log) # self.transformation_expander.set_vexpand(False) # vbox.pack_start(self.transformation_expander, False, True, 0) effects_properties_manager = EffectsPropertiesManager(app) self.effect_expander = EffectProperties(app, effects_properties_manager, self) self.effect_expander.set_vexpand(False) vbox.pack_start(self.effect_expander, True, True, 0)
def __init__(self, app): Gtk.Box.__init__(self) Loggable.__init__(self) self.app = app self.settings = app.settings self._project = None self.set_orientation(Gtk.Orientation.VERTICAL) self.infobar_box = Gtk.Box() self.infobar_box.set_orientation(Gtk.Orientation.VERTICAL) self.infobar_box.show() self.pack_start(self.infobar_box, False, False, 0) # Transformation boxed DISABLED # self.transformation_expander = TransformationProperties(instance, instance.action_log) # self.transformation_expander.set_vexpand(False) # vbox.pack_start(self.transformation_expander, False, False, 0) effects_properties_manager = EffectsPropertiesManager(app) self.effect_expander = EffectProperties(app, effects_properties_manager, self) self.effect_expander.set_vexpand(False) viewport = Gtk.ScrolledWindow() viewport.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC) viewport.set_shadow_type(Gtk.ShadowType.ETCHED_IN) viewport.set_visible(True) viewport.add(self.effect_expander) self.pack_start(viewport, True, True, 0)
def _register_alpha_widget(self, widgets): """Sets up an EffectsPropertiesManager instance to create custom effect UI.""" self.alpha_effect = GES.Effect.new("alpha") self.prop_name = "black-sensitivity" _, _, self.prop = self.alpha_effect.lookup_child(self.prop_name) self.effects_prop_manager = EffectsPropertiesManager(self.app) self.effects_prop_manager.connect("create-widget", self.create_alpha_widget_cb, widgets) self.element_settings_widget = GstElementSettingsWidget(self.alpha_effect, PROPS_TO_IGNORE) self.effects_prop_manager.emit("create-widget", self.element_settings_widget, self.alpha_effect) self.effects_prop_manager._connectAllWidgetCallbacks(self.element_settings_widget, self.alpha_effect) self.effects_prop_manager._postConfiguration(self.alpha_effect, self.element_settings_widget)
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)
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 __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)
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 EffectsPropertiesManagerTest(common.TestCase): """Tests for the EffectsPropertiesManager class.""" def create_alpha_widget_cb(self, unused_manager, unused_container, unused_effect, widgets): """Handles the request to create an effect widget.""" self.builder = Gtk.Builder() path = os.path.join(os.path.dirname(__file__), "plugins", "test_alpha.ui") self.builder.add_objects_from_file(path, widgets) self.element_settings_widget.mapBuilder(self.builder) return self.builder.get_object("GstAlpha::black-sensitivity") def _register_alpha_widget(self, widgets): """Sets up an EffectsPropertiesManager instance to create custom effect UI.""" self.alpha_effect = GES.Effect.new("alpha") self.prop_name = "black-sensitivity" _, _, self.prop = self.alpha_effect.lookup_child(self.prop_name) self.effects_prop_manager = EffectsPropertiesManager(self.app) self.effects_prop_manager.connect("create-widget", self.create_alpha_widget_cb, widgets) self.element_settings_widget = GstElementSettingsWidget(self.alpha_effect, PROPS_TO_IGNORE) self.effects_prop_manager.emit("create-widget", self.element_settings_widget, self.alpha_effect) self.effects_prop_manager._connectAllWidgetCallbacks(self.element_settings_widget, self.alpha_effect) self.effects_prop_manager._postConfiguration(self.alpha_effect, self.element_settings_widget) def test_wrapping(self): """Checks UI updating results in updating the effect.""" self.app = common.create_pitivi_mock() self._register_alpha_widget(("black_sens_adjustment", "GstAlpha::black-sensitivity")) # Check if the widget is wrapped correctly wrapped_spin_button = self.element_settings_widget.properties[self.prop] self.assertTrue(isinstance(wrapped_spin_button, DynamicWidget)) self.assertTrue(isinstance(wrapped_spin_button, NumericWidget)) # Check if the wrapper has the correct default value self.assertEqual(self.prop.default_value, wrapped_spin_button.getWidgetDefault()) # Check if the callbacks are functioning value = (1 + self.prop.default_value) % self.prop.maximum wrapped_spin_button.setWidgetValue(value) self.assertEqual(wrapped_spin_button.getWidgetValue(), value) _, prop_value = self.alpha_effect.get_child_property(self.prop_name) self.assertEqual(prop_value, value) def test_prop_keyframe(self): """Checks the keyframe button effect.""" uri = common.get_sample_uri("tears_of_steel.webm") asset = GES.UriClipAsset.request_sync(uri) ges_clip = asset.extract() # Add the clip to a timeline so it gets tracks. timeline = common.create_timeline_container() self.app = timeline.app ges_timeline = timeline.ges_timeline ges_timeline.append_layer() ges_layer, = ges_timeline.get_layers() ges_layer.add_clip(ges_clip) self._register_alpha_widget( ("black_sens_adjustment", "GstAlpha::black-sensitivity", "GstAlpha::black-sensitivity::keyframe")) ges_clip.add(self.alpha_effect) track_element = self.element_settings_widget._GstElementSettingsWidget__get_track_element_of_same_type( self.alpha_effect) prop_keyframe_button = \ list(self.element_settings_widget._GstElementSettingsWidget__widgets_by_keyframe_button.keys())[0] # Control the self.prop property on the timeline prop_keyframe_button.set_active(True) self.assertEqual(track_element.ui_element._TimelineElement__controlledProperty, self.prop) # Revert to controlling the default property prop_keyframe_button.set_active(False) self.assertNotEqual(track_element.ui_element._TimelineElement__controlledProperty, self.prop) def test_prop_reset(self): """Checks the reset button resets the property.""" self.app = common.create_pitivi_mock() self._register_alpha_widget( ("black_sens_adjustment", "GstAlpha::black-sensitivity", "GstAlpha::black-sensitivity::reset", "image1")) wrapped_spin_button = self.element_settings_widget.properties[self.prop] _, prop_value = self.alpha_effect.get_child_property(self.prop_name) self.assertEqual(self.prop.default_value, prop_value) self.assertEqual(self.prop.default_value, wrapped_spin_button.getWidgetValue()) # Set the property value to a different value than the default wrapped_spin_button.setWidgetValue((1 + self.prop.default_value) % self.prop.maximum) _, prop_value = self.alpha_effect.get_child_property(self.prop_name) self.assertEqual(prop_value, (1 + self.prop.default_value) % self.prop.maximum) # Reset the value of the property to default prop_reset_button = \ list(self.element_settings_widget._GstElementSettingsWidget__widgets_by_reset_button.keys())[0] prop_reset_button.clicked() _, prop_value = self.alpha_effect.get_child_property(self.prop_name) self.assertEqual(self.prop.default_value, prop_value) self.assertEqual(self.prop.default_value, wrapped_spin_button.getWidgetValue()) 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 EffectsPropertiesManagerTest(common.TestCase): """Tests for the EffectsPropertiesManager class.""" def create_alpha_widget_cb(self, unused_manager, unused_container, unused_effect, widgets): """Handles the request to create an effect widget.""" self.builder = Gtk.Builder() path = os.path.join(os.path.dirname(__file__), "plugins", "test_alpha.ui") self.builder.add_objects_from_file(path, widgets) self.element_settings_widget.mapBuilder(self.builder) return self.builder.get_object("GstAlpha::black-sensitivity") def _register_alpha_widget(self, widgets): """Sets up an EffectsPropertiesManager instance to create custom effect UI.""" self.alpha_effect = GES.Effect.new("alpha") self.prop_name = "black-sensitivity" _, _, self.prop = self.alpha_effect.lookup_child(self.prop_name) self.effects_prop_manager = EffectsPropertiesManager(self.app) self.effects_prop_manager.connect("create-widget", self.create_alpha_widget_cb, widgets) self.element_settings_widget = GstElementSettingsWidget( self.alpha_effect, PROPS_TO_IGNORE) self.effects_prop_manager.emit("create-widget", self.element_settings_widget, self.alpha_effect) self.effects_prop_manager._connectAllWidgetCallbacks( self.element_settings_widget, self.alpha_effect) self.effects_prop_manager._postConfiguration( self.alpha_effect, self.element_settings_widget) def test_wrapping(self): """Checks UI updating results in updating the effect.""" self.app = common.create_pitivi_mock() self._register_alpha_widget( ("black_sens_adjustment", "GstAlpha::black-sensitivity")) # Check if the widget is wrapped correctly wrapped_spin_button = self.element_settings_widget.properties[ self.prop] self.assertTrue(isinstance(wrapped_spin_button, DynamicWidget)) self.assertTrue(isinstance(wrapped_spin_button, NumericWidget)) # Check if the wrapper has the correct default value self.assertEqual(self.prop.default_value, wrapped_spin_button.getWidgetDefault()) # Check if the callbacks are functioning value = (1 + self.prop.default_value) % self.prop.maximum wrapped_spin_button.setWidgetValue(value) self.assertEqual(wrapped_spin_button.getWidgetValue(), value) _, prop_value = self.alpha_effect.get_child_property(self.prop_name) self.assertEqual(prop_value, value) def test_prop_keyframe(self): """Checks the keyframe button effect.""" uri = common.get_sample_uri("tears_of_steel.webm") asset = GES.UriClipAsset.request_sync(uri) ges_clip = asset.extract() # Add the clip to a timeline so it gets tracks. timeline = common.create_timeline_container() self.app = timeline.app ges_timeline = timeline.ges_timeline ges_timeline.append_layer() ges_layer, = ges_timeline.get_layers() ges_layer.add_clip(ges_clip) self._register_alpha_widget( ("black_sens_adjustment", "GstAlpha::black-sensitivity", "GstAlpha::black-sensitivity::keyframe")) ges_clip.add(self.alpha_effect) track_element = self.element_settings_widget._GstElementSettingsWidget__get_track_element_of_same_type( self.alpha_effect) prop_keyframe_button = \ list(self.element_settings_widget._GstElementSettingsWidget__widgets_by_keyframe_button.keys())[0] # Control the self.prop property on the timeline prop_keyframe_button.set_active(True) self.assertEqual( track_element.ui_element._TimelineElement__controlledProperty, self.prop) # Revert to controlling the default property prop_keyframe_button.set_active(False) self.assertNotEqual( track_element.ui_element._TimelineElement__controlledProperty, self.prop) def test_prop_reset(self): """Checks the reset button resets the property.""" self.app = common.create_pitivi_mock() self._register_alpha_widget( ("black_sens_adjustment", "GstAlpha::black-sensitivity", "GstAlpha::black-sensitivity::reset", "image1")) wrapped_spin_button = self.element_settings_widget.properties[ self.prop] _, prop_value = self.alpha_effect.get_child_property(self.prop_name) self.assertEqual(self.prop.default_value, prop_value) self.assertEqual(self.prop.default_value, wrapped_spin_button.getWidgetValue()) # Set the property value to a different value than the default wrapped_spin_button.setWidgetValue( (1 + self.prop.default_value) % self.prop.maximum) _, prop_value = self.alpha_effect.get_child_property(self.prop_name) self.assertEqual(prop_value, (1 + self.prop.default_value) % self.prop.maximum) # Reset the value of the property to default prop_reset_button = \ list(self.element_settings_widget._GstElementSettingsWidget__widgets_by_reset_button.keys())[0] prop_reset_button.clicked() _, prop_value = self.alpha_effect.get_child_property(self.prop_name) self.assertEqual(self.prop.default_value, prop_value) self.assertEqual(self.prop.default_value, wrapped_spin_button.getWidgetValue()) 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. clip (GES.Clip): The clip being configured. """ def __init__(self, app): Gtk.Expander.__init__(self) self.set_expanded(True) self.set_label(_("Effects")) Loggable.__init__(self) self.app = app self.clip = None self.effects_properties_manager = EffectsPropertiesManager(app) setup_custom_effect_widgets(self.effects_properties_manager) self.drag_lines_pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_size( os.path.join(get_pixmap_dir(), "grip-lines-solid.svg"), 15, 15) self.expander_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) self.effects_listbox = Gtk.ListBox() placeholder_label = Gtk.Label( _("To apply an effect to the clip, drag it from the Effect Library " "or use the button below.")) placeholder_label.set_line_wrap(True) placeholder_label.show() self.effects_listbox.set_placeholder(placeholder_label) # Add effect popover button self.effect_popover = EffectsPopover(app) self.add_effect_button = Gtk.MenuButton(_("Add Effect")) self.add_effect_button.set_popover(self.effect_popover) self.add_effect_button.props.halign = Gtk.Align.CENTER self.drag_dest_set(Gtk.DestDefaults.DROP, [EFFECT_TARGET_ENTRY], Gdk.DragAction.COPY) self.expander_box.pack_start(self.effects_listbox, False, False, 0) self.expander_box.pack_start(self.add_effect_button, False, False, PADDING) self.add(self.expander_box) # Connect all the widget signals 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.add_effect_button.connect("toggled", self._add_effect_button_cb) self.show_all() def _add_effect_button_cb(self, button): # MenuButton interacts directly with the popover, bypassing our subclassed method if button.props.active: self.effect_popover.search_entry.set_text("") def _create_effect_row(self, effect): effect_info = self.app.effects.get_info(effect.props.bin_description) vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) row_drag_icon = Gtk.Image.new_from_pixbuf(self.drag_lines_pixbuf) toggle = Gtk.CheckButton() toggle.props.active = effect.props.active effect_label = Gtk.Label(effect_info.human_name) effect_label.set_tooltip_text(effect_info.description) # Set up revealer + expander effect_config_ui = self.effects_properties_manager.get_effect_configuration_ui( effect) config_ui_revealer = Gtk.Revealer() config_ui_revealer.add(effect_config_ui) expander = Gtk.Expander() expander.set_label_widget(effect_label) expander.props.valign = Gtk.Align.CENTER expander.props.vexpand = True config_ui_revealer.props.halign = Gtk.Align.CENTER expander.connect("notify::expanded", self._toggle_expander_cb, config_ui_revealer) remove_effect_button = Gtk.Button.new_from_icon_name( "window-close", Gtk.IconSize.BUTTON) remove_effect_button.props.margin_right = PADDING row_widgets_box = Gtk.Box() row_widgets_box.pack_start(row_drag_icon, False, False, PADDING) row_widgets_box.pack_start(toggle, False, False, PADDING) row_widgets_box.pack_start(expander, True, True, PADDING) row_widgets_box.pack_end(remove_effect_button, False, False, 0) vbox.pack_start(row_widgets_box, False, False, 0) vbox.pack_start(config_ui_revealer, False, False, 0) event_box = Gtk.EventBox() event_box.add(vbox) row = Gtk.ListBoxRow(selectable=False, activatable=False) row.effect = effect row.toggle = toggle row.add(event_box) # Set up drag&drop event_box.drag_source_set(Gdk.ModifierType.BUTTON1_MASK, [EFFECT_TARGET_ENTRY], Gdk.DragAction.MOVE) event_box.connect("drag-begin", self._drag_begin_cb) event_box.connect("drag-data-get", self._drag_data_get_cb) row.drag_dest_set(Gtk.DestDefaults.ALL, [EFFECT_TARGET_ENTRY], Gdk.DragAction.MOVE | Gdk.DragAction.COPY) row.connect("drag-data-received", self._drag_data_received_cb) remove_effect_button.connect("clicked", self._remove_button_cb, row) toggle.connect("toggled", self._effect_active_toggle_cb, row) return row def _update_listbox(self): for row in self.effects_listbox.get_children(): self.effects_listbox.remove(row) for effect in self.clip.get_top_effects(): if effect.props.bin_description in HIDDEN_EFFECTS: continue effect_row = self._create_effect_row(effect) self.effects_listbox.add(effect_row) self.effects_listbox.show_all() def _toggle_expander_cb(self, expander, unused_prop, revealer): revealer.props.reveal_child = expander.props.expanded def _get_effect_row(self, effect): for row in self.effects_listbox.get_children(): if row.effect == effect: return row return None def _add_effect_row(self, effect): row = self._create_effect_row(effect) self.effects_listbox.add(row) self.effects_listbox.show_all() def _remove_effect_row(self, effect): row = self._get_effect_row(effect) self.effects_listbox.remove(row) def _move_effect_row(self, effect, new_index): row = self._get_effect_row(effect) self.effects_listbox.remove(row) self.effects_listbox.insert(row, new_index) def _remove_button_cb(self, button, row): effect = row.effect self._remove_effect(effect) def _remove_effect(self, effect): pipeline = self.app.project_manager.current_project.pipeline with self.app.action_log.started( "remove effect", finalizing_action=CommitTimelineFinalizingAction(pipeline), toplevel=True): effect.get_parent().remove(effect) def _effect_active_toggle_cb(self, toggle, row): effect = row.effect pipeline = self.app.project_manager.current_project.pipeline with self.app.action_log.started( "change active state", finalizing_action=CommitTimelineFinalizingAction(pipeline), toplevel=True): effect.props.active = toggle.props.active def set_clip(self, clip): if self.clip: self.clip.disconnect_by_func(self._track_element_added_cb) self.clip.disconnect_by_func(self._track_element_removed_cb) for track_element in self.clip.get_children(recursive=True): if isinstance(track_element, GES.BaseEffect): self._disconnect_from_track_element(track_element) self.clip = clip if self.clip: self.clip.connect("child-added", self._track_element_added_cb) self.clip.connect("child-removed", self._track_element_removed_cb) for track_element in self.clip.get_children(recursive=True): if isinstance(track_element, GES.BaseEffect): self._connect_to_track_element(track_element) self._update_listbox() self.show() else: self.hide() def _track_element_added_cb(self, unused_clip, track_element): if isinstance(track_element, GES.BaseEffect): self._connect_to_track_element(track_element) self._add_effect_row(track_element) 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, track_element, unused_param_spec): row = self._get_effect_row(track_element) row.toggle.props.active = track_element.props.active def _notify_priority_cb(self, track_element, unused_param_spec): index = self.clip.get_top_effect_index(track_element) row = self.effects_listbox.get_row_at_index(index) if not row: return if row.effect != track_element: self._move_effect_row(track_element, index) def _track_element_removed_cb(self, unused_clip, track_element): if isinstance(track_element, GES.BaseEffect): self._disconnect_from_track_element(track_element) self._remove_effect_row(track_element) def _drag_begin_cb(self, eventbox, context): """Draws the drag icon.""" row = eventbox.get_parent() alloc = row.get_allocation() surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, alloc.width, alloc.height) ctx = cairo.Context(surface) row.draw(ctx) ctx.paint_with_alpha(0.35) Gtk.drag_set_icon_surface(context, surface) def _drag_data_get_cb(self, eventbox, drag_context, selection_data, unused_info, unused_timestamp): row = eventbox.get_parent() effect_info = self.app.effects.get_info( row.effect.props.bin_description) effect_name = effect_info.human_name data = bytes(effect_name, "UTF-8") selection_data.set(drag_context.list_targets()[0], 0, data) def _drag_motion_cb(self, unused_widget, unused_drag_context, unused_x, 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") row = self.effects_listbox.get_row_at_y(y) if row: self.effects_listbox.drag_highlight_row(row) self.expander_box.drag_unhighlight() else: self.effects_listbox.drag_highlight() def _drag_leave_cb(self, unused_widget, 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.effects_listbox.drag_unhighlight_row() self.effects_listbox.drag_unhighlight() def _drag_data_received_cb(self, widget, drag_context, unused_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 if self.effects_listbox.get_row_at_y(y): # Drop happened inside the lisbox drop_index = widget.get_index() else: drop_index = len(self.effects_listbox.get_children()) - 1 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") self.debug("Effect dragged at position %s", drop_index) effect_info = self.app.effects.get_info(factory_name) pipeline = self.app.project_manager.current_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 listbox to change its position. source_eventbox = Gtk.drag_get_source_widget(drag_context) source_row = source_eventbox.get_parent() source_index = source_row.get_index() self._move_effect(self.clip, source_index, drop_index) drag_context.finish(True, False, timestamp) def _move_effect(self, clip, source_index, drop_index): # Handle edge cases if drop_index < 0: drop_index = 0 if drop_index > len(clip.get_top_effects()) - 1: drop_index = len(clip.get_top_effects()) - 1 if source_index == drop_index: # Noop. return effects = clip.get_top_effects() effect = effects[source_index] pipeline = self.app.project_manager.current_project.pipeline with self.app.action_log.started( "move effect", finalizing_action=CommitTimelineFinalizingAction(pipeline), toplevel=True): clip.set_top_effect_index(effect, drop_index)
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)
class TestCustomEffectUI(common.TestCase): """Tests for the custom effect UI create mechanism.""" def create_alpha_widget_cb(self, unused_manager, unused_container, unused_effect, widgets): """Handles the request to create an effect widget.""" self.builder = Gtk.Builder() path = os.path.join(os.path.dirname(__file__), "plugins", "test_alpha.ui") self.builder.add_objects_from_file(path, widgets) self.element_settings_widget.mapBuilder(self.builder) return self.builder.get_object("GstAlpha::black-sensitivity") def _register_alpha_widget(self, widgets): """Sets up an EffectsPropertiesManager instance to create custom effect UI.""" self.alpha_effect = GES.Effect.new("alpha") self.prop_name = "black-sensitivity" _, _, self.prop = self.alpha_effect.lookup_child(self.prop_name) self.effects_prop_manager = EffectsPropertiesManager(self.app) self.effects_prop_manager.connect("create-widget", self.create_alpha_widget_cb, widgets) self.element_settings_widget = GstElementSettingsWidget() self.element_settings_widget.setElement(self.alpha_effect, PROPS_TO_IGNORE) self.effects_prop_manager.emit("create-widget", self.element_settings_widget, self.alpha_effect) self.effects_prop_manager._connectAllWidgetCallbacks( self.element_settings_widget, self.alpha_effect) self.effects_prop_manager._postConfiguration( self.alpha_effect, self.element_settings_widget) def test_wrapping(self): """Checks UI updating results in updating the effect.""" self.app = common.create_pitivi_mock() self._register_alpha_widget( ("black_sens_adjustment", "GstAlpha::black-sensitivity")) # Check if the widget is wrapped correctly wrapped_spin_button = self.element_settings_widget.properties[ self.prop] self.assertTrue(isinstance(wrapped_spin_button, DynamicWidget)) self.assertTrue(isinstance(wrapped_spin_button, NumericWidget)) # Check if the wrapper has the correct default value self.assertEqual(self.prop.default_value, wrapped_spin_button.getWidgetDefault()) # Check if the callbacks are functioning value = (1 + self.prop.default_value) % self.prop.maximum wrapped_spin_button.setWidgetValue(value) self.assertEqual(wrapped_spin_button.getWidgetValue(), value) _, prop_value = self.alpha_effect.get_child_property(self.prop_name) self.assertEqual(prop_value, value) def test_prop_keyframe(self): """Checks the keyframe button effect.""" uri = common.get_sample_uri("tears_of_steel.webm") asset = GES.UriClipAsset.request_sync(uri) ges_clip = asset.extract() # Add the clip to a timeline so it gets tracks. timeline = common.create_timeline_container() self.app = timeline.app ges_timeline = timeline.ges_timeline ges_timeline.append_layer() ges_layer, = ges_timeline.get_layers() ges_layer.add_clip(ges_clip) self._register_alpha_widget( ("black_sens_adjustment", "GstAlpha::black-sensitivity", "GstAlpha::black-sensitivity::keyframe")) ges_clip.add(self.alpha_effect) track_element = self.element_settings_widget._GstElementSettingsWidget__get_track_element_of_same_type( self.alpha_effect) prop_keyframe_button = \ list(self.element_settings_widget._GstElementSettingsWidget__widgets_by_keyframe_button.keys())[0] # Control the self.prop property on the timeline prop_keyframe_button.set_active(True) self.assertEqual( track_element.ui_element._TimelineElement__controlledProperty, self.prop) # Revert to controlling the default property prop_keyframe_button.set_active(False) self.assertNotEqual( track_element.ui_element._TimelineElement__controlledProperty, self.prop) def test_prop_reset(self): """Checks the reset button resets the property.""" self.app = common.create_pitivi_mock() self._register_alpha_widget( ("black_sens_adjustment", "GstAlpha::black-sensitivity", "GstAlpha::black-sensitivity::reset", "image1")) wrapped_spin_button = self.element_settings_widget.properties[ self.prop] _, prop_value = self.alpha_effect.get_child_property(self.prop_name) self.assertEqual(self.prop.default_value, prop_value) self.assertEqual(self.prop.default_value, wrapped_spin_button.getWidgetValue()) # Set the property value to a different value than the default wrapped_spin_button.setWidgetValue( (1 + self.prop.default_value) % self.prop.maximum) _, prop_value = self.alpha_effect.get_child_property(self.prop_name) self.assertEqual(prop_value, (1 + self.prop.default_value) % self.prop.maximum) # Reset the value of the property to default prop_reset_button = \ list(self.element_settings_widget._GstElementSettingsWidget__widgets_by_reset_button.keys())[0] prop_reset_button.clicked() _, prop_value = self.alpha_effect.get_child_property(self.prop_name) self.assertEqual(self.prop.default_value, prop_value) self.assertEqual(self.prop.default_value, wrapped_spin_button.getWidgetValue())