X11: D&D (1)

D&D
X でドラッグアンドドロップを実現するためには、ウィンドウのプロパティ・セレクション・ClientMessage イベントを使って、ドロップ元とドロップ先のクライアント間で通信を行います。

ここでは、「ソース」=ドラッグを開始してデータを所持する側、「ターゲット」=データを受け取るドロップ先となります。

ドロップ後のデータの受け取りは、クリップボードと同じく、セレクションを介して行います。
データ型
D&D データのタイプは、基本的に MIME タイプのアトムで指定します。
例えば、ローカルのファイルパスを含む URI は「text/uri-list」、UTF-8 文字列の場合は「text/plain;charset=utf-8」となります。
アクション
ドロップされた先で、ターゲットがどのようなアクションを実行するかは、以下のようなアトムで指定されます。

XdndActionCopyコピー、または通常の処理
XdndActionMove移動
XdndActionLinkソースのデータをリンク
XdndActionAskドロップ時、ターゲットがアクションをユーザーに尋ねる
XdndActionPrivateソースとターゲット間のプライベート動作

ソースとターゲット間で、これ以上のアクションを行いたい場合は、任意のタイプを使用することも出来ます。
XdndAware プロパティ
まず、D&D の操作をサポートするには、ソース (ドラッグ元) とターゲット (ドロップを受け取る側) の両方のウィンドウで、XdndAware プロパティを設定する必要があります。
(type = "ATOM", format = 32)

タイプはアトムですが、実際にはバージョン番号の数値を設定します (0〜255)。
XDND の現在の最大バージョンは 5 です。
各ウィンドウで、サポートしている XDND の最大バージョンを指定します。

基本的に、トップレベルウィンドウの作成時にセットします (子ウィンドウがある場合、各子ウィンドウで設定する必要はない)。

実際の D&D 処理時に使用されるバージョンは、ソースとターゲットのバージョンのうち、小さい方の値となります。
手順
1.ドラッグ開始
ソース側で、ポインタボタンが押されて、ドラッグが開始される場合、ソースは、XSetSelectionOwner 関数を使って、XdndSelection セレクションの所有権を取得します。

また、ソースが対応しているデータ型が4つ以上ある場合は、ソースウィンドウの XdndTypeList プロパティに、type = "ATOM", format = 32 で、データ型のアトムのリストをセットする必要があります。

XdndSelection セレクションに対しては、CLIPBOARD セレクションと同じターゲットを使用することができます (TARGETS, MULTIPLE など)。
2.ターゲットウィンドウに入った
ドラッグ中、ポインタの位置が、XDND をサポートしている (XdndAware プロパティがある) ウィンドウの領域に入った場合、ソースはターゲットウィンドウに対して、XSendEvent() で ClientMessage イベント (message_type = XdndEnter) を送ります。

data.l[0]ソースウィンドウ ID
data.l[1]フラグ。
bit0: ソースが4つ以上のデータ型をサポートしている場合、ON。
bit24-31: ソースとターゲットがサポートしているバージョンの最小値。
data.l[2-4](Atom) ソースがサポートする、最初の3つのデータ型。0 でなし。
データ型が4つ以上ある場合、設定する値の順番は、任意で構いません。
また、その場合、すべて 0 でも構いません。

data.l[1] の bit0 が ON の場合、ソース側が実際にサポートしているデータ型は4つ以上あるため、ソースウィンドウの XdndTypeList プロパティから、ATOM のリストを読み込んで、すべてのデータ型を取得する必要があります。

このデータ型は、XConvertSelection() で変換できるターゲットのリストとなります。

XdndEnter を受け取ったターゲットウィンドウは、このウィンドウがドロップの対象になったものとして、準備を行う必要があります。
具体的には、ソースウィンドウの ID と、データ型のリストを保存します。
3.ポインタ位置が移動した時
ターゲットウィンドウ上で、ポインタ位置が移動した場合、ソースからターゲットに ClientMessage (message_type = XdndPosition) が送られます。

data.l[0]ソースウィンドウID
data.l[1]予約(フラグ)
data.l[2]ルート座標でのポインタ位置。
(X << 16) | Y
data.l[3](Time) XConvertSelection() でデータを取得するためのタイムスタンプ (ver1)
data.l[4](Atom) ユーザーによって要求されたアクション (ver2)

