X11: XKB (1)

XKB
XKB は、X のキーボードに関する拡張機能です。

キー押しなどのイベントは、X のイベントか、XInput 拡張機能のイベントを使って受け取ります。
XKB を使うと、キーボードの動作に関する詳細な設定などが行えます。
また、コアの X だけでは不十分な機能が拡張されています。
使い方
XKB は Xlib (X11R6 以降) に含まれているので、別途拡張機能のライブラリをリンクする必要はありません。
インクルードファイルは、<X11/XKBlib.h> が必要です。

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

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 で、ライブラリのバージョンに互換性があります。
(現在の最新バージョンは 1.0)

ヘッダで定義されているバージョンを使用したい場合は、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 で指定されたキーボードデバイスの現在の状態を、state_return に返します。

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

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

XkbUseCoreKbd の場合、コアのキーボードデバイスになります (XInput2 におけるマスターキーボードデバイス)。
デバイスを区別せずに、すべてのキーボードを共通に扱いたい場合は、XkbUseCoreKbd を指定します。
XkbStateRec 構造体
XkbStateRec は、「修飾子」「キーボードグループ」「ポインタボタン」の状態で構成されます。

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 がポインタ型になります。
修飾子 (mods)
キーボードの修飾子は、コアプロトコルで定義されている「Shift、Lock、Control、Mod1〜Mod5」が使用されます。
修飾子はそれぞれ、ON/OFF の状態に切り替えることができるものと考えます。
グループ (group)
XKB では、最大4個のグループを定義することができ、各グループは 0〜3 のインデックスで指定されます。
グループが変更されると、キーに対応する KeySym のセットが変化します。

ただし、一つのグループでは、キーと修飾子の組み合わせで KeySym を変更することができるので、基本的には1つのグループのみが使われることが多いです。

/usr/share/X11/xkb/symbols/jp ファイルに、日本語キーボード用の XKB 定義があるので、参考にしてみてください。
base, latched, locked, effective
修飾子キーとグループには、base, latched, locked, effective の4つの種類があります。

base現在、物理的または論理的に、実際に押されている状態のキーを示します。
latchedラッチ状態のキー。

XKB の機能によって、Shift などの装飾キーを一度押して離すと、ラッチ状態にすることができます (機能の変更が可能)。
この状態で通常のキーを押すと、そのキーは +Shift などの状態となり、その後、ラッチ状態は自動で解除されます。
Shift キーなどを押しながら、同時に別のキーを押すのが難しい場合に、この機能が使われます。
lockedロック状態のキー。
NumLock や CapsLock など、ON/OFF の状態が永続的に切り替わるもの。
effective現在有効な状態。
base, latched, locked によって計算されます。
各状況によっては、一部無効となる値もありますが、基本的には3つすべてを含んだ状態です。
(グループの場合は、それぞれから判断した一つのインデックス)
ほか
lookup_modsKeyPress/KeyRelease イベント時に、キーコードと修飾子から、対応する KeySym に変換する時に使用する、修飾子状態
grab_modsパッシブグラブ時の一致判定に使用される修飾子状態
compat_*XKB が無効な状態のクライアントに対して適用される値
イベント
キーボードの状態や設定が変化した時などに、XKB イベントを受け取ることができます。
XKB イベント
XKB のイベントであるかを判定するには、XEvent の type メンバが、XkbQueryExtension 関数の event_rtrn 引数で取得したイベントタイプ値と同じであるかどうかを比較します。

すべての XKB イベントでは、このイベントタイプはすべて同じ値になります。

XkbAnyEvent 構造体
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 イベントがある場合は、デバイスごとにイベントを選択する必要があります。

※XKB イベントは、X ウィンドウごとに選択することができません。主にキーボード設定関連のイベントが多いので、クライアントを対象にして送られてきます。

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_bitsbits_to_change に対応するそれぞれのイベントを、選択するか解除するかのマスク。
(ON なら選択、OFF なら解除)

XSelectInput 関数などとは違って、イベントマスク全体を上書きするのではなく、個々のイベントを選択したり選択解除することができます。

bits_to_change に、選択または選択解除したいイベントのマスクを、values_for_bits に、選択するか選択解除するかどうかのフラグをセットします。

//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そのイベントの詳細な条件のマスク (設定を変更するものを ON)
values_for_bitsbits_to_change に対応するビットで、選択するか選択解除するかのフラグ

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 e11-xkb1.c util.c -lX11

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

<e11-xkb1.c>
#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 キーを押した時 (この場合は押した瞬間)、NumLock が ON になるので、各 mods が Mod2Mask になっています。
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) です。

Shift + CapsLock を押した瞬間の値は以下のようになっています。

mods (0x13)mods は現在有効な状態です。
(ShiftMask | LockMask | Mod2Mask) で、Shift, CapsLock, NumLock が ON の状態です。
base_mods (0x03)現在物理的に押されているキー。
NumLock はロック状態でキーは押されていないので、(ShiftMask | LockMask) です。
locked_mods (0x12)ロック状態 (LED が ON になっている状態) の修飾子です。
NumLock と CapsLock が ON になっているので、(LockMask | Mod2Mask) です。