Wayland: キーボード入力

キーボード入力
今回は、キーボード入力を行います。

必要なパッケージ
キーマップの処理に xkbcommon を使うので、開発用パッケージをインストールしてください。

Debian/Ubuntulibxkbcommon-dev
RedHatlibxkbcommon-devel
プログラム
$ cc -o test 10-keyboard.c client.c imagebuf.c xdg-shell-protocol.c -lwayland-client -lrt -lxkbcommon

※libxkbcommon をリンクする必要があります。

押されたキーなどの情報を出力します。
ウィンドウがアクティブな状態で ESC キーが押されたら、終了します。

<10-keyboard.c>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
#include <unistd.h>

#include <wayland-client.h>
#include "xdg-shell-client-protocol.h"

#include <xkbcommon/xkbcommon.h>

#include "client.h"
#include "imagebuf.h"


//-------------

typedef struct
{
    client b;

    struct wl_keyboard *keyboard;
    struct xkb_keymap *xkb_keymap;
    struct xkb_state *xkb_state;
}client_ex;

//-------------


/* キーボード関連解放 */

static void _keyboard_release(client_ex *p)
{
    //XKB 解放

    xkb_keymap_unref(p->xkb_keymap);
    xkb_state_unref(p->xkb_state);

    p->xkb_keymap = NULL;
    p->xkb_state = NULL;

    //wl_keyboard

    if(p->b.seat_ver >= WL_KEYBOARD_RELEASE_SINCE_VERSION)
        wl_keyboard_release(p->keyboard);
    else
        wl_keyboard_destroy(p->keyboard);

    p->keyboard = NULL;
}

/* XKB キーマップ作成 */

static void _xkb_keymap(client_ex *p,char *mapstr)
{
    struct xkb_context *context;
    struct xkb_keymap *keymap;

    //コンテキスト作成

    context = xkb_context_new(XKB_CONTEXT_NO_FLAGS);
    if(!context) return;

    //キーマップ作成

    keymap = xkb_keymap_new_from_string(context,
            mapstr, XKB_KEYMAP_FORMAT_TEXT_V1, 0);

    if(!keymap)
    {
        xkb_context_unref(context);
        return;
    }

    //

    xkb_keymap_unref(p->xkb_keymap);
    xkb_state_unref(p->xkb_state);

    p->xkb_keymap = keymap;
    p->xkb_state = xkb_state_new(keymap);

    //

    xkb_context_unref(context);
}


//===================
// wl_keyboard
//===================


/* キーマップ */

static void _keyboard_keymap(void *data, struct wl_keyboard *keyboard,
    uint32_t format, int32_t fd, uint32_t size)
{
    char *mapstr;

    printf("keymap | format:%u, fd:%d, size:%u\n", format, fd, size);

    //XKB ver 1 以外は対応しない

    if(format != WL_KEYBOARD_KEYMAP_FORMAT_XKB_V1)
    {
        close(fd);
        return;
    }

    //文字列としてメモリにマッピング

    mapstr = mmap(NULL, size, PROT_READ, MAP_SHARED, fd, 0);

    if(mapstr == MAP_FAILED)
    {
        close(fd);
        return;
    }

    //キーマップ作成

    _xkb_keymap((client_ex *)data, mapstr);

    //

    munmap(mapstr, size);
    close(fd);
}

/* キーボードフォーカスが来た時 */

static void _keyboard_enter(void *data, struct wl_keyboard *keyboard,
    uint32_t serial, struct wl_surface *surface, struct wl_array *keys)
{
    printf("enter\n");
}

/* キーボードフォーカスが離れた時 */

static void _keyboard_leave(void *data, struct wl_keyboard *keyboard,
    uint32_t serial, struct wl_surface *surface)
{
    printf("leave\n");
}

/* キーが押された/離された時 */

