From: "David Herron" <david@twg.com>
Subject: Keyboard focus and keyboard driven traversal
Sensitivity: Personal
Encoding:  62 TEXT , 193 TEXT 
Conversion: Prohibited
Organization: The Wollongong Group, Inc., Palo Alto, CA
Conversion-With-Loss: Prohibited
Date: Tue, 28 Jul 1992 21:51:43 GMT

Greetings!

One of the fundamental things in Motif is that every application must be
equally drivable from either keyboard or mouse.  There are at least two
reasons for this:

- Some people are either unwilling or unable to use a mouse (effectively).
  While one might not have much sympathy for those unwilling to use mice,
  the ones `unable' to do so deserve lots of sympathy.

- Some work environments are too busy and crowded to put a mouse on the
  persons desk.  Take a look at a stock traders work area sometime.  They
  have umpty-umpt screens in front of them, and a keyboard for each one
  lying around, a TV screen (or more), telephones, etc.  No Room for extravagences
  like mice.

There is a complicated set of rules which a Motif application must follow
to allow this.  It is all detailed in the style guide, but I have not had
the time to learn it.  Fortunately the Motif toolkit implements either the
real thing, or something pretty close.

The basic idea is that TAB, BACK-TAB, and the arrow keys move keyboard focus
around the screen.  So if you have a data entry form to fill out the keyboard
use is: type value, TAB, type value, TAB, type value, ...  When done TAB
and ARROW to the right button, and hit RETURN.  Voila' no mouse activity
and much easier than: type, POSITION, type, POSITION, type, POSITION, ..., CLICK.

The other part is that each widget has a way to trigger actions using the 
keyboard.  Above I mentioned RETURN to activate a button, SPACE works as well.
The arrow keys scroll listboxes or edit boxes.  The arrow keys also move
panes in paned windows.  Hitting F10 enable keyboard driving of the
menu bar, LEFT and RIGHT change which menu pane is pulled down, while UP
and DOWN change which menu button is enabled.  etc...


For the application I'm writing at home (SEARCH) it seems that a lot of time
is being spent moving back and forth between keyboard and mouse.  To cut
this down I wanted to drive as many things as possible from the keyboard.
This meant having a mechanism for moving keyboard focus around the screen.
Once keyboard focus is on a widget, to indicate this to the user.

So I wrote up a TK module to handle focus setting.  This has gone through
at least 3 design-coding iterations, and is pretty useful at the moment.

It does not match up with Motif's style guide in a couple of ways:

- I could not figure out how to tell TK to bind an action to Shift<Key>Tab.
  So instead of BACK-TAB, this module uses Control-b.

- It indicates focus differently from Motif.  Motif draws a border around
  the outside of the widget.  FOCUS changes the relief of a widget.

- It does not use LEFT, RIGHT, UP or DOWN for some traversals, Motif does this
  when there are a group of buttons in the same area.

- You cannot reach all widgets in the interface.  Only those described to FOCUS.


A weirdity in FOCUS is that you must initialize it by calling FOCUS:traverse
after setting up all the traversals.  (Or maybe it's FOCUS:enter?)  Otherwise
it doesn't work.




#
# $Id: focus.tcl,v 1.3 1992/07/21 15:36:33 david Exp $
# focus -- Functions for implementing keyboard-driven focus traversal
#
# This module deals with FocusObjects as its unit-of-discourse.
# A FocusObject is something which may have focus, and may
# be traversed through.  It has a couple of subfields as so:
#
#	widget to which keyboard focus is to be directed.
#
#	widget whose relief is to be modified.
#
# When a widget has focus, the relief-widget is made `raised'.  When
# it loses focus, it is made `flat'.  Normally one will want
# this relief-widget to be a frame widget.  This is largely because
# each widget has a `natural & preferred' relief which is part of
# what identifies that widget to the user.  In order for the relief
# to show up, the frame must have -borderwidth > 0.
#
# Three main entry points exist:
#
# FOCUS:OBJ:create -- Creates an FocusObject instance.
# FOCUS:OBJ:obj    -- Get widget name for the FocusObject's object.
# FOCUS:OBJ:frame  -- Get the widget name for its frame.
#
# FOCUS:circle     -- Sets up a circular chain of FocusObject traversals.
# FOCUS:bind       -- Sets up more specialized traversals.
# FOCUS:now        -- Return the FocusObject name where it is NOW.
#
# The FocusObjects are stored in a global array called FOCUS_OBJ_heap.
# The `name' of the FocusObject is used to reference into the heap.
# Each is stored as a TCL list, with members in the order specified above.
#
#
#
# $Log: focus.tcl,v $
# Revision 1.3  1992/07/21  15:36:33  david
# Create functions to return parts of FocusObject structure, and find
# the FocusObject which corresponds to a certain widget.  Add support
# for rebinding the bindings.  Remember where focus currently is.
#
# Revision 1.2  1992/07/19  19:41:02  david
# Change indicator of when a widget has focus.  Used to set background
# color, now change -raised option on a frame widget.
# Revamped API to focus module to allow for more terse descriptions
# of the traversals.  Did this through creation of the FocusObject
# concept.
#
# Revision 1.1  1992/05/27  06:51:32  david
# Initial revision.
#
#
#

