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

« back to all changes in this revision

Viewing changes to pitivi/undo/undo.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
# Pitivi video editor
 
2
#
 
3
#       pitivi/undo/undo.py
 
4
#
 
5
# Copyright (c) 2009, Alessandro Decina <alessandro.d@gmail.com>
 
6
#
 
7
# This program is free software; you can redistribute it and/or
 
8
# modify it under the terms of the GNU Lesser General Public
 
9
# License as published by the Free Software Foundation; either
 
10
# version 2.1 of the License, or (at your option) any later version.
 
11
#
 
12
# This program is distributed in the hope that it will be useful,
 
13
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
14
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 
15
# Lesser General Public License for more details.
 
16
#
 
17
# You should have received a copy of the GNU Lesser General Public
 
18
# License along with this program; if not, write to the
 
19
# Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
 
20
# Boston, MA 02110-1301, USA.
 
21
 
 
22
"""
 
23
Base classes for the undo/redo feature implementation
 
24
"""
 
25
 
 
26
from pitivi.utils.signal import Signallable
 
27
from pitivi.utils.loggable import Loggable
 
28
 
 
29
 
 
30
class UndoError(Exception):
 
31
    """ Any exception related to the undo/redo feature."""
 
32
    pass
 
33
 
 
34
 
 
35
class UndoWrongStateError(UndoError):
 
36
    """ Exception related to the current state of the undo/redo stack. """
 
37
    pass
 
38
 
 
39
 
 
40
class UndoableAction(Signallable):
 
41
    """
 
42
    This class represents an action that can be undone.
 
43
    In other words, when your object's state changes, create an UndoableAction
 
44
    to allow reverting the change if needed later on.
 
45
    """
 
46
    __signals__ = {
 
47
        "done": [],
 
48
        "undone": [],
 
49
        "undone": [],
 
50
    }
 
51
 
 
52
    def do(self):
 
53
        raise NotImplementedError()
 
54
 
 
55
    def undo(self):
 
56
        raise NotImplementedError()
 
57
 
 
58
    def clean(self):
 
59
        # Meant to be overridden by UndoableActionStack?
 
60
        pass
 
61
 
 
62
    def _done(self):
 
63
        self.emit("done")
 
64
 
 
65
    def _undone(self):
 
66
        self.emit("undone")
 
67
 
 
68
 
 
69
class UndoableActionStack(UndoableAction):
 
70
    """
 
71
    Simply a stack of UndoableAction objects.
 
72
    """
 
73
    __signals__ = {
 
74
        "done": [],
 
75
        "undone": [],
 
76
        "cleaned": [],
 
77
    }
 
78
 
 
79
    def __init__(self, action_group_name):
 
80
        self.action_group_name = action_group_name
 
81
        self.done_actions = []
 
82
        self.undone_actions = []
 
83
        self.actions = []
 
84
 
 
85
    def push(self, action):
 
86
        self.done_actions.append(action)
 
87
 
 
88
    def _runAction(self, action_list, method_name):
 
89
        for action in action_list[::-1]:
 
90
            method = getattr(action, method_name)
 
91
            method()
 
92
 
 
93
    def do(self):
 
94
        self._runAction(self.undone_actions, "do")
 
95
        self.done_actions = self.undone_actions[::-1]
 
96
        self.emit("done")
 
97
 
 
98
    def undo(self):
 
99
        self._runAction(self.done_actions, "undo")
 
100
        self.undone_actions = self.done_actions[::-1]
 
101
        self.emit("undone")
 
102
 
 
103
    def clean(self):
 
104
        actions = self.done_actions + self.undone_actions
 
105
        self.undone_actions = []
 
106
        self.done_actions = []
 
107
        self._runAction(actions, "clean")
 
108
        self.emit("cleaned")
 
109
 
 
110
 
 
111
class UndoableActionLog(Signallable, Loggable):
 
112
    """
 
113
    This is the "master" class that handles all the undo/redo system. There is
 
114
    only one instance of it in Pitivi: application.py's "action_log" property.
 
115
    """
 
