Wayland - D&D

ドラッグ&ドロップ
ここでは、ドラッグ&ドロップの処理を行います。

$ cc -o test 13_dnd.c client.c imagebuf.c -lwayland-client -lrt

13_dnd.c
/******************************
 * D&D
 *
 * - ウィンドウ上を左ボタンでドラッグすると、
 *   UTF-8 テキストをドロップできる。
 * - また、テキストをこのウィンドウ上にドロップできる。
 * - 中ボタンで終了。
 ******************************/

#include <stdio.h>
#include <string.h>
#include <stdlib.h>

#define __USE_GNU
#include <fcntl.h>
#include <unistd.h>
#include <poll.h>

#include <linux/input.h>

#include <wayland-client.h>

#include "client.h"
#include "imagebuf.h"


//-------------

typedef struct
{
    Client b;
    Window *win;

    int recv_bufsize,
        recv_cursize;
    char *recv_buf;

    struct wl_data_device_manager *data_device_manager;
    struct wl_data_device *data_device;
    struct wl_data_source *data_source;
    struct wl_data_offer *data_offer;

    uint32_t data_device_manager_ver,
        device_enter_serial,
        source_actions,
        dnd_action;
}ClientEx;

//-------------


/* ソースデータ解放 */

static void _source_release(ClientEx *p)
{
    if(p->data_source)
    {
        wl_data_source_destroy(p->data_source);
        p->data_source = NULL;
    }
}

/* アクション表示 */

static void _put_action(uint32_t f)
{
    if(f == WL_DATA_DEVICE_MANAGER_DND_ACTION_NONE)
        printf(" NONE");
    else
    {
        if(f & WL_DATA_DEVICE_MANAGER_DND_ACTION_COPY)
            printf(" COPY");

        if(f & WL_DATA_DEVICE_MANAGER_DND_ACTION_MOVE)
            printf(" MOVE");

        if(f & WL_DATA_DEVICE_MANAGER_DND_ACTION_ASK)
            printf(" ASK");
    }
}

/* ドロップ先の情報設定 */

static void _target_enter_leave(ClientEx *p,int y)
{
    int type;

    type = ((y >> 8) >= p->win->height / 2);

    //受け取る側が対応可能なアクションを指定 (ver 3 以降)

    if(p->data_device_manager_ver >= WL_DATA_OFFER_SET_ACTIONS_SINCE_VERSION)
    {
        if(type == 0)
        {
            //上半分: COPY,MOVE

            wl_data_offer_set_actions(p->data_offer,
                WL_DATA_DEVICE_MANAGER_DND_ACTION_COPY
                    | WL_DATA_DEVICE_MANAGER_DND_ACTION_MOVE,
                WL_DATA_DEVICE_MANAGER_DND_ACTION_COPY);
        }
        else
        {
            //下半分: ASK

            wl_data_offer_set_actions(p->data_offer,
                WL_DATA_DEVICE_MANAGER_DND_ACTION_ASK,
                WL_DATA_DEVICE_MANAGER_DND_ACTION_ASK);
        }
    }

    //受け入れる MIME タイプ (NULL で D&D を受け入れない)

    wl_data_offer_accept(p->data_offer,
        p->device_enter_serial, "text/plain;charset=utf-8");
}

/* D&D データ受信 */

static void _poll_recv_handle(Client *client,int fd,int events)
{
    ClientEx *p = (ClientEx *)client;
    ssize_t ret = 0;

    if(events & POLLIN)
    {
        ret = read(fd, p->recv_buf + p->recv_cursize, p->recv_bufsize - p->recv_cursize - 1);

        p->recv_cursize += ret;

        printf("@ recv | read:%d byte, buf: %d/%d byte\n",
            (int)ret, p->recv_cursize, p->recv_bufsize);
    }

    //

    if(ret == 0)
    {
        //データ終了

        Client_poll_delete(client, fd);

        p->recv_buf[p->recv_cursize] = 0;

        printf("<data>\n");
        puts(p->recv_buf);
    }
    else
    {
        //データが続く場合、常に 1KB 以上の空き容量を作っておく

        if(p->recv_bufsize - p->recv_cursize < 1024)
        {
            p->recv_bufsize += 1024;
            p->recv_buf = (char *)realloc(p->recv_buf, p->recv_bufsize);
        }
    }
}

/* データ送信 */

static void _poll_write_handle(Client *p,int fd,int events)
{
    //書き込み出来る状態の場合、書き込み

    if((events & POLLOUT) && !(events & POLLERR))
    {
        printf("@ write\n");
        
        write(fd, "dnd_test", 8);
    }

    Client_poll_delete(p, fd);
}


//--------------------------------------
// wl_data_source (D&D 元データ管理)
//--------------------------------------


/* ドロップ先で、enter/move イベント時に MIME タイプが指定された時 */

static void _data_source_target(void *data,
    struct wl_data_source *source, const char *mime_type)
{
    //ドロップ先がデータを受け入れない場合、NULL が渡される

    printf("wl_data_source # target | mime_type:\"%s\"\n",
        (mime_type)? mime_type: "NULL");
}

/* データが要求された時 */

