X11: ICCCM (6) - ダイアログとポップアップ

WM_TRANSIENT_FOR
WM_TRANSIENT_FOR プロパティに、type = "WINDOW", format = 32 で、ウィンドウ ID (Window) を1つセットすると、そのウィンドウは、プロパティで指定したウィンドウよりも常に前面に表示されます。

つまり、WM_TRANSIENT_FOR の値で設定したウィンドウが背面で、WM_TRANSIENT_FOR プロパティを設定したウィンドウが前面に表示されます。

これは主に、ダイアログウィンドウなどの一時的なウィンドウで使われます。
関数 (Xutil.h)
void XSetTransientForHint(Display *display, Window w, Window prop_window);

WM_TRANSIENT_FOR プロパティをセットする関数があるので、それを使います。
プログラム (1)
メインの黒ウィンドウでポインタボタンを押すと、青いダイアログウィンドウが表示されます。
青いウィンドウは、常に黒ウィンドウの前面に表示されます。

青ウィンドウでウィンドウ装飾の閉じるボタンを押すと、ダイアログが閉じます。
黒ウィンドウで閉じるボタンを押すと、終了します。

$ cc -o run d07a-dialog.c util.c -lX11

<d07a-dialog.c>
#include <X11/Xlib.h>
#include <X11/Xutil.h>
#include "util.h"

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

    set_display(disp);

    win = create_test_window2(disp, 300, 300, 0, ButtonPressMask);

    //イベント

    XMapWindow(disp, win);

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

        if(event_quit(&ev))
        {
            //WM_DELETE_WINDOW

            if(ev.xany.window == win)
                //メインウィンドウなら終了
                break;
            else
            {
                //ダイアログ閉じる
                XDestroyWindow(disp, windlg);
                windlg = None;
                continue;
            }
        }

        switch(ev.type)
        {
            case ButtonPress:
                if(!windlg)
                {
                    //ダイアログ作成

                    windlg = create_test_window2(disp, 200, 200, rgb_to_pixel(0x0000ff), 0);

                    XSetTransientForHint(disp, windlg, win);
                    XMapWindow(disp, windlg);
                }
                break;
        }
    }

    XCloseDisplay(disp);

    return 0;
}
解説
WM_TRANSIENT_FOR は、基本的に指定ウィンドウよりも前面に表示するだけなので、青ウィンドウが表示されている間でも、黒ウィンドウの閉じるボタンを押すことができます (WM_DELETE_WINDOW が来る)。

また、この状態では、それぞれのウィンドウに対して入力フォーカスがセットされるので、青ウィンドウの表示中でも、黒ウィンドウをアクティブにできます。

そのため、通常のダイアログの場合は、ダイアログが表示されている間、他のウィンドウでの WM_DELETE_WINDOW やポインタイベント・キーイベントなどを無視する必要があります。
NetWM の場合
NetWM の場合は、_NET_WM_STATE プロパティの値に _NET_WM_STATE_MODAL が含まれていると、モーダルダイアログとなり、WM_TRANSIENT_FOR で指定されたウィンドウには、入力フォーカスがセットされません。

また、WM_TRANSIENT_FOR の値に None (0) またはルートウィンドウが指定されていた場合、同じグループのすべてのウィンドウに対して、前面に表示されます。
(ウィンドウグループについては後述します。WM_HINTS プロパティの window_group で設定します)
ポップアップウィンドウ
ウィンドウ装飾が必要なく、また、ウィンドウマネージャによる各制御が必要のないポップアップウィンドウの場合は、ウィンドウ属性の override_redirect を True にします。

例えば、ポップアップメニューや、コンボボックスの選択メニューを表示する場合に使います。
入力フォーカス
ポップアップウィンドウは、マップ時に、ウィンドウマネージャによる入力フォーカスがセットされないため、ウィンドウを表示している間、入力フォーカスは元のままになります。

例えば、エディタで右クリックメニューを表示して、貼り付けを行った場合、エディタに入力フォーカスを残しておいて、ポップアップが終了した後に、現在位置にテキストを貼り付ける必要があります。
そのため、ポップアップの表示時に、入力フォーカスはセットしません。

入力フォーカスがないということは、ポップアップウィンドウにキーイベントが来ないということなので、ポップアップウィンドウでキーイベントを処理したい場合は、マップ時にキーボードをグラブするか、ポップアップ表示中のすべてのウィンドウのキーイベントを、ポップアップウィンドウを対象にして処理する必要があります。
ポインタのグラブ
ポップアップの表示中は、基本的にポインタをグラブする必要があります。
これは、ポップアップウィンドウ外でポインタボタンが押された時に、ポップアップを終了させる必要があるからです。

XGrabPointer() の owner_events 引数は、ポップアップウィンドウが1つであれば False で構いませんが、ポップアップメニューから、さらにサブメニューのポップアップを表示する場合など、複数のポップアップウィンドウを表示する可能性がある場合は、True にします。

owner_events 引数が False の場合、すべてのウィンドウのポインタイベントが、グラブウィンドウ1つに対して送られてきます。

