X11: XKB (1)

XKB
XKB は、X のキーボードに関する拡張機能です。
これを使うことによって、コアの X で出来ないことが出来るようになったり、扱いにくい部分を、よりわかりやすく扱うことができるようになります。

XKB は Xlib (X11R6 以降) 内に含まれているので、拡張機能のライブラリをリンクする必要はありません。
インクルードファイルは、<X11/XKBlib.h> が必要です。

クライアントが XKB を明示的に使わない場合でも、Xlib のキーボード関連の関数は、XKB を使うように置き換えられているので、間接的に XKB が使用される形になります。
XKB とコアプロトコルの間には互換性があります。
初期化
クライアントが明示的に XKB の API を使用したい場合は、まず初期化を行う必要があります。

X ディスプレイのオープンと、XKB の初期化をまとめて行いたい場合は、XkbOpenDisplay() を使うことができます。

個別に XKB の初期化を行いたい場合は、XkbLibraryVersion() で XKB のライブラリのバージョンを確認し、XkbQueryExtension() で初期化&拡張機能の情報を取得します。
XKB ライブラリのバージョン確認
Bool XkbLibraryVersion(int *lib_major_in_out, int *lib_minor_in_out);

動的または静的にリンクされている XKB ライブラリのバージョンを確認します。

lib_major_in_out, lib_minor_in_out の各変数に、使用したいバージョンをセットして渡すと、ライブラリで実際に使用できるバージョンが返ります。
戻り値が True で、ライブラリのバージョンに互換性があります。

ヘッダで定義されているバージョンを使用したい場合は、lib_major_in_out に XkbMajorVersion を、lib_minor_in_out に XkbMinorVersion をセットします。
XKB の初期化
Bool XkbQueryExtension(Display *dpy, int *opcode_rtrn, int *event_rtrn,
    int *error_rtrn, int *major_in_out, int *minor_in_out);

サーバーに XKB 拡張機能が存在するかを確認し、バージョンの互換性の確認と初期化を行って、各情報を取得します。

major_in_out, minor_in_out には、XkbLibraryVersion() で取得したバージョンを指定します。
event_rtrn で取得した値は、XKB のイベントを判別するために使います。

戻り値が True で、拡張機能が存在し、バージョンに互換性があります。
キーボードの状態
現在のキーボードの状態を取得するには、XkbGetState() を使います。

Status XkbGetState(Display *display, unsigned int device_spec, XkbStatePtr state_return);

device_spec
XKB 関数では、device_spec 引数で、対象となるキーボードデバイスを指定する場合が多いです。
XInput 拡張機能におけるデバイス ID、または、XkbUseCoreKbd を指定します。

任意のキーボードデバイスを個別に指定したい場合は、デバイス ID を調べる必要があります。

XkbUseCoreKbd の場合、コアのキーボードデバイスになります (XInput 2.x の場合、マスターキーボードデバイス)。
キーボード全体を一つのデバイスとして扱いたい場合は、XkbUseCoreKbd を指定します。
XkbStateRec
XKB キーボードの状態は、修飾子の状態、キーボードグループ、ポインタボタンの状態で構成されます。

typedef struct {
    unsigned char      group;             /* effective group index */
    unsigned char      base_group;        /* base group index */
    unsigned char      latched_group;     /* latched group index */
    unsigned char      locked_group;      /* locked group index */
    unsigned char      mods;              /* effective modifiers */
    unsigned char      base_mods;         /* base modifiers */
    unsigned char      latched_mods;      /* latched modifiers */
    unsigned char      locked_mods;       /* locked modifiers */
    unsigned char      compat_state;      /* effective group -> modifiers */
    unsigned char      grab_mods;         /* modifiers used for grabs */
    unsigned char      compat_grab_mods;  /* mods used for compatibility mode grabs */
    unsigned char      lookup_mods;        /* mods used to lookup symbols */
    unsigned char      compat_lookup_mods; /* mods used for compatibility lookup */
    unsigned short     ptr_buttons;        /* ポインタボタンのマスク */
} XkbStateRec, *XkbStatePtr;

