1import string
2
3from idlelib.delegator import Delegator
4
5# tkinter import not needed because module does not create widgets,
6# although many methods operate on text widget arguments.
7
8#$ event <<redo>>
9#$ win <Control-y>
10#$ unix <Alt-z>
11
12#$ event <<undo>>
13#$ win <Control-z>
14#$ unix <Control-z>
15
16#$ event <<dump-undo-state>>
17#$ win <Control-backslash>
18#$ unix <Control-backslash>
19
20
21class UndoDelegator(Delegator):
22
23    max_undo = 1000
24
25    def __init__(self):
26        Delegator.__init__(self)
27        self.reset_undo()
28
29    def setdelegate(self, delegate):
30        if self.delegate is not None:
31            self.unbind("<<undo>>")
32            self.unbind("<<redo>>")
33            self.unbind("<<dump-undo-state>>")
34        Delegator.setdelegate(self, delegate)
35        if delegate is not None:
36            self.bind("<<undo>>", self.undo_event)
37            self.bind("<<redo>>", self.redo_event)
38            self.bind("<<dump-undo-state>>", self.dump_event)
39
40    def dump_event(self, event):
41        from pprint import pprint
42        pprint(self.undolist[:self.pointer])
43        print("pointer:", self.pointer, end=' ')
44        print("saved:", self.saved, end=' ')
45        print("can_merge:", self.can_merge, end=' ')
46        print("get_saved():", self.get_saved())
47        pprint(self.undolist[self.pointer:])
48        return "break"
49
50    def reset_undo(self):
51        self.was_saved = -1
52        self.pointer = 0
53        self.undolist = []
54        self.undoblock = 0  # or a CommandSequence instance
55        self.set_saved(1)
56
57    def set_saved(self, flag):
58        if flag:
59            self.saved = self.pointer
60        else:
61            self.saved = -1
62        self.can_merge = False
63        self.check_saved()
64
65    def get_saved(self):
66        return self.saved == self.pointer
67
68    saved_change_hook = None
69
70    def set_saved_change_hook(self, hook):
71        self.saved_change_hook = hook
72
73    was_saved = -1
74
75    def check_saved(self):
76        is_saved = self.get_saved()
77        if is_saved != self.was_saved:
78            self.was_saved = is_saved
79            if self.saved_change_hook:
80                self.saved_change_hook()
81
82    def insert(self, index, chars, tags=None):
83        self.addcmd(InsertCommand(index, chars, tags))
84
85    def delete(self, index1, index2=None):
86        self.addcmd(DeleteCommand(index1, index2))
87
88    # Clients should call undo_block_start() and undo_block_stop()
89    # around a sequence of editing cmds to be treated as a unit by
90    # undo & redo.  Nested matching calls are OK, and the inner calls
91    # then act like nops.  OK too if no editing cmds, or only one
92    # editing cmd, is issued in between:  if no cmds, the whole
93    # sequence has no effect; and if only one cmd, that cmd is entered
94    # directly into the undo list, as if undo_block_xxx hadn't been
95    # called.  The intent of all that is to make this scheme easy
96    # to use:  all the client has to worry about is making sure each
97    # _start() call is matched by a _stop() call.
98
99    def undo_block_start(self):
100        if self.undoblock == 0:
101            self.undoblock = CommandSequence()
102        self.undoblock.bump_depth()
103
104    def undo_block_stop(self):
105        if self.undoblock.bump_depth(-1) == 0:
106            cmd = self.undoblock
107            self.undoblock = 0
108            if len(cmd) > 0:
109                if len(cmd) == 1:
110                    # no need to wrap a single cmd
111                    cmd = cmd.getcmd(0)
112                # this blk of cmds, or single cmd, has already
113                # been done, so don't execute it again
114                self.addcmd(cmd, 0)
115
116    def addcmd(self, cmd, execute=True):
117        if execute:
118            cmd.do(self.delegate)
119        if self.undoblock != 0:
120            self.undoblock.append(cmd)
121            return
122        if self.can_merge and self.pointer > 0:
123            lastcmd = self.undolist[self.pointer-1]
124            if lastcmd.merge(cmd):
125                return
126        self.undolist[self.pointer:] = [cmd]
127        if self.saved > self.pointer:
128            self.saved = -1
129        self.pointer = self.pointer + 1
130        if len(self.undolist) > self.max_undo:
131            ##print "truncating undo list"
132            del self.undolist[0]
133            self.pointer = self.pointer - 1
134            if self.saved >= 0:
135                self.saved = self.saved - 1
136        self.can_merge = True
137        self.check_saved()
138
139    def undo_event(self, event):
140        if self.pointer == 0:
141            self.bell()
142            return "break"
143        cmd = self.undolist[self.pointer - 1]
144        cmd.undo(self.delegate)
145        self.pointer = self.pointer - 1
146        self.can_merge = False
147        self.check_saved()
148        return "break"
149
150    def redo_event(self, event):
151        if self.pointer >= len(self.undolist):
152            self.bell()
153            return "break"
154        cmd = self.undolist[self.pointer]
155        cmd.redo(self.delegate)
156        self.pointer = self.pointer + 1
157        self.can_merge = False
158        self.check_saved()
159        return "break"
160
161
162class Command:
163    # Base class for Undoable commands
164
165    tags = None
166
167    def __init__(self, index1, index2, chars, tags=None):
168        self.marks_before = {}
169        self.marks_after = {}
170        self.index1 = index1
171        self.index2 = index2
172        self.chars = chars
173        if tags:
174            self.tags = tags
175
176    def __repr__(self):
177        s = self.__class__.__name__
178        t = (self.index1, self.index2, self.chars, self.tags)
179        if self.tags is None:
180            t = t[:-1]
181        return s + repr(t)
182
183    def do(self, text):
184        pass
185
186    def redo(self, text):
187        pass
188
189    def undo(self, text):
190        pass
191
192    def merge(self, cmd):
193        return 0
194
195    def save_marks(self, text):
196        marks = {}
197        for name in text.mark_names():
198            if name != "insert" and name != "current":
199                marks[name] = text.index(name)
200        return marks
201
202    def set_marks(self, text, marks):
203        for name, index in marks.items():
204            text.mark_set(name, index)
205
206
207class InsertCommand(Command):
208    # Undoable insert command
209
210    def __init__(self, index1, chars, tags=None):
211        Command.__init__(self, index1, None, chars, tags)
212
213    def do(self, text):
214        self.marks_before = self.save_marks(text)
215        self.index1 = text.index(self.index1)
216        if text.compare(self.index1, ">", "end-1c"):
217            # Insert before the final newline
218            self.index1 = text.index("end-1c")
219        text.insert(self.index1, self.chars, self.tags)
220        self.index2 = text.index("%s+%dc" % (self.index1, len(self.chars)))
221        self.marks_after = self.save_marks(text)
222        ##sys.__stderr__.write("do: %s\n" % self)
223
224    def redo(self, text):
225        text.mark_set('insert', self.index1)
226        text.insert(self.index1, self.chars, self.tags)
227        self.set_marks(text, self.marks_after)
228        text.see('insert')
229        ##sys.__stderr__.write("redo: %s\n" % self)
230
231    def undo(self, text):
232        text.mark_set('insert', self.index1)
233        text.delete(self.index1, self.index2)
234        self.set_marks(text, self.marks_before)
235        text.see('insert')
236        ##sys.__stderr__.write("undo: %s\n" % self)
237
238    def merge(self, cmd):
239        if self.__class__ is not cmd.__class__:
240            return False
241        if self.index2 != cmd.index1:
242            return False
243        if self.tags != cmd.tags:
244            return False
245        if len(cmd.chars) != 1:
246            return False
247        if self.chars and \
248           self.classify(self.chars[-1]) != self.classify(cmd.chars):
249            return False
250        self.index2 = cmd.index2
251        self.chars = self.chars + cmd.chars
252        return True
253
254    alphanumeric = string.ascii_letters + string.digits + "_"
255
256    def classify(self, c):
257        if c in self.alphanumeric:
258            return "alphanumeric"
259        if c == "\n":
260            return "newline"
261        return "punctuation"
262
263
264class DeleteCommand(Command):
265    # Undoable delete command
266
267    def __init__(self, index1, index2=None):
268        Command.__init__(self, index1, index2, None, None)
269
270    def do(self, text):
271        self.marks_before = self.save_marks(text)
272        self.index1 = text.index(self.index1)
273        if self.index2:
274            self.index2 = text.index(self.index2)
275        else:
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)
284
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)
289        text.see('insert')
290        ##sys.__stderr__.write("redo: %s\n" % self)
291
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)
296        text.see('insert')
297        ##sys.__stderr__.write("undo: %s\n" % self)
298
299
300class CommandSequence(Command):
301    # Wrapper for a sequence of undoable cmds to be undone/redone
302    # as a unit
303
304    def __init__(self):
305        self.cmds = []
306        self.depth = 0
307
308    def __repr__(self):
309        s = self.__class__.__name__
310        strs = []
311        for cmd in self.cmds:
312            strs.append("    %r" % (cmd,))
313        return s + "(\n" + ",\n".join(strs) + "\n)"
314
315    def __len__(self):
316        return len(self.cmds)
317
318    def append(self, cmd):
319        self.cmds.append(cmd)
320
321    def getcmd(self, i):
322        return self.cmds[i]
323
324    def redo(self, text):
325        for cmd in self.cmds:
326            cmd.redo(text)
327
328    def undo(self, text):
329        cmds = self.cmds[:]
330        cmds.reverse()
331        for cmd in cmds:
332            cmd.undo(text)
333
334    def bump_depth(self, incr=1):
335        self.depth = self.depth + incr
336        return self.depth
337
338
339def _undo_delegator(parent):  # htest #
340    from tkinter import Toplevel, Text, Button
341    from idlelib.percolator import Percolator
342    undowin = Toplevel(parent)
343    undowin.title("Test UndoDelegator")
344    x, y = map(int, parent.geometry().split('+')[1:])
345    undowin.geometry("+%d+%d" % (x, y + 175))
346
347    text = Text(undowin, height=10)
348    text.pack()
349    text.focus_set()
350    p = Percolator(text)
351    d = UndoDelegator()
352    p.insertfilter(d)
353
354    undo = Button(undowin, text="Undo", command=lambda:d.undo_event(None))
355    undo.pack(side='left')
356    redo = Button(undowin, text="Redo", command=lambda:d.redo_event(None))
357    redo.pack(side='left')
358    dump = Button(undowin, text="Dump", command=lambda:d.dump_event(None))
359    dump.pack(side='left')
360
361if __name__ == "__main__":
362    from unittest import main
363    main('idlelib.idle_test.test_undo', verbosity=2, exit=False)
364
365    from idlelib.idle_test.htest import run
366    run(_undo_delegator)
367