TIP 26: Enhancements for the Tk Text Widget

Login
Bounty program for improvements to Tcl and certain Tcl packages.
Author:         Ludwig Callewaert <ludwig_callewaert@frontierd.com>
Author:         Ludwig Callewaert <ludwig.callewaert@belgacom.net>
State:          Final
Type:           Project
Vote:           Done
Created:        20-Feb-2001
Post-History:   
Discussions-To: news:comp.lang.tcl
Obsoletes:      19
Tcl-Version:    8.4

Abstract

This TIP proposes several enhancements for the Tk text widget. An unlimited undo/redo mechanism is proposed, with several user available customisation features. Related to this, a text modified indication is proposed. This means that the user can set, query or receive a virtual event when the content of the text widget is modified. And finally a virtual event is added that is generated whenever the selection changes in the text widget.

Rationale

The text widget provides a lot of features that make it ideally suited to create a text editor from it. The vast number of editors that are based on this widget are a proof of this. Yet some basic features are missing from the text widget and need to be re-invented over and over again by the authors of the various editors. This TIP adds a number of the missing features.

A first missing feature is an undo/redo mechanism. The mechanism proposed here is simple yet powerful enough to accommodate a very reasonable undo/redo strategy. It also provides sufficient user control, so that the actual strategy can be refined and tailored to the users need.

A second missing feature is a notification if the text in the widget has been modified with respect to a reference point. [19] deals partly with this. This implementation takes it some steps further. First of all, there is a link with the undo/redo mechanism, since undoing or redoing changes can take you to or away from the reference point, and as such changes the modified state of the widget. Secondly, with this implementation, a virtual event is generated whenever the modified state of the widget changes, allowing the user to bind to that event and for instance give a visual indication of the modified state of the widget.

Finally, a virtual event has been added that is triggered whenever the selection in the widget changes. At first it may seem not so useful, but there are a number of situations where this functionality is needed. A couple of examples where I ran into the need for this may clarify this. On Windows, if the text widget does not have the focus, the selection tag is not visible. This is consistent with other Windows applications. However, when implementing a search mechanism, the found string needs to be tagged with the selection tag. (You want it to be selected). The search (and replace) dialog box has the focus however, so this selection tag is invisible. To make it visible, another tag was used to duplicate the selection tag. This is very easy when the functionality described here is available. Otherwise it is very difficult to do this consistently. Another occasion was when I was implementing a rectangular cut and paste for the text widget. This was based on adding spaces on the fly, while selecting the rectangle. If for some reason the selection changes (for instance on Unix another application gets the selection) these spaces need to be removed again. Doing this is virtually impossible without this functionality. With it, it becomes trivial. The functionality itself adds little or no overhead to the text widget.

Specification

The undo/redo mechanism operates by adding two stacks of edit actions to the text widget. Every insert or delete operation is added to the undo stack in normal operation. At certain times a separator is added onto the stack. All insert and delete actions in between two separators are considered to be one edit action, and will be undone or redone as one.
The insertion of the separators is under user control. There is a default operation however. This will insert separators whenever the mode changes from insertion to deletion, or vise versa. Separators are also inserted when the keyboard or the mouse are used to move the insert mark. Finally, pressing the key also inserts a separator. By turning the autoseparators off and inserting them at the desired points, compound actions, such as search and replace, can be created.
The default paste function is an example of such an action.

Undoing an action, will re-apply in reverse order all inserts and deletes in between two separators. These inserts and deletes will now move to the redo stack. Redoing a change re-applies the inserts and deletes, and moves them again to the undo stack. Normal insertions or deletions will clear the redo stack.

It is also possible to clear the undo stack, giving the user some control over the depth of the stack.

Currently only text inserts and deletes can be undone. All other changes to the widget, such as the adding or deleting of tags, cannot be undone.

The modified state of the widget is implemented using a counter. Every insert or delete action, and every time such an action is redone, increments this counter. Every undone insert or delete decrements this counter. The widget is considered to be modified if the counter is not zero. A virtual event <> is generated whenever this counter changes from zero to non-zero or vice versa. A mechanism is provided to reset the counter to zero. The modified state can also be explicitely set by the user. In that case, the counter mechanism is not operational until the modified state has been reset again.

  1. pathName configure -undo 0|1 - this enables or disables the undo/redo mechanism. The default is zero.

  2. pathName configure -autoseparators 0|1 - when one inserts a separator automatically whenever insert changes to delete or vice versa. Separators are also inserted when the keyboard or the mouse is used to move the insert mark, or when the key is pressed. When off, no separators are inserted, except by the user (See 6). The default is one.

  3. pathName edit undo - undoes the last edit action if undo is enabled (See 1). The insert mark will be positioned at the last undone edit action. When undo deletes text, that is the index where the text was. When undo inserts text, the insert mark will be positioned at the end of the inserted text. The view will be adapted to make the insert mark visible. Raises an exception if there is nothing to undo. Does nothing if undo is disabled.

  4. pathName edit redo - redoes the last edit action if undo is enabled (See 1). The insert mark and widget view will be updated similar to what is done for the edit undo command. Raises an exception if there is nothing to redo. Does nothing if undo is disabled.

  5. pathName edit reset - resets the undo and redo stacks (clears them).

  6. pathName edit separator - inserts a separator on the undo stack, indicating an undo boundary. If a separator is already present, this will do nothing. This means that it is safe to issue the command several times, without any inserts or deletes occurring in between.

  7. pathName edit modified ?boolean? - If boolean is not specified returns the modified state of the widget (either 1 or zero). If boolean is specified, sets the modified state of the widget to that value.

  8. <> - this virtual event is generated whenever the modified state of the widget changes from modified to not modified or vice versa.

  9. <> - this virtual event is generated whenever the range tagged with the selection tag changes.

    1. <> - this virtual event calls pathName edit undo.
    2. <> - this virtual event calls pathName edit redo.
    3. - is bound to the <> virtual event.
    4. - is bound to the <> virtual event on all platforms except Win32.
    5. - is bound to the <> virtual event on Win32.

