X11: キーボードのグラブ

キーボードのグラブ
キーボードも、ポインタと同じように、アクティブグラブとパッシブグラブを行うことが出来ます。

明示的にグラブすると、入力フォーカスがない状態でも、指定ウィンドウで常にキーイベントを受信できます。

パッシブグラブは、主にホットキーとして扱います。
指定されたキーが押された時、自動的にアクティブなグラブが開始されます。
グラブ関数
アクティブなグラブ
int XGrabKeyboard(Display *display, Window grab_window, Bool owner_events,
    int pointer_mode, int keyboard_mode, Time time);

void XUngrabKeyboard(Display *display, Time time);

基本的に、ポインタのグラブ時と同じです。

XGrabKeyboard() でキーボードをグラブし、grab_window にキーイベントを報告するようにします。
XUngrabKeyboard() でアクティブなグラブを解除します。
パッシブグラブ
void XGrabKey(Display *display, int keycode, unsigned int modifiers, Window grab_window,
    Bool owner_events, int pointer_mode, int keyboard_mode);

void XUngrabKey(Display *display, int keycode, unsigned int modifiers, Window grab_window);

指定キーと修飾子が押された時に、アクティブなグラブを開始します。
keycode のキーが離されると、自動でグラブが解除されます。

keycode は、物理キー番号です。

以下の条件に当てはまる時、グラブが実行されます。

  • キーボードがグラブされておらず、指定キーと修飾子が押されていて、他の修飾子が押されていない場合。
  • grab_window がフォーカスウィンドウまたはその祖先であるか、grab_window がフォーカスウィンドウの子孫でその中にポインタがある場合。
  • 同じキーの組み合わせに対するパッシブグラブが、grab_window の祖先に存在しないこと。

他のクライアントが、同じウィンドウ上で同じキーの組み合わせを使用している場合、BadAccess エラーが発生します。
AnyModifier または AnyKey を使用する場合、いずれかの組み合わせで競合するグラブがある場合、BadAccess エラーが発生します。
キーコード
X において、KeyCode は物理キー番号、KeySym はキーシンボルです。

物理キー番号は、物理的なキーボード上にある各ボタンごとの番号です。
キーシンボルは、Enter キーやスペースキーなど、各キーの機能を識別するための番号で、<X11/keysym.h> のインクルードファイルで、「XK_」から始まるマクロで定義されています。

KeyPress イベントなどで送られてくるキーコードは物理キー番号なので、この番号だけでは、このキーが何のキーなのかを判断することはできません。

Xlib 内部に、各キーコードに対応する KeySym のリストが存在するので、その情報を使って、キーコードと KeySym を変換することができます。
KeySym →キーコード
KeyCode XKeysymToKeycode(Display *display, KeySym keysym);

指定した KeySym に対応しているキーコードを取得します。
キーコードが定義されていない場合、ゼロを返します。
プログラム
ルートウィンドウに、F11 キーのパッシブグラブを設定します。
(ウィンドウマネージャや他のクライアントによって、すでに F11 キーが設定されている場合は動作しないので、その場合は他のキーを指定してください)

このウィンドウに入力フォーカスがない状態でも、F11 キーを押すと、キーイベントが来るのを確認してください。
ポインタボタンが押されると終了します。

<14-keygrab.c>
#include <stdio.h>
#include <X11/Xlib.h>
#include <X11/keysym.h>
#include "util.h"

