Enabling the full Neo 2 Keyboard Layout in AWT/Swing Java Apps

Saturday, 20.11.2010, 14:42

This is a description of a hack that allows the somewhat esoteric but highly useful Neo 2 Keyboard Layout to be fully used with AWT/Swing based JAVA Applications


UPDATE: Another Neo user told me that in order for this hack to work you have to make sure that AWT uses the XToolkit. (There are two underlying toolkits AWT uses on Linux and Solaris to map from X11 to AWT, the XToolkit and the MToolkit, more info here: http://download.oracle.com/javase/1.5.0/docs/guide/awt/1.5/xawt.html). So if the hack does not work for you, try to set the following environment variable for the app you apply the hack to:

export AWT_TOOLKIT=XToolkit

Thanks to karottenreibe for the info & the missing keycodes!


I am using a different keyboard layout from the standard German qwertz: Neo 2. (A short english summary of the layout can be found here) A big plus of this layout is that all the special characters one needs for programming are readily available, which makes touch typing when coding much, much easier. I can keep my hands in the base position all the time. This is normally a major problem for programmers using a German keyboard layout, as all braces, brackets, etc. are on the top row of the keyboard, some only accessible by alt gr modifier key. Some German programmers use a US layout when writing code, but I hate switching layouts all the time and the basic qwertz/qwerty layout is broken by design anyway. This is why about two years ago I completely switched to using the Neo 2 layout, and have never regretted it since.

(Anybody doubting the importance of being able to type smooth and fast for programmers should really read this blog post.)

Neo 2 has another cool feature: all the arrow keys are available on the main keyboard as well (in the WASD position well known to ego shooter players) when pressing an additional modifier key. That is especially useful when using stuff like code completion while coding. As you type, you choose from the suggestions with the arrow keys and continue typing, all while keeping your hands in the same position, without the need to move your right hand to the cursor block. It does not make you that much faster, but you stay more focused. You do not think about typing, coding just feels a lot smoother that way.

JAVA Problems

There is however an annoying problem with Neo2: the arrow key mapping for the WASD keys is for some reason broken in Swing based java programs on Linux. This is not a major problem for most people, but I need to switch to using IntelliJ for my Scala coding, as the ScalaIDE for eclipse IMHO still is not remotely usable. IntelliJ is affected by said bug and when I started using IntelliJ, it felt like I had one of my hands tied to my back. Not being able to use the cursor the way I liked and having to move my hands around the keyboard all the time really broke my flow. Something had to be done.

The original ticket claimed only Swing apps were affected, and indeed eclipse (also written in JAVA but being SWT based) always worked fine with my freaky layout. Knowing that Swing is actually built atop of AWT, I however doubted the diagnosis that Swing was the culprit and first wrote a little debugging program using AWT and behold: my hunch was right, the AWT app had the same problem. I now started digging deeper till I finally came close to the root cause of the problem:

All X-Server keycodes are at some point mapped to Java Keycodes. This happens in sun.awt.X11.XKeysym, more specifically the method:

static Keysym2JavaKeycode getJavaKeycode( XKeyEvent ev ) 

Here we find the following code:

// we only need primary-layer keysym to derive a java keycode.
ndx = 0;
keysym = xkeycode2keysym(ev, ndx);

Building a Workaround

I am not 100% sure what goes wrong there, I think it may have to do with the fact that the passed index (ndx) is always zero and Neo 2 uses multiple layers. To investigate further at this point I would need to find out what “layers” are in respect to keysyms/the X-Server and whether it really maps to what Neo 2 calls layers. There is however a rather nasty but working way out to get the missing keys working for Neo users. The XKeyEvent passed to this mapping function has the 10th bit in its state set when the Neo 2 Modifier for the additional arrow keys is pressed. That way, we can determine if the special layer with the arrow keys is active. Luckily the OpenJDK is open source, so I simply took the OpenJDK implementation of XKeysym class and added this really nasty, disgusting, appalling but working hack:

//insert this after the normal keysym mapping in getJavaKeycode: 
if ((ev.get_state() & 0x20) != 0  && //neo 2 layer 4 keypress 
    neo2level4Hacks.containsKey(keysym))//it is one of the keys we want to remap
{
    keysym = neo2level4Hacks.get(keysym);
    //System.out.println("Applied Neo2 hack, new keysym: 0x" + Long.toHexString(keysym));
}

I also added a hash table with the remapped keys:

//add somewhere in the body of the XKeysym class
static final Hashtable<Long, Long> neo2level4Hacks = new Hashtable<Long,Long>(50);
static {
neo2level4Hacks.put(XKeySymConstants.XK_i, XKeySymConstants.XK_Left);
neo2level4Hacks.put(XKeySymConstants.XK_e, XKeySymConstants.XK_Right);
neo2level4Hacks.put(XKeySymConstants.XK_l, XKeySymConstants.XK_Up);
neo2level4Hacks.put(XKeySymConstants.XK_a, XKeySymConstants.XK_Down);
neo2level4Hacks.put(XKeySymConstants.XK_v, XKeySymConstants.XK_BackSpace);
neo2level4Hacks.put(XKeySymConstants.XK_c, XKeySymConstants.XK_Delete);
neo2level4Hacks.put(XKeySymConstants.XK_u, XKeySymConstants.XK_Home);
neo2level4Hacks.put(XKeySymConstants.XK_o, XKeySymConstants.XK_End);
neo2level4Hacks.put(XKeySymConstants.XK_p, XKeySymConstants.XK_Return);
neo2level4Hacks.put(XKeySymConstants.XK_odiaeresis, XKeySymConstants.XK_Tab);
neo2level4Hacks.put(XKeySymConstants.XK_8, XKeySymConstants.XK_Tab);
neo2level4Hacks.put(XKeySymConstants.XK_z, XKeySymConstants.XK_Undo);
neo2level4Hacks.put(XKeySymConstants.XK_udiaeresis, XKeySymConstants.XK_Escape);
neo2level4Hacks.put(XKeySymConstants.XK_adiaeresis, XKeySymConstants.XK_Insert);
neo2level4Hacks.put(XKeySymConstants.XK_w, XKeySymConstants.XK_Page_Down);
neo2level4Hacks.put(XKeySymConstants.XK_x, XKeySymConstants.XK_Page_Up);
neo2level4Hacks.put(XKeySymConstants.XK_space, XKeySymConstants.XK_0);
neo2level4Hacks.put(XKeySymConstants.XK_m, XKeySymConstants.XK_1);
neo2level4Hacks.put(XKeySymConstants.XK_comma, XKeySymConstants.XK_2);
neo2level4Hacks.put(XKeySymConstants.XK_period, XKeySymConstants.XK_3);
neo2level4Hacks.put(XKeySymConstants.XK_n, XKeySymConstants.XK_4);
neo2level4Hacks.put(XKeySymConstants.XK_r, XKeySymConstants.XK_5);
neo2level4Hacks.put(XKeySymConstants.XK_t, XKeySymConstants.XK_6);
neo2level4Hacks.put(XKeySymConstants.XK_h, XKeySymConstants.XK_7);
neo2level4Hacks.put(XKeySymConstants.XK_g, XKeySymConstants.XK_8);
neo2level4Hacks.put(XKeySymConstants.XK_f, XKeySymConstants.XK_9);
neo2level4Hacks.put(XKeySymConstants.XK_q, XKeySymConstants.XK_plus);
neo2level4Hacks.put(XKeySymConstants.XK_ssharp, XKeySymConstants.XK_minus);
neo2level4Hacks.put(XKeySymConstants.XK_0, XKeySymConstants.XK_asterisk);
neo2level4Hacks.put(XKeySymConstants.XK_9, XKeySymConstants.XK_slash);
neo2level4Hacks.put(XKeySymConstants.XK_d, XKeySymConstants.XK_comma);
neo2level4Hacks.put(XKeySymConstants.XK_y, XKeySymConstants.XK_period);
neo2level4Hacks.put(XKeySymConstants.XK_j, XKeySymConstants.XK_semicolon);
neo2level4Hacks.put(XKeySymConstants.XK_b, XKeySymConstants.XK_colon);
neo2level4Hacks.put(XKeySymConstants.XK_s, XKeySymConstants.XK_questiondown);
neo2level4Hacks.put(XKeySymConstants.XK_k, XKeySymConstants.XK_exclamdown);
}

This causes the mapping to emit “ordinary” Java left/right/up/down/etc. events that work like you had pressed the normal cursor keys. This basically is a hardcoded mapping of part of the Neo Layout into the XServer-to-AWT translation. Now we only need to replace the normal XKeysym class with our hacked version.

Enabling the Hack in an Application

I came across an article that described how to patch built-in JAVA classes. The key info in that article is the -bootclasspath/p command line switch that allows to add classes to the boot classpath before the standard library classes. Now we can include our patched version of the XKeysym before the normal implementation which causes the hack to be active. So all that is necessary is to add the hack to your JAVA programs is to download the jar (see below), put it somewhere on your local machine and add the following to the invocation of your java app :

-Xbootclasspath/p:/your/path/to/the/jar/neo2-awt-hack-0.2.jar

So if your app was started like this:

java -cp foo.jar com.example.Foo

start it like this instead:

java -Xbootclasspath/p:/your/path/to/the/jar/neo2-awt-hack-0.2.jar -cp foo.jar com.example.Foo

Many larger JAVA apps have a config file where you can put command like switches, e.g. more memory for the virtual machine. IntelliJ for example puts those switches in a file called idea.vmoptions in the bin folder. To enable the hack in IntelliJ simply add the bootclasspath switch there.

The patched version in the jar is based on the source of the OpenJDK Build b20 and works fine with that version. It also seems to work with the “official” Sun/Oracle Java 6 (at least the one for Ubuntu 10.04). While of course I never take any responsibility for the fitness of the code I publish here, I want to explicitly state again that this is highly experimental, hackish code that could have unknown side effects. Use it at your own risk!

You can download the binary here:

http://maven.henkelmann.eu/eu/henkelmann/neo2-awt-hack/0.2/neo2-awt-hack-0.2.jar

You can check out the source code from the git repo like so:

git clone http://git.henkelmann.eu/neo2-awt-hack.git

As the code is a modified part of the OpenJDK, it is published under the GPL, Version 2 (see the file named LICENSE in the source release).