XdndPosition を受け取ったターゲットウィンドウは、data.l[2] で指定された位置を元に、XTranslateCoordinates 関数を使って、ルート座標からターゲットウィンドウ上の位置へ座標を変換し、その位置から、実際のドロップ対象となる、子ウィンドウや内部のウィジェットを検索します。

また、その実際のターゲットが、指定されたアクションや、XdndEnter 時に取得したデータ型を受け付けるかどうかも判断する必要があります。

データ型だけでは判断できず、実際のデータを取得しないと判断できない場合は、XConvertSelection 関数で、所有者がソースである XdndSelection セレクションを指定し、time には data.l[4] の値を指定して、取得したいデータのターゲットを指定します。
ソースによって、実際にプロパティにデータがセットされた場合、SelectionNotify イベントが来ます。
必要があれば、これを複数回行えます。
4.ソースに情報を伝える
ターゲットウィンドウが XdndPosition を受け取った時、その位置でドロップを受け付けるかなどの情報をソースに伝えるために、ターゲットからソースに ClientMessage (message_type = XdndStatus) が送られます。

data.l[0]ターゲットウィンドウ ID。
これは、ソースが XdndLeave を送信した後に、ターゲットから XdndStatus が来た場合、ソースがそれを無視するために必要になります。
data.l[1]bit0: 現在のターゲットがドロップを受け付ける場合、ON。
bit1: 矩形内の移動中も XdndPosition の送信が必要な場合、ON。
data.l[2,3]ルート座標での矩形範囲。
ポインタがこの範囲内にいる間は、再び XdndPosition を送信させないようにする。
空 (0) の場合は、ポインタが移動する度に、XdndPosition が送信される。
※矩形範囲のあるなしにかかわらず、ソースは常に XdndPosition を送信してもよい。

l[2] = (X << 16) | Y
l[3] = (W << 16) | H
data.l[4]ターゲット側で受け入れられたアクション (ver2)。
通常は、XdndPosition で指定されたアクション or XdndActionCopy or XdndActionPrivate のいずれかのみが許可されます。
ドロップが受け入れられない場合は、None。

※ドロップを受け付けたくない場合、data.l[1] のフラグはすべて 0 にする必要があります。
bit0 が OFF でも、他のビットが ON の場合、ドロップを受け付けるものとして扱われてしまいます。
5.ソース側の処理
ソースが、ターゲットから XdndStatus を受け取った場合、指定されたアクションを元にカーソル形状を変更することで、ユーザーに対して、この位置でドロップした時にどのアクションが実行されるかを示すことができます。

ポインタ位置の移動時、XdndStatus で指定された矩形の範囲外に出た場合や、範囲指定がない場合は、ポインタ位置の移動時、再びターゲットに XdndPosition を送ります。

XdndPosition は通常、MotionNotify イベント時に送信されますが、ソースがターゲットからの XdndStatus を待っている間に、ポインタが新しい位置に移動した場合は、ソースは新しい位置を保存して、実際に XdndStatus を受信した後、すぐに新しい位置の XdndPosition を送信する必要があります。
6.ターゲットから離れた
ポインタが、現在のターゲットウィンドウ以外のウィンドウ上に来た場合、ソースはターゲットに ClientMessage (message_type = XdndLeave) を送信します。

data.l[0]ソースウィンドウ ID
data.l[1]予約(フラグ)

これを受け取ったターゲットは、このウィンドウがドロップ対象から外れたものとして扱い、現在保持しているデータなどを解放して、D&D の処理を完全に終了する必要があります。

また、ボタンが離された時点で、最後に受け取った XdndStatus により、ドロップが受け入れられなかった場合や、ソースが XdndStatus を一つも受信しなかった場合は、キャンセルと同じ扱いで、同様に XdndLeave が送信されます。
7.ドロップの受け入れ
ポインタボタンが離されて、その位置でドロップが受け入れられる場合、ソースからターゲットに ClientMessage (message_type = XdndDrop) が送信されます。

data.l[0]ソースウィンドウ ID
data.l[1]予約(フラグ)
data.l[2]データを取得するためのタイムスタンプ (ver1)

