msgcat and shortcut keys

A/AK Anton Kovalenko: msgcat is useful for native-language support in applications, but there's a couple of things that GUI application writer must be aware of. Consider the following code snippet from tk/library/msgbox.tcl:

   set key [string index [$w.$name cget -text] $underIdx]
   bind $w <Alt-[string tolower $key]>  [list $w.$name invoke]
   bind $w <Alt-[string toupper $key]>  [list $w.$name invoke]

Oops. When button label is translated (with help of msgcat) and $key is non-ascii character, both string tolower and string toupper are not valid keysyms anymore.

When I faced this problem, I tried first to make a function that will return appropriate keysym for unicode character. Alas, there's no (straightforward) way to turn something like \\u430 into Cyrillic_a. Worse than that, I have found that if I press Alt-Cyrillic_a, it behaves differently on X11 and Windows (and incorrectly in both cases):

  • On X11, application gets <Alt-f> (cyrillic a is on <f> key on most russian keyboards)
  • On Windows, application gets <Alt-agrave>...

It all had driven me sad, and I left over this problem, until the new related problem was found.

Look at this (for not doing so):

    bind $w <Alt-c> [list tk::ButtonInvoke $data(cancelBtn)]
    bind $w <Alt-d> [list focus $data(dirMenuBtn)]

When labels "Directory" and "Cancel" are translated, the result is very bad. It's not so bad if the user just cannot use shortcut keys corresponding to letters he sees underlined in labels; but it's much worse if he will press a key that leads to wrong action.

To address both of this problems, I wrote some simple procedures (which you can see in the bottom of this page) to manage sets of accelerator keys.

Now, if I need to bind some shortcut keys to some widget in dialog window, I can use something like following:

    proc about_dialog {} {
        destroy .about
        set w [toplevel .about]
        set ok [button $w.ok -text [mc {Ok}] ]
        set can [button $w.cancel -text [mc {Cancel}]]
        tk::accel_set_create $w
        tk::accel_to_widget $w $ok
        tk::accel_to_widget $w $can
        bind $w <Alt-Key> [list tk::accel_dispatch $w %A]
        bind $w <Destroy> [list tk::accel_set_end $w]

In this example, "accel_set_create .about" starts the definition of new set of accelerator keys (further I'll call them accel-sets, to be short). "accel_to_widget $ok" selects appropriate shortcut for button $ok, scanning it's real (translated) label.

More examples follow:

    set lab [label $w.lab_phone -text [mc "Phone:"]]
    set ent [entry $ -textvariable phone]
    tk::accel_to_widget $w $lab [list focus $ent]

(Some character in $lab text is underlined, and we can use corresponding key to get to entry $ent)

    set filemenu [menu .m.file]
    tk::accel_set_create $filemenu
    foreach lab cmd {
        Open cm_open
        Save cm_save
        Exit cm_exit
    } {
        set mclab [mc $lab]
        $filemenu add command -label $mclab -command $cmd \
           -underline [tk::accel_set_add $filemenu $mclab junk]

In this last example, we use the notion of accel-sets only to get some letters unredlined in translated menu labels. This letters will be unique (in this menu, of course). Capital letters are selected, if it's possible.

This approach to binding shortcut keys is slightly more convenient than the traditional approach of other toolkits: when &Cancel is translated to &Annuler, and then translator has to find ambigious shortcut combinations to get rid of them (if he's kind enough, and not lazy).

Now I'm working on the patch for tk library scripts, to get correct behavior of translated file dialogs and message boxes. That's why my procedures are in tk namespace :-).

Vince - this looks very useful! I assume the patch you're working on would also be callable and useful from outside Tk's core (i.e. so any applications written in Tcl/Tk could make use of it...)?

Anton Kovalenko - to make it "officially" callable from outside tk's core, we have just to export this procedures into root namespace.

Now, this is the patch #563140 at Sourceforge (tktoolkit).

A/AK - Now I think that "ampersand magic" is better. All underline handling in Tk standard dialogs is now done with "magic ampersands".

    # accel.tcl --
    # This file provides unified way to bind appropriate shortcut keys
    # for buttons/menus with underlined labels. 
    # NOTE: underlined character in a label may be non-ascii!
    # As of now, there is only support of russian cyrillic non-ascii 
    # key bindings, 
    # but it will be easy to add other languages
    # support as well (where it makes sense). 

    # If application needs shortcut keys for some buttons, labels, 
    # menu positions or whatever, it can 

    # Returns [list of one] appropriate keysym for unicode character, or {}
    # if nothing is appropriate.

    # Starts new shortcut set definition

    proc tk::accel_set_create {setname} {
        variable accel_sets
        set accel_sets($setname) [list]

    # Given shortcut set and label, 
    # returns the underline_position
    # Character is selected as following:
    # (must not be already in this accel set)
    # - First, try uppercase ascii characters
    # - Next, try uppercase non-ascii characters
    # - Then try all other alphanumeric characters
    proc tk::accel_set_add {setname label command} {
        variable accel_sets
        if {[info exists accel_sets($setname,for:$label)]} {
            return $accel_sets($setname,for:$label)
        } else {
            if {$command == ""} {
                return -1
        set ulinepos -1
        set foundchar "-"
        set founduline -1
        foreach char [split $label {}] {
            incr ulinepos
            if {-1!=[string first [string toupper $char] $accel_sets($setname)]} {
            if {![string is alnum $char]} {
            if {[string is ascii $char] && [string is upper $char ]} {
                set foundchar $char
                set founduline $ulinepos
            if {[string is upper $char]} {
                if {![string is upper $foundchar]} {
                    set foundchar $char
                    set founduline $ulinepos
            } else {
                if {$founduline==-1} {
                    set foundchar $char
                    set founduline $ulinepos
        set accel_sets($setname,for:$label) $founduline
        if {$founduline != -1} {
            set accel_sets($setname,[string toupper $foundchar]) $command
            append accel_sets($setname) [string toupper $foundchar]
        return $founduline

    # This procedure supposed to be an event handler for <Alt-Key>.
    # The second argument muts be %A.
    proc tk::accel_dispatch {setname char} {
        set char [string toupper $char]
        variable accel_sets
        if {[info exists accel_sets($setname,$char)]} {
            uplevel \#0 $accel_sets($setname,$char)

    # Convenient procedure for adding shortcut key to widgets.
    # Automatically selects underline position and adds appropriate
    # handler to accel-set.
    proc tk::accel_to_widget {setname widget {command {}}} {
        if {$command == ""} {
            set command [list $widget invoke]
        $widget configure -underline \
            [ ::tk::accel_set_add $setname [$widget cget -text] $command]

    # When accel-set is no longer needed, we can free it.
    proc tk::accel_set_end {setname} {
        variable accel_sets
        array unset accel_sets $setname,*
        array unset accel_sets $setname