X11: 入力メソッド (3) - On-The-Spot

On-The-Spot
On-The-Spot の入力スタイルを使用する場合、入力中のテキストを、自分のウィンドウ内で表示して描画することができます。

XIMPreeditCallbacks のスタイルを選択し、XIC 作成時に、XNPreeditAttributes で各コールバック関数を設定する必要があります。
プログラム
On-The-Spot で入力メソッドからの入力を処理します。
入力メソッドが On-The-Spot に対応していない場合は、実行できません。

<24-im3.c>
#include <stdio.h>
#include <X11/Xlib.h>
#include "util.h"


/* 前編集開始 */

static int _preedit_start(XIC ic,XPointer client_data,XPointer call_data)
{
    printf("{start}\n");

    //前編集文字列の最大サイズ (-1 で制限なし)
    return -1;
}

/* 前編集終了 */

static void _preedit_done(XIC ic,XPointer client_data,XPointer call_data)
{
    printf("{done}\n");
}

/* 前編集描画 */

static void _preedit_draw(XIC ic,XPointer client_data,XIMPreeditDrawCallbackStruct *p)
{
    XIMFeedback *fb;
    int i;

    printf("--- {draw} ---\n"
        "caret: %d\n" "chg_first: %d\n" "chg_length: %d\n",
        p->caret, p->chg_first, p->chg_length);

    if(!p->text)
        printf("text: NULL\n");
    else
    {
        printf("[text]\n" "length: %d\n" "is_wchar: %d\n",
            p->text->length, p->text->encoding_is_wchar);

        if(p->text->string.multi_byte)
        {
            if(p->text->encoding_is_wchar)
                printf("string: '%ls'\n", p->text->string.wide_char);
            else
                printf("string: '%s'\n", p->text->string.multi_byte);
        }

        if(p->text->feedback)
        {
            fb = p->text->feedback;
            printf("feedback:");

            for(i = 0; i < p->text->length; i++)
                printf(" 0x%lx", *(fb++));

            printf("\n");
        }
    }

    printf("--------\n");
}

/* 前編集カーソル */

static void _preedit_caret(XIC ic,XPointer client_data,XIMPreeditCaretCallbackStruct *p)
{
    printf("{caret}\n" "position: %d\n" "direction: %d\n" "style: %d\n",
        p->position, p->direction, p->style);
}

/* XIC 作成 */

static XIC _create_xic(XIM im,XIMStyle style,Window win)
{
    XIC ic;
    XIMCallback cb[4];
    XVaNestedList valist;

    cb[0].callback = (XIMProc)_preedit_start;
    cb[1].callback = (XIMProc)_preedit_done;
    cb[2].callback = (XIMProc)_preedit_draw;
    cb[3].callback = (XIMProc)_preedit_caret;

    valist = XVaCreateNestedList(0,
        XNPreeditStartCallback, cb,
        XNPreeditDoneCallback, cb + 1,
        XNPreeditDrawCallback, cb + 2,
        XNPreeditCaretCallback, cb + 3,
        (void *)0);

    //XIC 作成

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

    XFree(valist);

    return ic;
}

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(im, XIMPreeditCallbacks);
    if(!style)
    {
        printf("unsupported XIMPreeditCallbacks\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 = _create_xic(im, style, win);
    if(!ic) goto END;

    //イベントマスク

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

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

    //イベント

    XMapWindow(disp, win);

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

        if(XFilterEvent(&ev, None))
            continue;

        switch(ev.type)
        {
            case KeyPress:
                printf("[KeyPress] keycode(%d)\n", ev.xkey.keycode);
                put_xic_text(&ev, ic);
                break;
            case KeyRelease:
                printf("[KeyRelease] keycode(%d)\n", ev.xkey.keycode);
            case FocusIn:
                XSetICFocus(ic);
                break;
            case FocusOut:
                XUnsetICFocus(ic);
                break;
            case ButtonPress:
                goto END;
        }
    }

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

    XCloseDisplay(disp);

    return 0;
}
コールバック関数の設定
XCreateIC() 時に、XNPreeditAttributes で前編集に関する属性を設定します。
値は、XVaCreateNestedList() で作成した XVaNestedList 型 (void *) を指定します。

typedef void (*XIMProc)(XIM, XPointer, XPointer);

typedef struct {
  XPointer client_data; //関数に渡される値
  XIMProc callback; //関数
} XIMCallback;

各コールバックの値は、XIMCallback 構造体のポインタで指定します。
client_data は、callback 関数の2番目の引数に渡されます。今回は client_data は使わないので、設定していません。
XNPreeditStartCallback
前編集が開始された時 (最初の文字が入力された時) に来る関数です。

戻り値で、前編集文字列の最大サイズを指定できます。-1 の場合は、制限なしとなります。
XNPreeditDoneCallback
前編集が終了した時 (すべてのテキストが確定した時や、キャンセルされた時) に来る関数です。
XNPreeditDrawCallback
前編集のテキストが変化する時や、各文字の描画スタイルが変更される時に来る関数です。
XNPreeditCaretCallback
前編集内で文字カーソルを移動する必要がある時に来る関数です。
ただし、この関数は全く来ない場合があります。その場合、draw 関数の方でカーソル位置を判断します。
前編集描画のコールバック関数
XIMPreeditDrawCallbackStruct 構造体に、関連する情報が設定されています。

