Wayland: D&D

ドラッグ&ドロップ
今回は、ウィンドウ間でのドラッグ&ドロップの処理を行います。

$ cc -o test 16-dnd.c client.c imagebuf.c xdg-shell-protocol.c -lwayland-client -lrt

ウィンドウ内を左ボタンでドラッグすると、D&D が開始されます。
テキストエディタなどの、UTF-8 テキストをドロップできるウィンドウ上でボタンを離すと、"dnd_test" のテキストがドロップされます。

他のクライアントから、このウィンドウ内にテキストをドロップすることもできます。
("text/uri-list" 以外は "text/plain;charset=utf-8" として取得。ファイルまたはテキストの D&D に対応しているが、すべて読み込めるとは限らない)

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

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

<16-dnd.c>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>

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

#include <linux/input-event-codes.h>

#include <wayland-client.h>
#include "xdg-shell-client-protocol.h"

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


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

typedef struct
{
    client b;
    Toplevel *win;

    int recv_bufsize,
        recv_cursize,
        is_uri_list;
    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;
}client_ex;

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


/* wl_data_source 解放 */

static void _source_release(client_ex *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(client_ex *p,int y)
{
    int type;

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

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

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

            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,
        (p->is_uri_list)? "text/uri-list": "text/plain;charset=utf-8");
}

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

static void _recv_data(client_ex *p,int fd)
{
    ssize_t ret;

    p->recv_cursize = 0;

    while(1)
    {
        ret = read(fd, p->recv_buf + p->recv_cursize, p->recv_bufsize - p->recv_cursize - 1);

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

        if(ret <= 0)
        {
            //終了
            
            close(fd);

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

            printf("<data>\n");
            puts(p->recv_buf);

            break;
        }
        else
        {
            p->recv_cursize += ret;

            //バッファが一杯
            if(p->recv_bufsize == p->recv_cursize + 1)
            {
                p->recv_bufsize += 2048;
                p->recv_buf = (char *)realloc(p->recv_buf, p->recv_bufsize);
            }
        }
    }
}


//===================================
// 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);
    printf("@ write\n");
    
    write(fd, "dnd_test", 8);
    close(fd);
}

