enthought / traitsui
1
# ------------------------------------------------------------------------------
2
#
3
#  Copyright (c) 2005, Enthought, Inc.
4
#  All rights reserved.
5
#
6
#  This software is provided without warranty under the terms of the BSD
7
#  license included in LICENSE.txt and may be redistributed only
8
#  under the conditions described in the aforementioned license.  The license
9
#  is also available online at http://www.enthought.com/licenses/BSD.txt
10
#
11
#  Thanks for using Enthought open source!
12
#
13
#  Author: David C. Morrill
14
#  Date:   10/07/2004
15
#
16
# ------------------------------------------------------------------------------
17

18 4
""" Defines the manager for Undo and Redo history for Traits user interface
19
    support.
20
"""
21

22 4
import collections.abc
23

24 4
from traits.api import (
25
    Event,
26
    HasPrivateTraits,
27
    HasStrictTraits,
28
    HasTraits,
29
    Instance,
30
    Int,
31
    List,
32
    Property,
33
    Str,
34
    Trait,
35
)
36

37

38 4
NumericTypes = (int, float, complex)
39 4
SimpleTypes = (str, bytes) + NumericTypes
40

41

42 4
class AbstractUndoItem(HasPrivateTraits):
43
    """ Abstract base class for undo items.
44
    """
45

46 4
    def undo(self):
47
        """ Undoes the change.
48
        """
49
        raise NotImplementedError
50

51 4
    def redo(self):
52
        """ Re-does the change.
53
        """
54
        raise NotImplementedError
55

56 4
    def merge_undo(self, undo_item):
57
        """ Merges two undo items if possible.
58
        """
59 0
        return False
60

61

62 4
class UndoItem(AbstractUndoItem):
63
    """ A change to an object trait, which can be undone.
64
    """
65

66
    # -------------------------------------------------------------------------
67
    #  Trait definitions:
68
    # -------------------------------------------------------------------------
69

70
    #: Object the change occurred on
71 4
    object = Instance(HasTraits)
72

73
    #: Name of the trait that changed
74 4
    name = Str()
75

76
    #: Old value of the changed trait
77 4
    old_value = Property()
78

79
    #: New value of the changed trait
80 4
    new_value = Property()
81

82 4
    def _get_old_value(self):
83 4
        return self._old_value
84

85 4
    def _set_old_value(self, value):
86 4
        if isinstance(value, list):
87 0
            value = value[:]
88 4
        self._old_value = value
89

90 4
    def _get_new_value(self):
91 4
        return self._new_value
92

93 4
    def _set_new_value(self, value):
94 4
        if isinstance(value, list):
95 0
            value = value[:]
96 4
        self._new_value = value
97

98 4
    def undo(self):
99
        """ Undoes the change.
100
        """
101 4
        try:
102 4
            setattr(self.object, self.name, self.old_value)
103 0
        except Exception:
104 0
            from traitsui.api import raise_to_debug
105

106 0
            raise_to_debug()
107

108 4
    def redo(self):
109
        """ Re-does the change.
110
        """
111 4
        try:
112 4
            setattr(self.object, self.name, self.new_value)
113 0
        except Exception:
114 0
            from traitsui.api import raise_to_debug
115

116 0
            raise_to_debug()
117

118 4
    def merge_undo(self, undo_item):
119
        """ Merges two undo items if possible.
120
        """
121
        # Undo items are potentially mergeable only if they are of the same
122
        # class and refer to the same object trait, so check that first:
123 4
        if (
124
            isinstance(undo_item, self.__class__)
125
            and (self.object is undo_item.object)
126
            and (self.name == undo_item.name)
127
        ):
128 4
            v1 = self.new_value
129 4
            v2 = undo_item.new_value
130 4
            t1 = type(v1)
131 4
            if isinstance(v2, t1):
132

133 4
                if isinstance(t1, str):
134
                    # Merge two undo items if they have new values which are
135
                    # strings which only differ by one character (corresponding
136
                    # to a single character insertion, deletion or replacement
137
                    # operation in a text editor):
138 0
                    n1 = len(v1)
139 0
                    n2 = len(v2)
140 4
                    if abs(n1 - n2) > 1:
141 0
                        return False
142 0
                    n = min(n1, n2)
143 0
                    i = 0
144 4
                    while (i < n) and (v1[i] == v2[i]):
145 0
                        i += 1
146 4
                    if v1[i + (n2 <= n1):] == v2[i + (n2 >= n1):]:
147 0
                        self.new_value = v2
