This is not necessarily the current version of this TIP.
| TIP: | 26 |
| Title: | Enhancements for the Tk Text Widget |
| Version: | $Revision: 1.5 $ |
| Authors: |
Ludwig Callewaert <ludwig_callewaert at frontierd dot com> Ludwig Callewaert <ludwig dot callewaert at belgacom dot net> |
| State: | Draft |
| Type: | Project |
| Tcl-Version: | 8.4 |
| Vote: | Pending |
| Created: | Tuesday, 20 February 2001 |
| Discussions To: | news:comp.lang.tcl |
| Obsoletes: | TIP #19 |
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.
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. TIP #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 is 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.
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 semaphore (a marker if you will) is added onto the stack. All insert and delete actions in between two semaphores are considered to be one edit action, and will be undone or redone as one. The insertion of the semaphores is under user control. There is a default operation however. This will insert semaphores whenever the mode changes from insertion to deletion, or vise versa. Semaphores are also inserted when the cursor moves. By turning the autosemaphores off and inserting them at the desired points, compound actions can be creates, such as search and replace. 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 semaphores. 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 control over the depth of the stack.
The modified state of the widget is implemented using a counter. Every insert or delete action increments this counter also when redone. 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 mechansim is not operational until the modified state has been reset again.
pathName configure -undo 0|1 - this enables or disables the undo/redo mechanism. The default is zero.
pathName configure -autosemaphores 0|1 - when one inserts semaphore automatically whenever insert changes to delete or vice versa. When off, no semaphores are inserted, except by the user (See 6). The default is one.
pathName edit undo - undoes the last edit action if undo is enabled (See 1). Raises an exception if there is nothing to undo. Does nothing otherwise.
pathName edit redo - redoes the last edit action if undo is enabled (See 1). Raises an exception if there is nothing to redo. Does nothing otherwise.
pathName edit reset - resets the undo and redo stacks (clears them).
pathName edit semaphore - inserts a semaphore (marker) on the undo stack, indicating an undo boundary. If a semaphore 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.
pathName edit modified ?booelan? - 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.
<<Modified>> - this virtual event is generated whenever the modified state of the widget changes from modified to not modified or vice versa.
<<Selection>> - this virtual event is generated whenever the range tagged with the selection tag changes.
<<Undo>> - this virtual event calls pathName edit undo. Issues a bell signal if there is nothing to undo.
<<Redo>> - this virtual event calls pathName edit redo. Issues a bell signal if there is nothing to redo.
<Control-z> - is bound to the <<Undo>> virtual event.
<Control-Z> - is bound to the <<Redo>> virtual event.
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
# Pressing the return key should also mark a boundary
# in the undo stack
bind .t <Return> ".t edit semaphore"
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 semaphore on the undo stack
# So we can get back to this point with the undo
.t edit semaphore
}
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 autosemaphores state
set oldSema [.t cget -autosemaphores]
# Turn of automatic insertion of semaphores
.t configure -autosemaphores 0
# insert a semaphore before the edit operation
.t edit semaphore
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 semaphore after the edit operation
.t edit semaphore
# put the autosemaphores back in their original state
.t configure -autosemaphores $oldSema
}
}
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]
}
}
http://www.cs.man.ac.uk/fellowsd-bin/TIP/26.patch
The patch has received little testing so far, so any testing is encouraged.
This document has been placed in the public domain.
This is not necessarily the current version of this TIP.