X11: NetWM (6) - ユーザータイム

_NET_WM_USER_TIME
クライアントウィンドウの _NET_WM_USER_TIME プロパティには、最後のユーザーアクティビティが行われた X サーバーの時間をセットする必要があります。
(type = "CARDINAL", format = 32)

このプロパティは、クライアントが、各トップレベルウィンドウに対して設定する必要があります。

ユーザーアクティビティとは、ポインタのボタンを押したり、キーを押すなどの、ユーザーが起こした行動のことです。
具体的には、ButtonPress, KeyPress イベントが起きた時です。
※ ButtonRelease, KeyRelease イベントは対象外です。

例えば他にも、WM_DELETE_WINDOW が来た時は、ユーザーによって閉じるボタンが押されたということなので、ユーザーアクティビティとして扱うことができます。

各ウィンドウでこのイベントが起きた時は、イベントのタイムスタンプ値を、_NET_WM_USER_TIME に設定する値として、常に記録しておく必要があります。
プロパティを設定するタイミング
_NET_WM_USER_TIME プロパティは、以下のタイミングで設定/更新する必要があります。

  • トップレベルウィンドウをマップする前。
  • アクティブな (入力フォーカスがある) ウィンドウで、ユーザーアクティビティが行われた時。

各トップレベルウィンドウをマップする前には、そのウィンドウをマップする原因となったタイプスタンプを、_NET_WM_USER_TIME に設定する必要があります。
(新しいウィンドウをマップする時や、画面に表示されていない状態のウィンドウを表示する時)

例えば、ボタンのクリックでポップアップウィンドウを表示する場合、マップする前に、ButtonPress イベントのタイムスタンプを、_NET_WM_USER_TIME プロパティに設定することになります。

ただし、アプリの起動後に最初にマップするウィンドウの場合、設定できるタイプスタンプが存在しないので、その場合は、_NET_WM_USER_TIME プロパティを設定せずにマップします。

_NET_WM_USER_TIME に 0 を設定した場合は、特殊な値として扱われ、そのウィンドウのマップ時に、ウィンドウマネージャによって入力フォーカスがセットされません。

ウィンドウマネージャは、トップレベルウィンドウを表示する時 (MapRequest イベントを受け取った時) に、この値を参照することで、ユーザーアクションによって作成されたウィンドウなのか、それ以外の状況で作成されたウィンドウなのかを判断して、表示位置、重なり順、入力フォーカスを設定するかなどを調整します。
StartupNotify
アプリが StartupNotify とともに起動された場合、DESKTOP_STARTUP_ID 環境変数の文字列内に、X サーバーの時間が含まれています。
("TIME" 以降の 10 進数の数値)

例: "pcmanfm-834-arch-leafpad-85_TIME11126003"

これは、アプリの起動が、ユーザーアクションによって実行されたものとして扱えるため、アプリの起動後に最初にマップするウィンドウに対して、_NET_WM_USER_TIME でこのタイムスタンプを設定することができます。
_NET_WM_USER_TIME_WINDOW
_NET_WM_USER_TIME プロパティの代わりに、ウィンドウに _NET_WM_USER_TIME_WINDOW プロパティが設定されている場合、ユーザータイムの値を、指定されたウィンドウから参照することができます。
(type = "WINDOW", format = 32)

通常は、各トップレベルウィンドウに対して _NET_WM_USER_TIME プロパティを設定しますが、その代わりに _NET_WM_USER_TIME_WINDOW プロパティを設定した場合、アプリケーションが管理している一つのウィンドウだけに _NET_WM_USER_TIME プロパティを設定して、他のウィンドウは、その値を参照する形にすることができます。

※ルートウィンドウの _NET_SUPPORTED に、_NET_WM_USER_TIME_WINDOW が存在することを確認してください。
_NET_WM_USER_TIME_WINDOW がサポートされていない場合は、各ウィンドウに _NET_WM_USER_TIME プロパティを設定する必要があります。

基本的には、アプリケーションのグループリーダーウィンドウに _NET_WM_USER_TIME プロパティを設定して、他の通常ウィンドウは _NET_WM_USER_TIME_WINDOW プロパティで、グループリーダーウィンドウの ID を設定します。

これが必要な理由
アクティブなウィンドウで ButtonPress, KeyPress イベントが来た時は、常にユーザータイムを更新する必要があります。

ウィンドウマネージャなどは、PropertyNotify イベントで、ウィンドウのプロパティが変更されたことを常に監視しているため、ウィンドウマネージャが管理している通常のウィンドウで _NET_WM_USER_TIME プロパティが変更された場合、毎回 PropertyNotify イベントを処理することになります。
(実際はウィンドウをマップする時だけ必要なので、これは無駄な動作になります)

しかし、_NET_WM_USER_TIME_WINDOW プロパティを設定した場合は、グループリーダーなど、ウィンドウマネージャが管理していないウィンドウで _NET_WM_USER_TIME の値を変更できるので、ウィンドウマネージャによる PropertyNotify イベント処理が行われなくなり、効率がよくなります。
プログラム
左クリックでウィンドウを作成、閉じるボタンで閉じます。
メインウィンドウの閉じるボタンで終了します。

特に変わったことが起こるわけではありませんが、ユーザータイムの例として参考にしてください。

$ cc -o run d13-usertime.c util.c -lX11

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

static Window _create_window(int width,int height,unsigned int bkgnd,Window leader)
{
    Window win;
    XWMHints h;

    win = create_test_window2(g_disp.disp,
        width, height, rgb_to_pixel(bkgnd),
        ButtonPressMask | KeyPressMask);

    XChangeProperty(g_disp.disp, win,
        GET_ATOM("_NET_WM_USER_TIME_WINDOW"),
        XA_WINDOW, 32, PropModeReplace, (unsigned char *)&leader, 1);

    h.flags = WindowGroupHint;
    h.window_group = leader;

    XSetWMHints(g_disp.disp, win, &h);

    return win;
}

static void _set_user_time(Window win,unsigned long time)
{
    XChangeProperty(g_disp.disp, win,
        GET_ATOM("_NET_WM_USER_TIME"),
        XA_CARDINAL, 32, PropModeReplace, (unsigned char *)&time, 1);

    printf("[usertime] 0x%lx\n", time);
}

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

    set_display(disp);

    //グループリーダー

    leader = XCreateWindow(disp, g_disp.root,
        0, 0, 1, 1, 0,
        CopyFromParent, InputOnly, CopyFromParent,
        0, NULL);

    //通常ウィンドウ

    win = _create_window(200, 200, 0, leader);

    //イベント

    XMapWindow(disp, win);

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

        if(event_quit(&ev))
        {
            if(ev.xany.window == popup)
            {
                XDestroyWindow(disp, popup);
                popup = None;
                continue;
            }
            else
                break;
        }

        switch(ev.type)
        {
            case ButtonPress:
                _set_user_time(leader, ev.xbutton.time);

                if(ev.xbutton.button == Button1 && !popup)
                {
                    popup = _create_window(150, 150, 0x0000ff, leader);

                    XMapWindow(disp, popup);
                }
                break;
            case KeyPress:
                _set_user_time(leader, ev.xkey.time);
                break;
        }
    }

    XCloseDisplay(disp);

    return 0;
}