~timo-jyrinki/ubuntu/trusty/pitivi/backport_utopic_fixes

« back to all changes in this revision

Viewing changes to pitivi/timeline/elements.py

  • Committer: Package Import Robot
  • Author(s): Sebastian Dröge
  • Date: 2014-04-05 15:28:16 UTC
  • mfrom: (6.1.13 sid)
  • Revision ID: package-import@ubuntu.com-20140405152816-6lijoax4cngiz5j5
Tags: 0.93-3
* debian/control:
  + Depend on python-gi (>= 3.10), older versions do not work
    with pitivi (Closes: #732813).
  + Add missing dependency on gir1.2-clutter-gst-2.0 (Closes: #743692).
  + Add suggests on gir1.2-notify-0.7 and gir1.2-gnomedesktop-3.0.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# -*- coding: utf-8 -*-
 
2
# Pitivi video editor
 
3
#
 
4
#       pitivi/timeline/elements.py
 
5
#
 
6
# Copyright (c) 2013, Mathieu Duponchelle <mduponchelle1@gmail.com>
 
7
#
 
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.
 
12
#
 
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.
 
17
#
 
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.
 
22
 
 
23
"""
 
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
 
27
"""
 
28
 
 
29
import cairo
 
30
import math
 
31
import os
 
32
from datetime import datetime
 
33
 
 
34
import weakref
 
35
 
 
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
 
39
 
 
40
import pitivi.configure as configure
 
41
from pitivi.utils.ui import EXPANDED_SIZE, SPACING, KEYFRAME_SIZE, CONTROL_WIDTH, create_cogl_color
 
42
 
 
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
 
50
 
 
51
BORDER_NORMAL_COLOR = create_cogl_color(100, 100, 100, 255)
 
52
BORDER_SELECTED_COLOR = create_cogl_color(200, 200, 10, 255)
 
53
 
 
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)
 
58
 
 
59
 
 
60
class RoundedRectangle(Clutter.Actor):
 
61
    """
 
62
    Custom actor used to draw a rectangle that can have rounded corners
 
63
    """
 
64
    __gtype_name__ = 'RoundedRectangle'
 
65
 
 
66
    def __init__(self, width, height, arc, step,
 
67
                 color=None, border_color=None, border_width=0):
 
68
        """
 
69
        Creates a new rounded rectangle
 
70
        """
 
71
        Clutter.Actor.__init__(self)
 
72
 
 
73
        self.props.width = width
 
74
        self.props.height = height
 
75
 
 
76
        self._arc = arc
 
77
        self._step = step
 
78
        self._border_width = border_width
 
79
        self._color = color
 
80
        self._border_color = border_color
 
81
 
 
82
    def do_paint(self):
 
83
        # Set a rectangle for the clipping
 
84
        Cogl.clip_push_rectangle(0, 0, self.props.width, self.props.height)
 
85
 
 
86
        if self._border_color:
 
87
            # draw the rectangle for the border which is the same size as the
 
88
            # object
 
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)
 
96
            Cogl.path_close()
 
97
 
 
98
            # set color to border color
 
99
            Cogl.set_source_color(self._border_color)
 
100
            Cogl.path_fill()
 
101
 
 
102
        if self._color:
 
103
            # draw the content with is the same size minus the width of the border
 
104
            # finish the clip
 
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)
 
109
            Cogl.path_close()
 
110
 
 
111
            # set the color of the filled area
 
112
            Cogl.set_source_color(self._color)
 
113
            Cogl.path_fill()
 
114
 
 
115
        Cogl.clip_pop()
 
116
 
 
117
    def get_color(self):
 
118
        return self._color
 
119
 
 
120
    def set_color(self, color):
 
121
        self._color = color
 
122
        self.queue_redraw()
 
123
 
 
124
    def get_border_width(self):
 
125
        return self._border_width
 
126
 
 
127
    def set_border_width(self, width):
 
128
        self._border_width = width
 
129
        self.queue_redraw()
 
130
 
 
131
    def set_border_color(self, color):
 
132
        self._border_color = color
 
133
        self.queue_redraw()
 
134
 
 
135
 
 
136
class Ghostclip(Clutter.Actor):
 
137
    """
 
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.
 
142
    """
 
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
 
150
 
 
151
    def setNbrLayers(self, nbrLayers):
 
152
        self.nbrLayers = nbrLayers
 