int main(int argc,char **argv)
{
    Display *disp;
    XSetWindowAttributes attr;
    Window win,root;
    XEvent ev;
    int keycode;
    
    disp = XOpenDisplay(NULL);
    if(!disp) return 1;

    set_display(disp);

    root = DefaultRootWindow(disp);

    //ウィンドウ作成

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

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

    XMapWindow(disp, win);

    //グラブ

    keycode = XKeysymToKeycode(disp, XK_F11);

    XGrabKey(disp, keycode, AnyModifier, root,
        False, GrabModeAsync, GrabModeAsync);

    printf("root: 0x%lx\nwindow: 0x%lx\ngrab: %d\n", root, win, keycode);

    //イベント

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

        switch(ev.type)
        {
            case KeyPress:
            case KeyRelease:
                printf("- [Key%s] window(0x%lx) x(%d) y(%d) keycode(%u)\n",
                    (ev.type == KeyPress)? "Press": "Release",
                    ev.xkey.window, ev.xkey.x, ev.xkey.y, ev.xkey.keycode);

                put_state(ev.xkey.state);

                if(ev.type == KeyPress && ev.xkey.keycode == keycode
                    && (ev.xkey.state & ShiftMask))
                {
                    XUngrabKeyboard(disp, ev.xkey.time);
                    printf("# ungrab\n");
                }

                fflush(stdout);
                break;
            case FocusIn:
            case FocusOut:
                printf("* [Focus%s] window(0x%lx) mode(%d) detail(%d)\n",
                    (ev.type == FocusIn)? "In": "Out",
                    ev.xkey.window, ev.xfocus.mode, ev.xfocus.detail);
                fflush(stdout);
                break;
            case ButtonPress:
                goto END;
        }
    }

END:
    XCloseDisplay(disp);

    return 0;
}
解説
ルートウィンドウにパッシブグラブを設定
クライアントが作成したウィンドウに対して、キーのパッシブグラブを設定しても、ほとんど意味はありません。
なぜなら、そのウィンドウに入力フォーカスがないと、グラブが動作しないからです。

キーのパッシブグラブの開始条件には、以下の文があります。

「grab_window がフォーカスウィンドウまたはその祖先であるか、grab_window がフォーカスウィンドウの子孫でその中にポインタがあること」

つまり、他のトップレベルウィンドウに入力フォーカスがある状態で、任意のクライアントがパッシブグラブを実行するには、すべてのトップレベルウィンドウの祖先 (ルートウィンドウ) に対して、パッシブグラブを設定する必要があります

X のイベントは、ウィンドウに対して送られてくるのではなく、関数を実行する時に指定した Display のクライアントに対して送られてくるので、X サーバーや他のクライアントが作成したウィンドウを対象に指定したとしても、各クライアントで、そのウィンドウに関するイベントを取得することができます。
エラー
他のクライアントが、ルートウィンドウに対して同じキーをパッシブグラブしている場合、BadAccess エラーが出るので注意してください。

X エラーハンドラ関数内で、BadAccess が来たかどうかを判定することで、関数が失敗したかどうかを判断できます。
終了時
XCloseDisplay() でクライアントを閉じた時、このクライアントが設定した、ポインタやキーボードのアクティブグラブやパッシブグラブは、すべて自動的に解除されるので、終了時に明示的にグラブを解放する必要はありません。
ウィンドウにフォーカスがある状態で F11 を押した時
* [FocusIn] window(0x1c00001) mode(0) detail(3)
<キー押し>
* [FocusOut] window(0x1c00001) mode(1) detail(0)
- [KeyPress] window(0x764) x(319) y(904) keycode(95)
  state(0x10:Mod2)
<キー離し>
- [KeyRelease] window(0x764) x(319) y(904) keycode(95)
  state(0x10:Mod2)
* [FocusIn] window(0x1c00001) mode(2) detail(0)

プログラムウィンドウに入力フォーカスがある状態で F11 を押した場合、キーボードのグラブが開始されたことによって、入力フォーカスが一旦ルートウィンドウ (0x764) に移ります。

このウィンドウに FocusOut が来た後、ルートウィンドウに FocusIn が送られます。
FocusOut 時の mode が NotifyGrab (1) になっているので、キーボードのグラブによるフォーカスアウトであることがわかります。

その後、パッシブグラブが開始されたことによる KeyPress イベントが、ルートウィンドウを対象にして、このクライアントに来ます (ev.xkey.window がルートウィンドウになっています)。
ここで、押されたキーに関する処理を行います。

キーを離すと、自動的にグラブは解除されるので、KeyRelease イベントが来た後、入力フォーカスが、ルートウィンドウから本来のウィンドウに戻ります。

ルートウィンドウに FocusOut が送られ、このウィンドウに FocusIn が来ます。
FocusIn 時の mode が NotifyUngrab (2) になっているので、キーボードのアングラブによるフォーカスインであることがわかります。
他のウィンドウにフォーカスがある状態で F11 を押した時
<キー押し>
* [FocusOut] window(0x1c00001) mode(0) detail(3)
- [KeyPress] window(0x764) x(1351) y(646) keycode(95)
  state(0x10:Mod2)