True の場合は、発生したポインタイベントが、グラブしているクライアントが作成したウィンドウに対するものである場合、通常のポインタイベントとして、各ウィンドウに送られます。
つまり、複数ある各ポップアップウィンドウで、ポインタイベントを処理できます。

他のクライアントが作成したウィンドウのポインタイベントは、常にグラブウィンドウに対して送られてきます。
そのため、ButtonPress イベントが来た時、その位置が、ポップアップウィンドウ以外のウィンドウ上であれば、ポップアップを終了します。
(自身のクライアントが作成した、ポップアップ以外のウィンドウも含む)
プログラム (2)
ポインタボタン (右ボタン以外) を押すと、青いポップアップウィンドウを表示します。
ポップアップ以外の上でボタンを押すと、ポップアップを閉じます。

閉じるボタン、または右ボタンで終了します。

$ cc -o run d07b-popup.c util.c -lX11

<07b-popup.c>
#include <stdio.h>
#include <X11/Xlib.h>
#include "util.h"

#define POPUP_WIDTH  150
#define POPUP_HEIGHT 200

static Window _create_popup(Display *disp,int x,int y)
{
    XSetWindowAttributes attr;
    Window win;

    attr.background_pixel = rgb_to_pixel(0x0000ff);
    attr.override_redirect = True;

    //上位のウィンドウに渡さないイベント
    attr.do_not_propagate_mask = ButtonPressMask | ButtonReleaseMask
        | PointerMotionMask | ButtonMotionMask;

    attr.event_mask = ButtonPressMask | ButtonReleaseMask;
    
    win = XCreateWindow(disp, g_disp.root,
        x, y, POPUP_WIDTH, POPUP_HEIGHT, 0,
        CopyFromParent, CopyFromParent, CopyFromParent,
        CWBackPixel | CWDontPropagate | CWOverrideRedirect | CWEventMask,
        &attr);

    //

    XMapWindow(disp, win);

    //ポインタグラブ

    XGrabPointer(disp, win, True,
        ButtonPressMask | ButtonReleaseMask,
        GrabModeAsync, GrabModeAsync, None, None, CurrentTime);

    return win;
}

int main(int argc,char **argv)
{
    Display *disp;
    Window win,winpop = None;
    XEvent ev;
    int x,y;
    
    disp = XOpenDisplay(NULL);
    if(!disp) return 1;

    set_display(disp);

    win = create_test_window2(disp, 300, 300, 0, ButtonPressMask);

    //イベント

    XMapWindow(disp, win);

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

        if(event_quit(&ev))
        {
            if(ev.xany.window == win)
                break;
        }

        switch(ev.type)
        {
            case ButtonPress:
                x = ev.xbutton.x;
                y = ev.xbutton.y;
                
                printf("[ButtonPress] window(0x%lx) x(%d) y(%d)\n",
                    ev.xbutton.window, x, y);
                
                if(ev.xbutton.button == Button3) goto END;
                
                if(ev.xbutton.window == winpop)
                {
                    //ポップアップウィンドウ (グラブ中)

                    if(x < 0 || y < 0 || x >= POPUP_WIDTH || y >= POPUP_HEIGHT)
                    {
                        //領域外なら終了 (グラブは自動的に解除)
                        XDestroyWindow(disp, winpop);
                        winpop = None;
                        printf("* popup - close\n");
                    }
                }
                else
                {
                    //メインウィンドウ (グラブ中も来る)

                    if(winpop)
                    {
                        XDestroyWindow(disp, winpop);
                        winpop = None;
                        printf("* main - close\n");
                    }
                    else
                    {
                        winpop = _create_popup(disp,
                            ev.xbutton.x_root, ev.xbutton.y_root);
                    }
                }
                break;
        }
    }

END:
    XCloseDisplay(disp);

    return 0;
}
解説
メインウィンドウでボタンが押された場合、そのルート座標位置に、ポップアップを表示します。
override_redirect を True にして、ウィンドウを作成しています。

マップしていないウィンドウはグラブできないので、ウィンドウのマップ後にポインタをグラブします。
owner_events 引数を True にしているので、グラブ中、メインウィンドウのポインタイベントは、メインウィンドウに対して送られてきます。
(ポップアップウィンドウは1つしかないので、本来は False で構いませんが、動作確認のため True にしています)

ポップアップが表示されていて、メインウィンドウに ButtonPress が来た場合、ポップアップの終了とみなします。

ポップアップの ButtonPress イベントは、「ポップアップウィンドウ上でボタンが押された時」か「自身のクライアント以外が作成したウィンドウ上でボタンが押された時」に来ます。
ボタンが押されたウィンドウが、ポップアップウィンドウであるかそれ以外であるかは、x, y 座標の値で判断できます。

x,y 座標が、ポップアップウィンドウの領域内であれば、ポップアップウィンドウ上でボタンが押されたことになり、領域外の位置であれば、ポップアップウィンドウ以外の上でボタンが押されたことになります。