TIP:            26
Title:          Enhancements for the Tk Text Widget
Version:        $Revision: 1.9 $
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 <Return> 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 ''<<Modified>>'' 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 <Return>
       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.  ''<<Modified>>'' - this virtual event is generated whenever the
       modified state of the widget changes from modified to not
       modified or vice versa.

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

  10. ''<<Undo>>'' - this virtual event calls pathName edit undo.

  11. ''<<Redo>>'' - this virtual event calls pathName edit redo.

  12.  ''<Control-z>'' - is bound to the <<Undo>> virtual event.

  13.  ''<Control-Z>'' - is bound to the <<Redo>> virtual event on all
       platforms except Win32.

  14.  ''<Control-y>'' - is bound to the <<Redo>> 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.
