1
# -*- coding: utf-8 -*-
4
# pitivi/timeline/elements.py
6
# Copyright (c) 2013, Mathieu Duponchelle <mduponchelle1@gmail.com>
8
# This program is free software; you can redistribute it and/or
9
# modify it under the terms of the GNU Lesser General Public
10
# License as published by the Free Software Foundation; either
11
# version 2.1 of the License, or (at your option) any later version.
13
# This program is distributed in the hope that it will be useful,
14
# but WITHOUT ANY WARRANTY; without even the implied warranty of
15
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
16
# Lesser General Public License for more details.
18
# You should have received a copy of the GNU Lesser General Public
19
# License along with this program; if not, write to the
20
# Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
21
# Boston, MA 02110-1301, USA.
24
Convention throughout this file:
25
Every GES element which name could be mistaken with a UI element
26
is prefixed with a little b, example : bTimeline
32
from datetime import datetime
36
from gi.repository import Clutter, Gtk, GtkClutter, Cogl, GES, Gdk, Gst, GstController
37
from pitivi.utils.timeline import Zoomable, EditingContext, SELECT, UNSELECT, SELECT_ADD, Selected
38
from previewers import AudioPreviewer, VideoPreviewer
40
import pitivi.configure as configure
41
from pitivi.utils.ui import EXPANDED_SIZE, SPACING, KEYFRAME_SIZE, CONTROL_WIDTH, create_cogl_color
43
# Colors for keyframes and clips (RGBA)
44
KEYFRAME_LINE_COLOR = (237, 212, 0, 255) # "Tango" yellow
45
KEYFRAME_NORMAL_COLOR = Clutter.Color.new(0, 0, 0, 200)
46
KEYFRAME_SELECTED_COLOR = Clutter.Color.new(200, 200, 200, 200)
47
CLIP_SELECTED_OVERLAY_COLOR = Clutter.Color.new(60, 60, 60, 100)
48
GHOST_CLIP_COLOR = Clutter.Color.new(255, 255, 255, 50)
49
TRANSITION_COLOR = create_cogl_color(35, 85, 125, 125) # light blue
51
BORDER_NORMAL_COLOR = create_cogl_color(100, 100, 100, 255)
52
BORDER_SELECTED_COLOR = create_cogl_color(200, 200, 10, 255)
54
NORMAL_CURSOR = Gdk.Cursor.new(Gdk.CursorType.LEFT_PTR)
55
DRAG_CURSOR = Gdk.Cursor.new(Gdk.CursorType.HAND1)
56
DRAG_LEFT_HANDLEBAR_CURSOR = Gdk.Cursor.new(Gdk.CursorType.LEFT_SIDE)
57
DRAG_RIGHT_HANDLEBAR_CURSOR = Gdk.Cursor.new(Gdk.CursorType.RIGHT_SIDE)
60
class RoundedRectangle(Clutter.Actor):
62
Custom actor used to draw a rectangle that can have rounded corners
64
__gtype_name__ = 'RoundedRectangle'
66
def __init__(self, width, height, arc, step,
67
color=None, border_color=None, border_width=0):
69
Creates a new rounded rectangle
71
Clutter.Actor.__init__(self)
73
self.props.width = width
74
self.props.height = height
78
self._border_width = border_width
80
self._border_color = border_color
83
# Set a rectangle for the clipping
84
Cogl.clip_push_rectangle(0, 0, self.props.width, self.props.height)
86
if self._border_color:
87
# draw the rectangle for the border which is the same size as the
89
Cogl.path_round_rectangle(0, 0, self.props.width, self.props.height,
90
self._arc, self._step)
91
Cogl.path_round_rectangle(self._border_width, self._border_width,
92
self.props.width - self._border_width,
93
self.props.height - self._border_width,
94
self._arc, self._step)
95
Cogl.path_set_fill_rule(Cogl.PathFillRule.EVEN_ODD)
98
# set color to border color
99
Cogl.set_source_color(self._border_color)
103
# draw the content with is the same size minus the width of the border
105
Cogl.path_round_rectangle(self._border_width, self._border_width,
106
self.props.width - self._border_width,
107
self.props.height - self._border_width,
108
self._arc, self._step)
111
# set the color of the filled area
112
Cogl.set_source_color(self._color)
120
def set_color(self, color):
124
def get_border_width(self):
125
return self._border_width
127
def set_border_width(self, width):
128
self._border_width = width
131
def set_border_color(self, color):
132
self._border_color = color
136
class Ghostclip(Clutter.Actor):
138
The concept of a ghostclip is to represent future actions without
139
actually moving GESClips. They are created when the user wants
140
to change a clip of layer, and when the user does a drag and drop
141
from the media library.
143
def __init__(self, track_type, bElement=None):
144
Clutter.Actor.__init__(self)
145
self.track_type = track_type
146
self.bElement = bElement
147
self.set_background_color(GHOST_CLIP_COLOR)
148
self.props.visible = False
149
self.shouldCreateLayer = False
151
def setNbrLayers(self, nbrLayers):
152
self.nbrLayers = nbrLayers
154
def setWidth(self, width):
155
self.props.width = width
157
def update(self, priority, y, isControlledByBrother):
158
# Priority and y can be negative when dragging an asset to the ruler.
159
# Priority can also be negative when dragging a linked element.
160
self.priority = min(max(0, priority), self.nbrLayers)
163
# Here we make it so the calculation is the same for audio and video.
164
if self.track_type == GES.TrackType.AUDIO and not isControlledByBrother:
165
y -= self.nbrLayers * (EXPANDED_SIZE + SPACING)
167
# And here we take into account the fact that the pointer might actually be
168
# on the other track element, meaning we have to offset it.
169
if isControlledByBrother:
170
if self.track_type == GES.TrackType.AUDIO:
171
y += self.nbrLayers * (EXPANDED_SIZE + SPACING)
173
y -= self.nbrLayers * (EXPANDED_SIZE + SPACING)
175
# Would that be a new layer at the end or inserted ?
176
if self.priority == self.nbrLayers or y % (EXPANDED_SIZE + SPACING) < SPACING:
177
self.shouldCreateLayer = True
178
self.set_size(self.props.width, SPACING)
179
self.props.y = self.priority * (EXPANDED_SIZE + SPACING)
180
if self.track_type == GES.TrackType.AUDIO:
181
self.props.y += self.nbrLayers * (EXPANDED_SIZE + SPACING)
182
self.props.visible = True
184
self.shouldCreateLayer = False
185
# No need to mockup on the same layer
186
if self.bElement and self.priority == self.bElement.get_parent().get_layer().get_priority():
187
self.props.visible = False
188
# We would be moving to an existing layer.
189
elif self.priority < self.nbrLayers:
190
self.set_size(self.props.width, EXPANDED_SIZE)
191
self.props.y = self.priority * (EXPANDED_SIZE + SPACING) + SPACING
192
if self.track_type == GES.TrackType.AUDIO:
193
self.props.y += self.nbrLayers * (EXPANDED_SIZE + SPACING)
194
self.props.visible = True
196
def getLayerForY(self, y):
197
if self.track_type == GES.TrackType.AUDIO:
198
y -= self.nbrLayers * (EXPANDED_SIZE + SPACING)
199
priority = int(y / (EXPANDED_SIZE + SPACING))
204
class TrimHandle(Clutter.Texture):
205
def __init__(self, timelineElement, isLeft):
206
Clutter.Texture.__init__(self)
209
self.timelineElement = weakref.proxy(timelineElement)
210
self.dragAction = Clutter.DragAction()
212
self.set_from_file(os.path.join(configure.get_pixmap_dir(), "trimbar-normal.png"))
213
self.set_size(-1, EXPANDED_SIZE)
215
self.set_reactive(True)
217
self.add_action(self.dragAction)
218
self.dragAction.connect("drag-begin", self._dragBeginCb)
219
self.dragAction.connect("drag-end", self._dragEndCb)
220
self.dragAction.connect("drag-progress", self._dragProgressCb)
222
self.connect("enter-event", self._enterEventCb)
223
self.connect("leave-event", self._leaveEventCb)
225
self.timelineElement.connect("enter-event", self._elementEnterEventCb)
226
self.timelineElement.connect("leave-event", self._elementLeaveEventCb)
229
self.disconnect_by_func(self._enterEventCb)
230
self.disconnect_by_func(self._leaveEventCb)
231
self.timelineElement.disconnect_by_func(self._elementEnterEventCb)
232
self.timelineElement.disconnect_by_func(self._elementLeaveEventCb)
236
def _enterEventCb(self, unused_actor, unused_event):
237
self.timelineElement.set_reactive(False)
238
for elem in self.timelineElement.get_children():
239
elem.set_reactive(False)
240
self.set_reactive(True)
242
self.set_from_file(os.path.join(configure.get_pixmap_dir(), "trimbar-focused.png"))
244
self.timelineElement.timeline._container.embed.get_window().set_cursor(DRAG_LEFT_HANDLEBAR_CURSOR)
246
self.timelineElement.timeline._container.embed.get_window().set_cursor(DRAG_RIGHT_HANDLEBAR_CURSOR)
248
def _leaveEventCb(self, unused_actor, event):
249
self.timelineElement.set_reactive(True)
250
children = self.timelineElement.get_children()
252
other_actor = self.timelineElement.timeline._container.stage.get_actor_at_pos(Clutter.PickMode.ALL, event.x, event.y)
253
if other_actor not in children:
254
self.timelineElement.hideHandles()
256
for elem in children:
257
elem.set_reactive(True)
258
self.set_from_file(os.path.join(configure.get_pixmap_dir(), "trimbar-normal.png"))
259
self.timelineElement.timeline._container.embed.get_window().set_cursor(NORMAL_CURSOR)
261
def _elementEnterEventCb(self, unused_actor, unused_event):
264
def _elementLeaveEventCb(self, unused_actor, unused_event):
267
def _dragBeginCb(self, unused_action, unused_actor, event_x, event_y, unused_modifiers):
268
self.dragBeginStartX = event_x
269
self.dragBeginStartY = event_y
270
elem = self.timelineElement.bElement.get_parent()
271
self.timelineElement.setDragged(True)
274
edge = GES.Edge.EDGE_START
275
self._dragBeginStart = self.timelineElement.bElement.get_parent().get_start()
277
edge = GES.Edge.EDGE_END
278
self._dragBeginStart = self.timelineElement.bElement.get_parent().get_duration() + \
279
self.timelineElement.bElement.get_parent().get_start()
281
self._context = EditingContext(elem,
282
self.timelineElement.timeline.bTimeline,
283
GES.EditMode.EDIT_TRIM,
286
self.timelineElement.timeline._container.app.action_log)
288
self._context.connect("clip-trim", self.clipTrimCb)
289
self._context.connect("clip-trim-finished", self.clipTrimFinishedCb)
291
def _dragProgressCb(self, unused_action, unused_actor, delta_x, unused_delta_y):
292
# We can't use delta_x here because it fluctuates weirdly.
293
coords = self.dragAction.get_motion_coords()
294
delta_x = coords[0] - self.dragBeginStartX
295
new_start = self._dragBeginStart + Zoomable.pixelToNs(delta_x)
297
self._context.setMode(self.timelineElement.timeline._container.getEditionMode(isAHandle=True))
298
self._context.editTo(new_start, self.timelineElement.bElement.get_parent().get_layer().get_priority())
301
def _dragEndCb(self, unused_action, unused_actor, unused_event_x, unused_event_y, unused_modifiers):
302
self.timelineElement.setDragged(False)
303
self._context.finish()
305
self.timelineElement.set_reactive(True)
306
for elem in self.timelineElement.get_children():
307
elem.set_reactive(True)
309
self.set_from_file(os.path.join(configure.get_pixmap_dir(), "trimbar-normal.png"))
310
self.timelineElement.timeline._container.embed.get_window().set_cursor(NORMAL_CURSOR)
312
def clipTrimCb(self, unused_TrimStartContext, tl_obj, position):
313
# While a clip is being trimmed, ask the viewer to preview it
314
self.timelineElement.timeline._container.app.gui.viewer.clipTrimPreview(tl_obj, position)
316
def clipTrimFinishedCb(self, unused_TrimStartContext):
317
# When a clip has finished trimming, tell the viewer to reset itself
318
self.timelineElement.timeline._container.app.gui.viewer.clipTrimPreviewFinished()
321
class TimelineElement(Clutter.Actor, Zoomable):
323
@ivar bElement: the backend element.
324
@type bElement: GES.TrackElement
325
@ivar timeline: the containing graphic timeline.
326
@type timeline: TimelineStage
329
def __init__(self, bElement, timeline):
330
Zoomable.__init__(self)
331
Clutter.Actor.__init__(self)
333
self.timeline = timeline
334
self.bElement = bElement
335
self.bElement.selected = Selected()
336
self.bElement.ui_element = weakref.proxy(self)
337
self.track_type = self.bElement.get_track_type() # This won't change
338
self.isDragged = False
341
self.keyframesVisible = False
343
self.keyframedElement = None
344
self.rightHandle = None
345
self.isSelected = False
346
size = self.bElement.get_duration()
348
self.background = self._createBackground()
349
self.background.set_position(0, 0)
350
self.add_child(self.background)
352
self.preview = self._createPreview()
353
self.add_child(self.preview)
355
self.border = self._createBorder()
356
self.add_child(self.border)
358
self.marquee = self._createMarquee()
359
self.add_child(self.marquee)
361
self._createHandles()
363
self._linesMarker = self._createMarker()
364
self._keyframesMarker = self._createMarker()
366
self._createGhostclip()
369
self.set_reactive(True)
371
self._createMixingKeyframes()
373
self._connectToEvents()
377
def set_size(self, width, height, ease):
379
self.save_easing_state()
380
self.set_easing_duration(600)
381
self.background.save_easing_state()
382
self.background.set_easing_duration(600)
383
self.border.save_easing_state()
384
self.border.set_easing_duration(600)
385
self.preview.save_easing_state()
386
self.preview.set_easing_duration(600)
388
self.rightHandle.save_easing_state()
389
self.rightHandle.set_easing_duration(600)
391
self.marquee.set_size(width, height)
392
self.background.props.width = width
393
self.background.props.height = height
394
self.border.props.width = width
395
self.border.props.height = height
396
self.props.width = width
397
self.props.height = height
398
self.preview.set_size(width, height)
400
self.rightHandle.set_position(width - self.rightHandle.props.width, 0)
403
self.background.restore_easing_state()
404
self.border.restore_easing_state()
405
self.preview.restore_easing_state()
407
self.rightHandle.restore_easing_state()
408
self.restore_easing_state()
410
def addKeyframe(self, value, timestamp):
411
self.source.set(timestamp, value)
412
self.updateKeyframes()
414
def removeKeyframe(self, kf):
415
self.source.unset(kf.value.timestamp)
416
self.keyframes = sorted(self.keyframes, key=lambda keyframe: keyframe.value.timestamp)
417
self.updateKeyframes()
419
def showKeyframes(self, element, propname, isDefault=False):
420
binding = element.get_control_binding(propname.name)
422
source = GstController.InterpolationControlSource()
423
source.props.mode = GstController.InterpolationMode.LINEAR
424
if not (element.set_control_source(source, propname.name, "direct")):
425
print "There was something like a problem captain"
427
binding = element.get_control_binding(propname.name)
429
self.binding = binding
431
self.keyframedElement = element
432
self.source = self.binding.props.control_source
435
self.default_prop = propname
436
self.default_element = element
438
self.keyframesVisible = True
440
self.updateKeyframes()
442
def hideKeyframes(self):
443
for keyframe in self.keyframes:
444
self.remove_child(keyframe)
447
self.keyframesVisible = False
450
self.showKeyframes(self.default_element, self.default_prop)
454
def setKeyframePosition(self, keyframe, value):
455
x = self.nsToPixel(value.timestamp - self.bElement.props.in_point) - KEYFRAME_SIZE / 2
456
y = EXPANDED_SIZE - (value.value * EXPANDED_SIZE) - KEYFRAME_SIZE / 2
457
keyframe.set_position(x, y)
459
def drawLines(self, line=None):
460
for line_ in self.lines:
462
self.remove_child(line_)
470
for keyframe in self.keyframes:
471
if lastKeyframe and (not line or lastKeyframe != line.previousKeyframe):
472
self._createLine(keyframe, lastKeyframe, None)
474
self._createLine(keyframe, lastKeyframe, line)
475
lastKeyframe = keyframe
477
def updateKeyframes(self):
481
values = self.source.get_all()
482
if len(values) < 2 and self.bElement.props.duration > 0:
483
self.source.unset_all()
484
val = float(self.prop.default_value) / (self.prop.maximum - self.prop.minimum)
485
self.source.set(self.bElement.props.in_point, val)
486
self.source.set(self.bElement.props.duration + self.bElement.props.in_point, val)
488
for keyframe in self.keyframes:
489
self.remove_child(keyframe)
492
values = self.source.get_all()
493
values_count = len(values)
494
for i, value in enumerate(values):
495
has_changeable_time = i > 0 and i < values_count - 1
496
keyframe = self._createKeyframe(value, has_changeable_time)
497
self.keyframes.append(keyframe)
502
Zoomable.__del__(self)
503
self.disconnectFromEvents()
505
def disconnectFromEvents(self):
506
self.dragAction.disconnect_by_func(self._dragProgressCb)
507
self.dragAction.disconnect_by_func(self._dragBeginCb)
508
self.dragAction.disconnect_by_func(self._dragEndCb)
509
self.remove_action(self.dragAction)
510
self.bElement.selected.disconnect_by_func(self._selectedChangedCb)
511
self.bElement.disconnect_by_func(self._durationChangedCb)
512
self.bElement.disconnect_by_func(self._inpointChangedCb)
513
self.disconnect_by_func(self._clickedCb)
517
def _createMarker(self):
518
marker = Clutter.Actor()
519
self.add_child(marker)
522
def update(self, ease):
523
start = self.bElement.get_start()
524
duration = self.bElement.get_duration()
525
# The calculation of the duration assumes that the start is always
526
# int(pixels_float). In that case, the rounding can add up and a pixel
527
# might be lost if we ignore the start of the clip.
528
size = self.nsToPixel(start + duration) - self.nsToPixel(start)
529
# Avoid elements to become invisible.
531
self.set_size(size, EXPANDED_SIZE, ease)
533
def setDragged(self, dragged):
534
brother = self.timeline.findBrother(self.bElement)
536
brother.isDragged = dragged
537
self.isDragged = dragged
539
def _createMixingKeyframes(self):
540
if self.track_type == GES.TrackType.VIDEO:
545
for spec in self.bElement.list_children_properties():
546
if spec.name == propname:
547
self.showKeyframes(self.bElement, spec, isDefault=True)
551
def _createKeyframe(self, value, has_changeable_time):
552
keyframe = Keyframe(self, value, has_changeable_time)
553
self.insert_child_above(keyframe, self._keyframesMarker)
554
self.setKeyframePosition(keyframe, value)
557
def _createLine(self, keyframe, lastKeyframe, line):
559
line = Line(self, keyframe, lastKeyframe)
560
self.lines.append(line)
561
self.insert_child_above(line, self._linesMarker)
563
adj = self.nsToPixel(keyframe.value.timestamp - lastKeyframe.value.timestamp)
564
opp = (lastKeyframe.value.value - keyframe.value.value) * EXPANDED_SIZE
565
hyp = math.sqrt(adj ** 2 + opp ** 2)
567
# line length would be less than one pixel
571
line.props.width = hyp
572
line.props.height = KEYFRAME_SIZE
573
line.props.rotation_angle_z = math.degrees(math.asin(sinX))
574
line.props.x = self.nsToPixel(lastKeyframe.value.timestamp - self.bElement.props.in_point)
575
line.props.y = EXPANDED_SIZE - (EXPANDED_SIZE * lastKeyframe.value.value) - KEYFRAME_SIZE / 2
576
line.canvas.invalidate()
578
def _createGhostclip(self):
581
def _createBorder(self):
582
border = RoundedRectangle(0, 0, 0, 0)
583
border.bElement = self.bElement
584
border.set_border_color(BORDER_NORMAL_COLOR)
585
border.set_border_width(1)
586
border.set_position(0, 0)
589
def _createBackground(self):
590
raise NotImplementedError()
592
def _createHandles(self):
595
def _createPreview(self):
596
if isinstance(self.bElement, GES.AudioUriSource):
597
previewer = AudioPreviewer(self.bElement, self.timeline)
598
previewer.startLevelsDiscoveryWhenIdle()
600
if isinstance(self.bElement, GES.VideoUriSource):
601
return VideoPreviewer(self.bElement, self.timeline)
602
# TODO: GES.AudioTransition, GES.VideoTransition, GES.ImageSource, GES.TitleSource
603
return Clutter.Actor()
605
def _createMarquee(self):
606
marquee = Clutter.Actor()
607
marquee.bElement = self.bElement
608
marquee.set_background_color(CLIP_SELECTED_OVERLAY_COLOR)
609
marquee.props.visible = False
612
def _connectToEvents(self):
613
self.dragAction = Clutter.DragAction()
614
self.add_action(self.dragAction)
615
self.dragAction.connect("drag-progress", self._dragProgressCb)
616
self.dragAction.connect("drag-begin", self._dragBeginCb)
617
self.dragAction.connect("drag-end", self._dragEndCb)
618
self.bElement.selected.connect("selected-changed", self._selectedChangedCb)
619
self.bElement.connect("notify::duration", self._durationChangedCb)
620
self.bElement.connect("notify::in-point", self._inpointChangedCb)
621
# We gotta go low-level cause Clutter.ClickAction["clicked"]
622
# gets emitted after Clutter.DragAction["drag-begin"]
623
self.connect("button-press-event", self._clickedCb)
625
def _getLayerForY(self, y):
626
if self.track_type == GES.TrackType.AUDIO:
627
y -= self.nbrLayers * (EXPANDED_SIZE + SPACING)
628
priority = int(y / (EXPANDED_SIZE + SPACING))
631
# Interface (Zoomable)
633
def zoomChanged(self):
636
self.updateKeyframes()
640
def _clickedCb(self, action, actor):
643
def _dragBeginCb(self, action, actor, event_x, event_y, modifiers):
646
def _dragProgressCb(self, unused_action, unused_actor, unused_delta_x, unused_delta_y):
649
def _dragEndCb(self, action, actor, event_x, event_y, modifiers):
652
def _durationChangedCb(self, unused_element, unused_duration):
653
if self.keyframesVisible:
654
self.updateKeyframes()
656
def _inpointChangedCb(self, unused_element, unused_inpoint):
657
if self.keyframesVisible:
658
self.updateKeyframes()
660
def _selectedChangedCb(self, unused_selected, isSelected):
661
self.isSelected = isSelected
664
self.marquee.props.visible = isSelected
665
color = BORDER_SELECTED_COLOR if isSelected else BORDER_NORMAL_COLOR
666
self.border.set_border_color(color)
669
class Gradient(Clutter.Actor):
670
def __init__(self, rb, gb, bb, re, ge, be):
672
Creates a rectangle with a gradient. The first three parameters
673
are the gradient's RGB values at the top, the last three params
674
are the RGB values at the bottom.
676
Clutter.Actor.__init__(self)
677
self.canvas = Clutter.Canvas()
678
self.linear = cairo.LinearGradient(0, 0, 10, EXPANDED_SIZE)
679
self.linear.add_color_stop_rgb(0, rb / 255., gb / 255., bb / 255.)
680
self.linear.add_color_stop_rgb(1, re / 255., ge / 255., be / 255.)
681
self.canvas.set_size(10, EXPANDED_SIZE)
682
self.canvas.connect("draw", self._drawCb)
683
self.set_content(self.canvas)
684
self.canvas.invalidate()
686
def _drawCb(self, unused_canvas, cr, unused_width, unused_height):
687
cr.set_operator(cairo.OPERATOR_CLEAR)
689
cr.set_operator(cairo.OPERATOR_OVER)
690
cr.set_source(self.linear)
691
cr.rectangle(0, 0, 10, EXPANDED_SIZE)
695
class Line(Clutter.Actor):
697
A cairo line used for keyframe curves.
699
def __init__(self, timelineElement, keyframe, lastKeyframe):
700
Clutter.Actor.__init__(self)
701
self.timelineElement = weakref.proxy(timelineElement)
703
self.canvas = Clutter.Canvas()
704
self.canvas.set_size(1000, KEYFRAME_SIZE)
705
self.canvas.connect("draw", self._drawCb)
706
self.set_content(self.canvas)
707
self.set_reactive(True)
709
self.gotDragged = False
711
self.dragAction = Clutter.DragAction()
712
self.add_action(self.dragAction)
714
self.dragAction.connect("drag-begin", self._dragBeginCb)
715
self.dragAction.connect("drag-end", self._dragEndCb)
716
self.dragAction.connect("drag-progress", self._dragProgressCb)
718
self.connect("button-release-event", self._clickedCb)
719
self.connect("motion-event", self._motionEventCb)
720
self.connect("enter-event", self._enterEventCb)
721
self.connect("leave-event", self._leaveEventCb)
723
self.previousKeyframe = lastKeyframe
724
self.nextKeyframe = keyframe
726
def _drawCb(self, unused_canvas, cr, width, unused_height):
728
This is where we actually create the line segments for keyframe curves.
729
We draw multiple lines (one-third of the height each) to add a "shadow"
730
around the actual line segment to improve visibility.
732
cr.set_operator(cairo.OPERATOR_CLEAR)
734
cr.set_operator(cairo.OPERATOR_OVER)
736
# The "height budget" to draw line components = the tallest component...
737
_max_height = KEYFRAME_SIZE
739
# While normally all three lines would have an equal height,
740
# I make the shadow lines be 1/2 (3px) instead of 1/3 (2px),
741
# while keeping their 1/3 position... this softens them up.
743
# Upper shadow/border:
744
cr.set_source_rgba(0, 0, 0, 0.5) # 50% transparent black color
745
cr.move_to(0, _max_height / 3)
746
cr.line_to(width, _max_height / 3)
747
cr.set_line_width(_max_height / 3) # Special case: fuzzy 3px
749
# Lower shadow/border:
750
cr.set_source_rgba(0, 0, 0, 0.5) # 50% transparent black color
751
cr.move_to(0, _max_height * 2 / 3)
752
cr.line_to(width, _max_height * 2 / 3)
753
cr.set_line_width(_max_height / 3) # Special case: fuzzy 3px
755
# Draw the actual line in the middle.
756
# Do it last, so that it gets drawn on top and remains sharp.
757
cr.set_source_rgba(*KEYFRAME_LINE_COLOR)
758
cr.move_to(0, _max_height / 2)
759
cr.line_to(width, _max_height / 2)
760
cr.set_line_width(_max_height / 3)
763
def transposeXY(self, x, y):
764
x -= self.timelineElement.props.x + CONTROL_WIDTH - self.timelineElement.timeline._scroll_point.x
765
x += Zoomable.nsToPixel(self.timelineElement.bElement.props.in_point)
766
y -= self.timelineElement.props.y
770
self.timelineElement.set_reactive(True)
771
self.timelineElement.timeline._container.embed.get_window().set_cursor(NORMAL_CURSOR)
773
def _clickedCb(self, unused_actor, event):
775
self.gotDragged = False
777
x, unused_y = self.transposeXY(event.x, event.y)
778
timestamp = Zoomable.pixelToNs(x)
779
value = self._valueAtTimestamp(timestamp)
780
self.timelineElement.addKeyframe(value, timestamp)
782
def _valueAtTimestamp(self, timestamp):
783
timestamp_left = self.previousKeyframe.value.timestamp
784
value_left = self.previousKeyframe.value.value
785
timestamp_right = self.nextKeyframe.value.timestamp
786
value_right = self.nextKeyframe.value.value
787
height = value_right - value_left
788
duration = timestamp_right - timestamp_left
789
value = value_right - (timestamp_right - timestamp) * height / duration
790
return max(0.0, min(value, 1.0))
792
def _enterEventCb(self, unused_actor, unused_event):
793
self.timelineElement.set_reactive(False)
794
self.timelineElement.timeline._container.embed.get_window().set_cursor(DRAG_CURSOR)
796
def _leaveEventCb(self, unused_actor, unused_event):
799
def _motionEventCb(self, actor, event):
802
def _dragBeginCb(self, unused_action, unused_actor, event_x, event_y, unused_modifiers):
803
self.dragBeginStartX = event_x
804
self.dragBeginStartY = event_y
805
self.origY = self.props.y
806
self.previousKeyframe.startDrag(event_x, event_y, self)
807
self.nextKeyframe.startDrag(event_x, event_y, self)
809
def _dragProgressCb(self, unused_action, unused_actor, unused_delta_x, delta_y):
810
self.gotDragged = True
811
coords = self.dragAction.get_motion_coords()
812
delta_x = coords[0] - self.dragBeginStartX
813
delta_y = coords[1] - self.dragBeginStartY
815
self.previousKeyframe.updateValue(0, delta_y)
816
self.nextKeyframe.updateValue(0, delta_y)
820
def _dragEndCb(self, unused_action, unused_actor, unused_event_x, unused_event_y, unused_modifiers):
821
self.previousKeyframe.endDrag()
822
self.nextKeyframe.endDrag()
823
if self.timelineElement.timeline.getActorUnderPointer() != self:
827
class KeyframeMenu(GtkClutter.Actor):
828
def __init__(self, keyframe):
829
GtkClutter.Actor.__init__(self)
830
self.keyframe = keyframe
833
button = Gtk.Button()
834
button.set_label("Remove")
835
button.connect("clicked", self._removeClickedCb)
836
vbox.pack_start(button, False, False, False)
838
self.get_widget().add(vbox)
841
self.set_reactive(True)
844
GtkClutter.Actor.show(self)
848
GtkClutter.Actor.hide(self)
851
def _removeClickedCb(self, unused_button):
852
self.keyframe.remove()
855
class Keyframe(Clutter.Actor):
857
@ivar has_changeable_time: if False, it means this is an edge keyframe.
858
@type has_changeable_time: bool
861
def __init__(self, timelineElement, value, has_changeable_time):
862
Clutter.Actor.__init__(self)
865
self.timelineElement = weakref.proxy(timelineElement)
866
self.has_changeable_time = has_changeable_time
867
self.lastClick = datetime.now()
869
self.set_size(KEYFRAME_SIZE, KEYFRAME_SIZE)
870
self.set_background_color(KEYFRAME_NORMAL_COLOR)
872
self.dragAction = Clutter.DragAction()
873
self.add_action(self.dragAction)
875
self.dragAction.connect("drag-begin", self._dragBeginCb)
876
self.dragAction.connect("drag-end", self._dragEndCb)
877
self.dragAction.connect("drag-progress", self._dragProgressCb)
878
self.connect("enter-event", self._enterEventCb)
879
self.connect("leave-event", self._leaveEventCb)
880
self.connect("button-press-event", self._clickedCb)
883
self.dragProgressed = False
884
self.set_reactive(True)
886
def createMenu(self):
887
self.menu = KeyframeMenu(self)
888
self.timelineElement.timeline._container.stage.connect("button-press-event", self._stageClickedCb)
889
self.timelineElement.timeline.add_child(self.menu)
892
self.timelineElement.set_reactive(True)
893
self.set_background_color(KEYFRAME_NORMAL_COLOR)
894
self.timelineElement.timeline._container.embed.get_window().set_cursor(NORMAL_CURSOR)
897
# Can't remove edge keyframes !
898
if not self.has_changeable_time:
901
self.timelineElement.timeline.remove_child(self.menu)
903
self.timelineElement.removeKeyframe(self)
905
def _stageClickedCb(self, stage, event):
906
actor = stage.get_actor_at_pos(Clutter.PickMode.REACTIVE, event.x, event.y)
907
if actor != self.menu:
910
def _clickedCb(self, unused_actor, event):
911
if (event.modifier_state & Clutter.ModifierType.CONTROL_MASK):
913
elif (datetime.now() - self.lastClick).total_seconds() < 0.5:
916
self.lastClick = datetime.now()
918
def _enterEventCb(self, unused_actor, unused_event):
919
self.timelineElement.set_reactive(False)
920
self.set_background_color(KEYFRAME_SELECTED_COLOR)
921
self.timelineElement.timeline._container.embed.get_window().set_cursor(DRAG_CURSOR)
923
def _leaveEventCb(self, unused_actor, unused_event):
926
def startDrag(self, event_x, event_y, line=None):
927
self.dragBeginStartX = event_x
928
self.dragBeginStartY = event_y
929
self.lastTs = self.value.timestamp
930
self.valueStart = self.value.value
931
self.tsStart = self.value.timestamp
932
self.duration = self.timelineElement.bElement.props.duration
933
self.inpoint = self.timelineElement.bElement.props.in_point
934
self.start = self.timelineElement.bElement.props.start
938
if not self.dragProgressed and not self.line:
939
timeline = self.timelineElement.timeline
940
self.menu.set_position(self.timelineElement.props.x + self.props.x + 10, self.timelineElement.props.y + self.props.y + 10)
945
def updateValue(self, delta_x, delta_y):
946
newTs = self.tsStart + Zoomable.pixelToNs(delta_x)
947
newValue = self.valueStart - (delta_y / EXPANDED_SIZE)
949
# Don't overlap first and last keyframes.
950
newTs = min(max(newTs, self.inpoint + 1), self.duration + self.inpoint - 1)
952
newValue = min(max(newValue, 0.0), 1.0)
954
if not self.has_changeable_time:
957
self.timelineElement.source.unset(self.lastTs)
958
if (self.timelineElement.source.set(newTs, newValue)):
959
self.value = Gst.TimedValue()
960
self.value.timestamp = newTs
961
self.value.value = newValue
964
self.timelineElement.setKeyframePosition(self, self.value)
965
# Resort the keyframes list each time. Should be cheap as there should never be too much keyframes,
966
# if optimization is needed, check if resorting is needed, should not be in 99 % of the cases.
967
self.timelineElement.keyframes = sorted(self.timelineElement.keyframes, key=lambda keyframe: keyframe.value.timestamp)
968
self.timelineElement.drawLines(self.line)
969
# This will update the viewer. nifty.
971
self.timelineElement.timeline._container.seekInPosition(newTs + self.start)
973
def _dragBeginCb(self, unused_action, unused_actor, event_x, event_y, unused_modifiers):
974
self.dragProgressed = False
975
self.startDrag(event_x, event_y)
977
def _dragProgressCb(self, unused_action, unused_actor, delta_x, delta_y):
978
self.dragProgressed = True
979
coords = self.dragAction.get_motion_coords()
980
delta_x = coords[0] - self.dragBeginStartX
981
delta_y = coords[1] - self.dragBeginStartY
982
self.updateValue(delta_x, delta_y)
985
def _dragEndCb(self, unused_action, unused_actor, unused_event_x, unused_event_y, unused_modifiers):
987
if self.timelineElement.timeline.getActorUnderPointer() != self:
991
class URISourceElement(TimelineElement):
992
def __init__(self, bElement, timeline):
993
TimelineElement.__init__(self, bElement, timeline)
994
self.gotDragged = False
998
def hideHandles(self):
999
self.rightHandle.hide()
1000
self.leftHandle.hide()
1004
def _createGhostclip(self):
1005
self.ghostclip = Ghostclip(self.track_type, self.bElement)
1006
self.timeline.add_child(self.ghostclip)
1008
def _createHandles(self):
1009
self.leftHandle = TrimHandle(self, True)
1010
self.rightHandle = TrimHandle(self, False)
1012
self.leftHandle.set_position(0, 0)
1014
self.add_child(self.leftHandle)
1015
self.add_child(self.rightHandle)
1017
def _createBackground(self):
1018
if self.track_type == GES.TrackType.AUDIO:
1019
# Audio clips go from dark green to light green
1020
# (27, 46, 14, 255) to (73, 108, 33, 255)
1021
background = Gradient(27, 46, 14, 73, 108, 33)
1023
# Video clips go from almost black to gray
1024
# (15, 15, 15, 255) to (45, 45, 45, 255)
1025
background = Gradient(15, 15, 15, 45, 45, 45)
1026
background.bElement = self.bElement
1030
def _clickedCb(self, unused_action, unused_actor):
1031
#TODO : Let's be more specific, masks etc ..
1033
if self.timeline._container._controlMask:
1034
if not self.bElement.selected:
1036
self.timeline.current_group.add(self.bElement.get_toplevel_parent())
1038
self.timeline.current_group.remove(self.bElement.get_toplevel_parent())
1040
elif not self.bElement.selected:
1041
GES.Container.ungroup(self.timeline.current_group, False)
1042
self.timeline.current_group = GES.Group()
1043
self.timeline.current_group.add(self.bElement.get_toplevel_parent())
1044
self.timeline._container.gui.switchContextTab(self.bElement)
1046
children = self.bElement.get_toplevel_parent().get_children(True)
1047
selection = filter(lambda elem: isinstance(elem, GES.Source), children)
1049
self.timeline.selection.setSelection(selection, mode)
1051
if self.keyframedElement:
1052
self.showKeyframes(self.keyframedElement, self.prop)
1056
def _dragBeginCb(self, action, actor, event_x, event_y, modifiers):
1057
self.gotDragged = False
1058
mode = self.timeline._container.getEditionMode()
1060
# This can't change during a drag, so we can safely compute it now for drag events.
1061
nbrLayers = len(self.timeline.bTimeline.get_layers())
1062
self.brother = self.timeline.findBrother(self.bElement)
1063
self._dragBeginStart = self.bElement.get_start()
1064
self.dragBeginStartX = event_x
1065
self.dragBeginStartY = event_y
1067
self.nbrLayers = nbrLayers
1068
self.ghostclip.setNbrLayers(nbrLayers)
1069
self.ghostclip.setWidth(self.props.width)
1071
self.brother.ghostclip.setWidth(self.props.width)
1072
self.brother.ghostclip.setNbrLayers(nbrLayers)
1074
# We can also safely find if the object has a brother element
1075
self.setDragged(True)
1077
def _dragProgressCb(self, action, actor, delta_x, delta_y):
1078
# We can't use delta_x here because it fluctuates weirdly.
1079
if not self.gotDragged:
1080
self.gotDragged = True
1081
self._context = EditingContext(self.bElement,
1082
self.timeline.bTimeline,
1086
self.timeline._container.app.action_log)
1088
mode = self.timeline._container.getEditionMode()
1089
self._context.setMode(mode)
1091
coords = self.dragAction.get_motion_coords()
1092
delta_x = coords[0] - self.dragBeginStartX
1093
delta_y = coords[1] - self.dragBeginStartY
1094
y = coords[1] + self.timeline._container.point.y
1095
priority = self._getLayerForY(y)
1096
new_start = self._dragBeginStart + self.pixelToNs(delta_x)
1098
self.ghostclip.props.x = max(0, self.nsToPixel(self._dragBeginStart) + delta_x)
1099
self.ghostclip.update(priority, y, False)
1101
self.brother.ghostclip.props.x = max(0, self.nsToPixel(self._dragBeginStart) + delta_x)
1102
self.brother.ghostclip.update(priority, y, True)
1104
if not self.ghostclip.props.visible:
1105
self._context.editTo(new_start, self.bElement.get_parent().get_layer().get_priority())
1107
self._context.editTo(self._dragBeginStart, self.bElement.get_parent().get_layer().get_priority())
1109
self.timeline._updateSize(self.ghostclip)
1112
def _dragEndCb(self, action, actor, event_x, event_y, modifiers):
1113
coords = self.dragAction.get_motion_coords()
1114
delta_x = coords[0] - self.dragBeginStartX
1115
new_start = self._dragBeginStart + self.pixelToNs(delta_x)
1116
priority = self._getLayerForY(coords[1] + self.timeline._container.point.y)
1117
priority = min(priority, len(self.timeline.bTimeline.get_layers()))
1118
priority = max(0, priority)
1120
self.timeline._snapEndedCb()
1121
self.setDragged(False)
1123
self.ghostclip.props.visible = False
1125
self.brother.ghostclip.props.visible = False
1127
if self.ghostclip.shouldCreateLayer:
1128
self.timeline.createLayerForGhostClip(self.ghostclip)
1131
self._context.editTo(new_start, priority)
1132
self._context.finish()
1135
if self.preview and not type(self.preview) is Clutter.Actor:
1136
self.preview.cleanup()
1137
self.leftHandle.cleanup()
1138
self.leftHandle = None
1139
self.rightHandle.cleanup()
1140
self.rightHandle = None
1141
TimelineElement.cleanup(self)
1144
class TransitionElement(TimelineElement):
1145
def __init__(self, bElement, timeline):
1146
TimelineElement.__init__(self, bElement, timeline)
1147
self.isDragged = True
1148
self.set_reactive(True)
1150
def _createBackground(self):
1151
background = RoundedRectangle(0, 0, 0, 0)
1152
background.set_color(TRANSITION_COLOR)
1153
background.set_border_width(1)
1156
def _selectedChangedCb(self, selected, isSelected):
1157
TimelineElement._selectedChangedCb(self, selected, isSelected)
1160
self.timeline._container.app.gui.trans_list.activate(self.bElement)
1162
self.timeline._container.app.gui.trans_list.deactivate()
1164
def _clickedCb(self, action, actor):
1165
selection = {self.bElement}
1166
self.timeline.selection.setSelection(selection, SELECT)