X11: 入力メソッド (2) - XIC

入力コンテキスト
入力コンテキストは、クライアントと入力メソッド間の通信の状態などを保持します。

XIM はアプリケーションに対して1つ作成しますが、入力コンテキスト (XIC) は基本的に、入力対象のウィンドウ (トップレベルウィンドウ) に対して1つ作成します。

XCloseIM() によって入力メソッドが閉じられた場合、関連する入力コンテキストはすべて使用できなくなるので、入力コンテキストは XCloseIM() で閉じる前に破棄してください。
関数
作成と破棄
XIC XCreateIC(XIM im,...);

void XDestroyIC(XIC ic);

XCreateIC は、可変引数で属性値を指定し、入力コンテキストを作成します。
作成できなかった場合、NULL が返ります。

いくつか値を設定できますが、基本的に必要なのは以下の値です。

XNInputStyle使用する入力スタイル (XIMStyle)。
作成時に必ず指定する必要があります。
XNQueryInputStyle で取得したスタイル値のいずれかであること。
XNClientWindow入力メソッドがデータを表示したり、サブウィンドウを作成するためのウィンドウ (Window)。
一度だけ設定できます。
XNFocusWindow入力に関するイベントを受け取るウィンドウ (Window)。
設定されていない場合、XNClientWindow で設定されたウィンドウが使用される。

入力スタイルに関する追加の値が必要な場合、前編集領域の値は XNPreeditAttributes の名前で、スターテス領域の値は XNStatusAttributes の名前で指定し、それぞれの値には、XVaCreateNestedList() で作成した XVaNestedList を指定します。
XVaCreateNestedList
XVaNestedList XVaCreateNestedList(int dummy, ...);

XCreateIC() で、可変数の値を指定する時に使います。
返された値を、XNPreeditAttributes または XNStatusAttributes の値として指定します。

使用後は XFree() で解放します。

可変引数で指定する値は、XCreateIC() の時と同じです。
dummy は、C で可変引数を扱うために最低1つは引数が必要になるので、そのための、使用されないダミー引数です。値は何でも構いません。
コンテキスト作成後
入力コンテキストの作成後は、以下の処理をします。
イベントマスクの選択
入力メソッドで入力を行う各ウィンドウでは、入力メソッドで必要となるイベントのマスクを選択する必要があります。
基本的には、(KeyPressMask | KeyReleaseMask) です。

XGetICValues() で、入力メソッドから XNFilterEvents の値を取得して、入力メソッドが必要なイベントマスクを取得した後、その値をウィンドウの元々のイベントマスク値に加えて (OR)、XSelectInput() でイベントマスクを変更します。

unsigned long mask = 0;

XGetICValues(xic, XNFilterEvents, &mask, (void *)0);

XSelectInput(disp, window, mask | ...);

マスク値は unsigned long 型で取得するというように、Xlib マニュアルでは書かれていますが、実際は 32bit 整数として扱われているので、64bit OS の場合は、上位 32bit 部分が設定されません。
例えば、mask = (unsigned long)-1 で初期化すると、取得後の値は 0xffffffff00000003 というようになります。
マスク値を取得する前に、値を 0 で初期化してください。
入力フォーカスの変更時
トップレベルウィンドウ、または、ウィンドウ内の内部ウィジェットの入力フォーカスが変化した場合、入力コンテキストに対してフォーカスの変更を通知します。

void XSetICFocus(XIC ic);
void XUnsetICFocus(XIC ic);

フォーカスが変更されると、現在の入力状態がリセットされます。
イベントのフィルタリング
イベントループ内で X イベントを読み込んだ後、XFilterEvent() でイベントをフィルタリングし、必要なイベントを入力メソッド側で処理させます。

Bool XFilterEvent(XEvent *event, Window w);

w は、フィルタを適用するウィンドウです。
None で、XEvent 構造体の xany.window のウィンドウが使用されます。

True が返った場合は、入力メソッドによってイベントが処理された状態なので、そのイベントに関しては、それ以降何も行わないようにします。
KeyPress イベント時
KeyPress イベントが来た時は、XLookupString() の代わりに、以下のいずれかの関数で文字列や KeySym を取得します。

通常のキー押し時の文字列、または、入力メソッドによる変換後のテキストが取得できます。

int XmbLookupString(XIC ic, XKeyPressedEvent *event, char *buffer_return, int bytes_buffer,
    KeySym *keysym_return, Status *status_return);

int XwcLookupString(XIC ic, XKeyPressedEvent *event,
    wchar_t *buffer_return, int wchars_buffer, KeySym *keysym_return, Status *status_return);

