X11: ポインタのグラブ

ポインタのグラブ
通常は他のウィンドウに送られるポインタイベントを、常に特定のウィンドウで受信したい場合があります。
そのような場合、「ポインタのグラブ」を行います。

ポップアップウィンドウの表示中、ポップアップウィンドウ外でボタンが押された時は、ポップアップウィンドウを閉じたい場合があります。
この場合、ポインタをグラブして、ポインタイベントを常にポップアップウィンドウで取得するようにします。
ButtonPress イベントが来た時、イベントの window がポップアップウィンドウ以外であれば、ポップアップを閉じます。

ポインタのグラブは XGrabPointer 関数で、グラブの解除は XUngrabPointer 関数で行います。
グラブの種類
ポインタとキーボードのグラブには、「アクティブ」と「パッシブ」の2種類があります。

アクティブなグラブは、クライアントが任意のタイミングで明示的にグラブを行います。
パッシブグラブは、指定したボタンまたはキーが押された時に、自動的にアクティブグラブを開始し、ボタンやキーが離された時にグラブを解除します。
アクティブグラブ
ポインタをグラブする
int XGrabPointer(Display *display, Window grab_window, Bool owner_events, unsigned int event_mask,
    int pointer_mode, int keyboard_mode, Window confine_to, Cursor cursor, Time time);

アクティブにポインタをグラブし、成功した場合は GrabSuccess を返します。
以降のポインタイベントは、グラブしたクライアント (display で指定したクライアント) にのみ報告されます。

グラブ時、擬似的に EnterNotify と LeaveNotify イベントが生成されます。
成功した場合、最終ポインタグラブ時刻は、引数で指定された時刻に設定されます (CurrentTime は現在の X サーバー時刻に置き換えられます)。

grab_windowイベントを報告するウィンドウ
owner_eventsFalse の場合、ポインタイベントはすべて grab_window に報告され、引数の event_mask で指定されたイベントのみ報告されます。

True の場合、ポインタイベントが、通常であればこのクライアントに報告されるイベントの場合、通常通りのイベントとして報告されます。
それ以外の場合は、grab_window に報告され、引数の event_mask で選択されたイベントのみ報告されます。
クライアントが複数のウィンドウを管理していて、グラブ中もそれぞれのウィンドウでイベントを処理したいが、他のクライアントが作成したウィンドウ上のイベントは grab_window で処理したい場合に使います。

どちらの場合も、報告されないイベントは破棄されます。
event_maskグラブ中、クライアントに報告するイベントのマスク。
指定できるのは以下のみ。

ButtonPressMask, ButtonReleaseMask, EnterWindowMask, LeaveWindowMask
PointerMotionMask, PointerMotionHintMask
Button1MotionMask, Button2MotionMask, Button3MotionMask, Button4MotionMask
Button5MotionMask, ButtonMotionMask, KeymapStateMask
pointer_modeポインタイベントを凍結するか。

GrabModeAsync の場合、ポインタイベントの処理は通常通り続行され、ポインタが現在このクライアントによって凍結されている場合は、ポインタのイベントの処理が再開されます。

GrabModeSync の場合、ポインタイベンを凍結し、グラブしているクライアントが XAllowEvents を呼び出すか、ポインタのグラブが解放されるまで、X サーバーはそれ以上のポインタイベントを生成しません。
ポインタが凍結されている間、イベントは後で処理するためにサーバーのキューに入れられます。
keyboard_modeキーボードイベントを凍結するか。
GrabModeAsync, GrabModeSync
confine_toグラブ中、カーソルの位置を、指定したウィンドウの外に移動できないように制限します。None でなし。

confine_to ウィンドウは、grab_window と関係がある必要はありません。
ポインタが confine_to ウィンドウ内に表示されていない場合、グラブがアクティブになる直前に、ポインタは最も近い境界に自動的に移動され、通常どおり Enter/Leave イベントが生成されます。
その後 confine_to ウィンドウのサイズが変更されるなどした場合は、ポインタは必要に応じて自動的に移動し、ウィンドウ内の位置に留まります。
cursorグラブ中に表示されるカーソル。
None が指定されている場合は、ポインタが grab_window またはそのサブウィンドウのいずれかの上にあるとき、ウィンドウの通常のカーソルが表示されます。
それ以外 (grab_window かその子ではない) の場合は、grab_window のカーソルが表示されます。
timeグラブを行う時間、または CurrentTime (現在の時間)
戻り値成功時は GrabSuccess。
GrabNotViewable: grab_window または confine_to ウィンドウが表示されていない場合、または confine_to ウィンドウがルートウィンドウの完全に外側にある場合。
AlreadyGrabbed: ポインタが他のクライアントによって、アクティブにグラブされている場合。
GrabFrozen: 別のクライアントのアクティブなグラブによって、ポインタが凍結されている場合。
GrabInvalidTime: time が最後のポインタグラブ時刻より早いか、現在の X サーバー時刻よりも遅い場合。

