timeline.py 32.2 KB
Newer Older
1
# -*- coding: utf-8 -*-
2
# Pitivi video editor
3 4 5 6 7 8 9 10 11 12 13 14 15 16
# Copyright (c) 2009, Alessandro Decina <alessandro.d@gmail.com>
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this program; if not, write to the
Hicham HAOUARI's avatar
Hicham HAOUARI committed
17 18
# Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
# Boston, MA 02110-1301, USA.
19
from gi.repository import GES
20
from gi.repository import GObject
21
from gi.repository import Gst
22
from gi.repository import GstController
23

24
from pitivi.effects import PROPS_TO_IGNORE
25
from pitivi.undo.undo import Action
26
from pitivi.undo.undo import FinalizingAction
27
from pitivi.undo.undo import GObjectObserver
28
from pitivi.undo.undo import MetaContainerObserver
29
from pitivi.undo.undo import UndoableAction
30
from pitivi.undo.undo import UndoableAutomaticObjectAction
31
from pitivi.utils.loggable import Loggable
32 33


34 35 36
TRANSITION_PROPS = ["border", "invert", "transition-type"]


37 38 39 40
def child_property_name(pspec):
    return "%s::%s" % (pspec.owner_type.name, pspec.name)


41 42 43 44 45 46 47 48
class CommitTimelineFinalizingAction(FinalizingAction):
    def __init__(self, pipeline):
        self.__pipeline = pipeline

    def do(self):
        self.__pipeline.commit_timeline()


49 50 51 52 53 54 55 56 57
class TrackElementPropertyChanged(UndoableAction):

    def __init__(self, track_element, property_name, old_value, new_value):
        UndoableAction.__init__(self)
        self.track_element = track_element
        self.property_name = property_name
        self.old_value = old_value
        self.new_value = new_value

58 59 60 61 62 63
    def __repr__(self):
        return "<TrackElementPropertyChanged %s.%s: %s -> %s>" % (self.track_element,
                                                                  self.property_name,
                                                                  self.old_value,
                                                                  self.new_value)

64 65 66 67 68 69 70 71
    def do(self):
        self.track_element.set_child_property(
            self.property_name, self.new_value)

    def undo(self):
        self.track_element.set_child_property(
            self.property_name, self.old_value)

72
    def asScenarioAction(self):
73 74 75
        st = Gst.Structure.new_empty("set-child-property")
        st['element-name'] = self.track_element.get_name()
        st['property'] = self.property_name
76 77 78 79
        value = self.new_value
        if isinstance(self.new_value, GObject.GFlags) or\
                isinstance(self.new_value, GObject.GEnum):
            value = int(self.new_value)
80 81 82

        _, _, pspec = self.track_element.lookup_child(self.property_name)
        st['value'] = GObject.Value(pspec.value_type, value)
83 84 85
        return st


86
class TimelineElementObserver(Loggable):
87 88 89
    """Monitors the props of an element and all its children.

    Reports UndoableActions.
90 91 92

    Attributes:
        ges_timeline_element (GES.TimelineElement): The object to be monitored.
93 94
    """

95
    def __init__(self, ges_timeline_element, action_log):
96
        Loggable.__init__(self)
97
        self.ges_timeline_element = ges_timeline_element
98 99
        self.action_log = action_log

100 101
        self._properties = {}
        for prop in ges_timeline_element.list_children_properties():
102 103 104 105
            if prop.name in PROPS_TO_IGNORE:
                continue

            prop_name = child_property_name(prop)
106 107 108
            res, value = ges_timeline_element.get_child_property(prop_name)
            assert res, prop_name
            self._properties[prop_name] = value
109

110
        ges_timeline_element.connect('deep-notify', self._property_changed_cb)
111

112 113 114
    def release(self):
        self.ges_timeline_element.disconnect_by_func(self._property_changed_cb)
        self.ges_timeline_element = None
115

116 117
    def _property_changed_cb(self, ges_timeline_element, unused_gst_element, pspec):
        prop_name = child_property_name(pspec)
118 119 120
        if pspec.name in PROPS_TO_IGNORE:
            return

121 122
        if ges_timeline_element.get_control_binding(prop_name):
            self.debug("Property %s controlled", prop_name)