static void _keyboard_key(void *data, struct wl_keyboard *keyboard,
    uint32_t serial, uint32_t time, uint32_t key, uint32_t state)
{
    client_ex *p = (client_ex *)data;
    xkb_keysym_t sym;

    printf("key | key:%u, %s\n", key,
        (state == WL_KEYBOARD_KEY_STATE_PRESSED)? "press": "release");

    //現在の状態とキーコードから keysym (XKB_KEY_*) を取得

    sym = xkb_state_key_get_one_sym(p->xkb_state, key + 8);

    printf("-- xkb keyshm (0x%X)\n", sym);

    //ESC キーで終了

    if(sym == XKB_KEY_Escape)
        p->b.finish_loop = 1;
}

/* 装飾キーやロックの状態が変化した時 */

static void _keyboard_modifiers(void *data, struct wl_keyboard *keyboard,
    uint32_t serial, uint32_t mods_depressed,
    uint32_t mods_latched, uint32_t mods_locked, uint32_t group)
{
    client_ex *p = (client_ex *)data;
    xkb_mod_mask_t mods;

    printf("modifiers | depressed:%u, latched:%u, locaked:%u, group:%u\n",
        mods_depressed, mods_latched, mods_locked, group);

    //状態をセット

    xkb_state_update_mask(p->xkb_state,
        mods_depressed, mods_latched, mods_locked, group, 0, 0);

    //現在の状態のフラグ取得

    mods = xkb_state_serialize_mods(p->xkb_state, XKB_STATE_MODS_EFFECTIVE);

    //状態表示

    printf("-- mods: ");

    if(mods & (1 << xkb_keymap_mod_get_index(p->xkb_keymap, XKB_MOD_NAME_CTRL)))
        printf("Ctrl ");

    if(mods & (1 << xkb_keymap_mod_get_index(p->xkb_keymap, XKB_MOD_NAME_SHIFT)))
        printf("Shift ");

    if(mods & (1 << xkb_keymap_mod_get_index(p->xkb_keymap, XKB_MOD_NAME_ALT)))
        printf("Alt ");

    if(mods & (1 << xkb_keymap_mod_get_index(p->xkb_keymap, XKB_MOD_NAME_LOGO)))
        printf("Logo ");

    if(mods & (1 << xkb_keymap_mod_get_index(p->xkb_keymap, XKB_MOD_NAME_NUM)))
        printf("NumLock ");

    if(mods & (1 << xkb_keymap_mod_get_index(p->xkb_keymap, XKB_MOD_NAME_CAPS)))
        printf("CapsLock ");

    printf("\n");
}

/* キーリピートの情報 */

static void _keyboard_repeat_info(void *data, struct wl_keyboard *keyboard,
    int32_t rate, int32_t delay)
{
    printf("repeat_info | rate:%d, delay:%d\n", rate, delay);
}

static const struct wl_keyboard_listener g_keyboard_listener = {
    _keyboard_keymap, _keyboard_enter, _keyboard_leave, _keyboard_key,
    _keyboard_modifiers, _keyboard_repeat_info
};


//===================


/* wl_seat:capabilities */

static void _seat_capabilities(client *cl,struct wl_seat *seat,uint32_t cap)
{
    client_ex *p = (client_ex *)cl;

    if((cap & WL_SEAT_CAPABILITY_KEYBOARD) && !p->keyboard)
    {
        //wl_keyboard 作成

        p->keyboard = wl_seat_get_keyboard(cl->seat);

        wl_keyboard_add_listener(p->keyboard, &g_keyboard_listener, p);
    }
    else if(!(cap & WL_SEAT_CAPABILITY_KEYBOARD) && p->keyboard)
    {
        //wl_keyboard 解放

        _keyboard_release(p);
    }
}

/* main */

