[pitivi] effects: Allow reordering the effects of a clip



commit 8bc597f7ed671c1aecad57e67b6c79819d41b582
Author: Alexandru Băluț <alexandru balut gmail com>
Date:   Fri May 30 22:58:06 2014 +0200

    effects: Allow reordering the effects of a clip

 pitivi/clipproperties.py     |  143 +++++++++++++++++++++++++++++++++---------
 pitivi/effects.py            |   46 ++++----------
 tests/Makefile.am            |    1 +
 tests/test_clipproperties.py |   48 ++++++++++++++
 4 files changed, 174 insertions(+), 64 deletions(-)
---
diff --git a/pitivi/clipproperties.py b/pitivi/clipproperties.py
index 8d81036..861381a 100644
--- a/pitivi/clipproperties.py
+++ b/pitivi/clipproperties.py
@@ -20,6 +20,7 @@
 # Boston, MA 02110-1301, USA.
 
 import os
+import pickle
 
 from gi.repository import Gtk
 from gi.repository import Gdk
@@ -179,7 +180,14 @@ class EffectProperties(Gtk.Expander, Loggable):
                                            Gtk.PolicyType.AUTOMATIC)
         self.treeview_scrollwin.set_shadow_type(Gtk.ShadowType.ETCHED_IN)
 
-        self.storemodel = Gtk.ListStore(bool, str, str, str, str, object)
+        # 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):
+            def do_drag_data_get(self, path, selection_data):
+                data = pickle.dumps(path.get_indices())
+                selection_data.set(Gdk.Atom.intern("pitivi/effect", False), 0, data)
+
+        self.storemodel = EffectsListStore(bool, str, str, str, str, object)
         self.treeview = Gtk.TreeView(model=self.storemodel)
         self.treeview_scrollwin.add(self.treeview)
         self.treeview.set_property("has_tooltip", True)
@@ -211,10 +219,15 @@ class EffectProperties(Gtk.Expander, Loggable):
         namecol.add_attribute(namecell, "text", COL_NAME_TEXT)
         self.treeview.append_column(namecol)
 
-        self.treeview.drag_dest_set(Gtk.DestDefaults.ALL,
-            [EFFECT_TARGET_ENTRY], Gdk.DragAction.COPY)
+        # 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.drag_dest_add_text_targets()
         self.selection = self.treeview.get_selection()
 
         self._infobar = clip_properties.createInfoBar(
@@ -235,9 +248,9 @@ class EffectProperties(Gtk.Expander, Loggable):
 
         # Connect all the widget signals
         self.selection.connect("changed", self._treeviewSelectionChangedCb)
-        self.treeview.connect("drag-leave", self._dragLeaveCb)
-        self.treeview.connect("drag-drop", self._dragDropCb)
         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._vcontent.connect("notify", self._vcontentNotifyCb)
         removeEffectButton.connect("clicked", self._removeEffectCb)
@@ -314,32 +327,36 @@ class EffectProperties(Gtk.Expander, Loggable):
         self._removeEffectConfigurationWidget()
         self.effects_properties_manager.cleanCache(effect)
         effect.get_parent().remove(effect)
-        self._updateTreeview()
         self.app.action_log.commit()
+        self._updateTreeview()
 
-    def addEffectToClip(self, clip, bin_desc):
-        media_type = self.app.effects.getFactoryFromName(bin_desc).media_type
-
+    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.getFactoryFromName(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:
-                effect = GES.Effect.new(bin_description=bin_desc)
+                # Actually add the effect
+                self.app.action_log.begin("add effect")
+                effect = GES.Effect.new(bin_description=factory_name)
                 clip.add(effect)
-                self.updateAll()
+                if priority is not None and priority < len(model):
+                    clip.set_top_effect_priority(effect, priority)
                 self.app.project_manager.current_project.timeline.commit()
                 self.app.action_log.commit()
                 self.app.project_manager.current_project.pipeline.flushSeek()
+                self.updateAll()
                 break
 
-    def addEffectToCurrentSelection(self, bin_desc):
-        if self.clips:
-            # Trying to apply effect only on the first object of the selection
-            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, bin_desc)
+    def addEffectToCurrentSelection(self, factory_name):
+        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)
 
     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")
