Home > Software design >  Tcl/Tk: menu bar accelerators triggered on French keyboards
Tcl/Tk: menu bar accelerators triggered on French keyboards

Time:11-12

This has been puzzling us for a while: In our Tcl/Tk application we make use of keyboard shortcuts for menu items to trigger certain actions.

Basically we set an -accelerator in the menu. According to the documentation, this is for "display only", so we also add some keyboard bindings to the same commands as the matching menus.

As we have a number of these shortcuts, simple Modifier-Key bindings (e.g. Control n ) are no longer enough, and we started using multiple modifiers (e.g. Control Shift n).

The code is something like this:


# case insensitive version 'bind' - in case the user pressed CapsLock
proc bind_capslock {tag seq_prefix seq_nocase script} {
    bind $tag <${seq_prefix}-[string tolower ${seq_nocase}]> $script
    bind $tag <${seq_prefix}-[string toupper ${seq_nocase}]> $script
}

if { [tk windowingsystem] == "aqua" } {
    set modifier "Mod1"
    set accelerator "Command"
} else {
    set modifier "Control"
    set accelerator $modifier
}

menu .menubar
. configure -menu .menubar
menu .menubar.edit
.menubar add cascade -menu .menubar.edit -label Edit

.menubar.edit add command -label "Test Me" \
    -command {puts "you menued $accelerator-Shift-N"} \
    -accelerator "${accelerator}-Shift-N"

bind_capslock all $modifier-Shift-Key N {puts "you pressed $modifier-Shift-N"}

Typically, the menu -command is identical to the bind script (it's different here to proof my point).

Now, the above code works nicely, on Linux, Windows and macOS:

  • if i navigate to the menu and select Test Me, it will print out "you menued Command-Shift-N"
  • if instead I press Cmd Shift n, it will print out "you pressed Command-Shift-N"

Hooray!

Unfortunately this seems to not work on macOS with a French keyboard layout:

  • if i navigate to the menu and select Test Me, it will print out "you menued Command-Shift-N" (Cool!)
  • if instead I press Cmd Shift n, it will print out both "you menued Command-Shift-N" and "you pressed Command-Shift-N"

So for whatever reason, the shortcut specified as the menu's -accelerator triggers, even though the documentation explicitly mentions that the -accelerator is only for display.

Since the commands/scripts invoked by both actions are really the same, we end up with a duplicate invocation of the command!

What are we doing wrong? Why is this behaviour triggered only on French keyboard layouts (and not on e.g. US or German layouts), and only on macOS (not on Linux nor Windows)? Most importantly: How can we fix this? (How can we do a quick fix? And what would a proper fix be?)

The problem appeared a while ago (Tcl/Tk 8.5) but it persists with Tcl/Tk-8.6.12. macOS is 10.15 (Catalina), but other versions are affected as well.

CodePudding user response:

The issue is that menus work differently on macOS to how Tk really expects. In particular, we use NSMenuItem and set a selector (i.e., the invoke callback handler) immediately. But if an accelerator is set, then the menu also captures the key sequence described by the accelerator before the event really reaches Tk, and then also delivers us the event; this is the native GUI behaviour, and can't really be fixed. (Tk on macOS already reaches way deeper into the guts of the native GUI stuff than is sane.)

The simplest thing is to not put those bindings in place on macOS.

Otherwise… postpone the event handling to an idle event handler. Like that, you can implement a simple first-event-handler-wins rule in a small amount of code. The event handler you specify runs slightly later, but that doesn't matter for anything relating to keyboard bindings or menu callbacks!

# Most of your code doesn't need to change

set ::currentMenuAction {}
proc scheduleMenuAction script {
    global currentMenuAction
    if {$currentMenuAction eq ""} {
        # Prepend a command to clear the variable
        set act "set ::currentMenuAction {};$script"
        set currentMenuAction [after idle $act]
    }
}

# I don't like using lots of backslashes; your mileage may vary
.menubar.edit add command -label "Test Me" -command {
    scheduleMenuAction {puts "you menued $accelerator-Shift-N"}
} -accelerator "${accelerator}-Shift-N"

bind_capslock all $modifier-Shift-Key N {
    scheduleMenuAction {puts "you pressed $modifier-Shift-N"}
}
  • Related