Source code for madgui.util.undo

"""
This module provides a lightweight alternative for the ``QUndoStack`` that
can be used in the absence of Qt and without initializing a ``QApplication``.
"""

__all__ = [
    'Command',
    'UndoCommand',
    'UndoStack',
]

from contextlib import contextmanager
import logging

from madgui.util.misc import invalidate
from madgui.util.signal import Signal


[docs]class Command: """Base class for un-/re-doable actions. Command objects are only to be pushed onto an UndoStack and must not be called directly.""" text = ''
[docs] def undo(self): """Undo the action represented by this command."""
[docs] def redo(self): """Exec the action represented by this command."""
[docs]class UndoCommand(Command): """ A diff-based state transition, initialized with ``old`` and ``new`` values that must be ``apply``-ed to go backward or forward in time. """ def __init__(self, old, new, apply, text): self._old = new self._new = old self._apply = apply self.text = text
[docs] def undo(self): """Go backward in time by applying the ``old`` state.""" self._apply(self._old)
[docs] def redo(self): """Go forward in time by applying the ``new`` state.""" self._apply(self._new)
class Stack: # TODO: merge this class with History and Selection def __init__(self, items=None): self.items = [] if items is None else items self.index = 0 def clear(self): """Empty the stack.""" self.items.clear() self.index = 0 def push(self, item): """Remove all items past our current position.""" self.items[self.index:] = [item] self.index += 1 def pop(self): """Read the previous value from the buffer.""" if self.index > 0: self.index -= 1 return self.items[self.index] def unpop(self): """Increase index by one and return the newly available item.""" if self.index < len(self.items): self.index += 1 return self.items[self.index - 1] def can_pop(self): """Check whether an item can be popped from the stack.""" return self.index > 0 def can_unpop(self): """Check whether an item is available past the top of the stack.""" return self.index < len(self.items) def top(self): """Get the top item.""" if self.index > 0: return self.items[self.index - 1] def truncate(self): """Forget all elements above current index.""" del self.items[self.index:] class Macro(Command): """An accumulation of multiple recorded subcommands.""" def __init__(self, text, commands=None): self.text = text self.commands = [] if commands is None else commands def undo(self): """Undo all subcommands in reverse order.""" for command in self.commands[::-1]: command.undo() def redo(self): """Exec all subcommands in original order.""" for command in self.commands: command.redo() def count(self): """Return number of commands.""" return len(self.commands)
[docs]class UndoStack: """ Serves as lightweight replacement for QUndoStack. """ changed = Signal() def __init__(self): self._root = Stack() self._leaf = self._root
[docs] def clear(self): """Clear the stack. This can only be done when no macro is active.""" assert self._leaf is self._root self._root.clear() self.changed.emit()
[docs] def push(self, command): """Push and execute command, and truncate all history after current stack position.""" self._leaf.push(command) command.redo() self.changed.emit()
[docs] def truncate(self): """Truncate history after current stack position.""" self._leaf.truncate() self.changed.emit()
[docs] def count(self): """Return number of commands on the stack.""" return len(self._root.items)
[docs] def command(self, index): """Return the i-th command in the stack.""" return self._root.items[index]
[docs] def can_undo(self): """Check if an undo action can be performed.""" return self._leaf.can_pop()
[docs] def can_redo(self): """Check if a redo action can be performed.""" return self._leaf.can_unpop()
[docs] def undo(self): """Undo command before current stack pointer.""" if self._leaf.can_pop(): command = self._leaf.pop() command.undo() self.changed.emit()
[docs] def redo(self): """Redo command behind current stack pointer.""" if self._leaf.can_unpop(): command = self._leaf.unpop() command.redo() self.changed.emit()
[docs] @contextmanager def macro(self, text=""): if text: logging.info(text) stack = Stack() macro = Macro(text, stack.items) backup = self._leaf try: self.push(macro) # These two lines must be in this order to self._leaf = stack # avoid an infinite recursion! yield macro finally: self._leaf = backup stack.truncate() if macro.count() == 0 and self._leaf.top() is macro: self.undo() self.truncate()
[docs] @contextmanager def rollback(self, text="temporary change", transient=False): macro = None if transient: old = getattr(self.model, '_twiss', None) try: with self.macro(text) as macro: yield macro finally: if self._leaf.top() is macro: self.undo() self.truncate() if transient: if old is None: invalidate(self.model, 'twiss') else: self.model._twiss = old
[docs] def create_undo_action(self, parent): """Create a :class:`~PyQt5.QtWidgets.QAction` for an "Undo" button.""" from PyQt5.QtWidgets import QAction, QStyle from PyQt5.QtGui import QKeySequence icon = parent.style().standardIcon(QStyle.SP_ArrowBack) action = QAction(icon, "Undo", parent) action.setShortcut(QKeySequence.Undo) action.setStatusTip("Undo") action.triggered.connect(self.undo) action.setEnabled(self.can_undo()) self.changed.connect(lambda: action.setEnabled(self.can_undo())) return action
[docs] def create_redo_action(self, parent): """Create a :class:`~PyQt5.QtWidgets.QAction` for a "Redo" button.""" from PyQt5.QtWidgets import QAction, QStyle from PyQt5.QtGui import QKeySequence icon = parent.style().standardIcon(QStyle.SP_ArrowForward) action = QAction(icon, "Redo", parent) action.setShortcut(QKeySequence.Redo) action.setStatusTip("Redo") action.triggered.connect(self.redo) action.setEnabled(self.can_redo()) self.changed.connect(lambda: action.setEnabled(self.can_redo())) return action