/* キャンセル時 */

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

    _source_release((client_ex *)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((client_ex *)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)
{
    client_ex *p = (client_ex *)data;

    printf("wl_data_offer # offer | offer:%p, type:\"%s\"\n", offer, type);

    if(strcmp(type, "text/uri-list") == 0)
        p->is_uri_list |= 1;
}

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

    ((client_ex *)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");

    ((client_ex *)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);

    ((client_ex *)data)->is_uri_list = 0;
}

/* 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)
{
    client_ex *p = (client_ex *)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)
{
    client_ex *p = (client_ex *)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((client_ex *)data, y);
}

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

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

    printf("wl_data_device # drop\n");

    //データ受信

    if(pipe2(fd, O_CLOEXEC) != -1)
    {
        wl_data_offer_receive(p->data_offer,
            (p->is_uri_list)? "text/uri-list": "text/plain;charset=utf-8", fd[1]);

        wl_display_flush(p->b.display);

        close(fd[1]);

        _recv_data(p, fd[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);
    }

    //終了

    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(client_ex *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->sf.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");

    client_set_cursor(CLIENT(data), serial);
}

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)
{
    client_ex *p = (client_ex *)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)
{
}

//ver 5
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
};


//===========================


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

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

/* client_ex 破棄 */

static void _clientex_destroy(client *cl)
{
    client_ex *p = (client_ex *)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)
{
    client_ex *p;
    Toplevel *win;

    p = (client_ex *)client_new(sizeof(client_ex));

    p->b.destroy = _clientex_destroy;
    p->b.init_flags = CLIENT_INIT_FLAGS_SEAT | CLIENT_INIT_FLAGS_POINTER
        | CLIENT_INIT_FLAGS_CREATE_CURSOR;
    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 = toplevel_create(CLIENT(p), 256, 300, NULL);

    imagebuf_fill_horz(win->sf.img, 0, 150, 0xffff0000);
    imagebuf_fill_horz(win->sf.img, 150, 150, 0xff0000ff);

    //イベントループ

    client_loop_simple(CLIENT(p));

    //解放

    toplevel_destroy(win);

    client_destroy(CLIENT(p));

    return 0;
}
D&D を受け取る側
まずは、D&D を受け取るためのドロップ処理を説明していきます。

D&D の操作は、クリップボードと同様に、wl_data_device_manager を使用します。
初期化処理は、前回と同じように行います。

なお、wl_data_device_manager のバージョンが「3 以上」か「3 未満」かどうかによって、処理が若干異なるので、注意してください。
ver 3 では、「アクション」の概念が追加されています。

現状のサーバーは、基本的に ver 3 以上に対応していると思うので、サーバー側のバージョンが ver 3 未満なら対応しないといった方法もあります。

GNOME の場合、D&D 処理は、ver 3 以降の方法でしかドロップできないようです。
ドロップ時のアクションについて
まず、ドロップ時のアクションについて説明しておきます。

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

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

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

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

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

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

アクションの値
アクションは、enum wl_data_device_manager_dnd_actions の列挙型で、フラグ値になっています。

enum wl_data_device_manager_dnd_action {
    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 のイベント
D&D を受け取る側の処理は、wl_data_device の各イベントで行います。

struct wl_data_device_listener {
    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 イベント
クリップボードの時にも使いましたが、サーバー側で wl_data_offer が作成された時に来るので、同じように wl_data_offer のハンドラ設定を行います。

D&D の場合は、wl_data_device:enter イベントの直前に来ます。

enter イベント
D&D 操作中、このクライアントが管理するウィンドウ内に、ポインタが入った時に来ます。
データを操作するための wl_data_offer のポインタが送られてくるので、それを保持しておきます。

leave イベント
D&D 操作中に、ポインタがウィンドウの外に出た時や、D&D 処理が終了した時に来ます。
ここでは、enter イベント時に保持した wl_data_offer を破棄する必要があります。

motion イベント
D&D 操作中に、ウィンドウ内でポインタが移動した時に来ます。
ウィンドウ内に配置した各ウィジェットごとに D&D 判定を行いたい時などに処理します。

drop イベント
クライアントのウィンドウ内でユーザーがボタンを離した時、そのドロップを受け付ける場合に来ます。
クライアントがドロップを受け付けないように設定している場合、このイベントは発生せず、キャンセル扱いになります。

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

struct wl_data_offer_listener {
    void (*offer)(void *data, struct wl_data_offer *data_offer, const char *mime_type);
    //ver 3
    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 イベントの直前、または、ソース側が D&D 中に wl_data_source_set_actions() を実行して、アクションが変更された時に送信されます。

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

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

なお、D&D 中に、Ctrl や Shift キーが押された時、アクションを動的に変更する機能がサーバー側で実装されている場合、サーバー独自の判断で、アクションを変更する可能性があります。
(GNOME の場合、ドロップ側が COPY と MOVE で受け付け可能な場合で、D&D 中に Shift キーを押すと、MOVE が優先されます)

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

## ポインタがウィンドウ内に入った (上の赤の領域)

wl_data_device # data_offer | offer:0x644656ae7eb0
wl_data_offer # offer | offer:0x644656ae7eb0, type:"application/x-qabstractitemmodeldatalist"
wl_data_offer # offer | offer:0x644656ae7eb0, type:"text/uri-list"
wl_data_offer # offer | offer:0x644656ae7eb0, type:"libfm/files"
wl_data_offer # action | offer:0x644656ae7eb0, dnd_action: NONE
wl_data_offer # source_actions | offer:0x644656ae7eb0, source_actions: COPY MOVE
wl_data_device # enter | (244, 103), offer:0x644656ae7eb0
wl_data_offer # action | offer:0x644656ae7eb0, dnd_action: COPY

## ポインタが移動 (下の青の領域に入った)

wl_data_device # motion | (99, 151)
wl_data_offer # action | offer:0x644656ae7eb0, dnd_action: NONE
wl_data_device # motion | (100, 154)

## ボタンを離した (上の赤の領域内で)

wl_data_device # motion | (157, 106)
wl_data_device # drop
wl_data_device # leave
wl_pointer # enter
@ recv | read:23 byte, buf: 23/1024 byte
<data>
file:///usr/local/bin

## 青の領域でボタンを離した (キャンセル)

wl_data_offer # action | offer:0x644656ae7eb0, dnd_action: NONE
wl_data_offer # source_actions | offer:0x644656ae7eb0, source_actions: COPY MOVE
wl_data_device # enter | (254, 249), offer:0x644656ae7eb0
...
wl_data_device # leave
wl_pointer # enter

※イベントが来る順番は、D&D 元のクライアントによって、前後する可能性があります。

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

  • wl_data_offer のハンドラを設定すると、wl_data_offer:offer イベントで、D&D データの MIME タイプが送られてきます。
    ファイルパスは、基本的に "text/uri-list" で取得できます。

  • wl_data_offer:action イベントで、現時点において最終的に実行するべきアクションが、サーバーによって送られてきます。
    上記の場合は、イベントが enter の前に来ているので、NONE になっています。

  • wl_data_offer:source_actions イベントで、ソース側が対応可能なアクションが通知されます。
    ここでは、「COPY MOVE」なので、コピーと移動に対応しています。
    D&D の開始時点では、そのデータを移動するかコピーするかは、まだ決まっていません。

  • wl_data_device:enter イベントが来た時点で、このクライアントがドロップ処理の対応を開始します。
    上半分の赤の領域に入っているので、ドロップ側は COPY MOVE に対応可能であり、COPY を優先するように設定しています。

  • ドロップ側のアクションが変更されたので、再び wl_data_offer:action イベントが来ています。
    COPY を優先するようにしているので、最終的なアクションは COPY になります。
    なお、GNOME では、D&D 中に Shift キーを押すと、サーバーによってアクションが MOVE に変更されます。

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

  • ドロップ側が、ポインタ位置によって、対応可能なアクションを変更した場合、その設定から最終的なアクションを再び判断して、wl_data_offer:action が送られてきます。
    上記の場合は、ポインタが下半分の青の領域に移動した時、NONE に変更されています。
    青の領域ではドロップ側のアクションを ASK に設定していますが、ソース側は ASK アクションが非対応になっているので、サーバーはこの位置でのドロップを受け付けないように決定しています。

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

  • 最後に、キャンセル時も含めて、D&D 処理が完全に終了した時は、wl_data_device:leave が来るので、ここで D&D 処理を終了させます。

  • 最終的なアクションが NONE の状態で、下半分の青の領域にドロップした場合、D&D はキャンセル扱いになります。
    drop イベントは来ず、wl_data_device:leave が来ます。
ドロップ側のイベントの処理
wl_data_device : enter イベント
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);

wl_data_device:enter イベントが来た時、クライアントは D&D のドロップ処理の対応を開始します。

送られてきた wl_data_offer のポインタと serial 値を記録しておきます。

また、現在のポインタ位置によって、その位置でドロップを受け付けるかどうかを設定する必要があります。
wl_data_device : motion イベント
void (*motion)(void *data, struct wl_data_device *data_device,
    uint32_t time, wl_fixed_t x, wl_fixed_t y);

D&D 操作中にポインタが移動した時、motion イベントが来ます。
ドロップを受け付けるか
wl_data_device の enter と motion イベントで、現在のポインタ位置が通知されてきた時、ver 3 以降の場合は、wl_data_offer_set_actions()wl_data_offer_accept() を使って、そのポインタ位置で対応可能なアクションと MIME タイプを通知する必要があります。

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

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

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

enum wl_data_device_manager_dnd_action {
    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
};

ドロップ側が、現在のポインタ位置で対応可能なアクションを指定します。

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

ユーザーがボタンを離した時に、最終的なアクションが NONE だった場合、ドロップはキャンセル扱いになります。
ドロップ側が NONE に指定した場合や、指定したアクションがソース側で対応していなかった場合、NONE になります。

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

ドロップ側が受け入れ可能な、データの MIME タイプを指定します。

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

なお、ここで指定する MIME タイプは、あくまで目安のようなものです。
実際の受信時は、ここで指定した MIME タイプでデータが送られてくるわけではありません。

ver 3 以降の場合、mime_type が NULL であれば、ドロップを受け付けず、ドロップ時はキャンセル扱いになります。
ドロップを受け付ける場合は、ソース側が対応している MIME タイプのいずれかを指定します。

実際にデータを受信する時に、読み込む MIME タイプを指定することができます。
wl_data_offer : action イベント (ver 3)
ここで指定されたアクションは、ドロップ側が最終的に実行するべきアクションとなるので、実際にドロップされた時のために、値を記録しておきます。
wl_data_device : leave イベント
D&D 処理が終了した時 (キャンセル時も含む)、もしくは、D&D 中にポインタがウィンドウ外に出た時に、このイベントが来ます。

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

ここでは、ドロップに対応する処理の終了を行う必要があります。

enter イベント時に取得した wl_data_offer は、ここで wl_data_offer_destroy() を使って破棄する必要があります。
他にもドロップ処理用にデータを確保していた場合は、ここで解放します。
wl_data_device : drop イベント
ドロップが受け付け可能な状態で、ボタンが離された時、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 タイプを指定して、データの受信を要求します。

ver 3 以降の場合は、データの受信を終了した後、wl_data_offer_finish() を実行して、処理の終了を通知します。
これにより、ソース側に wl_data_source:dnd_finished イベントが送られます。

ASK アクション
最終的なアクションが ASK だった場合、ドロップ側は、メニューを表示するなどして、ユーザーにどのアクションを実行するかを選択させます。
アクションが決まったら、データを受信した後、wl_data_offer_set_actions() で ASK 以外のアクションを通知し、wl_data_offer_finish() で終了します。

キャンセルを行う場合は、wl_data_offer_finish() などは行わず、直ちに wl_data_offer_destroy() を実行する必要があります。

受信について
drop イベント内で wl_data_offer_finish() を実行した後、データの読み込みをイベントループ内で行う場合、ソース側のクライアントによっては、読み込みが失敗する場合があるので、注意してください。

wl_data_offer_finish() は、データを正常に受信した後に実行します。
この後に発生するイベントにより、ソース側で D&D データの破棄が行われる場合があるので、wl_data_offer_finish() の実行後はデータの読み込みが行えない場合があります。

※drop イベント後は、すぐに leave イベントが来るので、ここで wl_data_offer_destroy() を実行する必要があります。
そのため、イベントループ内でデータを読み込んだ後に wl_data_offer_finish() で終了させるといったことはできません。

データの受信は、基本的に、drop イベント内で wl_data_offer_finish() を実行する前に行った方が良いでしょう。

ソース側のクライアントによっては、wl_data_offer_finish() 実行後も読み込みが行える場合があります。
D&D のソース側
D&D のソース側 (ドラッグを開始する側) の処理を説明します。

ソース側では、wl_data_source を使って、D&D のソースデータを管理します。
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);

struct wl_data_source_listener {
    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);
    //ver 3
    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);
};

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

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

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

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

