X11: D&D (3) - XDS

XDS
Direct Save Protocol (XDS) は、ファイルマネージャなどに対してファイルをドラッグすることで、ソースが保持しているデータやファイルを、指定されたパスで直接ファイルに保存するための実装です。

XDND を実装した上で、以下の処理を行います。
step.1
データをファイルに保存したいソース側で、ドラッグが開始される前に、ソースの XdndDirectSave0 プロパティに、type = "text/plain" (必要に応じて charset 属性を含む), format = 8 で、ユーザーが保存したいファイル名 (ディレクトリパスなし) をセットします。
例: "test.txt"

※"XdndDirectSave0" の最後の 0 は、バージョンを表しています。使用されるバージョンは 0 のみです。

ソースが XdndPosition を送る時のアクションは、XdndActionDirectSave にします。
step.2
ターゲット (ファイルの保存先) が XdndPosition を受け取った場合、ターゲット上における現在のディレクトリが、書き込み可能な場合のみ、ドロップを受け入れる必要があります。

ポインタが、ターゲット内の、書き込み可能なディレクトリのアイコンや名前の上にある場合は、それが保存先となるので、アイコンや名前を強調表示します。
step.3
ドロップ時に、ターゲットが XdndDrop を受信した場合、まず、ソースウィンドウの XdndDirectSave0 プロパティをチェックします。
プロパティがない場合は、セレクションから text/uri-list のデータ型を取得して、通常のファイルの D&D として扱います。

XdndDirectSave0 プロパティが存在する場合、そのプロパティから、パス名なしの保存ファイル名を取得します。
そのファイル名に、現在の保存先ディレクトリのパスを付けた後、URI に変換し、その文字列を XdndDirectSave0 プロパティにセットします。
必要であれば、プロパティのタイプを、その文字列のエンコーディングに変更します ("text/plain;charset=utf-8" など)。

例: test.txt -> file:///path/test.txt

その後、XdndSelection セレクションに、XdndDirectSave0 ターゲットを要求します。
step.4
ソースが、SelectionRequest イベントで XdndDirectSave0 のデータ要求を受信した場合、自身の XdndDirectSave0 プロパティから、保存先の URI 文字列を取得して、指定されたファイルにデータを保存します。

要求者の指定プロパティには、type = "STRING", format = 8 で、以下のいずれかの1文字をセットします。

  • 成功時、'S' (0x53)。
  • 失敗時、'F' (0x46)。
  • データを保存することを拒否した場合、'E' (0x45)。

その後、ターゲットからの応答を待ちます。
step.5
ターゲットが SelectionNotify イベントを受け取った場合、プロパティから値を取得して、以下の処理を行います。

  • 'S' (成功) の場合、現在のディレクトリの表示を更新して、新しいファイルを表示し、XdndFinished を送信します。
  • 'F' (失敗) の場合、ソースが対応しているデータ型に "application/octet-stream" が含まれるかどうかを確認します。
    これが利用可能な場合は、XdndSelection セレクションにこのターゲットを要求し、データを取得した後、そのデータをファイルに保存します。成功した場合、現在のディレクトリの表示を更新します。
    それ以外の場合は、ソースウィンドウの XdndDirectSave0 プロパティを、長さ 0 の空データに変更して、失敗を示します。
    その後、XdndFinished を送信します。
  • 'E' (エラー) の場合、何もせずに XdndFinished を送信します。
step.6
ソースが XdndFinished を受け取ります。

ソースが 'S' または 'F' を送信していた場合は、自身の XdndDirectSave0 プロパティをチェックします。
このデータが空でない場合は、実際にファイルが保存されたことになります。

D&D の処理が終了したら、XdndDirectSave0 プロパティを削除する必要があります。
プログラム
XDS のソース側の処理を実装しています。

左ドラッグで D&D を開始し、ドロップ先に "test_xds.txt" のファイル名で、テキストファイルを作成します。

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

$ cc -o run d23-xds.c util.c -lX11

<d23-xds.c>
#include <stdio.h>
#include <stdlib.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_XdndDirectSave0,
    _ATOM_XdndActionDirectSave,
    _ATOM_TEXT_PLAIN,

    _ATOM_NUM
};

