Undo/Redo
Undo is a pretty common application functionality. I present a Python implementation where a History
object is used to perform any action that may be undone. This object can go back and forth in the actions history. The idea is that, for every action, you provide one way to perform the action and one way to revert the action.
Implementation
The History class definition follows. An instance maintains a list of actions, each action being composed of a way to perform something and a way to revert it:
class Action(object): """Describes an action, and a way to revert that action""" def __init__(self, do, undo): """Both do and undo are in the form (function, [arg1, arg2, ...]).""" self._do = do self._undo = undo def do(self): fun, args = self._do return fun(*args) def undo(self): fun, args = self._undo return fun(*args) class History(object): "Maintains a list of actions that can be undone and redone." def __init__(self): self._actions = [] self._last = -1 def _push(self, action): if self._last < len(self._actions) - 1: # erase previously undone actions del self._actions[self._last + 1:] self._actions.append(action) self._last = self._last + 1 def undo(self): if self._last < 0: return None else: action = self._actions[self._last] self._last = self._last - 1 return action.undo() def redo(self): if self._last == len(self._actions) - 1: return None else: self._last = self._last + 1 action = self._actions[self._last] return action.do() def add(self, do, undo): """Does an action and adds it to history. Both do and undo are in the form (function, [arg1, arg2, ...]). """ action = Action(do, undo) self._push(action) return action.do()
Usage
Download the history module and place it in your working directory to try out the examples.
First, a History
instance must be created.
from history import History h = History()
You can then add an action to the history…
h.add(perform, revert)
… and revert or redo that action later:
h.undo() h.redo()
The add()
method accepts as arguments two functions and their corresponding list of arguments:
def foo(x, y): .... def unfoo(z): .... perform = (foo, [arg1, arg2]) revert = (unfoo, [arg3]) h.add(perform, revert)
I take as an example the modification of a dictionary d
:
import history d = {"x": 0, "y": 0} h = history.History() def modify(d, key, value): d[key] = value def reset(d, saved): d.clear() d.update(saved) saved = d.copy() perform = (modify, [d, "x", 9999]) revert = (reset, [d, saved]) print "Original: ", d h.add(perform, revert) print "Modified: ", d h.undo() print "After undo:", d h.redo() print "After redo:", d
Original: {'y': 0, 'x': 0} Modified: {'y': 0, 'x': 9999} After undo: {'y': 0, 'x': 0} After redo: {'y': 0, 'x': 9999}
Advanced Example
In the code below, undo/redo is provided to a Calculator class through a Python decorator.
from history import History class Calculator(object): def __init__(self): self.value = 0. self._history = History() self.undo = self._history.undo self.redo = self._history.redo def _operation(method): def decorated(self, n): saved = self.value perform = (method, (self, n)) revert = (self._set_value, (saved,)) self._history.add(perform, revert) return decorated def _set_value(self, n): self.value = n @_operation def add(self, n): self.value += n @_operation def sub(self, n): self.value -= n @_operation def mult(self, n): self.value *= n @_operation def div(self, n): self.value /= n calculator = Calculator() print "Initial value:", calculator.value calculator.add(2) print "+ 2 =", calculator.value calculator.sub(3) print "- 3 =", calculator.value calculator.mult(5) print "* 5 =", calculator.value calculator.div(-2) print "/ -2 =", calculator.value print for _ in range(4): calculator.undo() print "undo:", calculator.value print for _ in range(4): calculator.redo() print "redo:", calculator.value
Output:
Initial value: 0.0 + 2 = 2.0 - 3 = -1.0 * 5 = -5.0 / -2 = 2.5 undo: -5.0 undo: -1.0 undo: 2.0 undo: 0.0 redo: 2.0 redo: -1.0 redo: -5.0 redo: 2.5