static void _data_source_send(void *data, struct wl_data_source *source,
    const char *mime_type, int32_t fd)
{
    printf("wl_data_source # send | mime_type:\"%s\"\n", mime_type);

#if 1
    Client_poll_add(CLIENT(data), fd, POLLOUT, _poll_write_handle);
#else
    //直接送信するテスト
    write(fd, "dnd_test", 8);
    close(fd);
#endif
}

/* キャンセル時 */

static void _data_source_cancelled(void *data, struct wl_data_source *source)
{
    printf("wl_data_source # cancelled | source:%p\n", source);

    _source_release((ClientEx *)data);
}

/* ドロップ先が drop イベントを受け取った時 */

void _data_source_dnd_drop_performed(void *data, struct wl_data_source *source)
{
    printf("wl_data_source # dnd_drop_performed\n");
}

/* D&D 処理が全て終了した */

void _data_source_dnd_finished(void *data, struct wl_data_source *source)
{
    printf("wl_data_source # dnd_finished\n");

    _source_release((ClientEx *)data);
}

/* サーバーが選択したアクションが通知される (D&D ソース向け) */

void _data_source_action(void *data, struct wl_data_source *source, uint32_t dnd_action)
{
    printf("wl_data_source # action | dnd_action:");
    _put_action(dnd_action);
    printf("\n");
}

static const struct wl_data_source_listener g_data_source_listener = {
    _data_source_target, _data_source_send, _data_source_cancelled,
    _data_source_dnd_drop_performed, _data_source_dnd_finished,
    _data_source_action
};


//--------------------------------------
// wl_data_offer (D&D 元の情報取得)
//--------------------------------------


/* D&D 元で提供されている MIME タイプの通知 */

static void _data_offer_offer(void *data, struct wl_data_offer *offer, const char *type)
{
    printf("wl_data_offer # offer | offer:%p, type:\"%s\"\n", offer, type);
}

/* D&D 元が対応可能なアクションの通知 */

static void _data_offer_source_actions(void *data,
    struct wl_data_offer *offer, uint32_t source_actions)
{
    printf("wl_data_offer # source_actions | offer:%p, source_actions:", offer);
    _put_action(source_actions);
    printf("\n");

    ((ClientEx *)data)->source_actions = source_actions;
}

/* サーバーが選択したアクションが通知される (ドロップ先向け) */

static void _data_offer_action(void *data, struct wl_data_offer *offer, uint32_t dnd_action)
{
    printf("wl_data_offer # action | offer:%p, dnd_action:", offer);
    _put_action(dnd_action);
    printf("\n");

    ((ClientEx *)data)->dnd_action = dnd_action;
}

static const struct wl_data_offer_listener g_data_offer_listener = {
    _data_offer_offer, _data_offer_source_actions, _data_offer_action
};


//--------------------------------------
// wl_data_device (D&D 受け入れる側)
//--------------------------------------


static void _device_data_offer(void *data,
    struct wl_data_device *data_device, struct wl_data_offer *offer)
{
    printf("wl_data_device # data_offer | offer:%p\n", offer);

    wl_data_offer_add_listener(offer, &g_data_offer_listener, data);
}

/* D&D ポインタがウィンドウに入った時 */

static void _device_enter(void *data, struct wl_data_device *data_device, uint32_t serial,
    struct wl_surface *surface, wl_fixed_t x, wl_fixed_t y, struct wl_data_offer *offer)
{
    ClientEx *p = (ClientEx *)data;

    printf("wl_data_device # enter | (%d, %d), offer:%p\n", x >> 8, y >> 8, offer);

    p->data_offer = offer;
    p->device_enter_serial = serial;

    _target_enter_leave(p, y);
}

/* D&D ポインタがウィンドウ外に出た or D&D が終了した時 */

static void _device_leave(void *data, struct wl_data_device *data_device)
{
    ClientEx *p = (ClientEx *)data;

    printf("wl_data_device # leave\n");

    if(p->data_offer)
    {
        wl_data_offer_destroy(p->data_offer);
        p->data_offer = NULL;
    }
}

/* ポインタがウィンドウ内で移動した時 */

static void _device_motion(void *data, struct wl_data_device *data_device,
    uint32_t time, wl_fixed_t x, wl_fixed_t y)
{
    printf("wl_data_device # motion | (%d, %d)\n", x >> 8, y >> 8);
    fflush(stdout);

    _target_enter_leave((ClientEx *)data, y);
}

/* ユーザーがボタンを離して、ドロップを受け入れる時 */

static void _device_drop(void *data, struct wl_data_device *data_device)
{
    ClientEx *p = (ClientEx *)data;
    int fd[2];

    printf("wl_data_device # drop\n");

    //データ受信開始

    if(pipe2(fd, O_CLOEXEC) != -1)
    {
        //データの送信を要求
    
        wl_data_offer_receive(p->data_offer, "text/plain;charset=utf-8", fd[1]);
        wl_display_flush(p->b.display);

        //

        close(fd[1]);

        Client_poll_add(CLIENT(p), fd[0], POLLIN, _poll_recv_handle);

        p->recv_cursize = 0;
    }

    //ASK の場合、ユーザーが選択したアクションをセット
    //(ここでは COPY で固定)

    if(p->dnd_action == WL_DATA_DEVICE_MANAGER_DND_ACTION_ASK
        && p->data_device_manager_ver >= WL_DATA_OFFER_SET_ACTIONS_SINCE_VERSION)
    {
        wl_data_offer_set_actions(p->data_offer,
            WL_DATA_DEVICE_MANAGER_DND_ACTION_COPY,
            WL_DATA_DEVICE_MANAGER_DND_ACTION_COPY);
    }

    //D&D 終了を通知する

    if(p->data_device_manager_ver >= WL_DATA_OFFER_FINISH_SINCE_VERSION)
        wl_data_offer_finish(p->data_offer);
}

