(root)/
Python-3.12.0/
Lib/
idlelib/
undo.py
       1  import string
       2  
       3  from 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  
      21  class ESC[4;38;5;81mUndoDelegator(ESC[4;38;5;149mDelegator):
      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  
     162  class ESC[4;38;5;81mCommand:
     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  
     207  class ESC[4;38;5;81mInsertCommand(ESC[4;38;5;149mCommand):
     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  
     264  class ESC[4;38;5;81mDeleteCommand(ESC[4;38;5;149mCommand):
     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  
     300  class ESC[4;38;5;81mCommandSequence(ESC[4;38;5;149mCommand):
     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(f"    {cmd!r}")
     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  
     339  def _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  
     361  if __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)