148 0
                        return True
149

150 4
                elif isinstance(v1, collections.abc.Sequence):
151
                    # Merge sequence types only if a single element has changed
152
                    # from the 'original' value, and the element type is a
153
                    # simple Python type:
154 4
                    v1 = self.old_value
155 4
                    if isinstance(v1, collections.abc.Sequence):
156
                        # Note: wxColour says it's a sequence type, but it
157
                        # doesn't support 'len', so we handle the exception
158
                        # just in case other classes have similar behavior:
159 4
                        try:
160 4
                            if len(v1) == len(v2):
161 0
                                diffs = 0
162 4
                                for i, item in enumerate(v1):
163 0
                                    titem = type(item)
164 0
                                    item2 = v2[i]
165 4
                                    if (
166
                                        (titem not in SimpleTypes)
167
                                        or (not isinstance(item2, titem))
168
                                        or (item != item2)
169
                                    ):
170 0
                                        diffs += 1
171 4
                                        if diffs >= 2:
172 0
                                            return False
173 4
                                if diffs == 0:
174 0
                                    return False
175 0
                                self.new_value = v2
176 0
                                return True
177 0
                        except Exception:
178 0
                            pass
179

180 4
                elif t1 in NumericTypes:
181
                    # Always merge simple numeric trait changes:
182 0
                    self.new_value = v2
183 0
                    return True
184 4
        return False
185

186
    def __repr__(self):
187
        """ Returns a "pretty print" form of the object.
188
        """
189
        n = self.name
190
        cn = self.object.__class__.__name__
191
        return "undo( %s.%s = %s )\nredo( %s.%s = %s )" % (
192
            cn,
193
            n,
194
            self.old_value,
195
            cn,
196
            n,
197
            self.new_value,
198
        )
199

200

201 4
class ListUndoItem(AbstractUndoItem):
202
    """ A change to a list, which can be undone.
203
    """
204

205
    # -------------------------------------------------------------------------
206
    #  Trait definitions:
207
    # -------------------------------------------------------------------------
208

209
    #: Object that the change occurred on
210 4
    object = Instance(HasTraits)
211

212
    #: Name of the trait that changed
213 4
    name = Str()
214

215
    #: Starting index
216 4
    index = Int()
217

218
    #: Items added to the list
219 4
    added = List()
220

221
    #: Items removed from the list
222 4
    removed = List()
223

224 4
    def undo(self):
225
        """ Undoes the change.
226
        """
227 0
        try:
228 0
            list = getattr(self.object, self.name)
229 0
            list[self.index : (self.index + len(self.added))] = self.removed
230 0
        except Exception:
231 0
            from traitsui.api import raise_to_debug
232

233 0
            raise_to_debug()
234

235 4
    def redo(self):
236
        """ Re-does the change.
237
        """
238 0
        try:
239 0
            list = getattr(self.object, self.name)
240 0
            list[self.index : (self.index + len(self.removed))] = self.added
241 0
        except Exception:
242 0
            from traitsui.api import raise_to_debug
243

244 0
            raise_to_debug()
245

246 4
    def merge_undo(self, undo_item):
247
        """ Merges two undo items if possible.
248
        """
249
        # Discard undo items that are identical to us. This is to eliminate
250
        # the same undo item being created by multiple listeners monitoring the
251
        # same list for changes:
252 4
        if (
253
            isinstance(undo_item, self.__class__)
254
            and (self.object is undo_item.object)
255
            and (self.name == undo_item.name)
256
            and (self.index == undo_item.index)
257
        ):
258 0
            added = undo_item.added
259 0
            removed = undo_item.removed
260 4
            if (len(self.added) == len(added)) and (
261
                len(self.removed) == len(removed)
262
            ):
263 4
                for i, item in enumerate(self.added):
264 4
                    if item is not added[i]:
265 0
                        break
266
                else:
267 4
                    for i, item in enumerate(self.removed):
268 4
                        if item is not removed[i]:
269 0
                            break
270
                    else:
271 0
                        return True
272 0
        return False
273

274
    def __repr__(self):
275
        """ Returns a 'pretty print' form of the object.
276
        """
277
        return "undo( %s.%s[%d:%d] = %s )" % (
278
            self.object.__class__.__name__,
279
            self.name,
280
            self.index,
281
            self.index + len(self.removed),
282
            self.added,
283
        )
284

285

286 4
class UndoHistory(HasStrictTraits):
287
    """ Manages a list of undoable changes.
288
    """
289