static void _device_selection(void *data,
    struct wl_data_device *data_device, struct wl_data_offer *offer)
{
    printf("wl_data_device # selection | offer:%p\n", offer);
}

static const struct wl_data_device_listener g_data_device_listener = {
    _device_data_offer, _device_enter, _device_leave,
    _device_motion, _device_drop, _device_selection
};


//------------------------
//
//------------------------


/* D&D 開始 */

static void _start_drag(ClientEx *p,uint32_t serial)
{
    //ソース準備

    p->data_source = wl_data_device_manager_create_data_source(p->data_device_manager);

    wl_data_source_add_listener(p->data_source, &g_data_source_listener, p);

    wl_data_source_offer(p->data_source, "text/plain;charset=utf-8");
    wl_data_source_offer(p->data_source, "UTF8_STRING");

    //D&D 元が対応可能なアクションをセット

    if(p->data_device_manager_ver >= WL_DATA_SOURCE_SET_ACTIONS_SINCE_VERSION)
    {
        wl_data_source_set_actions(p->data_source,
            WL_DATA_DEVICE_MANAGER_DND_ACTION_COPY
                | WL_DATA_DEVICE_MANAGER_DND_ACTION_MOVE
                | WL_DATA_DEVICE_MANAGER_DND_ACTION_ASK);
    }

    //D&D 開始

    wl_data_device_start_drag(p->data_device,
        p->data_source, p->win->surface, NULL, serial);
}


//------------------------
// wl_pointer
//------------------------


static void _pointer_enter(void *data, struct wl_pointer *pointer,
    uint32_t serial, struct wl_surface *surface, wl_fixed_t x, wl_fixed_t y)
{
    printf("wl_pointer # enter\n");
}

static void _pointer_leave(void *data, struct wl_pointer *pointer,
    uint32_t serial, struct wl_surface *surface)
{
    printf("wl_pointer # leave\n");
}

static void _pointer_motion(void *data, struct wl_pointer *pointer,
    uint32_t time, wl_fixed_t x, wl_fixed_t y)
{
    printf("wl_pointer # motion | (%d, %d)\n", x >> 8, y >> 8);
}

static void _pointer_button(void *data, struct wl_pointer *pointer,
    uint32_t serial, uint32_t time, uint32_t button, uint32_t state)
{
    ClientEx *p = (ClientEx *)data;

    if(state == WL_POINTER_BUTTON_STATE_PRESSED)
    {
        if(button == BTN_MIDDLE)
            //中ボタンで終了
            p->b.finish_loop = 1;
        else if(button == BTN_LEFT)
            //D&D開始
            _start_drag(p, serial);
    }
}

static void _pointer_axis(void *data, struct wl_pointer *pointer,
    uint32_t time, uint32_t axis, wl_fixed_t value)
{
}

static void _pointer_frame(void *data, struct wl_pointer *pointer)
{
}

static void _pointer_axis_source(void *data, struct wl_pointer *pointer, uint32_t axis_source)
{
}

static void _pointer_axis_stop(void *data, struct wl_pointer *pointer, uint32_t time, uint32_t axis)
{
}

static void _pointer_axis_discrete(void *data, struct wl_pointer *pointer, uint32_t axis, int32_t discrete)
{
}

static const struct wl_pointer_listener g_pointer_listener = {
    _pointer_enter, _pointer_leave, _pointer_motion, _pointer_button,
    _pointer_axis, _pointer_frame, _pointer_axis_source, _pointer_axis_stop, _pointer_axis_discrete
};


//------------------------
// wl_registry
//------------------------


static void _registry_global(
    void *data,struct wl_registry *reg,uint32_t id,const char *name,uint32_t ver)
{
    ClientEx *p = (ClientEx *)data;

    if(strcmp(name, "wl_data_device_manager") == 0)
    {
        if(ver >= 3) ver = 3;

        p->data_device_manager = wl_registry_bind(reg, id, &wl_data_device_manager_interface, ver);
        p->data_device_manager_ver = ver;
    }
}


//-----------------------


/* ClientEx 破棄 */

static void _clientex_destroy(Client *cl)
{
    ClientEx *p = (ClientEx *)cl;

    if(p->recv_buf)
        free(p->recv_buf);

    if(p->data_offer)
        wl_data_offer_destroy(p->data_offer);

    _source_release(p);

    if(p->data_device_manager_ver >= WL_DATA_DEVICE_RELEASE_SINCE_VERSION)
        wl_data_device_release(p->data_device);
    else
        wl_data_device_destroy(p->data_device);

    wl_data_device_manager_destroy(p->data_device_manager);
}

/* メイン */

