File: undoredo.py

package info (click to toggle)
editobj3 0.2%2Bds1-3
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 1,220 kB
  • sloc: javascript: 9,857; python: 5,475; makefile: 3
file content (145 lines) | stat: -rw-r--r-- 5,166 bytes parent folder | download | duplicates (4)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
# -*- coding: utf-8 -*-
# Editobj3
# Copyright (C) 2007-2014 Jean-Baptiste LAMY

# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.

# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Lesser General Public License for more details.

# You should have received a copy of the GNU Lesser General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

"""
editobj3.undoredo: Multiple undo/redo framework
-----------------------------------------------

This module contains a multiple undo/redo framework. It is used by Editobj3 dialog boxes,
and it automatically call :func:`editobj3.observe.scan()` when doing or undoing an operation.

.. data:: stack
   :annotation: the default undo/redo stack.
"""

__all__ = ["Stack", "UndoableOperation"]

import editobj3.observe as observe

class Stack(object):
  """An undo/redo stack.

:param limit: the maximum number of undo/redo, defaults to 20."""
  def __init__(self, limit = 20):
    self.limit     = limit
    self.undoables = []
    self.redoables = []
      
  def can_undo(self):
    """Returns True if it is possible to undo an operation."""
    if self.undoables: return self.undoables[-1]
    return False
  
  def can_redo(self):
    """Returns True if it is possible to redo an operation."""
    if self.redoables: return self.redoables[-1]
    return False
  
  def do_operation(self, operation):
    """Does the operation. Can be overriden, e.g. to save data after the changes performed by the operation.

:param operation: the operation.
:type operation: :class:`Operation`"""
    operation.do_func()
    
  def undo(self):
    """Undoes the last operation available."""
    if not self.undoables: raise ValueError("No operation to undo!")
    undo = self.undoables.pop()
    opposite = undo.opposite()
    
  def redo(self):
    """Redoes the last operation available."""
    if not self.redoables: raise ValueError("No operation to redo!")
    redo = self.redoables.pop()
    opposite = redo.opposite()
    
  def clear(self):
    """Clears all undo/redo operations."""
    self.undoables = []
    self.redoables = []

  def merge_last_operations(self, name = "", nb = 2):
    """Merges the NB last operations. They will now be undone / redone as a single operation, with the given NAME."""
    if not name: name = ", ".join(undoable.name for undoable in self.undoables[-nb:])
    doers   = [undoable.do_func   for undoable in self.undoables[-nb:]]
    undoers = [undoable.undo_func for undoable in self.undoables[-nb:]]
    def do_it():
      for doer in doers: doer()
    def undo_it():
      for undoer in reversed(undoers): undoer()
      
    del self.undoables[-nb + 1:]
    self.undoables[-1].do_func   = do_it
    self.undoables[-1].undo_func = undo_it
    self.undoables[-1].name      = name
    return self.undoables[-1]
  
  def __repr__(self):
    return "<%s, undoables:\n%s\n  redoables:\n%s\n>" % (
      self.__class__.__name__,
      "\n".join(["    %s" % repr(i) for i in self.undoables]),
      "\n".join(["    %s" % repr(i) for i in self.redoables]),
      )
    
stack = Stack()


class _Operation(object):
  def __init__(self, do_func, undo_func, name = "", stack_ = stack):
    self.do_func   = do_func
    self.undo_func = undo_func
    self.name      = name
    self.stack     = stack_ or stack
    stack.do_operation(self)
    
  def __repr__(self):
    return "<%s '%s' do_func='%s' undo_func='%s'>" % (self.__class__.__name__, self.name, self.do_func, self.undo_func)
    
class UndoableOperation(_Operation):
  """UndoableOperation(do_func, undo_func, name = "", stack = undoredo.stack)

An operation that can be undone.

:param do_func: a callable that do the operation when called.
:param undo_func: a callable that undo the operation when called.
:param name: the name of the operation.
:param stack: the undo/redo stack to add the operation to, defaults to undoredo.stack.
"""
  def __init__(self, do_func, undo_func, name = "", stack = None):
    _Operation.__init__(self, do_func, undo_func, name, stack)
    stack.undoables.append(self)
    if len(self.stack.undoables) > self.stack.limit: del self.stack.undoables[0]
    observe.scan()
    
  def opposite(self):
    return _RedoableOperation(self.undo_func, self.do_func, self.name, self.stack)

  def coalesce_with(self, previous_undoable_operation):
    self.undo_func = previous_undoable_operation.undo_func
    previous_undoable_operation.stack.undoables.remove(previous_undoable_operation)
    
class _RedoableOperation(_Operation):
  def __init__(self, do_func, undo_func, name = "", stack = stack):
    _Operation.__init__(self, do_func, undo_func, name, stack)
    stack.redoables.append(self)
    observe.scan()
    
  def opposite(self):
    return UndoableOperation(self.undo_func, self.do_func, self.name, self.stack)