[pitivi/1.0] timeline: Allow selecting a range of clips



commit 9f0c0a35b5ddbc8e45c43abcca065cf1a8fec7ff
Author: Harish Fulara <harish14143 iiitd ac in>
Date:   Fri Mar 2 22:50:02 2018 +0530

    timeline: Allow selecting a range of clips
    
    Fixes #1399

 pitivi/timeline/elements.py     |   9 +-
 pitivi/timeline/timeline.py     | 138 ++++++++++++++++++++-----------
 tests/common.py                 |  12 ++-
 tests/test_timeline_timeline.py | 179 ++++++++++++++++++++++++++++++++++++++++
 4 files changed, 282 insertions(+), 56 deletions(-)
---
diff --git a/pitivi/timeline/elements.py b/pitivi/timeline/elements.py
index 97bebd32..818f3c50 100644
--- a/pitivi/timeline/elements.py
+++ b/pitivi/timeline/elements.py
@@ -1070,13 +1070,12 @@ class Clip(Gtk.EventBox, Zoomable, Loggable):
                 self.timeline.current_group.remove(
                     self.ges_clip.get_toplevel_parent())
                 mode = UNSELECT
-        elif not self.get_state_flags() & Gtk.StateFlags.SELECTED:
-            self.timeline.resetSelectionGroup()
-            self.timeline.current_group.add(
-                self.ges_clip.get_toplevel_parent())
-            self.app.gui.switchContextTab(self.ges_clip)
+            clicked_layer, click_pos = self.timeline.get_clicked_layer_and_pos(event)
+            self.timeline.set_selection_meta_info(clicked_layer, click_pos, mode)
         else:
             self.timeline.resetSelectionGroup()
+            self.timeline.current_group.add(self.ges_clip.get_toplevel_parent())
+            self.app.gui.switchContextTab(self.ges_clip)
 
         parent = self.ges_clip.get_parent()
         if parent == self.timeline.current_group or parent is None:
diff --git a/pitivi/timeline/timeline.py b/pitivi/timeline/timeline.py
index e6b9ce49..823203b6 100644
--- a/pitivi/timeline/timeline.py
+++ b/pitivi/timeline/timeline.py
@@ -47,6 +47,7 @@ from pitivi.utils.timeline import SELECT
 from pitivi.utils.timeline import SELECT_ADD
 from pitivi.utils.timeline import Selection
 from pitivi.utils.timeline import TimelineError
+from pitivi.utils.timeline import UNSELECT
 from pitivi.utils.timeline import Zoomable
 from pitivi.utils.ui import EFFECT_TARGET_ENTRY
 from pitivi.utils.ui import LAYER_HEIGHT
@@ -129,6 +130,8 @@ class Marquee(Gtk.Box, Loggable):
         """Hides and resets the widget."""
         self.start_x = None
         self.start_y = None
+        self.end_x = None
+        self.end_y = None
         self.props.height_request = -1
         self.props.width_request = -1
         self.set_visible(False)
@@ -154,15 +157,15 @@ class Marquee(Gtk.Box, Loggable):
                 the coordinates of the second corner.
         """
         event_widget = Gtk.get_event_widget(event)
-        x, y = event_widget.translate_coordinates(
+        self.end_x, self.end_y = event_widget.translate_coordinates(
             self._timeline.layout.layers_vbox, event.x, event.y)
 
-        start_x = min(x, self.start_x)
-        start_y = min(y, self.start_y)
+        x = min(self.start_x, self.end_x)
+        y = min(self.start_y, self.end_y)
 
-        self.get_parent().move(self, start_x, start_y)
-        self.props.width_request = abs(self.start_x - x)
-        self.props.height_request = abs(self.start_y - y)
+        self.get_parent().move(self, x, y)
+        self.props.width_request = abs(self.start_x - self.end_x)
+        self.props.height_request = abs(self.start_y - self.end_y)
         self.set_visible(True)
 
     def find_clips(self):
@@ -171,49 +174,13 @@ class Marquee(Gtk.Box, Loggable):
         Returns:
             List[GES.Clip]: The clips under the marquee.
         """