owner_events は基本的に False、pointer_mode と keyboard_mode は基本的に GrabModeAsync に指定します。
グラブを解放する
void XUngrabPointer(Display *display, Time time);

現在のアクティブグラブを解放します。
(明示的にグラブされた場合、またはパッシブグラブでグラブが開始された場合、または自動的にグラブされた場合)

time が最後のポインタグラブ時刻よりも早い場合、または、現在の X サーバー時刻よりも後の場合、グラブを解放しません。
擬似的な EnterNotify と LeaveNotify イベントも生成されます。
自動グラブ
ウィンドウでボタンが押された時、アクティブなグラブが行われておらず、そのウィンドウを含む上位のウィンドウで、押されたボタンのパッシブグラブが設定されていない場合、X サーバーは、クライアントのアクティブグラブを自動的に開始します。
ボタンが離されると、グラブは自動で解放されます。

そのため、明示的にグラブを行わなくても、ボタンが押された時は、デフォルトで常にグラブされます。

X サーバーによる自動グラブは、XGrabPointer() に以下の引数を渡すのと同じです。

pointer_mode:  GrabModeAsync
keyboard_mode: GrabModeAsync
owner_events:  イベントマスクで OwnerGrabButtonMask が選択されている場合は True、それ以外は False
confine_to:    None
cursor:        None

上記以外の設定でグラブしたい場合は、ButtonPress 時に明示的にグラブする必要があります。

自動グラブが行われている間でも、XUngrabPointer() でグラブを解放したり、XChangeActivePointerGrab() でパラメータを変更できます。
プログラム
グラブ時に発生する EnterNotify と LeaveNotify イベントのパラメータを確認するため、背景が黒と白の2つのウィンドウを表示します。

Button1 (通常は左ボタン) が押された時、そのウィンドウでグラブを開始し、ボタンが離されるとグラブを解除します。
Space キーが押された時、現在アクティブなウィンドウでグラブを開始し、再び Space キーが押されるとグラブを解除します。
Enter キーで終了します。

08-grab.c
#include <stdio.h>
#include <X11/Xlib.h>
#include <X11/keysym.h>

int main(int argc,char **argv)
{
    Display *disp;
    XSetWindowAttributes attr;
    Window win[2];
    XEvent ev;
    KeySym key;
    int i,fgrab = 0;
    
    disp = XOpenDisplay(NULL);
    if(!disp) return 1;

    //ウィンドウ作成

    for(i = 0; i < 2; i++)
    {
        attr.background_pixel = (i)? 0xffffff: 0;
        attr.event_mask = ButtonPressMask | ButtonReleaseMask
            | PointerMotionMask | ButtonMotionMask
            | EnterWindowMask | LeaveWindowMask | KeyPressMask;

        win[i] = XCreateWindow(disp, DefaultRootWindow(disp),
            i * 100, 0, 200, 200, 0,
            CopyFromParent, CopyFromParent, CopyFromParent,
            CWBackPixel | CWEventMask, &attr);

        XMapWindow(disp, win[i]);
    }

    //イベント

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

        switch(ev.type)
        {
            case ButtonPress:
                printf("* [Press] x(%d) y(%d) button(%d)\n",
                    ev.xbutton.x, ev.xbutton.y, ev.xbutton.button);

                if(fgrab == 0 && ev.xbutton.button == Button1)
                {
                    XGrabPointer(disp, ev.xbutton.window, False,
                        ButtonPressMask | ButtonReleaseMask | EnterWindowMask | LeaveWindowMask
                        | PointerMotionMask | ButtonMotionMask,
                        GrabModeAsync, GrabModeAsync, None, None, CurrentTime);

                    fgrab = 1;
                    printf("# Grab - Button1\n");
                }

                fflush(stdout);
                break;
            case ButtonRelease:
                if(fgrab == 1 && ev.xbutton.button == Button1)
                {
                    XUngrabPointer(disp, CurrentTime);
                    fgrab = 0;

                    printf("# Ungrab - Button1\n");
                }
                break;
            case MotionNotify:
                printf("- [Motion] x(%d) y(%d)\n", ev.xmotion.x, ev.xmotion.y);
                fflush(stdout);
                break;
            case EnterNotify:
            case LeaveNotify:
                printf("$ [%s] <%s> x(%d) y(%d) mode(%d) detail(%d)\n",
                    (ev.type == EnterNotify)? "Enter": "Leave",
                    (ev.xcrossing.window == win[0])? "black": "white",
                    ev.xcrossing.x, ev.xcrossing.y,
                    ev.xcrossing.mode, ev.xcrossing.detail);
                fflush(stdout);
                break;

            case KeyPress:
                key = XLookupKeysym((XKeyEvent *)&ev, 0);
                if(key == XK_Return || key == XK_KP_Enter)
                    goto END;
                else if(key == ' ')
                {
                    if(fgrab == 2)
                    {
                        XUngrabPointer(disp, CurrentTime);
                        fgrab = 0;
                        printf("# Ungrab - key\n");
                    }
                    else if(fgrab == 0)
                    {
                        XGrabPointer(disp, ev.xkey.window, False,
                            ButtonPressMask | ButtonReleaseMask | EnterWindowMask | LeaveWindowMask
                            | PointerMotionMask | ButtonMotionMask,
                            GrabModeAsync, GrabModeAsync, None, None, CurrentTime);

                        fgrab = 2;
                        printf("# Grab - key\n");
                    }
                }
                break;
        }
    }