153
 
 
154
    def setWidth(self, width):
 
155
        self.props.width = width
 
156
 
 
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)
 
161
        y = max(0, y)
 
162
 
 
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)
 
166
 
 
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)
 
172
            else:
 
173
                y -= self.nbrLayers * (EXPANDED_SIZE + SPACING)
 
174
 
 
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
 
183
        else:
 
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
 
195
 
 
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))
 
200
 
 
201
        return priority
 
202
 
 
203
 
 
204
class TrimHandle(Clutter.Texture):
 
205
    def __init__(self, timelineElement, isLeft):
 
206
        Clutter.Texture.__init__(self)
 
207
 
 
208
        self.isLeft = isLeft
 
209
        self.timelineElement = weakref.proxy(timelineElement)
 
210
        self.dragAction = Clutter.DragAction()
 
211
 
 
212
        self.set_from_file(os.path.join(configure.get_pixmap_dir(), "trimbar-normal.png"))
 
213
        self.set_size(-1, EXPANDED_SIZE)
 
214
        self.hide()
 
215
        self.set_reactive(True)
 
216
 
 
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)
 
221
 
 
222
        self.connect("enter-event", self._enterEventCb)
 
223
        self.connect("leave-event", self._leaveEventCb)
 
224
 
 
225
        self.timelineElement.connect("enter-event", self._elementEnterEventCb)
 
226
        self.timelineElement.connect("leave-event", self._elementLeaveEventCb)
 
227
 
 
228
    def cleanup(self):
 
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)
 
233
 
 
234
    #Callbacks
 
235
 
 
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)
 
241
 
 
242
        self.set_from_file(os.path.join(configure.get_pixmap_dir(), "trimbar-focused.png"))
 
243
        if self.isLeft:
 
244
            self.timelineElement.timeline._container.embed.get_window().set_cursor(DRAG_LEFT_HANDLEBAR_CURSOR)
 
245
        else:
 
246
            self.timelineElement.timeline._container.embed.get_window().set_cursor(DRAG_RIGHT_HANDLEBAR_CURSOR)
 
247
 
 
248
    def _leaveEventCb(self, unused_actor, event):
 
249
        self.timelineElement.set_reactive(True)
 
250
        children = self.timelineElement.get_children()
 
251
 
 
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()
 
255
 
 
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)
 
260
 
 
261
    def _elementEnterEventCb(self, unused_actor, unused_event):
 
262
        self.show()
 
263
 
 
264
    def _elementLeaveEventCb(self, unused_actor, unused_event):
 
265
        self.hide()
 
266
 
 
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)
 
272
 
 
273
        if self.isLeft:
 
274
            edge = GES.Edge.EDGE_START
 
275
            self._dragBeginStart = self.timelineElement.bElement.get_parent().get_start()
 
276
        else:
 
277
            edge = GES.Edge.EDGE_END
 
278
            self._dragBeginStart = self.timelineElement.bElement.get_parent().get_duration() + \
 
279
                self.timelineElement.bElement.get_parent().get_start()
 
280
 
 
281
        self._context = EditingContext(elem,
 
282
                                       self.timelineElement.timeline.bTimeline,
 
283
                                       GES.EditMode.EDIT_TRIM,
 
284
                                       edge,
 
285
                                       None,
 
286
                                       self.timelineElement.timeline._container.app.action_log)
 
287
 
 
288
        self._context.connect("clip-trim", self.clipTrimCb)
 
289
        self._context.connect("clip-trim-finished", self.clipTrimFinishedCb)
 
290
 
 
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)
 
296
 
 
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())
 
299
        return False
 
300
 
 
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()
 
304
 
 
305
        self.timelineElement.set_reactive(True)
 
306
        for elem in self.timelineElement.get_children():
 
307
            elem.set_reactive(True)
 
308
 
 
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)
 
311
 
 
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)
 
315
 
 
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()
 
319
 
 
320
 
 
321
class TimelineElement(Clutter.Actor, Zoomable):
 
322
    """
 
323
    @ivar bElement: the backend element.
 
324
    @type bElement: GES.TrackElement
 
325
    @ivar timeline: the containing graphic timeline.
 
326
    @type timeline: TimelineStage
 
327
    """
 
328
 
 
329
    def __init__(self, bElement, timeline):
 
330
        Zoomable.__init__(self)
 
331
        Clutter.Actor.__init__(self)
 
332
 
 
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
 