123 124
            return

125 126 127
        old_value = self._properties[prop_name]
        res, new_value = ges_timeline_element.get_child_property(prop_name)
        assert res, prop_name
128 129 130 131
        if old_value == new_value:
            # Nothing to see here.
            return

132
        action = TrackElementPropertyChanged(
133 134
            ges_timeline_element, prop_name, old_value, new_value)
        self._properties[prop_name] = new_value
135 136 137
        self.action_log.push(action)


138 139 140 141 142 143 144 145 146 147 148
class TrackElementObserver(TimelineElementObserver):
    """Monitors the props of a track element.

    Reports UndoableActions.

    Args:
        ges_track_element (GES.TrackElement): The object to be monitored.
    """

    def __init__(self, ges_track_element, action_log):
        TimelineElementObserver.__init__(self, ges_track_element, action_log)
149 150 151 152 153
        if isinstance(ges_track_element, GES.BaseEffect):
            property_names = ("active", "priority",)
        else:
            property_names = ("active",)
        self.gobject_observer = GObjectObserver(ges_track_element, property_names, action_log)
154 155 156 157 158 159

    def release(self):
        TimelineElementObserver.release(self)
        self.gobject_observer.release()


160
class TrackElementAction(UndoableAction):
161

162
    def __init__(self, clip, track_element):
163 164 165 166
        UndoableAction.__init__(self)
        self.clip = clip
        self.track_element = track_element
        self.track_element_props = []
167
        for prop in self.track_element.list_children_properties():
168
            if not prop.flags & GObject.ParamFlags.WRITABLE or \
169 170 171 172 173 174
                    prop.name in PROPS_TO_IGNORE:
                continue
            prop_name = child_property_name(prop)
            res, value = self.track_element.get_child_property(prop_name)
            assert res
            self.track_element_props.append((prop_name, value))
175

176
    def add(self):
177
        assert self.clip.add(self.track_element)
178 179 180
        for prop_name, prop_value in self.track_element_props:
            self.track_element.set_child_property(prop_name, prop_value)

181
    def remove(self):
182 183
        self.clip.remove(self.track_element)

184

185 186 187 188
class TrackElementAdded(TrackElementAction):

    def __repr__(self):
        return "<TrackElementAdded %s, %s>" % (self.clip, self.track_element)
189 190 191 192 193 194 195

    def do(self):
        self.add()

    def undo(self):
        self.remove()

196
    def asScenarioAction(self):
197 198 199
        st = Gst.Structure.new_empty("container-add-child")
        st["container-name"] = self.clip.get_name()
        st["asset-id"] = self.track_element.get_id()
200 201 202
        asset = self.track_element.get_asset()
        if asset:
            st["child-type"] = GObject.type_name(asset.get_extractable_type())
203 204 205
        return st


206 207 208 209
class TrackElementRemoved(TrackElementAction):

    def __repr__(self):
        return "<TrackElementRemoved %s, %s>" % (self.clip, self.track_element)
210 211

    def do(self):
212
        self.remove()
213 214

    def undo(self):
215
        self.add()
216

217
    def asScenarioAction(self):
218 219 220 221
        st = Gst.Structure.new_empty("container-remove-child")
        st["container-name"] = self.clip.get_name()
        st["child-name"] = self.track_element.get_name()
        return st
222

223

224
class ControlSourceObserver(GObject.Object):
225
    """Monitors a control source's props and reports UndoableActions.
226

227 228 229 230
    Attributes:
        control_source (GstController.TimedValueControlSource): The object to be
            monitored.
    """
231

232
    def __init__(self, control_source, action_log, action_info):
233
        GObject.Object.__init__(self)
234

235 236
        self.action_log = action_log
        self.action_info = action_info
237
        self.control_source = control_source
238

239
        self.keyframes = {}
240
        for keyframe in self.control_source.get_all():
241
            self.keyframes[keyframe.timestamp] = (keyframe.timestamp, keyframe.value)
242

243 244 245
        control_source.connect("value-added", self._keyframe_added_cb)
        control_source.connect("value-changed", self._keyframe_moved_cb)
        control_source.connect("value-removed", self._keyframe_removed_cb)
