X11: D&D (2) - ソース側

プログラム
D&D のソース側を実装したプログラムです。

左ドラッグで、D&D を開始します。
対応しているデータは、text/uri-list のみです。

D&D 中でも、右ボタンを押すとプログラムが終了します。ソースがクラッシュした時の確認用に使ってください。

閉じるボタンで終了します。

$ cc -o run d22-dnd2.c util.c -lX11

<d22-dnd2.c>
#include <stdio.h>
#include <string.h>
#include <X11/Xlib.h>
#include <X11/Xatom.h>
#include <X11/cursorfont.h>
#include "util.h"

enum
{
    _ATOM_XdndSelection,
    _ATOM_XdndAware,
    _ATOM_XdndEnter,
    _ATOM_XdndPosition,
    _ATOM_XdndStatus,
    _ATOM_XdndLeave,
    _ATOM_XdndDrop,
    _ATOM_XdndFinished,
    _ATOM_XdndActionCopy,
    _ATOM_URI_LIST,

    _ATOM_NUM
};

const char *g_names[] = {
    "XdndSelection", "XdndAware", "XdndEnter", "XdndPosition",
    "XdndStatus", "XdndLeave", "XdndDrop", "XdndFinished",
    "XdndActionCopy", "text/uri-list"
};

const char *g_string = "file:///usr/include/X11/Xlib.h";

Atom atoms[_ATOM_NUM];
Window g_dnd_dst = None;
int g_action = None;

/* ClientMessage 送信 */

static void _send_message(int type,unsigned long val1,unsigned long val2,
    unsigned long val3,unsigned long val4,unsigned long val5)
{
    XClientMessageEvent ev;

    ev.type = ClientMessage;
    ev.window = g_dnd_dst;
    ev.message_type = atoms[type];
    ev.format = 32;
    ev.data.l[0] = val1;
    ev.data.l[1] = val2;
    ev.data.l[2] = val3;
    ev.data.l[3] = val4;
    ev.data.l[4] = val5;

    XSendEvent(g_disp.disp, g_dnd_dst, False, 0, (XEvent *)&ev);
}

/* XdndStatus */

static void _dnd_status(XClientMessageEvent *ev)
{
    if(ev->data.l[0] != g_dnd_dst) return;

    printf("[XdndStatus] target(0x%lx) flags(0x%lx) "
        "x(%ld) y(%ld) w(%ld) h(%ld) action(",
        ev->data.l[0], ev->data.l[1],
        (unsigned long)ev->data.l[2] >> 16, ev->data.l[2] & 0xffff,
        (unsigned long)ev->data.l[3] >> 16, ev->data.l[3] & 0xffff);

    put_atom_name(ev->data.l[4]);
    printf(")\n");

    if(ev->data.l[1] & 1)
        g_action = ev->data.l[4];
    else
        g_action = None;
}

/* XdndFinished */

static void _dnd_finished(XClientMessageEvent *ev)
{
    if(ev->data.l[0] != g_dnd_dst) return;

    printf("[XdndFinished] target(0x%lx) flags(0x%lx) action(",
        ev->data.l[0], ev->data.l[1]);

    put_atom_name(ev->data.l[2]);
    printf(")\n");

    g_dnd_dst = None;
}

static void _client_message(XClientMessageEvent *ev)
{
    Atom mestype;

    mestype = ev->message_type;

    if(mestype == atoms[_ATOM_XdndStatus])
        _dnd_status(ev);
    else if(mestype == atoms[_ATOM_XdndFinished])
        _dnd_finished(ev);
}

static void _send_leave(Window win)
{
    if(g_dnd_dst)
    {
        _send_message(_ATOM_XdndLeave, win, 0, 0, 0, 0);

        g_dnd_dst = None;

        printf("* XdndLeave\n");
    }
}

/* ルート座標下で XdndAware プロパティがあるウィンドウを取得
 * (下位も検索。ルートも含む) */

static Window _get_enter_window(Window root,int x,int y,int *ret_ver)
{
    Window child,win;
    long val = -1;
    int dx,dy;

    win = root;

    while(XTranslateCoordinates(g_disp.disp, root, win,
        x, y, &dx, &dy, &child))
    {
        if(!child && win != root) return None;

        if(child) win = child;
    
        if(read_prop32_array(win, atoms[_ATOM_XdndAware],
            XA_ATOM, &val, 1) == 0)
            val = -1;

        if(val > 5) val = 5;
        if(val != -1) break;

        if(!child) return None;
    }

    *ret_ver = val;

    return win;
}

