Version 13 of Disable autorepeat under X11

Updated 2013-07-05 10:50:26 by jkb

**Purpose:** describe how to distinguish between "true" Key events and autorepeated ones under X11

Introduction

Autorepeat is a functionality of the windowing system that generates multiple Key events when a Key is held down. What events a Tk application gets in that case depends on the windowing system. By default, under X11 an autorepeat sequence looks like this:

P----------^v-----^v-----^v-----R

P: Physical KeyPress (I'll call it true Keypress from now on)
R: Physical KeyRelease (true KeyRelease)
v: KeyPress generated from autorepeat (false KeyPress)
^: KeyRelease generated from autorepeat (false KeyRelease)
-: Time passing

Under Windows an autorepeat sequence looks like this:

P----------v-----v-----v-----R

For Mac, I don't know. If anyone does, pleas fill in!

While usually autorepeated Key events are just fine, in some cases one might be interested only in true Key events (e.g. games). Under Windows it's easy: act on the first KeyPress you get and ignore all that follow, until a KeyRelease occurs. Under X11 it's more tricky, because you always get two events, a Release and a Press. Since I spent many hours trying to find the best way to distinguish between true and false Key events under X (and bothering people on various mailing listst...), I put the results of my "research" into this page. Several Approaches will be discussed, chose the one that suits best your needs.


The simple one: disable autorepeat with xset

The easiest way is to disable autorepeat with xset, an utility to set X user preferences. Once autorepeat is disabled, you will get one KeyPress event when the User presses the key, and one KeyRelease when he releses it. This change affects all clients of the X server, so you'd best [bind] it to <FocusIn> and <FocusOut> events, so other widgets and applications still behave as they should:

bind $w <FocusIn> {toggleAutorepeat off}
bind $w <FocusOut> {toggleAutorepeat on}

proc toggleAutorepeat {state} {
        
        exec xset r $state
}

This way the setting affects all keys, however you can enable/disable autorepeat only for specific keys (see the manpage of xset [L1 ]). That utility is also available as a Tk package: Xop [L2 ].

The problem of this apporach is that if your program crashes or gets killed, autorepeat might stay turned off, which results in abnormal behaviour in other applications. So either write BugFree(TM) code, live with that risk, or take a look at the other possibilities.


More complex, but safer: compare serial numbers

This approach relies on the fact that autorepeated KeyRelease/KeyPress event pairs have equal serial numbers, and uses them to distinguish true from false events. The 'serial' field can be accessed using the '%#' subtitution in the bind script.

To check wether a KeyPress is a true one, is pretty simple: you could store the serial number of the last KeyRelease event in a global variable and compare it with the serial of the KeyPress that just occured - if they differ it's a true one. However I find it simpler to have a "boolean" that indicates if a KeyPress is the first one. Initially it has to be set to 1, set it to 0 whenever you detect a true Keypress, set it again to 1 on true KeyRelease. Anyway, both solutions work fine.

Yes, but how to detect if a KeyRelease event is a true one? You can't compare serial numbers immediately, since in case of a false KeyRelease the related KeyPress has still to occur. You can't wait for the next KeyPress either, since in case of a true KeyRelease (the user releases the key) you won't detect it until a key gets pressed again. The solution is to delay the comparison of the serial numbers for some millisecs, so in case of false release the matching KeyPress has time to put its serial into a global variable.

That's what it could look like:

global firstPress pressSerial delay
set firstPress 1
set pressSerial -1
set delay 10

bind $w <KeyPress> {keypressed %K %#}
bind $w <KeyRelease> {keyreleased %K %#}

proc keypressed {keycode serial} {        
        global firstPress pressSerial

        if {$firstPress} {
                    #do things you want to happen on true KeyPress...
                set firstPress 0
        }
    set pressSerial $serial
}

proc keyreleased {keycode serial} {
        global delay

            after $delay "if {$serial != \$pressSerial} {
                        #do things you want to happen on true KeyRelease
                        set firstPress 1
                    }"
}

The problem here is what delay to choose for the [after ms] script. It must be less than the repeat rate of the X server (30 ms on my system, use 'xset q' to check it out) otherwise, when the server is sending autorepeated events, a second KeyPress event might have time to arrive before the after script is run and the serial numbers will no longer match - the release event will incorrectly be considered as a true one. But the delay shouldn't be too short either: since - as I was told - false KeyRelease/KeyPress event pairs aren't put in the X event queue simultaneously, with a very short delay the comparison of serials might happen before the KeyPress event arrives - resulting in wrong behaviour again.

I don't have a clue what delay is "the best". I made some tests, and it turned out that with values just a few millisecs greathers than the repeat rate it no longer detects the events correctly. On the other hand, even with [set delay 0] it still works - it seems that under normal conditions the KeyPress event "arrives" before the after command adds his callback. But this probably wont alway be the case, so a delay of 0 ms isn't the safe choice...


The safest way: do it in C

This is, in my opinion, the cleanest solution: write a little extention in C that makes use of the XkbSetDetectableAutoRepeat() [L3 ] from the X11 library. With this function a client can ask the server to get a detectable autorepeat sequence i.e. a "Windows-style" one:

press---press---press---press---release

This makes it very easy to detect the true Key events. The advantage of this approach is that the change applies only to the client that makes the request (usually one application). So even if your program crashes, the others aren't touched.

That's the C code to implement it:

#include <tk.h>
#include <X11/Xutil.h>
#include <X11/Xlib.h>
#include <string.h>
#include <tclInt.h>
#include <stdbool.h>

Bool toggleDetectableAutorepeat (Tcl_Interp*, Bool enable);
Display * getDisplay (Tcl_Interp * interp);
int tdar_Cmd(ClientData cdata, Tcl_Interp  *interp, int objc,  Tcl_Obj * CONST objv[]);


//Bool tkewa_running = false;

int tdar_Cmd(ClientData cdata, Tcl_Interp  *interp, int objc,  Tcl_Obj * CONST objv[]) {
          int ret;
         if (objc != 2) {
                 Tcl_SetObjResult(interp, Tcl_NewStringObj("Wrong argc", -1));
                return TCL_ERROR;
         }
         int len = 0;
         char * arg;
         arg = Tcl_GetString(objv[1]);
         if ( strcmp(arg, "start") == 0) {
                 ret = toggleDetectableAutorepeat(interp, true);
         } else if (strcmp(arg, "stop") == 0) {
                 ret = toggleDetectableAutorepeat(interp, false);
         } else {
                 Tcl_SetObjResult(interp, Tcl_NewStringObj("Wrong arg: should be \"w start|stop\"", -1));
                 ret = TCL_ERROR;
         }
         return ret;
}

int Tdar_Init(Tcl_Interp *interp)  {
        if (Tcl_InitStubs(interp, TCL_VERSION, 0) == NULL) {
                Tcl_SetObjResult(interp, Tcl_NewStringObj("Failed to init TclStubs", -1)); 
                 return TCL_ERROR;
        }
        if (Tk_InitStubs(interp, TK_VERSION, 0) == NULL) {
                Tcl_SetObjResult(interp, Tcl_NewStringObj("Failed to init TkStubs", -1)); 
                 return TCL_ERROR;
        }
        Tcl_CreateObjCommand(interp,  "tdar", (Tcl_ObjCmdProc *)tdar_Cmd, NULL, NULL);
        return TCL_OK;
}

Display * getDisplay (Tcl_Interp * interp) {
        Display * display;
        display = Tk_Display(Tk_MainWindow(interp));
        return display;
}
int toggleDetectableAutorepeat (Tcl_Interp* interp, Bool enable) {
        Display * display;
        display = getDisplay(interp);
        if(display == NULL) {
                Tcl_SetObjResult(interp, Tcl_NewStringObj("Error while getting display connection to X server", -1));
                return TCL_ERROR;
        }
        Bool supported;
        Bool result = XkbSetDetectableAutoRepeat(display, enable, &supported);
        XFlush(display);
        if(!supported || enable != result) {
                Tcl_SetObjResult(interp, Tcl_NewStringObj("Could not set detectable autorepeat", -1));
                return TCL_ERROR;
        } else {
                return TCL_OK;
        }
}

Compile and load!

Note that the DetectableAutoRepeat setting applies to all widgets and toplevels of the application that changed it, however this should not cause problems: autorepeat still works and usually where that functionality is used, only KeyPress events are considered. If for some reaon you want/need DetectableAutorepeat to be set only for certain widgets or toplevels, you can always bind to <FocusIn> and <FocusOut>.


Consuming auto-repeat keys based on idle/busy status

In an attempt to solve this problem via a different route, I experimented with throwing away auto-repeat generated key presses that we have insufficient time to process.

If you're happy to accept the possibility of scheduling other "after idle" events in between your auto-repeat keys then we can use the idle event loop as a means to detect whether we are busy, and the %t time stamp on Release/Press events to distinguish between auto-repeated keys and manually pressed keys. The two together allow us to consume auto-repeat generated key presses and prevent "run-on" in an application that takes some time to process a specific key press.

Bindtags allows us to arbitrarily fix this for any window. As an example:

namespace eval ::auto_repeat {
    set release_time 0
    set key_idle 1

    # Key press. Consume any auto-repeat keys (detected as simultaneous
    # KeyRelease and KeyPress events) when we're busy so we don't
    # start accumulating a large queue.
    proc AutoRepeatPress {k t} {
        variable release_time
        variable key_idle

        if {$t == $release_time && !$key_idle} {
            return -code break;
        }

        set key_idle 0
        after idle {set ::auto_repeat::key_idle 1}
    }

    # Key release. If we've had an idle event loop process since
    # the last press then we're safe to assume auto-repeat is not
    # swamping the application
    proc AutoRepeatRelease {k t} {
        variable release_time
        variable key_idle

        if {$key_idle} {
            set release_time 0
        } else {
            set release_time $t
        }
    }

    bind AutoRepeat <Any-KeyPress>   {::auto_repeat::AutoRepeatPress   %K %t}
    bind AutoRepeat <Any-KeyRelease> {::auto_repeat::AutoRepeatRelease %K %t}
}


# Applies a correction to window $w to remove excess auto-repeated key
# events if processing is failing to keep up.
proc AutoRepeatCorrect {w} {
    bindtags $w [linsert [bindtags $w] 0 AutoRepeat]
}

pack [label .laggy -text "Laggy" -width 20 -height 10 -bg red] -fill both
bind .laggy <Key-a> {puts a; exec sleep 0.2}
bind .laggy <Key-b> {puts b; exec sleep 1}
bind .laggy <Key-c> {puts c; exec sleep 2}
bind .laggy <Any-Enter> {focus %W}

pack [label .fixed -text "Fixed" -width 20 -height 10 -bg blue] -fill both
bind .fixed <Key-a> {puts A; exec sleep 0.2}
bind .fixed <Key-b> {puts B; exec sleep 1}
bind .fixed <Key-c> {puts C; exec sleep 2}
bind .fixed <Any-Enter> {focus %W}

AutoRepeatCorrect .fixed

Clearly this relies on the Press/Release semantics of Unix and would not apply to Windows event methods.