Version 10 of Disable autorepeat under X11

Updated 2007-12-01 12:25:12 by lars_h

**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 soem reaon you want/need DetectableAutorepeat to be set only for certain widgets or toplevels, you can always bind to <FocusIn> and <FocusOut>.