int main(int argc,char **argv)
{
    Display *disp;
    Window win,enter_win;
    XEvent ev;
    Cursor cursor;
    int fdnd,ver;
    XSelectionEvent evs;
    
    disp = XOpenDisplay(NULL);
    if(!disp) return 1;

    set_display(disp);

    XInternAtoms(disp, (char **)g_names, _ATOM_NUM, False, atoms);

    win = create_test_window2(disp, 200, 200, 0,
        ButtonPress | ButtonMotionMask | ButtonReleaseMask);

    set_prop32_one(win, atoms[_ATOM_XdndAware], XA_ATOM, 5);

    //

    cursor = XCreateFontCursor(disp, XC_hand1);
    fdnd = 0;

    //イベント

    XMapWindow(disp, win);

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

        if(event_quit(&ev)) break;

        switch(ev.type)
        {
            case ClientMessage:
                _client_message((XClientMessageEvent *)&ev);
                break;
            case ButtonPress:
                if(!fdnd && ev.xbutton.button == Button1)
                {
                    XGrabPointer(disp, win, False,
                        ButtonPressMask | ButtonReleaseMask | ButtonMotionMask,
                        GrabModeAsync, GrabModeAsync, None, cursor,
                        ev.xbutton.time);

                    XSetSelectionOwner(disp, atoms[_ATOM_XdndSelection], win,
                        ev.xbutton.time);

                    g_action = None;
                    fdnd = 1;
                }
                else if(ev.xbutton.button == Button3)
                    goto END;
                break;
            case ButtonRelease:
                if(fdnd && ev.xbutton.button == Button1)
                {
                    if(g_dnd_dst)
                    {
                        if(!g_action)
                            _send_leave(win);
                        else
                            _send_message(_ATOM_XdndDrop, win, 0, ev.xbutton.time, 0, 0);
                    }

                    XUngrabPointer(disp, CurrentTime);
                    fdnd = 0;
                }
                break;
            case MotionNotify:
                if(fdnd)
                {
                    //カーソル下の XdndAware 対応ウィンドウ
                    enter_win = _get_enter_window(ev.xmotion.root,
                        ev.xmotion.x_root, ev.xmotion.y_root, &ver);

                    if(enter_win == win) enter_win = None;

                    if(g_dnd_dst != enter_win)
                    {
                        _send_leave(win);

                        if(enter_win)
                        {
                            g_dnd_dst = enter_win;
                        
                            //XdndEnter
                            _send_message(_ATOM_XdndEnter, win,
                                ver << 24, atoms[_ATOM_URI_LIST], 0, 0);

                            printf("* XdndEnter (0x%lx)\n", enter_win);
                        }
                    }

                    if(g_dnd_dst)
                    {
                        //XdndPosition
                        _send_message(_ATOM_XdndPosition, win, 0,
                            (ev.xmotion.x_root << 16) | ev.xmotion.y_root,
                            ev.xmotion.time, atoms[_ATOM_XdndActionCopy]);
                    }
                }
                break;
            case SelectionRequest:
                printf("[SelectionRequest] requestor(0x%lx) time(0x%lx) target(",
                    ev.xselectionrequest.requestor, ev.xselectionrequest.time);

                put_atom_name(ev.xselectionrequest.target);
                printf(")\n");

                if(ev.xselectionrequest.property == None)
                    ev.xselectionrequest.property = ev.xselectionrequest.target;

                if(ev.xselectionrequest.target == atoms[_ATOM_URI_LIST])
                {
                    XChangeProperty(g_disp.disp, ev.xselectionrequest.requestor,
                        ev.xselectionrequest.property,
                        atoms[_ATOM_URI_LIST], 8,
                        PropModeReplace, (unsigned char *)g_string, strlen(g_string));
                }
                else
                    ev.xselectionrequest.property = None;

                //SelectionNotify を送る

                evs.type = SelectionNotify;
                evs.requestor = ev.xselectionrequest.requestor;
                evs.selection = ev.xselectionrequest.selection;
                evs.target = ev.xselectionrequest.target;
                evs.property = ev.xselectionrequest.property;
                evs.time = ev.xselectionrequest.time;

                XSendEvent(disp, evs.requestor, False, 0, (XEvent *)&evs);
                break;
        }
    }