※クリップボードの時と同じように、ドロップ先が X11 プログラムの場合は、"UTF8_STRING" などの X11 で使用される名前を追加しておかないと、テキストとしてドロップできません。

対応可能なアクションの設定
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ドラッグを開始するウィンドウのサーフェス
serialwl_pointer:button イベント時に渡されたシリアル値
iconD&D 中のカーソル形状の wl_surface。
NULL の場合、カーソル形状は現状のままで変更されません。
D&D 中のカーソル形状の変更は、D&D を開始したクライアントが行う必要があります。
wl_data_source のイベント
target イベント
void (*target)(void *data, struct wl_data_source *wl_data_source, const char *mime_type);

ドロップを受け入れる側で wl_data_offer_accept() が実行され、受け入れ可能な MIME タイプが指定された時に来ます。

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

実際にデータを送信する際は、send イベントで MIME タイプが指定されるので、このイベントはデータの送信には関係しません。
ドロップ先にドロップが可能であるかを判断して、カーソル形状を変更するなどの目的で使います。

send イベント
void (*send)(void *data, struct wl_data_source *wl_data_source, const char *mime_type, int32_t fd);

ドロップを受け取る側から、データの送信が要求された時に来ます。
ドロップ先によっては、プレビューなどの目的で、D&D 中にデータが要求される場合があります。

