XDS
Direct Save Protocol (XDS) は、ファイルマネージャなどに対してファイルをドラッグすることで、ソースが保持しているデータやファイルを、指定されたパスで直接ファイルに保存するための実装です。
XDND を実装した上で、以下の処理を行います。
XDND を実装した上で、以下の処理を行います。
step.1
データをファイルに保存したいソース側で、ドラッグが開始される前に、ソースの XdndDirectSave0 プロパティに、type = "text/plain" (必要に応じて charset 属性を含む), format = 8 で、ユーザーが保存したいファイル名 (ディレクトリパスなし) をセットします。
例: "test.txt"
※"XdndDirectSave0" の最後の 0 は、バージョンを表しています。使用されるバージョンは 0 のみです。
ソースが XdndPosition を送る時のアクションは、XdndActionDirectSave にします。
例: "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" など)。
その後、XdndSelection セレクションに、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文字をセットします。
その後、ターゲットからの応答を待ちます。
要求者の指定プロパティには、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 プロパティを削除する必要があります。
ソースが 'S' または 'F' を送信していた場合は、自身の XdndDirectSave0 プロパティをチェックします。
このデータが空でない場合は、実際にファイルが保存されたことになります。
D&D の処理が終了したら、XdndDirectSave0 プロパティを削除する必要があります。
プログラム
XDS のソース側の処理を実装しています。
左ドラッグで D&D を開始し、ドロップ先に "test_xds.txt" のファイル名で、テキストファイルを作成します。
閉じるボタンで終了します。
<d23-xds.c>
左ドラッグで 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" ファイルが生成されます。
ターゲットが実行するアクションは、XdndActionCopy になっています。
ターゲット上では、ドロップ後はファイルのコピーとして扱われるため、これは正しい動作です。
XdndDirectSave0 ターゲットが要求された時、XdndDirectSave0 プロパティのタイプが text/plain (charset なし) であれば、保存パスの %XX をデコードして、ロケール文字列として扱う必要があります。
ファイラのディレクトリに、"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 は 以下のようになりました。
text/url-list と同じように、UTF-8 文字列のファイル名は、そのまま % でエスケープされています。
"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 をデコードして、ロケール文字列として扱う必要があります。