int main(void)
{
    ClientEx *p;
    Window *win;

    p = (ClientEx *)Client_new(sizeof(ClientEx));

    p->b.destroy = _clientex_destroy;
    p->b.init_flags = INIT_FLAGS_SEAT | INIT_FLAGS_POINTER;
    p->b.registry_global = _registry_global;
    p->b.pointer_listener = &g_pointer_listener;

    Client_init(CLIENT(p));

    //wl_data_device

    p->data_device = wl_data_device_manager_get_data_device(
        p->data_device_manager, p->b.seat);

    wl_data_device_add_listener(p->data_device, &g_data_device_listener, p);

    //受信バッファ

    p->recv_buf = (char *)malloc(1024);
    p->recv_bufsize = 1024;

    //ウィンドウ

    p->win = win = Window_create(CLIENT(p), 256, 300, NULL);

    ImageBuf_fillH(win->img, 0, 150, 0xffff0000);
    ImageBuf_fillH(win->img, 150, 150, 0xff0000ff);
    Window_update(win);

    //イベントループ (poll)

    Client_loop_poll(CLIENT(p));

    //解放

    Window_destroy(win);

    Client_destroy(CLIENT(p));

    return 0;
}

操作方法
ウィンドウ内を左ボタンでドラッグすると、D&D が開始されます。
UTF-8 テキストをドロップ可能なウィンドウ (テキストエディタなど) 上でボタンを離すと、テキストがドロップされます。

他のクライアントから、テキストをサンプルプログラムのウィンドウ内にドロップすることもできます。

上半分の赤い部分にドロップした場合、COPY アクションとして受け付けます。
下半分の青い部分にドロップした場合、ASK アクションとして受け付けます (最終的には COPY で処理)。

中ボタン押しで終了します。

※ カーソル形状の変更処理は行っていません。
D&Dを受け取る側
まずは、D&D を受け取る場合の処理を説明していきます。

D&D の操作は、クリップボードの時と同じで、wl_data_* を使用します。
これらの初期化処理などについては、前回の説明をご覧ください。

なお、wl_data_device_manager のバージョンが、「3 以上」か「2 以下」によって、行う処理が若干異なります。

ver 3 では、「アクション」の概念が追加されています。
現状ではほとんどのサーバーが ver 3 以降に対応していると思われます。
ドロップ時のアクションについて
まず先に、ドロップ時のアクションについて説明します。

D&D では、「COPY、MOVE、ASK」の3つのアクションを行うことができます。

COPYコピー。
ファイルや文字列などのコピー。D&D 元のデータはそのまま残ります。
MOVE移動。
ドロップ先にファイルなどを移動する。移動元のデータは削除されます。
ASKユーザーに尋ねる。
ドロップされた時に、ドロップ先でメニューなどを表示させて、ユーザーに COPY、MOVE、キャンセルなどのアクションを選択させる。

アクションは、D&D ソース側と受け取る側のそれぞれで、対応可能なものを指定する必要があります。
※ アクションの設定は、ver 3 以降の場合のみ行います。

ソース側は、そのデータが対応可能なアクションを、D&D 開始時に一回だけ設定します。

受け取る側に関しては、現在のカーソル位置によって、ドロップを受け取るクライアントやウィンドウが異なってくるため、カーソルが移動する度に、受け取る側が対応可能なアクションを設定します。

実際にドロップされた時は、双方のアクションを判定した上で、サーバーが、実際に実行するアクションを決定します。

アクションの値
アクションは、enum wl_data_device_manager_dnd_actions の列挙型で、フラグ値になっています。
対応するアクションを、複数指定します。

WL_DATA_DEVICE_MANAGER_DND_ACTION_NONE = 0
WL_DATA_DEVICE_MANAGER_DND_ACTION_COPY = 1
WL_DATA_DEVICE_MANAGER_DND_ACTION_MOVE = 2
WL_DATA_DEVICE_MANAGER_DND_ACTION_ASK  = 4

NONE は、ドロップを受け付けない場合に設定します。
wl_data_device のイベント
wl_data_device のイベントでは、D&D を受け取る側の処理を行うことになります。

void (*data_offer)(void *data, struct wl_data_device *data_device,
  struct wl_data_offer *id);

void (*enter)(void *data, struct wl_data_device *data_device,
  uint32_t serial, struct wl_surface *surface,
  wl_fixed_t x, wl_fixed_t y, struct wl_data_offer *id);

void (*leave)(void *data, struct wl_data_device *data_device);

void (*motion)(void *data, struct wl_data_device *data_device,
    uint32_t time, wl_fixed_t x, wl_fixed_t y);

void (*drop)(void *data, struct wl_data_device *data_device)

void (*selection)(void *data, struct wl_data_device *data_device,
  struct wl_data_offer *id);

selection は、クリップボードでしか使いません。

data_offer イベント
クリップボードでも使いましたが、D&D の場合は、wl_data_device:enter イベントの直前に来ます。
クリップボードの時と同じように、ここでは wl_data_offer のイベントハンドラを設定します。

enter イベント
D&D 中に、クライアントが管理するウィンドウ内にポインタが入った時に来ます。

D&D 元の情報を取得したりデータを受信するための wl_data_offer のポインタが送られてくるので、ここでそのポインタ値を保持しておきます。

leave イベント
D&D ポインタが、enter で入ったウィンドウの外に出た時や、D&D 処理が終了した時に来ます。

ここでは、enter 時に保持した wl_data_offer を破棄する必要があります。

motion イベント
enter で入ったウィンドウ内で、ポインタが移動した時に来ます。