339
        self.lines = []
 
340
        self.keyframes = []
 
341
        self.keyframesVisible = False
 
342
        self.source = None
 
343
        self.keyframedElement = None
 
344
        self.rightHandle = None
 
345
        self.isSelected = False
 
346
        size = self.bElement.get_duration()
 
347
 
 
348
        self.background = self._createBackground()
 
349
        self.background.set_position(0, 0)
 
350
        self.add_child(self.background)
 
351
 
 
352
        self.preview = self._createPreview()
 
353
        self.add_child(self.preview)
 
354
 
 
355
        self.border = self._createBorder()
 
356
        self.add_child(self.border)
 
357
 
 
358
        self.marquee = self._createMarquee()
 
359
        self.add_child(self.marquee)
 
360
 
 
361
        self._createHandles()
 
362
 
 
363
        self._linesMarker = self._createMarker()
 
364
        self._keyframesMarker = self._createMarker()
 
365
 
 
366
        self._createGhostclip()
 
367
 
 
368
        self.update(True)
 
369
        self.set_reactive(True)
 
370
 
 
371
        self._createMixingKeyframes()
 
372
 
 
373
        self._connectToEvents()
 
374
 
 
375
    # Public API
 
376
 
 
377
    def set_size(self, width, height, ease):
 
378
        if 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)
 
387
            if self.rightHandle:
 
388
                self.rightHandle.save_easing_state()
 
389
                self.rightHandle.set_easing_duration(600)
 
390
 
 
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)
 
399
        if self.rightHandle:
 
400
            self.rightHandle.set_position(width - self.rightHandle.props.width, 0)
 
401
 
 
402
        if ease:
 
403
            self.background.restore_easing_state()
 
404
            self.border.restore_easing_state()
 
405
            self.preview.restore_easing_state()
 
406
            if self.rightHandle:
 
407
                self.rightHandle.restore_easing_state()
 
408
            self.restore_easing_state()
 
409
 
 
410
    def addKeyframe(self, value, timestamp):
 
411
        self.source.set(timestamp, value)
 
412
        self.updateKeyframes()
 
413
 
 
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()
 
418
 
 
419
    def showKeyframes(self, element, propname, isDefault=False):
 
420
        binding = element.get_control_binding(propname.name)
 
421
        if not binding:
 
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"
 
426
                return
 
427
            binding = element.get_control_binding(propname.name)
 
428
 
 
429
        self.binding = binding
 
430
        self.prop = propname
 
431
        self.keyframedElement = element
 
432
        self.source = self.binding.props.control_source
 
433
 
 
434
        if isDefault:
 
435
            self.default_prop = propname
 
436
            self.default_element = element
 
437
 
 
438
        self.keyframesVisible = True
 
439
 
 
440
        self.updateKeyframes()
 
441
 
 
442
    def hideKeyframes(self):
 
443
        for keyframe in self.keyframes:
 
444
            self.remove_child(keyframe)
 
445
        self.keyframes = []
 
446
 
 
447
        self.keyframesVisible = False
 
448
 
 
449
        if self.isSelected:
 
450
            self.showKeyframes(self.default_element, self.default_prop)
 
451
 
 
452
        self.drawLines()
 
453
 
 
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)
 
458
 
 
459
    def drawLines(self, line=None):
 
460
        for line_ in self.lines:
 
461
            if line_ != line:
 
462
                self.remove_child(line_)
 
463
 
 
464
        if line:
 
465
            self.lines = [line]
 
466
        else:
 
467
            self.lines = []
 
468
 
 
469
        lastKeyframe = None
 
470
        for keyframe in self.keyframes:
 
471
            if lastKeyframe and (not line or lastKeyframe != line.previousKeyframe):
 
472
                self._createLine(keyframe, lastKeyframe, None)
 
473
            elif lastKeyframe:
 
474
                self._createLine(keyframe, lastKeyframe, line)
 
475
            lastKeyframe = keyframe
 
476
 
 
477
    def updateKeyframes(self):
 
478
        if not self.source:
 
479
            return
 
480
 
 
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)
 
487
 
 
488
        for keyframe in self.keyframes:
 
489
            self.remove_child(keyframe)
 
490
 
 
491
        self.keyframes = []
 
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)
 
498
 
 
499
        self.drawLines()
 
500
 
 
501
    def cleanup(self):
 
502
        Zoomable.__del__(self)
 
