[pitivi: 7/16] Add undo/redo support for keyframes.



commit c8056f68441603482b53422287639fbb4c288452
Author: Alessandro Decina <alessandro d gmail com>
Date:   Fri Jul 3 19:29:27 2009 +0200

    Add undo/redo support for keyframes.

 pitivi/timeline/timeline_undo.py |  158 ++++++++++++++++++++++++++++++++++++-
 pitivi/timeline/track.py         |   81 ++++++++++++-------
 pitivi/ui/curve.py               |    9 ++-
 pitivi/ui/trackobject.py         |    2 +-
 4 files changed, 213 insertions(+), 37 deletions(-)
---
diff --git a/pitivi/timeline/timeline_undo.py b/pitivi/timeline/timeline_undo.py
index 273cae3..efa333a 100644
--- a/pitivi/timeline/timeline_undo.py
+++ b/pitivi/timeline/timeline_undo.py
@@ -19,6 +19,7 @@
 # Free Software Foundation, Inc., 59 Temple Place - Suite 330,
 # Boston, MA 02111-1307, USA.
 
+from pitivi.signalinterface import Signallable
 from pitivi.utils import PropertyChangeTracker
 from pitivi.undo import UndoableAction
 
@@ -54,6 +55,50 @@ class TimelineObjectPropertyChangeTracker(PropertyChangeTracker):
             PropertyChangeTracker._propertyChangedCb(self,
                     timeline_object, value, property_name)
 
+class KeyframeChangeTracker(Signallable):
+    __signals__ = {
+        "keyframe-moved": ["keyframe"]
+    }
+
+    def __init__(self):
+        self.keyframes = None
+        self.obj = None
+
+    def connectToObject(self, obj):
+        self.obj = obj
+        self.keyframes = self._takeCurrentSnapshot(obj)
+        obj.connect("keyframe-added", self._keyframeAddedCb)
+        obj.connect("keyframe-removed", self._keyframeRemovedCb)
+        obj.connect("keyframe-moved", self._keyframeMovedCb)
+
+    def _takeCurrentSnapshot(self, obj):
+        keyframes = {}
+        for keyframe in self.obj.getKeyframes():
+            keyframes[keyframe] = self._getKeyframeSnapshot(keyframe)
+
+        return keyframes
+
+    def disconnectFromObject(self, obj):
+        self.obj = None
+        obj.disconnect_by_func(self._keyframeMovedCb)
+
+    def _keyframeAddedCb(self, interpolator, keyframe):
+        self.keyframes[keyframe] = self._getKeyframeSnapshot(keyframe)
+
+    def _keyframeRemovedCb(self, interpolator, keyframe):
+        pass
+
+    def _keyframeMovedCb(self, interpolator, keyframe):
+        old_snapshot = self.keyframes[keyframe]
+        new_snapshot = self._getKeyframeSnapshot(keyframe)
+        self.keyframes[keyframe] = new_snapshot
+
+        self.emit("keyframe-moved", interpolator,
+                keyframe, old_snapshot, new_snapshot)
+
+    def _getKeyframeSnapshot(self, keyframe):
+        return (keyframe.mode, keyframe.time, keyframe.value)
+
 class TimelineObjectPropertyChanged(UndoableAction):
     def __init__(self, timeline_object, property_name, old_value, new_value):
         self.timeline_object = timeline_object
@@ -107,24 +152,80 @@ class TimelineObjectRemoved(UndoableAction):
         self.timeline.addTimelineObject(self.timeline_object)
         self._undone()
 