-        x = self._timeline.layout.child_get_property(self, "x")
-        res = set()
+        start_layer = self._timeline._get_layer_at(self.start_y)[0]
+        end_layer = self._timeline._get_layer_at(self.end_y)[0]
+        start_pos = max(0, self._timeline.pixelToNs(self.start_x))
+        end_pos = max(0, self._timeline.pixelToNs(self.end_x))
 
-        w = self.props.width_request
-        for layer in self._timeline.ges_timeline.get_layers():
-            intersects, unused_rect = layer.ui.get_allocation().intersect(self.get_allocation())
-            if not intersects:
-                continue
-
-            for clip in layer.get_clips():
-                if not self.contains(clip, x, w):
-                    continue
-
-                toplevel = clip.get_toplevel_parent()
-                if isinstance(toplevel, GES.Group) and toplevel != self._timeline.current_group:
-                    res.update([c for c in toplevel.get_children(True)
-                                if isinstance(c, GES.Clip)])
-                else:
-                    res.add(clip)
-
-        self.debug("Result is %s", res)
-
-        return tuple(res)
-
-    def contains(self, clip, marquee_start, marquee_width):
-        if clip.ui is None:
-            return False
-
-        child_start = clip.ui.get_parent().child_get_property(clip.ui, "x")
-        child_end = child_start + clip.ui.get_allocation().width
-
-        marquee_end = marquee_start + marquee_width
-
-        if child_start <= marquee_start <= child_end:
-            return True
-
-        if child_start <= marquee_end <= child_end:
-            return True
-
-        if marquee_start <= child_start and marquee_end >= child_end:
-            return True
-
-        return False
+        return self._timeline.get_clips_in_between(start_layer,
+            end_layer, start_pos, end_pos)
 
 
 class LayersLayout(Gtk.Layout, Zoomable, Loggable):
@@ -379,6 +346,10 @@ class Timeline(Gtk.EventBox, Zoomable, Loggable):
         # Clip selection.
         self.selection = Selection()
         self.current_group = None
+        # The last layer where the user clicked.
+        self.last_clicked_layer = None
+        # Position where the user last clicked.
+        self.last_click_pos = 0
         self.resetSelectionGroup()
 
         # Clip editing.
@@ -728,11 +699,82 @@ class Timeline(Gtk.EventBox, Zoomable, Loggable):
         if allow_seek and res and (button == 1 and self.app.settings.leftClickAlsoSeeks):
             self._seek(event)
 
+        if allow_seek and res and button == 1:
+            if self.app.settings.leftClickAlsoSeeks:
+                event_widget = Gtk.get_event_widget(event)
+                if self._getParentOfType(event_widget, LayerControls) is None:
+                    self._seek(event)
+
+            # Allowing group clips selection by shift+clicking anywhere on the timeline.
+            if self.get_parent()._shiftMask:
+                last_clicked_layer = self.last_clicked_layer
+                if not last_clicked_layer:
+                    clicked_layer, click_pos = self.get_clicked_layer_and_pos(event)
+                    self.set_selection_meta_info(clicked_layer, click_pos, SELECT)
+                else:
+                    self.resetSelectionGroup()
+                    last_click_pos = self.last_click_pos
+                    cur_clicked_layer, cur_click_pos = self.get_clicked_layer_and_pos(event)
+                    clips = self.get_clips_in_between(
+                        last_clicked_layer, cur_clicked_layer, last_click_pos, cur_click_pos)
+                    for clip in clips:
+                        self.current_group.add(clip.get_toplevel_parent())
+                    self.selection.setSelection(clips, SELECT)
+            elif not self.get_parent()._controlMask:
+                clicked_layer, click_pos = self.get_clicked_layer_and_pos(event)
+                self.set_selection_meta_info(clicked_layer, click_pos, SELECT)
+
         self._snapEndedCb()
         self.update_visible_overlays()
 
         return False
 
