Global hotkeys with vala

The issue

Whilst working on Diodon I needed to implement a global hotkey. After browsing the web and some source code I came up with the following solution.

Get the code

Here is a sample class to easily bind a delegate with a global hotkey.

/**
 * This class is in charge to grab keybindings on the X11 display
 * and filter X11-events and passing on such events to the registed
 * handler methods.
 * 
 * @author Oliver Sauder 
 */
public class KeybindingManager : GLib.Object
{
    /**
     * list of binded keybindings
     */
    private Gee.List bindings = new Gee.ArrayList();
    
    /**
     * locked modifiers used to grab all keys whatever lock key
     * is pressed.
     */
    private static uint[] lock_modifiers = {
        0,
        Gdk.ModifierType.MOD2_MASK, // NUM_LOCK
        Gdk.ModifierType.LOCK_MASK, // CAPS_LOCK
        Gdk.ModifierType.MOD5_MASK, // SCROLL_LOCK
        Gdk.ModifierType.MOD2_MASK|Gdk.ModifierType.LOCK_MASK,
        Gdk.ModifierType.MOD2_MASK|Gdk.ModifierType.MOD5_MASK,
        Gdk.ModifierType.LOCK_MASK|Gdk.ModifierType.MOD5_MASK,
        Gdk.ModifierType.MOD2_MASK|Gdk.ModifierType.LOCK_MASK|Gdk.ModifierType.MOD5_MASK
    };
    
    /**
     * Helper class to store keybinding
     */
    private class Keybinding
    {
        public Keybinding(string accelerator, int keycode,
            Gdk.ModifierType modifiers, KeybindingHandlerFunc handler)
        {
            this.accelerator = accelerator;
            this.keycode = keycode;
            this.modifiers = modifiers;
            this.handler = handler;
        }
    
        public string accelerator { get; set; }
        public int keycode { get; set; }
        public Gdk.ModifierType modifiers { get; set; }
        public KeybindingHandlerFunc handler { get; set; }
    }
    
    /**
     * Keybinding func needed to bind key to handler
     * 
     * @param event passing on gdk event
     */
    public delegate void KeybindingHandlerFunc(Gdk.Event event);

    public KeybindingManager()
    {
        // init filter to retrieve X.Events
        Gdk.Window rootwin = Gdk.get_default_root_window();
        if(rootwin != null) {
            rootwin.add_filter(event_filter);
        }
    }
    
    /**
     * Bind accelerator to given handler
     *
     * @param accelerator accelerator parsable by Gtk.accelerator_parse
     * @param handler handler called when given accelerator is pressed
     */
    public void bind(string accelerator, KeybindingHandlerFunc handler)
    {
        debug("Binding key " + accelerator);
        
        // convert accelerator
        uint keysym;
        Gdk.ModifierType modifiers;
        Gtk.accelerator_parse(accelerator, out keysym, out modifiers);

        Gdk.Window rootwin = Gdk.get_default_root_window();     
        X.Display display = Gdk.x11_drawable_get_xdisplay(rootwin);
        X.ID xid = Gdk.x11_drawable_get_xid(rootwin);
        int keycode = display.keysym_to_keycode(keysym);            
                 
        if(keycode != 0) {
            // trap XErrors to avoid closing of application
            // even when grabing of key fails
            Gdk.error_trap_push();

            // grab key finally
            // also grab all keys which are combined with a lock key such NumLock
            foreach(uint lock_modifier in lock_modifiers) {     
                display.grab_key(keycode, modifiers|lock_modifier, xid, false,
                    X.GrabMode.Async, X.GrabMode.Async);
            }
            
            // wait until all X request have been processed
            Gdk.flush();
            
            // store binding
            Keybinding binding = new Keybinding(accelerator, keycode, modifiers, handler);
            bindings.add(binding);
            
            debug("Successfully binded key " + accelerator);
        }
    }
    
    /**
     * Unbind given accelerator.
     *
     * @param accelerator accelerator parsable by Gtk.accelerator_parse
     */
    public void unbind(string accelerator)
    {
        debug("Unbinding key " + accelerator);
        
        Gdk.Window rootwin = Gdk.get_default_root_window();     
        X.Display display = Gdk.x11_drawable_get_xdisplay(rootwin);
        X.ID xid = Gdk.x11_drawable_get_xid(rootwin);
        
        // unbind all keys with given accelerator
        Gee.List remove_bindings = new Gee.ArrayList();
        foreach(Keybinding binding in bindings) {
            if(str_equal(accelerator, binding.accelerator)) {
                foreach(uint lock_modifier in lock_modifiers) {
                    display.ungrab_key(binding.keycode, binding.modifiers, xid);
                }
                remove_bindings.add(binding);                    
            }
        }
        
        // remove unbinded keys
        bindings.remove_all(remove_bindings);
    }
    
    /**
     * Event filter method needed to fetch X.Events
     */
    public Gdk.FilterReturn event_filter(Gdk.XEvent gdk_xevent, Gdk.Event gdk_event)
    {
        Gdk.FilterReturn filter_return = Gdk.FilterReturn.CONTINUE;
                   
        void* pointer = &gdk_xevent;
        X.Event* xevent = (X.Event*) pointer;
         
         if(xevent->type == X.EventType.KeyPress) {
            foreach(Keybinding binding in bindings) {
                // remove NumLock, CapsLock and ScrollLock from key state
                uint event_mods = xevent.xkey.state & ~ (lock_modifiers[7]);
                if(xevent->xkey.keycode == binding.keycode && event_mods == binding.modifiers) {
                    // call all handlers with pressed key and modifiers
                    binding.handler(gdk_event);
                }
            }
         }
         
        return filter_return;
    }
}

How to use it

As some bindings needed to be adjusted you need at least version 0.11.3 of vala.

Furthermore are following packages needed:

  • gtk+-2.0
  • x11
  • gdk-x11-2.0
  • gee-1.0

Here is a sample how to compile this class:

valac --pkg gtk+-2.0 --pkg x11 --pkg gdk-x11-2.0 --pkg gee-1.0 keybinding-manager.vala

So and finally here some sample code how to bind a key:

public static int main (string[] args)
{
    Gtk.init (ref args);
    
    KeybindingManager manager = new KeybindingManager();
    manager.bind("V", test);

    Gtk.main ();
    return 0;
}
    
private static void test()
{
    debug("hotkey pressed");
}

Hope this sample class will be helpful for you. Happy coding ;).

3 thoughts on “Global hotkeys with vala

  1. Nice thx, this will help me in my Project. But i also want a windows version so im still thinling about how to bind vala with WIN32API function calls or make extern c calls, i hope i can still pack it in a “nice” class…

    1. Oh one more think, i also found sth interresting which will maby make the whole XLib calls go away and maby also working on windows, (just read some hints in mailing list but did not try it out yet if it rly works) I’m speaking about a low level GDK add filter function which claims to be able to intercept all events (from specific window or with null from all but im not shure if that means system wide or Gtk apps wide …) before they reach the rest of Gtk. http://mail.gnome.org/archives/gtk-devel-list/2004-March/msg00246.html

      1. What this sample code actually does is to bind a keybinding to a window so that that Gdk Window will be noticed of such X.Event.

        See constructor (rootwin.add_filter(event_filter)) where the method which is mentioned in the mailinglist is used.

        So therefore I currently do not see a way to register a global keybinding without using XLib.
        However I would definitely be interested if you would be able to find one.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.