これを受け取ったターゲットが、実際にドロップを受け入れる場合は、ソースから必要なデータを取得します。

XConvertSelection 関数で、XdndSelection セレクションを指定し、time には data.l[2] の値を指定して、取得したいデータのターゲットを指定します (XdndEnter 時に取得したリストを使う)。

その後、ターゲットは、ソースに ClientMessage (message_type = XdndFinished) を送信する必要があります (ver2 以降の場合)。

※実際にドロップを受け入れたかどうかにかかわらず、XdndDrop を受け取った場合は、常に XdndFinished を送信する必要があります。
これにより、ソースがデータを保持する必要がなくなった (ターゲットがこれ以上のデータを要求しない) ことを通知します。

■ XdndFinished
data.l[0]ターゲットウィンドウ ID
data.l[1]bit0: ターゲットがドロップを正しく受け入れた場合 ON (ver5)。
ソースで使用されているバージョンが 5 未満の場合、常に ON であるものとして扱います。
data.l[2]ターゲットによって実行されるアクション (ver5)。
現在のターゲットがドロップを拒否した場合、つまり data.l[1] の bit0 が OFF の場合、None。

ver 5 の場合は、実際にドロップを受け入れたかどうかによって、フラグとアクションをセットします。
ver 5 未満の場合、ソースは、常にドロップが受け入れられたものとして扱う必要があります。
注意点
D&D 処理を行っている間、実際に処理しているソースまたはターゲット以外のウィンドウからの ClientMessage は無視する必要があります。

また、D&D 処理中、一方のクライアントがクラッシュした場合に備えて、対策をしておく必要があります。
クラッシュ時
一方がクラッシュした後に、存在しないウィンドウに対して XSendEvent() などを実行した場合、BadWindow エラーが出ます。

BadWindow エラーは、エラーハンドラで捕捉できます。
D&D 処理中に BadWindow エラーが出た場合は、相手がクラッシュしたものとして扱ってください。

次の ClientMessage イベントを待っている間に一方がクラッシュした場合、データを解放できるタイミングがわからなくなるので、ソースかターゲットのウィンドウが確定した時点で、XSelectInput(disp, win, StructureNotifyMask) を行い、そのウィンドウが破棄された時の DestroyNotify イベントを受信できるようにします。

ターゲットの Enter 中にソースがクラッシュした場合は、XdndLeave が来たものとして扱います。
ドロップ後にターゲットがクラッシュした場合は、D&D 処理を終了させます。

異なるクライアントが、同じウィンドウに対して、同じイベントマスクを選択すると、各クライアントごとにそのウィンドウのイベントを取得することができます。
X サーバーは、対象となるすべてのクライアントに対して、同じイベントを送信します。
ただし、一部のイベントは、一つのクライアントにしか送信されません。
XdndActionAsk
ソースが XdndActionAsk アクションに対応している場合、ソースウィンドウに以下のプロパティが設定されている場合があります。
XdndActionList プロパティ (ver2)
type = "ATOM", format = 32。

ソースがサポートしているすべてのアクションのリストが設定されています。
XdndActionDescription プロパティ (ver2)
type = "STRING", format = 8。

ターゲット側でユーザーにアクションを尋ねる時に表示する文字列。
XdndActionList プロパティの各アクションに対応しており、NULL 区切りの ASCII 文字列となっています。

「キャンセル」の動作は含まれません。
プログラム
D&D で、ドロップを受け取るウィンドウです。
ファイラなどでファイルを D&D してみてください。

青い部分にドラッグすると、ドロップを受け取り、text/uri-list で URI リストの文字列を取得して、表示します。
黒い部分にドラッグした場合、ドロップは拒否されます。

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

$ cc -o run d21-dnd1.c util.c -lX11

<d21-dnd1.c>
#include <stdio.h>
#include <stdlib.h>
#include <X11/Xlib.h>
#include <X11/Xatom.h>
#include "util.h"

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

    _ATOM_NUM
};

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

Atom atoms[_ATOM_NUM], g_action = None;
Window g_dnd_src = None;
int g_width,g_height;

/* 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_src;
    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_src, False, 0, (XEvent *)&ev);
}

/* XdndEnter */

