Esempio n. 1
0
    def test_dependent_properties(self):
        """Checks dependent properties updating is handled correctly."""
        mainloop = common.create_main_loop()
        app = common.create_pitivi()
        app.project_manager.new_blank_project()
        manager = EffectsPropertiesManager(app)

        called = False

        def set_child_property(prop_name, value):
            nonlocal called
            called = True

            self.assertEqual(prop_name, "aspect-ratio")
            GES.Effect.set_child_property(effect, prop_name, value)

            # When setting the aspect-ratio property, and the stars align,
            # the effect also changes the left/right properties.
            # Here we simulate the updating of the dependent properties.
            GES.Effect.set_child_property(effect, "left", 100)
            GES.Effect.set_child_property(effect, "right", 100)

        effect = GES.Effect.new("aspectratiocrop")
        effect.set_child_property = set_child_property

        effect_widget = manager.getEffectConfigurationUI(effect)

        widgets = {prop.name: widget
                   for prop, widget in effect_widget.properties.items()}
        # Simulate the user choosing an aspect-ratio.
        widgets["aspect-ratio"].setWidgetValue(Gst.Fraction(4, 3))

        mainloop.run(until_empty=True)

        self.assertTrue(called)
Esempio n. 2
0
    def test_dependent_properties(self):
        """Checks dependent properties updating is handled correctly."""
        mainloop = common.create_main_loop()
        app = common.create_pitivi()
        app.project_manager.new_blank_project()
        manager = EffectsPropertiesManager(app)

        called = False

        def set_child_property(prop_name, value):
            nonlocal called
            called = True

            self.assertEqual(prop_name, "aspect-ratio")
            GES.Effect.set_child_property(effect, prop_name, value)

            # When setting the aspect-ratio property, and the stars align,
            # the effect also changes the left/right properties.
            # Here we simulate the updating of the dependent properties.
            GES.Effect.set_child_property(effect, "left", 100)
            GES.Effect.set_child_property(effect, "right", 100)

        effect = GES.Effect.new("aspectratiocrop")
        effect.set_child_property = set_child_property

        effect_widget = manager.getEffectConfigurationUI(effect)

        widgets = {
            prop.name: widget
            for prop, widget in effect_widget.properties.items()
        }
        # Simulate the user choosing an aspect-ratio.
        widgets["aspect-ratio"].setWidgetValue(Gst.Fraction(4, 3))

        mainloop.run(until_empty=True)

        self.assertTrue(called)
