In many respects the Gtk widget set presents a more powerful set of widgets compared to Tk offerings. The GtkTextView widget, however, has a particularly large gap in its features set, one which used to plague the Tk text widget too, the lack of an undo/redo function. The Gnome GtkSourceView widget does have in-built undo/redo, but this is not unlimited and the sourceView widget was designed for code editing applications, not the editing and display of 'simple text'.
This does not mean that undo/redo functionality cannot be implemented. The 0.9.92 release of Gnocl [L1 ] has extended support for both widget events and signals generated by the GtkTextView widget which enables a reliable undo/redo capability. Here's the code, and the support packages to do the job.
#--------------- # UndoRedoDemo.tcl #--------------- # Created by William J Giddings # October, 2008 #--------------- # Description: # Demonstrates simple textBuffer undo/redo functionality. #--------------- # Notes: # cd /Desktop/GnoclEdit_2/undoer/wjgstuff # # This version has a single buffer to show edit history # #--------------- # basic Tcl/Gnocl Script #! /bin/sh/ #\ exec tclsh "$0" "$@" package require Gnocl source gnoclTextUndoRedo.tcl source gnoclBind.tcl set text [gnocl::text] set box1 [gnocl::box -orientation vertical] set box2 [gnocl::box -orientation horizontal ] set undo [gnocl::button -text "%#Undo" -onClicked { on_undo $text } ] set redo [gnocl::button -text "%#Redo" -onClicked { on_redo $text } ] $box2 add [list $undo $redo] $box1 add $box2 $box1 add $text -fill {1 1} -expand 1 gnocl::window -title "Text" -child $box1 set userAction 1 #---------------- # UNDO/REDO STUFF #---------------- # respond to text insert signals $text configure -onInsertText { if { $userAction } { # add the event details to the undo stack push ${text}.UNDO "insert-text \{%r %c\} \{%t\} \{%l\}" # clear the redo stack catch { unset ${text}.REDO } } } # respond to text insert signals $text configure -onDeleteRange { if { $userAction } { push ${text}.UNDO "delete-range \{%r %c\} \{%l %o\} \{%t\}" } } # respond to text action signals $text configure -onBeginUserAction { set userAction 1 } $text configure -onEndUserAction { set userAction 0 } # there are no default Undo/Redo bindings, so set your own # (GtkSourceView widget defaults to Ctrl-z & Ctrl-Z gnocl::bind $text <Ctrl-Key-z> "on_undo $text" gnocl::bind $text <Ctrl-Key-y> "on_redo $text" $text insert end "Ctrl-z undo\nCtrl-y redo" #----------------- gnocl::mainLoop # EOF UndoRedoDemo.tcl
And now, the support packages:
#--------------- # gnoclTextUndoRedo.tcl #--------------- # Created by WIlliam J Giddings # 03/08/2008 #--------------- # Description: # Provide undo/redo functionality for the gnocl text widget. #--------------- # Notes: # #--------------- # use arrays to create stacks to contain details of the events # this is more effecient than using lists # see Welch, Jones & Hobbs pp101 proc push {stack value} { upvar $stack S if { ! [info exists S(top)] } { set S(top) 0 } set S($S(top)) $value incr S(top) } proc pop { stack } { upvar $stack S if { ![info exists S(top)] } { return {} } if {$S(top) == 0 } { return {} } else { incr S(top) -1 set x $S($S(top)) unset S($S(top)) return $x } } # # UNDO / REDO proceedures # # Notes: # ----- # Create undo/redo buffers unique to the widget. proc on_undo { w } { global ${w}.UNDO global ${w}.REDO if { [array size ${w}.UNDO] == 0 } { return } set action [pop ${w}.UNDO] switch [lindex $action 0 ] { "insert-text" { # determine the end of range to delete from length of text inserted set col [expr [lindex [lindex $action 1] 1] +[lindex $action 3] ] set row [lindex [lindex $action 1] 0] $w erase [lindex $action 1] [list $row $col] # resposition the cursor $w setCursor [list $row $col] } "delete-range" { # strip leading and trailing braces from the string $w insert [lindex $action 1] [string trim [lindex $action 3] \{\}] # resposition the cursor to the end of the inserted text $w setCursor [lindex $action 1] $w setCursor cursor+[string length [lindex $action 2]] } } # display the changes $w scrollToPosition cursor $w configure -hasFocus 1 push ${w}.REDO $action } proc on_redo { w } { global ${w}.UNDO global ${w}.REDO if { [array size ${w}.REDO] == 0 } { return } set action [pop ${w}.REDO] switch [lindex $action 0 ] { "insert-text" { # determine the end of range to delete from length of text inserted # The text is returned as a list, so if there is purely whitespace this # will re mis-read as a sub-list by the interpreter. # So, get the text as the first item to prevent this. $w insert [lindex $action 1] [lindex [lindex $action 2] 0] # resposition the cursor to the end of the inserted text $w setCursor [lindex $action 1] $w setCursor cursor+[string length [lindex $action 2]] } "delete-range" { # determine the end of range to delete from length of text inserted set col [expr [lindex [lindex $action 1] 1] +[lindex $action 3] ] set row [lindex [lindex $action 1] 0] $w erase [lindex $action 1] [list $row $col] # resposition the cursor $w setCursor [list $row $col] } } # display the changes $w scrollToPosition cursor $w configure -hasFocus 1 push ${w}.UNDO $action } # EOF gnoclTextUndoRedo.tcl
And now for the binding code.
#--------------- # gnocl::bind.tcl #--------------- # This file adds keysequence bindings to a gnocl widget # # Author: William J Giddings, 13-Sept-2007 #--------------- # basic Tcl/Gnocl Script #!/bin/sh/ #\ exec tclsh "$0" "$@" # Modifier Bitmask Values # # 0 | no modifiers # 1 | Shift # 2 | Caps_Lock on # 4 | Control_L/R # 8 | Alt_L/R # 16 | Num_Lock (on) # 32 | ? # 64 | Super_L/R # 128 | alt-gr # I've seen these values appear, but.. # 256 | Button-1 # 512 | Button-2 # package require Gnocl #--------------- # As I like real-words rather than deniary #--------------- proc kb_modifiers {v} { set state 0 set flags { Shift 1 Caps_Lock 2 Ctrl 4 Alt 8 Num_Lock 16 Super 64 Alt-Gr 128 Button_1 256 Button_2 512 } foreach {a b} $flags { if {$v & $b } {lappend state $a} } return $state } #--------------- # create binding handler #--------------- proc gnocl::keyBindingHandler {w s K} { # remove Num_Lock On event bitmask set event [lindex $s 0] if {16 & $event} { set event [expr 16 ^ $event ] } set s [lreplace $s 0 0 $event] # check for Shift, if a single letter, restore to lowercase if {1 & $event && [string length $K] == "1"} { set K [string tolower $K] } set events [array names ::keyBindings] # sorry, not the best practice to error trap with catch, but its the easiest! catch { eval $::keyBindings($s,$K) } } #--------------- # create binding handler #--------------- proc gnocl::buttonBindingHandler { w s b x y} { # remove Num_Lock On event bitmask set event [lindex $s 0] if {16 & $event} { set event [expr 16 ^ $event ] } set s [lreplace $s 0 0 $event] # execute binding catch { # save current pointer coordinate of last click set ::gnocl::x $x set ::gnocl::y $y eval [ set ${w}.buttonBindings($s,Button$b) ] } } #--------------- # assign bindings to (text) widget # concatenate these bindings with others which may have been assigned to events #--------------- proc gnocl::bind {widget event script} { # what are the existing bindings? # cget not yet implemented # puts "keyPress [$widget cget -onKeyPress]" # puts "buttonPress [$widget cget -onButtonPress]" set event [string trimleft $event "<"] set event [string trimright $event ">" ] set tmp "-" regsub -all -- - $event " " event # parse event and create BITMASK set bitMask 0 foreach {eventType bitVal} { Shift 1 Ctrl 4 Alt 8 } { if { [string first $eventType $event] != -1 } { set bitMask [expr $bitMask + $bitVal] } } if { [string first Key $event] != -1 } { # add to the list of Key events set ::keyBindings($bitMask,[lindex $event end]) $script } elseif { [string first Button $event] !=-1 } { # add to the list of Button Events set ${widget}.buttonBindings($bitMask,[lindex $event end]) $script } # attach bindings $widget configure -onKeyPress { gnocl::keyBindingHandler %w %s %K } $widget configure -onButtonPress { gnocl::buttonBindingHandler %w %s %b %x %y } } #----- DEMO CODE ----- proc bind:demo {} { set txt [gnocl::text] gnocl::window \ -child $txt \ -title "GNOCL Text Bindings" \ -visible 1 \ -width 250 \ -height 120 \ -onDestroy {exit} $txt insert end TEST # Add some bindings, some of these will conflict with GTK defaults # These bindings do not replace the defaults as in TK gnocl::bind $txt <Shift-Key-a> {puts "Say 'Shift-a'"} gnocl::bind $txt <Alt-Key-A> {puts "Say 'Alt-a'"} gnocl::bind $txt <Alt-Key-a> {puts "Say 'Alt-a'"} gnocl::bind $txt <Ctrl-Key-a> {puts "Say 'Ctrl-a'"} gnocl::bind $txt <Shift-Alt-Key-a> {puts "Say 'Shift-Alt-a'"} gnocl::bind $txt <Shift-Ctrl-Key-a> {puts "Say 'Shift-Ctrl-a'"} gnocl::bind $txt <Shift-Alt-Ctrl-Key-a> {puts "Say 'Shift-Alt-Ctrl-a'"} gnocl::bind $txt <Ctrl-Key-F1> {puts "Ctrl F1"} gnocl::bind $txt <Shift-Key-F1> {puts "Shift F1"} gnocl::bind $txt <Key-F2> {puts "F2"} gnocl::bind $txt <Alt-Button1> {puts "Alt Button1!"} gnocl::bind $txt <Ctrl-Button1> {puts "Ctrl Button1!"} gnocl::bind $txt <Shift-Button1> {puts "Shift Button1! $::gnocl::x $::gnocl::y"} gnocl::bind $txt <Alt-Button2> {puts "Alt Button2!"} gnocl::bind $txt <Ctrl-Button2> {puts "Ctrl Button2!"} gnocl::bind $txt <Shift-Button2> {puts "Shift Button2!"} gnocl::bind $txt <Ctrl-Key-z> {puts "UNDO!"} gnocl::bind $txt <Shift-Ctrl-Key-z> {puts "REDO!"} } # EOF gnocl::bind.tcl