ウィンドウ内に配置した各ウィジェットごとに D&D 判定を行いたい場合は、ここで処理する必要があります (enter 時も)。

drop イベント
ユーザーがボタンを離した時、クライアントがそのドロップを受け付ける場合に来ます。

クライアントがドロップを受け付けないようにしている場合、このイベントは発生せず、キャンセル扱いになります。

ここでは、データの受信を開始したり、D&D 処理の終了を通知したりします。
wl_data_offer のイベント
wl_data_device:data_offer イベントが来た時、wl_data_offer_add_listener()wl_data_offer のイベントハンドラを設定すると、D&D ソース側の情報を取得することができます。

void (*offer)(void *data, struct wl_data_offer *data_offer,
  const char *mime_type);

void (*source_actions)(void *data, struct wl_data_offer *data_offer,
  uint32_t source_actions);

void (*action)(void *data, struct wl_data_offer *data_offer,
  uint32_t dnd_action);

offer イベント
クリップボードの時も使いましたが、ソース側が送信可能な MIME タイプが一つずつ通知されます。

wl_data_device:data_offer イベントの直後に来ます。

この情報で、D&D のデータの種類を判別することができます。

source_actions イベント
* ver 3 から

ソース側が対応可能なアクションが通知されます。

wl_data_device:enter イベントの直前、または、wl_data_source_set_actions() で対応可能なアクションが変更された場合に送信されます。

action イベント
* ver 3 から

サーバーによって、ドロップ先が最終的に実行するべきアクション (1つ、または無し) が選択・変更された時に来ます。

サーバーは、「ソース側が対応可能なアクション」と「受け入れる側が対応出来るアクション」を照らし合わせて、ドロップ時に実行するべきアクションを一つ選択します。

なお、ドロップ時に Ctrl や Shift が押されていた場合に、アクションを動的に変更する機能がサーバー側で実装されている場合、サーバーが独自の判断でアクションを変更する可能性があります。

D&D を受け取る側では、このイベントで最後に受け取ったアクションを、ドロップ時の最終的なアクションとして扱う必要があります。
動作例
GNOME 上で、ファイルマネージャーからファイルを選択し、このウィンドウにドロップするまでのイベントを見てみます。

▼ ポインタがウィンドウ内に入った
wl_data_device # data_offer | offer:0x562413f3d0c0
wl_data_offer # offer | offer:0x562413f3d0c0, type:"text/uri-list"
wl_data_offer # offer | offer:0x562413f3d0c0, type:"application/x-fmlist-ptr"
wl_data_offer # offer | offer:0x562413f3d0c0, type:"UTF8_STRING"
wl_data_offer # offer | offer:0x562413f3d0c0, type:"COMPOUND_TEXT"
wl_data_offer # offer | offer:0x562413f3d0c0, type:"TEXT"
wl_data_offer # offer | offer:0x562413f3d0c0, type:"STRING"
wl_data_offer # offer | offer:0x562413f3d0c0, type:"text/plain;charset=utf-8"
wl_data_offer # offer | offer:0x562413f3d0c0, type:"text/plain"
wl_data_offer # offer | offer:0x562413f3d0c0, type:"text/uri-list"
wl_data_offer # offer | offer:0x562413f3d0c0, type:"application/x-fmlist-ptr"
wl_data_offer # offer | offer:0x562413f3d0c0, type:"UTF8_STRING"
wl_data_offer # offer | offer:0x562413f3d0c0, type:"COMPOUND_TEXT"
wl_data_offer # offer | offer:0x562413f3d0c0, type:"TEXT"
wl_data_offer # offer | offer:0x562413f3d0c0, type:"STRING"
wl_data_offer # offer | offer:0x562413f3d0c0, type:"text/plain;charset=utf-8"
wl_data_offer # offer | offer:0x562413f3d0c0, type:"text/plain"
wl_data_offer # offer | offer:0x562413f3d0c0, type:"DELETE"
wl_data_offer # action | offer:0x562413f3d0c0, dnd_action: NONE
wl_data_offer # source_actions | offer:0x562413f3d0c0, source_actions: COPY MOVE ASK
wl_data_device # enter | (255, 92), offer:0x562413f3d0c0
▼ ポインタが移動
wl_data_device # motion | (255, 92)
wl_data_offer # action | offer:0x562413f3d0c0, dnd_action: COPY
wl_data_device # motion | (253, 92)
▼ ボタンを離した
wl_data_device # drop
wl_data_device # leave
wl_pointer # enter
@ recv | read:13 byte, buf: 13/1024 byte
<data>
file:///etc

まず、D&D が開始され、ポインタがクライアントの管理するウィンドウ内に入った時、最初に wl_data_device:data_offer イベントが来ます。

その後、wl_data_offer:offer イベントで、D&D データの MIME タイプが送られてきます。
上記の場合、同じタイプが複数送られてきていますが、重複しているものは無視してください。

wl_data_offer:action イベントで、最終的に実行するべきアクションがサーバーによって送られてきます。
ウィンドウ内に入った段階では NONE となっています。

wl_data_offer:source_actions では、D&D 元が対応可能なアクションが通知されます。
ここでは、「COPY MOVE ASK」で、すべてに対応しています。

そして、wl_data_device:enter イベントが来ます。