構造体の実体は XkbStateRec で、そのポインタは XkbStatePtr 型となります。
XKB で定義されている構造体は、基本的に、*Rec が実体、*Ptr がポインタ型になります。
修飾子
キーボードの修飾子は、コアプロトコルで定義されている「Shift、Lock、Control、Mod1〜Mod5」が使用されます。
修飾子はそれぞれ、ON/OFF の状態に切り替えることができるものと考えます。

「ロックされた (Locked)」修飾子は、一度ロックされると、明示的に解除されるまで、ON の状態が継続します (NumLock や CapsLock など)。
「ラッチされた (Latched)」修飾子は、ON になると、次に通常のキーが押された時に自動で解除されます (一度だけの一時的な ON 状態)。
グループ
XKB では、4つのグループを定義することができ、0〜3 のインデックスで指定されます。
グループが変更されると、キーが押された時に変換される KeySym が変化します。

一つのグループでは、修飾子の状態によって KeySym を切り替えることができるので、基本的には1つのグループのみが使われます。

/usr/share/X11/xkb/symbols/jp ファイルに、日本語キーボード用の XKB 定義があるので、見てみてください。
グループと修飾子
base_group と base_mods は、現在、物理的または論理的に押されているキーを表しています。
locked_group と locked_mods は、ロック状態にあるキーを示します。
latched_group と latched_mods は、ラッチ状態にあるキーを示します。

mods は、base_mods, locked_mods, latched_mods それぞれの値を OR した値であり、修飾子が ON の状態であるかを判定する時に使います。
group は、base_group, latched_group, locked_group から計算されたインデックス値です。

lookup_mods は、KeyPress/KeyRelease イベント時に、キーコードから KeySym を取得する時に使用するための修飾子状態です。
grab_mods は、パッシブグラブ時の一致判定に使用される修飾子状態です。
compat_* は、XKB が無効な状態のクライアントに適用される値です。
イベント
キーボードの状態が変化した時に、XKB イベントを受け取ることができます。
XKB イベント
XKB のイベントかどうかは、XEvent 共用体の type メンバが、XkbQueryExtension 関数の event_rtrn 引数で取得したイベントタイプ値と同じであるかどうかで判定します。
XKB のイベントは複数ありますが、このイベントタイプはすべて同じ値になります。

XKB のすべてのイベント構造体では、先頭に同じデータがあり、それらは XkbAnyEvent 構造体で定義されています。

typedef struct {
    int             type; //XkbQueryExtension で取得した値
    unsigned long   serial;
    Bool            send_event;
    Display *       display;
    Time            time; //イベントが起きたときのタイムスタンプ
    int             xkb_type; //XKB イベントの詳細タイプ
    unsigned int    device;   //デバイスID
} XkbAnyEvent;

XKB イベントの詳細なイベントタイプを判断するには、xkb_type を参照します。

device は、キーボードのデバイス ID です。イベント選択時などで XkbUseCoreKbd が指定されていた場合、実際のデバイス ID になります。

XkbEvent 構造体
なお、XEvent 共用体と同じように、すべての XKB イベント構造体と XEvent を含んだ、XkbEvent 共用体が定義されています。
※XKB のイベント構造体は、XEvent のサイズを超えることはありません。

typedef union _XkbEvent {
    int                            type;
    XkbAnyEvent                    any;
    XkbStateNotifyEvent            state;
    XkbMapNotifyEvent              map;
    XkbControlsNotifyEvent         ctrls;
    XkbIndicatorNotifyEvent        indicators;
    XkbBellNotifyEvent             bell;
    XkbAccessXNotifyEvent          accessx;
    XkbNamesNotifyEvent            names;
    XkbCompatMapNotifyEvent        compat;
    XkbActionMessageEvent          message;
    XkbExtensionDeviceNotifyEvent  device;
    XkbNewKeyboardNotifyEvent      new_kbd;
    XEvent                         core;
} XkbEvent;
XKB イベントの選択
受信したい XKB イベントがある場合は、クライアントとデバイスごとに、イベントを選択する必要があります。
XkbSelectEvents と XkbSelectEventDetails の2つの関数を使って、選択または選択解除することができます。