503
        self.disconnectFromEvents()
 
504
 
 
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)
 
514
 
 
515
    # private API
 
516
 
 
517
    def _createMarker(self):
 
518
        marker = Clutter.Actor()
 
519
        self.add_child(marker)
 
520
        return marker
 
521
 
 
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.
 
530
        size = max(size, 1)
 
531
        self.set_size(size, EXPANDED_SIZE, ease)
 
532
 
 
533
    def setDragged(self, dragged):
 
534
        brother = self.timeline.findBrother(self.bElement)
 
535
        if brother:
 
536
            brother.isDragged = dragged
 
537
        self.isDragged = dragged
 
538
 
 
539
    def _createMixingKeyframes(self):
 
540
        if self.track_type == GES.TrackType.VIDEO:
 
541
            propname = "alpha"
 
542
        else:
 
543
            propname = "volume"
 
544
 
 
545
        for spec in self.bElement.list_children_properties():
 
546
            if spec.name == propname:
 
547
                self.showKeyframes(self.bElement, spec, isDefault=True)
 
548
 
 
549
        self.hideKeyframes()
 
550
 
 
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)
 
555
        return keyframe
 
556
 
 
557
    def _createLine(self, keyframe, lastKeyframe, line):
 
558
        if not line:
 
559
            line = Line(self, keyframe, lastKeyframe)
 
560
            self.lines.append(line)
 
561
            self.insert_child_above(line, self._linesMarker)
 
562
 
 
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)
 
566
        if hyp < 1:
 
567
            # line length would be less than one pixel
 
568
            return
 
569
 
 
570
        sinX = opp / hyp
 
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()
 
577
 
 
578
    def _createGhostclip(self):
 
579
        pass
 
580
 
 
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)
 
587
        return border
 
588
 
 
589
    def _createBackground(self):
 
590
        raise NotImplementedError()
 
591
 
 
592
    def _createHandles(self):
 
593
        pass
 
594
 
 
595
    def _createPreview(self):
 
596
        if isinstance(self.bElement, GES.AudioUriSource):
 
597
            previewer = AudioPreviewer(self.bElement, self.timeline)
 
598
            previewer.startLevelsDiscoveryWhenIdle()
 
599
            return previewer
 
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()
 
604
 
 
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
 
610
        return marquee
 
611
 
 
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)
 
624
 
 
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))
 
629
        return priority
 
630
 
 
631
    # Interface (Zoomable)
 
632
 
 
633
    def zoomChanged(self):
 
634
        self.update(True)
 
635
        if self.isSelected:
 
636
            self.updateKeyframes()
 
637
 
 
638
    # Callbacks
 
639
 
 
640
    def _clickedCb(self, action, actor):
 
641
        pass
 
642
 
 
643
    def _dragBeginCb(self, action, actor, event_x, event_y, modifiers):
 
644
        pass
 
645
 
 
646
    def _dragProgressCb(self, unused_action, unused_actor, unused_delta_x, unused_delta_y):
 
647
        return False
 
648
 
 
649
    def _dragEndCb(self, action, actor, event_x, event_y, modifiers):
 
650
        pass
 
651
 
 
652
    def _durationChangedCb(self, unused_element, unused_duration):
 
653
        if self.keyframesVisible:
 
654
            self.updateKeyframes()
 
655
 
 
656
    def _inpointChangedCb(self, unused_element, unused_inpoint):
 
657
        if self.keyframesVisible:
 
658
            self.updateKeyframes()
 
659
 
 
660
    def _selectedChangedCb(self, unused_selected, isSelected):
 
661
        self.isSelected = isSelected
 
662
        if not isSelected:
 
663
            self.hideKeyframes()
 
664
        self.marquee.props.visible = isSelected
 
665
        color = BORDER_SELECTED_COLOR if isSelected else BORDER_NORMAL_COLOR
 
666
        self.border.set_border_color(color)
 
667
 
 
668
 
 
669
class Gradient(Clutter.Actor):
 
670
    def __init__(self, rb, gb, bb, re, ge, be):
 
671
        """
 
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.
 
675
        """
 
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()
 
685
 
 
686
    def _drawCb(self, unused_canvas, cr, unused_width, unused_height):
 
687
        cr.set_operator(cairo.OPERATOR_CLEAR)
 
688
        cr.paint()
 
689
        cr.set_operator(cairo.OPERATOR_OVER)
 