proc FOCUS:OBJ:create {{name} {object} {frame}} {
	global FOCUS_OBJ_heap
	set FOCUS_OBJ_heap($name) [list $object $frame ]
}

proc FOCUS:OBJ:obj name {
	global FOCUS_OBJ_heap
	return [lindex $FOCUS_OBJ_heap($name) 0]
}


proc FOCUS:OBJ:frame name {
	global FOCUS_OBJ_heap
	return [lindex $FOCUS_OBJ_heap($name) 1]
}

proc FOCUS:OBJ:find object {
	global FOCUS_OBJ_heap

	foreach name [array names FOCUS_OBJ_heap] {
		set obj $FOCUS_OBJ_heap($name)
		if {"$object" == [lindex $obj 1] || \
		    "$object" == [lindex $obj 0]} {
			return $name
		}
	}
	return ""
}

# FOCUS:circle -- Create a chain of FOCUS:traversals for the
#	Objects specified in the arg-list.  Use <Tab> to move forward
#	through the circle, and <Control-b> to move backwards.  We'd
#	**LIKE** to use BackTab for moving backwards, but cannot figure
#	out how to specify that to TK.
#
# We destroy any preexisting binding.
#
proc FOCUS:circle {args} {
	global FOCUS_OBJ_heap

	foreach arg $args {
		if {![info exists first]} {
			set first $arg
			set last $arg
			continue
		}

		set to $FOCUS_OBJ_heap($arg)
		set from $FOCUS_OBJ_heap($last)

		set to_w   [lindex $to   0]
		set from_w [lindex $from 0]

		catch {bind $from_w <Tab>}
		bind $from_w <Tab>       "FOCUS:traverse $last $arg"
		catch {bind $to_w   <Control-b>}
		bind $to_w   <Control-b> "FOCUS:traverse $arg  $last"

		set last $arg
	}

	if {[info exists last]} {
		if {"$first" != "$last"} {
			set to $FOCUS_OBJ_heap($first)
			set from $FOCUS_OBJ_heap($last)

			set to_w   [lindex $to   0]
			set from_w [lindex $from 0]

			catch {bind $from_w <Tab>}
			bind $from_w <Tab>       "FOCUS:traverse $last  $first"
			catch {bind $to_w   <Control-b>}
			bind $to_w   <Control-b> "FOCUS:traverse $first $last"
		}
	}
}

# FOCUS:bind -- Create special purpose bindings from a particular Object.
#	The Object is specified in $from_nm.
#	For each of the args, they are a TCL list of the following members
#		binding
#		destination FocusObject name
#
# We destroy any preexisting binding.
#
proc FOCUS:bind {{from_nm} args} {
	global FOCUS_OBJ_heap

	set from $FOCUS_OBJ_heap($from_nm)
	set from_w [lindex $from 0]

	foreach arg $args {
		set binding [lindex $arg 0]
		set to_nm   [lindex $arg 1]

		set to   $FOCUS_OBJ_heap($to_nm)
		set to_w [lindex $to   0]

		catch {bind $from_w $binding}
		bind $from_w $binding "FOCUS:traverse $from_nm $to_nm"
	}
}

# FOCUS:enter -- Utility to perform the `entering' part of the
#	focus traversal.
#
proc FOCUS:enter {{w} {f}} {
	focus $w
	$f configure -relief raised
}

# FOCUS:leave -- Handle the `leaving' part of traversal.
#
proc FOCUS:leave {{w} {f}} {
	$f configure -relief flat
}

# FOCUS:traverse -- Do the job of traversing from one widget to
#	the other.  Simply call FOCUS:enter and FOCUS:leave
#	to do the trick.
#
proc FOCUS:traverse {{from_nm} {to_nm}} {
	global FOCUS_OBJ_heap FOCUS_now

	set to   $FOCUS_OBJ_heap($to_nm)
	set from $FOCUS_OBJ_heap($from_nm)

	FOCUS:leave [lindex $from 0] [lindex $from 1]
	FOCUS:enter [lindex $to   0] [lindex $to   1]

	set FOCUS_now $to_nm
}

proc FOCUS:now {} {
	global FOCUS_now
	return $FOCUS_now
}
Newsgroups: comp.lang.tcl