Key Pages

Projects

There are several issues related to keybaord handling that need to be addressed:

Currently Tk does these all badly. Let's take them one at a time:


Accelerator keys

There are two parts to handling accelerator keys: showing the user which keys are available and initiating the action on the key press.

Tk can show the key by using the -underline field for the label or button. The underline field gives the offset into the label for the key to underline. To support i18n, you not only need to change the label, but also the underline. You also need to bind the correct action to the key based on the label, since it will likely be a different key for different languages.

The ideal action is as follows (from observing Windows 2000):

  1. When Alt is pressed, show the available accelerators --- these stay up for the duration of the dialog, or until a bare Alt is pressed again. This means that users who don't bother with accelerators don't get the screen clutter, which IMHO is a good thing. Tk does not support this.
  2. If the widget is disabled or unmapped, ignore it. Windows applications are not consistent about this. Some go the next active field even if the label is disabled.
  3. If the widget can take focus, move to it, otherwise traverse into the next available focus item, which is presumably the entry following a label.
  4. If the focus is on a button class of some sort, invoke it.
  5. If the focus is on a menu item, open that menu item.

For the developer, the ideal would be to insert an & in the appropriate label, and have Tk automagically figure everything out (okay, even better would be to have Tk choose the correct accelerators automatically --- any AI hackers out there ;-). We could define a new option -autoaccel for widgets buttons and labels which if true, scans the widget for &, remembers the position, strips the & from the text and registers the appropriate key with the toplevel for the widget. For 9.0, -autoaccel could default to true.

What does it mean to register an accelerator? Tk already does "bind all <Alt-Key> { tk::TraverseToMenu %W %A }". The traverse to menu function would have to be extended or replaced. For each widget in the list associated with each key (you need a list because the notebook widget might use the same accelerators on different pages), check if the widget is mapped and enabled. If you can focus on the widget, do so, otherwise use tk_nextFocus to move to the next editable widget. If the focus widget can be invoked (e.g., if it is a button), invoke it. When Alt is pressed the first time or pressed by itself, run through all the registered widgets set or clear -underline. Whenever the label changes, the registration function needs to clear the current accelerator associated with the widget, then scan the label for a new one.

Does this sound general enough yet easy and complete enough for inclusion into the core?

Accelerators for <Escape> and <Return> must be registered separately. <Return> has other complications (see below).

Invoking a button (e.g., Apply) may cause the button to disable itself. On disable, if the button has focus, it should move it to the next available widget.


FocusIn versus TraverseIn

Joe English: I've been looking into focus and keyboard traversal issues in the BWidget toolkit, and have a couple patches that might be worth applying to core Tk.

The first patch adds new virtual events <<TraverseIn>> and <<TraverseOut>>, and modifies the <Tab> and <Shift-Tab> bindings to generate them. This is for things like 'entry' widgets, which normally auto-highlight when tabbed into, but should _not_ do this when they receive focus for other reasons (e.g., when switching focus among toplevels). See Tcllib bug #720032 for more discussion: [link]

Paul Kienzle: Clicking in an entry widget (Windows 2000) which does not yet have the focus is like <<TraverseIn>> in that it selects the text. Bug or feature?


Shift-Tab and Megawidgets