<キー離し>
- [KeyRelease] window(0x764) x(1351) y(646) keycode(95)
  state(0x10:Mod2)

他のトップレベルウィンドウに入力フォーカスがある状態で F11 を押した場合、同じように FocusOut が来た後、KeyPress が来ます。

キーが離された時は、ルートウィンドウに FocusOut が送られて、本来のフォーカスがあったウィンドウに FocusIn が送られるので、このクライアントにはフォーカス関連のイベントは来ません。
グラブの解除
パッシブグラブによって、アクティブなグラブが開始された後、キーが離される前に明示的にグラブを解除したい場合は、XUngrabKeyboard() を実行します。
グラブによるフォーカス変更の例
ウィンドウでフォーカス変更が行われた場合は、ウィンドウ内のエディット上でカーソルを消すなど、内部で処理を行う場合があります。

ただし、キーボードのグラブによって生成された FocusOut/FocusIn は、一時的なフォーカス変更によるものなので、XFocusChangeEvent 構造体の mode 値が NotifyGrab または NotifyUngrab の場合、フォーカスイベントをどのように扱うかは、それぞれで判断してください。
Alt+Tab
Alt+Tab によって、アクティブにするウィンドウをリストから選択する場合、以下のようになります。
(ウィンドウマネージャによって実装が異なります)

<Alt+Tab 押し時>
* [FocusOut] window(0x1c00001) mode(1) detail(0)
| ウィンドウ内にポインタがある場合
| * [FocusOut] window(0x1c00001) mode(1) detail(5)
<終了時>
* [FocusIn] window(0x1c00001) mode(2) detail(3)

プログラムウィンドウにフォーカスがある状態で、Alt+Tab を押して、そこで同じウィンドウを選択した場合、キーのグラブによって FocusOut (NotifyGrab) が来た後、グラブの解除によって FocusIn (NotifyUngrab) が来るので、フォーカスは実質的に変更されていません。

なお、detail が NotifyPointer (5) の場合は、ポインタがウィンドウ内にいるという情報なので、あまり気にしなくて良いです。

detail が NotifyNonlinear (3) の場合は、変化したフォーカスウィンドウが、親や子の関係にないということです。つまり、別のトップレベルウィンドウの場合などです。

Alt+Tab の場合、キーグラブによってルートウィンドウにフォーカスが移った後、ウィンドウマネージャがウィンドウを選択するためのウィンドウを表示することで、そこにフォーカスが移るので、Alt+Tab の終了時は、そのウィンドウから元のウィンドウへとフォーカスが移ります。

<Alt+Tab 押し時>
* [FocusOut] window(0x1c00001) mode(1) detail(0)
<終了時>
* [FocusIn] window(0x1c00001) mode(2) detail(3)
* [FocusOut] window(0x1c00001) mode(0) detail(3)

Alt+Tab を押して、別のウィンドウを選択した場合、グラブによる FocusOut と FocusIn の後、別のウィンドウがアクティブになるので、通常の FocusOut (NotifyNormal) が来ています。
フレームウィンドウの操作時
ウィンドウマネージャによって動作が異なるかもしれませんが、タイトルバーをドラッグしてウィンドウ位置を移動したり、ウィンドウ枠をドラッグしてサイズを変更した場合も、FocusOut/FocusIn が来る場合があります。

おそらく、ESC キーなどで動作をキャンセルできるようにするため、キーボードのグラブが行われているものと思われます。

<フレーム部分でボタンを押した>
* [FocusOut] window(0x1c00001) mode(1) detail(3)
<ボタンを離した>
* [FocusIn] window(0x1c00001) mode(2) detail(3)

この場合も、グラブによる FocusOut/FocusIn イベントが来ます。

ここで注意するべきなのは、FocusOut と FocusIn の間で、ドラッグ中にウィンドウの位置やサイズが変更されているかもしれないということです。

FocusOut と FocusIn の間でウィンドウに変化がなければ、グラブ時のフォーカスイベントを無視しても問題ありませんが、このように、ウィンドウに変化があるようなフォーカス移動が行われている可能性もあるので、注意してください。