XkbSelectEvents は、指定した XKB イベントの選択と選択解除を行います。
XkbSelectEventDetails は、指定した XKB イベントで、さらに詳細な条件を指定します。

XkbSelectEvents
Bool XkbSelectEvents(Display *display, unsigned int device_spec,
    unsigned long int bits_to_change, unsigned long int values_for_bits);

XKB イベントの選択と選択解除を行います。

device_spec は、デバイス ID または XkbUseCoreKbd です。

bits_to_change で、設定を変更するイベントのマスクを指定します。
values_for_bits で、それぞれのイベントを選択するか解除するかのマスクを指定します (ON なら選択、OFF なら解除)。

//XkbStateNotifyMask を選択
XkbSelectEvents(disp, XkbUseCoreKbd, XkbStateNotifyMask, XkbStateNotifyMask);

//XkbStateNotifyMask を解除
XkbSelectEvents(disp, XkbUseCoreKbd, XkbStateNotifyMask, 0);

XkbSelectEventDetails
Bool XkbSelectEventDetails(Display *display, unsigned int device_spec, unsigned int event_type,
    unsigned long int bits_to_change, unsigned long int values_for_bits);

XKB イベントの詳細を選択または選択解除します。

event_type で、対象となる XKB イベントタイプを指定し、bits_to_change と values_for_bits で、そのイベントの詳細なマスクを指定します。

bits_to_change と values_for_bits に何を指定するかは、XKB イベントのタイプによって異なります。
XkbStateNotify イベントであれば、XkbStateNotifyEvent 構造体の changed で使われるのと同じ値を指定します。

//XkbStateNotify イベントで mods が変化した時のみ受信
XkbSelectEventDetails(disp, XkbUseCoreKbd, XkbStateNotify,
    XkbModifierStateMask, XkbModifierStateMask);
XkbStateNotify イベント
XkbStateNotify イベントは、修飾子の ON/OFF が変化したり、グループが変更されたり、ポインタボタンの押し状態が変化した時に来ます。

※ウィンドウに関係なく送られてくるので、他のクライアントが作成したウィンドウ上で状態が変化した場合でも、常にイベントが送られてきます。

typedef struct {
    int            type;
    unsigned long  serial;
    Bool           send_event;
    Display *      display;
    Time           time;
    int            xkb_type;
    int            device;
    unsigned int   changed;
    int            group;
    int            base_group;
    int            latched_group;
    int            locked_group;
    unsigned int   mods;
    unsigned int   base_mods;
    unsigned int   latched_mods;
    unsigned int   locked_mods;
    int            compat_state;
    unsigned char  grab_mods;
    unsigned char  compat_grab_mods;
    unsigned char  lookup_mods;
    unsigned char  compat_lookup_mods;
    int            ptr_buttons;
    KeyCode        keycode;
    char           event_type;
    char           req_major;
    char           req_minor;
} XkbStateNotifyEvent;

//changed
#define XkbModifierStateMask    (1L << 0)
#define XkbModifierBaseMask     (1L << 1)
#define XkbModifierLatchMask    (1L << 2)
#define XkbModifierLockMask     (1L << 3)
#define XkbGroupStateMask       (1L << 4)
#define XkbGroupBaseMask        (1L << 5)
#define XkbGroupLatchMask       (1L << 6)
#define XkbGroupLockMask        (1L << 7)
#define XkbCompatStateMask      (1L << 8)
#define XkbGrabModsMask         (1L << 9)
#define XkbCompatGrabModsMask   (1L << 10)
#define XkbLookupModsMask       (1L << 11)
#define XkbCompatLookupModsMask     (1L << 12)
#define XkbPointerButtonMask        (1L << 13)
#define XkbAllStateComponentsMask   (0x3fff)

changed何が変更されたかを示すフラグ。
keycodeイベントを引き起こしたキーコード。プログラムによる場合は 0。
event_typereq_major または req_minor が 0 以外の場合、コアイベントのタイプ。
キーイベントによる状態変化の場合、KeyPress (2) または KeyRelease (3)。
ボタンイベントの場合、ButtonPress (4) または ButtonRelease (5)。keycode はボタン番号。