Xmb はマルチバイト文字列で、Xwc はワイド文字列で取得します。
入力メソッドに関連付けられているロケールの文字列となります。

buffer_return文字列が返ります。
bytes_bufferbuffer_return のバッファバイト数
keysym_returnKeySym が返ります
status_return何が返されたかを示す値が返ります
戻り値buffer_return にセットされた文字列の長さ。
Xmb の場合はバイト数。Xwc の場合は文字数。

[Status]
XBufferOverflowbuffer_return のサイズが足りない。
必要なサイズが戻り値で返され、buffer_return および keysym_return の内容は変更されません。
XLookupNone文字列も KeySym もない。
buffer_return と keysym_return の内容は変更されず、関数は 0 を返します。
XLookupCharsbuffer_return のみに文字列が入った。
keysym_return 引数の内容は変更されません。
XLookupKeySymKeySym のみが入った。
buffer_return 引数の内容は変更されず、関数は 0 を返します。
XLookupBothKeySym と文字列の両方が返されます。
プログラム
入力コンテキストを作成し、入力メソッドによる入力ができるようにします。
変換前のテキストは、入力メソッド側が作成したウィンドウ上に表示され、変換が確定すると、その文字列が (端末に) 表示されます。

$ cc -o run 23-im2.c util.c -lX11

<23-im2.c>
#include <stdio.h>
#include <stdlib.h>
#include <X11/Xlib.h>
#include "util.h"

void _put_text(XKeyEvent *ev,XIC ic)
{
    char m[64],*buf;
    KeySym keysym;
    Status ret;
    int len;

    buf = m;

    len = XmbLookupString(ic, ev, m, 63, &keysym, &ret);

    //バッファ長さが足りない

    if(ret == XBufferOverflow)
    {
        buf = (char *)malloc(len + 1);
        if(!buf) return;

        len = XmbLookupString(ic, ev, buf, len, &keysym, &ret);
    }

    //表示

    buf[len] = 0;

    if(ret == XLookupBoth || ret == XLookupKeySym)
        printf("# KeySym: %lu\n", keysym);

    if(ret == XLookupBoth || ret == XLookupChars)
    {
        if(len == 1 && !(buf[0] >= 0x20 && buf[0] < 0x7f))
            //ASCII 制御文字
            printf("# <%d>\n", buf[0]);
        else
            printf("# '%s'\n", buf);
    }

    if(buf != m) free(buf);
}

int main(int argc,char **argv)
{
    Display *disp;
    XSetWindowAttributes attr;
    Window win;
    XEvent ev;
    XIM im = None;
    XIC ic = None;
    XIMStyle style;
    unsigned long mask;
    
    disp = XOpenDisplay(NULL);
    if(!disp) return 1;

    init_locale();

    //IM 開く

    im = XOpenIM(disp, NULL, NULL, NULL);
    if(!im)
    {
        printf("failed XOpenIM\n");
        goto END;
    }

    style = get_xim_style_nothing(im);
    if(!style)
    {
        printf("unsupported IM style\n");
        goto END;
    }

    //ウィンドウ作成

    attr.background_pixel = 0;
    attr.event_mask = ButtonPressMask | FocusChangeMask
        | KeyPressMask | KeyReleaseMask;

    win = XCreateWindow(disp, DefaultRootWindow(disp),
        0, 0, 200, 200, 0,
        CopyFromParent, CopyFromParent, CopyFromParent,
        CWBackPixel | CWEventMask, &attr);

    printf("window: 0x%lx\n", win);

    //IC 作成

    ic = XCreateIC(im,
        XNInputStyle, style,
        XNClientWindow, win,
        XNFocusWindow, win,
        (void *)0);

    if(!ic) goto END;

    //イベントマスク

    mask = 0;
    XGetICValues(ic, XNFilterEvents, &mask, (void *)0);

    printf("event mask: 0x%lx\n", mask);

    XSelectInput(disp, win, mask | attr.event_mask);

    //イベント

    XMapWindow(disp, win);

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

        if(XFilterEvent(&ev, None))
        {
            printf("-[filter] window(0x%lx) ", ev.xany.window);

            switch(ev.type)
            {
                case KeyPress:
                    printf("<KeyPress> keycode(%d)\n",
                        ev.xkey.keycode);
                    break;
                case KeyRelease:
                    printf("<KeyRelease> keycode(%d)\n",
                        ev.xkey.keycode);
                    break;
                case ClientMessage:
                    printf("<ClientMessage>\n");
                    break;
                default:
                    printf("<%d>\n", ev.type);
                    break;
            }
            continue;
        }

        switch(ev.type)
        {
            case KeyPress:
                printf("[KeyPress] keycode(%d)\n", ev.xkey.keycode);
                _put_text((XKeyEvent *)&ev, ic);
                break;
            case KeyRelease:
                printf("[KeyRelease] keycode(%d)\n", ev.xkey.keycode);
            case FocusIn:
                XSetICFocus(ic);
                printf("[FocusIn] window(0x%lx) mode(%d)\n",
                    ev.xfocus.window, ev.xfocus.mode);
                break;
            case FocusOut:
                XUnsetICFocus(ic);
                printf("[FocusOut] window(0x%lx) mode(%d)\n",
                    ev.xfocus.window, ev.xfocus.mode);
                break;
            case ButtonPress:
                goto END;
        }
    }

    //