690
        cr.set_source(self.linear)
 
691
        cr.rectangle(0, 0, 10, EXPANDED_SIZE)
 
692
        cr.fill()
 
693
 
 
694
 
 
695
class Line(Clutter.Actor):
 
696
    """
 
697
    A cairo line used for keyframe curves.
 
698
    """
 
699
    def __init__(self, timelineElement, keyframe, lastKeyframe):
 
700
        Clutter.Actor.__init__(self)
 
701
        self.timelineElement = weakref.proxy(timelineElement)
 
702
 
 
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)
 
708
 
 
709
        self.gotDragged = False
 
710
 
 
711
        self.dragAction = Clutter.DragAction()
 
712
        self.add_action(self.dragAction)
 
713
 
 
714
        self.dragAction.connect("drag-begin", self._dragBeginCb)
 
715
        self.dragAction.connect("drag-end", self._dragEndCb)
 
716
        self.dragAction.connect("drag-progress", self._dragProgressCb)
 
717
 
 
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)
 
722
 
 
723
        self.previousKeyframe = lastKeyframe
 
724
        self.nextKeyframe = keyframe
 
725
 
 
726
    def _drawCb(self, unused_canvas, cr, width, unused_height):
 
727
        """
 
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.
 
731
        """
 
732
        cr.set_operator(cairo.OPERATOR_CLEAR)
 
733
        cr.paint()
 
734
        cr.set_operator(cairo.OPERATOR_OVER)
 
735
 
 
736
        # The "height budget" to draw line components = the tallest component...
 
737
        _max_height = KEYFRAME_SIZE
 
738
 
 
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.
 
742
 
 
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
 
748
        cr.stroke()
 
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
 
754
        cr.stroke()
 
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)
 
761
        cr.stroke()
 
762
 
 
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
 
767
        return x, y
 
768
 
 
769
    def _ungrab(self):
 
770
        self.timelineElement.set_reactive(True)
 
771
        self.timelineElement.timeline._container.embed.get_window().set_cursor(NORMAL_CURSOR)
 
772
 
 
773
    def _clickedCb(self, unused_actor, event):
 
774
        if self.gotDragged:
 
775
            self.gotDragged = False
 
776
            return
 
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)
 
781
 
 
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))
 
791
 
 
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)
 
795
 
 
796
    def _leaveEventCb(self, unused_actor, unused_event):
 
797
        self._ungrab()
 
798
 
 
799
    def _motionEventCb(self, actor, event):
 
800
        pass
 
801
 
 
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)
 
808
 
 
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
 
814
 
 
815
        self.previousKeyframe.updateValue(0, delta_y)
 
816
        self.nextKeyframe.updateValue(0, delta_y)
 
817
 
 
818
        return False
 
819
 
 
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:
 
824
            self._ungrab()
 
825
 
 
826
 
 
827
class KeyframeMenu(GtkClutter.Actor):
 
828
    def __init__(self, keyframe):
 
829
        GtkClutter.Actor.__init__(self)
 
830
        self.keyframe = keyframe
 
831
        vbox = Gtk.VBox()
 
832
 
 
833
        button = Gtk.Button()
 
834
        button.set_label("Remove")
 
835
        button.connect("clicked", self._removeClickedCb)
 
836
        vbox.pack_start(button, False, False, False)
 
837
 
 
838
        self.get_widget().add(vbox)
 
839
        self.vbox = vbox
 
840
        self.vbox.hide()
 
841
        self.set_reactive(True)
 
842
 
 
843
    def show(self):
 
844
        GtkClutter.Actor.show(self)
 
845
        self.vbox.show_all()
 
846
 
 
847
    def hide(self):
 
848
        GtkClutter.Actor.hide(self)
 
849
        self.vbox.hide()
 
850
 
 
851
    def _removeClickedCb(self, unused_button):
 
852
        self.keyframe.remove()
 
853
 
 
854
 
 
855
class Keyframe(Clutter.Actor):
 
856
    """
 
857
    @ivar has_changeable_time: if False, it means this is an edge keyframe.
 
858
    @type has_changeable_time: bool
 
859
    """
 
860
 
 
861
    def __init__(self, timelineElement, value, has_changeable_time):
 
862
        Clutter.Actor.__init__(self)
 
863
 
 
864
        self.value = value
 
865
        self.timelineElement = weakref.proxy(timelineElement)
 