Esempio n. 3
0
class EffectProperties(Gtk.Expander, Loggable):
    """Widget for viewing a list of effects and configuring them.

    Attributes:
        app (Pitivi): The app.
    """

    # pylint: disable=too-many-statements
    def __init__(self, app, clip_properties):
        Gtk.Expander.__init__(self)
        self.set_expanded(True)
        self.set_label(_("Effects"))
        Loggable.__init__(self)

        # Global variables related to effects
        self.app = app

        self._project = None
        self._selection = None
        self.selected_effects = []
        self.clips = []
        self._effect_config_ui = None
        self.effects_properties_manager = EffectsPropertiesManager(app)
        self.clip_properties = clip_properties

        # The toolbar that will go between the list of effects and properties
        buttons_box = Gtk.ButtonBox()
        buttons_box.set_halign(Gtk.Align.END)
        buttons_box.set_margin_end(SPACING)
        buttons_box.props.margin_top = SPACING / 2

        remove_effect_button = Gtk.Button()
        remove_icon = Gtk.Image.new_from_icon_name("list-remove-symbolic",
                                                   Gtk.IconSize.BUTTON)
        remove_effect_button.set_image(remove_icon)
        remove_effect_button.set_always_show_image(True)
        remove_effect_button.set_label(_("Remove effect"))
        buttons_box.pack_start(remove_effect_button,
                               expand=False,
                               fill=False,
                               padding=0)

        # We need to specify Gtk.TreeDragSource because otherwise we are hitting
        # bug https://bugzilla.gnome.org/show_bug.cgi?id=730740.
        class EffectsListStore(Gtk.ListStore, Gtk.TreeDragSource):
            """Just a work around!"""

            # pylint: disable=non-parent-init-called
            def __init__(self, *args):
                Gtk.ListStore.__init__(self, *args)
                # Set the source index on the storemodel directly,
                # to avoid issues with the selection_data API.
                # FIXME: Work around
                # https://bugzilla.gnome.org/show_bug.cgi?id=737587
                self.source_index = None

            def do_drag_data_get(self, path, unused_selection_data):
                self.source_index = path.get_indices()[0]

        self.storemodel = EffectsListStore(bool, str, str, str, str, object)
        self.treeview = Gtk.TreeView(model=self.storemodel)
        self.treeview.set_property("has_tooltip", True)
        self.treeview.set_headers_visible(False)
        self.treeview.props.margin_top = SPACING
        self.treeview.props.margin_left = SPACING
        # Without this, the treeview hides the border of its parent.
        # I should file a bug about this.
        self.treeview.props.margin_right = 1

        activated_cell = Gtk.CellRendererToggle()
        activated_cell.props.xalign = 0
        activated_cell.props.xpad = 0
        activated_cell.connect("toggled", self._effectActiveToggleCb)
        self.treeview.insert_column_with_attributes(-1,
                                                    _("Active"),
                                                    activated_cell,
                                                    active=COL_ACTIVATED)

        type_col = Gtk.TreeViewColumn(_("Type"))
        type_col.set_spacing(SPACING)
        type_col.set_sizing(Gtk.TreeViewColumnSizing.AUTOSIZE)
        type_cell = Gtk.CellRendererText()
        type_cell.props.xpad = PADDING
        type_col.pack_start(type_cell, expand=True)
        type_col.add_attribute(type_cell, "text", COL_TYPE)
        self.treeview.append_column(type_col)

        name_col = Gtk.TreeViewColumn(_("Effect name"))
        name_col.set_spacing(SPACING)
        name_cell = Gtk.CellRendererText()
        name_cell.props.xpad = PADDING
        name_cell.set_property("ellipsize", Pango.EllipsizeMode.END)
        name_col.pack_start(name_cell, expand=True)
        name_col.add_attribute(name_cell, "text", COL_NAME_TEXT)
        self.treeview.append_column(name_col)

        # Allow the treeview to accept EFFECT_TARGET_ENTRY when drag&dropping.
        self.treeview.enable_model_drag_dest([EFFECT_TARGET_ENTRY],
                                             Gdk.DragAction.COPY)

        # Enable reordering by drag&drop.
        self.treeview.enable_model_drag_source(Gdk.ModifierType.BUTTON1_MASK,
                                               [EFFECT_TARGET_ENTRY],
                                               Gdk.DragAction.MOVE)

        self.treeview_selection = self.treeview.get_selection()
        self.treeview_selection.set_mode(Gtk.SelectionMode.SINGLE)

        self._infobar = clip_properties.createInfoBar(
            _("Select a clip on the timeline to configure its associated effects"
              ))
        self._infobar.show_all()

        # Prepare the main container widgets and lay out everything
        self._vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
        self._vbox.pack_start(self.treeview,
                              expand=False,
                              fill=False,
                              padding=0)
        self._vbox.pack_start(buttons_box, expand=False, fill=False, padding=0)
        separator = Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL)
        separator.set_margin_top(SPACING)
        separator.set_margin_left(SPACING)
        separator.set_margin_right(SPACING)
        self._vbox.pack_start(separator, expand=False, fill=False, padding=0)
        self._vbox.show_all()
        self.add(self._vbox)
        self.hide()

        effects_actions_group = Gio.SimpleActionGroup()
        self.treeview.insert_action_group("clipproperties-effects",
                                          effects_actions_group)
        buttons_box.insert_action_group("clipproperties-effects",
                                        effects_actions_group)
        self.app.shortcuts.register_group("clipproperties-effects",
                                          _("Clip Effects"),
                                          position=60)

        self.remove_effect_action = Gio.SimpleAction.new("remove-effect", None)
        self.remove_effect_action.connect("activate", self._removeEffectCb)
        effects_actions_group.add_action(self.remove_effect_action)
        self.app.shortcuts.add("clipproperties-effects.remove-effect",
                               ["Delete"], _("Remove the selected effect"))
        self.remove_effect_action.set_enabled(False)
        remove_effect_button.set_action_name(
            "clipproperties-effects.remove-effect")

        # Connect all the widget signals
        self.treeview_selection.connect("changed",
                                        self._treeviewSelectionChangedCb)
        self.treeview.connect("drag-motion", self._dragMotionCb)
        self.treeview.connect("drag-leave", self._dragLeaveCb)
        self.treeview.connect("drag-data-received", self._dragDataReceivedCb)
        self.treeview.connect("query-tooltip", self._treeViewQueryTooltipCb)
        self.app.project_manager.connect_after("new-project-loaded",
                                               self._newProjectLoadedCb)
        self.connect('notify::expanded', self._expandedCb)

    def _newProjectLoadedCb(self, unused_app, project):
        if self._selection is not None:
            self._selection.disconnect_by_func(self._selectionChangedCb)
            self._selection = None
        self._project = project
        if project:
            self._selection = project.ges_timeline.ui.selection
            self._selection.connect('selection-changed',
                                    self._selectionChangedCb)
            self.selected_effects = self._selection.getSelectedEffects()
        self.__updateAll()

    def _selectionChangedCb(self, selection):
        for clip in self.clips:
            clip.disconnect_by_func(self._trackElementAddedCb)
            clip.disconnect_by_func(self._trackElementRemovedCb)

        self.selected_effects = selection.getSelectedEffects()

        if selection:
            self.clips = list(selection.selected)
            for clip in self.clips:
                clip.connect("child-added", self._trackElementAddedCb)
                clip.connect("child-removed", self._trackElementRemovedCb)
            self.show()
        else:
            self.clips = []
            self.hide()
        self.__updateAll()

    def _trackElementAddedCb(self, unused_clip, track_element):
        if isinstance(track_element, GES.BaseEffect):
            selec = self._selection.getSelectedEffects()
            self.selected_effects = selec
            self.__updateAll()
            for path, row in enumerate(self.storemodel):
                if row[COL_TRACK_EFFECT] == track_element:
                    self.treeview_selection.select_path(path)
                    break

    def _trackElementRemovedCb(self, unused_clip, track_element):
        if isinstance(track_element, GES.BaseEffect):
            selec = self._selection.getSelectedEffects()
            self.selected_effects = selec
            self.__updateAll()

    def _removeEffectCb(self, unused_action, unused_param):
        selected = self.treeview_selection.get_selected()
        if not selected[1]:
            # Cannot remove nothing,
            return
        effect = self.storemodel.get_value(selected[1], COL_TRACK_EFFECT)
        self._removeEffect(effect)

    def _removeEffect(self, effect):
        pipeline = self._project.pipeline
        with self.app.action_log.started(
                "remove effect", CommitTimelineFinalizingAction(pipeline)):
            self.__remove_configuration_widget()
            self.effects_properties_manager.cleanCache(effect)
            effect.get_parent().remove(effect)
        self._updateTreeview()

    def addEffectToClip(self, clip, factory_name, priority=None):
        """Adds the specified effect if it can be applied to the clip."""
        if factory_name in ALLOWED_ONLY_ONCE_EFFECTS:
            for effect in clip.find_track_elements(None, GES.TrackType.VIDEO,
                                                   GES.BaseEffect):
                for elem in effect.get_nleobject().iterate_recurse():
                    if elem.get_factory().get_name() == factory_name:
                        self.error(
                            "Not adding %s as it would be duplicate"
                            " and this is not allowed.", factory_name)
                        # TODO Let the user know about why it did not work.
                        return effect

        model = self.treeview.get_model()
        media_type = self.app.effects.getInfo(factory_name).media_type

        for track_element in clip.get_children(False):
            track_type = track_element.get_track_type()
            if track_type == GES.TrackType.AUDIO and media_type == AUDIO_EFFECT or \
                    track_type == GES.TrackType.VIDEO and media_type == VIDEO_EFFECT:
                # Actually add the effect
                pipeline = self._project.pipeline
                with self.app.action_log.started(
                        "add effect",
                        CommitTimelineFinalizingAction(pipeline)):
                    effect = GES.Effect.new(bin_description=factory_name)
                    clip.add(effect)
                    if priority is not None and priority < len(model):
                        clip.set_top_effect_priority(effect, priority)
                break
        return None

    def addEffectToCurrentSelection(self, factory_name):
        """Adds an effect to the current selection.

        Args:
            factory_name (str): The name of the GstElementFactory for creating
                the effect.
        """
        if not self.clips or len(self.clips) > 1:
            return
        clip = self.clips[0]
        # Checking that this effect can be applied on this track object
        # Which means, it has the corresponding media_type
        self.addEffectToClip(clip, factory_name)

    # pylint: disable=too-many-arguments
    def _dragMotionCb(self, unused_tree_view, unused_drag_context, unused_x,
                      unused_y, unused_timestamp):
        self.debug(
            "Something is being dragged in the clip properties' effects list")
        self.drag_highlight()

    def _dragLeaveCb(self, unused_tree_view, unused_drag_context,
                     unused_timestamp):
        self.info(
            "The item being dragged has left the clip properties' effects list"
        )
        self.drag_unhighlight()

    # pylint: disable=too-many-arguments
    def _dragDataReceivedCb(self, treeview, drag_context, x, y, selection_data,
                            unused_info, timestamp):
        if not self.clips or len(self.clips) > 1:
            # Indicate that a drop will not be accepted.
            Gdk.drag_status(drag_context, 0, timestamp)
            return
        clip = self.clips[0]
        dest_row = treeview.get_dest_row_at_pos(x, y)
        if drag_context.get_suggested_action() == Gdk.DragAction.COPY:
            # An effect dragged probably from the effects list.
            factory_name = str(selection_data.get_data(), "UTF-8")
            drop_index = self.__get_new_effect_index(dest_row)
            self.addEffectToClip(clip, factory_name, drop_index)
        elif drag_context.get_suggested_action() == Gdk.DragAction.MOVE:
            # An effect dragged from the same treeview to change its position.
            # Source
            source_index, drop_index = self.__get_move_indexes(
                dest_row, treeview.get_model())
            self.__move_effect(clip, source_index, drop_index)

        drag_context.finish(True, False, timestamp)

    # pylint: disable=no-self-use
    def __get_new_effect_index(self, dest_row):
        # Target
        if dest_row:
            drop_path, drop_pos = dest_row
            drop_index = drop_path.get_indices()[0]
            if drop_pos != Gtk.TreeViewDropPosition.BEFORE:
                drop_index += 1
        else:
            # This should happen when dragging after the last row.
            drop_index = None

        return drop_index

    def __get_move_indexes(self, dest_row, model):
        source_index = self.storemodel.source_index
        self.storemodel.source_index = None

        # Target
        if dest_row:
            drop_path, drop_pos = dest_row
            drop_index = drop_path.get_indices()[0]
            drop_index = self.calculateEffectPriority(source_index, drop_index,
                                                      drop_pos)
        else:
            # This should happen when dragging after the last row.
            drop_index = len(model) - 1
            drop_pos = Gtk.TreeViewDropPosition.INTO_OR_BEFORE

        return source_index, drop_index

    def __move_effect(self, clip, source_index, drop_index):
        if source_index == drop_index:
            # Noop.
            return
        # The paths are different.
        effects = clip.get_top_effects()
        effect = effects[source_index]
        pipeline = self._project.ges_timeline.get_parent()
        with self.app.action_log.started(
                "move effect", CommitTimelineFinalizingAction(pipeline)):
            clip.set_top_effect_priority(effect, drop_index)

        new_path = Gtk.TreePath.new()
        new_path.append_index(drop_index)
        self.__updateAll(path=new_path)

    @staticmethod
    def calculateEffectPriority(source_index, drop_index, drop_pos):
        """Calculates where the effect from source_index will end up."""
        if drop_pos in (Gtk.TreeViewDropPosition.INTO_OR_BEFORE,
                        Gtk.TreeViewDropPosition.INTO_OR_AFTER):
            return drop_index
        if drop_pos == Gtk.TreeViewDropPosition.BEFORE:
            if source_index < drop_index:
                return drop_index - 1
        elif drop_pos == Gtk.TreeViewDropPosition.AFTER:
            if source_index > drop_index:
                return drop_index + 1
        return drop_index

    def _effectActiveToggleCb(self, cellrenderertoggle, path):
        _iter = self.storemodel.get_iter(path)
        tck_effect = self.storemodel.get_value(_iter, COL_TRACK_EFFECT)
        with self.app.action_log.started("change active state"):
            tck_effect.props.active = not tck_effect.props.active
            cellrenderertoggle.set_active(tck_effect.is_active())
            self._updateTreeview()
            self._project.ges_timeline.commit()

    def _expandedCb(self, unused_expander, unused_params):
        self.__updateAll()

    def _treeViewQueryTooltipCb(self, view, x, y, keyboard_mode, tooltip):
        is_row, x, y, unused_model, path, tree_iter = view.get_tooltip_context(
            x, y, keyboard_mode)
        if not is_row:
            return False

        view.set_tooltip_row(tooltip, path)
        description = self.storemodel.get_value(tree_iter, COL_DESC_TEXT)
        bin_description = self.storemodel.get_value(tree_iter,
                                                    COL_BIN_DESCRIPTION_TEXT)
        tooltip.set_text("%s\n%s" % (bin_description, description))
        return True

    def __updateAll(self, path=None):
        if len(self.clips) == 1:
            self.show()
            self._infobar.hide()
            self._updateTreeview()
            if path:
                self.treeview_selection.select_path(path)
        else:
            self.hide()
            self.__remove_configuration_widget()
            self.storemodel.clear()
            self._infobar.show()

    def _updateTreeview(self):
        self.storemodel.clear()
        clip = self.clips[0]
        for effect in clip.get_top_effects():
            if effect.props.bin_description in HIDDEN_EFFECTS:
                continue
            effect_info = self.app.effects.getInfo(
                effect.props.bin_description)
            to_append = [effect.props.active]
            track_type = effect.get_track_type()
            if track_type == GES.TrackType.AUDIO:
                to_append.append("Audio")
            elif track_type == GES.TrackType.VIDEO:
                to_append.append("Video")
            to_append.append(effect.props.bin_description)
            to_append.append(effect_info.human_name)
            to_append.append(effect_info.description)
            to_append.append(effect)
            self.storemodel.append(to_append)
        self._vbox.set_visible(len(self.storemodel) > 0)

    def _treeviewSelectionChangedCb(self, unused_treeview):
        selection_is_emtpy = self.treeview_selection.count_selected_rows() == 0
        self.remove_effect_action.set_enabled(not selection_is_emtpy)

        self._updateEffectConfigUi()

    def _updateEffectConfigUi(self):
        model, tree_iter = self.treeview_selection.get_selected()
        if tree_iter:
            effect = model.get_value(tree_iter, COL_TRACK_EFFECT)
            self._showEffectConfigurationWidget(effect)
        else:
            self.__remove_configuration_widget()

    def __remove_configuration_widget(self):
        if not self._effect_config_ui:
            # Nothing to remove.
            return

        self._effect_config_ui.deactivate_keyframe_toggle_buttons()
        self._vbox.remove(self._effect_config_ui)
        self._effect_config_ui = None

    def _showEffectConfigurationWidget(self, effect):
        self.__remove_configuration_widget()
        self._effect_config_ui = self.effects_properties_manager.getEffectConfigurationUI(
            effect)
        if not self._effect_config_ui:
            return
        self._effect_config_ui.show()
        self._effect_config_ui.show_all()
        self._vbox.add(self._effect_config_ui)