Example

The following code illustrates how the new features are intended to be used.

   global fileName
   global modState
   global undoVar

   set fileName "None"
   set modState ""
   set undoVar  0


   text .t -background white -wrap none
   # Example 1: The Modified event will update a text label
   bind .t <<Modified>>  updateState
   # Example 2: The Selection event will create a tag that
   #            duplicates the selection
   bind .t <<Selection>> duplicateSelection

   frame .l
   label .l.l -text "File: "
   label .l.f -textvariable fileName
   label .l.m -textvariable modState

   grid .l.l -sticky w   -column 0 -row 0
   grid .l.f -sticky w   -column 1 -row 0
   grid .l.m -sticky e   -column 2 -row 0

   grid columnconfigure .l 1 -weight 1

   frame .b
   button .b.l -text "Load"   -width 8 -command loadFile
   button .b.s -text "Save"   -width 8 -command saveFile
   button .b.i -text "Indent" -width 8 -command blockIndent

   checkbutton .b.e -text "Enable Undo" -onvalue 1 -offvalue 0 -|   |   variable undoVar
   trace variable undoVar w setUndo
   button .b.u -text "Undo"     -width 8 -command "undo"
   button .b.r -text "Redo"     -width 8 -command "redo"
   button .b.m -text "Modified" -width 8 -command ".t edit modified on"

   grid .b.l -row 0 -column 0
   grid .b.s -row 0 -column 1
   grid .b.i -row 0 -column 2
   grid .b.e -row 0 -column 3
   grid .b.u -row 0 -column 4
   grid .b.r -row 0 -column 5
   grid .b.m -row 0 -column 6

   grid columnconfigure .b 0 -weight 1
   grid columnconfigure .b 1 -weight 1
   grid columnconfigure .b 2 -weight 1
   grid columnconfigure .b 3 -weight 1
   grid columnconfigure .b 4 -weight 1
   grid columnconfigure .b 5 -weight 1


   grid .l -sticky ew   -column 0 -row 0
   grid .t -sticky news -column 0 -row 1
   grid .b -sticky ew   -column 0 -row 2

   grid rowconfigure    . 1 -weight 1
   grid columnconfigure . 0 -weight 1



   proc updateState {args} {
      global modState

      # Check the modified state and update the label
      if { [.t edit modified] } {
         set modState "Modified"
      } else {
         set modState ""
      }
   }


   proc setUndo {args} {
      global undoVar

      # Turn undo on or off
      if { $undoVar } {
         .t configure -undo 1
      } else {
         .t configure -undo 0
      }
   }

   proc undo {} {
      # edit undo throws an exception when there is nothing to
      # undo. So catch it.
      if { [catch {.t edit undo}] } {
         bell
      }
   }

   proc redo {} {
      # edit redo throws an exception when there is nothing to
      # undo. So catch it.
      if { [catch {.t edit redo}] } {
         bell
      }
   }

   proc loadFile {} {

      set file [tk_getOpenFile]
      if { ![string equal $file ""] } {
         set fileName $file
         set f [open $file r]
         set content [read $f]
         set oldUndo [.t cget -undo]

         # Turn off undo. We do not want to be able to undo
         # the loading of a file
         .t configure -undo 0
         .t delete 1.0 end
         .t insert end $content
         # Reset the modified state
         .t edit modified 0
         # Clear the undo stack
         .t edit reset
         # Set undo to the old state
         .t configure -undo $oldUndo
      }
   }

   proc saveFile {} {
      # The saving bit is not actually done
      # So the contents in the file are not updated

      # Saving clears the modified state
      .t edit modified 0
      # Make sure there is a separator on the undo stack
      # So we can get back to this point with the undo
      .t edit separator
   }

   proc blockIndent {} {
      set indent "   "

      # Block indent should be treated as one operation from
      # the undo point of view

      # if there is a selection
      if { ![catch {.t index sel.first} ] } {
         scan [.t index sel.first] "%d.%d" startline startchar
         scan [.t index sel.last]  "%d.%d" stopline  stopchar
         if { $stopchar == 0 } {
            incr stopline -1
         }

         # Get the original autoseparators state
         set oldSep [.t cget -autoseparators]
         # Turn of automatic insertion of separators
         .t configure -autoseparators 0
         # insert a separator before the edit operation
         .t edit separator
         for {set i $startline} { $i <= $stopline} {incr i} {
            .t insert "$i.0" $indent
         }
         .t tag add sel $startline.0 "$stopline.end + 1 char"
         # insert a separator after the edit operation
         .t edit separator
         # put the autoseparators back in their original state
         .t configure -autoseparators $oldSep
      }
   }

   proc duplicateSelection {args} {
      .t tag configure dupsel -background tomato
      .t tag remove dupsel 1.0 end

      if { ![catch {.t index sel.first} ] } {
         eval .t tag add dupsel [.t tag ranges sel]
      }
   }

Reference Implementation

http://www.cs.man.ac.uk/fellowsd-bin/TIP/26.patch

The patch has received little testing so far, so any testing is encouraged.

Copyright

This document has been placed in the public domain.

History