866
        self.has_changeable_time = has_changeable_time
 
867
        self.lastClick = datetime.now()
 
868
 
 
869
        self.set_size(KEYFRAME_SIZE, KEYFRAME_SIZE)
 
870
        self.set_background_color(KEYFRAME_NORMAL_COLOR)
 
871
 
 
872
        self.dragAction = Clutter.DragAction()
 
873
        self.add_action(self.dragAction)
 
874
 
 
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)
 
881
 
 
882
        self.createMenu()
 
883
        self.dragProgressed = False
 
884
        self.set_reactive(True)
 
885
 
 
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)
 
890
 
 
891
    def _unselect(self):
 
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)
 
895
 
 
896
    def remove(self):
 
897
        # Can't remove edge keyframes !
 
898
        if not self.has_changeable_time:
 
899
            return
 
900
 
 
901
        self.timelineElement.timeline.remove_child(self.menu)
 
902
        self._unselect()
 
903
        self.timelineElement.removeKeyframe(self)
 
904
 
 
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:
 
908
            self.menu.hide()
 
909
 
 
910
    def _clickedCb(self, unused_actor, event):
 
911
        if (event.modifier_state & Clutter.ModifierType.CONTROL_MASK):
 
912
            self.remove()
 
913
        elif (datetime.now() - self.lastClick).total_seconds() < 0.5:
 
914
            self.remove()
 
915
 
 
916
        self.lastClick = datetime.now()
 
917
 
 
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)
 
922
 
 
923
    def _leaveEventCb(self, unused_actor, unused_event):
 
924
        self._unselect()
 
925
 
 
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
 
935
        self.line = line
 
936
 
 
937
    def endDrag(self):
 
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)
 
941
            self.menu.show()
 
942
 
 
943
        self.line = None
 
944
 
 
945
    def updateValue(self, delta_x, delta_y):
 
946
        newTs = self.tsStart + Zoomable.pixelToNs(delta_x)
 
947
        newValue = self.valueStart - (delta_y / EXPANDED_SIZE)
 
948
 
 
949
        # Don't overlap first and last keyframes.
 
950
        newTs = min(max(newTs, self.inpoint + 1), self.duration + self.inpoint - 1)
 
951
 
 
952
        newValue = min(max(newValue, 0.0), 1.0)
 
953
 
 
954
        if not self.has_changeable_time:
 
955
            newTs = self.lastTs
 
956
 
 
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
 
962
            self.lastTs = newTs
 
963
 
 
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.
 
970
            if not self.line:
 
971
                self.timelineElement.timeline._container.seekInPosition(newTs + self.start)
 
972
 
 
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)
 
976
 
 
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)
 
983
        return False
 
984
 
 
985
    def _dragEndCb(self, unused_action, unused_actor, unused_event_x, unused_event_y, unused_modifiers):
 
986
        self.endDrag()
 
987
        if self.timelineElement.timeline.getActorUnderPointer() != self:
 
988
            self._unselect()
 
989
 
 
990
 
 
991
class URISourceElement(TimelineElement):
 
992
    def __init__(self, bElement, timeline):
 
993
        TimelineElement.__init__(self, bElement, timeline)
 
994
        self.gotDragged = False
 
995
 
 
996
    # public API
 
997
 
 
998
    def hideHandles(self):
 
999
        self.rightHandle.hide()
 
1000
        self.leftHandle.hide()
 
1001
 
 
1002
    # private API
 
1003
 
 
1004
    def _createGhostclip(self):
 
1005
        self.ghostclip = Ghostclip(self.track_type, self.bElement)
 
1006
        self.timeline.add_child(self.ghostclip)
 
1007
 
 
1008
    def _createHandles(self):
 
1009
        self.leftHandle = TrimHandle(self, True)
 
1010
        self.rightHandle = TrimHandle(self, False)
 
1011
 
 
1012
        self.leftHandle.set_position(0, 0)
 
1013
 
 
1014
        self.add_child(self.leftHandle)
 
1015
        self.add_child(self.rightHandle)
 
1016
 
 
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)
 
1022
        else:
 
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
 
1027
        return background
 
1028
 
 
1029
    # Callbacks
 
1030
    def _clickedCb(self, unused_action, unused_actor):
 
1031
        #TODO : Let's be more specific, masks etc ..
 
1032
        mode = SELECT
 
1033
        if self.timeline._container._controlMask:
 
