X11: パッシブグラブとイベント凍結

パッシブグラブとイベント凍結
ウィンドウマネージャの機能として、Alt+左ドラッグによるウィンドウの位置移動を実装したいという場合、以下のような手順が行われています。

  1. クライアントがウィンドウを作成し、マップする。
  2. ウィンドウマネージャが MapRequest イベントを受信する。
  3. ウィンドウ装飾と枠を表示するためのフレームウィンドウが作成される。
  4. XReparentWindow() で、クライアントのウィンドウの親を、ルートウィンドウからフレームウィンドウに変更する。
    フレームウィンドウの中に、クライアントのウィンドウが配置される形になる。
  5. フレームウィンドウに Button1 のパッシブグラブが設定される。

この状態で、中身のウィンドウ (またはフレームウィンドウ) 上で左ボタンが押された場合、フレームウィンドウでパッシブグラブが開始されて、最初の ButtonPress イベントが来るので、そこでウィンドウの位置移動を開始させることになります。
キー修飾子
パッシブグラブを設定する時のキー修飾子には、NumLockCapsLock といった、ON/OFF の状態があるキーも含まれるため、NumLock が ON になっている状態の場合、Alt キーの修飾子は Mod1Mask | Mod2Mask で指定しておかないと、実際には動作しません。

このように、ON/OFF 状態があるキーによって、修飾子の設定値が左右されてしまうのは厄介なので、パッシブグラブで修飾子を指定する時は、すべての ON/OFF 状態の組み合わせをそれぞれ設定するか、AnyModifier ですべての修飾子セットを指定します (修飾子なしも含む)。

AnyModifier を使う場合は、実際にグラブを行いたいキーの組み合わせと、実際にはグラブを行いたくないキーの組み合わせがあります。
そのため、グラブが開始されて、最初の ButtonPress イベントが来た時に、グラブを続行するか、キャンセルするかを選択できれば、それらを問題なく処理することができます。

それを実現することができるのが、グラブ時のイベント凍結です。
イベント凍結
グラブ関数の pointer_mode 引数に GrabModeSync を指定すると、実際にグラブが開始された時、ポインタのイベントは凍結され、グラブウィンドウに対して、ポインタイベントは送られなくなります。
(パッシブグラブ時の最初の ButtonPress イベントは、常に送られてきます)

イベントを凍結するというのは、つまり、後で改めてイベントを処理できるように、イベントを X サーバーの内部キューに入れておいて、退避させておくということです。

それによって何が出来るかというと、凍結したイベントを、通常のグラブ状態として続行させるか、グラブが行われなかったものとして、本来イベントが送られるウィンドウに対して、通常のイベントとして送り直すかを、グラブが開始された後に選択できるということです。

グラブが開始された後に XAllowEvents() を実行すると、凍結を解除したり、再凍結したりすることができます。

グラブが解除された場合、凍結されているイベントは、通常のイベントとしてまとめて送られます。
XAllowEvents 関数
void XAllowEvents(Display *display, int event_mode, Time time);

凍結されているイベントを解放したりします。
time は、最終アクティブグラブ時間以降で、現在の X サーバー時刻以前であること。

event_mode で、処理方法を指定します。
ポインタのみ、キーボードのみ、両方の3通りに対して指定できます。

AsyncPointerポインタイベントの凍結を解除し、グラブを通常通り続行する
SyncPointer次の ButtonPress または ButtonRelease イベントが来るまで、凍結を解除してグラブを通常通り続行する。
その後は、再びイベントを凍結させる。
ReplayPointerポインタが凍結されている場合、グラブを解除し、パッシブグラブ時の最初の ButtonPress も含めて、凍結されているイベントを、グラブが解除されている状態として再処理する。
AsyncKeyboard
SyncKeyboard
ReplayKeyboard
キーボードイベント
SyncBoth
AsyncBoth
両方のイベント
使い方
ポインタイベントが凍結した状態でグラブが開始された後に、XAllowEvents で AsyncPointer を指定して実行した場合、イベントの凍結を解除して、通常のグラブを続行する形になります。

ReplayPointer を指定した場合は、グラブを解除し、凍結されているイベントを、本来送られるウィンドウに対して、通常通りのイベントとして送ります。
つまり、パッシブグラブで ButtonPress イベントが来た時に実行した場合、パッシブグラブが行われなかったものとしてイベントを再処理し、本来のウィンドウに対して、ButtonPress イベントが再び来ることになります。

このように、イベントを凍結した状態でグラブを開始すると、実際にグラブが開始された時に、そのグラブを続行するか、キャンセルするかを、後から選択することができます。
プログラム
親と子のパッシブグラブの関係を確認するため、ウィンドウマネージャによる制御を行わずに、擬似的にフレームウィンドウと中身のウィンドウを作成します。

緑の部分と黒の部分、それぞれの上で左ボタンドラッグをした時の動作を確認してください。
左ボタン以外を押すと終了します。

$ cc -o run 11-passive2.c util.c -lX11

<11-passive2.c>
#include <stdio.h>
#include <X11/Xlib.h>
#include <X11/cursorfont.h>
#include "util.h"