+class InterpolatorKeyframeAdded(UndoableAction):
+    def __init__(self, track_object, keyframe):
+        self.track_object = track_object
+        self.keyframe = keyframe
+
+    def do(self):
+        self.track_object.newKeyframe(self.keyframe)
+        self._done()
+
+    def undo(self):
+        self.track_object.removeKeyframe(self.keyframe)
+        self._undone()
+
+class InterpolatorKeyframeRemoved(UndoableAction):
+    def __init__(self, track_object, keyframe):
+        self.track_object = track_object
+        self.keyframe = keyframe
+
+    def do(self):
+        self.track_object.removeKeyframe(self.keyframe)
+        self._undone()
+
+    def undo(self):
+        self.track_object.newKeyframe(self.keyframe.time,
+                self.keyframe.value, self.keyframe.mode)
+        self._done()
+
+class InterpolatorKeyframeChanged(UndoableAction):
+    def __init__(self, track_object, keyframe, old_snapshot, new_snapshot):
+        self.track_object = track_object
+        self.keyframe = keyframe
+        self.old_snapshot = old_snapshot
+        self.new_snapshot = new_snapshot
+
+    def do(self):
+        self._setSnapshot(self.new_snapshot)
+        self._done()
+
+    def undo(self):
+        self._setSnapshot(self.old_snapshot)
+        self._undone()
+
+    def _setSnapshot(self, snapshot):
+        mode, time, value = snapshot
+        self.keyframe.setMode(mode)
+        self.keyframe.setTime(time)
+        self.keyframe.setValue(value)
+
 class TimelineLogObserver(object):
-    propertyChangedAction = TimelineObjectPropertyChanged
+    timelinePropertyChangedAction = TimelineObjectPropertyChanged
     timelineObjectAddedAction = TimelineObjectAdded
     timelineObjectRemovedAction = TimelineObjectRemoved
+    interpolatorKeyframeAddedAction = InterpolatorKeyframeAdded
+    interpolatorKeyframeRemovedAction = InterpolatorKeyframeRemoved
+    interpolatorKeyframeChangedAction = InterpolatorKeyframeChanged
 
     def __init__(self, log):
         self.log = log
-        self.property_trackers = {}
+        self.timeline_object_property_trackers = {}
+        self.interpolator_keyframe_trackers = {}
 
     def startObserving(self, timeline):
         self._connectToTimeline(timeline)
         for timeline_object in timeline.timeline_objects:
             self._connectToTimelineObject(timeline_object)
+            for track_object in timeline_object.track_objects:
+                self._connectToTrackObject(track_object)
 
     def stopObserving(self, timeline):
         self._disconnectFromTimeline(timeline)
         for timeline_object in timeline.timeline_objects:
             self._disconnectFromTimelineObject(timeline_object)
+            for track_object in timeline_object.track_objects:
+                self._disconnectFromTrackObject(track_object)
 
     def _connectToTimeline(self, timeline):
         timeline.connect("timeline-object-added", self._timelineObjectAddedCb)
@@ -140,13 +241,39 @@ class TimelineLogObserver(object):
         for property_name in tracker.property_names:
             tracker.connect(property_name + "-changed",
                     self._timelineObjectPropertyChangedCb, property_name)
-        self.property_trackers[timeline_object] = tracker
+        self.timeline_object_property_trackers[timeline_object] = tracker
+
+        timeline_object.connect("track-object-added", self._timelineObjectTrackObjectAddedCb)
+        timeline_object.connect("track-object-removed", self._timelineObjectTrackObjectRemovedCb)
 
     def _disconnectFromTimelineObject(self, timeline_object):
-        tracker = self.property_trackers.pop(timeline_object)
+        tracker = self.timeline_object_property_trackers.pop(timeline_object)
         tracker.disconnectFromObject(timeline_object)
         tracker.disconnect_by_func(self._timelineObjectPropertyChangedCb)
 
+    def _connectToTrackObject(self, track_object):
+        for prop, interpolator in track_object.getInterpolators().itervalues():
+            self._connectToInterpolator(interpolator)
+
+    def _disconnectFromTrackObject(self, track_object):
+        for prop, interpolator in track_object.getInterpolators().itervalues():
+            self._disconnectToInterpolator(interpolator)
+
+    def _connectToInterpolator(self, interpolator):
+        interpolator.connect("keyframe-added", self._interpolatorKeyframeAddedCb)
+        interpolator.connect("keyframe-removed",
+                self._interpolatorKeyframeRemovedCb)
+
+        tracker = KeyframeChangeTracker()
+        tracker.connectToObject(interpolator)
+        tracker.connect("keyframe-moved", self._interpolatorKeyframeMovedCb)
+        self.interpolator_keyframe_trackers[interpolator] = tracker
+
+    def _disconnectFromInterpolator(self, interpolator):
+        tracker = self.interpolator_keyframe_trackers.pop(interpolator)
+        tracker.disconnectFromObject(interpolator)
+        tracker.disconnect_by_func(self._interpolatorKeyframeMovedCb)
+
     def _timelineObjectAddedCb(self, timeline, timeline_object):
         self._connectToTimelineObject(timeline_object)
         action = self.timelineObjectAddedAction(timeline, timeline_object)