246

247
    def release(self):
248 249 250
        self.control_source.disconnect_by_func(self._keyframe_added_cb)
        self.control_source.disconnect_by_func(self._keyframe_moved_cb)
        self.control_source.disconnect_by_func(self._keyframe_removed_cb)
251
        self.control_source = None
252

253 254
    def _keyframe_added_cb(self, control_source, keyframe):
        self.keyframes[keyframe.timestamp] = (keyframe.timestamp, keyframe.value)
255

256 257
        action = KeyframeAddedAction(control_source, keyframe, self.action_info)
        self.action_log.push(action)
258

259
    def _keyframe_moved_cb(self, control_source, keyframe):
260
        old_snapshot = self.keyframes[keyframe.timestamp]
261
        new_snapshot = (keyframe.timestamp, keyframe.value)
262 263
        self.keyframes[keyframe.timestamp] = new_snapshot

264 265 266 267 268 269
        action = KeyframeChangedAction(control_source,
                                       old_snapshot, new_snapshot)
        self.action_log.push(action)

    def _keyframe_removed_cb(self, control_source, keyframe):
        del self.keyframes[keyframe.timestamp]
270

271 272 273
        action = KeyframeRemovedAction(control_source, keyframe,
                                       self.action_info)
        self.action_log.push(action)
274

275

276
class ClipAction(UndoableAction):
277

278
    def __init__(self, layer, clip):
279
        UndoableAction.__init__(self)
280
        self.layer = layer
281
        self.clip = clip
282

283
    def add(self):
284
        self.clip.set_name(None)
285
        children = self.clip.get_children(False)
286
        self.layer.add_clip(self.clip)
287 288 289 290
        # GES adds children if the clip had none. Make sure they are removed.
        for child in self.clip.get_children(False):
            if child not in children:
                self.clip.remove(child)
291
        self.layer.get_timeline().get_asset().pipeline.commit_timeline()
292

293 294 295 296
    def _child_added_cb(self, clip, track_element):
        clip.remove(track_element)

    def remove(self):
297
        self.layer.remove_clip(self.clip)
298
        self.layer.get_timeline().get_asset().pipeline.commit_timeline()
299

300 301 302 303 304 305 306 307 308 309 310 311

class ClipAdded(ClipAction):

    def __repr__(self):
        return "<ClipAdded %s>" % self.clip

    def do(self):
        self.add()

    def undo(self):
        self.remove()

312
    def asScenarioAction(self):
Thibault Saunier's avatar
Thibault Saunier committed
313
        timeline = self.layer.get_timeline()
314 315 316
        if hasattr(self.layer, "splitting_object") and \
                self.layer.splitting_object is True:
            return None
Thibault Saunier's avatar
Thibault Saunier committed
317
        elif hasattr(timeline, "ui") and timeline.ui and timeline.ui.editing_context is not None:
318
            return None
319

320 321 322 323 324 325 326 327 328 329
        st = Gst.Structure.new_empty("add-clip")
        st.set_value("name", self.clip.get_name())
        st.set_value("layer-priority", self.layer.props.priority)
        st.set_value("asset-id", self.clip.get_asset().get_id())
        st.set_value("type", GObject.type_name(self.clip))
        st.set_value("start", float(self.clip.props.start / Gst.SECOND))
        st.set_value("inpoint", float(self.clip.props.in_point / Gst.SECOND))
        st.set_value("duration", float(self.clip.props.duration / Gst.SECOND))
        return st

330

331
class ClipRemoved(ClipAction):
332

333
    def __init__(self, layer, clip):
334
        ClipAction.__init__(self, layer, clip)
335 336
        self.transition_removed_actions = []

337 338 339
    def __repr__(self):
        return "<ClipRemoved %s>" % self.clip

340 341 342 343 344
    def expand(self, action):
        if not isinstance(action, TransitionClipRemovedAction):
            return False
        self.transition_removed_actions.append(action)
        return True
345 346

    def do(self):
347
        self.remove()
348 349

    def undo(self):
350
        self.add()
351 352 353
        # Update the automatically created transitions.
        for action in self.transition_removed_actions:
            action.undo()
354

355
    def asScenarioAction(self):