#define FRAME_WIDTH 15

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

    set_display(disp);

    //フレームウィンドウ作成

    attr.background_pixel = rgb_to_pixel(0x00ff00);
    attr.event_mask = ButtonPressMask | ButtonReleaseMask | ButtonMotionMask;
    attr.override_redirect = True;

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

    XGrabButton(disp, Button1, AnyModifier, winf, False,
        ButtonPressMask | ButtonReleaseMask | ButtonMotionMask,
        GrabModeSync, GrabModeAsync, None, XCreateFontCursor(disp, XC_hand1));

    //中身のウィンドウ作成

    attr.background_pixel = 0;
    attr.event_mask = ButtonPressMask | ButtonReleaseMask | ButtonMotionMask;

    winc = XCreateWindow(disp, winf,
        FRAME_WIDTH, FRAME_WIDTH, 200 - FRAME_WIDTH * 2, 200 - FRAME_WIDTH * 2, 0,
        CopyFromParent, CopyFromParent, CopyFromParent,
        CWBackPixel | CWEventMask, &attr);

    //イベント

    XMapWindow(disp, winc);
    XMapWindow(disp, winf);

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

        switch(ev.type)
        {
            case ButtonPress:
            case ButtonRelease:
                printf("* [%s] <%s> x(%d) y(%d) button(%d)\n",
                    (ev.type == ButtonPress)? "Press": "Release",
                    (ev.xbutton.window == winc)? "content": "frame",
                    ev.xbutton.x, ev.xbutton.y, ev.xbutton.button);

                fflush(stdout);

                if(ev.type == ButtonPress)
                {
                    if(ev.xbutton.button == Button1)
                    {
                        if(ev.xbutton.window == winf)
                        {
                            XAllowEvents(disp,
                                (ev.xbutton.subwindow)? ReplayPointer: AsyncPointer,
                                ev.xbutton.time);

                            XSync(disp, 0);
                        }
                    }
                    else
                        goto END;
                }
                break;
            case MotionNotify:
                printf("- [Motion] <%s> x(%d) y(%d)\n",
                    (ev.xmotion.window == winc)? "content": "frame",
                    ev.xmotion.x, ev.xmotion.y);
                fflush(stdout);
                break;
        }
    }

END:
    XCloseDisplay(disp);

    return 0;
}
解説
ウィンドウ
フレームウィンドウとして、背景が緑のウィンドウを作成し、Button1 のパッシブグラブを設定します。
また、中身のウィンドウとして、背景が黒の子ウィンドウを作成します。

子ウィンドウは、親の左上を原点とした相対位置で指定するので、黒の子ウィンドウは、余白を付ける形で、親の中央に配置します。
緑の部分はフレームウィンドウの領域で、黒の部分は子ウィンドウの領域となります。

なお、今回は、ウィンドウマネージャによる制御を行わずにウィンドウを表示したいので、フレームウィンドウのウィンドウ属性の override_redirect を True に設定しています。
※装飾フレームが付かないので、ウィンドウの移動はできません。

子ウィンドウも含めて、作成されたウィンドウは常に非表示の状態なので、子ウィンドウに対しても XMapWindow() を行う必要があります。
XMapWindow() は、下位のウィンドウの表示状態を変更しません。
左ボタンドラック時の動作
黒 (中身) の部分で左ボタンドラッグを行うと、以下のようになります。

* [Press] <frame> x(107) y(87) button(1)
* [Press] <content> x(92) y(72) button(1)
- [Motion] <content> x(93) y(72)
...

緑 (フレーム) の部分で左ボタンドラッグを行うと、以下のようになります。

* [Press] <frame> x(191) y(87) button(1)
- [Motion] <frame> x(190) y(87)
ButtonPress 時
親であるフレームウィンドウで Button1 のパッシブグラブが設定されているため、緑と黒の部分のどちらで左ボタンを押しても、常にフレームウィンドウ側でグラブが開始され、ButtonPress イベントが送られてきます。

ただし今回は、グラブの設定で GrabModeSync が指定されているので、グラブが開始された時、ポインタイベントは凍結された状態となっています。
※パッシブグラブ時の最初の ButtonPress イベントは常に来るので、問題ありません。

ButtonPress イベント時に、ev.xbutton.window がフレームウィンドウの場合、パッシブグラブによってグラブが開始されたということなので、XAllowEvents() を実行することで、グラブを継続させるか、キャンセルさせています。

実際にボタンが押されたウィンドウは、ev.xbutton.subwindow で取得できます。

subwindow が None (0) の場合、window の子ウィンドウ上ではなく、自身の上でボタンが押されたことになります。
None でない場合は、window の子ウィンドウ上でボタンが押されたことになり、その子ウィンドウが指定されています。

subwindow が 0 ならグラブを続行し、それ以外ではグラブをキャンセルしています。
XAllowEvents()
緑の部分
今回の場合、フレームウィンドウの領域 (緑の部分) でボタンが押された場合はグラブを続行させたいので、ev.xbutton.subwindow が None の場合は、XAllowEvents() で AsyncPointer を指定して、ポインタイベントの凍結を解除し、グラブを続行させます。

グラブ時のカーソルを指定しているので、カーソル形状が変わっているのを確認してください。

黒の部分
実際にボタンが押されたのが、子ウィンドウ (黒の部分) の領域の場合は、グラブをキャンセルして、通常通りのイベントを子ウィンドウに送りたいので、XAllowEvents() で ReplayPointer を指定してグラブを解除し、凍結されているイベントを再処理させています。

フレームウィンドウで ButtonPress イベントが来た後、子ウィンドウに対して再び ButtonPress イベントが来ていることに注目してください。
最初の ButtonPress イベントも含めて、本来のウィンドウに対して、改めて通常のイベントが送られています (座標も子ウィンドウの原点が基準になっています)。

グラブがキャンセルされた後は、本来のウィンドウに対してポインタイベントを送ることができるので、ウィンドウマネージャが処理しなかったパッシブグラブのイベントは、このようにして、本来のクライアントに送り返すことができます。