290
    # -------------------------------------------------------------------------
291
    #  Trait definitions:
292
    # -------------------------------------------------------------------------
293

294
    #: List of accumulated undo changes
295 4
    history = List()
296
    #: The current position in the list
297 4
    now = Int()
298
    #: Fired when state changes to undoable
299 4
    undoable = Event(False)
300
    #: Fired when state changes to redoable
301 4
    redoable = Event(False)
302
    #: Can an action be undone?
303 4
    can_undo = Property()
304
    #: Can an action be redone?
305 4
    can_redo = Property()
306

307 4
    def add(self, undo_item, extend=False):
308
        """ Adds an UndoItem to the history.
309
        """
310 4
        if extend:
311 0
            self.extend(undo_item)
312 0
            return
313

314
        # Try to merge the new undo item with the previous item if allowed:
315 4
        now = self.now
316 4
        if now > 0:
317 4
            previous = self.history[now - 1]
318 4
            if (len(previous) == 1) and previous[0].merge_undo(undo_item):
319 0
                self.history[now:] = []
320 0
                return
321

322 4
        old_len = len(self.history)
323 4
        self.history[now:] = [[undo_item]]
324 4
        self.now += 1
325 4
        if self.now == 1:
326 4
            self.undoable = True
327 4
        if self.now <= old_len:
328 4
            self.redoable = False
329

330 4
    def extend(self, undo_item):
331
        """ Extends the undo history.
332

333
        If possible the method merges the new UndoItem with the last item in
334
        the history; otherwise, it appends the new item.
335
        """
336 4
        if self.now > 0:
337 4
            undo_list = self.history[self.now - 1]
338 4
            if not undo_list[-1].merge_undo(undo_item):
339 4
                undo_list.append(undo_item)
340

341 4
    def undo(self):
342
        """ Undoes an operation.
343
        """
344 4
        if self.can_undo:
345 4
            self.now -= 1
346 4
            items = self.history[self.now]
347 4
            for i in range(len(items) - 1, -1, -1):
348 4
                items[i].undo()
349 4
            if self.now == 0:
350 0
                self.undoable = False
351 4
            if self.now == (len(self.history) - 1):
352 4
                self.redoable = True
353

354 4
    def redo(self):
355
        """ Redoes an operation.
356
        """
357 4
        if self.can_redo:
358 4
            self.now += 1
359 4
            for item in self.history[self.now - 1]:
360 4
                item.redo()
361 4
            if self.now == 1:
362 0
                self.undoable = True
363 4
            if self.now == len(self.history):
364 4
                self.redoable = False
365

366 4
    def revert(self):
367
        """ Reverts all changes made so far and clears the history.
368
        """
369 4
        history = self.history[: self.now]
370 4
        self.clear()
371 4
        for i in range(len(history) - 1, -1, -1):
372 0
            items = history[i]
373 4
            for j in range(len(items) - 1, -1, -1):
374 0
                items[j].undo()
375

376 4
    def clear(self):
377
        """ Clears the undo history.
378
        """
379 4
        old_len = len(self.history)
380 4
        old_now = self.now
381 4
        self.now = 0
382 4
        del self.history[:]
383 4
        if old_now > 0:
384 0
            self.undoable = False
385 4
        if old_now < old_len:
386 0
            self.redoable = False
387

388 4
    def _get_can_undo(self):
389
        """ Are there any undoable operations?
390
        """
391 4
        return self.now > 0
392

393 4
    def _get_can_redo(self):
394
        """ Are there any redoable operations?
395
        """
396 4
        return self.now < len(self.history)
397

398

399 4
class UndoHistoryUndoItem(AbstractUndoItem):
400
    """ An undo item for the undo history.
401
    """
402

403
    # -------------------------------------------------------------------------
404
    #  Trait definitions:
405
    # -------------------------------------------------------------------------
406

407
    #: The undo history to undo or redo
408 4
    history = Instance(UndoHistory)
409

410 4
    def undo(self):
411
        """ Undoes the change.
412
        """
413 0
        history = self.history
414 4
        for i in range(history.now - 1, -1, -1):
415 0
            items = history.history[i]
416 4
            for j in range(len(items) - 1, -1, -1):
417 0
                items[j].undo()
418

419 4
    def redo(self):
420
        """ Re-does the change.
421
        """
422 0
        history = self.history
423 4
        for i in range(0, history.now):
424 4
            for item in history.history[i]:
425 0
                item.redo()

Read our documentation on viewing source code .

Loading