typedef struct _XIMPreeditDrawCallbackStruct {
  int caret;      //カーソルの文字位置
  int chg_first;  //変更される文字位置
  int chg_length; //変更される文字数
  XIMText *text;
} XIMPreeditDrawCallbackStruct;

typedef struct _XIMText {
  unsigned short length; //文字数
  XIMFeedback *feedback;
  Bool encoding_is_wchar; //string がワイド文字列か
  union {
    char *multi_byte;
    wchar_t *wide_char;
  } string;
} XIMText;

typedef unsigned long XIMFeedback;

text, feedback, text->string のポインタは、NULL の場合があるので、注意してください。
テキストの変更
  • <text != NULL で chg_length != 0 の場合>
    chg_first 文字位置から chg_length 文字分のテキストを、text の文字列に置き換えます。
  • <text != NULL で chg_length == 0 の場合>
    chg_first 文字位置に、text の文字列を挿入します。
  • <text == NULL で chg_length != 0 の場合>
    chg_first 文字位置から chg_length 文字分のテキストを削除します。

上記のテキスト編集を行った後、文字カーソル位置を、caret の文字位置に移動させます。

# 'a' を入力した場合
caret: 1
chg_first: 0
chg_length: 0
[text]
length: 1
is_wchar: 0
string: 'あ'
feedback: 0x2

# DEL キーで1文字削除した場合
caret: 0
chg_first: 0
chg_length: 1
text: NULL

# 'a' の後に 'a' を入力した場合
caret: 2
chg_first: 0
chg_length: 1
[text]
length: 2
is_wchar: 0
string: 'ああ'
feedback: 0x2 0x2

最初に 'a' を入力すると、前編集が開始されます。
chg_length == 0 なので、chg_first (0) の位置に 'あ' を挿入することになります。
文字を挿入した後、caret == 1 で、文字カーソルの位置を1文字の位置に移動します ('あ' の後にカーソルが来る)。

その後 Delete キーを押すと、text == NULL で chg_length == 1 なので、chg_first (0) から1文字を削除します。
文字カーソル位置は caret == 0 で、先頭位置になります。
この場合、編集中の文字がなくなるので、前編集が終了します。

その後 'a' 'a' と続けて入力した場合、chg_first (0) から1文字分のテキストを、text の文字列 ('ああ' の2文字) に置き換えます。
これは、すでにある 'あ' の文字を削除して、'ああ' に置き換えるということです。

text->encoding_is_wchar が True の場合、文字列はワイド文字列で、False の場合はマルチバイト文字列です。
入力メソッドのロケールによる文字列となります。
text->length は文字数です (マルチバイト文字列の場合、バイト数ではない)。
各文字の描画スタイル
前編集テキストは、変換の状況によって、一部の文字を強調表示する必要があります。
各文字のスタイルは、text->feedback の値で判断できます。

text->feedback == NULL の場合、すべての文字は通常のスタイルとして扱います。
NULL でない場合は、text->length 個の配列となり、string の各文字に対応します。

なお、string のポインタ (multi_byte or wide_char) が NULL の場合は、テキストの変更がなく、chg_first の位置から text->length 文字数分の各文字のスタイルのみを変更することになります。

#define XIMReverse              1L
#define XIMUnderline            (1L<<1)
#define XIMHighlight            (1L<<2)
#define XIMPrimary              (1L<<5)
#define XIMSecondary            (1L<<6)
#define XIMTertiary             (1L<<7)
#define XIMVisibleToForward     (1L<<8)
#define XIMVisibleToBackward    (1L<<9)
#define XIMVisibleToCenter      (1L<<10)

XIMReverse は、前景色と背景色を反転。
XIMUnderline は、下線。
XIMHighlight, XIMPrimary, XIMSecondary, XIMTertiary は、XIMReverse・XIMUnderline とは別の形で強調表示を行います。

caret: 0
chg_first: 0
chg_length: 8
[text]
length: 6
is_wchar: 0
string: '回答して待つ'
feedback: 0x1 0x1 0x1 0x1 0x2 0x2

例えば、「かいとうしてまつ」と入力して変換した場合、「回答して」が反転、「待つ」が下線になります。
この状態で右矢印キーを押すと、変換対象が「待つ」に切り替わります。

caret: 4
chg_first: 0
chg_length: 6
[text]
length: 6
is_wchar: 0
string: '回答して待つ'
feedback: 0x2 0x2 0x2 0x2 0x1 0x1

先頭から6文字が「回答して待つ」に置き換わり (結果として変わらない)、カーソル位置は「待つ」の先頭位置に変わります。
また、feedback の値が変化しています。

入力メソッドによって動作は異なるかもしれませんが、このように、string に対して、常に現在の前編集テキストすべてが指定されているような場合は、内部で文字列を保持する必要はなく、string の文字列を表示すればよいことになります。
後は、feedback の値に基づいて、各文字ごとにスタイルを変えて描画します。