END:
    if(ic) XDestroyIC(ic);
    if(im) XCloseIM(im);

    XCloseDisplay(disp);

    return 0;
}
解説
入力スタイルは、前編集が XIMPreeditNothing または XIMPreeditNone、スタータス領域が XIMStatusNothing または XIMStatusNone のスタイルを選択しています。

この場合、変換前のテキストやステータスは、入力メソッドが作成したウィンドウによって表示される (もしくは表示されない) ので、アプリケーション側で必要になる処理はありません。

入力メソッドが作成するウィンドウは、コンテキストの XNClientWindow で指定されたウィンドウを元に、位置などが調整されます。
通常はウィンドウの下端に表示されます。
入力メソッドのホットキーの押し時
入力メソッドで設定されているホットキーを押すと、入力メソッドによる入力が開始されます。

# キー押し
[FocusIn] window(0x1a00003) mode(1892)
-[filter] window(0x1a00003) <KeyPress> keycode(49)
# キー離し
-[filter] window(0x1a00003) <KeyRelease> keycode(49)
-[filter] window(0x1a00002) <ClientMessage>
[KeyRelease] keycode(49)
[FocusIn] window(0x1a00003) mode(1892)

keycode 49 は、半角/全角キーです。
XFilterEvent() によって、このキーの Press と Release イベントが入力メソッド側で処理され、クライアントのウィンドウではない 0x1a00002 で、ClientMessage イベントも処理されています。

「$ xwininfo -root -tree」で調べてみると、0x1a00002 はルートウィンドウの子になっています。
数値的に、XCreateWindow() で作成したウィンドウよりも小さい値なので、おそらく XOpenIM() 時に、入力メソッドがこのクライアント用に作成したウィンドウだと思われます。

その後、クライアントに通常の KeyRelease が来ています。

FocusIn/Out 時の mode に、本来定義されている値 (0〜3) と異なる値が指定されていますが、おそらく入力メソッド関連の値であると思われます。
ただし、クライアントが明示的にウィンドウマネージャを介してフォーカスを受け取るように指定している場合 (WM_TAKE_FOCUS)、この FocusIn イベントは来ません。
入力中のキー押し時
[FocusIn] window(0x1a00003) mode(1892)
-[filter] window(0x1a00003) <KeyPress> keycode(38)
-[filter] window(0x1a00003) <KeyRelease> keycode(38)
-[filter] window(0x1a00002) <ClientMessage>
[KeyRelease] keycode(38)
[FocusIn] window(0x1a00003) mode(1892)

keycode 38 は 'a' です。
前編集ウィンドウに、「あ」が表示されます。
変換確定時
[FocusIn] window(0x1a00003) mode(1892)
-[filter] window(0x1a00003) <KeyPress> keycode(36)
-[filter] window(0x1a00002) <ClientMessage>
[KeyPress] keycode(0)
# '秋'
-[filter] window(0x1a00003) <KeyRelease> keycode(36)
-[filter] window(0x1a00002) <ClientMessage>
[KeyRelease] keycode(36)
[FocusIn] window(0x1a00003) mode(1892)

keycode 36 は Enter キーです。
「あき」と入力した後、Space キーで変換し、Enter キーで確定すると、上記のようになります。

Enter キーが押されたあと、KeyPress イベントで keycode = 0 が来ます。
有効なキーコードは 8〜255 であるため、このキーコードは無効な値となります。

変換後の文字列は KeyPress イベントで取得する必要があるので、アプリケーションがそれを取得できるようにするために、keycode = 0 の KeyPress が送られてきます。
keycode = 0 は、入力メソッドによる変換後のテキストがある場合にのみ使われます。

その結果、XmbLookupString() で "秋" の文字列が取得できます。
入力途中のフォーカス変更時
入力の途中でフォーカスが変更された場合、FocusOut イベントで XUnsetICFocus() が実行されることにより、入力メソッドのフォーカスが失われて、前編集ウィンドウが消えます。

この場合、変換前のテキストは破棄されます。