それ以外の場合は、状態変更を引き起こしたリクエストのコードが、req_major,req_minor に設定される。keycode は 0。
プログラム
XkbStateNotify イベントを受け取って表示します。
閉じるボタンで終了します。

$ cc -o run e07-xkb1.c util.c -lX11

なお、ポインタボタンの状態変化を受け取ると、ちょっと面倒な状態になるので (あらゆる場所でポインタボタンが押されると、毎回イベントが来る)、ポインタボタンの状態変化は受け取らないようにしています。

#include <stdio.h>
#include <X11/Xlib.h>
#include <X11/XKBlib.h>
#include "util.h"

static unsigned int g_count = 0;

static void _put_state(XkbStateNotifyEvent *ev)
{
    unsigned int f = ev->changed;
    
    printf("----[%d]-----\n"
        "device: %d / changed: 0x%x\n",
        g_count++, ev->device, ev->changed);

    if(f & XkbModifierStateMask) printf("mods: 0x%x\n", ev->mods);
    if(f & XkbModifierBaseMask) printf("base_mods: 0x%x\n", ev->base_mods);
    if(f & XkbModifierLatchMask) printf("latched_mods: 0x%x\n", ev->latched_mods);
    if(f & XkbModifierLockMask) printf("locked_mods: 0x%x\n", ev->locked_mods);

    if(f & XkbGroupStateMask) printf("group: %d\n", ev->group);
    if(f & XkbGroupBaseMask) printf("base_group: %d\n", ev->base_group);
    if(f & XkbGroupLatchMask) printf("latched_group: %d\n", ev->latched_group);
    if(f & XkbGroupLockMask) printf("locked_group: %d\n", ev->locked_group);

    if(f & XkbCompatStateMask) printf("compat_state: 0x%x\n", ev->compat_state);

    if(f & XkbGrabModsMask) printf("grab_mods: 0x%x\n", ev->grab_mods);
    if(f & XkbCompatGrabModsMask) printf("compat_grab_mods: 0x%x\n", ev->compat_grab_mods);

    if(f & XkbLookupModsMask) printf("lookup_mods: 0x%x\n", ev->lookup_mods);
    if(f & XkbCompatLookupModsMask) printf("compat_lookup_mods: 0x%x\n", ev->compat_lookup_mods);

    //if(f & XkbPointerButtonMask) printf("ptr_buttons: 0x%x\n", ev->ptr_buttons);
    
    printf("keycode: %d / event_type: %d / req_major: %d / req_minor: %d\n",
        ev->keycode, ev->event_type, ev->req_major, ev->req_minor);
}

int main(int argc,char **argv)
{
    Display *disp;
    Window win;
    XkbEvent ev;
    int major,minor,opcode,event_type,error_base;
    unsigned long mask;
    
    //XKB ライブラリ

    major = XkbMajorVersion;
    minor = XkbMinorVersion;

    if(!XkbLibraryVersion(&major, &minor))
    {
        printf("unsupported XKB library\n");
        return 1;
    }

    printf("library: ver %d.%d\n", major, minor);

    //

    disp = XOpenDisplay(NULL);
    if(!disp) return 1;

    set_display(disp);

    //XKB 初期化

    if(!XkbQueryExtension(disp, &opcode, &event_type, &error_base,
        &major, &minor))
    {
        XCloseDisplay(disp);
        printf("unsupported XKB (server)\n");
        return 1;
    }

    printf("server: ver %d.%d\n", major, minor);
    printf("opcode: %d\nevent:%d\nerror:%d\n\n", opcode, event_type, error_base);

    //

    win = create_test_window2(disp, 200, 200, 0, ButtonPress);

    //XKB イベント選択 (ポインタボタン以外)

    mask = XkbAllStateComponentsMask - XkbPointerButtonMask;

    XkbSelectEventDetails(disp, XkbUseCoreKbd, XkbStateNotify, mask, mask);

    //イベント

    XMapWindow(disp, win);

    while(1)
    {
        XNextEvent(disp, (XEvent *)&ev);

        if(event_quit((XEvent *)&ev)) break;

        if(ev.type == event_type
            && ev.any.xkb_type == XkbStateNotify)
        {
            _put_state((XkbStateNotifyEvent *)&ev);
        }
    }

    XCloseDisplay(disp);

    return 0;
}
実行例
Shift キー
Shift キーを押して離すと、以下のようになります。

