3 from Delegator
import Delegator
13 #$ event <<dump-undo-state>>
14 #$ win <Control-backslash>
15 #$ unix <Control-backslash>
18 class UndoDelegator(Delegator
):
23 Delegator
.__init
__(self
)
26 def setdelegate(self
, delegate
):
27 if self
.delegate
is not None:
28 self
.unbind("<<undo>>")
29 self
.unbind("<<redo>>")
30 self
.unbind("<<dump-undo-state>>")
31 Delegator
.setdelegate(self
, delegate
)
32 if delegate
is not None:
33 self
.bind("<<undo>>", self
.undo_event
)
34 self
.bind("<<redo>>", self
.redo_event
)
35 self
.bind("<<dump-undo-state>>", self
.dump_event
)
37 def dump_event(self
, event
):
38 from pprint
import pprint
39 pprint(self
.undolist
[:self
.pointer
])
40 print "pointer:", self
.pointer
,
41 print "saved:", self
.saved
,
42 print "can_merge:", self
.can_merge
,
43 print "get_saved():", self
.get_saved()
44 pprint(self
.undolist
[self
.pointer
:])
51 self
.undoblock
= 0 # or a CommandSequence instance
54 def set_saved(self
, flag
):
56 self
.saved
= self
.pointer
59 self
.can_merge
= False
63 return self
.saved
== self
.pointer
65 saved_change_hook
= None
67 def set_saved_change_hook(self
, hook
):
68 self
.saved_change_hook
= hook
72 def check_saved(self
):
73 is_saved
= self
.get_saved()
74 if is_saved
!= self
.was_saved
:
75 self
.was_saved
= is_saved
76 if self
.saved_change_hook
:
77 self
.saved_change_hook()
79 def insert(self
, index
, chars
, tags
=None):
80 self
.addcmd(InsertCommand(index
, chars
, tags
))
82 def delete(self
, index1
, index2
=None):
83 self
.addcmd(DeleteCommand(index1
, index2
))
85 # Clients should call undo_block_start() and undo_block_stop()
86 # around a sequence of editing cmds to be treated as a unit by
87 # undo & redo. Nested matching calls are OK, and the inner calls
88 # then act like nops. OK too if no editing cmds, or only one
89 # editing cmd, is issued in between: if no cmds, the whole
90 # sequence has no effect; and if only one cmd, that cmd is entered
91 # directly into the undo list, as if undo_block_xxx hadn't been
92 # called. The intent of all that is to make this scheme easy
93 # to use: all the client has to worry about is making sure each
94 # _start() call is matched by a _stop() call.
96 def undo_block_start(self
):
97 if self
.undoblock
== 0:
98 self
.undoblock
= CommandSequence()
99 self
.undoblock
.bump_depth()
101 def undo_block_stop(self
):
102 if self
.undoblock
.bump_depth(-1) == 0:
107 # no need to wrap a single cmd
109 # this blk of cmds, or single cmd, has already
110 # been done, so don't execute it again
113 def addcmd(self
, cmd
, execute
=True):
115 cmd
.do(self
.delegate
)
116 if self
.undoblock
!= 0:
117 self
.undoblock
.append(cmd
)
119 if self
.can_merge
and self
.pointer
> 0:
120 lastcmd
= self
.undolist
[self
.pointer
-1]
121 if lastcmd
.merge(cmd
):
123 self
.undolist
[self
.pointer
:] = [cmd
]
124 if self
.saved
> self
.pointer
:
126 self
.pointer
= self
.pointer
+ 1
127 if len(self
.undolist
) > self
.max_undo
:
128 ##print "truncating undo list"
130 self
.pointer
= self
.pointer
- 1
132 self
.saved
= self
.saved
- 1
133 self
.can_merge
= True
136 def undo_event(self
, event
):
137 if self
.pointer
== 0:
140 cmd
= self
.undolist
[self
.pointer
- 1]
141 cmd
.undo(self
.delegate
)
142 self
.pointer
= self
.pointer
- 1
143 self
.can_merge
= False
147 def redo_event(self
, event
):
148 if self
.pointer
>= len(self
.undolist
):
151 cmd
= self
.undolist
[self
.pointer
]
152 cmd
.redo(self
.delegate
)
153 self
.pointer
= self
.pointer
+ 1
154 self
.can_merge
= False
161 # Base class for Undoable commands
165 def __init__(self
, index1
, index2
, chars
, tags
=None):
166 self
.marks_before
= {}
167 self
.marks_after
= {}
175 s
= self
.__class
__.__name
__
176 t
= (self
.index1
, self
.index2
, self
.chars
, self
.tags
)
177 if self
.tags
is None:
184 def redo(self
, text
):
187 def undo(self
, text
):
190 def merge(self
, cmd
):
193 def save_marks(self
, text
):
195 for name
in text
.mark_names():
196 if name
!= "insert" and name
!= "current":
197 marks
[name
] = text
.index(name
)
200 def set_marks(self
, text
, marks
):
201 for name
, index
in marks
.items():
202 text
.mark_set(name
, index
)
205 class InsertCommand(Command
):
207 # Undoable insert command
209 def __init__(self
, index1
, chars
, tags
=None):
210 Command
.__init
__(self
, index1
, None, chars
, tags
)
213 self
.marks_before
= self
.save_marks(text
)
214 self
.index1
= text
.index(self
.index1
)
215 if text
.compare(self
.index1
, ">", "end-1c"):
216 # Insert before the final newline
217 self
.index1
= text
.index("end-1c")
218 text
.insert(self
.index1
, self
.chars
, self
.tags
)
219 self
.index2
= text
.index("%s+%dc" % (self
.index1
, len(self
.chars
)))
220 self
.marks_after
= self
.save_marks(text
)
221 ##sys.__stderr__.write("do: %s\n" % self)
223 def redo(self
, text
):
224 text
.mark_set('insert', self
.index1
)
225 text
.insert(self
.index1
, self
.chars
, self
.tags
)
226 self
.set_marks(text
, self
.marks_after
)
228 ##sys.__stderr__.write("redo: %s\n" % self)
230 def undo(self
, text
):
231 text
.mark_set('insert', self
.index1
)
232 text
.delete(self
.index1
, self
.index2
)
233 self
.set_marks(text
, self
.marks_before
)
235 ##sys.__stderr__.write("undo: %s\n" % self)
237 def merge(self
, cmd
):
238 if self
.__class
__ is not cmd
.__class
__:
240 if self
.index2
!= cmd
.index1
:
242 if self
.tags
!= cmd
.tags
:
244 if len(cmd
.chars
) != 1:
247 self
.classify(self
.chars
[-1]) != self
.classify(cmd
.chars
):
249 self
.index2
= cmd
.index2
250 self
.chars
= self
.chars
+ cmd
.chars
253 alphanumeric
= string
.ascii_letters
+ string
.digits
+ "_"
255 def classify(self
, c
):
256 if c
in self
.alphanumeric
:
257 return "alphanumeric"
263 class DeleteCommand(Command
):
265 # Undoable delete command
267 def __init__(self
, index1
, index2
=None):
268 Command
.__init
__(self
, index1
, index2
, None, None)
271 self
.marks_before
= self
.save_marks(text
)
272 self
.index1
= text
.index(self
.index1
)
274 self
.index2
= text
.index(self
.index2
)
276 self
.index2
= text
.index(self
.index1
+ " +1c")
277 if text
.compare(self
.index2
, ">", "end-1c"):
278 # Don't delete the final newline
279 self
.index2
= text
.index("end-1c")
280 self
.chars
= text
.get(self
.index1
, self
.index2
)
281 text
.delete(self
.index1
, self
.index2
)
282 self
.marks_after
= self
.save_marks(text
)
283 ##sys.__stderr__.write("do: %s\n" % self)
285 def redo(self
, text
):
286 text
.mark_set('insert', self
.index1
)
287 text
.delete(self
.index1
, self
.index2
)
288 self
.set_marks(text
, self
.marks_after
)
290 ##sys.__stderr__.write("redo: %s\n" % self)
292 def undo(self
, text
):
293 text
.mark_set('insert', self
.index1
)
294 text
.insert(self
.index1
, self
.chars
)
295 self
.set_marks(text
, self
.marks_before
)
297 ##sys.__stderr__.write("undo: %s\n" % self)
299 class CommandSequence(Command
):
301 # Wrapper for a sequence of undoable cmds to be undone/redone
309 s
= self
.__class
__.__name
__
311 for cmd
in self
.cmds
:
312 strs
.append(" %r" % (cmd
,))
313 return s
+ "(\n" + ",\n".join(strs
) + "\n)"
316 return len(self
.cmds
)
318 def append(self
, cmd
):
319 self
.cmds
.append(cmd
)
324 def redo(self
, text
):
325 for cmd
in self
.cmds
:
328 def undo(self
, text
):
334 def bump_depth(self
, incr
=1):
335 self
.depth
= self
.depth
+ incr
339 from Percolator
import Percolator
341 root
.wm_protocol("WM_DELETE_WINDOW", root
.quit
)
350 if __name__
== "__main__":