Thibault Saunier's avatar
Thibault Saunier committed
356 357 358
        timeline = self.layer.get_timeline()
        if hasattr(timeline, "ui") and timeline.ui\
                and timeline.ui.editing_context is not None:
359 360
            return None

361 362 363 364
        st = Gst.Structure.new_empty("remove-clip")
        st.set_value("name", self.clip.get_name())
        return st

365

366 367 368 369 370 371 372 373 374 375 376
class TransitionClipAction(UndoableAction):

    def __init__(self, ges_layer, ges_clip, track_element):
        UndoableAction.__init__(self)
        self.ges_layer = ges_layer
        self.start = ges_clip.props.start
        self.duration = ges_clip.props.duration
        self.track_element = track_element

    @staticmethod
    def get_video_element(ges_clip):
377
        for track_element in ges_clip.get_children(recursive=True):
378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416
            if isinstance(track_element, GES.VideoTransition):
                return track_element
        return None

    def find_video_transition(self):
        for ges_clip in self.ges_layer.get_clips():
            if isinstance(ges_clip, GES.TransitionClip) and \
                    ges_clip.props.start == self.start and \
                    ges_clip.props.duration == self.duration:
                # Got the transition clip, now find its video element, if any.
                track_element = TransitionClipAction.get_video_element(ges_clip)
                if not track_element:
                    # Probably the audio transition clip.
                    continue
                # Double lucky!
                return track_element


class TransitionClipAddedAction(TransitionClipAction):

    @classmethod
    def new(cls, ges_layer, ges_clip):
        track_element = cls.get_video_element(ges_clip)
        if not track_element:
            return None
        return cls(ges_layer, ges_clip, track_element)

    def do(self):
        """Searches the transition clip created automatically to update it."""
        track_element = self.find_video_transition()
        assert track_element
        UndoableAutomaticObjectAction.update_object(self.track_element, track_element)

    def undo(self):
        # The transition is being removed, nothing to do.
        pass


class TransitionClipRemovedAction(TransitionClipAction):
417 418 419 420 421 422 423 424 425 426 427

    def __init__(self, ges_layer, ges_clip, track_element):
        UndoableAction.__init__(self)
        self.ges_layer = ges_layer
        self.start = ges_clip.props.start
        self.duration = ges_clip.props.duration
        self.track_element = track_element

        self.properties = []
        for property_name in TRANSITION_PROPS:
            field_name = property_name.replace("-", "_")
428
            value = self.track_element.get_property(field_name)
429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453
            self.properties.append((property_name, value))

    @classmethod
    def new(cls, ges_layer, ges_clip):
        track_element = cls.get_video_element(ges_clip)
        if not track_element:
            return None
        return cls(ges_layer, ges_clip, track_element)

    def do(self):
        # The transition is being removed, nothing to do.
        pass

    def undo(self):
        # Search the transition clip created automatically to update it.
        for ges_clip in self.ges_layer.get_clips():
            if isinstance(ges_clip, GES.TransitionClip) and \
                    ges_clip.props.start == self.start and \
                    ges_clip.props.duration == self.duration:
                # Got the transition clip, now find its video element, if any.
                track_element = self.get_video_element(ges_clip)
                if not track_element:
                    # Probably the audio transition clip.
                    continue
                # Double lucky!
454
                UndoableAutomaticObjectAction.update_object(self.track_element, track_element)
455 456 457 458 459
                for prop_name, value in self.properties:
                    track_element.set_property(prop_name, value)
                break


460
class LayerAdded(UndoableAction):
461

462 463 464 465
    def __init__(self, ges_timeline, ges_layer):
        UndoableAction.__init__(self)
        self.ges_timeline = ges_timeline
        self.ges_layer = ges_layer
466 467

    def do(self):
468
        self.ges_timeline.add_layer(self.ges_layer)
469 470

    def undo(self):
471 472
        self.ges_timeline.remove_layer(self.ges_layer)
        self.ges_timeline.get_asset().pipeline.commit_timeline()
473

474
    def asScenarioAction(self):
475
        st = Gst.Structure.new_empty("add-layer")
476 477
        st.set_value("priority", self.ges_layer.props.priority)
        st.set_value("auto-transition", self.ges_layer.props.auto_transition)
