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
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 <
pathName configure -undo 0|1 - this enables or disables the undo/redo mechanism. The default is zero.
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. 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.
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.
pathName edit reset - resets the undo and redo stacks (clears them).
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.
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.
<
> - this virtual event is generated whenever the modified state of the widget changes from modified to not modified or vice versa.<
> - this virtual event is generated whenever the range tagged with the selection tag changes.- <
> - this virtual event calls pathName edit undo. - <
> - this virtual event calls pathName edit redo. - is bound to the < > virtual event. - is bound to the < > virtual event on all platforms except Win32. - 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.