Text widget undo/redo limitations and enhancements

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?

Vince: I don't know why you'd want to rename a Tk widget without replacing it, and the issue you point out wrt "%W" bindings is a good one, and just suggests that the undo mechanism has exactly the same 'problem' as the bindings, and therefore we should not look for an undo-specific solution. In fact it suggests we shouldn't look for any solution. The answer should be "if you rename the widget, you are responsible for all commands issued through the old window name".

This would resolve all issues except those which arise through peer text widgets, where we can do this:

    text .t -undo 1
    .t peer create .tt -undo 1
    # This inserts text into the data underlying both .t and .tt
    .tt insert end "hello there\n"
    destroy .tt
    # This must undo the insert
    .t edit undo

This means the undo-stack representation of insert and delete (and replace) actions must be independent of individual peer widgets, yet each individual peer probably wants to know how it should update its display accordingly (does it need to scroll, colour some new text, etc).

Does this make sense?

WHD: Makes sense to me.


Bryan Oakley 02-Oct-2006: another limiation is that there's no way to use introspection to determine if there's anything on the undo or redo stacks. A well-polished app should disable the "Undo" item on an edit menubar if there's nothing to be undone (and likewise for redo). A workaround is to try and do an undo, followed by a redo. If the undo fails there's nothing to undo. Unfortunately, this seems to mess with the undo boundaries. That is, {.text undo; .text redo} doesn't leave the undo/redo stacks in the same exact condition.

What the text widget needs is a way to get the undo and redo stacks as lists and be able to manipulate them. Then, for instance, I could place boundaries after every word, then collapse whole sentences into a single undoable action. MB: You are right. It was the goal of the "edit modified" command to give some insight, but it is not complete : - the text widget can be unmodified whereas there is a redo or a redo (or nothing) command in the stack, - the text widget can be modified whereas there is an undo or a redo command in the stack. What I don't understand is why "{.text undo; .text redo} doesn't leave the undo/redo stacks in the same exact condition". I think it should, isn't it ?

I think it would be a good thing to include in the emergent new undo/redo mechanism the ability to undo/redo tag configuration/insertion/deletion. These events are actually not handled , and may be of crucial importance.


Category Widget Category GUI