int main(void)
{
    client_ex *p;
    Toplevel *win;

    p = (client_ex *)client_new(sizeof(client_ex));

    p->b.init_flags = CLIENT_INIT_FLAGS_SEAT;
    p->b.seat_capabilities = _seat_capabilities;

    client_init(CLIENT(p));

    //

    win = toplevel_create(CLIENT(p), 256, 256, NULL);

    imagebuf_fill(win->sf.img, 0xffff0000);

    //

    client_loop_simple(CLIENT(p));

    //解放

    toplevel_destroy(win);

    _keyboard_release(p);
    
    client_destroy(CLIENT(p));

    return 0;
}
wl_keyboard
キーボード操作を行うためには wl_keyboard が必要なので、wl_pointer の時と同じように、wl_seat のバインド後、wl_seat の capabilities イベント内で、作成と解放の処理を行います。
wl_keyboard のイベント
struct wl_keyboard_listener {
    void (*keymap)(void *data, struct wl_keyboard *wl_keyboard,
        uint32_t format, int32_t fd, uint32_t size);
    void (*enter)(void *data, struct wl_keyboard *wl_keyboard,
        uint32_t serial, struct wl_surface *surface, struct wl_array *keys);
    void (*leave)(void *data, struct wl_keyboard *wl_keyboard,
        uint32_t serial, struct wl_surface *surface);
    void (*key)(void *data, struct wl_keyboard *wl_keyboard,
        uint32_t serial, uint32_t time, uint32_t key, uint32_t state);
    void (*modifiers)(void *data, struct wl_keyboard *wl_keyboard,
        uint32_t serial, uint32_t mods_depressed, uint32_t mods_latched,
        uint32_t mods_locked, uint32_t group);
    //ver 4
    void (*repeat_info)(void *data, struct wl_keyboard *wl_keyboard, int32_t rate, int32_t delay);
};

keymapキーマップの設定を行う時
enterキーボードフォーカスが来た時。
(クライアント内のウィンドウがアクティブになって、キー入力ができる状態になった時)
leaveキーボードフォーカスが離れた時
keyキーが押された or 離された時
modifiers装飾キーが押された/離された、または、ロックの状態が変化した時
repeat_info(ver 4) キーリピートの設定が通知される
キーマップ
キーマップ」とは、キーボードの物理的なキー情報から、プログラムなどによって共通で認識できるコードへと変換をするための、割り当てのことです。

例えば、ハードウェア上の 0 番のボタンは、ESC キーに割り当てる、といった感じの情報です。

キーボードの配列は、英語や日本語などの言語ごとに異なるため、日本語キーボードを使っているのに、英語キーボードの配列設定になっていたりすると、正しく入力が行なえません。
(押したキーと、実際に出力された文字が一致しないなど)

Wayland でキーボードを操作する場合、キーマップを処理するライブラリを使って、ハードウェアのキー情報から、共通の一般的なキーコードへと変換する処理を行う必要があります。

XKB
XKB は、X11 において、より詳細なキーボード処理を行うための拡張機能として整備されたものですが、Wayland でも、その仕様をそのまま使えるようになっています。

現在 Wayland では、キーマップのフォーマットとして「XKB (ver 1)」のみ対応しています。
XKB を使うためのライブラリとして、xkbcommon を使います。

ヘッダファイルは /usr/include/xkbcommon ディレクトリ下にあり、xkbcommon.h をメインのインクルードファイルとして使います。

libxkbcommon をリンクする必要があります。
keymap イベント
wl_keyboard の keymap イベントは、キーマップの設定を行う必要がある時に呼ばれます。

キーボードを使う時は、最初にキーマップの設定が必要になるので、キーボードが有効になった時か、キーマップ設定が変更された時に呼ばれます。

ハンドラ処理
keymap イベントの処理は、以下のようになっています。

static void _keyboard_keymap(void *data, struct wl_keyboard *keyboard,
    uint32_t format, int32_t fd, uint32_t size)
{
    char *mapstr;

    printf("keymap | format:%u, fd:%d, size:%u\n", format, fd, size);

    //XKB ver 1 以外は対応しない

    if(format != WL_KEYBOARD_KEYMAP_FORMAT_XKB_V1)
    {
        close(fd);
        return;
    }

    //文字列としてメモリにマッピング

    mapstr = mmap(NULL, size, PROT_READ, MAP_SHARED, fd, 0);

    if(mapstr == MAP_FAILED)
    {
        close(fd);
        return;
    }

    //キーマップ作成

    _xkb_keymap((client_ex *)data, mapstr);

    //

    munmap(mapstr, size);
    close(fd);
}