const char *g_names[] = {
    "XdndSelection", "XdndAware", "XdndEnter", "XdndPosition",
    "XdndStatus", "XdndLeave", "XdndDrop", "XdndFinished",
    "XdndDirectSave0", "XdndActionDirectSave", "text/plain"
};

const char *g_filename = "test_xds.txt";

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");

    XDeleteProperty(g_disp.disp, ev->window, atoms[_ATOM_XdndDirectSave0]);

    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;
}

static int _save_file(char *path)
{
    FILE *fp;
    const char *str = "テスト ABCDEFG";

    if(strncmp(path, "file://", 7) != 0) return 1;

    fp = fopen(path + 7, "wb");
    if(!fp) return 1;

    fwrite(str, 1, strlen(str), fp);
    
    fclose(fp);

    return 0;
}

int main(int argc,char **argv)
{
    Display *disp;
    Window win,enter_win;
    XEvent ev;
    Cursor cursor;
    int fdnd,ver,ret;
    XSelectionEvent evs;
    char *path,cresult;
    
    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);

                    XChangeProperty(disp, win, atoms[_ATOM_XdndDirectSave0],
                        atoms[_ATOM_TEXT_PLAIN], 8,
                        PropModeReplace, (unsigned char *)g_filename, strlen(g_filename));

                    g_action = None;
                    fdnd = 1;
                }
                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_XdndDirectSave0], 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_XdndActionDirectSave]);
                    }
                }
                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_XdndDirectSave0])
                {
                    path = (char *)read_prop8(win, atoms[_ATOM_XdndDirectSave0], NULL, NULL);
                    if(!path)
                        ret = 1;
                    else
                    {
                        printf("* path: %s\n", path);
                        ret = _save_file(path);
                        free(path);
                    }

                    cresult = (ret)? 'F': 'S';
                
                    XChangeProperty(g_disp.disp, ev.xselectionrequest.requestor,
                        ev.xselectionrequest.property, XA_STRING, 8,
                        PropModeReplace, (unsigned char *)&cresult, 1);
                }
                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;
        }
    }

    XCloseDisplay(disp);

    return 0;
}
解説
プログラムのウィンドウ内で左ドラッグした後、ファイラにドロップしてみてください。
ファイラのディレクトリに、"test_xds.txt" ファイルが生成されます。
XdndDirectSave0 プロパティのタイプ
D&D を開始した時、ソースの XdndDirectSave0 プロパティのタイプを "text/plain;charset=utf-8" にした場合、XdndDirectSave0 ターゲットを要求された時に、ファイルの URI にファイル名部分が含まれませんでした (ディレクトリパスのみ)。

"text/plain" で設定すれば、問題ないようです。

ターゲットであるファイルマネージャー側は、各 charset の文字列に対応しなければならないわけですが、それは実装的に大変ということで、そもそも charset 属性はサポートしていないのでしょう。

仕様書においては、charset 属性が設定されていない場合は ISO-8859 とみなされるということですが、実際にはエンコーディングに関係なく、そのままの文字列として扱われているのかもしれません (もしくは、ロケール文字列)。

例えば、ファイル名を "テスト.txt" (UTF-8) にした場合、セットされた URI は 以下のようになりました。

file:///path/%E3%83%86%E3%82%B9%E3%83%88.txt

text/url-list と同じように、UTF-8 文字列のファイル名は、そのまま % でエスケープされています。
イベント
[XdndStatus] target(0x1600081) flags(0x1) x(0) y(0) w(0) h(0) action(XdndActionCopy)
[SelectionRequest] requestor(0x160008a) time(0xad1e47) target(XdndDirectSave0)
* path: file:///path/test_xds.txt
[XdndFinished] target(0x1600081) flags(0x1) action(XdndActionCopy)

ターゲットが実行するアクションは、XdndActionCopy になっています。
ターゲット上では、ドロップ後はファイルのコピーとして扱われるため、これは正しい動作です。

XdndDirectSave0 ターゲットが要求された時、XdndDirectSave0 プロパティのタイプが text/plain (charset なし) であれば、保存パスの %XX をデコードして、ロケール文字列として扱う必要があります。