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 @@ + + width="21.166298mm" + height="11.90625mm" + viewBox="0 0 21.166298 11.90625" + version="1.1" + id="svg8" + inkscape:version="0.92.4 5da689c313, 2019-01-14" + sodipodi:docname="defaultthumbnail.svg"> + id="defs2"> + id="linearGradient6010" + inkscape:collect="always"> + style="stop-color:#e06666;stop-opacity:1;" /> + style="stop-color:#e06666;stop-opacity:0;" /> + id="linearGradient5994" + inkscape:collect="always"> + style="stop-color:#8b0000;stop-opacity:1;" /> + style="stop-color:#af0000;stop-opacity:1" /> + id="linearGradient5966" + inkscape:collect="always"> + style="stop-color:#eeeeec;stop-opacity:1;" /> + style="stop-color:#cbcbc3;stop-opacity:1" /> + id="linearGradient5958" + inkscape:collect="always"> + style="stop-color:#df0000;stop-opacity:1" /> + style="stop-color:#ba0000;stop-opacity:1" /> + id="linearGradient5172" + inkscape:collect="always"> + style="stop-color:#cc0000;stop-opacity:1;" /> - - - - + style="stop-color:#ba0000;stop-opacity:1" /> + sodipodi:type="inkscape:persp3d" /> + style="stop-color:black;stop-opacity:0;" /> + id="stop3710" /> + style="stop-color:black;stop-opacity:0;" /> - + + fx="4.9929786" + cy="43.5" + cx="4.9929786" + gradientTransform="matrix(2.003784,0,0,1.4,-20.01187,-104.4)" + gradientUnits="userSpaceOnUse" + id="radialGradient2096" + xlink:href="#linearGradient3688" + inkscape:collect="always" /> + id="linearGradient3688" + inkscape:collect="always"> + style="stop-color:black;stop-opacity:1;" /> + style="stop-color:black;stop-opacity:0;" /> + fx="4.9929786" + cy="43.5" + cx="4.9929786" + gradientTransform="matrix(2.003784,0,0,1.4,27.98813,-17.4)" + gradientUnits="userSpaceOnUse" + id="radialGradient2094" + xlink:href="#linearGradient3688" + inkscape:collect="always" /> + id="linearGradient9372" + inkscape:collect="always"> + style="stop-color:#fce94f;stop-opacity:1;" /> + style="stop-color:#fce94f;stop-opacity:0;" /> + id="linearGradient6739" + inkscape:collect="always"> + style="stop-color:#c00;stop-opacity:1" /> + style="stop-color:#730000;stop-opacity:1" /> + y2="9.0032368" + x2="11" + y1="5.7221918" + x1="11" + id="linearGradient6745" + xlink:href="#linearGradient6739" + inkscape:collect="always" /> + - - + inkscape:collect="always" /> - - + + + - + id="radialGradient10283" + xlink:href="#linearGradient9372" + inkscape:collect="always" /> + + x2="3.2703688" + y1="14.189748" + x1="5.6568542" + gradientUnits="userSpaceOnUse" + id="linearGradient5952" + xlink:href="#linearGradient5172" + inkscape:collect="always" /> + y2="19.545048" + x2="9.982995" + y1="25.639849" + x1="9.7178297" + id="linearGradient5964" + xlink:href="#linearGradient5958" + inkscape:collect="always" /> + x2="8.4977846" + y1="13.208529" + x1="8.859499" + id="linearGradient5972" + xlink:href="#linearGradient5966" + inkscape:collect="always" /> + x2="25.749866" + y1="35.811172" + x1="25.749866" + id="linearGradient6000" + xlink:href="#linearGradient5994" + inkscape:collect="always" /> - + + x2="45.068298" + y1="25.116896" + x1="39.439995" + id="linearGradient6016" + xlink:href="#linearGradient6010" + inkscape:collect="always" /> - - + showgrid="true" + showguides="false" + inkscape:window-width="1850" + inkscape:window-height="1016" + inkscape:window-x="70" + inkscape:window-y="27" + inkscape:window-maximized="1" /> + id="metadata5"> image/svg+xml + + inkscape:groupmode="layer" + id="layer1" + transform="translate(-128.50053,-98.142981)"> + inkscape:label="Layer 1" + id="layer1-6" + transform="matrix(0.27164802,0,0,0.27580143,132.55853,97.257168)"> + transform="matrix(1.0206646,0,0,1.1351534,-0.426478,-6.9707553)" + inkscape:label="Shadow" + id="g2043"> - - - + inkscape:label="Shadow" + id="g2036" + style="display:inline"> + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - 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 @@ - + @@ -7,59 +7,18 @@ False False 1 - - - True - False - Show video effects - True - video-x-generic - True - - - - effects library video togglebutton - - - - - False - True - - - - - True - False - Show audio effects - True - audio-x-generic - - - - effects library audio togglebutton - - - - - False - True - - True False - + + Favourites True - False - - - - effect category combobox - - + True + True + True + @@ -74,22 +33,16 @@ False 5 - + True True True - 3 - + False + edit-find-symbolic edit-clear-symbolic - Clear the current search Search... - - - - - effects library search entry - - + + 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."""