X11: パッシブグラブ

パッシブグラブ
ウィンドウに対して、あらかじめボタンとキー修飾子の組み合わせを指定して、パッシブグラブを設定しておくと、実際にそのボタンとキーの組み合わせが押された瞬間に、アクティブなグラブが開始されます。

ボタンが離された場合 (キーの状態は関係なし)、自動的にグラブは解放されます。
ウィンドウマネージャの機能
パッシブグラブは、主にウィンドウマネージャで使われます。

例えば、各ウィンドウ上で Alt+左ドラッグしたら、ウィンドウの移動を行うといった機能を、ウィンドウマネージャが実装したい場合、作成されたトップレベルウィンドウ (または、ウィンドウマネージャが作成した装飾フレームのウィンドウ) に対して、左ボタンと Alt キーの組み合わせで、パッシブグラブを設定します。

各ウィンドウ上で、Alt+左ボタンが押された場合、アクティブなグラブが開始され、パッシブグラブを設定したクライアント (この場合はウィンドウマネージャ) に対して、グラブ開始時の ButtonPress イベントが送られてきます。
後は、ウィンドウマネージャがグラブ中のポインタイベントを処理すれば、任意の機能を実装することができます。

ただし、ウィンドウマネージャによって、このようなパッシブグラブが設定されている場合、クライアント側がそれを無効にすることはできません。
これは、ウィンドウの親またはその上位のウィンドウで、パッシブグラブが設定されていた場合、子よりも親のパッシブグラブが優先されるためです。
関数
XGrabButton 関数
void XGrabButton(Display *display, unsigned int button, unsigned int modifiers,
    Window grab_window, Bool owner_events, unsigned int event_mask, int pointer_mode,
    int keyboard_mode, Window confine_to, Cursor cursor);

パッシブなグラブを設定します。

以下の条件に当てはまる場合、自動的にアクティブなグラブが行われます。
最初の ButtonPress イベントは、常に送られてきます。

  • ポインタがグラブされていない状態で、指定されたボタンが押されている・指定された修飾子が押されている・他のボタンや修飾子が押されていない場合。
  • 現在、grab_window 上にポインタがある。
  • confine_to ウィンドウが指定されている場合、それが表示されている。
  • grab_window の祖先で、同じボタン/キーの組み合わせのパッシブグラブが存在しない。

buttonグラブを行うボタン番号、または AnyButton
modifiersボタンと同時に押されている修飾子キー、または AnyModifier (修飾子なしも含むすべてのセット)。
※NumLock (Mod2) や CapsLock (Lock) も含まれるので、注意してください。

ShiftMask、LockMask、ControlMask
Mod1Mask、Mod2Mask、Mod3Mask、Mod4Mask、Mod5Mask

同じクライアントで、同じ grab_window ウィンドウ、同じボタンとキーの組み合わせを指定した場合、設定が上書きされます。

grab_window 上でボタンが押された場合、そのウィンドウのルートウィンドウから順に子を辿って、押された組み合わせに一致するパッシブグラブを探します。
親ウィンドウで同じ組み合わせのパッシブグラブが存在する場合は、親のパッシブグラブが実行されます。

他のクライアントが、同じウィンドウ上で同じボタン/キーの組み合わせを使用している場合、BadAccess エラーが発生します。
AnyModifier または AnyButton を指定する場合、いずれかの組み合わせで競合するグラブがある場合、BadAccess エラーが発生します。
XUngrabButton 関数
void XUngrabButton(Display *display, unsigned int button, unsigned int modifiers, Window grab_window);

同じクライアントによって、XGrabButton() で設定された組み合わせを解除する。
(他のクライアントが設定したものは解除できない)

button に AnyButton を指定した場合、すべてのボタンが対象。
modifiers に AnyModifier を指定した場合、すべてのセットが対象。
プログラム
ウィンドウの作成時に右ボタンをグラブ、ウィンドウが実際に表示された時に左ボタンをグラブしています。

実際にこのクライアントのグラブによるものなのかを判断できるようにするため、グラブ中のカーソルを指定しています。
ボタンを押した時にカーソルが変化したら、このクライアントによるパッシブグラブで、そうでない場合は、結果的に X サーバーによる自動グラブが行われています。

<10-passivegrab.c>
#include <stdio.h>
#include <X11/Xlib.h>
#include <X11/keysym.h>
#include <X11/cursorfont.h>
#include "util.h"

int main(int argc,char **argv)
{
    Display *disp;
    XSetWindowAttributes attr;
    Window win;
    XEvent ev;
    KeySym key;
    Cursor cursor;
    int fmap = 0;
    
    disp = XOpenDisplay(NULL);
    if(!disp) return 1;

    set_display(disp);

    //ウィンドウ作成

    attr.background_pixel = 0;
    attr.event_mask = ButtonPressMask | ButtonReleaseMask
        | PointerMotionMask | ButtonMotionMask | KeyPressMask
        | StructureNotifyMask;

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

    //グラブ中のカーソル

    cursor = XCreateFontCursor(disp, XC_hand1);

    //右ボタンをグラブ

    XGrabButton(disp, Button3, AnyModifier, win, False,
        ButtonPressMask | ButtonReleaseMask | PointerMotionMask | ButtonMotionMask,
        GrabModeAsync, GrabModeAsync, None, cursor);

    //イベント

    XMapWindow(disp, win);

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

        switch(ev.type)
        {
            case ButtonPress:
            case ButtonRelease:
                printf("* [%s] x(%d) y(%d) button(%d)\n",
                    (ev.type == ButtonPress)? "Press": "Release",
                    ev.xbutton.x, ev.xbutton.y, ev.xbutton.button);
                fflush(stdout);
                break;
            case MotionNotify:
                printf("- [Motion] x(%d) y(%d)\n", ev.xmotion.x, ev.xmotion.y);
                fflush(stdout);
                break;

            case MapNotify:
                if(fmap == 0)
                {
                    printf("# grab Button1\n");

                    //左ボタンをグラブ
                    XGrabButton(disp, Button1, AnyModifier, win, False,
                        ButtonPressMask | ButtonReleaseMask | PointerMotionMask | ButtonMotionMask,
                        GrabModeAsync, GrabModeAsync, None, cursor);

                    fmap = 1;
                }
                break;

            case KeyPress:
                key = XLookupKeysym((XKeyEvent *)&ev, 0);
                if(key == XK_Return || key == XK_KP_Enter)
                    goto END;
                break;
        }
    }