static void _dnd_enter(XClientMessageEvent *ev)
{
    int i;

    g_dnd_src = (Window)ev->data.l[0];

    printf("[XdndEnter] src(0x%lx) flags(0x%lx:ver=%d)",
        ev->data.l[0], ev->data.l[1],
        (unsigned int)ev->data.l[1] >> 24);

    for(i = 2; i <= 4; i++)
    {
        printf(" type%d(", i - 1);
        put_atom_name(ev->data.l[i]);
        printf(")");
    }

    printf("\n");

    //4つ以上のタイプ

    if(ev->data.l[1] & 1)
        put_prop_atoms(g_dnd_src, atoms[_ATOM_XdndTypeList], "XdndTypeList");

    //ソースウィンドウの破棄を通知するようにする

    XSelectInput(g_disp.disp, g_dnd_src, StructureNotifyMask);
}

/* XdndPosition */

static void _dnd_position(XClientMessageEvent *ev)
{
    int x,y,flag;
    Window child;

    if(ev->data.l[0] != g_dnd_src) return;

    x = (unsigned long)ev->data.l[2] >> 16;
    y = ev->data.l[2] & 0xffff;

    printf("[XdndPosition] src(0x%lx) x(%d) y(%d) time(0x%lx) action(",
        ev->data.l[0], x, y, ev->data.l[3]);

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

    g_action = ev->data.l[4];

    //XdndStatus

    XTranslateCoordinates(g_disp.disp, g_disp.root, ev->window,
        x, y, &x, &y, &child);

    flag = (x < g_width / 2);

    _send_message(_ATOM_XdndStatus, ev->window,
        (flag)? 3: 0, 0, 0, g_action);
}

/* XdndDrop */

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

    printf("[XdndDrop] src(0x%lx) time(0x%lx)\n",
        ev->data.l[0], ev->data.l[2]);

    XConvertSelection(g_disp.disp, atoms[_ATOM_XdndSelection],
        atoms[_ATOM_URI_LIST], atoms[_ATOM_MY_SELECTION],
        ev->window, (Time)ev->data.l[2]);
}

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

    mestype = ev->message_type;

    if(mestype == atoms[_ATOM_XdndEnter])
        _dnd_enter(ev);
    else if(mestype == atoms[_ATOM_XdndPosition])
        _dnd_position(ev);
    else if(mestype == atoms[_ATOM_XdndLeave])
    {
        if(g_dnd_src == (Window)ev->data.l[0])
        {
            printf("[XdndLeave]\n");
            g_dnd_src = None;
        }
    }
    else if(mestype == atoms[_ATOM_XdndDrop])
        _dnd_drop(ev);
}

int main(int argc,char **argv)
{
    Display *disp;
    Window win;
    XEvent ev;
    char *buf;
    int size;
    
    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,
        StructureNotifyMask | ExposureMask);

    g_width = 200;
    g_height = 200;

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

    XSetForeground(disp, g_disp.gc, rgb_to_pixel(0x0000ff));

    //イベント

    XMapWindow(disp, win);

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

        if(event_quit(&ev)) break;

        switch(ev.type)
        {
            case Expose:
                XFillRectangle(disp, win, g_disp.gc,
                    0, 0, g_width / 2, g_height);
                break;
            case ClientMessage:
                _client_message((XClientMessageEvent *)&ev);
                break;
            case ConfigureNotify:
                g_width = ev.xconfigure.width;
                g_height = ev.xconfigure.height;
                break;
            case SelectionNotify:
                printf("[SelectionNotify] target(");
                put_atom_name(ev.xselection.target);
                printf(")\n");

                if(ev.xselection.property != None)
                {
                    buf = (char *)read_prop8(ev.xselection.requestor,
                        ev.xselection.property, NULL, &size);

                    if(buf)
                    {
                        printf("-----------\n");
                        fwrite(buf, 1, size, stdout);
                        printf("\n-----------\n");

                        free(buf);
                    }

                    XDeleteProperty(disp, ev.xselection.requestor,
                        ev.xselection.property);
                }

                //データを受け取ったら、D&D終了
                _send_message(_ATOM_XdndFinished, win, 1, g_action, 0, 0);
                g_dnd_src = None;
                break;
            case DestroyNotify:
                if(g_dnd_src && ev.xdestroywindow.window == g_dnd_src)
                {
                    printf("* Destroy src\n");
                    g_dnd_src = None;
                }
                break;
        }
    }

    XCloseDisplay(disp);

    return 0;
}
解説
※XDND のバージョンは、5であるものとして処理しています。
実際は、XdndEnter で指定されたバージョンに合わせて調整する必要があります (古いアプリにも対応する場合)。