引数 format は、設定するキーマップのフォーマットです。
現在設定可能なのは、XKB (ver 1) だけです。

WL_KEYBOARD_KEYMAP_FORMAT_NO_KEYMAP = 0
WL_KEYBOARD_KEYMAP_FORMAT_XKB_V1 = 1

fd, size は、キーマップの設定情報が入った共有メモリの、ファイルディスクリプタとデータサイズです。

渡された fd は、mmap() を使ってメモリにマッピングした後、バッファの中身を文字列データとして使います。
設定が終わったら、アンマップしてクローズします。

XKB の場合は、以下のような感じで、キーの設定情報が入っています。

xkb_keymap {
xkb_keycodes "(unnamed)" {
    minimum = 8;
    maximum = 255;
    <ESC>                = 9;
    <AE01>               = 10;
    <AE02>               = 11;
...
XKB キーマップ作成
渡されたキーマップ設定の文字列から、XKB のキーマップを作成します。

struct xkb_context *context;
struct xkb_keymap *keymap;

//コンテキスト作成

context = xkb_context_new(XKB_CONTEXT_NO_FLAGS);
if(!context) return;

//キーマップ作成

keymap = xkb_keymap_new_from_string(context,
        mapstr, XKB_KEYMAP_FORMAT_TEXT_V1, 0);

if(!keymap)
{
    xkb_context_unref(context);
    return;
}

//

xkb_keymap_unref(p->xkb_keymap);
xkb_state_unref(p->xkb_state);

p->xkb_keymap = keymap;
p->xkb_state = xkb_state_new(keymap);

//

xkb_context_unref(context);

まず、xkb_context_new() で、キーマップの作成に必要なコンテキスト (xkb_context) を作成します。

次に、xkb_keymap_new_from_string() で、文字列の設定データからキーマップ (xkb_keymap) を作成します。

あとは、xkb_state_new() で、キーボードの現在状態を管理するための xkb_state を作成します。

xkb_*_unref() は、オブジェクトの参照カウンタをデクリメントし、カウンタが 0 になったら解放します。

xkb_context は、キーマップを作成したら必要なくなるので、ここで解放します。

xkb_keymapxkb_state のポインタは保持しておきます。
(キーマップが変更された場合は、先に、現在使われているポインタを解放しています。
unref 関数は NULL ポインタを判定してくれるので、新規状態でも問題なく動作します)

ちなみに、Wayland 関数の wl_*_destroy() などの場合は、NULL ポインタを判定してくれません。
key イベント
キーが押されたり離された時は、key イベントが呼ばれます。

static void _keyboard_key(void *data, struct wl_keyboard *keyboard,
    uint32_t serial, uint32_t time, uint32_t key, uint32_t state)
{
    client_ex *p = (client_ex *)data;
    xkb_keysym_t sym;

    printf("key | key:%u, %s\n", key,
        (state == WL_KEYBOARD_KEY_STATE_PRESSED)? "press": "release");

    //現在の状態とキーコードから keysym (XKB_KEY_*) を取得

    sym = xkb_state_key_get_one_sym(p->xkb_state, key + 8);

    printf("-- xkb keyshm (0x%X)\n", sym);

    //ESC キーで終了

    if(sym == XKB_KEY_Escape)
        p->b.finish_loop = 1;
}
引数
key は、linux/input-event-codes.h で定義されているキーコードです。

物理的なキーコードではなく、Linux の evdev によるキーコードのため、一般的なキーであれば、どのキーが押されたかということは判別できます。

state は、押されたか離されたかの状態です。

WL_KEYBOARD_KEY_STATE_RELEASED = 0
WL_KEYBOARD_KEY_STATE_PRESSED = 1
キーコード変換
XKB を使って、key のキーコードから、一般的に使用するキーの識別子に変換したい場合、xkb_state_key_get_one_sym() を使います。

Shift や NumLock などのキーの現在の状態と、key 引数で指定されたキーを組み合わせて、実際に文字として表示したりするためのキーコードを取得できます。

戻り値として、変換されたキーの識別子が返ります。
XKB のキー識別子 (XKB_KEY_*) は、xkbcommon-keysyms.h で定義されています。

※xkb_state_key_get_one_sym() にキーコードを渡す時は、key 引数の値を +8 する必要があります。
これは、Linux の evdev のキーコードを、XKB のキーコードに合わせる際に必要になります。
例えば、'A' キーを押した場合、キーコードは「30 (KEY_A)」、keysym は「0x61 (XKB_KEY_a)」となります。

Shift キーと同時に 'A' キーを押した場合、キーコードは「30 (KEY_A)」で同じですが、keysym は「0x41 (XKB_KEY_A)」になります。
modifiers イベント
Shift や NumLock などの修飾キーの状態が変化すると、modifiers イベントが呼ばれます。

装飾キーが押したり離された時や、NumLock などが ON/OFF された時に来ます。

static void _keyboard_modifiers(void *data, struct wl_keyboard *keyboard,
    uint32_t serial, uint32_t mods_depressed,
    uint32_t mods_latched, uint32_t mods_locked, uint32_t group)
{
    client_ex *p = (client_ex *)data;
    xkb_mod_mask_t mods;

    printf("modifiers | depressed:%u, latched:%u, locaked:%u, group:%u\n",
        mods_depressed, mods_latched, mods_locked, group);

    //状態をセット

    xkb_state_update_mask(p->xkb_state,
        mods_depressed, mods_latched, mods_locked, group, 0, 0);

    //現在の状態のフラグ取得

    mods = xkb_state_serialize_mods(p->xkb_state, XKB_STATE_MODS_EFFECTIVE);

    //状態表示

    printf("-- mods: ");

    if(mods & (1 << xkb_keymap_mod_get_index(p->xkb_keymap, XKB_MOD_NAME_CTRL)))
        printf("Ctrl ");

    ...
}

まず、xkb_state_update_mask() を使い、modifiers イベントに渡された値をそのまま指定して、装飾キーなどの状態を更新します。

どの状態が変更されたかを取得したい場合は、xkb_state_serialize_mods() で、現在の状態のフラグ値を取得します。

xkb_keymap_mod_get_index() を使うと、指定名のキーのビット位置を取得できます。
各名前は xkbcommon-names.h で定義されています。

指定キーのビットが ON であれば、そのキーは ON の状態となります。

※wl_pointer のイベントでは、Shift や Ctrl の装飾キーが現在押されているかどうかを判別できないため、このイベントで装飾キーの状態を取得して、記録しておく必要があります。
repeat_info イベント (ver 4)
void (*repeat_info)(void *data, struct wl_keyboard *wl_keyboard, int32_t rate, int32_t delay);

repeat_info イベントでは、キーリピートの設定情報が通知されます。

キーリピートは、キーが押し続けられている間、一定間隔でそのキーが押されているものとして扱う処理のことです。
'A' キーを押した直後は 'A' を出力し、一定時間を過ぎたら、一定間隔ごとに 'A' を繰り返し出力する、というようなことです。

サーバーはキーリピートを処理しないため、必要に応じて、クライアントが自分で実装する必要があります。
実装方法
実際の実装方法としては、以下のようになります。

  1. キーが押された時、キーリピートの判定を開始する。
  2. 押されてから一定時間が経過したら、キーリピートを開始する。
  3. 一定時間が経過するごとに、同じキーが再度押されたとみなす。
  4. キーが離されるまで、繰り返す。
設定値
上記の実装において、設定として、2つの時間値が指定可能であり、クライアントは独自の時間間隔を使うこともできますが、ユーザーがデスクトップ側で設定した値を使いたい場合は、このイベントで送られてきた値を使います。

delay最初にキーが押された時間から、キーリピートを開始するまでの時間
rateキーリピートする間隔

2つとも、単位はミリ秒 (ms) です。

GNOME の場合は、デフォルトで「delay = 500」「rate = 33」となっています。