Portable keystate

ulis, 2002-10-05.

It is not so easy to deal with keystate (shift, control,...) in a portable manner.

Here is a portable method:

Each key press or key release of the control and shift keys is trapped, and its state saved in a global.


  set ::Control 0
  set ::Shift 0
  event add <<ControlOn>>   <KeyPress-Control_L>    <KeyPress-Control_R>
  event add <<ControlOff>>  <KeyRelease-Control_L>  <KeyRelease-Control_R>
  event add <<ShiftOn>>     <KeyPress-Shift_L>      <KeyPress-Shift_R>
  event add <<ShiftOff>>    <KeyRelease-Shift_L>    <KeyRelease-Shift_R>
  bind . <<ControlOn>>  \
  { 
    set ::Control 1
    .s.c config -text "control on" -fg red
    break 
  }
  bind . <<ControlOff>> \
  { 
    set ::Control 0
    .s.c config -text "control off" -fg black
    break 
  }
  bind . <<ShiftOn>>    \
  { 
    set ::Shift 1
    .s.s config -text "shift on" -fg red
    break 
  }
  bind . <<ShiftOff>>   \
  { 
    set ::Shift 0
    .s.s config -text "shift off" -fg black
    break 
  }
  bind . <KeyPress> \
  { 
    set key %K
    if {$::Shift}   { set key Shift-$key } 
    if {$::Control} { set key Ctrl-$key } 
    .k.k config -text $key
  }
  frame .s
  label .s.c -width 15 -text "control off" -bd 2 -relief ridge
  label .s.s -width 15 -text "shift off"   -bd 2 -relief ridge
  frame .k
  label .k.l -width 15 -text "last key"
  label .k.k -width 15 -bd 1 -relief sunken -bg white
  pack .s.c .s.s .k.l .k.k -padx 20 -pady 5
  pack .s .k -pady 10
  focus -force .
  raise .

Donald Arseneau The preceding doesn't handle multiple modifier keys held down at the same time. Nor does it work when a key is being held while the focus enters the application. Here is the increased bookkeeping and binding needed to make sense of those situations. But it still does not always work. Better would be for Tk to provide portable mnemonics for keystate, in addition to the numeric %s.


 #  Internal record of modifier-key states
 namespace eval KeyState {
    variable Control   0
    variable Control_L 0
    variable Control_R 0
    variable Alt     0
    variable Alt_L   0
    variable Alt_R   0
    variable Shift   0
    variable Shift_L 0
    variable Shift_R 0

    proc modPress {modifier RL} {
        puts "$modifier pressed"
        set KeyState::${modifier}_${RL} 1
        set KeyState::${modifier} 1
    }

    proc modRelease {modifier RL} {
        puts "$modifier released"
        set KeyState::${modifier}_${RL} 0
        set k [expr \
            { [set KeyState::${modifier}_R] || [set KeyState::${modifier}_L] } ]
        if { $k != [set KeyState::${modifier}] } {
            set KeyState::${modifier} $k
        }
    }

 }

 foreach {modifier RL} [list Control R Control L Shift R Shift L Alt R Alt L] {
    bind all <KeyPress-${modifier}_${RL}> [list + KeyState::modPress $modifier $RL]
    bind all <KeyRelease-${modifier}_${RL}> [list + KeyState::modRelease $modifier $RL]
 }


 # That is sufficient to keep track of the modifier keys while the application 
 # has focus, but what if a user presses, and holds, Shift while working in
 # some other application?  Now we bind all combinations of the <Enter> event
 # (that's the mouse cursor entering a window, not the Enter key) to update the
 # status of modifier keys when the user returns attention to our application.
 #
 # We will only set the key-state variables when they actually *change*.
 # This means variable traces won't be firing every time the mouse moves
 # between widgets.
 #
 # We will bind to "all", but you may want to set the bindings for a particular
 # toplevel window or even a specific widget.  Note: these bindings are in
 # addition to previously existing bindings.
 #
 # These combinations could be done in an obscure loop, but let's be explicit.

 namespace eval KeyState {
    proc modEnterOneKey { key state } {
        if { [set KeyState::$key] != $state } {
            set KeyState::$key $state
        }
        if { $state == 0 } {
            set KeyState::${key}_L 0
            set KeyState::${key}_R 0
        }
    }

    proc modEnter { c s a } {
        modEnterOneKey Control $c
        modEnterOneKey Shift   $s
        modEnterOneKey Alt     $a
    }

    namespace export *
 }

 bind all <Control-Shift-Alt-Enter> {+KeyState::modEnter 1 1 1 }
 bind all <Control-Shift-Enter> {+KeyState::modEnter 1 1 0 }
 bind all <Control-Alt-Enter> {+KeyState::modEnter 1 0 1 }
 bind all <Shift-Alt-Enter> {+KeyState::modEnter 0 1 1 }
 bind all <Control-Enter> {+KeyState::modEnter 1 0 0 }
 bind all <Shift-Enter> {+KeyState::modEnter 0 1 0 }
 bind all <Alt-Enter> {+KeyState::modEnter 0 0 1 }
 bind all <Enter> {+KeyState::modEnter 0 0 0 }

 ##############################################################################
 #
 #  Demonstration:
 #  Control-Shift-Alt modifiers displayed by direct association (-variable and
 #  -textvariable) and by variable traces
 #
 ##############################################################################

 trace variable KeyState::Control w {showState .cl}
 trace variable KeyState::Shift   w {showState .sl}
 trace variable KeyState::Alt     w {showState .al}

 proc showState { win var nil op } {
    puts "Keystate for $var is now [set $var]"
    if { [set $var] } {
        $win configure -text "Pressed"
    } else {
        $win configure -text "Released"
    }
 }

 checkbutton .cc -justify left -variable KeyState::Control -text "Control"
 label .cl -width 12 -justify left -text "Released" -bd 2 -relief sunken
 label .cv -width 3 -textvariable KeyState::Control -bd 2 -relief sunken

 checkbutton .sc -justify left -variable KeyState::Shift -text "Shift"
 label .sl -width 12 -justify left -text "Released" -bd 2 -relief sunken
 label .sv -width 3 -textvariable KeyState::Shift -bd 2 -relief sunken

 checkbutton .ac -justify left -variable KeyState::Alt -text "Alt"
 label .al -width 12 -justify left -text "Released" -bd 2 -relief sunken
 label .av -width 3 -textvariable KeyState::Alt -bd 2 -relief sunken

 grid .cc -in .  -row 1 -column 1 -sticky w
 grid .cl -in .  -row 1 -column 2
 grid .cv -in .  -row 1 -column 3
 grid .sc -in .  -row 2 -column 1 -sticky w
 grid .sl -in .  -row 2 -column 2
 grid .sv -in .  -row 2 -column 3
 grid .ac -in .  -row 3 -column 1 -sticky w
 grid .al -in .  -row 3 -column 2
 grid .av -in .  -row 3 -column 3

