Pull to refresh

Регистрация глобальных нажатий клавиш с использованием JNA

Reading time15 min
Views11K
Здравствуйте, в этой статье я расскажу вам как регистрировать глобальные нажатия клавиш из Java под Windows, Linux, BSD и Mac OSX с использованием отличной библиотеки JNA.

Для чего нужен JNA


Java с десктопом дружит сложно, для некоторых вещей нужно писать мосты для взаимодействия с операционной системой. Одной из таких функциональностей являются глобальные хоткеи, весьма популярные в аудио плеерах, когда даже в скрытом состоянии программой можно управлять с помощью определенных сочетаний клавиш или медиа-кнопок. На помощь приходит JNA — надстройка над jni и libffi для вызова нативных библиотек, она поддерживает почти все популярные платформы, разрабатывается уже долгое время и весьма стабильна.

Для джавы уже есть несколько достаточно стабильных библиотек для всех платформ: JIntelliType для Windows, которая даже поддерживает медиа-кнопки, JXGrabKey для систем Linux, и ossuport-connector для Mac OSX. Однако, все они используют jni, имеют разный интерфейс, и с библиотеками на jni не всегда удобно работать, потому что нужно прописывать пути к нативным библиотекам, разбираться с разрядностью системы и пр. Плюс это будет интересным упражнением по использованию JNA, потому что эту задачу можно сделать полностью на java достаточно малыми усилиями и получить легко поддерживаемый кроссплатформенный код.

Windows


Проще всего с глобальными хоткеями работать в Windows:

public class User32 {
    static {
        Native.register(NativeLibrary.getInstance("user32", W32APIOptions.DEFAULT_OPTIONS));
    }
 
    public static final int MOD_ALT = 0x0001;
    public static final int MOD_CONTROL = 0x0002;
    public static final int MOD_SHIFT = 0x0004;
    public static final int MOD_WIN = 0x0008;
    public static final int WM_HOTKEY = 0x0312;
 
    public static native boolean RegisterHotKey(Pointer hWnd, int id, int fsModifiers, int vk);
    public static native boolean UnregisterHotKey(Pointer hWnd, int id);
    public static native boolean PeekMessage(MSG lpMsg, Pointer hWnd, int wMsgFilterMin, int wMsgFilterMax, int wRemoveMsg);
 
    public static class MSG extends Structure {
        public Pointer hWnd;
        public int message;
        public int wParam;
        public int lParam;
        public int time;
        public int x;
        public int y;
    }
}


Здесь мы используем так называемый direct mapping. Помимо того что он быстрее, с ним проще работать, так как можно сделать static import и использовать методы как родные. Нам нужны три метода из User32:
  • RegisterHotKey — собственно регистрирует хоткей. Первым параметром можно ставить null, так как окна у нас нет. Второй параметр — уникальный номер нашего хоткея, генерируется нами, он после будет использован для идентификация хоткея и для его удаления через UnregisterHotKey. В fsModifiers пишем какие модификаторы нам нужны (ctrl, shift, alt или win). В vk пишем виртуальный код. Интересно, что виртуальные коды используемые в KeyEvent в AWT почти всегда совпадают с виртуальными кодами в Windows:

    // регистрируем сочетание WIN+F
    RegisterHotKey(null1, 0x8, KeyEvent.VK_F);

  • PeekMessage — проверяет входящие сообщения и забивает их в структуру MSG. Здесь используется неблокирующий вызов PeekMessage вместо блокирующего GetMessage из-за того что все методы должны вызываться из одного потока и нам необходимо иметь возможность этим потоком управлять:
    MSG msg = new MSG();
    while (listen) {
          while (PeekMessage(msg, null00, PM_REMOVE)) {
               if (msg.message == WM_HOTKEY) {
                   System.out.println("Yattaaaa. Hotkey with id " + msg.wParam);
               }
          }
     
          Thread.sleep(300);
    }

    Здесь мы регистрируем сочетание WIN+F и проверяем сообщения каждые 300 мс.

X11


К сожалению, для X11 direct mapping использовать не получится, так как это почему-то вызывает ошибки при работе с FreeBSD. Маппинг выглядит немного сложнее:

public interface X11 extends Library {
    public static X11 Lib = (X11) Native.loadLibrary("X11", X11.class);
    public static final int GrabModeAsync = 1;
    public static final int KeyPress = 2;
 
    public static final int ShiftMask = (1);
    public static final int LockMask = (1 << 1);
    public static final int ControlMask = (1 << 2);
    public static final int Mod1Mask = (1 << 3);
    public static final int Mod2Mask = (1 << 4);
    public static final int Mod3Mask = (1 << 5);
    public static final int Mod4Mask = (1 << 6);
    public static final int Mod5Mask = (1 << 7);
 