@@ -349,9 +366,75 @@ class EffectProperties(Gtk.Expander, Loggable):
         self.info("The item being dragged has left the clip properties' effects list")
         self.drag_unhighlight()
 
-    def _dragDropCb(self, unused_tree_view, unused_drag_context, unused_x, unused_y, unused_timestamp):
-        self.info("An item has been dropped onto the clip properties' effects list")
-        self.addEffectToCurrentSelection(self.app.gui.effectlist.getSelectedItems())
+    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]
+        model = treeview.get_model()
+        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")
+            # Target
+            dest_row = treeview.get_dest_row_at_pos(x, y)
+            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
+            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_indices = pickle.loads(selection_data.get_data())
+            source_index = source_indices[0]
+            # Target
+            dest_row = treeview.get_dest_row_at_pos(x, y)
+            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
+            self.moveEffect(clip, source_index, drop_index)
+        drag_context.finish(True, False, timestamp)
+
+    def moveEffect(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.app.project_manager.current_project.timeline.commit()
+        self.app.action_log.commit()
+        self.app.project_manager.current_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)
@@ -377,12 +460,13 @@ class EffectProperties(Gtk.Expander, Loggable):
         tooltip.set_text("%s\n%s" % (bin_description, description))
         return True
 
-    def updateAll(self):
+    def updateAll(self, path=None):
         if self.get_expanded():
             if len(self.clips) == 1:
                 self._setEffectDragable()
                 self._updateTreeview()
-                self._updateEffectConfigUi()
+                if path:
+                    self.selection.select_path(path)
             else:
                 self._removeEffectConfigurationWidget()
                 self.storemodel.clear()
@@ -393,9 +477,8 @@ class EffectProperties(Gtk.Expander, Loggable):
 
     def _updateTreeview(self):
         self.storemodel.clear()