END:
    XCloseDisplay(disp);

    return 0;
}

デスクトップ環境によって動作が異なるかもしれませんが、OpenBox では、右ボタンのグラブは成功し、左ボタンのグラブは失敗します (BadAccess エラー)。
解説
MapNotify イベント
XMapWindow() などでウィンドウをマップした後、ウィンドウが実際に画面上に表示された時に来るイベントです。

このイベントが来た場合、クライアントのウィンドウには、ウィンドウマネージャによって作成されたフレームウィンドウが付き、ウィンドウの設定も変更された状態になっています。

ちなみに、MapNotify イベントは、最小化から復帰した時にも来るので、最初の MapNotify イベント時のみ、パッシブグラブを設定しています。
ウィンドウ作成時のグラブ
ウィンドウの作成直後に、Button3 (右ボタン) のパッシブグラブを設定しています。
ウィンドウ上で右ボタンを押してドラッグした時、カーソルが変わった場合は、パッシブグラブによるグラブが成功しています。

環境によっては、エラーが出たり、カーソルが変わらない場合があるかもしれません。
マップ時のグラブ
最初に MapNotify イベントが来た時に、Button1 (左ボタン) のパッシブグラブを設定しています。
環境にもよりますが、BadAccess エラーが出る場合があります。

--- X error ---
 type(0) serial(26) error_code(10)
 request_code(28) minor_code(0) resourceid(0x1c00006)
 [Error] BadAccess (attempt to access private resource denied)
 [Request] X_GrabButton
------------

エラーが出ても出なくても、左ボタンのドラッグ時にカーソルが変わらない場合は、このクライアントによるパッシブグラブが行われていません。

実際には、ウィンドウマネージャが作成したフレームウィンドウにパッシブグラブが設定されているか、ウィンドウマネージャがクライアントのウィンドウにパッシブグラブを設定したことにより、ウィンドウマネージャのクライアントの方にイベントが送られている可能性があります。

その場合、グラブがキャンセルされて、通常通りにポインタイベントが送られた結果、X サーバーの自動グラブが行われている場合があります。
BadAccess エラー
MapNotify イベント時に、XGrabButton() で BadAccess エラーが出た場合は、ウィンドウマネージャが、同じウィンドウに対して、すでに同じ組み合わせのパッシブグラブを設定しています。

ウィンドウの作成直後 (マップする前) は、まだウィンドウマネージャによってウィンドウが制御されていないため、BadAccess エラーが出ることはありません。

しかし、MapNotify イベントが来た後に XGrabButton() を実行した場合は、このウィンドウがすでにウィンドウマネージャによって制御された後なので、Button1 のパッシブグラブがすでに設定されている可能性があります。
(他のクライアントが設定したパッシブグラブは、別のクライアントが変更することはできません)

マップ時に実際に起こっていること
クライアントが XMapWindow() などでウィンドウを表示させる要求をした時、SubstructureRedirectMask のイベントマスクを選択している親ウィンドウがある場合 (ウィンドウマネージャの場合はルートウィンドウ)、X サーバーはそのウィンドウをマップせず、代わりに、SubstructureRedirectMask が選択されているウィンドウに MapRequest イベントを送ります。

そのイベントを受け取ったウィンドウマネージャは、ウィンドウ装飾を付けたり、ウィンドウの設定を変更したりした後、そのウィンドウを実際に表示させ、その後、本来のクライアントに MapNotify イベントが来る形になります。
親のパッシブグラブが優先される
パッシブグラブは、同じボタンとキーの組み合わせが、親を含む上位のウィンドウで設定されている場合、ルートウィンドウに近い親の方で、グラブが実行されます。

そのため、子のウィンドウ上でボタンが押されていたとしても、実際は親のウィンドウで設定されたパッシブグラブが実行されてしまいます。

  • 他のクライアントが設定したパッシブグラブは、別のクライアントによって解除したり、設定を上書きすることはできません。
  • ウィンドウをマップする前にパッシブグラブを設定したとしても、設定自体は失敗しませんが、実際は親であるフレームウィンドウの方でパッシブグラブが行われてしまうので、ウィンドウマネージャによるマウス操作の機能を、特定のクライアントで無効にすることはできません。
    (ウィンドウマネージャ側の実装方法にもよります)
  • ただし、ウィンドウ属性の override_redirect を True にすると、ウィンドウマネージャによる制御が行われないので (ウィンドウ装飾も付かない)、ポップアップウィンドウなどを作成する場合は、気にする必要はありません。

しかし、左ボタンのパッシブグラブがウィンドウマネージャ側で行われているとするなら、中身のウィンドウで左ボタンを操作した時の動作が通常通りになっているのは、どういうことでしょうか。
それに関しては、次で説明します。