+    def set_selection_meta_info(self, clicked_layer, click_pos, mode):
+        if mode == UNSELECT:
+            self.last_clicked_layer = None
+            self.last_click_pos = 0
+        else:
+            self.last_clicked_layer = clicked_layer
+            self.last_click_pos = click_pos
+
+    def get_clicked_layer_and_pos(self, event):
+        """Gets layer and position in the timeline where user clicked."""
+        event_widget = Gtk.get_event_widget(event)
+        x, y = event_widget.translate_coordinates(self.layout.layers_vbox, event.x, event.y)
+        clicked_layer = self._get_layer_at(y)[0]
+        click_pos = max(0, self.pixelToNs(x))
+        return clicked_layer, click_pos
+
+    def get_clips_in_between(self, layer1, layer2, pos1, pos2):
+        """Gets all clips between pos1 and pos2 within layer1 and layer2."""
+        layers = self.ges_timeline.get_layers()
+        layer1_pos = layer1.props.priority
+        layer2_pos = layer2.props.priority
+
+        if layer2_pos >= layer1_pos:
+            layers_pos = range(layer1_pos, layer2_pos + 1)
+        else:
+            layers_pos = range(layer2_pos, layer1_pos + 1)
+
+        # The interval in which the clips will be selected.
+        start = min(pos1, pos2)
+        end = max(pos1, pos2)
+
+        clips = set()
+        for layer_pos in layers_pos:
+            layer = layers[layer_pos]
+            clips.update(layer.get_clips_in_interval(start, end))
+
+        grouped_clips = set()
+        # Also include those clips which are grouped with currently selected clips.
+        for clip in clips:
+            toplevel = clip.get_toplevel_parent()
+            if isinstance(toplevel, GES.Group) and toplevel != self.current_group:
+                grouped_clips.update([c for c in toplevel.get_children(True)
+                                if isinstance(c, GES.Clip)])
+
+        return clips.union(grouped_clips)
+
     def _motion_notify_event_cb(self, unused_widget, event):
         if self.draggingElement:
             if type(self.draggingElement) == TransitionClip and \