478 479
        return st

480 481

class LayerRemoved(UndoableAction):
482

483 484 485 486
    def __init__(self, ges_timeline, ges_layer):
        UndoableAction.__init__(self)
        self.ges_timeline = ges_timeline
        self.ges_layer = ges_layer
487

488 489 490
    def __repr__(self):
        return "<LayerRemoved %s>" % self.ges_layer

491
    def do(self):
492 493
        self.ges_timeline.remove_layer(self.ges_layer)
        self.ges_timeline.get_asset().pipeline.commit_timeline()
494 495

    def undo(self):
496
        self.ges_timeline.add_layer(self.ges_layer)
497

498
    def asScenarioAction(self):
499
        st = Gst.Structure.new_empty("remove-layer")
500
        st.set_value("priority", self.ges_layer.props.priority)
501 502
        return st

503

504 505 506 507 508 509 510 511
class LayerMoved(UndoableAction):

    def __init__(self, ges_layer, old_priority, priority):
        UndoableAction.__init__(self)
        self.ges_layer = ges_layer
        self.old_priority = old_priority
        self.priority = priority

512 513 514 515 516
    def __repr__(self):
        return "<LayerMoved %s: %s -> %s>" % (self.ges_layer,
                                              self.old_priority,
                                              self.priority)

517 518 519 520 521 522 523 524 525 526 527 528
    def do(self):
        self.ges_layer.props.priority = self.priority

    def undo(self):
        self.ges_layer.props.priority = self.old_priority

    def asScenarioAction(self):
        st = Gst.Structure.new_empty("move-layer")
        st.set_value("priority", self.ges_layer.props.priority)
        return st


529
class KeyframeAddedAction(UndoableAction):
530

531
    def __init__(self, control_source, keyframe, action_info):
532
        UndoableAction.__init__(self)
533
        self.control_source = control_source
534
        self.keyframe = keyframe
535
        self.action_info = action_info
536 537

    def do(self):
538
        self.control_source.set(self.keyframe.timestamp, self.keyframe.value)
539 540

    def undo(self):
541
        self.control_source.unset(self.keyframe.timestamp)
542

543
    def asScenarioAction(self):
544
        st = Gst.Structure.new_empty("add-keyframe")
545 546
        for key, value in self.action_info.items():
            st.set_value(key, value)
547 548 549 550
        st.set_value("timestamp", float(self.keyframe.timestamp / Gst.SECOND))
        st.set_value("value", self.keyframe.value)
        return st

551

552
class KeyframeRemovedAction(UndoableAction):
553

554
    def __init__(self, control_source, keyframe, action_info):
555
        UndoableAction.__init__(self)
556
        self.control_source = control_source
557
        self.keyframe = keyframe
558
        self.action_info = action_info
559 560

    def do(self):
561
        self.control_source.unset(self.keyframe.timestamp)
562 563

    def undo(self):
564
        self.control_source.set(self.keyframe.timestamp, self.keyframe.value)
565

566
    def asScenarioAction(self):
567
        st = Gst.Structure.new_empty("remove-keyframe")
568 569
        for key, value in self.action_info.items():
            st.set_value(key, value)
570 571 572 573
        st.set_value("timestamp", float(self.keyframe.timestamp / Gst.SECOND))
        st.set_value("value", self.keyframe.value)
        return st

574

575
class KeyframeChangedAction(UndoableAction):
576

577
    def __init__(self, control_source, old_snapshot, new_snapshot):
578
        UndoableAction.__init__(self)
579
        self.control_source = control_source
580 581 582 583
        self.old_snapshot = old_snapshot
        self.new_snapshot = new_snapshot

    def do(self):
584
        self._applySnapshot(self.new_snapshot)
585 586

    def undo(self):
587
        self._applySnapshot(self.old_snapshot)
588

589
    def _applySnapshot(self, snapshot):
590
        time, value = snapshot
591
        self.control_source.set(time, value)
592

593

594
class ControlSourceSetAction(UndoableAction):
595

