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)
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)