JH: While you can capture the keypresses and store that state, I have found the %s binding field to be reliable at least for Shift, Control and Caps_Lock (the last however not via VNC).

Test this out:

 destroy .t
 toplevel .t
 bind .t <Key> [list keystate %A %K %N %k %s]

 proc keystate {A K N k s} {
     set bits [list]
     foreach {bit id} {
       1024 "1024-bit RMB"
       512 "512-bit MMB"
       256 "256-bit LMB"
       128 "128-bit ??"
       64 "64-bit ??"
       32 SCROLL_LOCK(WIN)
       16 "16-bit ??"
       8 NUM_LOCK
       4 CONTROL
       2 CAPS_LOCK
       1 SHIFT
     } {
         if {$s & $bit} {
             lappend bits $id
         }
     }

     puts [list KEY $A SYM $K $N CODE $k STATE $s ($bits)]
 }

I should add to the above that I found the 16-bit to always trigger on Linux using the OS X keyboard ... but I didn't have any indication of what it was. Also, the 64-bit was triggering for the OS X Command key, and the option/alt key was triggering NUM_LOCK.


male - 2005-11-01:

After a research on comp.lang.tcl I found an articel [1 ] dealing with modifier detection.

I took this source, changed it a bit and added the support for the Alt modifier (on MS Windows, probably on other platforms too):

 namespace eval ::tk {
   namespace eval keyModifiers {
     # array of bit masks to recognize the modifers:
     # - shift - mod5 masks taken from .../tcl/include/X11/X.h
     # - alt mask defined by analysing the status field of Alt-KeyPress
     #   (analysed on MS Windows)
     #
     array set masks [list \
       shift   [list [expr {1 <<  0}] "Shift"] \
       lock    [list [expr {1 <<  1}] "Lock"] \
       control [list [expr {1 <<  2}] "Control"] \
       mod1    [list [expr {1 <<  3}] "Mod1"] \
       mod2    [list [expr {1 <<  4}] "Mod2"] \
       mod3    [list [expr {1 <<  5}] "Mod3"] \
       mod4    [list [expr {1 <<  6}] "Mod4"] \
       mod5    [list [expr {1 <<  7}] "Mod5"] \
       alt     [list [expr {1 << 17}] "Alt"] \
     ];

     # MS Windows modifier name map:
     # - Mod1 is identical to "Num"-lock key
     # - Mod3 is identical to "Scroll"-lock key
     #
     set maps [list \
       "Mod1" "Num" \
       "Mod3" "Scroll" \
     ];

     proc keyModifiers {state {mapToRealName 0}} {
       variable masks;
       variable maps;

       set modifiers [list];

       foreach mask [array names masks] {
         lassign $masks($mask) bits label;

         if {$state & $bits} {
           lappend modifiers $label;
         }
       }

       set modifiers [join $modifiers "-"];

       if {$mapToRealName == 1} {
         set modifiers [string map $maps $modifiers];
       }

       return $modifiers;
     }

      namespace export -clear keyModifiers;
   }

   namespace import -force keyModifiers::keyModifiers;
 }

IDG The 'probably other platforms' doesn't apply well to the Alt modifier. On the machine I am sitting at, left Alt key gives %s=8, right Alt key gives 128, and event generate <Alt-A> gives 131072. Yecch!


MHo 2010-03-10: Still it's unclear to me how to simply detect the state of one of the shift keys (Shift, Alt, etc.) without waiting for an event first. As mentioned above, there are situations when a program is entered and the key is already pressed and the app have to know this.

Lars H: When a program "is entered", it usually receives an event telling it about this (e.g. some window getting focus), does it not?

MHo Shure, but this won't help telling, if such a key is pressed or not, or does it? I mean something like: if the [Alt]-Key is pressed, while program xyz is started, the program xyz should be able to notice this. A simple sort of keystat or instat function; not event based, but for polling.

Lars H: According to the bind manpage, Enter and Leave events provide the same %s data as e.g. KeyPress events. That's what the code above is looking at! It's another matter that polling may be a more intuitive interface for this information, but do the underlying system (e.g. Xlib) provide such an interface?

MHo Binding to the Enter-Event leads one step forward, but if the user starts the application without using a mouse or with the mouse pointer staying elsewhere, the application gets no Enter-Event, automatically. Even event generate . <Enter> at the beginning of the program won't help here.