[pitivi] timeline: Allow selecting a range of clips
- From: Alexandru Băluț <alexbalut src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [pitivi] timeline: Allow selecting a range of clips
- Date: Mon, 2 Apr 2018 16:21:34 +0000 (UTC)
commit 599bffc3025ab0339d9b706ca6c003d3ae1bf533
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 | 10 +--
pitivi/timeline/timeline.py | 149 ++++++++++++++++++++-------------
tests/common.py | 12 ++-
tests/test_timeline_timeline.py | 179 ++++++++++++++++++++++++++++++++++++++++
4 files changed, 286 insertions(+), 64 deletions(-)
---
diff --git a/pitivi/timeline/elements.py b/pitivi/timeline/elements.py
index aba3417d..dbc546ab 100644
--- a/pitivi/timeline/elements.py
+++ b/pitivi/timeline/elements.py
@@ -595,6 +595,7 @@ class MultipleKeyframeCurve(KeyframeCurve):
markup = _("Timestamp: %s") % Gst.TIME_ARGS(event.xdata)
self.set_tooltip_markup(markup)
+
class TimelineElement(Gtk.Layout, Zoomable, Loggable):
__gsignals__ = {
# Signal the keyframes curve are being hovered
@@ -1264,13 +1265,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 b953645f..eab4cbfd 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):
@@ -380,6 +347,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.
@@ -734,20 +705,86 @@ class Timeline(Gtk.EventBox, Zoomable, Loggable):
self._scrolling = False
- if allow_seek and res and (button == 1 and self.app.settings.leftClickAlsoSeeks):
- if self.__next_seek_position is not None:
- self._project.pipeline.simple_seek(self.__next_seek_position)
- self.__next_seek_position = None
- else:
- event_widget = Gtk.get_event_widget(event)
- if self._getParentOfType(event_widget, LayerControls) is None:
- self._seek(event)
+ if allow_seek and res and button == 1:
+ if self.app.settings.leftClickAlsoSeeks:
+ if self.__next_seek_position is not None:
+ self._project.pipeline.simple_seek(self.__next_seek_position)
+ self.__next_seek_position = None
+ else:
+ 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]