Version 6 of Text widget undo/redo limitations and enhancements

Updated 2004-09-08 16:30:11

The Tk text widget, in 8.4 or newer, has a nice '.text configure -undo 1' support for an unlimited undo/redo stack for textual insertions/deletions. This is particularly useful for text editors that make use of the Tk text widget.

The mechanism is at the same time robust to being renamed and fragile to it:

Renaming with overloading:

    text .t -undo 1
    .t insert end "hello there"
    rename .t my.t
    proc .t {args} { puts "captured: $args" ; uplevel 1 my.t $args }
    .t edit undo

The above will work and the overloading proc .t will intercept the undo operation. This allows .t to perform actions like colouring any new text that has appeared (either because a delete operation has been undone, or because the widget has scrolled as a result of the action).

But, if you rename and do not overload:

    text .t -undo 1
    .t insert end "hello there"
    rename .t my.t
    #proc .t {args} { puts "captured: $args" ; uplevel 1 my.t $args }
    my.t edit undo

Then this will throw an error invalid command name ".t". The undo mechanism internally doesn't realise the widget has a new name.


why is this surprising? If you rename a command, of course it won't be available, and of course tcl will throw an error. I don't see why this is a limitation

Oops - sorry - fixed the above example so it now correctly shows something surprising.


While it would be easy to make the undo mechanism always use the current widgetCmd data structure (and hence fix this second example), it would break the first example. And the first example is important -- an undo/redo action can trigger scrolling, changing of the 'insert' position, text appearing or disappearing, etc. The programmer needs to have the capability to notice exactly what has happened and take appropriate action!

Finally, this is further complicated with TIP#169, in which peer text widgets were introduced, which may show all or part of the underlying textual data. With these peer widgets, it could be that an undo operation may or may not apply to what is currently shown (at least in the current peer):

    text .t -undo 0
    for {set i 0} {$i < 20} {incr i} {
        .t insert end "hello there $i"
    }
    text .t -undo 1
    .t insert end "add one more line"
    # Only show the middle 10 lines
    .t configure -startline 5 -endline 15
    # This should undo the action that isn't visible?
    .t undo edit

So, we need a more flexible solution to these problems.

One such solution would be to provide the text widget with a callback or event mechanism. For example, we could envisage new virtual events: <<Inserted>>, <<Deleted>> (and <<Replaced>> ?) whose user_data field contains further relevant information:

    [list $start_idx $end_idx $characters]

so that any client can easily capture whatever information it needs about the action that just took place. This would certainly work well if we ignore the complications of peer text widgets. Perhaps <<Edited>> would be a better virtual event. Perhaps a minor modification to it might also resolve the situation there as well?


EB: I have found a workaround for having the undo mecanism calling the overloaded command instead of the widget command. In fact, to perform undo (or redo), [$path edit undo] recalls all necessary insert/delete on $path in the global namespace, so renaming the widget with the same name inside a namespace and invoking [$path edit undo] inside that namespace will invoke the wrapper when calling the insert/delete:

  namespace eval editor {}

  proc editor::editor {path args} {
      set path [eval [list text $path] $args]
      rename $path ::editor::$path
      interp alias {} $path {} ::editor::TextWrapper $path
      return $path
  }

  proc editor::TextWrapper {path cmd args} {
      switch $cmd {
          ...
          default {
              # [$path edit undo] will recall $path the global namespace, our wrapper.
              return [eval [list $path $cmd] $args]
          }
  }

Vince -- interesting!


WHD: I think you've got a more general problem here. If I'm not mistaken, all of the text widget's event bindings are written using "%W" to get the window name--which in your example remains ".t". If you don't provide a replacement ".t" command that passes its subcommands through to "my.t", I think you're going to find that a whole lot of stuff doesn't work properly. In any event, why would you want to rename a Tk widget without replacing it?