END:
    XCloseDisplay(disp);

    return 0;
}
解説
※ターゲットの XDND バージョンにかかわらず、常に ver 5 として処理しています。
通常は、ターゲットのバージョンに合わせて処理を調整してください。

色々と細かい部分の処理は省いています。
MotionNotify イベント
ドラッグ中、現在のポインタ位置が、どのウィンドウ上にあるのかは、ソース側が判断する必要があります。

通常は、EnterNotify/LeaveNotify イベントで、ポインタがウィンドウ上に表示されたか離れたかを判断することができますが、プログラムで作成したウィンドウで EnterWindowMask, LeaveWindowMask を選択しても、そのウィンドウに対する EnterNotify/LeaveNotify イベントしか来ません。
(他のクライアントが作成したウィンドウの EnterNotify/LeaveNotify イベントは受け取れない)

そのため、ソース側は、ルート座標の位置を元にして、その下にあるウィンドウを取得する必要があります。

Bool XTranslateCoordinates(Display *display,
    Window src_w, Window dest_w, int src_x, int src_y,
    int *dest_x_return, int *dest_y_return, Window *child_return);

XTranslateCoordinates 関数は、src ウィンドウの座標を、dest ウィンドウの座標に変換します。
また、dest の直下の子ウィンドウ上に指定座標があれば、child_return に子ウィンドウを返します (なければ None)。

この関数を使って、ルートから順に、指定位置の下にある子ウィンドウを辿ることができます。

フレームウィンドウ
ウィンドウマネージャによって、装飾フレームウィンドウが作成されていることを思い出してくだい。

ウィンドウがマップされた時、実際のウィンドウは、装飾フレームウィンドウの子になっているので、ルートの直下の子ウィンドウは、装飾フレームウィンドウになります (ウィンドウ属性の override_redirect が True でない場合)。

中身のウィンドウを取得したい場合は、さらにその子ウィンドウを検査する必要があります。

ターゲットウィンドウの検索方法
今回の場合、D&D をサポートしているウィンドウには XdndAware プロパティがあって、バージョン値が設定されています。

そのため、まず、src_w と dest_w をルートウィンドウに指定して、指定座標下にある、ルートウィンドウの直下のウィンドウを取得します。
子ウィンドウがなければ、ルートウィンドウの XdndAware プロパティを調べます。
(ルートウィンドウに対して D&D が出来る場合もあるので、ルートウィンドウも対象に含めてください)

子ウィンドウがある場合、子ウィンドウの XdndAware プロパティを調べて、存在すれば、それをターゲットにします。
プロパティが存在しない場合は、dest_w を子ウィンドウにして、さらにその直下の子ウィンドウを調べます。返った子ウィンドウがなければ、ターゲットはなしとなります。
XdndAware プロパティが見つかるまでこれを繰り返すことで、ターゲットウィンドウを取得します。

指定座標下のすべてのウィンドウで XdndAware プロパティがない場合は、ターゲットなしとなるので、ターゲットが現在 Enter 状態の場合は、Leave 扱いにします。

ソースウィンドウをドロップ先として含めたくない場合は、ソースウィンドウをターゲットにしないことにも注意してください。

XdndEnter/XdndPosition
現在のターゲットと、ポインタ位置下にあるターゲットウィンドウが異なる場合、現在のターゲットに XdndLeave を送信した後、新しいターゲットに XdndEnter を送信します。

XdndEnter では、ターゲットとソースが対応しているバージョン値の小さい方の値を指定します。

その後、XdndPosition で現在位置を送信します。

ターゲットウィンドウに変化がない場合は、XdndPosition だけを送信します。
SelectionRequest イベント
ドロップ中またはドロップ後に、ターゲットからデータを要求された場合、クリップボードの時と同じように SelectionRequest イベントが来ます。
データを変換して、プロパティに値をセットしてください。

これは完全にクリップボードと同じように処理できるので (MULTIPLE ターゲットも含めて)、クリップボードの処理も実装している場合は、XdndSelection セレクションと CLIPBOARD セレクションの処理を同じものにしても構いません。