116
    __signals__ = {
 
117
        "begin": ["stack", "nested"],
 
118
        "push": ["stack", "action"],
 
119
        "rollback": ["stack", "nested"],
 
120
        "commit": ["stack", "nested"],
 
121
        "undo": ["stack"],
 
122
        "redo": ["stack"],
 
123
        "cleaned": [],
 
124
    }
 
125
 
 
126
    def __init__(self):
 
127
        Loggable.__init__(self)
 
128
 
 
129
        self.undo_stacks = []
 
130
        self.redo_stacks = []
 
131
        self.stacks = []
 
132
        self.running = False
 
133
        self._checkpoint = self._takeSnapshot()
 
134
 
 
135
    def begin(self, action_group_name):
 
136
        self.debug("Begining %s", action_group_name)
 
137
        if self.running:
 
138
            return
 
139
 
 
140
        stack = UndoableActionStack(action_group_name)
 
141
        nested = self._stackIsNested(stack)
 
142
        self.stacks.append(stack)
 
143
        self.emit("begin", stack, nested)
 
144
 
 
145
    def push(self, action):
 
146
        self.debug("Pushing %s", action)
 
147
        if self.running:
 
148
            return
 
149
 
 
150
        try:
 
151
            stack = self._getTopmostStack()
 
152
        except UndoWrongStateError:
 
153
            return
 
154
 
 
155
        stack.push(action)
 
156
        self.emit("push", stack, action)
 
157
 
 
158
    def rollback(self):
 
159
        if self.running:
 
160
            return
 
161
 
 
162
        stack = self._getTopmostStack(pop=True)
 
163
        if stack is None:
 
164
            return
 
165
        nested = self._stackIsNested(stack)
 
166
        self.emit("rollback", stack, nested)
 
167
        stack.undo()
 
168
 
 
169
    def commit(self):
 
170
        if self.running:
 
171
            return
 
172
 
 
173
        stack = self._getTopmostStack(pop=True)
 
174
        if stack is None:
 
175
            return
 
176
        nested = self._stackIsNested(stack)
 
177
        if not self.stacks:
 
178
            self.undo_stacks.append(stack)
 
179
        else:
 
180
            self.stacks[-1].push(stack)
 
181
 
 
182
        if self.redo_stacks:
 
183
            self.redo_stacks = []
 
184
 
 
185
        self.debug("%s pushed", stack)
 
186
        self.emit("commit", stack, nested)
 
187
 
 
188
    def undo(self):
 
189
        if self.stacks or not self.undo_stacks:
 
190
            raise UndoWrongStateError()
 
191
 
 
192
        stack = self.undo_stacks.pop(-1)
 
193
        self._runStack(stack, stack.undo)
 
194
        self.redo_stacks.append(stack)
 
195
        self.emit("undo", stack)
 
196
 
 
197
    def redo(self):
 
198
        if self.stacks or not self.redo_stacks:
 
199
            raise UndoWrongStateError()
 
200
 
 
201
        stack = self.redo_stacks.pop(-1)
 
202
        self._runStack(stack, stack.do)
 
203
        self.undo_stacks.append(stack)
 
204
        self.emit("redo", stack)
 
205
 
 
206
    def clean(self):
 
207
        stacks = self.redo_stacks + self.undo_stacks
 
208
        self.redo_stacks = []
 
209
        self.undo_stacks = []
 
210
 
 
211
        for stack in stacks:
 
212
            self._runStack(stack, stack.clean)
 
213
        self.emit("cleaned")
 
214
 
 
215
    def _takeSnapshot(self):
 
216
        return list(self.undo_stacks)
 
217
 
 
218
    def checkpoint(self):
 
219
        if self.stacks:
 
220
            raise UndoWrongStateError()
 
221
 
 
222
        self._checkpoint = self._takeSnapshot()
 
223
 
 
224
    def dirty(self):
 
225
        current_snapshot = self._takeSnapshot()
 
226
        return current_snapshot != self._checkpoint
 