Esempio n. 4
0
class EffectProperties(Gtk.Expander, Loggable):
    """
    Widget for viewing a list of effects and configuring them.

    @type app: C{Pitivi}
    @type effects_properties_manager: C{EffectsPropertiesManager}
    """

    # pylint: disable=too-many-statements
    def __init__(self, app, clip_properties):
        Gtk.Expander.__init__(self)
        self.set_expanded(True)
        self.set_label(_("Effects"))
        Loggable.__init__(self)

        # Global variables related to effects
        self.app = app

        self._project = None
        self._selection = None
        self.selected_effects = []
        self.clips = []
        self._effect_config_ui = None
        self.effects_properties_manager = EffectsPropertiesManager(app)
        self.clip_properties = clip_properties

        # The toolbar that will go between the list of effects and properties
        buttons_box = Gtk.ButtonBox()
        buttons_box.set_halign(Gtk.Align.END)
        buttons_box.set_margin_end(SPACING)
        buttons_box.props.margin_top = SPACING / 2

        remove_effect_button = Gtk.Button()
        remove_icon = Gtk.Image.new_from_icon_name("list-remove-symbolic",
                                                   Gtk.IconSize.BUTTON)
        remove_effect_button.set_image(remove_icon)
        remove_effect_button.set_always_show_image(True)
        remove_effect_button.set_label(_("Remove effect"))
        buttons_box.pack_start(remove_effect_button,
                               expand=False, fill=False, padding=0)

        # We need to specify Gtk.TreeDragSource because otherwise we are hitting
        # bug https://bugzilla.gnome.org/show_bug.cgi?id=730740.
        class EffectsListStore(Gtk.ListStore, Gtk.TreeDragSource):
            """
            Just a work around!
            """
            # pylint: disable=non-parent-init-called
            def __init__(self, *args):
                Gtk.ListStore.__init__(self, *args)
                # Simply set the source index on the storemodrel directly
                # to avoid issues with the selection_data API
                # FIXME: Work around
                # https://bugzilla.gnome.org/show_bug.cgi?id=737587
                self.source_index = None

            def do_drag_data_get(self, path, unused_selection_data):
                self.source_index = path.get_indices()[0]

        self.storemodel = EffectsListStore(bool, str, str, str, str, object)
        self.treeview = Gtk.TreeView(model=self.storemodel)
        self.treeview.set_property("has_tooltip", True)
        self.treeview.set_headers_visible(False)
        self.treeview.props.margin_top = SPACING
        self.treeview.props.margin_left = SPACING
        # Without this, the treeview hides the border of its parent.
        # I should file a bug about this.
        self.treeview.props.margin_right = 1

        activated_cell = Gtk.CellRendererToggle()
        activated_cell.props.xalign = 0
        activated_cell.props.xpad = 0
        activated_cell.connect("toggled", self._effectActiveToggleCb)
        self.treeview.insert_column_with_attributes(-1,
                                                    _("Active"), activated_cell,
                                                    active=COL_ACTIVATED)

        type_col = Gtk.TreeViewColumn(_("Type"))
        type_col.set_spacing(SPACING)
        type_col.set_sizing(Gtk.TreeViewColumnSizing.AUTOSIZE)
        type_cell = Gtk.CellRendererText()
        type_cell.props.xpad = PADDING
        type_col.pack_start(type_cell, expand=True)
        type_col.add_attribute(type_cell, "text", COL_TYPE)
        self.treeview.append_column(type_col)

        name_col = Gtk.TreeViewColumn(_("Effect name"))
        name_col.set_spacing(SPACING)
        name_cell = Gtk.CellRendererText()
        name_cell.props.xpad = PADDING
        name_cell.set_property("ellipsize", Pango.EllipsizeMode.END)
        name_col.pack_start(name_cell, expand=True)
        name_col.add_attribute(name_cell, "text", COL_NAME_TEXT)
        self.treeview.append_column(name_col)

        # Allow the treeview to accept EFFECT_TARGET_ENTRY when drag&dropping.
        self.treeview.enable_model_drag_dest([EFFECT_TARGET_ENTRY],
                                             Gdk.DragAction.COPY)

        # Enable reordering by drag&drop.
        self.treeview.enable_model_drag_source(Gdk.ModifierType.BUTTON1_MASK,
                                               [EFFECT_TARGET_ENTRY],
                                               Gdk.DragAction.MOVE)

        self.treeview_selection = self.treeview.get_selection()
        self.treeview_selection.set_mode(Gtk.SelectionMode.SINGLE)

        self._infobar = clip_properties.createInfoBar(
            _("Select a clip on the timeline to configure its associated effects"))
        self._infobar.show_all()

        # Prepare the main container widgets and lay out everything
        self._vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
        self._vbox.pack_start(self.treeview, expand=False, fill=False, padding=0)
        self._vbox.pack_start(buttons_box, expand=False, fill=False, padding=0)
        separator = Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL)
        separator.set_margin_top(SPACING)
        separator.set_margin_left(SPACING)
        separator.set_margin_right(SPACING)
        self._vbox.pack_start(separator, expand=False, fill=False, padding=0)
        self._vbox.show_all()
        self.add(self._vbox)
        self.hide()

        effects_actions_group = Gio.SimpleActionGroup()
        self.treeview.insert_action_group("clipproperties_effects", effects_actions_group)
        buttons_box.insert_action_group("clipproperties_effects", effects_actions_group)

        self.remove_effect_action = Gio.SimpleAction.new("remove_effect", None)
        self.remove_effect_action.connect("activate", self._removeEffectCb)
        effects_actions_group.add_action(self.remove_effect_action)
        self.app.add_accelerator("Delete", "clipproperties_effects.remove_effect", None)
        self.remove_effect_action.set_enabled(False)
        remove_effect_button.set_action_name("clipproperties_effects.remove_effect")

        # Connect all the widget signals
        self.treeview_selection.connect("changed", self._treeviewSelectionChangedCb)
        self.treeview.connect("drag-motion", self._dragMotionCb)
        self.treeview.connect("drag-leave", self._dragLeaveCb)
        self.treeview.connect("drag-data-received", self._dragDataReceivedCb)
        self.treeview.connect("query-tooltip", self._treeViewQueryTooltipCb)
        self.app.project_manager.connect(
            "new-project-loaded", self._newProjectLoadedCb)
        self.connect('notify::expanded', self._expandedCb)

    def _newProjectLoadedCb(self, unused_app, project):
        if self._selection is not None:
            self._selection.disconnect_by_func(self._selectionChangedCb)
            self._selection = None
        self._project = project
        if project:
            self._selection = project.timeline.ui.selection
            self._selection.connect('selection-changed', self._selectionChangedCb)
            self.selected_effects = self._selection.getSelectedEffects()
        self.__updateAll()

    def _selectionChangedCb(self, selection):
        for clip in self.clips:
            clip.disconnect_by_func(self._trackElementAddedCb)
            clip.disconnect_by_func(self._trackElementRemovedCb)

        self.selected_effects = selection.getSelectedEffects()

        if selection:
            self.clips = list(selection.selected)
            for clip in self.clips:
                clip.connect("child-added", self._trackElementAddedCb)
                clip.connect("child-removed", self._trackElementRemovedCb)
            self.show()
        else:
            self.clips = []
            self.hide()
        self.__updateAll()

    def _trackElementAddedCb(self, unused_clip, track_element):
        if isinstance(track_element, GES.BaseEffect):
            selec = self._selection.getSelectedEffects()
            self.selected_effects = selec
            self.__updateAll()

    def _trackElementRemovedCb(self, unused_clip, track_element):
        if isinstance(track_element, GES.BaseEffect):
            selec = self._selection.getSelectedEffects()
            self.selected_effects = selec
            self.__updateAll()

    def _removeEffectCb(self, unused_action, unused_param):
        selected = self.treeview_selection.get_selected()
        if not selected[1]:
            # Cannot remove nothing,
            return
        effect = self.storemodel.get_value(selected[1], COL_TRACK_EFFECT)
        self._removeEffect(effect)

    def _removeEffect(self, effect):
        self.app.action_log.begin("remove effect")
        self.__remove_configuration_widget()
        self.effects_properties_manager.cleanCache(effect)
        effect.get_parent().remove(effect)
        self._project.timeline.commit()
        self.app.action_log.commit()
        self._updateTreeview()

    def addEffectToClip(self, clip, factory_name, priority=None):
        """Adds the specified effect if it can be applied to the clip."""

        model = self.treeview.get_model()
        media_type = self.app.effects.getInfo(factory_name).media_type
        for track_element in clip.get_children(False):
            track_type = track_element.get_track_type()
            if track_type == GES.TrackType.AUDIO and media_type == AUDIO_EFFECT or \
                    track_type == GES.TrackType.VIDEO and media_type == VIDEO_EFFECT:
                # Actually add the effect
                self.app.action_log.begin("add effect")
                effect = GES.Effect.new(bin_description=factory_name)
                clip.add(effect)
                if priority is not None and priority < len(model):
                    clip.set_top_effect_priority(effect, priority)
                self._project.timeline.commit()
                self.app.action_log.commit()
                self.__updateAll()
                break

    def addEffectToCurrentSelection(self, factory_name):
        """
        Add an effect to the current selection

        Args:
            factory_name (str): The name of the GstElementFactory to creaye the
            effect from.
        """
        if not self.clips or len(self.clips) > 1:
            return
        clip = self.clips[0]
        # Checking that this effect can be applied on this track object
        # Which means, it has the corresponding media_type
        self.addEffectToClip(clip, factory_name)

    # pylint: disable=too-many-arguments
    def _dragMotionCb(self, unused_tree_view, unused_drag_context, unused_x, unused_y, unused_timestamp):
        self.debug(
            "Something is being dragged in the clip properties' effects list")
        self.drag_highlight()

    def _dragLeaveCb(self, unused_tree_view, unused_drag_context, unused_timestamp):
        self.info(
            "The item being dragged has left the clip properties' effects list")
        self.drag_unhighlight()

    # pylint: disable=too-many-arguments
    def _dragDataReceivedCb(self, treeview, drag_context, x, y, selection_data, unused_info, timestamp):
        if not self.clips or len(self.clips) > 1:
            # Indicate that a drop will not be accepted.
            Gdk.drag_status(drag_context, 0, timestamp)
            return
        clip = self.clips[0]
        dest_row = treeview.get_dest_row_at_pos(x, y)
        if drag_context.get_suggested_action() == Gdk.DragAction.COPY:
            # An effect dragged probably from the effects list.
            factory_name = str(selection_data.get_data(), "UTF-8")
            drop_index = self.__get_new_effect_index(dest_row)
            self.addEffectToClip(clip, factory_name, drop_index)
        elif drag_context.get_suggested_action() == Gdk.DragAction.MOVE:
            # An effect dragged from the same treeview to change its position.
            # Source
            source_index, drop_index = self.__get_move_indexes(
                dest_row, treeview.get_model())
            self.__move_effect(clip, source_index, drop_index)

        drag_context.finish(True, False, timestamp)

    # pylint: disable=no-self-use
    def __get_new_effect_index(self, dest_row):
        # Target
        if dest_row:
            drop_path, drop_pos = dest_row
            drop_index = drop_path.get_indices()[0]
            if drop_pos != Gtk.TreeViewDropPosition.BEFORE:
                drop_index += 1
        else:
            # This should happen when dragging after the last row.
            drop_index = None

        return drop_index

    def __get_move_indexes(self, dest_row, model):
        source_index = self.storemodel.source_index
        self.storemodel.source_index = None

        # Target
        if dest_row:
            drop_path, drop_pos = dest_row
            drop_index = drop_path.get_indices()[0]
            drop_index = self.calculateEffectPriority(
                source_index, drop_index, drop_pos)
        else:
            # This should happen when dragging after the last row.
            drop_index = len(model) - 1
            drop_pos = Gtk.TreeViewDropPosition.INTO_OR_BEFORE

        return source_index, drop_index

    def __move_effect(self, clip, source_index, drop_index):
        if source_index == drop_index:
            # Noop.
            return
        # The paths are different.
        effects = clip.get_top_effects()
        effect = effects[source_index]
        self.app.action_log.begin("move effect")
        clip.set_top_effect_priority(effect, drop_index)
        self._project.timeline.commit()
        self.app.action_log.commit()
        self._project.pipeline.flushSeek()
        new_path = Gtk.TreePath.new()
        new_path.append_index(drop_index)
        self.__updateAll(path=new_path)

    @staticmethod
    def calculateEffectPriority(source_index, drop_index, drop_pos):
        """
        Return where the effect from source_index will end up
        """
        if drop_pos in (Gtk.TreeViewDropPosition.INTO_OR_BEFORE, Gtk.TreeViewDropPosition.INTO_OR_AFTER):
            return drop_index
        if drop_pos == Gtk.TreeViewDropPosition.BEFORE:
            if source_index < drop_index:
                return drop_index - 1
        elif drop_pos == Gtk.TreeViewDropPosition.AFTER:
            if source_index > drop_index:
                return drop_index + 1
        return drop_index

    def _effectActiveToggleCb(self, cellrenderertoggle, path):
        _iter = self.storemodel.get_iter(path)
        tck_effect = self.storemodel.get_value(_iter, COL_TRACK_EFFECT)
        self.app.action_log.begin("change active state")
        tck_effect.set_active(not tck_effect.is_active())
        cellrenderertoggle.set_active(tck_effect.is_active())
        self._updateTreeview()
        self._project.timeline.commit()
        self.app.action_log.commit()

    def _expandedCb(self, unused_expander, unused_params):
        self.__updateAll()

    def _treeViewQueryTooltipCb(self, view, x, y, keyboard_mode, tooltip):
        is_row, x, y, unused_model, path, tree_iter = view.get_tooltip_context(
            x, y, keyboard_mode)
        if not is_row:
            return False

        view.set_tooltip_row(tooltip, path)
        description = self.storemodel.get_value(tree_iter, COL_DESC_TEXT)
        bin_description = self.storemodel.get_value(
            tree_iter, COL_BIN_DESCRIPTION_TEXT)
        tooltip.set_text("%s\n%s" % (bin_description, description))
        return True

    def __updateAll(self, path=None):
        if len(self.clips) == 1:
            self.show()
            self._infobar.hide()
            self._updateTreeview()
            if path:
                self.treeview_selection.select_path(path)
        else:
            self.hide()
            self.__remove_configuration_widget()
            self.storemodel.clear()
            self._infobar.show()

    def _updateTreeview(self):
        self.storemodel.clear()
        clip = self.clips[0]
        for effect in clip.get_top_effects():
            if effect.props.bin_description in HIDDEN_EFFECTS:
                continue
            effect_info = self.app.effects.getInfo(effect.props.bin_description)
            to_append = [effect.props.active]
            track_type = effect.get_track_type()
            if track_type == GES.TrackType.AUDIO:
                to_append.append("Audio")
            elif track_type == GES.TrackType.VIDEO:
                to_append.append("Video")
            to_append.append(effect.props.bin_description)
            to_append.append(effect_info.human_name)
            to_append.append(effect_info.description)
            to_append.append(effect)
            self.storemodel.append(to_append)
        self._vbox.set_visible(len(self.storemodel) > 0)

    def _treeviewSelectionChangedCb(self, unused_treeview):
        selection_is_emtpy = self.treeview_selection.count_selected_rows() == 0
        self.remove_effect_action.set_enabled(not selection_is_emtpy)

        self._updateEffectConfigUi()

    def _updateEffectConfigUi(self):
        model, tree_iter = self.treeview_selection.get_selected()
        if tree_iter:
            effect = model.get_value(tree_iter, COL_TRACK_EFFECT)
            self._showEffectConfigurationWidget(effect)
        else:
            self.__remove_configuration_widget()

    def __remove_configuration_widget(self):
        if not self._effect_config_ui:
            # Nothing to remove.
            return

        self._effect_config_ui.deactivate_keyframe_toggle_buttons()
        self._vbox.remove(self._effect_config_ui)
        self._effect_config_ui = None

    def _showEffectConfigurationWidget(self, effect):
        self.__remove_configuration_widget()
        self._effect_config_ui = self.effects_properties_manager.getEffectConfigurationUI(
            effect)
        if not self._effect_config_ui:
            return
        self._effect_config_ui.show()
        self._effect_config_ui.show_all()
        self._vbox.add(self._effect_config_ui)