    public Pointer XOpenDisplay(String name);
    public NativeLong XDefaultRootWindow(Pointer display);
    public byte XKeysymToKeycode(Pointer display, long keysym);
    public int XGrabKey(Pointer display, int code, int modifiers, NativeLong root, int ownerEvents, int pointerMode, int keyBoardMode);
    public int XUngrabKey(Pointer display, int code, int modifiers, NativeLong root);
    public int XNextEvent(Pointer display, XEvent event);
    public int XPending(Pointer display);
    public int XCloseDisplay(Pointer display);
 
    public static class XEvent extends Union {
        public int type;
        public XKeyEvent xkey;
        public NativeLong[] pad = new NativeLong[24];
    }
 
    public static class XKeyEvent extends Structure {
        public int type;            // of event
        public NativeLong serial;   // # of last request processed by server
        public int send_event;      // true if this came from a SendEvent request
        public Pointer display;     // public Display the event was read from
        public NativeLong window;         // "event" window it is reported relative to
        public NativeLong root;           // root window that the event occurred on
        public NativeLong subwindow;      // child window
        public NativeLong time;     // milliseconds
        public int x, y;            // pointer x, y coordinates in event window
        public int x_root, y_root;  // coordinates relative to root
        public int state;           // key or button mask
        public int keycode;         // detail
        public int same_screen;     // same screen flag
    }
}

  • XOpenDisplay, XCloseDisplay, XDefaultRootWindow — получают и закрывают дефолтный дисплей и коренное окно.
  • XKeysymToKeycode — конвертирует символ (описания символов берутся в keysymdef.h) в клавитурный код, он потребуется позже:
    // для букв и цифр номер символа также соответствует значению в KeyEvent
    byte code = XKeysymToKeycode(display, KeyEvent.VK_F);
     
  • XGrabKey, XUngrabKey — регистрируют и удаляют хоткей. В code здесь пишется значение полученное ранее с XKeysymToKeycode, в modifiers пишутся модификаторы, в root значение из XDefaultRootWindow. Остальные значения забиваются единицами. Интересно, что в X11 нажатые ScrollLock, NumLock, CapsLock и еще какой-то Lock также считаются модификаторами, поэтому вместо одного хоткея приходится регистрировать 16, для каждой возможной комбинации:
    // этот хак найден в плагине global hotkey в плеере deadbeef
    for (int flags = 0; flags < 16; i++) {
        int ret = modifiers;
        if ((flags & 1) != 0)
            ret |= LockMask;
        if ((flags & 2) != 0)
            ret |= Mod2Mask;
        if ((flags & 4) != 0)
            ret |= Mod3Mask;
        if ((flags & 8) != 0)
            ret |= Mod5Mask;
        XGrabKey(display, code, ret, root, 1, GrabModeAsync, GrabModeAsync);
        // XUngrabKey(display, code, ret, root);
    }
     

    Также, в отличие от Windows, где удалять хоткей при выходе программы необязательно, если не вызвать XUngrabKey, то иксы будут держать его до самого перезапуска.
  • XPending и XNextEvent — проверяют наличие и достают следующее событие:
    while (listening) {
        while (Lib.XPending(display) > 0) {
            Lib.XNextEvent(display, event);
            if (event.type == KeyPress) {
                // считываем наше событие из union XEvent
                // JNA не знает какое именно поле заполнять в union,
                // поэтому ему нужно сказать какое из полей считать. 
                XKeyEvent xkey = (XKeyEvent) event.readField("xkey");
     
                // очищаем мусорные модификаторы
                int state = xkey.state & (ShiftMask | ControlMask | Mod1Mask | Mod4Mask);
                System.out.println("Yattaaaa, hotkey with code: " + xkey.keycode + " and modifiers: " + state);
            }
        }
     
        Thread.sleep(300);
    }


Mac OSX


Основу кода для Mac OSX составили наработки Torsten Uhlmann, автора ossupport-connector:

public interface Carbon extends Library {
    public static Carbon Lib = (Carbon) Native.loadLibrary("Carbon", Carbon.class);
 
    public static final int cmdKey = 0x0100;
    public static final int shiftKey = 0x0200;
    public static final int optionKey = 0x0800;
    public static final int controlKey = 0x1000;
 
    // OS_TYPE объединяет символы строки в int
    private static final int kEventClassKeyboard = OS_TYPE("keyb");
    private static final int typeEventHotKeyID = OS_TYPE("hkid");
    private static final int kEventParamDirectObject = OS_TYPE("----");
 
    public Pointer GetEventDispatcherTarget();
 
