X11: ICCCM (5) - WM_PROTOCOLS

WM_PROTOCOLS
WM_PROTOCOLS プロパティには、type = "ATOM", format = 32 で、Atom のリストをセットします。

クライアントとウィンドウマネージャ間で、ClientMessage イベントを使用した通信を行うために、必要となる機能のアトムのリストを指定します。

ICCCM では、以下が定義されています。

WM_TAKE_FOCUS入力フォーカスを受け取る
WM_DELETE_WINDOWトップレベルウィンドウの削除要求を受け取る。
(ウィンドウ装飾の閉じるボタンが押された時)
ClientMessage イベント
typedef struct {
 int           type;
 unsigned long serial;
 Bool          send_event;
 Display       *display;
 Window        window;
 Atom          message_type;
 int           format;
 union{
     char  b[20];
     short s[10];
     long  l[5];
 } data;
} XClientMessageEvent;

ClientMessage イベントは、クライアントが、XSendEvent 関数を使って ClientMessage イベントを送信した時に来ます。
別のクライアントやウィンドウに、イベントを通知したい場合に使います。

message_typeデータをどのように解釈するかを示すアトム
formatデータのフォーマット。
「8 = data.b」「16 = data.s」「32 = data.l」を使います。
data渡されたデータ。
最大で 20 byte (format = 32 は 32bit 扱い)。
WM_DELETE_WINDOW
WM_PROTOCOLS プロパティのアトムリスト内に、WM_DELETE_WINDOW のアトムがある場合、ウィンドウマネージャが作成したウィンドウ装飾上の「閉じる」ボタンが押されたり、ウィンドウマネージャによって、クライアントウィンドウを閉じる要求が行われた時に、以下の値で ClientMessage イベントが送られてきます。

message_type = (Atom) WM_PROTOCOLS
format = 32
data.l[0] = (Atom) WM_DELETE_WINDOW

クライアントがこれを受け取った場合、ウィンドウを実際に閉じたり、ダイアログを表示して、本当に終了するかを尋ねたりする必要があります。
結果として閉じる処理をキャンセルする場合は、何も行う必要はありません。

WM_PROTOCOLS プロパティに WM_DELETE_WINDOW が含まれていないウィンドウで、ウィンドウマネージャからの閉じる要求が行われた場合は、クライアントが強制終了される場合があります。
WM_TAKE_FOCUS
WM_PROTOCOLS プロパティのアトムリスト内に、WM_TAKE_FOCUS のアトムがある場合、ウィンドウが入力フォーカスを受け取る (または受け取った) タイミングで、ClientMessage イベントが送られてきます。

message_type = (Atom) WM_PROTOCOLS
format = 32
data.l[0] = (Atom) WM_TAKE_FOCUS
data.l[1] = (Time) タイムスタンプ
応答
クライアントがこれを受け取った時、フォーカスが必要な場合は、XSetInputFocus() で入力フォーカスをセットします。

XSetInputFocus(disp, ev.xclient.window, RevertToParent, ev.xclient.data.l[1]);

この時、revert_to 引数には RevertToParent を指定してください。この値以外は問題があります。

time 引数には、イベントからのタイムスタンプを指定します。
WM_HINTS
WM_HINTS プロパティの input の値が True (通常はデフォルト) の場合、ウィンドウマネージャが入力フォーカスを制御し、適切なウィンドウに自動で入力フォーカスをセットします。
ウィンドウマネージャによって XSetInputFocus() が実行された後、クライアントに FocusIn イベントが来て、その後に WM_TAKE_FOCUS が来ます。

通常は自動で入力フォーカスがセットされるので、ウィンドウが入力フォーカスを取得した時に、(子ウィンドウを含む) 他のウィンドウに対して入力フォーカスを設定するようなことがない場合は、WM_TAKE_FOCUS を受け取る必要はありません。

トップレベルウィンドウで入力フォーカスを受け取って、WM_TAKE_FOCUS が来た時に、そのウィンドウ以外のウィンドウに入力フォーカスを設定したい場合は、WM_TAKE_FOCUS が必要になります。

input の値が False の場合は、ウィンドウマネージャが XSetInputFocus() を行わないので、WM_TAKE_FOCUS が来たタイミングで、クライアントが明示的に入力フォーカスを設定する必要があります。
WM_TAKE_FOCUS が必要な理由
XSetInputFocus() で入力フォーカスを設定する場合は、time 引数に適切なタイムスタンプ (CurrentTime 以外) が必要になります。