596 597 598 599 600 601 602 603 604 605 606 607 608
    def __init__(self, track_element, binding):
        UndoableAction.__init__(self)
        self.track_element = track_element
        self.control_source = binding.props.control_source
        self.property_name = binding.props.name
        self.binding_type = "direct-absolute" if binding.props.absolute else "direct"

    def do(self):
        self.track_element.set_control_source(self.control_source,
                                              self.property_name, self.binding_type)

    def undo(self):
        assert self.track_element.remove_control_binding(self.property_name)
609 610 611

    def asScenarioAction(self):
        st = Gst.Structure.new_empty("set-control-source")
612 613 614
        st.set_value("element-name", self.track_element.get_name())
        st.set_value("property-name", self.property_name)
        st.set_value("binding-type", self.binding_type)
615 616 617 618 619
        st.set_value("source-type", "interpolation")
        st.set_value("interpolation-mode", "linear")
        return st


620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641
class ControlSourceRemoveAction(UndoableAction):

    def __init__(self, track_element, binding):
        UndoableAction.__init__(self)
        self.track_element = track_element
        self.control_source = binding.props.control_source
        self.property_name = binding.props.name
        self.binding_type = "direct-absolute" if binding.props.absolute else "direct"

    def do(self):
        assert self.track_element.remove_control_binding(self.property_name)

    def undo(self):
        self.track_element.set_control_source(self.control_source,
                                              self.property_name, self.binding_type)

    def asScenarioAction(self):
        st = Gst.Structure.new_empty("remove-control-source")
        st.set_value("element-name", self.track_element.get_name())
        st.set_value("property-name", self.property_name)
        return st

642

643 644 645 646 647
class LayerObserver(MetaContainerObserver, Loggable):
    """Monitors a Layer and reports UndoableActions.

    Args:
        ges_layer (GES.Layer): The layer to observe.
648 649

    Attributes:
650
        action_log (UndoableActionLog): The action log where to report actions.
651 652
    """

653 654
    def __init__(self, ges_layer, action_log):
        MetaContainerObserver.__init__(self, ges_layer, action_log)
655
        Loggable.__init__(self)
656
        self.action_log = action_log
657
        self.priority = ges_layer.props.priority
658

659 660 661
        self.keyframe_observers = {}
        self.track_element_observers = {}

662 663
        ges_layer.connect("clip-added", self._clipAddedCb)
        ges_layer.connect("clip-removed", self._clipRemovedCb)
664
        ges_layer.connect("notify::priority", self.__layer_moved_cb)
665

666
        self.clip_observers = {}
667 668 669
        for ges_clip in ges_layer.get_clips():
            self._connectToClip(ges_clip)

670 671 672
    def _connectToClip(self, ges_clip):
        ges_clip.connect("child-added", self._clipTrackElementAddedCb)
        ges_clip.connect("child-removed", self._clipTrackElementRemovedCb)
673

674
        for track_element in ges_clip.get_children(recursive=True):
675
            self._connectToTrackElement(track_element)
676

677
        if isinstance(ges_clip, GES.TransitionClip):
678 679
            return

680 681 682 683 684 685 686 687
        props = ["start", "duration", "in-point", "priority"]
        clip_observer = GObjectObserver(ges_clip, props, self.action_log)
        self.clip_observers[ges_clip] = clip_observer

    def _disconnectFromClip(self, ges_clip):
        ges_clip.disconnect_by_func(self._clipTrackElementAddedCb)
        ges_clip.disconnect_by_func(self._clipTrackElementRemovedCb)

688
        for child in ges_clip.get_children(recursive=True):
Thibault Saunier's avatar
Thibault Saunier committed
689 690
            self._disconnectFromTrackElement(child)

691 692 693 694
        if isinstance(ges_clip, GES.TransitionClip):
            return

        clip_observer = self.clip_observers.pop(ges_clip)
695
        clip_observer.release()
696

697
    def _control_binding_added_cb(self, track_element, binding):
698
        self._connectToControlSource(track_element, binding)
699 700 701 702 703 704
        action = ControlSourceSetAction(track_element, binding)
        self.action_log.push(action)

    def _control_binding_removed_cb(self, track_element, binding):
        self._disconnectFromControlSource(binding)
        action = ControlSourceRemoveAction(track_element, binding)
705
        self.action_log.push(action)