クライアントのウィンドウ内でポインタが移動している間は、wl_data_device:motion イベントが来ます。

受け取る側が、ポインタ位置によって、対応可能なアクションを変更した場合、再び wl_data_offer:action が送られてきます。
ここでは、NONE から COPY に変更されました。

ユーザーがボタンを離した時、その位置へのドロップが可能 (最後の wl_data_offer:action で NONE 以外が送られている状態) なら、wl_data_device:drop が来ます。
ここで、データ受信の開始などの処理を行います。

最後に、D&D 処理が完全に終了した時は、wl_data_device:leave が来るので、終了処理を行います。
※ ソース側でデータの送信が行われるのは leave イベントの後になるので、注意してください。
イベントの処理
※ GNOME の場合、D&D 処理は、ver 3 以降の方法でしかドロップできないようです。

wl_data_device : enter イベント
wl_data_device:enter イベントが来たら、送られてきた wl_data_offer と、serial 値を記録しておきます。

また、enter 時のポインタ位置から、その位置でドロップを受け付けるかどうかを通知します。

ドロップを受け付けるか
ver 3 以降の場合、enter と motion イベントでドロップ先の位置が変わった時は、
wl_data_offer_set_actions()wl_data_offer_accept() を使って、その位置で対応可能なアクションと MIME タイプを通知する必要があります。

ポインタ移動時は毎回この処理をしないと、ドロップが行なえません。

※ ver 3 未満の場合は、wl_data_offer_accept() だけ使います。

## アクションを指定 (ver 3 以降)

void wl_data_offer_set_actions(struct wl_data_offer *wl_data_offer,
  uint32_t dnd_actions, uint32_t preferred_action);

## 受け入れ可能な MIME タイプを指定

void wl_data_offer_accept(struct wl_data_offer *wl_data_offer,
  uint32_t serial, const char *mime_type);

wl_data_offer_set_actions
dnd_actions は、受け入れる側が対応可能なアクションを、フラグで複数指定します。
preferred_action は、dnd_actions で指定した中で、優先するアクションを一つ指定します。

ボタンが離された時、WL_DATA_DEVICE_MANAGER_DND_ACTION_NONE (0) を指定していた場合や、ソース側で対応しているアクションと一つも一致しなかった場合は、D&D はキャンセル扱いになります。

wl_data_offer_accept
serial は、wl_data_device:enter イベント時に渡された値を指定します。
mime_type で、MIME タイプを指定します。

なお、ここで指定する MIME タイプは、目安みたいなものです。
実際の受信時は、ここで指定した MIME タイプでやりとりされるわけではありません。

ver 3 以降の場合は、mime_type が NULL ならドロップを受け付けない、それ以外ならドロップを受け付ける、という形になります。

GNOME の場合は、適当な文字列を指定しても問題なくドロップできました。
とりあえず、ソース側が対応している MIME タイプのうち、いずれか一つを指定しておけば良いでしょう。

実際にデータを受信する際には、そこで受信する MIME タイプを指定します。

wl_data_device : motion イベント
ポインタが移動した時、motion イベントが来ます。
enter の時と同じように、アクションと MIME タイプを通知します。

wl_data_offer : action イベント (ver 3 から)
wl_data_offer_set_actions() で、受け入れる側で対応可能なアクションが変更された時、サーバーが、ソース側で対応可能なアクションと照合して、最終的に実行するべきアクションが変更された場合に、このイベントが来ます。

ここで指定されたアクションは、受け入れる側が最終的に実行するべきアクションとなるので、実際にドロップされた時のために値を記録しておきます。

wl_data_device : drop イベント
ドロップを受け付けている状態で、ユーザーがボタンを離した時、このイベントが来ます。
ドロップを受け付けていない状態の場合は、来ません。

wl_data_device : leave イベント
D&D 処理が終了した時、もしくは、D&D ポインタがウィンドウ外に出た時にこのイベントが来ます。

ドロップを受け付けていない状態でボタンが離された時は、drop イベントは呼ばれずに、このイベントで D&D 処理が終了します。

ここでは、D&D の終了処理を行う必要があります。

wl_data_offer_destroy() で、enter 時に受け取った wl_data_offer を破棄します。
他にもデータを確保したりしていた場合は、解放します。
ドロップ時の処理
wl_data_device:drop イベントが来た時は、ソース側に対して D&D データの送信を要求し、ドロップ処理の終了を通知します。

## データの受信を要求

void wl_data_offer_receive(struct wl_data_offer *wl_data_offer,
  const char *mime_type, int32_t fd);

## ドロップ処理が終わったことを通知

void wl_data_offer_finish(struct wl_data_offer *wl_data_offer);

データの受信方法は、クリップボードの時と同じです。

pipe で fd を作成した後、wl_data_offer_receive() で受け取りたい MIME タイプを指定して、データの受信を要求します。

その後、イベントループ内で、poll などを使って入力を待ち、受信していきます。

ver 3 以降の場合は、drop イベント内で最後に wl_data_offer_finish() を実行して、受け取る側がドロップ処理を終了したことを通知します。

注意点
GNOME 上で実行した時、wl_data_offer_receive() の後、すぐに書き込み用 fd をクローズすると、データの受信が行えない (データが送信されず、すぐに閉じられる) 場合がありました。