END:
    XCloseDisplay(disp);

    return 0;
}
解説
Button1 を押してグラブした場合
※X サーバーは自動グラブを行うため、実際は XGrabPointer()/XUngrabPointer() を行わなくても、同じ動作になります。

Button1 を押したウィンドウ上で、Button1 を離すと、Enter/Leave イベントが発生しないのを確認してください。

ボタンを押したウィンドウではない方で、ボタンを離す
ボタンを押したウィンドウではない方 (黒なら白、白なら黒のウィンドウ) の上でボタンを離すと、Leave と Enter イベントが発生するのを確認してください。

# Ungrab - Button1
$ [Leave] <white> x(-99) y(60) mode(2) detail(3)
$ [Enter] <black> x(129) y(56) mode(2) detail(3)

グラブ中のポインタは、グラブウィンドウ上で表示されているという扱いになります。

そのため、グラブウィンドウ以外の上でグラブを解除すると、ポインタがグラブウィンドウから、本来のウィンドウ上に表示されることになるので、ボタンを押した方で Leave イベント、ボタンを離した方で Enter イベントが生成されます。

この時、XCrossingEvent 構造体の mode 値は、グラブ解除によってイベントが生成されたという意味で、NotifyUngrab (2) になります。

ルートウィンドウ上でボタンを離す
ボタンを押してグラブした後、ルートウィンドウ (デスクトップの背景) 上でボタンを離してみてください。

# Ungrab - Button1
$ [Leave] <black> x(-68) y(35) mode(2) detail(0)

Leave 時の detail が NotifyAncestor (0) になっています。
なお、他のトップレベルウィンドウの上で離した時は、NotifyNonlinear (3) になります。

NotifyAncestor は、ウィンドウの祖先 (親またはさらにその上) という意味です。
ルートウィンドウは、プログラムウィンドウの親にあたるので、グラブを解除した後、ポインタはグラブウィンドウの親の上に表示されたという意味になります。

グラブ解除時の Enter イベントは、ルートウィンドウの方に送られているので、ここでは表示されません。
Space キー押しでグラブした場合
ポインタがプログラムウィンドウ上に表示されていない時に、グラブを開始した場合のイベントを確認するため、Space キーが押された時、入力フォーカスのあるウィンドウでグラブを開始するようにしています。

ポインタがアクティブなウィンドウ上にある状態で Space キーを押した後、ポインタがそのウィンドウ上にある状態で Space キーを離すと、Enter/Leave イベントが発生しないのを確認してください。

ポインタがウィンドウ外にある状態でキーを押す
どちらかのウィンドウがアクティブで、ポインタをそのウィンドウの外に移動させた状態で、Space キーを押してみてください。
グラブした後に、Enter イベントが来ます。

# Grab - key
$ [Enter] <white> x(225) y(53) mode(1) detail(0)

グラブが行われた瞬間、ポインタは、現在表示されているウィンドウから、グラブウィンドウの方に移る形になるので、グラブウィンドウに対して Enter イベントが来ます。
Leave イベントは、元々ポインタが表示されていたウィンドウに送られています。

mode は、グラブの開始によってイベントが生成されたという意味で、NotifyGrab (1) になっています。
detail は、ポインタが元々表示されていたウィンドウが、グラブウィンドウとどういう関係なのかを表します。