Version 5 of Undo/Redo for Gnocl Text Widgets

Updated 2008-11-26 13:56:26 by LV

WJG (25/11/08) What use is text widget without an undo/redo? For some reasons known only to the Gtk core development team, there's no direct support for undo/redo (as yet at least) but the signals to implement the feature are already in place. Perhaps one reason for this is the introduction of the Gnome sourceView widget which has full support for undo/redo plus a whole host of code editing functionality (gnocl::sourceView will be shipped with the release of Gnocl 0.9.93). So, here's the script for undo/redo.


 #---------------
 # 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 efficient than using lists
 # see Welch, Jones & Hobbs(2003, 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 procedures
 #

 # 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]

                            # reposition 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]

                                # reposition the cursor
                                $w setCursor [list $row $col]
                        }
        }

        # display the changes
        $w scrollToPosition cursor
        $w configure -hasFocus 1
        push ${w}.UNDO $action

 }

Note, to run the demo, it'll be necessary to load a further module, gnoclBind.tcl. Get it here: [L1 ]


So, at last, here's the demo script:

 #---------------
 # UndoRedoDemo.tcl
 #---------------
 # Created by WIlliam J Giddings
 # March, 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" "[email protected]"

 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