706

707
    def _connectToTrackElement(self, track_element):
708
        if isinstance(track_element, GES.VideoTransition):
709 710 711 712 713 714
            ges_clip = track_element.get_toplevel_parent()
            ges_layer = ges_clip.props.layer
            action = TransitionClipAddedAction(ges_layer, ges_clip,
                                               track_element)
            self.action_log.push(action)

715 716
            observer = GObjectObserver(track_element, TRANSITION_PROPS,
                                       self.action_log)
717 718 719
            self.track_element_observers[track_element] = observer
            return

720
        for prop, binding in track_element.get_all_control_bindings().items():
721
            self._connectToControlSource(track_element, binding)
722
        track_element.connect("control-binding-added",
723 724 725
                              self._control_binding_added_cb)
        track_element.connect("control-binding-removed",
                              self._control_binding_removed_cb)
726 727
        if isinstance(track_element, GES.BaseEffect) or \
                isinstance(track_element, GES.VideoSource):
728
            observer = TrackElementObserver(track_element, self.action_log)
729
            self.track_element_observers[track_element] = observer
730

731
    def _disconnectFromTrackElement(self, track_element):
732 733 734
        if not isinstance(track_element, GES.VideoTransition):
            track_element.disconnect_by_func(self._control_binding_added_cb)
            track_element.disconnect_by_func(self._control_binding_removed_cb)
735 736
        for prop, binding in track_element.get_all_control_bindings().items():
            self._disconnectFromControlSource(binding)
737 738 739 740
        observer = self.track_element_observers.pop(track_element, None)
        # We only keep track of some track_elements.
        if observer:
            observer.release()
741

742
    def _connectToControlSource(self, track_element, binding):
743
        control_source = binding.props.control_source
744 745
        action_info = {"element-name": track_element.get_name(),
                       "property-name": binding.props.name}
746 747 748 749
        if control_source not in self.keyframe_observers:
            observer = ControlSourceObserver(control_source, self.action_log,
                                             action_info)
            self.keyframe_observers[control_source] = observer
750 751 752

    def _disconnectFromControlSource(self, binding):
        control_source = binding.props.control_source
753 754
        observer = self.keyframe_observers.pop(control_source)
        observer.release()
755

756
    def _clipAddedCb(self, layer, clip):
757
        self._connectToClip(clip)
758 759
        if isinstance(clip, GES.TransitionClip):
            return
760
        action = ClipAdded(layer, clip)
761
        self.action_log.push(action)
762

763
    def _clipRemovedCb(self, layer, clip):
764
        self._disconnectFromClip(clip)
765
        if isinstance(clip, GES.TransitionClip):
766 767 768
            action = TransitionClipRemovedAction.new(layer, clip)
            if action:
                self.action_log.push(action)
769
            return
770
        action = ClipRemoved(layer, clip)
771
        self.action_log.push(action)
772

773 774
    def _clipTrackElementAddedCb(self, clip, ges_track_element):
        self._connectToTrackElement(ges_track_element)
775 776
        action = TrackElementAdded(clip, ges_track_element)
        self.action_log.push(action)
777

778 779 780
    def _clipTrackElementRemovedCb(self, clip, ges_track_element):
        self.debug("%s REMOVED from %s", ges_track_element, clip)
        self._disconnectFromTrackElement(ges_track_element)
781 782
        action = TrackElementRemoved(clip, ges_track_element)
        self.action_log.push(action)
783

784
    def __layer_moved_cb(self, ges_layer, unused_param):
785
        current = ges_layer.props.priority
786
        action = LayerMoved(ges_layer, self.priority, current)
787
        self.action_log.push(action)
788 789
        self.priority = current

790

791 792 793 794 795 796 797
class TimelineElementAddedToGroup(UndoableAction):

    def __init__(self, ges_group, ges_timeline_element):
        UndoableAction.__init__(self)
        self.ges_group = ges_group
        self.ges_timeline_element = ges_timeline_element

798 799 800
    def __repr__(self):
        return "<TimelineElementAddedToGroup %s, %s>" % (self.ges_group, self.ges_timeline_element)

