Commit 87252891 authored by Harish Fulara's avatar Harish Fulara

timeline: Allow selecting a range of clips

Fixes #1399
parent dad1817d
......@@ -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 \
......
......@@ -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
......@@ -260,6 +262,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:
......@@ -354,6 +358,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)
......@@ -449,6 +455,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:
......@@ -530,3 +538,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