227
 
 
228
    def _runStack(self, unused_stack, run):
 
229
        self.running = True
 
230
        try:
 
231
            run()
 
232
        finally:
 
233
            self.running = False
 
234
 
 
235
    def _getTopmostStack(self, pop=False):
 
236
        stack = None
 
237
        try:
 
238
            if pop:
 
239
                stack = self.stacks.pop(-1)
 
240
            else:
 
241
                stack = self.stacks[-1]
 
242
        except IndexError:
 
243
            raise UndoWrongStateError()
 
244
 
 
245
        return stack
 
246
 
 
247
    def _stackIsNested(self, unused_stack):
 
248
        return bool(len(self.stacks))
 
249
 
 
250
 
 
251
class DebugActionLogObserver(Loggable):
 
252
    """
 
253
    Allows getting more debug output from the UndoableActionLog than the default
 
254
    """
 
255
    # FIXME: this looks overengineered at first glance. This is used in only one
 
256
    # place in the codebase (in application.py). Why not just put the debug
 
257
    # directly in UndoableActionLog if we really need them anyway?
 
258
    def startObserving(self, log):
 
259
        self._connectToActionLog(log)
 
260
 
 
261
    def stopObserving(self, log):
 
262
        self._disconnectFromActionLog(log)
 
263
 
 
264
    def _connectToActionLog(self, log):
 
265
        log.connect("begin", self._actionLogBeginCb)
 
266
        log.connect("commit", self._actionLogCommitCb)
 
267
        log.connect("rollback", self._actionLogRollbackCb)
 
268
        log.connect("push", self._actionLogPushCb)
 
269
 
 
270
    def _disconnectFromActionLog(self, log):
 
271
        for method in (self._actionLogBeginCb, self._actionLogCommitCb,
 
272
                self._actionLogrollbackCb, self._actionLogPushCb):
 
273
            log.disconnect_by_func(method)
 
274
 
 
275
    def _actionLogBeginCb(self, unused_log, stack, nested):
 
276
        self.debug("begin action %s nested %s", stack.action_group_name, nested)
 
277
 
 
278
    def _actionLogCommitCb(self, unused_log, stack, nested):
 
279
        self.debug("commit action %s nested %s", stack.action_group_name, nested)
 
280
 
 
281
    def _actionLogRollbackCb(self, unused_log, stack, nested):
 
282
        self.debug("rollback action %s nested %s", stack.action_group_name, nested)
 
283
 
 
284
    def _actionLogPushCb(self, unused_log, stack, action):
 
285
        self.debug("push %s in %s", action, stack.action_group_name)
 
286
 
 
287
 
 
288
class PropertyChangeTracker(Signallable):
 
289
    """
 
290
    BaseClass to track a class property, Used for undo/redo
 
291
    """
 
292
    __signals__ = {}
 
293
 
 
294
    def __init__(self):
 
295
        self.properties = {}
 
296
        self.obj = None
 
297
 
 
298
    def connectToObject(self, obj):
 
299
        self.obj = obj
 
300
        self.properties = self._takeCurrentSnapshot(obj)
 
301
        for property_name in self.property_names:
 
302
            signal_name = "notify::" + property_name
 
303
            self.__signals__[signal_name] = []
 
304
            obj.connect(signal_name,
 
305
                    self._propertyChangedCb, property_name)
 
306
 
 
307
    def _takeCurrentSnapshot(self, obj):
 
308
        properties = {}
 
309
        for property_name in self.property_names:
 
310
            properties[property_name] = obj.get_property(property_name.replace("-", "_"))
 
311
 
 
312
        return properties
 
313
 
 
314
    def disconnectFromObject(self, obj):
 
315
        self.obj = None
 
316
        obj.disconnect_by_func(self._propertyChangedCb)
 
317
 
 
318
    def _propertyChangedCb(self, object, value, property_name):
 
319
        old_value = self.properties[property_name]
 
320
        self.properties[property_name] = value
 
321
        self.emit("notify::" + property_name, object, old_value, value)