1034
            if not self.bElement.selected:
 
1035
                mode = SELECT_ADD
 
1036
                self.timeline.current_group.add(self.bElement.get_toplevel_parent())
 
1037
            else:
 
1038
                self.timeline.current_group.remove(self.bElement.get_toplevel_parent())
 
1039
                mode = UNSELECT
 
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)
 
1045
 
 
1046
        children = self.bElement.get_toplevel_parent().get_children(True)
 
1047
        selection = filter(lambda elem: isinstance(elem, GES.Source), children)
 
1048
 
 
1049
        self.timeline.selection.setSelection(selection, mode)
 
1050
 
 
1051
        if self.keyframedElement:
 
1052
            self.showKeyframes(self.keyframedElement, self.prop)
 
1053
 
 
1054
        return False
 
1055
 
 
1056
    def _dragBeginCb(self, action, actor, event_x, event_y, modifiers):
 
1057
        self.gotDragged = False
 
1058
        mode = self.timeline._container.getEditionMode()
 
1059
 
 
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
 
1066
 
 
1067
        self.nbrLayers = nbrLayers
 
1068
        self.ghostclip.setNbrLayers(nbrLayers)
 
1069
        self.ghostclip.setWidth(self.props.width)
 
1070
        if self.brother:
 
1071
            self.brother.ghostclip.setWidth(self.props.width)
 
1072
            self.brother.ghostclip.setNbrLayers(nbrLayers)
 
1073
 
 
1074
        # We can also safely find if the object has a brother element
 
1075
        self.setDragged(True)
 
1076
 
 
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,
 
1083
                                           None,
 
1084
                                           GES.Edge.EDGE_NONE,
 
1085
                                           None,
 
1086
                                           self.timeline._container.app.action_log)
 
1087
 
 
1088
        mode = self.timeline._container.getEditionMode()
 
1089
        self._context.setMode(mode)
 
1090
 
 
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)
 
1097
 
 
1098
        self.ghostclip.props.x = max(0, self.nsToPixel(self._dragBeginStart) + delta_x)
 
1099
        self.ghostclip.update(priority, y, False)
 
1100
        if self.brother:
 
1101
            self.brother.ghostclip.props.x = max(0, self.nsToPixel(self._dragBeginStart) + delta_x)
 
1102
            self.brother.ghostclip.update(priority, y, True)
 
1103
 
 
1104
        if not self.ghostclip.props.visible:
 
1105
            self._context.editTo(new_start, self.bElement.get_parent().get_layer().get_priority())
 
1106
        else:
 
1107
            self._context.editTo(self._dragBeginStart, self.bElement.get_parent().get_layer().get_priority())
 
1108
 
 
1109
        self.timeline._updateSize(self.ghostclip)
 
1110
        return False
 
1111
 
 
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)
 
1119
 
 
1120
        self.timeline._snapEndedCb()
 
1121
        self.setDragged(False)
 
1122
 
 
1123
        self.ghostclip.props.visible = False
 
1124
        if self.brother:
 
1125
            self.brother.ghostclip.props.visible = False
 
1126
 
 
1127
        if self.ghostclip.shouldCreateLayer:
 
1128
            self.timeline.createLayerForGhostClip(self.ghostclip)
 
1129
 
 
1130
        if self.gotDragged:
 
1131
            self._context.editTo(new_start, priority)
 
1132
            self._context.finish()
 
1133
 
 
1134
    def cleanup(self):
 
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)
 
1142
 
 
1143
 
 
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)
 
1149
 
 
1150
    def _createBackground(self):
 
1151
        background = RoundedRectangle(0, 0, 0, 0)
 
1152
        background.set_color(TRANSITION_COLOR)
 
1153
        background.set_border_width(1)
 
1154
        return background
 
1155
 
 
1156
    def _selectedChangedCb(self, selected, isSelected):
 
1157
        TimelineElement._selectedChangedCb(self, selected, isSelected)
 
1158
 
 
1159
        if isSelected:
 
1160
            self.timeline._container.app.gui.trans_list.activate(self.bElement)
 
1161
        else:
 
1162
            self.timeline._container.app.gui.trans_list.deactivate()
 
1163
 
 
1164
    def _clickedCb(self, action, actor):
 
1165
        selection = {self.bElement}
 
1166
        self.timeline.selection.setSelection(selection, SELECT)
 
1167
        return False