cancelled イベント
void (*cancelled)(void *data, struct wl_data_source *wl_data_source);

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

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

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

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

dnd_drop_performed イベント (ver 3)
void (*dnd_drop_performed)(void *data, struct wl_data_source *wl_data_source);

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

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

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

操作としてのドロップ処理は終了したが、ソースデータはまだ有効な状態です。

dnd_finished イベント (ver 3)
void (*dnd_finished)(void *data, struct wl_data_source *wl_data_source);

ドロップが受け入れられ、ドロップ先がデータを受信して、ドロップ処理が終了した時。
キャンセル時には来ません。

この時点で wl_data_source は使われなくなるので、ソースデータなどを解放します。

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

action イベント (ver 3)
void (*action)(void *data, struct wl_data_source *wl_data_source, uint32_t dnd_action);

サーバーが選択した最終的なアクション (一つまたは無し) が通知されます。

アクションが 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:0x55e745f90ff0
wl_data_offer # offer | offer:0x55e745f90ff0, type:"text/plain;charset=utf-8"
wl_data_offer # offer | offer:0x55e745f90ff0, type:"UTF8_STRING"
wl_data_source # action | dnd_action: NONE
wl_data_offer # action | offer:0x55e745f90ff0, dnd_action: NONE
wl_data_offer # source_actions | offer:0x55e745f90ff0, source_actions: COPY MOVE ASK
wl_data_device # enter | (169, 64), offer:0x55e745f90ff0
wl_data_source # action | dnd_action: COPY
wl_data_offer # action | offer:0x55e745f90ff0, dnd_action: COPY
wl_data_source # target | mime_type:"text/plain;charset=utf-8"
wl_data_device # motion | (171, 64)

