| 1 | # vim: set expandtab ts=4 sw=4:
|
|---|
| 2 |
|
|---|
| 3 | # === UCSF ChimeraX Copyright ===
|
|---|
| 4 | # Copyright 2017 Regents of the University of California.
|
|---|
| 5 | # All rights reserved. This software provided pursuant to a
|
|---|
| 6 | # license agreement containing restrictions on its disclosure,
|
|---|
| 7 | # duplication and use. For details see:
|
|---|
| 8 | # http://www.rbvi.ucsf.edu/chimerax/docs/licensing.html
|
|---|
| 9 | # This notice must be embedded in or attached to all copies,
|
|---|
| 10 | # including partial copies, of the software or any revisions
|
|---|
| 11 | # or derivations thereof.
|
|---|
| 12 | # === UCSF ChimeraX Copyright ===
|
|---|
| 13 |
|
|---|
| 14 | """This module defines classes for maintaining stacks of "undo"
|
|---|
| 15 | and "redo" callbacks. Actions can register "undo" and "redo"
|
|---|
| 16 | functions which may be invoked via GUI, command or programmatically.
|
|---|
| 17 | """
|
|---|
| 18 |
|
|---|
| 19 | import abc
|
|---|
| 20 | from .state import StateManager
|
|---|
| 21 |
|
|---|
| 22 |
|
|---|
| 23 | class Undo(StateManager):
|
|---|
| 24 | """A per-session undo manager for tracking undo/redo callbacks.
|
|---|
| 25 |
|
|---|
| 26 | 'Undo' managers are per-session singletons that track
|
|---|
| 27 | undo/redo callbacks in two stacks: the undo and redo stacks.
|
|---|
| 28 | Actions can register objects that conform to the
|
|---|
| 29 | 'UndoAction'. When registered, an UndoAction instance
|
|---|
| 30 | is pushed on to the undo stack and the redo stack is cleared.
|
|---|
| 31 |
|
|---|
| 32 | When an "undo" is requested, an UndoAction is popped
|
|---|
| 33 | off the undo stack, and its undo callback is invoked
|
|---|
| 34 | If the undo callback throws an error or the UndoAction
|
|---|
| 35 | 'can_redo' attribute is false, the redo stack is cleared,
|
|---|
| 36 | because we cannot establish the "original" state for the
|
|---|
| 37 | next redo; otherwise, the UndoAction is pushed onto the
|
|---|
| 38 | redo stack.
|
|---|
| 39 |
|
|---|
| 40 | When a "redo" is requested, an UndoAction is popped off the
|
|---|
| 41 | redo stack, its redo callback is invoked, and the UndoAction
|
|---|
| 42 | is pushed on to the undo stack.
|
|---|
| 43 |
|
|---|
| 44 | Maximum stack depths are supported. If zero, there is no
|
|---|
| 45 | limit. Otherwise, if a stack grows deeper than its
|
|---|
| 46 | allowed maximum, the bottom of stack is discarded.
|
|---|
| 47 |
|
|---|
| 48 | Attributes
|
|---|
| 49 | ----------
|
|---|
| 50 | max_depth : int
|
|---|
| 51 | Maximum depth for both the undo and redo stacks.
|
|---|
| 52 | Default is 10. Setting to 0 removes limit.
|
|---|
| 53 | redo_stack : list
|
|---|
| 54 | List of UndoAction instances
|
|---|
| 55 | undo_stack : list
|
|---|
| 56 | List of UndoAction instances
|
|---|
| 57 | """
|
|---|
| 58 | # Most of this code is modeled after tools.Tools
|
|---|
| 59 |
|
|---|
| 60 | def __init__(self, session, first=False, max_depth=10):
|
|---|
| 61 | """Initialize per-session state manager for undo/redo actions.
|
|---|
| 62 |
|
|---|
| 63 | Parameters
|
|---|
| 64 | ----------
|
|---|
| 65 | session : instance of chimerax.core.session.Session
|
|---|
| 66 | Session for which this state manager was created.
|
|---|
| 67 | """
|
|---|
| 68 | import weakref
|
|---|
| 69 | self._session = weakref.ref(session)
|
|---|
| 70 | self.max_depth = max_depth
|
|---|
| 71 | self.undo_stack = []
|
|---|
| 72 | self.redo_stack = []
|
|---|
| 73 | self._register_stack = []
|
|---|
| 74 |
|
|---|
| 75 | @property
|
|---|
| 76 | def session(self):
|
|---|
| 77 | """Returns the session this undo state manager is in.
|
|---|
| 78 | """
|
|---|
| 79 | return self._session()
|
|---|
| 80 |
|
|---|
| 81 | def register_push(self, handler):
|
|---|
| 82 | """Push handler onto undo registration stack.
|
|---|
| 83 |
|
|---|
| 84 | Parameters
|
|---|
| 85 | ----------
|
|---|
| 86 | handler : instance of UndoHandler
|
|---|
| 87 | Handler that processes registration requests
|
|---|
| 88 |
|
|---|
| 89 | Returns
|
|---|
| 90 | -------
|
|---|
| 91 | The registered handler.
|
|---|
| 92 | """
|
|---|
| 93 | self._register_stack.insert(0, handler)
|
|---|
| 94 | return handler
|
|---|
| 95 |
|
|---|
| 96 | def register_pop(self):
|
|---|
| 97 | """Pop last pushed handler from undo registration stack.
|
|---|
| 98 |
|
|---|
| 99 | Returns
|
|---|
| 100 | -------
|
|---|
| 101 | The popped handler.
|
|---|
| 102 | """
|
|---|
| 103 | handler = self._register_stack.pop(0)
|
|---|
| 104 | return handler
|
|---|
| 105 |
|
|---|
| 106 | def aggregate(self, name):
|
|---|
| 107 | return UndoAggregateHandler(self, name)
|
|---|
| 108 |
|
|---|
| 109 | def block(self):
|
|---|
| 110 | return UndoBlockHandler(self, None)
|
|---|
| 111 |
|
|---|
| 112 | def register(self, action):
|
|---|
| 113 | """Register undo/redo actions with state manager.
|
|---|
| 114 |
|
|---|
| 115 | Parameters
|
|---|
| 116 | ----------
|
|---|
| 117 | action : instance of UndoAction
|
|---|
| 118 | Action that can change session between "before"
|
|---|
| 119 | and "after" states.
|
|---|
| 120 |
|
|---|
| 121 | Returns
|
|---|
| 122 | -------
|
|---|
| 123 | The registered action.
|
|---|
| 124 | """
|
|---|
| 125 | if len(self._register_stack):
|
|---|
| 126 | return self._register_stack[0].register(action)
|
|---|
| 127 | self._push(self.undo_stack, action)
|
|---|
| 128 | self.redo_stack.clear()
|
|---|
| 129 | self._update_ui()
|
|---|
| 130 | return action
|
|---|
| 131 |
|
|---|
| 132 | def deregister(self, action, delete_history=True):
|
|---|
| 133 | """Deregisters undo/redo actions from state manager.
|
|---|
| 134 | If the action is on the undo stack, all prior undo
|
|---|
| 135 | actions are deleted if 'delete_history' is True
|
|---|
| 136 | (default). Similarly, if the action is on the redo
|
|---|
| 137 | stack all subsequent redo actions are deleted if
|
|---|
| 138 | 'delete_history' is True. The 'delete_history'
|
|---|
| 139 | default is True because the deregistering action is
|
|---|
| 140 | the one to establish the "current" state for the
|
|---|
| 141 | next undo/redo action, so removing the action would
|
|---|
| 142 | likely prevent the next undo/redo action from working
|
|---|
| 143 | properly.
|
|---|
| 144 |
|
|---|
| 145 | Parameters
|
|---|
| 146 | ----------
|
|---|
| 147 | action : instance of UndoAction
|
|---|
| 148 | A previously registered UndoAction instance.
|
|---|
| 149 | """
|
|---|
| 150 | self._remove(self.undo_stack, action, delete_history)
|
|---|
| 151 | self._remove(self.redo_stack, action, delete_history)
|
|---|
| 152 | self._update_ui()
|
|---|
| 153 |
|
|---|
| 154 | def clear(self):
|
|---|
| 155 | """Clear both undo and redo stacks.
|
|---|
| 156 | """
|
|---|
| 157 | self.undo_stack.clear()
|
|---|
| 158 | self.redo_stack.clear()
|
|---|
| 159 | self._update_ui()
|
|---|
| 160 |
|
|---|
| 161 | def top_undo_name(self):
|
|---|
| 162 | """Return name for top undo action, or None if stack is empty.
|
|---|
| 163 | """
|
|---|
| 164 | return self._name(self.undo_stack)
|
|---|
| 165 |
|
|---|
| 166 | def top_redo_name(self):
|
|---|
| 167 | """Return name for top redo action, or None if stack is empty.
|
|---|
| 168 | """
|
|---|
| 169 | return self._name(self.redo_stack)
|
|---|
| 170 |
|
|---|
| 171 | def undo(self, silent=True):
|
|---|
| 172 | """Execute top undo action. Normally, if no undo action is
|
|---|
| 173 | available, nothing happens. If "silent" is False, an IndexError
|
|---|
| 174 | is raised for accessing invalid stack location.
|
|---|
| 175 | """
|
|---|
| 176 | try:
|
|---|
| 177 | inst = self._pop(self.undo_stack)
|
|---|
| 178 | except IndexError:
|
|---|
| 179 | if not silent:
|
|---|
| 180 | raise
|
|---|
| 181 | else:
|
|---|
| 182 | return
|
|---|
| 183 | from .errors import UserError
|
|---|
| 184 | try:
|
|---|
| 185 | inst.undo()
|
|---|
| 186 | except UserError:
|
|---|
| 187 | raise
|
|---|
| 188 | except Exception as e:
|
|---|
| 189 | self.session.logger.report_exception("undo failed: %s" % str(e))
|
|---|
| 190 | self.redo_stack.clear()
|
|---|
| 191 | else:
|
|---|
| 192 | if inst.can_redo:
|
|---|
| 193 | self._push(self.redo_stack, inst)
|
|---|
| 194 | else:
|
|---|
| 195 | self.redo_stack.clear()
|
|---|
| 196 | self._update_ui()
|
|---|
| 197 |
|
|---|
| 198 | def redo(self, silent=True):
|
|---|
| 199 | """Execute top redo action. Normally, if no redo action is
|
|---|
| 200 | available, nothing happens. If "silent" is False, an IndexError
|
|---|
| 201 | is raised for accessing invalid stack location.
|
|---|
| 202 | """
|
|---|
| 203 | try:
|
|---|
| 204 | inst = self._pop(self.redo_stack)
|
|---|
| 205 | except IndexError:
|
|---|
| 206 | if not silent:
|
|---|
| 207 | raise
|
|---|
| 208 | else:
|
|---|
| 209 | return
|
|---|
| 210 | from .errors import UserError
|
|---|
| 211 | try:
|
|---|
| 212 | inst.redo()
|
|---|
| 213 | except UserError:
|
|---|
| 214 | raise
|
|---|
| 215 | except Exception as e:
|
|---|
| 216 | self.session.logger.report_exception("redo failed: %s" % str(e))
|
|---|
| 217 | else:
|
|---|
| 218 | self._push(self.undo_stack, inst)
|
|---|
| 219 | self._update_ui()
|
|---|
| 220 |
|
|---|
| 221 | def set_depth(self, depth):
|
|---|
| 222 | """Set the maximum depth for the undo and redo stacks.
|
|---|
| 223 |
|
|---|
| 224 | Parameter
|
|---|
| 225 | ---------
|
|---|
| 226 | depth : int
|
|---|
| 227 | Maximum depth for stacks. Values <= 0 means unlimited.
|
|---|
| 228 | """
|
|---|
| 229 | if depth < 0:
|
|---|
| 230 | depth = 0
|
|---|
| 231 | self.max_depth = depth
|
|---|
| 232 | self._trim(self.undo_stack)
|
|---|
| 233 | self._trim(self.redo_stack)
|
|---|
| 234 |
|
|---|
| 235 | # State methods
|
|---|
| 236 |
|
|---|
| 237 | def take_snapshot(self, session, flags):
|
|---|
| 238 | return {"version":1, "max_depth":self.max_depth}
|
|---|
| 239 |
|
|---|
| 240 | @classmethod
|
|---|
| 241 | def restore_snapshot(cls, session, data):
|
|---|
| 242 | return cls(session, max_depth=data["max_depth"])
|
|---|
| 243 |
|
|---|
| 244 | def reset_state(self, session):
|
|---|
| 245 | """Reset state to data-less state"""
|
|---|
| 246 | self.clear()
|
|---|
| 247 |
|
|---|
| 248 | # Internal methods
|
|---|
| 249 |
|
|---|
| 250 | def _trim(self, stack):
|
|---|
| 251 | if self.max_depth > 0:
|
|---|
| 252 | while len(stack) > self.max_depth:
|
|---|
| 253 | stack.pop(0)
|
|---|
| 254 |
|
|---|
| 255 | def _push(self, stack, inst):
|
|---|
| 256 | stack.append(inst)
|
|---|
| 257 | self._trim(stack)
|
|---|
| 258 |
|
|---|
| 259 | def _pop(self, stack):
|
|---|
| 260 | return stack.pop()
|
|---|
| 261 |
|
|---|
| 262 | def _remove(self, stack, action, delete_history):
|
|---|
| 263 | try:
|
|---|
| 264 | n = stack.index(action)
|
|---|
| 265 | except ValueError:
|
|---|
| 266 | pass
|
|---|
| 267 | else:
|
|---|
| 268 | if delete_history:
|
|---|
| 269 | del stack[:n+1]
|
|---|
| 270 | else:
|
|---|
| 271 | del stack[n]
|
|---|
| 272 |
|
|---|
| 273 | def _name(self, stack):
|
|---|
| 274 | try:
|
|---|
| 275 | return stack[-1].name
|
|---|
| 276 | except IndexError:
|
|---|
| 277 | return None
|
|---|
| 278 |
|
|---|
| 279 | def _update_ui(self):
|
|---|
| 280 | session = self._session()
|
|---|
| 281 | if session is None:
|
|---|
| 282 | return
|
|---|
| 283 | try:
|
|---|
| 284 | f = session.ui.update_undo
|
|---|
| 285 | except AttributeError:
|
|---|
| 286 | pass
|
|---|
| 287 | else:
|
|---|
| 288 | f(self)
|
|---|
| 289 |
|
|---|
| 290 |
|
|---|
| 291 | class UndoAction:
|
|---|
| 292 | """An instance holding the name for a pair of undo/redo callbacks.
|
|---|
| 293 |
|
|---|
| 294 | Attributes
|
|---|
| 295 | ----------
|
|---|
| 296 | name : str
|
|---|
| 297 | Name for the pair of undo/redo callbacks that changes
|
|---|
| 298 | session between start and end states.
|
|---|
| 299 | can_redo : boolean
|
|---|
| 300 | Whether this instance supports redoing an action after
|
|---|
| 301 | undoing it.
|
|---|
| 302 | """
|
|---|
| 303 |
|
|---|
| 304 | def __init__(self, name, can_redo=True):
|
|---|
| 305 | self.name = name
|
|---|
| 306 | self.can_redo = can_redo
|
|---|
| 307 |
|
|---|
| 308 | def undo(self):
|
|---|
| 309 | """Undo an action.
|
|---|
| 310 | """
|
|---|
| 311 | raise NotImplementedError("undo")
|
|---|
| 312 |
|
|---|
| 313 | def redo(self):
|
|---|
| 314 | """Redo an action.
|
|---|
| 315 | """
|
|---|
| 316 | raise NotImplementedError("redo")
|
|---|
| 317 |
|
|---|
| 318 |
|
|---|
| 319 | class UndoState(UndoAction):
|
|---|
| 320 | """An instance that stores tuples of (owner,
|
|---|
| 321 | attribute name, old values, new values) and uses the
|
|---|
| 322 | information to undo/redo actions. 'owner' may be
|
|---|
| 323 | a simple instance or an ordered container such as
|
|---|
| 324 | a list or an 'atomic.molarray.Collection' instance.
|
|---|
| 325 |
|
|---|
| 326 | Attributes
|
|---|
| 327 | ----------
|
|---|
| 328 | name : str
|
|---|
| 329 | can_redo : boolean
|
|---|
| 330 | Inherited from UndoAction.
|
|---|
| 331 | state : list
|
|---|
| 332 | List of (owner, attribute, old, new, options) tuples that
|
|---|
| 333 | have been added to the action.
|
|---|
| 334 | """
|
|---|
| 335 |
|
|---|
| 336 | _valid_options = ["A", "M", "MA", "MK", "S"]
|
|---|
| 337 |
|
|---|
| 338 | def __init__(self, name, can_redo=True):
|
|---|
| 339 | super().__init__(name, can_redo)
|
|---|
| 340 | self.state = []
|
|---|
| 341 |
|
|---|
| 342 | def add(self, owner, attribute, old_value, new_value, option="A", *,
|
|---|
| 343 | deleted_check=lambda obj: hasattr(obj, 'deleted') and obj.deleted):
|
|---|
| 344 | """Add another tuple of (owner, attribute, old_value, new_value,
|
|---|
| 345 | option) to the undo action state.
|
|---|
| 346 |
|
|---|
| 347 | Parameters
|
|---|
| 348 | ----------
|
|---|
| 349 | owner : instance
|
|---|
| 350 | An instance or a container of instances. If owner
|
|---|
| 351 | is a container, then undo/redo callbacks will check
|
|---|
| 352 | to make sure that old_value has the same number of
|
|---|
| 353 | elements.
|
|---|
| 354 | attribute : string
|
|---|
| 355 | Name of attribute whose value changes with undo/redo.
|
|---|
| 356 | old_value : object
|
|---|
| 357 | Value for attribute after undo.
|
|---|
| 358 | If owner is a container, then old_value should be
|
|---|
| 359 | a container of values with the same number of elements.
|
|---|
| 360 | Otherwise, any value is acceptable.
|
|---|
| 361 | new_value : object
|
|---|
| 362 | Value for attribute after redo.
|
|---|
| 363 | Even if owner is a container, new_value may be a
|
|---|
| 364 | simple value, in which case all elements in the
|
|---|
| 365 | owner container will receive the same attribute value.
|
|---|
| 366 | option : string
|
|---|
| 367 | Option specifying how the attribute and values are
|
|---|
| 368 | used for updating state. If option is "A" (default),
|
|---|
| 369 | the attribute is changed to the value using setattr.
|
|---|
| 370 | If option is "M", the attribute is assumed to be callable
|
|---|
| 371 | with a single argument of the value. If option is "MA",
|
|---|
| 372 | the attribute is called with the values as its argument
|
|---|
| 373 | list, i.e., attribute(*value). If option is "MK", the
|
|---|
| 374 | attribute iscalled with the values as keywords, i.e.,
|
|---|
| 375 | attribute(**value). If option is "S" then owner is a sequence
|
|---|
| 376 | and old and new values are sequences of the same length
|
|---|
| 377 | and setattr is used to set each element of the owner sequence
|
|---|
| 378 | to the corresponding element of the value sequence.
|
|---|
| 379 | """
|
|---|
| 380 | if option not in self._valid_options:
|
|---|
| 381 | raise ValueError("invalid UndoState option: %s" % option)
|
|---|
| 382 | self.state.append((owner, attribute, old_value, new_value, option, deleted_check))
|
|---|
| 383 |
|
|---|
| 384 | def undo(self):
|
|---|
| 385 | """Undo action (set owner attributes to old values).
|
|---|
| 386 | """
|
|---|
| 387 | self._consistency_check()
|
|---|
| 388 | for owner, attribute, old_value, new_value, option, deleted_check in reversed(self.state):
|
|---|
| 389 | self._update_owner(owner, attribute, old_value, option, deleted_check)
|
|---|
| 390 |
|
|---|
| 391 | def redo(self):
|
|---|
| 392 | """Redo action (set owner attributes to new values).
|
|---|
| 393 | """
|
|---|
| 394 | self._consistency_check()
|
|---|
| 395 | for owner, attribute, old_value, new_value, option, deleted_check in self.state:
|
|---|
| 396 | self._update_owner(owner, attribute, new_value, option, deleted_check)
|
|---|
| 397 |
|
|---|
| 398 | def _consistency_check(self):
|
|---|
| 399 | for owner, attribute, old_value, new_value, option, deleted_check in self.state:
|
|---|
| 400 | try:
|
|---|
| 401 | owner_length = len(owner)
|
|---|
| 402 | except TypeError:
|
|---|
| 403 | # Not a container, so move on
|
|---|
| 404 | continue
|
|---|
| 405 | else:
|
|---|
| 406 | # Is a container, old_value must be the same length
|
|---|
| 407 | try:
|
|---|
| 408 | value_length = len(old_value)
|
|---|
| 409 | except TypeError:
|
|---|
| 410 | value_length = 1
|
|---|
| 411 | if value_length != owner_length:
|
|---|
| 412 | from .errors import UserError
|
|---|
| 413 | raise UserError("Undo failed, probably because "
|
|---|
| 414 | "structures have been modified.")
|
|---|
| 415 |
|
|---|
| 416 | def _update_owner(self, owner, attribute, value, option, deleted_check):
|
|---|
| 417 | if option != "S":
|
|---|
| 418 | if deleted_check(owner):
|
|---|
| 419 | return
|
|---|
| 420 | if option == "A":
|
|---|
| 421 | setattr(owner, attribute, value)
|
|---|
| 422 | elif option == "M":
|
|---|
| 423 | getattr(owner, attribute)(value)
|
|---|
| 424 | elif option == "MK":
|
|---|
| 425 | getattr(owner, attribute)(**value)
|
|---|
| 426 | elif option == "MA":
|
|---|
| 427 | getattr(owner, attribute)(*value)
|
|---|
| 428 | elif option == "S":
|
|---|
| 429 | for e,v in zip(owner, value):
|
|---|
| 430 | if deleted_check(e):
|
|---|
| 431 | continue
|
|---|
| 432 | setattr(e, attribute, v)
|
|---|
| 433 |
|
|---|
| 434 |
|
|---|
| 435 | class UndoHandler(metaclass=abc.ABCMeta):
|
|---|
| 436 | """An instance that intercepts undo registration
|
|---|
| 437 | requests. For example, multiple undo actions may
|
|---|
| 438 | be aggregated into a single undo action; or
|
|---|
| 439 | undo actions may be blocked and replaced with
|
|---|
| 440 | a more efficient undo mechanism.
|
|---|
| 441 | """
|
|---|
| 442 |
|
|---|
| 443 | def __init__(self, mgr, name):
|
|---|
| 444 | """Initialize undo handler.
|
|---|
| 445 |
|
|---|
| 446 | Parameters
|
|---|
| 447 | ----------
|
|---|
| 448 | mgr : instance of Undo
|
|---|
| 449 | Undo manager for which this undo handler was created.
|
|---|
| 450 | name : str
|
|---|
| 451 | Name of undo action to register
|
|---|
| 452 | """
|
|---|
| 453 | self.name = name
|
|---|
| 454 | self.mgr = mgr
|
|---|
| 455 |
|
|---|
| 456 | def __enter__(self):
|
|---|
| 457 | self.mgr.register_push(self)
|
|---|
| 458 | return self
|
|---|
| 459 |
|
|---|
| 460 | def __exit__(self, exc_type, exc_value, exc_traceback):
|
|---|
| 461 | self.mgr.register_pop()
|
|---|
| 462 | if not exc_type:
|
|---|
| 463 | self.finish()
|
|---|
| 464 |
|
|---|
| 465 | @abc.abstractmethod
|
|---|
| 466 | def register(self, action):
|
|---|
| 467 | """Register undo/redo actions.
|
|---|
| 468 |
|
|---|
| 469 | Parameters
|
|---|
| 470 | ----------
|
|---|
| 471 | action : instance of UndoAction
|
|---|
| 472 | Action that can change session between "before"
|
|---|
| 473 | and "after" states.
|
|---|
| 474 |
|
|---|
| 475 | Returns
|
|---|
| 476 | -------
|
|---|
| 477 | The registered action.
|
|---|
| 478 | """
|
|---|
| 479 | pass
|
|---|
| 480 |
|
|---|
| 481 | @abc.abstractmethod
|
|---|
| 482 | def finish(self):
|
|---|
| 483 | """Finish processing intercepted registration requests."""
|
|---|
| 484 | pass
|
|---|
| 485 |
|
|---|
| 486 |
|
|---|
| 487 | class UndoAggregateHandler(UndoHandler):
|
|---|
| 488 | """An instance that intercepts undo registration
|
|---|
| 489 | requests and aggregates them into a single undo action.
|
|---|
| 490 | """
|
|---|
| 491 |
|
|---|
| 492 | def __init__(self, mgr, name):
|
|---|
| 493 | super().__init__(mgr, name)
|
|---|
| 494 | self.actions = []
|
|---|
| 495 |
|
|---|
| 496 | def register(self, action):
|
|---|
| 497 | self.actions.append(action)
|
|---|
| 498 |
|
|---|
| 499 | def finish(self):
|
|---|
| 500 | a = UndoAggregateAction(self.name, self.actions)
|
|---|
| 501 | self.mgr.register(a)
|
|---|
| 502 |
|
|---|
| 503 |
|
|---|
| 504 | class UndoAggregateAction(UndoAction):
|
|---|
| 505 | """An instance that executes a list of UndoAction
|
|---|
| 506 | instances as a group."""
|
|---|
| 507 |
|
|---|
| 508 | def __init__(self, name, actions):
|
|---|
| 509 | can_redo = all([a.can_redo for a in actions])
|
|---|
| 510 | super().__init__(name, can_redo=can_redo)
|
|---|
| 511 | self.actions = actions
|
|---|
| 512 |
|
|---|
| 513 | def undo(self):
|
|---|
| 514 | for a in reversed(self.actions):
|
|---|
| 515 | a.undo()
|
|---|
| 516 |
|
|---|
| 517 | def redo(self):
|
|---|
| 518 | for a in self.actions:
|
|---|
| 519 | a.redo()
|
|---|
| 520 |
|
|---|
| 521 |
|
|---|
| 522 | class UndoBlockHandler(UndoHandler):
|
|---|
| 523 | """An instance that intercepts undo registration
|
|---|
| 524 | requests and discards them.
|
|---|
| 525 | """
|
|---|
| 526 |
|
|---|
| 527 | def register(self, action):
|
|---|
| 528 | pass
|
|---|
| 529 |
|
|---|
| 530 | def finish(self):
|
|---|
| 531 | pass
|
|---|