wl_data_offer_receive() の後に wl_display_flush() を実行して、クライアントのデータをサーバーに送った後、クローズすると、問題なく動作しました。
おそらくタイミング的な問題だと思われます。
D&D のソース側
D&D のソース側 (D&D を開始する側) の処理を説明します。

ソース側では、wl_data_source を使って、ソースデータを管理します。
D&D の開始
ボタンが押された時に D&D を開始する場合、以下の処理を行います。

wl_data_source の準備
## wl_data_source の作成

struct wl_data_source *wl_data_device_manager_create_data_source(
  struct wl_data_device_manager *wl_data_device_manager);

## wl_data_source のハンドラ設定

int wl_data_source_add_listener(struct wl_data_source *wl_data_source,
  const struct wl_data_source_listener *listener, void *data);

## ソースが対応可能な MIME タイプを指定

void wl_data_source_offer(struct wl_data_source *wl_data_source,
  const char *mime_type);

ソースデータを管理するために wl_data_source が必要なので、作成します。
また、ハンドラも設定しておきます。

次に、ソースが対応可能な MIME タイプをセットします。
複数の形式に対応可能なら、複数回実行します。

※ クリップボードの時と同じように、Wayland 上で実行した X11 プログラムで文字列を扱う場合、"UTF8_STRING" などを指定しておかないと、文字列として認識されません。

対応可能なアクションの設定
void wl_data_source_set_actions(struct wl_data_source *wl_data_source,
  uint32_t dnd_actions);

ver 3 以降では、D&D を開始する前に、一度だけ、ソースデータが対応可能なアクションを指定します。

D&D 開始
void wl_data_device_start_drag(struct wl_data_device *wl_data_device,
  struct wl_data_source *source, struct wl_surface *origin,
  struct wl_surface *icon, uint32_t serial);

この関数で、D&D 処理を開始します。

source は、データソースです。NULL にすると、クライアント内部のみの D&D 操作となります。
origin は、ドラッグを開始するサーフェスを指定します。
serial には、wl_pointer:button イベント時に渡されたシリアル値を指定します。

icon は、D&D 中のカーソル形状として使うイメージの wl_surface を指定します。
NULL にすると、カーソル形状は現状のまま、一切変更されません。
なお、カーソル形状の変更は、D&D を開始したクライアントが行う必要があります。
wl_data_source のイベント
void (*target)(void *data, struct wl_data_source *wl_data_source,
  const char *mime_type);

void (*send)(void *data, struct wl_data_source *wl_data_source,
  const char *mime_type, int32_t fd);

void (*cancelled)(void *data, struct wl_data_source *wl_data_source);

void (*dnd_drop_performed)(void *data, struct wl_data_source *wl_data_source);

void (*dnd_finished)(void *data, struct wl_data_source *wl_data_source);

void (*action)(void *data, struct wl_data_source *wl_data_source,
  uint32_t dnd_action);

target イベント
ドロップを受け入れる側で、enter/move イベント時に wl_data_offer_accept() が実行された時に来ます。

受け入れる側で指定された MIME タイプがそのまま渡されます。
NULL の場合は、ドロップ先がドロップを受け付けないという意味です。

send イベント
ドロップを受け取る側から、データの送信を要求された時に来ます。

cancelled イベント
D&D がキャンセル扱いになった時に来ます。

このイベントが来た時点で、渡された wl_data_source は無効となるので、ソースデータを破棄する必要があります。

具体的には、以下のような時に来ます。

  • ドロップ先がドロップを受け付けなかった。
  • ドロップ時に、最終的なアクションが NONE になった。
  • ボタンを離した先が、有効なサーフェスではなかった。
  • サーバー側が、独自に設定したタイムアウトなどで、操作をキャンセルした。

dnd_drop_performed イベント
* ver 3 から

ユーザーがボタンを離して、ドロップ先がドロップを受け付けた時に来ます。
ドロップ先が wl_data_device:drop を受け取った後に呼ばれます。

ドロップ先でドロップが受け付けられなかった場合など、D&D 処理がキャンセルになった時は来ません。

※ここではまだ wl_data_source を破棄しないでください。

dnd_finished イベント
* ver 3 から

D&D 処理がすべて終了した時に来ます。
この時点で wl_data_source は使われなくなるので、ソースデータなどを解放します。

実行されたアクションが MOVE の場合は、ここで元データを削除できます。

action イベント
* ver 3 から

サーバーが選択したアクション (一つまたは無し) が通知されます。
ここで渡されたアクションが、最終的に実行するべきアクションとなります。

アクションが ASK だった場合、wl_data_source:dnd_finished イベントの直前にこのイベントが来て、最終的にドロップ先が選択したアクションが通知されます。