----[4]----- # Shift 押し
device: 3 / changed: 0x1f03
mods: 0x11
base_mods: 0x1
compat_state: 0x11
grab_mods: 0x11
compat_grab_mods: 0x11
lookup_mods: 0x11
compat_lookup_mods: 0x11
keycode: 50 / event_type: 2 / req_major: 0 / req_minor: 0
----[5]----- # Shift 離し
device: 3 / changed: 0x1f03
mods: 0x10
base_mods: 0x0
compat_state: 0x10
grab_mods: 0x10
compat_grab_mods: 0x10
lookup_mods: 0x10
compat_lookup_mods: 0x10
keycode: 50 / event_type: 3 / req_major: 0 / req_minor: 0

device は、xinput コマンドで調べてみると、id=3 は "Virtual core keyboard" でした。
これは、サーバーによって作成されているマスターキーボードデバイスです。

Shift キーが押された時、NumLock と Shift の2つが ON の状態なので、mods は (ShiftMask | Mod2Mask) です。
現在物理的にキーが押されているのは Shift だけなので、base_mods は ShiftMask のみです。
NumLock の解除とセット
----[82]----- # NumLock 押し
device: 3 / changed: 0x2
base_mods: 0x10
keycode: 77 / event_type: 2 / req_major: 0 / req_minor: 0
----[83]----- # NumLock 離し (解除)
device: 3 / changed: 0x1f0b
mods: 0x0
base_mods: 0x0
locked_mods: 0x0
compat_state: 0x0
grab_mods: 0x0
compat_grab_mods: 0x0
lookup_mods: 0x0
compat_lookup_mods: 0x0
keycode: 77 / event_type: 3 / req_major: 0 / req_minor: 0
----[84]----- # NumLock 押し (セット)
device: 3 / changed: 0x1f0b
mods: 0x10
base_mods: 0x10
locked_mods: 0x10
compat_state: 0x10
grab_mods: 0x10
compat_grab_mods: 0x10
lookup_mods: 0x10
compat_lookup_mods: 0x10
keycode: 77 / event_type: 2 / req_major: 0 / req_minor: 0
----[85]----- # NumLock 離し
device: 3 / changed: 0x2
base_mods: 0x0
keycode: 77 / event_type: 3 / req_major: 0 / req_minor: 0

NumLock が ON の状態で、NumLock キーを押して離した時、NumLock が解除され、mods が 0 になります。
NumLock が OFF の状態で、NumLock キーを押した時、mods = Mod2Mask で、NumLock が ON になります。
CapsLock ON
----[195]----- # CapsLock 押し
device: 3 / changed: 0x1f0b
mods: 0x13
base_mods: 0x3
locked_mods: 0x12
compat_state: 0x13
grab_mods: 0x13
compat_grab_mods: 0x13
lookup_mods: 0x13
compat_lookup_mods: 0x13
keycode: 66 / event_type: 2 / req_major: 0 / req_minor: 0
----[196]----- # CapsLock 離し
device: 3 / changed: 0x2
base_mods: 0x1
keycode: 66 / event_type: 3 / req_major: 0 / req_minor: 0
----[197]----- # Shift 離し
device: 3 / changed: 0x1f03
mods: 0x12
base_mods: 0x0
compat_state: 0x12
grab_mods: 0x12
compat_grab_mods: 0x12
lookup_mods: 0x12
compat_lookup_mods: 0x12
keycode: 50 / event_type: 3 / req_major: 0 / req_minor: 0

Shift + CapsLock(英数) で、CapsLock の ON/OFF ができます。
CapsLock は LockMask (0x02) です。

mods は、(ShiftMask | LockMask | Mod2Mask) で、Shift, CapsLock, NumLock が ON の状態です。
現在押されている修飾子キーは Shift と Lock なので、base_mods は (ShiftMask | LockMask) です。
locked_mods は、ロック状態にある修飾子です。NumLock と CapsLock が ON なので、(LockMask | Mod2Mask) です。