Joe English: The second patch -- not yet comitted, I'm not sure if it's exactly the right thing to do -- changes the algorithm for finding the "previous widget" in traversal order in the <Shift-Tab> binding, so that it never traverses from a child to an immediate parent. This was to work around a specific problem with the BWidget ComboBox (cycling through with <Tab> works properly, but you have to press <issue probably affects other compound widgets as well. See Tcllib bug #765667: [link]

Arrow keys and button groups

Paul Kienzle: Arrows in Windows usually move between widgets in a subgroup only, though there are frequent exceptions, such as:

Windows 2000 IE >Tools>Internet Options... General

where arrows cycle amongst

{Clear History} Colors... Fonts... Languages... Accessibility... OK

but not {Cancel Apply}. KDE konqueror is similarly inconsistent, sometimes trapping arrow keys in subgroups, sometimes not.

I believe allowing arrows to cycle amongst immediate siblings should give reasonable behaviour for button groups (which is where we need it), and since the target behaviour is so erratic outside button groups, I'm inclined to ignore it.

The following code adds arrow traversal to dialogs. It requires all the buttons to be in the same frame, which may be a problem if some choices can involve sub-choices which are enabled/disabled depending on the value of the choice. For now, I'm assuming those cases can be handled by making a subframe on the same level as the buttons, but this complicates tab-through support for radio buttons (more later). A complicating issue is that widgets do not in general consume the keys that they use. For example, if an entry widget processes the key A, it still sends the key A up to the next bind tag for processing. So rather than allowing arrow keys amongst any widgets in a frame, we have to explicitly list which widget classes can use arrow key traversal. Surely there is a better way to do this?

 # Arrow keys to cycle within a group.
 foreach t { Button Radiobutton Checkbutton Entry } {
     bind $t <Up> { tk_focusPrevInGroup %W }
     bind $t <Down> { tk_focusNextInGroup %W }
 }
 foreach t { Button Radiobutton Checkbutton } {
     bind $t <Right> { tk_focusNextInGroup %W }
     bind $t <Left> { tk_focusPrevInGroup %W }
 }

 proc tk_focusNextInGroup w {
     set searching 0
     set found $w
     foreach c [winfo children [winfo parent $w]] {
	 if { $c eq $w } {
	     set searching 1
	 } elseif { $searching && [tk::FocusOK $c] } { 
	     set found $c
	     break
	 }
     }
     if { [winfo class $found] eq "Radiobutton" } { $found invoke }
     tk::TabToWindow $found
 }

 proc tk_focusPrevInGroup w {
     set searching 1
     set found $w
     foreach c [winfo children [winfo parent $w]] {
	 if { $c eq $w } {
	     set searching 0
	 } elseif { $searching && [tk::FocusOK $c] } { 
	     set found $c
	 }
     }
     if { [winfo class $found] eq "Radiobutton" } { $found invoke }
     tk::TabToWindow $found
 }

Tab keys and radio buttons

Paul Kienzle: I've been looking into creating Internet Explorer like property dialogs, where a big issue is tabbing through radio buttons. Gnome, KDE, Windows and Mac all treat a set of radio buttons as a single entity when tabbing, but Tk tabs through each button. They use arrow keys to move between radio buttons, invoking each as they move. Tk tabs between radio buttons, but only invokes them when spacebar is pressed.

We can get Tk to do the right thing most of the time if we add some special handling for radio buttons to tk_focusNext/tk_focusPrev.

What I've done is check whether I'm entering or leaving a radio button group. If the current widget is a radio button, I'm leaving, otherwise I'm entering. If leaving, do the normal traversal, but skip over any radiobuttons from the same group. If entering, do the normal traversal, but skip over any radio buttons which are not active. It seems to work for a fairly complicated test case (subframes associated with the radio buttons, and unrelated widgets which are not part of the radio button group).

bind all <Tab> { tk::TabToWindow [tk_focusNextRadio %W] }
bind all <<PrevWindow>> { tk::TabToWindow [tk_focusPrevRadio %W] }
proc tk_focusNextRadio w {
    set next [tk_focusNext $w]
    if { [winfo class $w] eq "Radiobutton" } {
	# Leaving radio group
	set var [$w cget -variable]
	while { [winfo class $next] eq "Radiobutton" } {
	    if { $var eq [$next cget -variable] } {
		if { $next eq $w } break ;# singleton radio widget in toplevel
		set next [tk_focusNext $next]
	    } else {
		break
	    }
	}
    } elseif { [winfo class $next] eq "Radiobutton" } {
	# Entering radio group
	set var [$next cget -variable]
	upvar \#0 $var val
	if { [info exists val] } { # protect against missing or undefined -variable
	    set default $next
	    while { [winfo class $next] eq "Radiobutton" } {
		if { $var ne [$next cget -variable] } {
		    # Leaving radio group without finding value
		    # Default to the first radio element, after invoking it
		    set next $default
		    $next invoke
		    break
		} elseif { [$next cget -value] eq $val } {
		    # Leaving radio group after finding value
		    break
		} else {
		    # Still in radio group, still haven't found value
		    set next [tk_focusNext $next]
		}
	    }
	}
    }
    
    return $next
}

proc tk_focusPrevRadio w {
    set next [tk_focusPrev $w]
    if { [winfo class $w] eq "Radiobutton" } {
	# Leaving radio group
	set var [$w cget -variable]
	while { [winfo class $next] eq "Radiobutton" } {
	    if { $var eq [$next cget -variable] } {
		if { $next eq $w } break ;# singleton radio widget in toplevel
		set next [tk_focusPrev $next]
	    } else {
		break
	    }
	}
    } elseif { [winfo class $next] eq "Radiobutton" } {
	# Entering radio group
	set var [$next cget -variable]
	upvar \#0 $var val
	if { [info exists val] } { # protect against missing or undefined -variable
	    while { [winfo class $next] eq "Radiobutton" } {
		if { $var ne [$next cget -variable] } {
		    # Leaving radio group without finding value
		    # Default to the first radio element, after invoking it
		    set next $default
		    $next invoke
		    break
		} elseif { [$next cget -value] eq $val } {
		    # Leaving radio group after finding value
		    break
		} else {
		    # Still in radio group, still haven't found value
		    # Since we are going backwards, we are still not at the first
		    set default $next
		    set next [tk_focusPrev $next]
		}
	    }
	}
    }
    
    return $next
}

<Return> accelerator

Paul Kienzle: <Return> as an accelerator presents some challenges. The UI can't identify the active widget using underlines (obviously), so it draws a box around the default recipient of the action instead. That means that as you traverse the dialog, you have to check whether you are on a button, and if so, move the outline to that button, but if not, move the outline back to the default button.

I have an icky example which hardcodes the default widget:

# Yuck! Move the "active" square to the current widget if it is
# a button, otherwise move it to the default button.
bind . <FocusIn> {
    if {[winfo class %W] eq "Button"} {
	.bf.ok configure -default normal
	%W configure -default active
    } else {
	.bf.ok configure -default active
    }
}
bind . <FocusOut> {
    .bf.ok configure -default active
    if {[winfo class %W] eq "Button"} {
	%W configure -default normal
    }
}

Some comments:


<Return> not processed by buttons

Paul Kienzle: By default Tk does not process <Return> on buttons to invoke them. The following code fixes this:
# Default button bindings: Return activates the button
bind Button <Return> { %W invoke ; break }
# bind Checkbutton <Return> { %W invoke ; break }
# Radiobutton doesn't need this since we can no longer traverse to a radio button without activating it

DKF: Generally, <Return> should invoke the dialog default button, so it isn't the responsibility of the button but the overall toplevel bindtag to trap the event.

Paul Kienzle: The Windows 2000 box beside me says that <Return> does not toggle a Checkbutton (perhaps that was a Windows ME behaviour?). However, we still have the problem of the roving default behaviour: on Windows 2000, if a button has focus, then it should respond to <Return>. The easiest way to implement this is to bind <Return> to a button, though I suppose the same infrastructure which moves the "-default active" to the correct button could also rebind <Return> at the top level to that button.


Paul Kienzle: I've created a somewhat complicated dialog to test some of these things.

It runs on bare 8.4.2. I haven't included Joe English's patches.

Features

Using -uniform, the buttons are all the same width. It works in this case because the buttons are all in the same grid. If the buttons were in a different grid, you would have to fix their sizes by hand. Maybe uniform could be a new command which takes a list of widgets which are not necessarily packed together and makes them all the same size?

The accelerator keys are hard-coded, which is bad for internationalization. They are bound to the top level dialog. I may add code for automating accelerators later.

The disabled state in Windows uses grooved text. This could be drawn by first writing the text in the highlight colour one pixel down and to the right, then writing the text in the disabled colour at the usual location. This style is used on disabled buttons, labels and menus, but not in entry boxes. RS put a pure-Tcl example of disabled text up on the wiki, though I think it is shifted up and left by a pixel.

The position of the check box square is different from a similar box in the IE internet options dialog, where the square is flush left with the preceding and following lines. Using "checkbutton .af.activate -bg blue \" will show that the offending space is in the checkbutton. I can't get rid of it by setting -padx 0.

Document Icontestdialog.tcl


Posted at Aug 12/2003 01:33 AM:
DKF: Magical appearing accelerators would be best handled through either something bound (as an option) to the toplevel or through the tk command.


Posted at Aug 13/2003 01:44 PM:
Will Duquette: It seems to me that "accelerator" is being used here for two different things. The first (and the one most discussed on this page) involves the underlined characters in menu labels and other places. But the other meaning is "hotkey", e.g., some other keystroke that also invokes the function. On Windows, for example, one can paste text by typing Alt-E-P, or by typing Ctrl-V. It's the latter that gets displayed as part of the menu item when you use the "-accelerator" option.

Now, here's my issue. These hotkeys vary from platform to platform, and are reflected in the default text widget bindings. That's great--just create the text widget and you get the correct bindings. But setting -accelerator for the Edit menu items is a pain.

Just as you can say "bind .text <Paste> ..." to bind to the platform-specific Edit/Paste hotkey, you should be able to specify "-accelerator <Paste>" menu when defining the Edit/Paste menu item, and have that menu item display the right keystroke in the accepted form for that platform.


Posted at Aug 13/2003 04:33 PM:
DKF: I've got some code to handle that sort of thing somewhere. It works by pulling apart binding sequences (including virtual events) and constructing the accelerator text from that.


Posted at Jan 28/2004 10:22 PM:
amit nice

Forum Home  -  Site Home  -  Find Pages: