diff --git a/data/pixmaps/effects/defaultthumbnail.svg b/data/pixmaps/effects/defaultthumbnail.svg
index b2bc02bde7f64c88500bce0d18efb09e3cc8585c..0d593758ed4671e387333e28f3739a010277ab81 100644
--- a/data/pixmaps/effects/defaultthumbnail.svg
+++ b/data/pixmaps/effects/defaultthumbnail.svg
@@ -1,5 +1,6 @@
+
diff --git a/data/pixmaps/grip-lines-solid.svg b/data/pixmaps/grip-lines-solid.svg
new file mode 100644
index 0000000000000000000000000000000000000000..7d8101c63cd468f4659b160b322910dfd1ece72f
--- /dev/null
+++ b/data/pixmaps/grip-lines-solid.svg
@@ -0,0 +1,5 @@
+
+
diff --git a/data/pixmaps/star-regular.svg b/data/pixmaps/star-regular.svg
new file mode 100644
index 0000000000000000000000000000000000000000..8ea949761bd879bee5085783a24c98e1505cdf25
--- /dev/null
+++ b/data/pixmaps/star-regular.svg
@@ -0,0 +1,5 @@
+
+
diff --git a/data/pixmaps/star-solid.svg b/data/pixmaps/star-solid.svg
new file mode 100644
index 0000000000000000000000000000000000000000..d45e927b54a60ef68b9ac67971bf3e9c315b22a6
--- /dev/null
+++ b/data/pixmaps/star-solid.svg
@@ -0,0 +1,5 @@
+
+
diff --git a/data/ui/effectslibrary.ui b/data/ui/effectslibrary.ui
index 99310fa376ec449688b625b2d2823faa8bf6862c..cad46a66485201c4ca4e43f511585ecf7ef2b04c 100644
--- a/data/ui/effectslibrary.ui
+++ b/data/ui/effectslibrary.ui
@@ -1,5 +1,5 @@
-
+
diff --git a/pitivi/clipproperties.py b/pitivi/clipproperties.py
index 3c65ce1aeb76e15b3816b1bdb511f747821dd978..186aaf93d158268e90e2b6c738c447b69bd9b128 100644
--- a/pitivi/clipproperties.py
+++ b/pitivi/clipproperties.py
@@ -19,18 +19,20 @@ import bisect
import os
from gettext import gettext as _
+import cairo
from gi.repository import Gdk
+from gi.repository import GdkPixbuf
from gi.repository import GES
-from gi.repository import Gio
from gi.repository import Gst
from gi.repository import GstController
from gi.repository import Gtk
-from gi.repository import Pango
from pitivi.clip_properties.alignment import AlignmentEditor
from pitivi.clip_properties.color import ColorProperties
from pitivi.clip_properties.title import TitleProperties
+from pitivi.configure import get_pixmap_dir
from pitivi.configure import get_ui_dir
+from pitivi.effects import EffectsPopover
from pitivi.effects import EffectsPropertiesManager
from pitivi.effects import HIDDEN_EFFECTS
from pitivi.undo.timeline import CommitTimelineFinalizingAction
@@ -41,7 +43,6 @@ from pitivi.utils.pipeline import PipelineError
from pitivi.utils.timeline import SELECT
from pitivi.utils.ui import disable_scroll
from pitivi.utils.ui import EFFECT_TARGET_ENTRY
-from pitivi.utils.ui import fix_infobar
from pitivi.utils.ui import PADDING
from pitivi.utils.ui import SPACING
@@ -99,7 +100,7 @@ class ClipProperties(Gtk.ScrolledWindow, Loggable):
self.color_expander.set_vexpand(False)
vbox.pack_start(self.color_expander, False, False, 0)
- self.effect_expander = EffectProperties(app, self)
+ self.effect_expander = EffectProperties(app)
self.effect_expander.set_vexpand(False)
vbox.pack_start(self.effect_expander, False, False, 0)
@@ -108,6 +109,7 @@ class ClipProperties(Gtk.ScrolledWindow, Loggable):
self.title_expander.set_source(None)
self.color_expander.set_source(None)
+ self.effect_expander.set_clip(None)
self._project = None
self._selection = None
@@ -189,7 +191,9 @@ class ClipProperties(Gtk.ScrolledWindow, Loggable):
title_source = None
color_clip_source = None
if single_clip_selected:
- for child in list(selected_clips)[0].get_children(False):
+ ges_clip = list(selected_clips)[0]
+ self.effect_expander.set_clip(ges_clip)
+ for child in ges_clip.get_children(False):
if isinstance(child, GES.TitleSource):
title_source = child
break
@@ -211,176 +215,179 @@ class EffectProperties(Gtk.Expander, Loggable):
clip (GES.Clip): The clip being configured.
"""
- def __init__(self, app, clip_properties):
+ def __init__(self, app):
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://gitlab.gnome.org/GNOME/pygobject/issues/90
- 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._effect_active_toggle_cb)
- 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_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)
- # 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)
-
- # 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()
+ self.expander_box.pack_start(self.effects_listbox, False, False, 0)
+ self.expander_box.pack_start(self.add_effect_button, False, False, PADDING)
- 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._remove_effect_cb)
- effects_actions_group.add_action(self.remove_effect_action)
- self.app.set_accels_for_action(
- "clipproperties-effects.remove-effect", ["Delete"])
- self.remove_effect_action.set_enabled(False)
- remove_effect_button.set_action_name(
- "clipproperties-effects.remove-effect")
+ self.add(self.expander_box)
# Connect all the widget signals
- self.treeview_selection.connect(
- "changed", self._treeview_selection_changed_cb)
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._tree_view_query_tooltip_cb)
- self.app.project_manager.connect_after(
- "new-project-loaded", self._new_project_loaded_cb)
- self.connect('notify::expanded', self._expanded_cb)
- def _new_project_loaded_cb(self, unused_app, project):
- if self._selection is not None:
- self._selection.disconnect_by_func(self._selection_changed_cb)
- self._selection = None
- self._project = project
- if project:
- self._selection = project.ges_timeline.ui.selection
- self._selection.connect(
- 'selection-changed', self._selection_changed_cb)
- self.__update_all()
+ self.add_effect_button.connect("toggled", self._add_effect_button_cb)
- def _selection_changed_cb(self, selection):
+ 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)
@@ -388,24 +395,23 @@ class EffectProperties(Gtk.Expander, Loggable):
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
+ 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_all()
+
+ 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.__update_all()
- for path, row in enumerate(self.storemodel):
- if row[COL_TRACK_EFFECT] == track_element:
- self.treeview_selection.select_path(path)
- break
+ self._add_effect_row(track_element)
def _connect_to_track_element(self, track_element):
track_element.connect("notify::active", self._notify_active_cb)
@@ -415,73 +421,84 @@ class EffectProperties(Gtk.Expander, Loggable):
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._update_treeview()
+ 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
- def _notify_priority_cb(self, unused_track_element, unused_param_spec):
- self._update_treeview()
+ 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.__update_all()
+ self._remove_effect_row(track_element)
- def _remove_effect_cb(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._remove_effect(effect)
- if next_selection_index >= 0:
- self.treeview_selection.select_path(next_selection_index)
+ def _drag_begin_cb(self, eventbox, context):
+ """Draws the drag icon."""
+ row = eventbox.get_parent()
+ alloc = row.get_allocation()
- def _remove_effect(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.clean_cache(effect)
- effect.get_parent().remove(effect)
+ 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
- def _drag_motion_cb(self, unused_widget, unused_drag_context, unused_x, unused_y, unused_timestamp):
+ 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")
- 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()
+ 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, unused_drag_context, unused_timestamp):
+ 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.no_effect_infobar.drag_unhighlight()
- self._vbox.drag_unhighlight()
- def _drag_data_received_cb(self, widget, drag_context, x, y, selection_data, unused_info, timestamp):
+ 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
- dest_row = self.treeview.get_dest_row_at_pos(x, y)
+ 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")
- 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.get_info(factory_name)
- pipeline = self._project.pipeline
+ pipeline = self.app.project_manager.current_project.pipeline
with self.app.action_log.started("add effect",
finalizing_action=CommitTimelineFinalizingAction(
pipeline),
@@ -489,172 +506,37 @@ class EffectProperties(Gtk.Expander, Loggable):
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
+ 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()
- # Target
- if dest_row:
- drop_path, drop_pos = dest_row
- drop_index = drop_path.get_indices()[0]
- drop_index = self.calculate_effect_priority(
- source_index, drop_index, drop_pos)
- else:
- # This should happen when dragging after the last row.
- drop_index = len(model) - 1
+ self._move_effect(self.clip, source_index, drop_index)
- return source_index, drop_index
+ drag_context.finish(True, False, timestamp)
- def __move_effect(self, clip, source_index, drop_index):
+ 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
- # The paths are different.
+
effects = clip.get_top_effects()
effect = effects[source_index]
- pipeline = self._project.ges_timeline.get_parent()
+ 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)
- new_path = Gtk.TreePath.new()
- new_path.append_index(drop_index)
- self.__update_all(path=new_path)
-
- @staticmethod
- def calculate_effect_priority(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 _effect_active_toggle_cb(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 _expanded_cb(self, unused_expander, unused_params):
- self.__update_all()
-
- def _tree_view_query_tooltip_cb(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 __update_all(self, path=None):
- if self.clip:
- self.show()
- self.no_effect_infobar.hide()
- self._update_treeview()
- if path:
- self.treeview_selection.select_path(path)
- else:
- self.hide()
- self.__remove_configuration_widget()
- self.storemodel.clear()
-
- def _update_treeview(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.get_info(
- 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 _treeview_selection_changed_cb(self, unused_treeview):
- selection_is_emtpy = self.treeview_selection.count_selected_rows() == 0
- self.remove_effect_action.set_enabled(not selection_is_emtpy)
-
- self._update_effect_config_ui()
-
- def _update_effect_config_ui(self):
- model, tree_iter = self.treeview_selection.get_selected()
- if tree_iter:
- effect = model.get_value(tree_iter, COL_TRACK_EFFECT)
- self._show_effect_configuration_widget(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 _show_effect_configuration_widget(self, effect):
- self.__remove_configuration_widget()
- self._effect_config_ui = self.effects_properties_manager.get_effect_configuration_ui(
- 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 TransformationProperties(Gtk.Expander, Loggable):
"""Widget for configuring the placement and size of the clip."""
diff --git a/pitivi/effects.py b/pitivi/effects.py
index 9affbb4d43ef97dfbaebdb46ce3da398cbd05c68..6e707585dd8f55df6e4eac0401eb6ae0434d1afd 100644
--- a/pitivi/effects.py
+++ b/pitivi/effects.py
@@ -33,6 +33,7 @@ import sys
import threading
from gettext import gettext as _
+import cairo
from gi.repository import Gdk
from gi.repository import GdkPixbuf
from gi.repository import GES
@@ -40,88 +41,96 @@ from gi.repository import GLib
from gi.repository import GObject
from gi.repository import Gst
from gi.repository import Gtk
-from gi.repository import Pango
from pitivi.configure import get_pixmap_dir
from pitivi.configure import get_ui_dir
from pitivi.settings import GlobalSettings
from pitivi.utils.loggable import Loggable
from pitivi.utils.ui import EFFECT_TARGET_ENTRY
+from pitivi.utils.ui import PADDING
from pitivi.utils.ui import SPACING
from pitivi.utils.widgets import FractionWidget
from pitivi.utils.widgets import GstElementSettingsWidget
(VIDEO_EFFECT, AUDIO_EFFECT) = list(range(1, 3))
-AUDIO_EFFECTS_CATEGORIES = ()
-
ALLOWED_ONLY_ONCE_EFFECTS = ['videoflip']
-VIDEO_EFFECTS_CATEGORIES = (
+EFFECTS_CATEGORIES = (
(_("Colors"), (
# Mostly "serious" stuff that relates to correction/adjustments
# Fancier stuff goes into the "fancy" category
- "cogcolorspace", "videobalance", "chromahold", "gamma",
- "coloreffects", "exclusion", "burn", "dodge", "videomedian",
- "frei0r-filter-color-distance", "frei0r-filter-threshold0r",
- "frei0r-filter-contrast0r", "frei0r-filter-saturat0r",
- "frei0r-filter-white-balance", "frei0r-filter-brightness",
- "frei0r-filter-gamma", "frei0r-filter-invert0r",
- "frei0r-filter-hueshift0r", "frei0r-filter-equaliz0r",
- "frei0r-filter-bw0r", "frei0r-filter-glow",
- "frei0r-filter-twolay0r", "frei0r-filter-3-point-color-balance",
- "frei0r-filter-coloradj-rgb", "frei0r-filter-curves",
- "frei0r-filter-levels", "frei0r-filter-primaries",
- "frei0r-filter-sop-sat", "frei0r-filter-threelay0r",
- "frei0r-filter-tint0r",
+ 'burn', 'chromahold', 'cogcolorspace', 'coloreffects', 'dodge',
+ 'exclusion', 'frei0r-filter-3-point-color-balance',
+ 'frei0r-filter-brightness', 'frei0r-filter-bw0r',
+ 'frei0r-filter-color-distance', 'frei0r-filter-coloradj-rgb',
+ 'frei0r-filter-contrast0r', 'frei0r-filter-curves',
+ 'frei0r-filter-equaliz0r', 'frei0r-filter-gamma', 'frei0r-filter-glow',
+ 'frei0r-filter-hueshift0r', 'frei0r-filter-invert0r',
+ 'frei0r-filter-levels', 'frei0r-filter-primaries',
+ 'frei0r-filter-saturat0r', 'frei0r-filter-sop-sat',
+ 'frei0r-filter-threelay0r', 'frei0r-filter-threshold0r',
+ 'frei0r-filter-tint0r', 'frei0r-filter-twolay0r',
+ 'frei0r-filter-white-balance', 'gamma', 'videobalance', 'videomedian',
)),
(_("Compositing"), (
- "alpha", "alphacolor", "gdkpixbufoverlay",
- "frei0r-filter-transparency", "frei0r-filter-mask0mate",
- "frei0r-filter-alpha0ps", "frei0r-filter-alphagrad",
- "frei0r-filter-alphaspot", "frei0r-filter-bluescreen0r",
- "frei0r-filter-select0r",
+ 'alpha', 'alphacolor', 'frei0r-filter-alpha0ps',
+ 'frei0r-filter-alphagrad', 'frei0r-filter-alphaspot',
+ 'frei0r-filter-bluescreen0r', 'frei0r-filter-mask0mate',
+ 'frei0r-filter-select0r', 'frei0r-filter-transparency',
+ 'gdkpixbufoverlay',
)),
(_("Noise & blur"), (
- "gaussianblur", "diffuse", "dilate", "marble", "smooth",
- "frei0r-filter-hqdn3d", "frei0r-filter-squareblur",
- "frei0r-filter-sharpness", "frei0r-filter-edgeglow",
- "frei0r-filter-facebl0r",
+ 'diffuse', 'dilate', 'frei0r-filter-edgeglow', 'frei0r-filter-facebl0r',
+ 'frei0r-filter-hqdn3d', 'frei0r-filter-sharpness',
+ 'frei0r-filter-squareblur', 'gaussianblur', 'marble', 'smooth',
)),
(_("Analysis"), (
- "videoanalyse", "videodetect", "videomark", "revtv",
- "navigationtest", "frei0r-filter-rgb-parade",
- "frei0r-filter-r", "frei0r-filter-g", "frei0r-filter-b",
- "frei0r-filter-vectorscope", "frei0r-filter-luminance",
- "frei0r-filter-opencvfacedetect", "frei0r-filter-pr0be",
- "frei0r-filter-pr0file",
+ 'frei0r-filter-b', 'frei0r-filter-g', 'frei0r-filter-luminance',
+ 'frei0r-filter-opencvfacedetect', 'frei0r-filter-pr0be',
+ 'frei0r-filter-pr0file', 'frei0r-filter-r', 'frei0r-filter-rgb-parade',
+ 'frei0r-filter-vectorscope', 'navigationtest', 'revtv', 'videoanalyse',
+ 'videodetect', 'videomark',
)),
(_("Geometry"), (
- "cogscale", "aspectratiocrop", "cogdownsample", "videoscale",
- "videocrop", "videoflip", "videobox", "gdkpixbufscale",
- "kaleidoscope", "mirror", "pinch", "sphere", "square", "fisheye",
- "stretch", "twirl", "waterriple", "rotate", "bulge", "circle",
- "frei0r-filter-letterb0xed", "frei0r-filter-k-means-clustering",
- "frei0r-filter-lens-correction", "frei0r-filter-defish0r",
- "frei0r-filter-perspective", "frei0r-filter-c0rners",
- "frei0r-filter-scale0tilt", "frei0r-filter-pixeliz0r",
- "frei0r-filter-flippo", "frei0r-filter-3dflippo",
+ 'aspectratiocrop', 'bulge', 'circle', 'cogdownsample', 'cogscale',
+ 'fisheye', 'frei0r-filter-3dflippo', 'frei0r-filter-c0rners',
+ 'frei0r-filter-defish0r', 'frei0r-filter-flippo',
+ 'frei0r-filter-k-means-clustering', 'frei0r-filter-lens-correction',
+ 'frei0r-filter-letterb0xed', 'frei0r-filter-perspective',
+ 'frei0r-filter-pixeliz0r', 'frei0r-filter-scale0tilt', 'gdkpixbufscale',
+ 'kaleidoscope', 'mirror', 'pinch', 'rotate', 'sphere', 'square',
+ 'stretch', 'twirl', 'videobox', 'videocrop', 'videoflip', 'videoscale',
+ 'waterriple',
)),
(_("Fancy"), (
- "rippletv", "streaktv", "radioactv", "optv", "solarize",
- "quarktv", "vertigotv", "shagadelictv", "warptv", "dicetv",
- "agingtv", "edgetv", "bulge", "circle", "fisheye", "tunnel",
- "kaleidoscope", "mirror", "pinch", "sphere", "square",
- "stretch", "twirl", "waterripple", "glfiltersobel", "chromium",
- "frei0r-filter-sobel", "frei0r-filter-cartoon",
- "frei0r-filter-water", "frei0r-filter-nosync0r",
- "frei0r-filter-k-means-clustering", "frei0r-filter-delay0r",
- "frei0r-filter-distort0r", "frei0r-filter-light-graffiti",
- "frei0r-filter-tehroxx0r", "frei0r-filter-vertigo",
+ 'agingtv', 'bulge', 'chromium', 'circle', 'dicetv', 'edgetv', 'fisheye',
+ 'frei0r-filter-cartoon', 'frei0r-filter-delay0r',
+ 'frei0r-filter-distort0r', 'frei0r-filter-k-means-clustering',
+ 'frei0r-filter-light-graffiti', 'frei0r-filter-nosync0r',
+ 'frei0r-filter-sobel', 'frei0r-filter-tehroxx0r',
+ 'frei0r-filter-vertigo', 'frei0r-filter-water', 'glfiltersobel',
+ 'kaleidoscope', 'mirror', 'optv', 'pinch', 'quarktv', 'radioactv',
+ 'rippletv', 'shagadelictv', 'solarize', 'sphere', 'square', 'streaktv',
+ 'stretch', 'tunnel', 'twirl', 'vertigotv', 'warptv', 'waterripple',
)),
(_("Time"), (
- "videorate", "frei0r-filter-delay0r", "frei0r-filter-baltan",
- "frei0r-filter-nervous",
+ 'frei0r-filter-baltan', 'frei0r-filter-delay0r',
+ 'frei0r-filter-nervous', 'videorate',
+ )),
+ (_("Audio"), (
+ "pitch", "freeverb", "removesilence", "festival", "speed",
+ "audiorate", "volume", "equalizer-nbands", "equalizer-3bands",
+ "equalizer-10bands", "rglimiter", "rgvolume", "audiopanorama",
+ "audioinvert", "audiokaraoke", "audioamplify", "audiodynamic",
+ "audiocheblimit", "audiochebband", "audioiirfilter", "audiowsinclimit",
+ "audiowsincband", "audiofirfilter", "audioecho", "scaletempo", "stereo",
+ 'audioamplify', 'audiochebband', 'audiocheblimit', 'audiodynamic',
+ 'audioecho', 'audiofirfilter', 'audioiirfilter', 'audioinvert',
+ 'audiokaraoke', 'audiopanorama', 'audiorate', 'audiowsincband',
+ 'audiowsinclimit', 'equalizer-10bands', 'equalizer-3bands',
+ 'equalizer-nbands', 'festival', 'freeverb', 'pitch', 'removesilence',
+ 'rglimiter', 'rgvolume', 'scaletempo', 'speed', 'stereo', 'volume',
)),
)
@@ -146,15 +155,13 @@ HIDDEN_EFFECTS = [
"gdkpixbufoverlay"]
GlobalSettings.add_config_section('effect-library')
+GlobalSettings.add_config_option('favourite_effects',
+ section='effect-library',
+ key='favourite-effects',
+ default=[])
-(COL_NAME_TEXT,
- COL_DESC_TEXT,
- COL_EFFECT_TYPE,
- COL_EFFECT_CATEGORIES,
- COL_ELEMENT_NAME,
- COL_ICON) = list(range(6))
-
-ICON_WIDTH = 48 + 2 * 6 # 48 pixels, plus a margin on each side
+ICON_WIDTH = 80
+ICON_HEIGHT = 45
class EffectInfo:
@@ -182,8 +189,9 @@ class EffectInfo:
os.path.join(pixdir, self.effect_name + ".png"),
ICON_WIDTH, ICON_WIDTH)
except GLib.Error:
- icon = GdkPixbuf.Pixbuf.new_from_file(
- os.path.join(pixdir, "defaultthumbnail.svg"))
+ icon = GdkPixbuf.Pixbuf.new_from_file_at_size(
+ os.path.join(pixdir, "defaultthumbnail.svg"), ICON_WIDTH, ICON_HEIGHT)
+
return icon
@property
@@ -323,32 +331,22 @@ class EffectsManager(Loggable):
List[str]: The categories which contain the effect.
"""
categories = []
- for category_name, effects in AUDIO_EFFECTS_CATEGORIES:
- if effect_name in effects:
- categories.append(category_name)
- for category_name, effects in VIDEO_EFFECTS_CATEGORIES:
+ for category_name, effects in EFFECTS_CATEGORIES:
if effect_name in effects:
categories.append(category_name)
if not categories:
categories.append(_("Uncategorized"))
- categories.insert(0, _("All effects"))
return categories
@property
- def video_categories(self):
- """Gets all video effect categories names."""
- return EffectsManager._get_categories_names(VIDEO_EFFECTS_CATEGORIES)
-
- @property
- def audio_categories(self):
- """Gets all audio effect categories names."""
- return EffectsManager._get_categories_names(AUDIO_EFFECTS_CATEGORIES)
+ def categories(self):
+ """Gets the name of all effect categories."""
+ return EffectsManager._get_categories_names(EFFECTS_CATEGORIES)
@staticmethod
def _get_categories_names(categories):
ret = [category_name for category_name, unused_effects in categories]
ret.sort()
- ret.insert(0, _("All effects"))
if categories:
# Add Uncategorized only if there are other categories defined.
ret.append(_("Uncategorized"))
@@ -367,8 +365,13 @@ class EffectListWidget(Gtk.Box, Loggable):
self.app = instance
- self._dragged_items = None
- self._effect_type = VIDEO_EFFECT
+ self._drag_icon = GdkPixbuf.Pixbuf.new_from_file_at_size(
+ os.path.join(get_pixmap_dir(), "effects", "defaultthumbnail.svg"),
+ ICON_WIDTH, ICON_HEIGHT)
+ self._star_icon_regular = GdkPixbuf.Pixbuf.new_from_file_at_size(
+ os.path.join(get_pixmap_dir(), "star-regular.svg"), 15, 15)
+ self._star_icon_solid = GdkPixbuf.Pixbuf.new_from_file_at_size(
+ os.path.join(get_pixmap_dir(), "star-solid.svg"), 15, 15)
self.set_orientation(Gtk.Orientation.VERTICAL)
builder = Gtk.Builder()
@@ -376,64 +379,29 @@ class EffectListWidget(Gtk.Box, Loggable):
builder.connect_signals(self)
toolbar = builder.get_object("effectslibrary_toolbar")
toolbar.get_style_context().add_class(Gtk.STYLE_CLASS_INLINE_TOOLBAR)
- self.video_togglebutton = builder.get_object("video_togglebutton")
- self.audio_togglebutton = builder.get_object("audio_togglebutton")
- self.categories_widget = builder.get_object("categories")
self.search_entry = builder.get_object("search_entry")
+ self.fav_view_toggle = builder.get_object("favourites_toggle")
+ self.fav_view_toggle.set_image(Gtk.Image.new_from_pixbuf(self._star_icon_solid))
+
+ self.main_view = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
- # Store
- self.storemodel = Gtk.ListStore(
- str, str, int, object, str, GdkPixbuf.Pixbuf)
- self.storemodel.set_sort_column_id(
- COL_NAME_TEXT, Gtk.SortType.ASCENDING)
-
- # Create the filter for searching the storemodel.
- self.model_filter = self.storemodel.filter_new()
- self.model_filter.set_visible_func(self._set_row_visible_func, data=None)
-
- self.view = Gtk.TreeView(model=self.model_filter)
- self.view.props.headers_visible = False
- self.view.get_selection().set_mode(Gtk.SelectionMode.SINGLE)
-
- icon_col = Gtk.TreeViewColumn()
- icon_col.set_spacing(SPACING)
- icon_col.set_sizing(Gtk.TreeViewColumnSizing.FIXED)
- icon_col.props.fixed_width = ICON_WIDTH
- icon_cell = Gtk.CellRendererPixbuf()
- icon_cell.props.xpad = 6
- icon_col.pack_start(icon_cell, True)
- icon_col.add_attribute(icon_cell, "pixbuf", COL_ICON)
-
- text_col = Gtk.TreeViewColumn()
- text_col.set_expand(True)
- text_col.set_spacing(SPACING)
- text_col.set_sizing(Gtk.TreeViewColumnSizing.AUTOSIZE)
- text_cell = Gtk.CellRendererText()
- text_cell.props.yalign = 0.0
- text_cell.props.xpad = 6
- text_cell.set_property("ellipsize", Pango.EllipsizeMode.END)
- text_col.pack_start(text_cell, True)
- text_col.set_cell_data_func(
- text_cell, self.view_description_cell_data_func, None)
-
- self.view.append_column(icon_col)
- self.view.append_column(text_col)
-
- self.view.connect("query-tooltip", self._tree_view_query_tooltip_cb)
- self.view.props.has_tooltip = True
-
- # Make the treeview a drag source which provides effects.
- self.view.enable_model_drag_source(
- Gdk.ModifierType.BUTTON1_MASK, [EFFECT_TARGET_ENTRY], Gdk.DragAction.COPY)
-
- self.view.connect("button-press-event", self._button_press_event_cb)
- self.view.connect("select-cursor-row", self._enter_press_event_cb)
- self.view.connect("drag-data-get", self._dnd_drag_data_get_cb)
+ self.category_view = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
+
+ # Used for showing search results and favourites
+ self.search_view = Gtk.ListBox(activate_on_single_click=False)
+ self.search_view.connect("row-activated", self.effects_listbox_row_activated_cb)
+
+ placeholder_text = Gtk.Label(_("No effects"))
+ placeholder_text.props.visible = True
+ self.search_view.set_placeholder(placeholder_text)
+
+ self.main_view.pack_start(self.category_view, True, True, 0)
+ self.main_view.pack_start(self.search_view, True, True, 0)
scrollwin = Gtk.ScrolledWindow()
scrollwin.props.hscrollbar_policy = Gtk.PolicyType.NEVER
scrollwin.props.vscrollbar_policy = Gtk.PolicyType.AUTOMATIC
- scrollwin.add(self.view)
+ scrollwin.add(self.main_view)
self.pack_start(toolbar, False, False, 0)
self.pack_start(scrollwin, True, True, 0)
@@ -441,108 +409,136 @@ class EffectListWidget(Gtk.Box, Loggable):
# Delay the loading of the available effects so the application
# starts faster.
GLib.idle_add(self._load_available_effects_cb)
- self.populate_categories_widget()
- # Individually show the tab's widgets.
- # If you use self.show_all(), the tab will steal focus on startup.
scrollwin.show_all()
toolbar.show_all()
+ self.search_view.hide()
- def _tree_view_query_tooltip_cb(self, view, x, y, keyboard_mode, tooltip):
- is_row, x, y, model, path, tree_iter = view.get_tooltip_context(
- x, y, keyboard_mode)
- if not is_row:
- return False
+ def _load_available_effects_cb(self):
+ self._set_up_category_view()
+ self.add_effects_to_listbox(self.search_view)
- view.set_tooltip_row(tooltip, path)
- tooltip.set_markup(self.format_description(model, tree_iter))
- return True
+ def _set_up_category_view(self):
+ """Adds expanders and effects to the category view."""
+ # Add category expanders
+ for category in self.app.effects.categories:
+ widget = self._create_category_widget(category)
+ self.category_view.add(widget)
- def view_description_cell_data_func(self, unused_column, cell, model, iter_, unused_data):
- cell.props.markup = self.format_description(model, iter_)
+ # Add effects to category expanders
+ for expander in self.category_view.get_children():
+ listbox = expander.get_child()
+ category_name = expander.get_label()
- def format_description(self, model, iter_):
- name, desc = model.get(iter_, COL_NAME_TEXT, COL_DESC_TEXT)
- escape = GLib.markup_escape_text
- return "%s\n%s" % (escape(name), escape(desc))
+ self.add_effects_to_listbox(listbox, category_name)
- def _load_available_effects_cb(self):
- self._add_factories(self.app.effects.video_effects, VIDEO_EFFECT)
- self._add_factories(self.app.effects.audio_effects, AUDIO_EFFECT)
- return False
+ self.category_view.show_all()
+
+ def add_effects_to_listbox(self, listbox, category=None, only_text=False):
+ """Adds effect rows to the given listbox."""
+ effects = self.app.effects.video_effects + self.app.effects.audio_effects
+ for effect in effects:
+ name = effect.get_name()
- def _add_factories(self, elements, effect_type):
- for element in elements:
- name = element.get_name()
if name in HIDDEN_EFFECTS:
continue
+
effect_info = self.app.effects.get_info(name)
- self.storemodel.append([effect_info.human_name,
- effect_info.description,
- effect_type,
- effect_info.categories,
- name,
- effect_info.icon])
-
- def populate_categories_widget(self):
- self.categories_widget.get_model().clear()
- icon_column = self.view.get_column(0)
-
- if self._effect_type is VIDEO_EFFECT:
- for category in self.app.effects.video_categories:
- self.categories_widget.append_text(category)
- icon_column.props.visible = True
- else:
- for category in self.app.effects.audio_categories:
- self.categories_widget.append_text(category)
- icon_column.props.visible = False
- self.categories_widget.set_active(0)
+ if not category or category in effect_info.categories:
+ widget = self._create_effect_widget(name, only_text)
+ listbox.add(widget)
- def _dnd_drag_data_get_cb(self, unused_view, drag_context, selection_data, unused_info, unused_timestamp):
- data = bytes(self.get_selected_effect(), "UTF-8")
+ def _create_category_widget(self, category):
+ """Creates an expander for the given category."""
+ expander = Gtk.Expander(label=category, margin=SPACING)
+
+ listbox = Gtk.ListBox(activate_on_single_click=False)
+ listbox.connect("row-activated", self.effects_listbox_row_activated_cb)
+
+ expander.add(listbox)
+
+ return expander
+
+ def _create_effect_widget(self, effect_name, only_text):
+ """Creates list box row for the given effect."""
+ effect_info = self.app.effects.get_info(effect_name)
+
+ effect_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, margin=SPACING / 2)
+ effect_box.effect_name = effect_name
+ effect_box.set_tooltip_text(effect_info.description)
+ label = Gtk.Label(effect_info.human_name, xalign=0)
+
+ if not only_text:
+ # Show effect thumbnail
+ icon = Gtk.Image.new_from_pixbuf(effect_info.icon)
+ effect_box.pack_start(icon, False, True, SPACING / 2)
+
+ # Set up favourite button
+ fav_button = Gtk.Button()
+ fav_button.props.relief = Gtk.ReliefStyle.NONE
+ fav_button.props.halign = Gtk.Align.CENTER
+ fav_button.props.valign = Gtk.Align.CENTER
+ fav_button.set_tooltip_text(_("Add to Favourites"))
+
+ starred = effect_name in self.app.settings.favourite_effects
+ self._set_fav_button_state(fav_button, starred)
+ fav_button.connect("clicked", self._fav_button_clicked_cb, effect_box.effect_name)
+ effect_box.pack_end(fav_button, False, True, SPACING / 2)
+
+ effect_box.pack_start(label, True, True, 0)
+
+ # Set up drag behavoir
+ eventbox = Gtk.EventBox(visible_window=False)
+ eventbox.drag_source_set(Gdk.ModifierType.BUTTON1_MASK, [EFFECT_TARGET_ENTRY], Gdk.DragAction.COPY)
+ eventbox.connect("drag-data-get", self._drag_data_get_cb)
+ eventbox.connect("drag-begin", self._drag_begin_cb)
+ eventbox.add(effect_box)
+
+ row = Gtk.ListBoxRow(selectable=False)
+ row.add(eventbox)
+ row.show_all()
+
+ return row
+
+ def _drag_data_get_cb(self, eventbox, drag_context, selection_data, unused_info, unused_timestamp):
+ effect_box = eventbox.get_child()
+ data = bytes(effect_box.effect_name, "UTF-8")
selection_data.set(drag_context.list_targets()[0], 0, data)
- def _row_under_mouse_selected(self, view, event):
- result = view.get_path_at_pos(int(event.x), int(event.y))
- if result:
- path = result[0]
- selection = view.get_selection()
- return selection.path_is_selected(path) and\
- selection.count_selected_rows() > 0
- return False
-
- def _enter_press_event_cb(self, unused_view, unused_event=None):
- self._add_selected_effect()
-
- def _button_press_event_cb(self, view, event):
- chain_up = True
-
- if event.button == 3:
- chain_up = False
- elif event.type == getattr(Gdk.EventType, '2BUTTON_PRESS'):
- self._add_selected_effect()
- else:
- chain_up = not self._row_under_mouse_selected(view, event)
+ def _drag_begin_cb(self, eventbox, context):
+ # Draw drag-icon
+ icon = self._drag_icon
+ icon_height = icon.get_height()
+ icon_width = icon.get_width()
- if chain_up:
- self._dragged_items = None
- else:
- self._dragged_items = self.get_selected_effect()
+ surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, icon_width, icon_height)
+ ctx = cairo.Context(surface)
+ # Center the icon around the cursor.
+ ctx.translate(icon_width / 2, icon_height / 2)
+ surface.set_device_offset(-icon_width / 2, -icon_height / 2)
+
+ Gdk.cairo_set_source_pixbuf(ctx, icon, 0, 0)
+ ctx.paint_with_alpha(0.35)
- Gtk.TreeView.do_button_press_event(view, event)
- return True
+ Gtk.drag_set_icon_surface(context, surface)
- def _add_selected_effect(self):
+ def effects_listbox_row_activated_cb(self, listbox, row):
+ """Handles the activation of a row representing an effect."""
+ effect_box = row.get_child().get_child()
+ self.apply_effect(effect_box.effect_name)
+
+ def apply_effect(self, effect_name):
"""Adds the selected effect to the single selected clip, if any."""
- effect = self.get_selected_effect()
- effect_info = self.app.effects.get_info(effect)
+ effect_info = self.app.effects.get_info(effect_name)
if not effect_info:
return
+
timeline = self.app.gui.editor.timeline_ui.timeline
clip = timeline.selection.get_single_clip()
if not clip:
return
+
pipeline = timeline.ges_timeline.get_parent()
from pitivi.undo.timeline import CommitTimelineFinalizingAction
with self.app.action_log.started("add effect",
@@ -550,50 +546,151 @@ class EffectListWidget(Gtk.Box, Loggable):
toplevel=True):
clip.ui.add_effect(effect_info)
- def get_selected_effect(self):
- if self._dragged_items:
- return self._dragged_items
- unused_model, rows = self.view.get_selection().get_selected_rows()
- path = self.model_filter.convert_path_to_child_path(rows[0])
- return self.storemodel[path][COL_ELEMENT_NAME]
-
- def _toggle_view_type_cb(self, widget):
- """Switches the view mode between video and audio.
+ def _set_fav_button_state(self, button, is_active):
+ """Manages the state of the favourite button."""
+ button.active = is_active
- This makes the two togglebuttons behave like a group of radiobuttons.
- """
- if widget is self.video_togglebutton:
- self.audio_togglebutton.set_active(not widget.get_active())
+ if button.active:
+ image = Gtk.Image.new_from_pixbuf(self._star_icon_solid)
else:
- assert widget is self.audio_togglebutton
- self.video_togglebutton.set_active(not widget.get_active())
+ image = Gtk.Image.new_from_pixbuf(self._star_icon_regular)
+
+ button.props.image = image
- if self.video_togglebutton.get_active():
- self._effect_type = VIDEO_EFFECT
+ def _fav_button_clicked_cb(self, clicked_button, effect):
+ """Adds effect to favourites and syncs the state of favourite button."""
+ # Toggle the state of clicked button
+ self._set_fav_button_state(clicked_button, not clicked_button.active)
+
+ # Get all listboxes which contain the effect
+ effect_info = self.app.effects.get_info(effect)
+ all_effect_listboxes = [category_expander.get_child()
+ for category_expander in self.category_view.get_children()
+ if category_expander.get_label() in effect_info.categories]
+ all_effect_listboxes.append(self.search_view)
+
+ # Find and sync state in other listboxes
+ for listbox in all_effect_listboxes:
+ for row in listbox.get_children():
+ effect_box = row.get_child().get_child()
+ if effect == effect_box.effect_name:
+ fav_button = effect_box.get_children()[2]
+ # Sync the state with the clicked button
+ self._set_fav_button_state(fav_button, clicked_button.active)
+
+ # Update the favourites list
+ if clicked_button.active:
+ self.app.settings.favourite_effects.append(effect)
else:
- self._effect_type = AUDIO_EFFECT
- self.populate_categories_widget()
- self.model_filter.refilter()
+ self.app.settings.favourite_effects = \
+ [fav for fav in self.app.settings.favourite_effects if fav != effect]
+ self.search_view.invalidate_filter()
+
+ def _favourites_filter(self, row):
+ """Filters search_view to show favourites."""
+ effect_box = row.get_child().get_child()
+ effect_name = effect_box.effect_name
+
+ return effect_name in self.app.settings.favourite_effects
+
+ def _favourites_toggle_cb(self, toggle):
+ """Manages the visiblity and filtering of Favourites in search_view."""
+ if toggle.get_active():
+ self.search_entry.set_text("")
+ self.search_view.set_filter_func(self._favourites_filter)
+ self.search_view.invalidate_filter()
+ self._switch_to_view(self.search_view)
+ else:
+ self._switch_to_view(self.category_view)
+
+ def _search_filter(self, row):
+ """Filters search_view to show search results."""
+ effect_box = row.get_child().get_child()
+ label = effect_box.get_children()[1]
- def _category_changed_cb(self, combobox):
- self.model_filter.refilter()
+ label_text = label.get_text().lower()
+ search_key = self.search_entry.get_text().lower()
- def _search_entry_changed_cb(self, unused_entry):
- self.model_filter.refilter()
+ return search_key in label_text
- def _search_entry_icon_press_cb(self, entry, icon_pos, event):
+ def _search_entry_changed_cb(self, search_entry):
+ """Manages the visiblity and filtering search results in search_view."""
+ if search_entry.get_text():
+ self.fav_view_toggle.props.active = False
+ self.search_view.set_filter_func(self._search_filter)
+ self.search_view.invalidate_filter()
+ self._switch_to_view(self.search_view)
+ else:
+ self._switch_to_view(self.category_view)
+
+ def _search_entry_icon_release_cb(self, entry, icon_pos, event):
entry.set_text("")
- def _set_row_visible_func(self, model, model_iter, data):
- if not self._effect_type == model.get_value(model_iter, COL_EFFECT_TYPE):
- return False
- if model.get_value(model_iter, COL_EFFECT_CATEGORIES) is None:
- return False
- if self.categories_widget.get_active_text() not in model.get_value(model_iter, COL_EFFECT_CATEGORIES):
- return False
- text = self.search_entry.get_text().lower()
- return text in model.get_value(model_iter, COL_DESC_TEXT).lower() or\
- text in model.get_value(model_iter, COL_NAME_TEXT).lower()
+ def _switch_to_view(self, next_view):
+ """Shows next_view and hides all other views."""
+ if next_view.props.visible:
+ # It's already visible, no need to switch to it.
+ return
+
+ for child_view in self.main_view.get_children():
+ child_view.props.visible = child_view == next_view
+
+
+class EffectsPopover(Gtk.Popover, Loggable):
+ """Popover for adding effects."""
+
+ def __init__(self, app):
+ Gtk.Popover.__init__(self)
+ Loggable.__init__(self)
+
+ self.app = app
+
+ vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, margin=PADDING)
+
+ self.search_entry = Gtk.SearchEntry()
+ self.search_entry.connect("search-changed", self._search_entry_cb)
+
+ scroll_window = Gtk.ScrolledWindow()
+ scroll_window.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC)
+ scroll_window.props.max_content_height = 350
+ scroll_window.props.propagate_natural_height = True
+
+ self.listbox = Gtk.ListBox()
+ self.listbox.connect("row-activated", self._effect_row_activate_cb)
+ self.listbox.set_filter_func(self._search_filter)
+ placeholder_text = Gtk.Label(_("No effects"))
+ placeholder_text.props.visible = True
+ self.listbox.set_placeholder(placeholder_text)
+
+ self.app.gui.editor.effectlist.add_effects_to_listbox(self.listbox, only_text=True)
+ scroll_window.add(self.listbox)
+
+ vbox.pack_start(self.search_entry, False, False, 0)
+ vbox.pack_end(scroll_window, True, True, 0)
+ vbox.show_all()
+
+ self.add(vbox)
+
+ def _effect_row_activate_cb(self, listbox, row):
+ effect_box = row.get_child().get_child()
+ self.app.gui.editor.effectlist.apply_effect(effect_box.effect_name)
+ self.hide()
+
+ def _search_entry_cb(self, search_entry):
+ self.listbox.invalidate_filter()
+
+ def _search_filter(self, row):
+ effect_box = row.get_child().get_child()
+ label = effect_box.get_children()[0]
+
+ label_text = label.get_text().lower()
+ search_key = self.search_entry.get_text().lower()
+
+ return search_key in label_text
+
+ def popup(self):
+ self.search_entry.set_text("")
+ Gtk.Popover.popup(self)
PROPS_TO_IGNORE = ['name', 'qos', 'silent', 'message', 'parent']
@@ -606,7 +703,7 @@ def create_widget_accumulator_func(ihint, return_accu, handler_return, *args):
class EffectsPropertiesManager(GObject.Object, Loggable):
- """Provides and caches UIs for editing effects.
+ """Provides UIs for editing effects.
Attributes:
app (Pitivi): The app.
@@ -636,7 +733,6 @@ class EffectsPropertiesManager(GObject.Object, Loggable):
def __init__(self, app):
GObject.Object.__init__(self)
Loggable.__init__(self)
- self.cache_dict = {}
self.app = app
def get_effect_configuration_ui(self, effect):
@@ -648,21 +744,15 @@ class EffectsPropertiesManager(GObject.Object, Loggable):
Returns:
GstElementSettingsWidget: A container for configuring the effect.
"""
- if effect not in self.cache_dict:
- effect_widget = GstElementSettingsWidget(effect, PROPS_TO_IGNORE)
- widget = self.emit("create_widget", effect_widget, effect)
- # The default handler of `create_widget` handles visibility
- # itself and returns None
- if widget is not None:
- effect_widget.show_widget(widget)
- self.cache_dict[effect] = effect_widget
- self._connect_all_widget_callbacks(effect_widget, effect)
-
- return self.cache_dict[effect]
-
- def clean_cache(self, effect):
- if effect in self.cache_dict:
- self.cache_dict.pop(effect)
+ effect_widget = GstElementSettingsWidget(effect, PROPS_TO_IGNORE)
+ widget = self.emit("create_widget", effect_widget, effect)
+ # The default handler of `create_widget` handles visibility
+ # itself and returns None
+ if widget is not None:
+ effect_widget.show_widget(widget)
+ self._connect_all_widget_callbacks(effect_widget, effect)
+
+ return effect_widget
def _post_configuration(self, effect, effect_set_ui):
effect_name = effect.get_property("bin-description")
diff --git a/pitivi/timeline/timeline.py b/pitivi/timeline/timeline.py
index 3e2f5d559bfa8d8d1110c215dfbd0c8b6dbcab2b..c261d8036203b9fa29cd56771275b61b4bf5d402 100644
--- a/pitivi/timeline/timeline.py
+++ b/pitivi/timeline/timeline.py
@@ -30,6 +30,7 @@ from pitivi.autoaligner import AutoAligner
from pitivi.configure import get_ui_dir
from pitivi.configure import in_devel
from pitivi.dialogs.prefs import PreferencesDialog
+from pitivi.effects import EffectsPopover
from pitivi.settings import GlobalSettings
from pitivi.timeline.elements import Clip
from pitivi.timeline.elements import TransitionClip
@@ -1619,6 +1620,7 @@ class TimelineContainer(Gtk.Grid, Zoomable, Loggable):
left_size_group.add_widget(self.zoom_box)
self.timeline = Timeline(self.app, left_size_group, self.editor_state)
+ self.effects_popover = EffectsPopover(self.app)
# Vertical Scrollbar. It will be displayed only when needed.
self.vscrollbar = Gtk.Scrollbar(orientation=Gtk.Orientation.VERTICAL,
@@ -1758,6 +1760,13 @@ class TimelineContainer(Gtk.Grid, Zoomable, Loggable):
self.seek_backward_clip_action,
_("Seeks to the first clip edge before the playhead."))
+ self.add_effect_action = Gio.SimpleAction.new("add-effect", None)
+ self.add_effect_action.connect("activate", self.__add_effect_cb)
+ group.add_action(self.add_effect_action)
+ self.app.shortcuts.add("timeline.add-effect", ["e"],
+ self.add_effect_action,
+ _("Add an effect to the selected clip"))
+
if in_devel():
self.gapless_action = Gio.SimpleAction.new("toggle-gapless-mode", None)
self.gapless_action.connect("activate", self._gaplessmode_toggled_cb)
@@ -2038,6 +2047,12 @@ class TimelineContainer(Gtk.Grid, Zoomable, Loggable):
self._project.pipeline.simple_seek(position)
self.timeline.scroll_to_playhead(align=Gtk.Align.CENTER, when_not_in_view=True)
+ def __add_effect_cb(self, unused_action, unused_parameter):
+ clip = self.timeline.selection.getSingleClip()
+ if clip:
+ self.effects_popover.set_relative_to(clip.ui)
+ self.effects_popover.popup()
+
def _align_selected_cb(self, unused_action, unused_parameter):
if not self.ges_timeline:
return
diff --git a/tests/test_clipproperties.py b/tests/test_clipproperties.py
index 083217ff991a95e93035ebc52eab7d6a6b00d4d3..899895566cb6a7af7a7d422ab55ee4763fc3edf2 100644
--- a/tests/test_clipproperties.py
+++ b/tests/test_clipproperties.py
@@ -19,56 +19,14 @@
from unittest import mock
from gi.repository import GES
-from gi.repository import Gtk
from pitivi.clipproperties import ClipProperties
-from pitivi.clipproperties import EffectProperties
from pitivi.clipproperties import TransformationProperties
from tests import common
from tests.test_timeline_timeline import BaseTestTimeline
from tests.test_undo_timeline import BaseTestUndoTimeline
-class EffectPropertiesTest(common.TestCase):
- """Tests for the EffectProperties class."""
-
- def test_calculate_effect_priority(self):
- """Checks the effect priority calculation."""
- # Dragging 1 onto itself and nearby.
- self.assertEqual(1, EffectProperties.calculate_effect_priority(
- 1, 0, Gtk.TreeViewDropPosition.AFTER))
- self.assertEqual(1, EffectProperties.calculate_effect_priority(
- 1, 1, Gtk.TreeViewDropPosition.BEFORE))
- self.assertEqual(1, EffectProperties.calculate_effect_priority(
- 1, 1, Gtk.TreeViewDropPosition.INTO_OR_BEFORE))
- self.assertEqual(1, EffectProperties.calculate_effect_priority(
- 1, 1, Gtk.TreeViewDropPosition.INTO_OR_AFTER))
- self.assertEqual(1, EffectProperties.calculate_effect_priority(
- 1, 1, Gtk.TreeViewDropPosition.AFTER))
- self.assertEqual(1, EffectProperties.calculate_effect_priority(
- 1, 2, Gtk.TreeViewDropPosition.BEFORE))
-
- # Dragging 0 and 3 between rows 1 and 2.
- self.assertEqual(1, EffectProperties.calculate_effect_priority(
- 0, 1, Gtk.TreeViewDropPosition.AFTER))
- self.assertEqual(1, EffectProperties.calculate_effect_priority(
- 0, 2, Gtk.TreeViewDropPosition.BEFORE))
- self.assertEqual(2, EffectProperties.calculate_effect_priority(
- 3, 1, Gtk.TreeViewDropPosition.AFTER))
- self.assertEqual(2, EffectProperties.calculate_effect_priority(
- 3, 2, Gtk.TreeViewDropPosition.BEFORE))
-
- # Dragging 0 and 2 onto 1.
- self.assertEqual(1, EffectProperties.calculate_effect_priority(
- 0, 1, Gtk.TreeViewDropPosition.INTO_OR_BEFORE))
- self.assertEqual(1, EffectProperties.calculate_effect_priority(
- 0, 1, Gtk.TreeViewDropPosition.INTO_OR_AFTER))
- self.assertEqual(1, EffectProperties.calculate_effect_priority(
- 2, 1, Gtk.TreeViewDropPosition.INTO_OR_BEFORE))
- self.assertEqual(1, EffectProperties.calculate_effect_priority(
- 2, 1, Gtk.TreeViewDropPosition.INTO_OR_AFTER))
-
-
class TransformationPropertiesTest(BaseTestTimeline):
"""Tests for the TransformationProperties widget."""