黒い部分にドロップした場合、XdndLeave が来るのを確認してください。
ドラッグ中、カーソル形状が変化する場合もあります。
XdndDrop
ここでは、XdndDrop 時、XConvertSelection() でデータを要求し、SelectionNotify イベントが来た時に D&D を終了しています。

通常は、クリップボード時と同じように、XConvertSelection() の後に poll() などを使ってイベントを待機し、その場でデータを取得するようにしてください。
ファイラからの D&D
[XdndEnter] src(0x1600004) flags(0x5000001:ver=5)
 type1(application/x-qabstractitemmodeldatalist) type2(text/uri-list) type3(text/x-moz-url)
<XdndTypeList> (5) application/x-qabstractitemmodeldatalist,
 text/uri-list, text/x-moz-url, text/plain, libfm/files, 
[XdndPosition] src(0x1600004) x(139) y(846) time(0xdb9d96) action(XdndActionCopy)

ファイラ (pcmanfm-qt) でファイルを D&D した場合、上記のようになります。

XdndEnter で渡された型タイプは、XdndTypeList プロパティの先頭の3つの値になっています。
テキストの D&D
[XdndEnter] src(0x1a03dbb) flags(0x5000001:ver=5) type1(0) type2(0) type3(0)
<XdndTypeList> (8) GTK_TEXT_BUFFER_CONTENTS, application/x-gtk-text-buffer-rich-text,
 UTF8_STRING, COMPOUND_TEXT, TEXT, STRING, text/plain;charset=utf-8, text/plain

テキストエディタ (Mousepad) で選択したテキストを D&D した場合、上記のようになります。

XdndEnter で渡される型タイプが、すべて 0 になっています。
また、型タイプは通常 MIME タイプのみであるはずですが、STRING なども含まれています。
画像の D&D
[XdndEnter] src(0xe000c4) flags(0x5000001:ver=5) type1(0) type2(0) type3(0)
<XdndTypeList> (20) text/x-moz-url, _NETSCAPE_URL, text/x-moz-url-data, text/x-moz-url-desc,
 application/x-moz-custom-clipdata, text/_moz_htmlcontext, text/_moz_htmlinfo, text/html,
 text/plain, text/plain;charset=utf-8, application/x-moz-nativeimage,
 image/png, image/jpeg, image/jpg, image/gif, application/x-moz-file-promise,
 XdndDirectSave0, application/x-moz-file-promise-url, text/uri-list,
 application/x-moz-file-promise-dest-filename,

FireFox 上で画像 (リンクあり) を D&D した場合、上記のようになります。

image/png, image/jpeg, image/jpg, image/gif などがあるので、各画像フォーマットでデータを取得することができます。
XdndDirectSave0 については、後述します。
text/uri-list
D&D では、ファイルに限らず、テキストや画像など、多くのタイプのデータを扱うことができますが、基本的には、ファイラから任意のファイルを D&D するといった使い方が一般的です。

D&D でファイルパスを取得したい場合は、text/uri-list の MIME タイプを使います。
(text/plain も text/x-moz-url も、text/uri-list と同じです)

Unix における、ローカルファイルの URI は、「file://<hostname>/<path>」となります。
hostname は、gethostname() で取得できる文字列です。hostname は空にすることができます。

例:
file:///usr/include/X11/X.h
file:///mnt/%E3%83%86%E3%82%B9%E3%83%88.txt # テスト.txt (UTF-8)

パスは ASCII 文字のみです。
それ以外のバイトは、「%XX」の形式で、2桁の16進数で表記します (%00 と %2F '/' は含まれません)。

エンコーディングは特定されておらず、基本的にロケールの文字列として扱われます。

複数の URI がある場合は、改行で区切られます。