-
-        obj = self.clips[0]
-        for effect in obj.get_top_effects():
+        clip = self.clips[0]
+        for effect in clip.get_top_effects():
             if effect.props.bin_description in HIDDEN_EFFECTS:
                 continue
             asset = self.app.effects.getFactoryFromName(
@@ -431,9 +514,9 @@ class EffectProperties(Gtk.Expander, Loggable):
             if self._config_ui_h_pos is None:
                 self._config_ui_h_pos = self.app.gui.settings.mainWindowHeight // 3
 
-        tree_iter = self.selection.get_selected()[1]
+        model, tree_iter = self.selection.get_selected()
         if tree_iter:
-            effect = self.storemodel.get_value(tree_iter, COL_TRACK_EFFECT)
+            effect = model.get_value(tree_iter, COL_TRACK_EFFECT)
             self._showEffectConfigurationWidget(effect)
         else:
             self._removeEffectConfigurationWidget()
diff --git a/pitivi/effects.py b/pitivi/effects.py
index c82d26b..98e83f6 100644
--- a/pitivi/effects.py
+++ b/pitivi/effects.py
@@ -36,7 +36,6 @@ Effects global handling
 """
 import re
 import os
-import time
 
 from gi.repository import GLib
 from gi.repository import Gst
@@ -51,7 +50,7 @@ from pitivi.configure import get_ui_dir, get_pixmap_dir
 from pitivi.settings import GlobalSettings
 
 from pitivi.utils.loggable import Loggable
-from pitivi.utils.ui import SPACING, TYPE_PITIVI_EFFECT
+from pitivi.utils.ui import EFFECT_TARGET_ENTRY, SPACING
 
 from pitivi.utils.widgets import GstElementSettingsWidget, FractionWidget
 
@@ -75,7 +74,7 @@ class EffectFactory():
     """
     def __init__(self, effect_name, media_type, categories=[_("Uncategorized")],
                 human_name="", description="", icon=None):
-        self.effec_tname = effect_name
+        self.effect_name = effect_name
         self.media_type = media_type
         self.categories = categories
         self.description = description
@@ -407,17 +406,11 @@ class EffectListWidget(Gtk.VBox, Loggable):
         self.view.append_column(icon_col)
         self.view.append_column(text_col)
 
-        self.view.drag_source_set(0, [], Gdk.DragAction.COPY)
-        self.view.enable_model_drag_source(Gdk.ModifierType.BUTTON1_MASK, [("pitivi/effect", 0, 
TYPE_PITIVI_EFFECT)], Gdk.DragAction.COPY)
-        #self.view.drag_source_set_target_list([("pitivi/effect", 0, TYPE_PITIVI_EFFECT)])
-        self.view.drag_source_set_target_list([])
-        self.view.drag_source_add_uri_targets()
-        self.view.drag_source_add_text_targets()
+        # 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._buttonPressEventCb)
         self.view.connect("select-cursor-row", self._enterPressEventCb)
-        self.view.connect("drag-begin", self._dndDragBeginCb)
-        self.view.connect("drag-end", self._dndDragEndCb)
         self.view.connect("drag-data-get", self._dndDragDataGetCb)
 
         scrollwin = Gtk.ScrolledWindow()
@@ -479,24 +472,10 @@ class EffectListWidget(Gtk.VBox, Loggable):
 
         self.categoriesWidget.set_active(0)
 
-    def _dndDragBeginCb(self, unused_view, context):
-        self.info("Drag operation begun")
-        model, paths = self.view.get_selection().get_selected_rows()
-
-        if not paths:
-            context.drag_abort(int(time.time()))
-            return
-
-        path = paths[0]
-        pixbuf = model.get_value(model.get_iter(path), COL_ICON)
-        if pixbuf:
-            Gtk.drag_set_icon_pixbuf(context, pixbuf, 0, 0)
-
-    def _dndDragEndCb(self, unused_view, unused_context):
-        self.info("Drag operation ended")
-
-    def _dndDragDataGetCb(self, unused_view, unused_context, data, unused_info, unused_timestamp):
-        data.set_uris([self.getSelectedItems()])
+    def _dndDragDataGetCb(self, unused_view, drag_context, selection_data, unused_info, unused_timestamp):
+        factory_name = bytes(self.getSelectedEffectFactoryName(), "UTF-8")
+        selection_data.set(drag_context.list_targets()[0], 0, factory_name)
+        return True
 
     def _rowUnderMouseSelected(self, view, event):
         result = view.get_path_at_pos(int(event.x), int(event.y))
@@ -508,7 +487,7 @@ class EffectListWidget(Gtk.VBox, Loggable):
         return False
 
     def _enterPressEventCb(self, unused_view, unused_event=None):
-        factory_name = self.getSelectedItems()
+        factory_name = self.getSelectedEffectFactoryName()
         if factory_name is not None:
             self.app.gui.clipconfig.effect_expander.addEffectToCurrentSelection(factory_name)
 
@@ -518,7 +497,7 @@ class EffectListWidget(Gtk.VBox, Loggable):
         if event.button == 3:
             chain_up = False
         elif event.type == getattr(Gdk.EventType, '2BUTTON_PRESS'):
-            factory_name = self.getSelectedItems()
+            factory_name = self.getSelectedEffectFactoryName()
             if factory_name is not None:
                 self.app.gui.clipconfig.effect_expander.addEffectToCurrentSelection(factory_name)
         else:
@@ -527,17 +506,16 @@ class EffectListWidget(Gtk.VBox, Loggable):
         if chain_up:
             self._draggedItems = None
         else:
-            self._draggedItems = self.getSelectedItems()
+            self._draggedItems = self.getSelectedEffectFactoryName()
 
         Gtk.TreeView.do_button_press_event(view, event)
         return True
 
-    def getSelectedItems(self):
+    def getSelectedEffectFactoryName(self):
         if self._draggedItems:
             return self._draggedItems
         model, rows = self.view.get_selection().get_selected_rows()
         path = self.modelFilter.convert_path_to_child_path(rows[0])
-
         return self.storemodel[path][COL_ELEMENT_NAME]
 
     def _toggleViewTypeCb(self, widget):
diff --git a/tests/Makefile.am b/tests/Makefile.am
index 63478af..180ffd9 100644
--- a/tests/Makefile.am
+++ b/tests/Makefile.am
@@ -5,6 +5,7 @@
 tests =        \
        test_application.py \
        test_check.py \
+       test_clipproperties.py \
        test_common.py \
        test_log.py \
        test_mainwindow.py \
diff --git a/tests/test_clipproperties.py b/tests/test_clipproperties.py
new file mode 100644
index 0000000..d4d7fac
--- /dev/null
+++ b/tests/test_clipproperties.py
@@ -0,0 +1,48 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2014, Alex Băluț <alexandru balut gmail com>
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this program; if not, write to the
+# Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
+# Boston, MA 02110-1301, USA.
+
+import unittest
+
+from gi.repository import Gtk
+
+from pitivi.clipproperties import EffectProperties
+
+
+class EffectPropertiesTest(unittest.TestCase):
+
+    def testCalculateEffectPriority(self):
+        # Dragging 1 onto itself and nearby.
+        self.assertEqual(1, EffectProperties.calculateEffectPriority(1, 0, Gtk.TreeViewDropPosition.AFTER))
+        self.assertEqual(1, EffectProperties.calculateEffectPriority(1, 1, Gtk.TreeViewDropPosition.BEFORE))
+        self.assertEqual(1, EffectProperties.calculateEffectPriority(1, 1, 
Gtk.TreeViewDropPosition.INTO_OR_BEFORE))
+        self.assertEqual(1, EffectProperties.calculateEffectPriority(1, 1, 
Gtk.TreeViewDropPosition.INTO_OR_AFTER))
+        self.assertEqual(1, EffectProperties.calculateEffectPriority(1, 1, Gtk.TreeViewDropPosition.AFTER))
+        self.assertEqual(1, EffectProperties.calculateEffectPriority(1, 2, Gtk.TreeViewDropPosition.BEFORE))
+
+        # Dragging 0 and 3 between rows 1 and 2.
+        self.assertEqual(1, EffectProperties.calculateEffectPriority(0, 1, Gtk.TreeViewDropPosition.AFTER))
+        self.assertEqual(1, EffectProperties.calculateEffectPriority(0, 2, Gtk.TreeViewDropPosition.BEFORE))
+        self.assertEqual(2, EffectProperties.calculateEffectPriority(3, 1, Gtk.TreeViewDropPosition.AFTER))
+        self.assertEqual(2, EffectProperties.calculateEffectPriority(3, 2, Gtk.TreeViewDropPosition.BEFORE))
+
+        # Dragging 0 and 2 onto 1.
+        self.assertEqual(1, EffectProperties.calculateEffectPriority(0, 1, 
Gtk.TreeViewDropPosition.INTO_OR_BEFORE))
+        self.assertEqual(1, EffectProperties.calculateEffectPriority(0, 1, 
Gtk.TreeViewDropPosition.INTO_OR_AFTER))
+        self.assertEqual(1, EffectProperties.calculateEffectPriority(2, 1, 
Gtk.TreeViewDropPosition.INTO_OR_BEFORE))
+        self.assertEqual(1, EffectProperties.calculateEffectPriority(2, 1, 
Gtk.TreeViewDropPosition.INTO_OR_AFTER))


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]