    public int InstallEventHandler(Pointer inTarget, EventHandlerProcPtr inHandler, int inNumTypes, EventTypeSpec[] inList, Pointer inUserData, PointerByReference outRef);
    public int RegisterEventHotKey(int inHotKeyCode, int inHotKeyModifiers, EventHotKeyID.ByValue inHotKeyID, Pointer inTarget, int inOptions, PointerByReference outRef);
    public int GetEventParameter(Pointer inEvent, int inName, int inDesiredType, Pointer outActualType, int inBufferSize, IntBuffer outActualSize, EventHotKeyID outData);
    public int RemoveEventHandler(Pointer inHandlerRef);
    public int UnregisterEventHotKey(Pointer inHotKey);
 
    public class EventTypeSpec extends Structure {
        public int eventClass;
        public int eventKind;
    }
 
    public static class EventHotKeyID extends Structure {
        public int signature;
        public int id;
 
        public static class ByValue extends EventHotKeyID implements Structure.ByValue {
        }
    }
 
    public static interface EventHandlerProcPtr extends Callback {
        public int callback(Pointer inHandlerCallRef, Pointer inEvent, Pointer inUserData);
    }
}

  • GetEventDispatcherTarget — получает ссылку на системный обработчик событий
  • InstallEventHandler — добавляет к этому обработчику событий наш обработчик. В отличие от остальных платформ, в Carbon события приходят асинхронно через callback:
    eventHandlerReference = new PointerByReference();
    // собственно, сам обработчик
    keyListener = new EventHandler();
     
    //магия JNA для создания массива из одной структуры
    EventTypeSpec[] eventTypes = (EventTypeSpec[]) (new EventTypeSpec().toArray(1));
    eventTypes[0].eventClass = kEventClassKeyboard;
    eventTypes[0].eventKind = kEventHotKeyPressed;
     
    int status = Lib.InstallEventHandler(Lib.GetEventDispatcherTarget(), keyListener, 1, eventTypes, null, eventHandlerReference);

    Здесь мы указываем что будем обрабатывать сообщения клавиатуры, а именно нажатия горячих клавиш.
    В eventHandlerReference приходит обратная ссылка на обработчик, которая нужна в RemoveEventHandler.

    Обработчик событий выглядит следующим образом:
    private class EventHandler implements Carbon.EventHandlerProcPtr {
            public int callback(Pointer inHandlerCallRef, Pointer inEvent, Pointer inUserData) {
                EventHotKeyID eventHotKeyID = new EventHotKeyID();
                // получаем параметры события
                int ret = Lib.GetEventParameter(inEvent, kEventParamDirectObject, typeEventHotKeyID, null, eventHotKeyID.size()null, eventHotKeyID);
                if (ret != 0) {
                    logger.warning("Could not get event parameters. Error code: " + ret);
                } else {
                    // Получаем и обрабатываем идентификатор события здесь
                    int eventId = eventHotKeyID.id;
                    logger.info("Received event id: " + eventId);
                }
                return 0;
            }
        }

  • RegisterEventHotKey — регистрирует хоткей. На вход идет код клавишы. Таблица кодов находится здесь. Далее список модификаторов. Потом структура EventHotKeyID.ByValue, в которую забивается наш идентификатор и четырехбуквенная подпись. ByValue используется потому что по-умолчанию структуры передаются по ссылке, а нам нужно по значению. Возвращается ссылка на этот хоткей, которая используется в UnregisterEventHotKey:

    EventHotKeyID.ByValue hotKeyReference = new EventHotKeyID.ByValue();
    hotKeyReference.id = 1;
    hotKeyReference.signature = OS_TYPE("hk01");
    PointerByReference gMyHotKeyRef = new PointerByReference();
     
    int status = Lib.RegisterEventHotKey(code, modifiers, hotKeyReference, Lib.GetEventDispatcherTarget()0, gMyHotKeyRef);



Заключение


В целом все достаточно просто, хотя и были некоторые проблемы, такие как падения при использовании direct mapping на FreeBSD, отказ JNA мапить boolean в XGrabKey на int, странные ошибки при передаче структуры по ссылке, а не по значению в Carbon, ошибки, генерируемые X11 если хоткей уже занят, которые просто вырубали программу, сложность нахождения какой-либо документации по Carbon.

Весь этот код собран в библиотеку jkeymaster под лицензией LGPL 3. Интерфейс основан на KeyStroke из Swing, для Windows и X11 можно регистрировать медиа-кнопки — Play/Pause, Stop, Next Track, Previous Track.

Замечания и патчи приветствуются.

p.s. Пост написан tulskiy, который благодаря mgarin теперь есть на хабре. Так что все плюсы ему.
Tags:
Hubs:
+23
Comments11

Articles