そのため、MOVE アクション時のデータ削除などは、wl_data_source:dnd_finished イベント時に行う必要があります。
イベント動作
▼ D&D 開始時
wl_pointer # leave
wl_data_source # target | mime_type:"NULL"
wl_data_device # data_offer | offer:0x5595df511c60
wl_data_offer # offer | offer:0x5595df511c60, type:"text/plain;charset=utf-8"
wl_data_offer # offer | offer:0x5595df511c60, type:"UTF8_STRING"
wl_data_source # action | dnd_action: NONE
wl_data_offer # action | offer:0x5595df511c60, dnd_action: NONE
wl_data_offer # source_actions | offer:0x5595df511c60, source_actions: COPY MOVE ASK
wl_data_device # enter | (204, 94), offer:0x5595df511c60
wl_data_source # action | dnd_action: COPY
wl_data_offer # action | offer:0x5595df511c60, dnd_action: COPY
wl_data_source # target | mime_type:"text/plain;charset=utf-8"
wl_data_device # motion | (206, 96)
▼ クライアントのウィンドウ外に出た
wl_data_device # leave
wl_data_source # target | mime_type:"NULL"
wl_data_source # action | dnd_action: NONE
▼ ドロップ可能なウィンドウ内に入った
wl_data_source # action | dnd_action: COPY
wl_data_source # target | mime_type:"UTF8_STRING"
▼ ドロップした
wl_data_source # dnd_drop_performed
wl_data_source # target | mime_type:"UTF8_STRING"
wl_data_source # send | mime_type:"UTF8_STRING"
wl_data_source # target | mime_type:"UTF8_STRING"
@ write
wl_data_source # dnd_finished

▼ キャンセル時
wl_data_source # target | mime_type:"NULL"
wl_data_source # cancelled | source:0x5595df511e30

※このウィンドウではドロップを受け付ける処理もしているので、wl_data_device のイベントも来ます。

D&D 開始時はまだドロップ対象が存在しないので、wl_data_source:target で NULL、wl_data_source:action で NONE が来ます。

ドロップ先で enter が来たら、アクションと MIME タイプが通知されてきます。

その後は、ドロップ先でポインタが移動するたびに、target イベントが送られてきます。
ドロップ先でアクションが変わると、action イベントが来ます。

ボタンが離され、ドロップが受け付けられると、wl_data_source:dnd_drop_performed が来ます。

ドロップ先でデータの受信が要求されたら、send イベントが来るので、データを送信する準備をします。

その後、wl_data_source:dnd_finished で、D&D 処理が終了します。

ボタンを離した時、ドロップ先でドロップが受け付けられない状態の場合は、D&D 処理がキャンセルになります。

キャンセルになった時は、dnd_drop_performed と dnd_finished は発生せず、cancelled だけが来て終了するので、そこで終了処理を行います。
データの送受信
データの送信については、wl_data_source:send イベント時に直接 write() でデータを送信することも可能ではありますが、送信・受信共に poll で処理した方が良いです。

今回は、drop/send イベント時に、poll で待つ fd を登録して、メインループ内で poll を行っています。

読み書きが出来る状態になったら、登録したハンドラを実行し、処理が終わったら fd を閉じて、poll のリストから削除しています。
ASK アクションについて
ASK アクションを使うと、ドロップ時に、ユーザーに対して実行するアクションを選択させることができます。

例えば、ファイルを D&D する場合、ASK アクションで、ドロップされた時に「コピー、移動、キャンセル」などのメニューを表示して、ユーザーに選択させることができます。

※ ソース側と受け取る側の両方が ASK アクションに対応していることが必須となります。

同じクライアント同士で D&D する場合は、コピーや移動以外のコマンドを独自に追加して処理することもできます。

(1) ソース側で ASK に対応する
まずは、ソース側が ASK アクションに対応する必要があります。

D&D 開始時に、wl_data_source_set_actions() で、ASK アクションを指定しておきます。

wl_data_source_set_actions(data_source,
    WL_DATA_DEVICE_MANAGER_DND_ACTION_COPY
        | WL_DATA_DEVICE_MANAGER_DND_ACTION_MOVE
        | WL_DATA_DEVICE_MANAGER_DND_ACTION_ASK);

(2) 受け取る側が ASK に対応する
受け取る側で ASK を受け付ける場合は、enter/motion イベント時に、wl_data_offer_set_actions() で、ASK に対応していることを通知します。

ドロップ時に常に ASK を実行したい場合は、以下のようにします。

wl_data_offer_set_actions(data_offer,
    WL_DATA_DEVICE_MANAGER_DND_ACTION_ASK,
    WL_DATA_DEVICE_MANAGER_DND_ACTION_ASK);

(3) 受け取る側で drop イベントが来た時
受け取る側の wl_data_offer:action イベントで、最後に来た値が ASK だった場合、ドロップ時は ASK アクションの処理を行う必要があります。

drop イベントが来たら、メニューなどを表示して、ユーザーにアクションを選択させます。

その後、wl_data_offer_set_actions() を使って、選択したアクションをソース側に通知します。

そして、最後に wl_data_offer_finish() を呼び出して、D&D 処理の終了を通知します。

wl_data_offer_set_actions(data_offer,
    WL_DATA_DEVICE_MANAGER_DND_ACTION_COPY,
    WL_DATA_DEVICE_MANAGER_DND_ACTION_COPY);

wl_data_offer_finish(data_offer);

ユーザーがキャンセルを選んだ場合は、上記の処理を行わずに、wl_data_offer_destroy()wl_data_offer を破棄します。
すると、ソース側では wl_data_source:cancelled が発生します。

(4) ソース側の最終処理
wl_data_source:dnd_drop_performed イベントの後、受け取る側で最終的なアクションが決定されたら、
wl_data_source:actionwl_data_source:dnd_finished イベントが来ます。

最終的なアクションが MOVE だった場合は、dnd_finished イベント時に、移動元データの削除を行います。