801 802 803 804 805 806 807 808 809 810 811 812 813 814
    def do(self):
        self.ges_group.add(self.ges_timeline_element)

    def undo(self):
        self.ges_group.remove(self.ges_timeline_element)


class TimelineElementRemovedFromGroup(UndoableAction):

    def __init__(self, ges_group, ges_timeline_element):
        UndoableAction.__init__(self)
        self.ges_group = ges_group
        self.ges_timeline_element = ges_timeline_element

815 816 817
    def __repr__(self):
        return "<TimelineElementRemovedFromGroup %s, %s>" % (self.ges_group, self.ges_timeline_element)

818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852
    def do(self):
        self.ges_group.remove(self.ges_timeline_element)

    def undo(self):
        self.ges_group.add(self.ges_timeline_element)


class GroupObserver(Loggable):
    """Monitors a Group and reports UndoableActions.

    Args:
        ges_group (GES.Group): The group to observe.

    Attributes:
        action_log (UndoableActionLog): The action log where to report actions.
    """

    def __init__(self, ges_group, action_log):
        Loggable.__init__(self)
        self.log("INIT %s", ges_group)
        self.ges_group = ges_group
        self.action_log = action_log

        ges_group.connect_after("child-added", self.__child_added_cb)
        ges_group.connect("child-removed", self.__child_removed_cb)

    def __child_added_cb(self, ges_group, ges_timeline_element):
        action = TimelineElementAddedToGroup(ges_group, ges_timeline_element)
        self.action_log.push(action)

    def __child_removed_cb(self, ges_group, ges_timeline_element):
        action = TimelineElementRemovedFromGroup(ges_group, ges_timeline_element)
        self.action_log.push(action)


853 854 855 856 857 858 859 860 861 862 863 864 865 866
class TimelineObserver(Loggable):
    """Monitors a project's timeline and reports UndoableActions.

    Attributes:
        ges_timeline (GES.Timeline): The timeline to be monitored.
        action_log (UndoableActionLog): The action log where to report actions.
    """

    def __init__(self, ges_timeline, action_log):
        Loggable.__init__(self)
        self.ges_timeline = ges_timeline
        self.action_log = action_log

        self.layer_observers = {}
867
        self.group_observers = {}
868 869 870 871 872 873
        for ges_layer in ges_timeline.get_layers():
            self._connect_to_layer(ges_layer)

        ges_timeline.connect("layer-added", self.__layer_added_cb)
        ges_timeline.connect("layer-removed", self.__layer_removed_cb)

874 875 876 877 878 879 880
        for ges_group in ges_timeline.get_groups():
            self._connect_to_group(ges_group)

        ges_timeline.connect("group-added", self.__group_added_cb)
        # We don't care about the group-removed signal because this greatly
        # simplifies the logic.

881
    def __layer_added_cb(self, ges_timeline, ges_layer):
882 883
        action = LayerAdded(self.ges_timeline, ges_layer)
        self.action_log.push(action)
884
        self._connect_to_layer(ges_layer)
885 886 887 888

    def _connect_to_layer(self, ges_layer):
        layer_observer = LayerObserver(ges_layer, self.action_log)
        self.layer_observers[ges_layer] = layer_observer
889

890
    def __layer_removed_cb(self, ges_timeline, ges_layer):
891
        action = LayerRemoved(ges_timeline, ges_layer)
892
        self.action_log.push(action)
893 894 895

    def _connect_to_group(self, ges_group):
        if not ges_group.props.serialize:
896
            return False
897 898 899 900 901 902 903 904

        # A group is added when it gets its first element, thus
        # when undoing/redoing a group can be added multiple times.
        # This is the only complexity caused by the fact that we keep alive
        # all the GroupObservers which have been created.
        if ges_group not in self.group_observers:
            group_observer = GroupObserver(ges_group, self.action_log)
            self.group_observers[ges_group] = group_observer
905
        return True
906 907

    def __group_added_cb(self, unused_ges_timeline, ges_group):
908 909 910
        if not self._connect_to_group(ges_group):
            return

911 912 913 914
        # This should be a single clip.
        for ges_clip in ges_group.get_children(recursive=False):
            action = TimelineElementAddedToGroup(ges_group, ges_clip)
            self.action_log.push(action)