例えば、FocusIn イベントでフォーカスを取得した時に、(子ウィンドウを含む) 他のウィンドウにフォーカスを与えたい場合、FocusIn イベントにはタイムスタンプがないので、適切に XSetInputFocus() を実行することができません。

そのため、タイムスタンプを持った WM_TAKE_FOCUS が必要になります。
入力モード
WM_HINTS の input 値と、WM_TAKE_FOCUS を受け取るかによって、4つの入力モードがあります。

入力なしinput = Fale, WM_TAKE_FOCUS = なし。
ウィンドウマネージャは、入力フォーカスに全く関与しない。
パッシブinput = True, WM_TAKE_FOCUS = なし。
ウィンドウマネージャが自動で XSetInputFocus を行う。クライアントがそのタイミングを知る必要はない。
ローカルアクティブinput = True, WM_TAKE_FOCUS = あり。
ウィンドウマネージャが自動で XSetInputFocus を行った後、入力フォーカスが必要なタイミングで WM_TAKE_FOCUS が来る。
グローバルアクティブinput = False, WM_TAKE_FOCUS = あり。
ウィンドウマネージャによる XSetInputFocus は行わず、WM_TAKE_FOCUS が来た時にクライアントが明示的に行う。

通常はローカルアクティブを使います。

  • トップレベルウィンドウに対する入力フォーカスのセットは、ウィンドウマネージャに任せた上で、クライアントが任意の子ウィンドウに入力フォーカスをセットしたい場合は、ローカルアクティブを使います。
  • 入力フォーカスが必要なタイミングで、クライアントの判断によって XSetInputFocus() を行いたい場合は、グローバルアクティブを使います。
  • トップレベルウィンドウへの入力フォーカスを、すべてウィンドウマネージャに任せたい場合は、パッシブにします。
プログラム
ウィンドウ装飾の閉じるボタンを押すと、WM_DELETE_WINDOW が来ます (ウィンドウは閉じない)。
入力フォーカスが来ると、WM_TAKE_FOCUS が来ます。

ポインタボタンを押すと終了します。

$ cc -o run d06-protocol.c util.c -lX11

<d06-protocol.c>
#include <stdio.h>
#include <X11/Xlib.h>
#include <X11/Xutil.h>
#include <X11/Xatom.h>
#include "util.h"

const char *g_atom_names[] = {
    "WM_PROTOCOLS",
    "WM_DELETE_WINDOW",
    "WM_TAKE_FOCUS"
};

int main(int argc,char **argv)
{
    Display *disp;
    Window win;
    XEvent ev;
    Atom atom[3],type;
    
    disp = XOpenDisplay(NULL);
    if(!disp) return 1;

    win = create_test_window(disp, ButtonPressMask | FocusChangeMask);

    //アトム取得

    XInternAtoms(disp, (char **)g_atom_names, 3, False, atom);

    //WM_PROTOCOLS

    XChangeProperty(disp, win, atom[0], XA_ATOM, 32,
        PropModeReplace, (unsigned char *)&atom[1], 2);

    //イベント

    XMapWindow(disp, win);

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

        switch(ev.type)
        {
            case ClientMessage:
                if(ev.xclient.message_type == atom[0])
                {
                    type = (Atom)ev.xclient.data.l[0];

                    if(type == atom[1])
                        printf("WM_DELETE_WINDOW\n");
                    else if(type == atom[2])
                    {
                        printf("WM_TAKE_FOCUS: time=%lu\n", ev.xclient.data.l[1]);

                        XSetInputFocus(disp, ev.xclient.window, RevertToParent, ev.xclient.data.l[1]);
                    }
                }
                break;
            case FocusIn:
                printf("FocusIn\n");
                break;
            case ButtonPress:
                goto END;
        }
    }

END:
    XCloseDisplay(disp);

    return 0;
}
タイミング
WM_TAKE_FOCUS は、最初にマップされた時や、ポインタボタンが押されたときにも来ます。
また、装飾フレームウィンドウ上でポインタボタンを押した時にも来ます。
(入力フォーカスをセットするかどうかは、ウィンドウマネージャが制御しているので、実装によるかもしれません)

他のウィンドウに入力フォーカスがある状態で、入力フォーカスのないウィンドウ上でボタンを押した場合、入力フォーカスがそのウィンドウに移ることになるので、ポインタボタンが押された時にも入力フォーカスがセットされます。

FocusIn イベントは、実際に入力フォーカスが変化した時にしか来ませんが、WM_TAKE_FOCUS は、ウィンドウに入力フォーカスがあるかどうかに関係なく、入力フォーカスをセットする必要があるタイミングで、常に来ます。