@@ -159,6 +286,27 @@ class TimelineLogObserver(object):
 
     def _timelineObjectPropertyChangedCb(self, tracker, timeline_object,
             old_value, new_value, property_name):
-        action = self.propertyChangedAction(timeline_object,
+        action = self.timelinePropertyChangedAction(timeline_object,
                 property_name, old_value, new_value)
         self.log.push(action)
+
+    def _timelineObjectTrackObjectAddedCb(self, timeline_object, track_object):
+        self._connectToTrackObject(track_object)
+
+    def _timelineObjectTrackObjectRemovedCb(self, timeline_object,
+            track_object):
+        self._disconnectFromTrackObject(track_object)
+
+    def _interpolatorKeyframeAddedCb(self, track_object, keyframe):
+        action = self.interpolatorKeyframeAddedAction(track_object, keyframe)
+        self.log.push(action)
+
+    def _interpolatorKeyframeRemovedCb(self, track_object, keyframe):
+        action = self.interpolatorKeyframeRemovedAction(track_object, keyframe)
+        self.log.push(action)
+
+    def _interpolatorKeyframeMovedCb(self, tracker, track_object,
+            keyframe, old_snapshot, new_snapshot):
+        action = self.interpolatorKeyframeChangedAction(track_object,
+                keyframe, old_snapshot, new_snapshot)
+        self.log.push(action)
diff --git a/pitivi/timeline/track.py b/pitivi/timeline/track.py
index 8d0a9ef..157a182 100644
--- a/pitivi/timeline/track.py
+++ b/pitivi/timeline/track.py
@@ -165,40 +165,45 @@ class Interpolator(Signallable, Loggable):
         self._controller = gst.Controller(self._element, prop.name)
         self._controller.set_interpolation_mode(prop.name, gst.INTERPOLATE_LINEAR)
 
-    def newKeyframe(self, time, value=None, mode=None):
+    def newKeyframe(self, time_or_keyframe, value=None, mode=None):
         """add a new keyframe at the specified time, optionally with specified
         value and specified mode. If not specified, these will be computed so
         that the new keyframe likes on the existing curve at that timestampi
 
         returns: the keyframe object"""
+
+        if isinstance(time_or_keyframe, Keyframe):
+            keyframe = time_or_keyframe
+        else:
+            # TODO: calculate value so that the new point doesn't change the shape
+            # of the curve when added. This might be tricky to achieve with cubic
+            # interpolation, but should work fine for linear and step
+            # interpolation.
+            if value is None:
+                value = self._default
+            if mode is None:
+                # FIXME: Controller.get_interpolation_mode is not wrapped in
+                # gst-python, so for now we assume the default is linear.
+                # Use the following code to get the current mode when this method becomes
+                # available.
+                # mode = self._controller.get_interpolation_mode()
+                mode = gst.INTERPOLATE_LINEAR
+
+            keyframe = Keyframe(self)
+            keyframe._time = time_or_keyframe
+            keyframe._value = value
+            keyframe._mode = mode
+
         self.debug("time:%s, value:%r, mode:%r",
-                   gst.TIME_ARGS(time), value, mode)
-        # TODO: calculate value so that the new point doesn't change the shape
-        # of the curve when added. This might be tricky to achieve with cubic
-        # interpolation, but should work fine for linear and step
-        # interpolation.
-        if value is None:
-            value = self._default
-        if mode is None:
-            # FIXME: Controller.get_interpolation_mode is not wrapped in
-            # gst-python, so for now we assume the default is linear.
-            # Use the following code to get the current mode when this method becomes
-            # available.
-            # mode = self._controller.get_interpolation_mode()
-            mode = gst.INTERPOLATE_LINEAR
-
-        kf = Keyframe(self)
-        kf._time = time
-        kf._value = value
-        kf._mode = mode
-
-        self._keyframes.append(kf)
-
-        self._controller.set(self._property.name, kf.time, kf.value)
-
-        self.emit("keyframe-added", kf)
-
-        return kf
+                   gst.TIME_ARGS(keyframe.time), keyframe.value, keyframe.mode)
+
+        self._keyframes.append(keyframe)
+
+        self._controller.set(self._property.name, keyframe.time, keyframe.value)
+
+        self.emit("keyframe-added", keyframe)
+
+        return keyframe
 
     def removeKeyframe(self, keyframe):
         self._controller.unset(self._property.name, keyframe.time)
