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""" Defines the manager for Undo and Redo history for Traits user interface 19 support. 20""" 21 22import collections.abc 23 24from 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 38NumericTypes = (int, float, complex) 39SimpleTypes = (str, bytes) + NumericTypes 40 41 42class AbstractUndoItem(HasPrivateTraits): 43 """ Abstract base class for undo items. 44 """ 45 46 def undo(self): 47 """ Undoes the change. 48 """ 49 raise NotImplementedError 50 51 def redo(self): 52 """ Re-does the change. 53 """ 54 raise NotImplementedError 55 56 def merge_undo(self, undo_item): 57 """ Merges two undo items if possible. 58 """ 59 return False 60 61 62class 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 object = Instance(HasTraits) 72 73 #: Name of the trait that changed 74 name = Str() 75 76 #: Old value of the changed trait 77 old_value = Property() 78 79 #: New value of the changed trait 80 new_value = Property() 81 82 def _get_old_value(self): 83 return self._old_value 84 85 def _set_old_value(self, value): 86 if isinstance(value, list): 87 value = value[:] 88 self._old_value = value 89 90 def _get_new_value(self): 91 return self._new_value 92 93 def _set_new_value(self, value): 94 if isinstance(value, list): 95 value = value[:] 96 self._new_value = value 97 98 def undo(self): 99 """ Undoes the change. 100 """ 101 try: 102 setattr(self.object, self.name, self.old_value) 103 except Exception: 104 from traitsui.api import raise_to_debug 105 106 raise_to_debug() 107 108 def redo(self): 109 """ Re-does the change. 110 """ 111 try: 112 setattr(self.object, self.name, self.new_value) 113 except Exception: 114 from traitsui.api import raise_to_debug 115 116 raise_to_debug() 117 118 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 if ( 124 isinstance(undo_item, self.__class__) 125 and (self.object is undo_item.object) 126 and (self.name == undo_item.name) 127 ): 128 v1 = self.new_value 129 v2 = undo_item.new_value 130 t1 = type(v1) 131 if isinstance(v2, t1): 132 133 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 n1 = len(v1) 139 n2 = len(v2) 140 if abs(n1 - n2) > 1: 141 return False 142 n = min(n1, n2) 143 i = 0 144 while (i < n) and (v1[i] == v2[i]): 145 i += 1 146 if v1[i + (n2 <= n1):] == v2[i + (n2 >= n1):]: 147 self.new_value = v2 148 return True 149 150 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 v1 = self.old_value 155 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 try: 160 if len(v1) == len(v2): 161 diffs = 0 162 for i, item in enumerate(v1): 163 titem = type(item) 164 item2 = v2[i] 165 if ( 166 (titem not in SimpleTypes) 167 or (not isinstance(item2, titem)) 168 or (item != item2) 169 ): 170 diffs += 1 171 if diffs >= 2: 172 return False 173 if diffs == 0: 174 return False 175 self.new_value = v2 176 return True 177 except Exception: 178 pass 179 180 elif t1 in NumericTypes: 181 # Always merge simple numeric trait changes: 182 self.new_value = v2 183 return True 184 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 201class 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 object = Instance(HasTraits) 211 212 #: Name of the trait that changed 213 name = Str() 214 215 #: Starting index 216 index = Int() 217 218 #: Items added to the list 219 added = List() 220 221 #: Items removed from the list 222 removed = List() 223 224 def undo(self): 225 """ Undoes the change. 226 """ 227 try: 228 list = getattr(self.object, self.name) 229 list[self.index : (self.index + len(self.added))] = self.removed 230 except Exception: 231 from traitsui.api import raise_to_debug 232 233 raise_to_debug() 234 235 def redo(self): 236 """ Re-does the change. 237 """ 238 try: 239 list = getattr(self.object, self.name) 240 list[self.index : (self.index + len(self.removed))] = self.added 241 except Exception: 242 from traitsui.api import raise_to_debug 243 244 raise_to_debug() 245 246 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 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 added = undo_item.added 259 removed = undo_item.removed 260 if (len(self.added) == len(added)) and ( 261 len(self.removed) == len(removed) 262 ): 263 for i, item in enumerate(self.added): 264 if item is not added[i]: 265 break 266 else: 267 for i, item in enumerate(self.removed): 268 if item is not removed[i]: 269 break 270 else: 271 return True 272 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 286class UndoHistory(HasStrictTraits): 287 """ Manages a list of undoable changes. 288 """ 289 290 # ------------------------------------------------------------------------- 291 # Trait definitions: 292 # ------------------------------------------------------------------------- 293 294 #: List of accumulated undo changes 295 history = List() 296 #: The current position in the list 297 now = Int() 298 #: Fired when state changes to undoable 299 undoable = Event(False) 300 #: Fired when state changes to redoable 301 redoable = Event(False) 302 #: Can an action be undone? 303 can_undo = Property() 304 #: Can an action be redone? 305 can_redo = Property() 306 307 def add(self, undo_item, extend=False): 308 """ Adds an UndoItem to the history. 309 """ 310 if extend: 311 self.extend(undo_item) 312 return 313 314 # Try to merge the new undo item with the previous item if allowed: 315 now = self.now 316 if now > 0: 317 previous = self.history[now - 1] 318 if (len(previous) == 1) and previous[0].merge_undo(undo_item): 319 self.history[now:] = [] 320 return 321 322 old_len = len(self.history) 323 self.history[now:] = [[undo_item]] 324 self.now += 1 325 if self.now == 1: 326 self.undoable = True 327 if self.now <= old_len: 328 self.redoable = False 329 330 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 if self.now > 0: 337 undo_list = self.history[self.now - 1] 338 if not undo_list[-1].merge_undo(undo_item): 339 undo_list.append(undo_item) 340 341 def undo(self): 342 """ Undoes an operation. 343 """ 344 if self.can_undo: 345 self.now -= 1 346 items = self.history[self.now] 347 for i in range(len(items) - 1, -1, -1): 348 items[i].undo() 349 if self.now == 0: 350 self.undoable = False 351 if self.now == (len(self.history) - 1): 352 self.redoable = True 353 354 def redo(self): 355 """ Redoes an operation. 356 """ 357 if self.can_redo: 358 self.now += 1 359 for item in self.history[self.now - 1]: 360 item.redo() 361 if self.now == 1: 362 self.undoable = True 363 if self.now == len(self.history): 364 self.redoable = False 365 366 def revert(self): 367 """ Reverts all changes made so far and clears the history. 368 """ 369 history = self.history[: self.now] 370 self.clear() 371 for i in range(len(history) - 1, -1, -1): 372 items = history[i] 373 for j in range(len(items) - 1, -1, -1): 374 items[j].undo() 375 376 def clear(self): 377 """ Clears the undo history. 378 """ 379 old_len = len(self.history) 380 old_now = self.now 381 self.now = 0 382 del self.history[:] 383 if old_now > 0: 384 self.undoable = False 385 if old_now < old_len: 386 self.redoable = False 387 388 def _get_can_undo(self): 389 """ Are there any undoable operations? 390 """ 391 return self.now > 0 392 393 def _get_can_redo(self): 394 """ Are there any redoable operations? 395 """ 396 return self.now < len(self.history) 397 398 399class 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 history = Instance(UndoHistory) 409 410 def undo(self): 411 """ Undoes the change. 412 """ 413 history = self.history 414 for i in range(history.now - 1, -1, -1): 415 items = history.history[i] 416 for j in range(len(items) - 1, -1, -1): 417 items[j].undo() 418 419 def redo(self): 420 """ Re-does the change. 421 """ 422 history = self.history 423 for i in range(0, history.now): 424 for item in history.history[i]: 425 item.redo() 426