Commit 599bffc3 authored by Harish Fulara's avatar Harish Fulara Committed by Alexandru Băluț

timeline: Allow selecting a range of clips

Fixes #1399
parent 0c895ba2
Pipeline #7937 passed with stage
in 49 minutes and 2 seconds
......@@ -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:
......
......@@ -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 \
......
......@@ -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)
......
......@@ -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)
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment