Tab/Focus Sequencing

This is something that has stumped me for some time. As a recent re-convert back to Tcl I needed to solve a problem regarding a dynamic Tk interface that I needed more control over the Tab and Shift-Tab Sequence for how a user would navigate through the interface using the Keyboard.

After a lot of digging, and talking to the guys at ActiveState, I came up with the following.

Assume you have 5 entry fields that you needed to control the tab sequence through.

set count 1
foreach item [list .one .two .three .four .five] {
     frame .fr$count
     pack .fr$count

     label .l_$count -text "ITEM: $count"
     entry $item
     pack .l_$count $item -side left -in .fr$count
     incr count
}

By default, Tk's focus model will be going to the next widget that is displayed - which is OK if you are always building your interfaces based on the default tab-sequence that Tk expects. But most of my code does not work around the tab-sequece. (I just don't think or plan my code around that level of detail in life).

To solve this, you can override the default bindings and replace them with your own hard coded ones, or a new binding that allows more control over what is next.

The default binding is as follows:

# default bindings for <Tab>  --- execute "bind all <Tab>"
tk::TabToWindow [tk_focusNext %W]

Here is the override I created:

# Override the Bindings
bind all <Tab> {break}
bind all <Shift-Tab> {break}

This effectively overrides ALL Tab and Shift-Tab bindings to a NULL return/action.

Now to fix the binding to do your own thing:

# Add Custom Binding
bind <widget> <Tab> {customTabSeq %W next}
bind <widget> <Shift-Tab> {customTabSeq %W prev}

The above will call a process called customTabSeq that will pass in the current widget and the direction of tab you want to do.

Here is the code for the customTabSeq:

# Custom Tab Sequence Procedure

     proc customTabSeq {w direction} {
           
            set currentWidget $w
            
            set base ".[lindex [split $w .] 1]"
            set widgetPath ".[string trimleft $w .$base]"
            
            
            # list of input elements that I want to sequence through
            set tabSequence [list <<list of widgets here>>]
            
            set curLocation [lsearch $tabSequence $currentWidget]
            puts "Current Location: $curLocation"
            if {[string match $direction next]} {
                if {[string match $currentWidget [lindex $tabSequence end]]} {
                    set newLocation [lindex $tabSequence 0]
                } else {
                    set newLocation [lindex $tabSequence [expr $curLocation + 1]]
                }
            } else {
                if {[string match $currentWidget [lindex $tabSequence 0]]} {
                    set newLocation [lindex $tabSequence end]
                } else {
                    set newLocation [lindex $tabSequence [expr $curLocation - 1]]
                }
            }
            
            focus -force $newLocation
            
        } 

FULL WORKING EXAMPLE

bind all <Tab> {break}
bind all <Shift-Tab> {break}

set count 1
foreach item [list .one .two .three .four .five] {
     frame .fr$count
     pack .fr$count

     label .l_$count -text "ITEM: $count"
     entry $item

     bind $item <Tab> {customTabSeq %W next}
     bind $item <Shift-Tab> {customTabSeq %W prev}

     pack .l_$count $item -side left -in .fr$count
     incr count
}

 proc customTabSeq {w direction} {
           
            set currentWidget $w
            
            set base ".[lindex [split $w .] 1]"
            set widgetPath ".[string trimleft $w .$base]"
            
            
            # list of input elements that I want to sequence through
            set tabSequence [list .five .three .one .four .two]
            
            set curLocation [lsearch $tabSequence $currentWidget]
           
            if {[string match $direction next]} {
                if {[string match $currentWidget [lindex $tabSequence end]]} {
                    set newLocation [lindex $tabSequence 0]
                } else {
                    set newLocation [lindex $tabSequence [expr $curLocation + 1]]
                }
            } else {
                if {[string match $currentWidget [lindex $tabSequence 0]]} {
                    set newLocation [lindex $tabSequence end]
                } else {
                    set newLocation [lindex $tabSequence [expr $curLocation - 1]]
                }
            }
           
            puts "MOVING FROM $currentWidget ---> $newLocation"
 
            focus -force $newLocation
            
        } 

Hope this helps others out there also....