@@ -322,13 +327,29 @@ class TrackObject(Signallable, Loggable):
 
         factory_properties = self.factory.getInterpolatedProperties(self.stream).keys()
 
+        old_interpolators = self.interpolators
+        self.interpolators = {}
         for gst_object, gst_object_property in \
                 get_controllable_properties(self.gnl_object):
             if gst_object_property.name not in factory_properties:
                 continue
 
-            self.interpolators[gst_object_property.name] = (gst_object_property,
-                    Interpolator(self, gst_object, gst_object_property))
+            try:
+                interpolator = old_interpolators[gst_object_property.name][1]
+            except KeyError:
+                interpolator = Interpolator(self, gst_object, gst_object_property)
+            else:
+                interpolator.attachToElementProperty(gst_object_property,
+                        gst_object)
+
+                # remove and add again the keyframes so they are set on the
+                # current controller
+                for keyframe in list(interpolator.keyframes):
+                    interpolator.removeKeyframe(keyframe)
+                    interpolator.newKeyframe(keyframe)
+
+            self.interpolators[gst_object_property.name] = \
+                    (gst_object_property, interpolator)
 
     def release(self):
         self._disconnectFromSignals()
diff --git a/pitivi/ui/curve.py b/pitivi/ui/curve.py
index ecffa34..2bd27e9 100644
--- a/pitivi/ui/curve.py
+++ b/pitivi/ui/curve.py
@@ -52,6 +52,7 @@ class Curve(goocanvas.ItemSimple, goocanvas.Item, View, Zoomable):
     class Controller(Controller):
 
         def drag_start(self, item, target, event):
+            self._view.app.action_log.begin("volume change")
             initial = self.from_item_event(item, event)
             self._kf = self._view.findKeyframe(initial)
             if self._kf:
@@ -63,6 +64,7 @@ class Curve(goocanvas.ItemSimple, goocanvas.Item, View, Zoomable):
 
         def drag_end(self, item, target, event):
             self._kf = None
+            self._view.app.action_log.commit()
 
         def set_pos(self, obj, pos):
             interpolator = self._view.interpolator
@@ -81,9 +83,13 @@ class Curve(goocanvas.ItemSimple, goocanvas.Item, View, Zoomable):
             kf = self._view.findKeyframe(pos)
             if kf is None:
                 time, value = self.xyToTimeValue(pos)
+                self._view.app.action_log.begin("add volume point")
                 interpolator.newKeyframe(time, value)
+                self._view.app.action_log.commit()
             else:
+                self._view.app.action_log.begin("remove volume point")
                 self._view.interpolator.removeKeyframe(kf)
+                self._view.app.action_log.commit()
 
         def xyToTimeValue(self, pos):
             bounds = self._view.bounds
@@ -98,11 +104,12 @@ class Curve(goocanvas.ItemSimple, goocanvas.Item, View, Zoomable):
         def leave(self, item, target):
             self._view.normal()
 
-    def __init__(self, element, interpolator, height=LAYER_HEIGHT_EXPANDED,
+    def __init__(self, instance, element, interpolator, height=LAYER_HEIGHT_EXPANDED,
         **kwargs):
         super(Curve, self).__init__(**kwargs)
         View.__init__(self)
         Zoomable.__init__(self)
+        self.app = instance
         self.keyframes = {}
         self.height = float(height)
         self.element = element
diff --git a/pitivi/ui/trackobject.py b/pitivi/ui/trackobject.py
index ec6ad83..2a0fd48 100644
--- a/pitivi/ui/trackobject.py
+++ b/pitivi/ui/trackobject.py
@@ -275,7 +275,7 @@ class TrackObject(View, goocanvas.Group, Zoomable):
             self.add_child(thing)
 
         for prop, interpolator in element.getInterpolators().itervalues():
-            self.add_child(Curve(element, interpolator, 50))
+            self.add_child(Curve(instance, element, interpolator, 50))
 
         self.element = element
         self.settings = instance.settings



[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]