Esempio n. 5
0
class EffectProperties(Gtk.Expander, Loggable):
    """Widget for viewing a list of effects and configuring them.

    Attributes:
        app (Pitivi): The app.
        clip (GES.Clip): The clip being configured.
    """

    # pylint: disable=too-many-statements
    def __init__(self, app, clip_properties):
        Gtk.Expander.__init__(self)
        self.set_expanded(True)
        self.set_label(_("Effects"))
        Loggable.__init__(self)

        # Global variables related to effects
        self.app = app

        self._project = None
        self._selection = None
        self.clip = None
        self._effect_config_ui = None
        self.effects_properties_manager = EffectsPropertiesManager(app)
        setup_custom_effect_widgets(self.effects_properties_manager)
        self.clip_properties = clip_properties

        no_effect_label = Gtk.Label(
            _("To apply an effect to the clip, drag it from the Effect Library."))
        no_effect_label.set_line_wrap(True)
        self.no_effect_infobar = Gtk.InfoBar()
        fix_infobar(self.no_effect_infobar)
        self.no_effect_infobar.props.message_type = Gtk.MessageType.OTHER
        self.no_effect_infobar.get_content_area().add(no_effect_label)

        # The toolbar that will go between the list of effects and properties
        buttons_box = Gtk.ButtonBox()
        buttons_box.set_halign(Gtk.Align.END)
        buttons_box.set_margin_end(SPACING)
        buttons_box.props.margin_top = SPACING / 2

        remove_effect_button = Gtk.Button()
        remove_icon = Gtk.Image.new_from_icon_name("list-remove-symbolic",
                                                   Gtk.IconSize.BUTTON)
        remove_effect_button.set_image(remove_icon)
        remove_effect_button.set_always_show_image(True)
        remove_effect_button.set_label(_("Remove effect"))
        buttons_box.pack_start(remove_effect_button,
                               expand=False, fill=False, padding=0)

        # We need to specify Gtk.TreeDragSource because otherwise we are hitting
        # bug https://bugzilla.gnome.org/show_bug.cgi?id=730740.
        class EffectsListStore(Gtk.ListStore, Gtk.TreeDragSource):
            """Just a work around!"""
            # pylint: disable=non-parent-init-called
            def __init__(self, *args):
                Gtk.ListStore.__init__(self, *args)
                # Set the source index on the storemodel directly,
                # to avoid issues with the selection_data API.
                # FIXME: Work around
                # https://bugzilla.gnome.org/show_bug.cgi?id=737587
                self.source_index = None

            def do_drag_data_get(self, path, unused_selection_data):
                self.source_index = path.get_indices()[0]

        self.storemodel = EffectsListStore(bool, str, str, str, str, object)
        self.treeview = Gtk.TreeView(model=self.storemodel)
        self.treeview.set_property("has_tooltip", True)
        self.treeview.set_headers_visible(False)
        self.treeview.props.margin_top = SPACING
        self.treeview.props.margin_left = SPACING
        # Without this, the treeview hides the border of its parent.
        # I should file a bug about this.
        self.treeview.props.margin_right = 1

        activated_cell = Gtk.CellRendererToggle()
        activated_cell.props.xalign = 0
        activated_cell.props.xpad = 0
        activated_cell.connect("toggled", self._effectActiveToggleCb)
        self.treeview.insert_column_with_attributes(-1,
                                                    _("Active"), activated_cell,
                                                    active=COL_ACTIVATED)

        type_col = Gtk.TreeViewColumn(_("Type"))
        type_col.set_spacing(SPACING)
        type_col.set_sizing(Gtk.TreeViewColumnSizing.AUTOSIZE)
        type_cell = Gtk.CellRendererText()
        type_cell.props.xpad = PADDING
        type_col.pack_start(type_cell, expand=True)
        type_col.add_attribute(type_cell, "text", COL_TYPE)
        self.treeview.append_column(type_col)

        name_col = Gtk.TreeViewColumn(_("Effect name"))
        name_col.set_spacing(SPACING)
        name_cell = Gtk.CellRendererText()
        name_cell.props.xpad = PADDING
        name_cell.set_property("ellipsize", Pango.EllipsizeMode.END)
        name_col.pack_start(name_cell, expand=True)
        name_col.add_attribute(name_cell, "text", COL_NAME_TEXT)
        self.treeview.append_column(name_col)

        # Allow the entire expander to accept EFFECT_TARGET_ENTRY when
        # drag&dropping.
        self.drag_dest_set(Gtk.DestDefaults.DROP, [EFFECT_TARGET_ENTRY],
                           Gdk.DragAction.COPY)

        # Allow also the treeview to accept EFFECT_TARGET_ENTRY when
        # drag&dropping so the effect can be dragged at a specific position.
        self.treeview.enable_model_drag_dest([EFFECT_TARGET_ENTRY],
                                             Gdk.DragAction.COPY)

        # Enable reordering by drag&drop.
        self.treeview.enable_model_drag_source(Gdk.ModifierType.BUTTON1_MASK,
                                               [EFFECT_TARGET_ENTRY],
                                               Gdk.DragAction.MOVE)

        self.treeview_selection = self.treeview.get_selection()
        self.treeview_selection.set_mode(Gtk.SelectionMode.SINGLE)

        self._infobar = clip_properties.createInfoBar(
            _("Select a clip on the timeline to configure its associated effects"))
        self._infobar.show_all()

        # Prepare the main container widgets and lay out everything
        self._expander_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
        self._vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
        self._vbox.pack_start(self.treeview, expand=False, fill=False, padding=0)
        self._vbox.pack_start(buttons_box, expand=False, fill=False, padding=0)
        separator = Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL)
        separator.set_margin_top(SPACING)
        separator.set_margin_left(SPACING)
        separator.set_margin_right(SPACING)
        self._vbox.pack_start(separator, expand=False, fill=False, padding=0)
        self._vbox.show_all()
        self._expander_box.pack_start(self.no_effect_infobar, expand=False, fill=False, padding=0)
        self._expander_box.pack_start(self._vbox, expand=False, fill=False, padding=0)
        self._expander_box.show_all()
        self.add(self._expander_box)
        self.hide()

        effects_actions_group = Gio.SimpleActionGroup()
        self.treeview.insert_action_group("clipproperties-effects", effects_actions_group)
        buttons_box.insert_action_group("clipproperties-effects", effects_actions_group)
        self.app.shortcuts.register_group("clipproperties-effects", _("Clip Effects"), position=60)

        self.remove_effect_action = Gio.SimpleAction.new("remove-effect", None)
        self.remove_effect_action.connect("activate", self._removeEffectCb)
        effects_actions_group.add_action(self.remove_effect_action)
        self.app.shortcuts.add("clipproperties-effects.remove-effect", ["Delete"],
                               _("Remove the selected effect"))
        self.remove_effect_action.set_enabled(False)
        remove_effect_button.set_action_name("clipproperties-effects.remove-effect")

        # Connect all the widget signals
        self.treeview_selection.connect("changed", self._treeviewSelectionChangedCb)
        self.connect("drag-motion", self._drag_motion_cb)
        self.connect("drag-leave", self._drag_leave_cb)
        self.connect("drag-data-received", self._drag_data_received_cb)
        self.treeview.connect("drag-motion", self._drag_motion_cb)
        self.treeview.connect("drag-leave", self._drag_leave_cb)
        self.treeview.connect("drag-data-received", self._drag_data_received_cb)
        self.treeview.connect("query-tooltip", self._treeViewQueryTooltipCb)
        self.app.project_manager.connect_after(
            "new-project-loaded", self._newProjectLoadedCb)
        self.connect('notify::expanded', self._expandedCb)

    def _newProjectLoadedCb(self, unused_app, project):
        if self._selection is not None:
            self._selection.disconnect_by_func(self._selectionChangedCb)
            self._selection = None
        self._project = project
        if project:
            self._selection = project.ges_timeline.ui.selection
            self._selection.connect('selection-changed', self._selectionChangedCb)
        self.__updateAll()

    def _selectionChangedCb(self, selection):
        if self.clip:
            self.clip.disconnect_by_func(self._trackElementAddedCb)
            self.clip.disconnect_by_func(self._trackElementRemovedCb)
            for track_element in self.clip.get_children(recursive=True):
                if isinstance(track_element, GES.BaseEffect):
                    self._disconnect_from_track_element(track_element)

        clips = list(selection.selected)
        self.clip = clips[0] if len(clips) == 1 else None
        if self.clip:
            self.clip.connect("child-added", self._trackElementAddedCb)
            self.clip.connect("child-removed", self._trackElementRemovedCb)
            for track_element in self.clip.get_children(recursive=True):
                if isinstance(track_element, GES.BaseEffect):
                    self._connect_to_track_element(track_element)
        self.__updateAll()

    def _trackElementAddedCb(self, unused_clip, track_element):
        if isinstance(track_element, GES.BaseEffect):
            self._connect_to_track_element(track_element)
            self.__updateAll()
            for path, row in enumerate(self.storemodel):
                if row[COL_TRACK_EFFECT] == track_element:
                    self.treeview_selection.select_path(path)
                    break

    def _connect_to_track_element(self, track_element):
        track_element.connect("notify::active", self._notify_active_cb)
        track_element.connect("notify::priority", self._notify_priority_cb)

    def _disconnect_from_track_element(self, track_element):
        track_element.disconnect_by_func(self._notify_active_cb)
        track_element.disconnect_by_func(self._notify_priority_cb)

    def _notify_active_cb(self, unused_track_element, unused_param_spec):
        self._updateTreeview()

    def _notify_priority_cb(self, unused_track_element, unused_param_spec):
        self._updateTreeview()

    def _trackElementRemovedCb(self, unused_clip, track_element):
        if isinstance(track_element, GES.BaseEffect):
            self._disconnect_from_track_element(track_element)
            self.__updateAll()

    def _removeEffectCb(self, unused_action, unused_param):
        selected = self.treeview_selection.get_selected()
        if not selected[1]:
            # Cannot remove nothing,
            return
        effect = self.storemodel.get_value(selected[1], COL_TRACK_EFFECT)
        selection_path = self.storemodel.get_path(selected[1])
        # Preserve selection in the tree view.
        next_selection_index = selection_path.get_indices()[0]
        effect_count = self.storemodel.iter_n_children()
        if effect_count - 1 == next_selection_index:
            next_selection_index -= 1
        self._removeEffect(effect)
        if next_selection_index >= 0:
            self.treeview_selection.select_path(next_selection_index)

    def _removeEffect(self, effect):
        pipeline = self._project.pipeline
        with self.app.action_log.started("remove effect",
                                         finalizing_action=CommitTimelineFinalizingAction(pipeline),
                                         toplevel=True):
            self.__remove_configuration_widget()
            self.effects_properties_manager.cleanCache(effect)
            effect.get_parent().remove(effect)

    def _drag_motion_cb(self, unused_widget, unused_drag_context, unused_x, unused_y, unused_timestamp):
        """Highlights some widgets to indicate it can receive drag&drop."""
        self.debug(
            "Something is being dragged in the clip properties' effects list")
        self.no_effect_infobar.drag_highlight()
        # It would be nicer to highlight only the treeview, but
        # it does not seem to have a visible effect.
        self._vbox.drag_highlight()

    def _drag_leave_cb(self, unused_widget, unused_drag_context, unused_timestamp):
        """Unhighlights the widgets which can receive drag&drop."""
        self.debug(
            "The item being dragged has left the clip properties' effects list")
        self.no_effect_infobar.drag_unhighlight()
        self._vbox.drag_unhighlight()

    # pylint: disable=too-many-arguments
    def _drag_data_received_cb(self, widget, drag_context, x, y, selection_data, unused_info, timestamp):
        if not self.clip:
            # Indicate that a drop will not be accepted.
            Gdk.drag_status(drag_context, 0, timestamp)
            return

        dest_row = self.treeview.get_dest_row_at_pos(x, y)
        if drag_context.get_suggested_action() == Gdk.DragAction.COPY:
            # An effect dragged probably from the effects list.
            factory_name = str(selection_data.get_data(), "UTF-8")
            if widget is self.treeview:
                drop_index = self.__get_new_effect_index(dest_row)
            else:
                drop_index = len(self.storemodel)
            self.debug("Effect dragged at position %s", drop_index)
            effect_info = self.app.effects.getInfo(factory_name)
            pipeline = self._project.pipeline
            with self.app.action_log.started("add effect",
                                             finalizing_action=CommitTimelineFinalizingAction(pipeline),
                                             toplevel=True):
                effect = self.clip.ui.add_effect(effect_info)
                if effect:
                    self.clip.set_top_effect_index(effect, drop_index)
        elif drag_context.get_suggested_action() == Gdk.DragAction.MOVE:
            # An effect dragged from the same treeview to change its position.
            # Source
            source_index, drop_index = self.__get_move_indexes(
                dest_row, self.treeview.get_model())
            self.__move_effect(self.clip, source_index, drop_index)

        drag_context.finish(True, False, timestamp)

    # pylint: disable=no-self-use
    def __get_new_effect_index(self, dest_row):
        # Target
        if dest_row:
            drop_path, drop_pos = dest_row
            drop_index = drop_path.get_indices()[0]
            if drop_pos != Gtk.TreeViewDropPosition.BEFORE:
                drop_index += 1
        else:
            # This should happen when dragging after the last row.
            drop_index = None

        return drop_index

    def __get_move_indexes(self, dest_row, model):
        source_index = self.storemodel.source_index
        self.storemodel.source_index = None

        # Target
        if dest_row:
            drop_path, drop_pos = dest_row
            drop_index = drop_path.get_indices()[0]
            drop_index = self.calculateEffectPriority(
                source_index, drop_index, drop_pos)
        else:
            # This should happen when dragging after the last row.
            drop_index = len(model) - 1

        return source_index, drop_index

    def __move_effect(self, clip, source_index, drop_index):
        if source_index == drop_index:
            # Noop.
            return
        # The paths are different.
        effects = clip.get_top_effects()
        effect = effects[source_index]
        pipeline = self._project.ges_timeline.get_parent()
        with self.app.action_log.started("move effect",
                                         finalizing_action=CommitTimelineFinalizingAction(pipeline),
                                         toplevel=True):
            clip.set_top_effect_index(effect, drop_index)

        new_path = Gtk.TreePath.new()
        new_path.append_index(drop_index)
        self.__updateAll(path=new_path)

    @staticmethod
    def calculateEffectPriority(source_index, drop_index, drop_pos):
        """Calculates where the effect from source_index will end up."""
        if drop_pos in (Gtk.TreeViewDropPosition.INTO_OR_BEFORE, Gtk.TreeViewDropPosition.INTO_OR_AFTER):
            return drop_index
        if drop_pos == Gtk.TreeViewDropPosition.BEFORE:
            if source_index < drop_index:
                return drop_index - 1
        elif drop_pos == Gtk.TreeViewDropPosition.AFTER:
            if source_index > drop_index:
                return drop_index + 1
        return drop_index

    def _effectActiveToggleCb(self, cellrenderertoggle, path):
        _iter = self.storemodel.get_iter(path)
        effect = self.storemodel.get_value(_iter, COL_TRACK_EFFECT)
        pipeline = self._project.ges_timeline.get_parent()
        with self.app.action_log.started("change active state",
                                         finalizing_action=CommitTimelineFinalizingAction(pipeline),
                                         toplevel=True):
            effect.props.active = not effect.props.active
        # This is not strictly necessary, but makes sure
        # the UI reflects the current status.
        cellrenderertoggle.set_active(effect.is_active())

    def _expandedCb(self, unused_expander, unused_params):
        self.__updateAll()

    def _treeViewQueryTooltipCb(self, view, x, y, keyboard_mode, tooltip):
        is_row, x, y, unused_model, path, tree_iter = view.get_tooltip_context(
            x, y, keyboard_mode)
        if not is_row:
            return False

        view.set_tooltip_row(tooltip, path)
        description = self.storemodel.get_value(tree_iter, COL_DESC_TEXT)
        bin_description = self.storemodel.get_value(
            tree_iter, COL_BIN_DESCRIPTION_TEXT)
        tooltip.set_text("%s\n%s" % (bin_description, description))
        return True

    def __updateAll(self, path=None):
        if self.clip:
            self.show()
            self._infobar.hide()
            self._updateTreeview()
            if path:
                self.treeview_selection.select_path(path)
        else:
            self.hide()
            self.__remove_configuration_widget()
            self.storemodel.clear()
            self._infobar.show()

    def _updateTreeview(self):
        self.storemodel.clear()
        for effect in self.clip.get_top_effects():
            if effect.props.bin_description in HIDDEN_EFFECTS:
                continue
            effect_info = self.app.effects.getInfo(effect.props.bin_description)
            to_append = [effect.props.active]
            track_type = effect.get_track_type()
            if track_type == GES.TrackType.AUDIO:
                to_append.append("Audio")
            elif track_type == GES.TrackType.VIDEO:
                to_append.append("Video")
            to_append.append(effect.props.bin_description)
            to_append.append(effect_info.human_name)
            to_append.append(effect_info.description)
            to_append.append(effect)
            self.storemodel.append(to_append)
        has_effects = len(self.storemodel) > 0
        self.no_effect_infobar.set_visible(not has_effects)
        self._vbox.set_visible(has_effects)

    def _treeviewSelectionChangedCb(self, unused_treeview):
        selection_is_emtpy = self.treeview_selection.count_selected_rows() == 0
        self.remove_effect_action.set_enabled(not selection_is_emtpy)

        self._updateEffectConfigUi()

    def _updateEffectConfigUi(self):
        model, tree_iter = self.treeview_selection.get_selected()
        if tree_iter:
            effect = model.get_value(tree_iter, COL_TRACK_EFFECT)
            self._showEffectConfigurationWidget(effect)
        else:
            self.__remove_configuration_widget()

    def __remove_configuration_widget(self):
        if not self._effect_config_ui:
            # Nothing to remove.
            return

        self._effect_config_ui.deactivate_keyframe_toggle_buttons()
        self._vbox.remove(self._effect_config_ui)
        self._effect_config_ui = None

    def _showEffectConfigurationWidget(self, effect):
        self.__remove_configuration_widget()
        self._effect_config_ui = self.effects_properties_manager.getEffectConfigurationUI(
            effect)
        if not self._effect_config_ui:
            return
        self._effect_config_ui.show()
        self._effect_config_ui.show_all()
        self._vbox.add(self._effect_config_ui)