Version 0 of Undo/Redo for Gnocl Text Widgets

Updated 2008-11-25 20:36:59 by wjg

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 not direct support for undo/redo (as yet at least) but the signals to implement the feature are already in place. The Gnome sourceView widget has support for undo/redo plus a whole host of code editing functionality, but gnocl::sourceView will be shipped with layer with the release of Gnocl 0.9.93. So, here's the script for undo/redo. One of the nice


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

 }

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


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

enter categories here