## クライアントのウィンドウ外に出た
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:"text/plain;charset=utf-8"

## ドロップした
wl_data_source # dnd_drop_performed
wl_data_device # data_offer | offer:0x55e745f91950
wl_data_offer # offer | offer:0x55e745f91950, type:"text/plain;charset=utf-8"
wl_data_device # selection | offer:0x55e745f91950
wl_data_source # send | mime_type:"text/plain;charset=utf-8"
@ write
wl_data_source # dnd_finished

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

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

  • D&D 開始直後は、MIME タイプは NULL、アクションは NONE になります。
  • ドロップ先で wl_data_device:enter が来たら、受け付け可能なアクションと MIME タイプが通知されてきます。
  • その後は、ドロップ先でポインタが移動するたびに、target イベントが送られてきます。
    ドロップ先でアクションが変更されたことにより、最終的なアクションが変化した場合、action イベントが来ます。
  • ボタンが離されて、ドロップが受け付けられた時、wl_data_source:dnd_drop_performed が来ます。
  • ドロップ先でデータの受信が要求されたら、wl_data_source:send イベントが来るので、データを送信します。
  • ドロップ先で wl_data_offer_finish() が実行されると、wl_data_source:dnd_finished が来るので、そこで D&D 処理を終了します。
  • ボタンを離した時、ドロップ先でドロップが受け付けられない状態の場合は、D&D 処理がキャンセルになります。
    キャンセルになった時は、dnd_drop_performed と dnd_finished は発生せず、cancelled だけが来るので、そこで終了処理を行います。