diff --git a/tests/common.py b/tests/common.py
index ea8e091c..67138c00 100644
--- a/tests/common.py
+++ b/tests/common.py
@@ -21,13 +21,13 @@ A collection of objects to use for testing
 """
 import contextlib
 import gc
-import glob
 import os
 import shutil
 import tempfile
 import unittest
 from unittest import mock
 
+from gi.repository import Gdk
 from gi.repository import GLib
 from gi.repository import Gst
 from gi.repository import Gtk
@@ -220,12 +220,18 @@ class TestCase(unittest.TestCase, Loggable):
         self.assertEqual(ges_clip.selected.selected, selected)
 
         # Simulate a click on the clip.
-        event = mock.Mock()
+        event = mock.Mock(spec=Gdk.EventButton)
+        event.x = 0
+        event.y = 0
         event.get_button.return_value = (True, 1)
         with mock.patch.object(Gtk, "get_event_widget") as get_event_widget:
             get_event_widget.return_value = ges_clip.ui
             ges_clip.ui.timeline._button_press_event_cb(None, event)
-        ges_clip.ui._button_release_event_cb(None, event)
+            with mock.patch.object(ges_clip.ui, "translate_coordinates") as translate_coordinates:
+                translate_coordinates.return_value = (0, 0)
+                with mock.patch.object(ges_clip.ui.timeline, "_get_layer_at") as _get_layer_at:
+                    _get_layer_at.return_value = ges_clip.props.layer, None
+                    ges_clip.ui._button_release_event_cb(None, event)
 
         self.assertEqual(bool(ges_clip.ui.get_state_flags() & Gtk.StateFlags.SELECTED),
                          expect_selected)
diff --git a/tests/test_timeline_timeline.py b/tests/test_timeline_timeline.py
index c262cc47..fec86c1f 100644
--- a/tests/test_timeline_timeline.py
+++ b/tests/test_timeline_timeline.py
@@ -20,8 +20,10 @@ from unittest import mock
 
 from gi.repository import Gdk
 from gi.repository import GES
+from gi.repository import Gst
 from gi.repository import Gtk
 
+from pitivi.utils.timeline import UNSELECT
 from pitivi.utils.ui import LAYER_HEIGHT
 from pitivi.utils.ui import SEPARATOR_HEIGHT
 from tests import common
@@ -291,6 +293,8 @@ class TestGrouping(BaseTestTimeline):
         event = mock.Mock()
         event.keyval = Gdk.KEY_Control_L
         timeline_container.do_key_press_event(event)
+        timeline.get_clicked_layer_and_pos = mock.Mock()
+        timeline.get_clicked_layer_and_pos.return_value = (None, None)
 
         # Select the 2 clips
         for clip in clips:
@@ -385,6 +389,8 @@ class TestGrouping(BaseTestTimeline):
         event = mock.Mock()
         event.keyval = Gdk.KEY_Control_L
         timeline_container.do_key_press_event(event)
+        timeline.get_clicked_layer_and_pos = mock.Mock()
+        timeline.get_clicked_layer_and_pos.return_value = (None, None)
         self.toggle_clip_selection(clips[1], expect_selected=True)
         timeline_container.do_key_release_event(event)
 
@@ -480,6 +486,8 @@ class TestCopyPaste(BaseTestTimeline):
         event = mock.Mock()
         event.keyval = Gdk.KEY_Control_L
         timeline_container.do_key_press_event(event)
+        timeline.get_clicked_layer_and_pos = mock.Mock()
+        timeline.get_clicked_layer_and_pos.return_value = (None, None)
 
         # Select the 2 clips
         for clip in clips:
@@ -561,3 +569,174 @@ class TestEditing(BaseTestTimeline):
         timeline._button_release_event_cb(None, event)
         self.assertEqual(len(timeline.ges_timeline.get_layers()), 1,
                          "No new layer should have been created")
+
+
+class TestShiftSelection(BaseTestTimeline):
+
+    def __reset_clips_selection(self, timeline):
+        """Unselects all clips in the timeline."""
+        layers = timeline.ges_timeline.get_layers()
+        for layer in layers:
+            clips = layer.get_clips()
+            timeline.selection.setSelection(clips, UNSELECT)
+            timeline.set_selection_meta_info(layer, 0, UNSELECT)
+
+    def __check_selected(self, selected_clips, not_selected_clips):
+        for clip in selected_clips:
+            self.assertEqual(clip.selected._selected, True)
+        for clip in not_selected_clips:
+            self.assertEqual(clip.selected._selected, False)
+
+    def __check_simple(self, left_click_also_seeks):
+        timeline_container = create_timeline_container()
+        timeline = timeline_container.timeline
+        timeline.app.settings.leftClickAlsoSeeks = left_click_also_seeks
+        ges_layer = timeline.ges_timeline.append_layer()
+        asset = GES.UriClipAsset.request_sync(
+            common.get_sample_uri("1sec_simpsons_trailer.mp4"))
+        ges_clip1 = ges_layer.add_asset(asset, 0 * Gst.SECOND, 0,
+            1 * Gst.SECOND, GES.TrackType.UNKNOWN)
+        ges_clip2 = ges_layer.add_asset(asset, 1 * Gst.SECOND, 0,
+            1 * Gst.SECOND, GES.TrackType.UNKNOWN)
+
+        event = mock.Mock()
+        event.get_button.return_value = (True, 1)
+        timeline._seek = mock.Mock()
+        timeline._seek.return_value = True
+        timeline.get_clicked_layer_and_pos = mock.Mock()
+
+        with mock.patch.object(Gtk, "get_event_widget") as get_event_widget:
+            get_event_widget.return_value = timeline
+
+            # Simulate click on first and shift+click on second clip.
+            timeline.get_clicked_layer_and_pos.return_value = (ges_layer, 0.5 * Gst.SECOND)
+            timeline._button_release_event_cb(None, event)
+            timeline.get_parent()._shiftMask = True
+            timeline.get_clicked_layer_and_pos.return_value = (ges_layer, 1.5 * Gst.SECOND)
+            timeline._button_release_event_cb(None, event)
+            self.__check_selected([ges_clip1, ges_clip2], [])
+
+    def test_simple(self):
+        self.__check_simple(left_click_also_seeks=False)
+        self.__check_simple(left_click_also_seeks=True)
+
+    def __check_shift_selection_single_layer(self, left_click_also_seeks):
+        """Checks group clips selection across a single layer."""
+        timeline_container = create_timeline_container()
+        timeline = timeline_container.timeline
+        timeline.app.settings.leftClickAlsoSeeks = left_click_also_seeks
+        ges_layer = timeline.ges_timeline.append_layer()
+        ges_clip1 = self.add_clip(ges_layer, 5 * Gst.SECOND, duration=2 * Gst.SECOND)
+        ges_clip2 = self.add_clip(ges_layer, 15 * Gst.SECOND, duration=2 * Gst.SECOND)
+        ges_clip3 = self.add_clip(ges_layer, 25 * Gst.SECOND, duration=2 * Gst.SECOND)
+        ges_clip4 = self.add_clip(ges_layer, 35 * Gst.SECOND, duration=2 * Gst.SECOND)
+
+        event = mock.Mock()
+        event.get_button.return_value = (True, 1)
+        timeline.get_parent()._shiftMask = True
+        timeline._seek = mock.Mock()
+        timeline._seek.return_value = True
+        timeline.get_clicked_layer_and_pos = mock.Mock()
+
+        with mock.patch.object(Gtk, "get_event_widget") as get_event_widget:
+            get_event_widget.return_value = timeline
+
+            # Simulate shift+click before first and on second clip.
+            timeline.get_clicked_layer_and_pos.return_value = (ges_layer, 1 * Gst.SECOND)
+            timeline._button_release_event_cb(None, event)
+            timeline.get_clicked_layer_and_pos.return_value = (ges_layer, 17 * Gst.SECOND)
+            timeline._button_release_event_cb(None, event)
+            self.__check_selected([ges_clip1, ges_clip2], [ges_clip3, ges_clip4])
+            self.__reset_clips_selection(timeline)
+            timeline.resetSelectionGroup()
+
+            # Simiulate shift+click before first and after fourth clip.
+            timeline.get_clicked_layer_and_pos.return_value = (ges_layer, 1 * Gst.SECOND)
+            timeline._button_release_event_cb(None, event)
+            timeline.get_clicked_layer_and_pos.return_value = (ges_layer, 39 * Gst.SECOND)
+            timeline._button_release_event_cb(None, event)
+            self.__check_selected([ges_clip1, ges_clip2, ges_clip3, ges_clip4], [])
+            self.__reset_clips_selection(timeline)
+            timeline.resetSelectionGroup()
+
+            # Simiulate shift+click on first, after fourth and before third clip.
+            timeline.get_clicked_layer_and_pos.return_value = (ges_layer, 6 * Gst.SECOND)
+            timeline._button_release_event_cb(None, event)
+            timeline.get_clicked_layer_and_pos.return_value = (ges_layer, 40 * Gst.SECOND)
+            timeline._button_release_event_cb(None, event)
+            timeline.get_clicked_layer_and_pos.return_value = (ges_layer, 23 * Gst.SECOND)
+            timeline._button_release_event_cb(None, event)
+            self.__check_selected([ges_clip1, ges_clip2], [ges_clip3, ges_clip4])
+            self.__reset_clips_selection(timeline)
+            timeline.resetSelectionGroup()
+
+            # Simulate shift+click twice on the same clip.
+            timeline.get_clicked_layer_and_pos.return_value = (ges_layer, 6 * Gst.SECOND)
+            timeline._button_release_event_cb(None, event)
+            timeline.get_clicked_layer_and_pos.return_value = (ges_layer, 6.5 * Gst.SECOND)
+            timeline._button_release_event_cb(None, event)
+            self.__check_selected([ges_clip1], [ges_clip2, ges_clip3, ges_clip4])
+
+    def test_shift_selection_single_layer(self):
+        self.__check_shift_selection_single_layer(left_click_also_seeks=False)
+        self.__check_shift_selection_single_layer(left_click_also_seeks=True)
+
+    def __check_shift_selection_multiple_layers(self, left_click_also_seeks):
+        """Checks group clips selection across multiple layers."""
+        timeline_container = create_timeline_container()
+        timeline = timeline_container.timeline
+        timeline.app.settings.leftClickAlsoSeeks = left_click_also_seeks
+        ges_layer1 = timeline.ges_timeline.append_layer()
+        ges_clip11 = self.add_clip(ges_layer1, 5 * Gst.SECOND, duration=2 * Gst.SECOND)
+        ges_clip12 = self.add_clip(ges_layer1, 15 * Gst.SECOND, duration=2 * Gst.SECOND)
+        ges_clip13 = self.add_clip(ges_layer1, 25 * Gst.SECOND, duration=2 * Gst.SECOND)
+        ges_layer2 = timeline.ges_timeline.append_layer()
+        ges_clip21 = self.add_clip(ges_layer2, 0 * Gst.SECOND, duration=2 * Gst.SECOND)
+        ges_clip22 = self.add_clip(ges_layer2, 6 * Gst.SECOND, duration=2 * Gst.SECOND)
+        ges_clip23 = self.add_clip(ges_layer2, 21 * Gst.SECOND, duration=2 * Gst.SECOND)
+        ges_layer3 = timeline.ges_timeline.append_layer()
+        ges_clip31 = self.add_clip(ges_layer3, 3 * Gst.SECOND, duration=2 * Gst.SECOND)
+        ges_clip32 = self.add_clip(ges_layer3, 10 * Gst.SECOND, duration=2 * Gst.SECOND)
+        ges_clip33 = self.add_clip(ges_layer3, 18 * Gst.SECOND, duration=2 * Gst.SECOND)
+
+        event = mock.Mock()
+        event.get_button.return_value = (True, 1)
+        timeline.get_parent()._shiftMask = True
+        timeline._seek = mock.Mock()
+        timeline._seek.return_value = True
+        timeline.get_clicked_layer_and_pos = mock.Mock()
+
+        with mock.patch.object(Gtk, "get_event_widget") as get_event_widget:
+            get_event_widget.return_value = timeline
+
+            timeline.get_clicked_layer_and_pos.return_value = (ges_layer2, 3 * Gst.SECOND)
+            timeline._button_release_event_cb(None, event)
+            timeline.get_clicked_layer_and_pos.return_value = (ges_layer1, 9 * Gst.SECOND)
+            timeline._button_release_event_cb(None, event)
+            self.__check_selected([ges_clip11, ges_clip22], [ges_clip12, ges_clip13,
+                ges_clip21, ges_clip23, ges_clip31, ges_clip32, ges_clip33])
+            timeline.get_clicked_layer_and_pos.return_value = (ges_layer3, 12 * Gst.SECOND)
+            timeline._button_release_event_cb(None, event)
+            self.__check_selected([ges_clip22, ges_clip31, ges_clip32], [ges_clip11,
+                ges_clip12, ges_clip13, ges_clip21, ges_clip23, ges_clip33])
+            timeline.get_clicked_layer_and_pos.return_value = (ges_layer1, 22 * Gst.SECOND)
+            timeline._button_release_event_cb(None, event)
+            self.__check_selected([ges_clip11, ges_clip12, ges_clip22, ges_clip23],
+                [ges_clip13, ges_clip21, ges_clip31, ges_clip32, ges_clip33])
+            self.__reset_clips_selection(timeline)
+            timeline.resetSelectionGroup()
+
+            timeline.get_clicked_layer_and_pos.return_value = (ges_layer1, 3 * Gst.SECOND)
+            timeline._button_release_event_cb(None, event)
+            timeline.get_clicked_layer_and_pos.return_value = (ges_layer2, 26 * Gst.SECOND)
+            timeline._button_release_event_cb(None, event)
+            self.__check_selected([ges_clip11, ges_clip12, ges_clip13, ges_clip22, ges_clip23],
+                [ges_clip21, ges_clip31, ges_clip32, ges_clip33])
+            timeline.get_clicked_layer_and_pos.return_value = (ges_layer3, 30 * Gst.SECOND)
+            timeline._button_release_event_cb(None, event)
+            self.__check_selected([ges_clip11, ges_clip12, ges_clip13, ges_clip22, ges_clip23,
+                ges_clip31, ges_clip32, ges_clip33], [ges_clip21])
+
+    def test_shift_selection_multiple_layers(self):
+        self.__check_shift_selection_multiple_layers(left_click_also_seeks=False)
+        self.__check_shift_selection_multiple_layers(left_click_also_seeks=True)


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