Page 1 of 2 12 LastLast
Results 1 to 10 of 18

Thread: Simple Java JNI Tutorial: Global Keypress Capture (Hotkey) using X11

  1. #1
    Join Date
    Oct 2005
    Beans
    80

    Simple Java JNI Tutorial: Global Keypress Capture (Hotkey) using X11

    This tutorial is for Java devs who want to be able to capture key presses globally under linux (specifically ubunutu.) This is of course OS dependent as Java does not allow for such a thing without the use of the JNI (Java Native Interface.)

    This tutorial is as self-contained as possible; however, if you would like more information about JNI a good place to start is the documentation at http://java.sun.com/developer/online...CBook/jni.html .

    The Problem

    The problem is that you would like your Java application to detect keys pressed by the user even if your Java application does not have focus. This is not a trivial problem, as I have learned. For security reasons or convenience, the developers of Java have omitted to give Java developers a painless way of capturing system-wide events such as key presses.

    The Solution

    Upon further investigation, most operating systems do offer developers a means of capturing such events. On windows, developers can use hooks to listen for global (input) events. On linux, developers can use the X11 library to capture gloabl input events. Unfortunately, both of these are not directly accessible through Java code (windows hooks might be.)

    Under linux, a developer can write his/her own code using C/C++ X11 libraries. This, of course, is only part of the solution, since then your Java application must be able to work in harmony with the C/C++ code. Fortunately, JNI allows just that.

    The Dirty Details

    Before we do anything, make sure that you have X11-dev libraries installed with

    Code:
    sudo apt-get install libx11-dev
    Let's start with the implementation. First we write a self-contained Java class as follows:
    Code:
    class KeyGrabber {
    
         static {
             System.loadLibrary("KeyGrabber");
         }
    
         private native void listen();
    
         public static void fire_key_event(){
    		System.out.println("key pressed (java code)");
    	}
    
         public static void main(String[] args) {
             new KeyGrabber().listen();
         }
     }
    In the Java code the listen() function is labeled native which tells the Java compiler that it is implemented in C/C++ code that it must link. The listen() function will be the X11 code required to listen for global key presses.

    The fire_key_event() function is completely implemented in Java. This function will be called from the native function listen() whenever the X11 code detects a global key press.

    Next we compile this Java code with:

    Code:
    javac KeyGrabber.java
    .

    To implement the listen() function in C/C++ we must now use JNI. We start by

    Code:
    javah -jni KeyGrabber
    which will generate a header file called KeyGrabber.h that looks like

    Code:
    /* DO NOT EDIT THIS FILE - it is machine generated */
    #include <jni.h>
    /* Header for class KeyGrabber */
    
    #ifndef _Included_KeyGrabber
    #define _Included_KeyGrabber
    #ifdef __cplusplus
    extern "C" {
    #endif
    /*
     * Class:     KeyGrabber
     * Method:    listen
     * Signature: ()V
     */
    JNIEXPORT void JNICALL Java_KeyGrabber_listen
      (JNIEnv *, jobject);
    
    #ifdef __cplusplus
    }
    #endif
    #endif
    Since we only had one native function in the Java code we only have one corresponding function prototype in the JNI header, namely:

    Code:
    JNIEXPORT void JNICALL Java_KeyGrabber_listen
      (JNIEnv *, jobject);
    We must implement this function according to this prototype. We can then implement the X11 code necessary to detect global key presses in KeyGrabber.c as follows.

    Code:
    #include <X11/Xlib.h>
    #include"KeyGrabber.h"
    #include<stdio.h>
    
    JNIEXPORT void JNICALL Java_KeyGrabber_listen
      (JNIEnv *env, jobject obj){
    
    printf("starting to listen to key F3 (native code)\n");
    jclass cls = (*env)->FindClass(env, "KeyGrabber");
    if(cls == NULL){
    	printf("cannot find class KeyGrabber\n") ;
    	exit(-1);
    }
    jmethodID mid = (*env)->GetStaticMethodID(env, cls, "fire_key_event", "()V" );
    if(mid ==0){
    	printf("cannot find method fun\n");
    	exit(-1);
    }
    
    Window root;
    XEvent ev;
        
    Display * dpy = XOpenDisplay(0);
    
    if(!dpy) return 1;
    
    root = DefaultRootWindow(dpy);
    
    
    char * key_string = "F3";
    
    KeyCode key = XKeysymToKeycode(dpy, XStringToKeysym(key_string));
    
    XGrabKey(dpy, key , AnyModifier, root,
        True, GrabModeAsync, GrabModeAsync);
    
    for(;;)
    {
    	XNextEvent(dpy, &ev);
    	if(ev.type == KeyPress && ev.xkey.keycode == key){
    		(*env)->CallStaticVoidMethod(env,cls,mid);
    	}
    }
    
    
    
    printf("leaving c code\n");
    
    return;
    
    }
    This code listens for any key press of F3 with any modifier key, and when it detects such a key, calls the Java function "fire_key_event".

    Here is a detailed break down of the sections of the code:

    Code:
    jclass cls = (*env)->FindClass(env, "KeyGrabber");
    The jclass class is defined in jni.h which is included when you include "KeyGrabber.h". The env pointer that is passed to every native function is essentially pointing to the JVM. You can manipulate anything that the JVM can manipulate.

    Our immediate goal is to be able to call the fire_key_event method in KeyGrabber class whenever the X11 code detects a global key press. The first order of business is to get a handle on the KeyGrabber class, which is what this line does.

    The next order of business is to get a handle on the fire_key_event method in the KeyGrabber class. That is what

    Code:
    jmethodID mid = (*env)->GetStaticMethodID(env, cls, "fire_key_event", "()V" );
    does.

    The first three parameters passed to GetStaticMethodID are clear: pointer to the environment (JVM), the class with the method, the name of the method. The last parameter is the signature of the function (this of course is required because of method overloading.) To get the signature of the desired method use the following command:

    Code:
    javap -s KeyGrabber
    which for this example gives:

    Code:
    Compiled from "KeyGrabber.java"
    class KeyGrabber extends java.lang.Object{
    KeyGrabber();
      Signature: ()V
    public static void fire_key_event();
      Signature: ()V
    public static void main(java.lang.String[]);
      Signature: ([Ljava/lang/String;)V
    static {};
      Signature: ()V
    }
    and then you can copy and paste the signature straight from terminal .


    The next bit of code

    Code:
    Window root;
    XEvent ev;
        
    Display * dpy = XOpenDisplay(0);
    
    if(!dpy) return 1;
    
    root = DefaultRootWindow(dpy);
    
    
    char * key_string = "F3";
    
    KeyCode key = XKeysymToKeycode(dpy, XStringToKeysym(key_string));
    
    XGrabKey(dpy, key , AnyModifier, root,
        True, GrabModeAsync, GrabModeAsync);
    is the X11 needed to capture global key presses (in this case only key presses of F3 with any modifier.) I won't go into too much detail, but essentially the XGrabKey function call tells X11 that your code is interested in capturing key press of the F3 key with any modifier on display 0 in the root window (meaning globally.) So the following code

    Code:
    for(;;)
    {
    	XNextEvent(dpy, &ev);
    	if(ev.type == KeyPress && ev.xkey.keycode == key){
    		(*env)->CallStaticVoidMethod(env,cls,mid);
    	}
    }
    is an infinite loop that listens to all the registered events (in this case only F3 key presses.) If the event type is a KeyPress and the keycode is F3 then the C code will use the env pointer to call the static void method fire_key_event in the KeyGrabber class.

    And its that straightforward (hopefully.)


    The last order of business is to compile everything and get it running.

    To compile the C code, it is a bit tricky. Provided you have a valid Java dev installation you can compile it with:

    Code:
    gcc --shared -o libKeyGrabber.so -I<path to java>/include -I<path to java>/include/linux -lX11 KeyGrabber.c <path to java>/jre/lib/i386/server/libjvm.so
    Then to run the Java you must make sure that Java can link to the libKeyGrabber.so library that you have just compiled, so use the following command

    Code:
    java -Djava.library.path=. KeyGrabber
    and everything should work! (eh hopefully.)


    I've probably missed a lot of things so I hope you won't hesitate to post any questions and comments!

  2. #2
    Join Date
    Sep 2008
    Location
    Germany
    Beans
    Hidden!
    Distro
    Ubuntu

    Re: Simple Java JNI Tutorial: Global Keypress Capture (Hotkey) using X11

    First of all, nice tutorial, very well written!

    Now here's a problem I ran into while trying to implement it: The call to XNextEvent is blocking, i.e. it stops the entire thread until the specified key is pressed. A real program however often cannot entirely freeze just to wait for the next event, therefore one would normally call KeyGrabber.listen() inside a separate thread.

    That's where the headache for me started: When the Java program is terminated, you have to stop the thread that contains the XNextEvent call. However, I just couldn't figure out how to do that. I tried calling XSendEvent from a different thread in order to unblock XNextEvent, but it somehow didn't work.

    Any hints that could solve this problem would be appreciated!

  3. #3
    Join Date
    Jun 2006
    Beans
    12

    Re: Simple Java JNI Tutorial: Global Keypress Capture (Hotkey) using X11

    Thank you for your tutorial, I used your guide and the inspiration of JIntellitype to create my own generic library for this.

    It's not feature complete but allows conversion of swing keycodes and modifiers to x11 ones.

    Also not all java keycodes have been mapped yet, as of i dont know much about some special keycodes java has.

    If somebody wants a library to start with and maybe improve a bit, visit the project website on sourceforge!

    http://jxgrabkey.sourceforge.net/

  4. #4
    Join Date
    Sep 2008
    Location
    Germany
    Beans
    Hidden!
    Distro
    Ubuntu

    Re: Simple Java JNI Tutorial: Global Keypress Capture (Hotkey) using X11

    @ Subes
    Your source code seems to contain some AWT / Swing stuff. Does it work on SWT, too?

    Because if it does, you just made my day.

  5. #5
    Join Date
    Jun 2006
    Beans
    12

    Re: Simple Java JNI Tutorial: Global Keypress Capture (Hotkey) using X11

    Havent tested it on swt and dont know much about swt
    The awt stuff im using is all about the keycodes and keymodifiers, it should work on any toolkit, as long as you give it the keycodes from the KeyEvent class.
    Tho the code is still not final and may have bugs, youre free do help in development
    If you need a KeyGrabber textfield, i have one implemented in Coopnet
    you can look there for hints about how to use JXGrabKey

    (http://coopnet.sourceforge.net)

    look in the packages coopnetclient.utils.hotkeys.JXGrabKeyHandler for the jxgrabkey implementation
    and coopnetclient.frames.components.KeyGrabberTextFiel d for the grabber

  6. #6
    Join Date
    Sep 2008
    Location
    Germany
    Beans
    Hidden!
    Distro
    Ubuntu

    Re: Simple Java JNI Tutorial: Global Keypress Capture (Hotkey) using X11

    I have downloaded the source code and skimmed through it, and I think it will work on SWT, too. Man, I just couldn't figure out the "XPending" part!
    Now there's only this one little thing I'd like to ask of you: Could you please change the license from GPL to LGPL? I have a EPL program over here, and, as we all know, GPL and LGPL are incompatible. Many thanks in advance!

  7. #7
    Join Date
    Jun 2006
    Beans
    12

    Re: Simple Java JNI Tutorial: Global Keypress Capture (Hotkey) using X11

    ah, right forgot about the restrictions gpl is pulling in ^^
    ill change it in the next few days

    #edit
    ok, changed the license
    do you wanna have commit rights to the code? ill make an invite for you
    Last edited by subes; October 15th, 2008 at 09:03 PM.

  8. #8
    Join Date
    Sep 2008
    Location
    Germany
    Beans
    Hidden!
    Distro
    Ubuntu

    Re: Simple Java JNI Tutorial: Global Keypress Capture (Hotkey) using X11

    Quote Originally Posted by qforce View Post
    and, as we all know, GPL and LGPL are incompatible. Many thanks in advance!
    err, I mean GPL and EPL are imcompatible.

    @subes
    Concerning the SVN write access: Right now I'm too busy working on my own projects, but I promise I'll send you all the bugs I may find.

  9. #9
    Join Date
    Jun 2006
    Beans
    12

    Re: Simple Java JNI Tutorial: Global Keypress Capture (Hotkey) using X11

    sure, thats ok too
    its best if you create tracker items for those

  10. #10
    Join Date
    Jan 2009
    Beans
    1

    Re: Simple Java JNI Tutorial: Global Keypress Capture (Hotkey) using X11

    Great tutorial!

    I was just wondering, how would one do this in OSX where there is no running X11 by default? Is there another library for keyboard capture? I could use SDL or something, but there must be a better way?

Page 1 of 2 12 LastLast

Bookmarks

Posting Permissions

  • You may not post new threads
  • You may not post replies
  • You may not post attachments
  • You may not edit your posts
  •