| 1 | #///////////////////////////////////////////////////////////////////////////// |
|---|
| 2 | # FILE : supertext.tcl |
|---|
| 3 | # PURPOSE : This file |
|---|
| 4 | #///////////////////////////////////////////////////////////////////////////// |
|---|
| 5 | |
|---|
| 6 | # Supertext.tcl v1.0b1 |
|---|
| 7 | # |
|---|
| 8 | # Copyright (c) 1998 Bryan Oakley |
|---|
| 9 | # All Rights Reserved |
|---|
| 10 | # |
|---|
| 11 | # this code is freely distributable, but is provided as-is with |
|---|
| 12 | # no waranty expressed or implied. |
|---|
| 13 | |
|---|
| 14 | # send comments to boakley@austin.rr.com |
|---|
| 15 | |
|---|
| 16 | # What is this? |
|---|
| 17 | # |
|---|
| 18 | # This is a replacement for (or superset of , or subclass of, ...) |
|---|
| 19 | # the tk text widget. Its big feature is that it supports unlimited |
|---|
| 20 | # undo. It also has two poorly documented options: -preproc and |
|---|
| 21 | # -postproc. |
|---|
| 22 | |
|---|
| 23 | # The entry point to this widget is Supertext::text; it takes all of |
|---|
| 24 | # the same arguments as the standard text widget and exhibits all of |
|---|
| 25 | # the same behaviors. The proc Supertext::overrideTextCommand may be |
|---|
| 26 | # called to have the supertext widget be used whenever the command |
|---|
| 27 | # "text" is used (ie: it imports Supertext::text as the command "text"). |
|---|
| 28 | # Use at your own risk... |
|---|
| 29 | |
|---|
| 30 | # To access the undo feature, use ".widget undo". It will undo the |
|---|
| 31 | # most recent insertion or deletion. On windows and the mac |
|---|
| 32 | # this command is bound to <Control-z>; on unix it is bound to |
|---|
| 33 | # <Control-_> |
|---|
| 34 | |
|---|
| 35 | # if you are lucky, you might find documentation here: |
|---|
| 36 | # http://www1.clearlight.com/~oakley/tcl/supertext.html |
|---|
| 37 | |
|---|
| 38 | package provide Supertext 1.0 |
|---|
| 39 | |
|---|
| 40 | namespace eval Supertext { |
|---|
| 41 | variable undo |
|---|
| 42 | variable undoIndex |
|---|
| 43 | variable text "::text" |
|---|
| 44 | variable preProc |
|---|
| 45 | variable postProc |
|---|
| 46 | |
|---|
| 47 | namespace export text |
|---|
| 48 | } |
|---|
| 49 | |
|---|
| 50 | # this proc is probably attempting to be more clever than it should... |
|---|
| 51 | # When called, it will (*gasp*) rename the tk command "text" to "_text_", |
|---|
| 52 | # then import our text command into the global scope. |
|---|
| 53 | # |
|---|
| 54 | # Use at your own risk! |
|---|
| 55 | |
|---|
| 56 | proc Supertext::overrideTextCommand {} { |
|---|
| 57 | variable text |
|---|
| 58 | |
|---|
| 59 | set text "::_text_" |
|---|
| 60 | rename ::text $text |
|---|
| 61 | uplevel #0 namespace import Supertext::text |
|---|
| 62 | } |
|---|
| 63 | |
|---|
| 64 | proc Supertext::text {w args} { |
|---|
| 65 | variable text |
|---|
| 66 | variable undo |
|---|
| 67 | variable undoIndex |
|---|
| 68 | variable preProc |
|---|
| 69 | variable postProc |
|---|
| 70 | |
|---|
| 71 | # this is what we will rename our widget proc to... |
|---|
| 72 | set original __$w |
|---|
| 73 | |
|---|
| 74 | # do we have any of our custom options? If so, process them and |
|---|
| 75 | # strip them out before sending them to the real text command |
|---|
| 76 | if {[set i [lsearch -exact $args "-preproc"]] >= 0} { |
|---|
| 77 | set j [expr $i + 1] |
|---|
| 78 | set preProc($original) [lindex $args $j] |
|---|
| 79 | set args [lreplace $args $i $j] |
|---|
| 80 | } else { |
|---|
| 81 | set preProc($original) {} |
|---|
| 82 | } |
|---|
| 83 | |
|---|
| 84 | if {[set i [lsearch -exact $args "-postproc"]] >= 0} { |
|---|
| 85 | set j [expr $i + 1] |
|---|
| 86 | set postProc($original) [lindex $args $j] |
|---|
| 87 | set args [lreplace $args $i $j] |
|---|
| 88 | } else { |
|---|
| 89 | set postProc($original) {} |
|---|
| 90 | } |
|---|
| 91 | |
|---|
| 92 | # let the text command create the widget... |
|---|
| 93 | eval $text $w $args |
|---|
| 94 | |
|---|
| 95 | # now, rename the resultant widget proc so we can create our own |
|---|
| 96 | rename ::$w $original |
|---|
| 97 | |
|---|
| 98 | # here's where we create our own widget proc. |
|---|
| 99 | proc ::$w {command args} \ |
|---|
| 100 | "namespace eval Supertext widgetproc $w $original \$command \$args" |
|---|
| 101 | |
|---|
| 102 | # set up platform-specific binding for undo; the only one I'm |
|---|
| 103 | # really sure about is winders; what should the mac be? On unix |
|---|
| 104 | # I just picked what I'm used to in emacs :-) |
|---|
| 105 | switch $::tcl_platform(platform) { |
|---|
| 106 | unix {event add <<Undo>> <Control-z>} |
|---|
| 107 | windows {event add <<Undo>> <Control-z>} |
|---|
| 108 | macintosh {event add <<Undo>> <Control-z>} |
|---|
| 109 | } |
|---|
| 110 | bind $w <<Undo>> "$w undo" |
|---|
| 111 | |
|---|
| 112 | set undo($original) {} |
|---|
| 113 | set undoIndex($original) -1 |
|---|
| 114 | set clones($original) {} |
|---|
| 115 | |
|---|
| 116 | return $w |
|---|
| 117 | } |
|---|
| 118 | |
|---|
| 119 | # this is the command that we associate with a supertext widget. |
|---|
| 120 | proc Supertext::widgetproc {this w command args} { |
|---|
| 121 | variable undo |
|---|
| 122 | variable undoIndex |
|---|
| 123 | variable preProc |
|---|
| 124 | variable postProc |
|---|
| 125 | |
|---|
| 126 | # these will be the arguments to the pre and post procs |
|---|
| 127 | set originalCommand $command |
|---|
| 128 | set originalArgs $args |
|---|
| 129 | |
|---|
| 130 | # is there a pre-proc? If so, run it. If there is a problem, |
|---|
| 131 | # die. This is potentially bad, because once there is a problem |
|---|
| 132 | # in a preproc the user must fix the preproc -- there is no |
|---|
| 133 | # way to unconfigure the preproc. Oh well. The other choice |
|---|
| 134 | # is to ignore errors, but then how will the caller know if |
|---|
| 135 | # the proc fails? |
|---|
| 136 | if {[info exists preProc($w)] && $preProc($w) != ""} { |
|---|
| 137 | if {[catch "$preProc($w) $command $args" error]} { |
|---|
| 138 | return -code error "error during processing of -preproc: $error" |
|---|
| 139 | } |
|---|
| 140 | } |
|---|
| 141 | |
|---|
| 142 | # if the command is "undo", we need to morph it into the appropriate |
|---|
| 143 | # command for undoing the last item on the stack |
|---|
| 144 | if {$command == "undo"} { |
|---|
| 145 | if {$undoIndex($w) == ""} { |
|---|
| 146 | # ie: last command was anything _but_ an undo... |
|---|
| 147 | set undoIndex($w) [expr [llength $undo($w)] -1] |
|---|
| 148 | } |
|---|
| 149 | # if the index is pointing to a valid list element, |
|---|
| 150 | # lets undo it... |
|---|
| 151 | if {$undoIndex($w) < 0} { |
|---|
| 152 | # nothing to undo... |
|---|
| 153 | bell |
|---|
| 154 | } else { |
|---|
| 155 | # data is a list comprised of a command token |
|---|
| 156 | # (i=insert, d=delete) and parameters related |
|---|
| 157 | # to that token |
|---|
| 158 | set data [lindex $undo($w) $undoIndex($w)] |
|---|
| 159 | |
|---|
| 160 | if {[lindex $data 0] == "d"} { |
|---|
| 161 | set command "delete" |
|---|
| 162 | } else { |
|---|
| 163 | set command "insert" |
|---|
| 164 | } |
|---|
| 165 | set args [lrange $data 1 end] |
|---|
| 166 | # adjust the index |
|---|
| 167 | incr undoIndex($w) -1 |
|---|
| 168 | } |
|---|
| 169 | } |
|---|
| 170 | |
|---|
| 171 | # now, process the command (either the original one, or the morphed |
|---|
| 172 | # undo command |
|---|
| 173 | switch $command { |
|---|
| 174 | configure { |
|---|
| 175 | # we have to deal with configure specially, since the |
|---|
| 176 | # user could try to configure the -preproc or -postproc |
|---|
| 177 | # options... |
|---|
| 178 | if {[llength $args] == 0} { |
|---|
| 179 | # first, the case where they just type "configure"; lets |
|---|
| 180 | # get it out of the way |
|---|
| 181 | set list [$w configure] |
|---|
| 182 | lappend list [list -preproc preproc Preproc {} $preProc($w)] |
|---|
| 183 | lappend list [list -postproc postproc Postproc {} $postProc($w)] |
|---|
| 184 | set result $list |
|---|
| 185 | |
|---|
| 186 | } elseif {[llength $args] == 1} { |
|---|
| 187 | # this means they are wanting specific configuration |
|---|
| 188 | # information |
|---|
| 189 | set option [lindex $args 0] |
|---|
| 190 | if {$option == "-preproc"} { |
|---|
| 191 | set result [list -preproc preproc Preproc {} $preProc($w)] |
|---|
| 192 | |
|---|
| 193 | } elseif {$option == "-postproc"} { |
|---|
| 194 | set result [list -postproc postproc Postproc {} $postProc($w)] |
|---|
| 195 | |
|---|
| 196 | } else { |
|---|
| 197 | if {[catch "$w $command $args" result]} { |
|---|
| 198 | regsub $w $result $this result |
|---|
| 199 | return -code error $result |
|---|
| 200 | } |
|---|
| 201 | } |
|---|
| 202 | } else { |
|---|
| 203 | # ok, the user is actually configuring something... |
|---|
| 204 | # we'll deal with our special options first |
|---|
| 205 | if {[set i [lsearch -exact $args "-preproc"]] >= 0} { |
|---|
| 206 | set j [expr $i + 1] |
|---|
| 207 | set preProc($w) [lindex $args $j] |
|---|
| 208 | set args [lreplace $args $i $j] |
|---|
| 209 | set result {} |
|---|
| 210 | } |
|---|
| 211 | |
|---|
| 212 | if {[set i [lsearch -exact $args "-postproc"]] >= 0} { |
|---|
| 213 | set j [expr $i + 1] |
|---|
| 214 | set postProc($w) [lindex $args $j] |
|---|
| 215 | set args [lreplace $args $i $j] |
|---|
| 216 | set result {} |
|---|
| 217 | } |
|---|
| 218 | |
|---|
| 219 | # now, process any remaining args |
|---|
| 220 | if {[llength $args] > 0} { |
|---|
| 221 | if {[catch "$w $command $args" result]} { |
|---|
| 222 | regsub $w $result $this result |
|---|
| 223 | return -code error $result |
|---|
| 224 | } |
|---|
| 225 | } |
|---|
| 226 | } |
|---|
| 227 | } |
|---|
| 228 | |
|---|
| 229 | undo { |
|---|
| 230 | # if an undo command makes it to here, that means there |
|---|
| 231 | # wasn't anything to undo; this effectively becomes a |
|---|
| 232 | # no-op |
|---|
| 233 | set result {} |
|---|
| 234 | } |
|---|
| 235 | |
|---|
| 236 | insert { |
|---|
| 237 | |
|---|
| 238 | if {[catch {set index [text_index $w [lindex $args 0]]}]} { |
|---|
| 239 | set index [lindex $args 0] |
|---|
| 240 | } |
|---|
| 241 | |
|---|
| 242 | # since the insert command can have an arbitrary number |
|---|
| 243 | # of strings and possibly tags, we need to ferret that out |
|---|
| 244 | # now... what a pain! |
|---|
| 245 | set myargs [lrange $args 1 end] |
|---|
| 246 | set length 0 |
|---|
| 247 | while {[llength $myargs] > 0} { |
|---|
| 248 | incr length [string length [lindex $myargs 0]] |
|---|
| 249 | if {[llength $myargs] > 1} { |
|---|
| 250 | # we have a tag... |
|---|
| 251 | set myargs [lrange $myargs 2 end] |
|---|
| 252 | } else { |
|---|
| 253 | set myargs [lrange $myargs 1 end] |
|---|
| 254 | } |
|---|
| 255 | } |
|---|
| 256 | |
|---|
| 257 | # now, let the real widget command do the dirty work |
|---|
| 258 | # of inserting the text. If we fail, do some munging |
|---|
| 259 | # of the error message so the right widget name appears... |
|---|
| 260 | |
|---|
| 261 | if {[catch "$w $command $args" result]} { |
|---|
| 262 | regsub $w $result $this result |
|---|
| 263 | return -code error $result |
|---|
| 264 | } |
|---|
| 265 | |
|---|
| 266 | # we need this for the undo stack; index2 couldn't be |
|---|
| 267 | # computed until after we inserted the data... |
|---|
| 268 | set index2 [text_index $w "$index + $length chars"] |
|---|
| 269 | |
|---|
| 270 | if {$originalCommand == "undo"} { |
|---|
| 271 | # let's do a "see" so what we just did is visible; |
|---|
| 272 | # also, we'll move the insertion cursor to the end |
|---|
| 273 | # of what we just did... |
|---|
| 274 | $w see $index2 |
|---|
| 275 | $w mark set insert $index2 |
|---|
| 276 | } else { |
|---|
| 277 | # since the original command wasn't undo, we need |
|---|
| 278 | # to reset the undoIndex. This means that the next |
|---|
| 279 | # time an undo is called for we'll start at the |
|---|
| 280 | # end of the stack |
|---|
| 281 | set undoIndex($w) "" |
|---|
| 282 | } |
|---|
| 283 | |
|---|
| 284 | # add a delete command on the undo stack. |
|---|
| 285 | lappend undo($w) "d $index $index2" |
|---|
| 286 | } |
|---|
| 287 | |
|---|
| 288 | delete { |
|---|
| 289 | |
|---|
| 290 | # this converts the insertion index into an absolute address |
|---|
| 291 | set index [text_index $w [lindex $args 0]] |
|---|
| 292 | |
|---|
| 293 | # lets get the data we are about to delete; we'll need |
|---|
| 294 | # it to be able to undo it (obviously. Duh.) |
|---|
| 295 | set data [eval $w get $args] |
|---|
| 296 | |
|---|
| 297 | # add an insert on the undo stack |
|---|
| 298 | lappend undo($w) [list "i" $index $data] |
|---|
| 299 | |
|---|
| 300 | if {$originalCommand == "undo"} { |
|---|
| 301 | # let's do a "see" so what we just did is visible; |
|---|
| 302 | # also, we'll move the insertion cursor to a suitable |
|---|
| 303 | # spot |
|---|
| 304 | $w see $index |
|---|
| 305 | $w mark set insert $index |
|---|
| 306 | } else { |
|---|
| 307 | # since the original command wasn't undo, we need |
|---|
| 308 | # to reset the undoIndex. This means that the next |
|---|
| 309 | # time an undo is called for we'll start at the |
|---|
| 310 | # end of the stack |
|---|
| 311 | set undoIndex($w) "" |
|---|
| 312 | } |
|---|
| 313 | |
|---|
| 314 | # let the real widget command do the actual deletion. If |
|---|
| 315 | # we fail, do some munging of the error message so the right |
|---|
| 316 | # widget name appears... |
|---|
| 317 | if {[catch "$w $command $args" result]} { |
|---|
| 318 | regsub $w $result $this result |
|---|
| 319 | return -code error $result |
|---|
| 320 | } |
|---|
| 321 | } |
|---|
| 322 | |
|---|
| 323 | default { |
|---|
| 324 | # if the command wasn't one of the special commands above, |
|---|
| 325 | # just pass it on to the real widget command as-is. If |
|---|
| 326 | # we fail, do some munging of the error message so the right |
|---|
| 327 | # widget name appears... |
|---|
| 328 | if {[catch "$w $command $args" result]} { |
|---|
| 329 | regsub $w $result $this result |
|---|
| 330 | return -code error $result |
|---|
| 331 | } |
|---|
| 332 | } |
|---|
| 333 | } |
|---|
| 334 | |
|---|
| 335 | # is there a post-proc? If so, run it. |
|---|
| 336 | if {[info exists postProc($w)] && $postProc($w) != ""} { |
|---|
| 337 | if {[catch "$postProc($w) $originalCommand $originalArgs" error]} { |
|---|
| 338 | return -code error "error during processing of -postproc: $error" |
|---|
| 339 | } |
|---|
| 340 | } |
|---|
| 341 | |
|---|
| 342 | # we're outta here! |
|---|
| 343 | return $result |
|---|
| 344 | } |
|---|
| 345 | |
|---|
| 346 | # this returns a normalized index (ie: line.column), with special |
|---|
| 347 | # handling for the index "end"; to undo something we pretty much |
|---|
| 348 | # _have_ to have a precise row and column number. |
|---|
| 349 | proc Supertext::text_index {w i} { |
|---|
| 350 | if {$i == "end"} { |
|---|
| 351 | set index [$w index "end-1c"] |
|---|
| 352 | } else { |
|---|
| 353 | set index [$w index $i] |
|---|
| 354 | } |
|---|
| 355 | return $index |
|---|
| 356 | } |
|---|
| 357 | |
|---|
| 358 | |
|---|