クリップボード
今回は、クリップボードのコピーと貼り付けを行います (テキストのみ)。
ウィンドウがアクティブな状態で、キーボードのキーを押して、操作を行います。
<15-clipboard.c>
$ cc -o test 15-clipboard.c client.c imagebuf.c xdg-shell-protocol.c -lwayland-client -lrt
ウィンドウがアクティブな状態で、キーボードのキーを押して、操作を行います。
C | 適当な文字列で、UTF-8 文字列をコピー |
---|---|
V | テキストの貼り付け |
R | クリップボードデータの所有権を放棄 (自身がコピーしているクリップボードデータは、もうこのクライアントが保持しない) |
ESC | プログラム終了 |
<15-clipboard.c>
#include <stdio.h> #include <string.h> #include <stdlib.h> #define __USE_GNU #include <fcntl.h> #include <unistd.h> #include <errno.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; struct pollfd poll[2]; 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; char *copy_text, //コピー文字列 *recv_buf; //受信バッファ int text_size, //コピー文字列のサイズ recv_cursize, //受信バッファ、現在の読み込みサイズ recv_bufsize; //受信バッファ確保サイズ }client_ex; //------------- /* ソースデータ解放 */ static void _source_release(client_ex *p) { if(p->data_source) { wl_data_source_destroy(p->data_source); p->data_source = NULL; } if(p->copy_text) { free(p->copy_text); p->copy_text = NULL; } } //======================== // wl_data_source //======================== static void _data_source_target(void *data, struct wl_data_source *source, const char *mime_type) { } /* データが要求された時 */ static void _data_source_send(void *data, struct wl_data_source *source, const char *mime_type, int32_t fd) { client_ex *p = (client_ex *)data; if(!mime_type) close(fd); else { printf("wl_data_source # send | mime_type:\"%s\", fd:%d\n", mime_type, fd); //fd に書き込んで、送る write(fd, p->copy_text, p->text_size); 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); } void _data_source_dnd_drop_performed(void *data, struct wl_data_source *source) { } void _data_source_dnd_finished(void *data, struct wl_data_source *source) { } void _data_source_action(void *data, struct wl_data_source *source, uint32_t dnd_action) { } 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 //======================== /* 提供されている MIME タイプが通知される */ static void _data_offer_offer(void *data, struct wl_data_offer *offer, const char *type) { printf("wl_data_offer # offer | type:\"%s\"\n", type); } static void _data_offer_source_actions(void *data, struct wl_data_offer *offer, uint32_t source_actions) { } static void _data_offer_action(void *data, struct wl_data_offer *offer, uint32_t 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 //======================== 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); } 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) { } static void _device_leave(void *data, struct wl_data_device *data_device) { } static void _device_motion(void *data, struct wl_data_device *data_device, uint32_t time, wl_fixed_t x, wl_fixed_t y) { } static void _device_drop(void *data, struct wl_data_device *data_device) { } /* クリップボード用の wl_data_offer が送られてくる */ static void _device_selection(void *data, struct wl_data_device *data_device, struct wl_data_offer *offer) { client_ex *p = (client_ex *)data; printf("wl_data_device # selection | offer:%p\n", offer); //前回の data_offer は破棄する if(p->data_offer) wl_data_offer_destroy(p->data_offer); //新しい offer を保持 //(クリップボードにデータがない場合は NULL) p->data_offer = 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 }; //======================== // コピー/貼り付け処理 //======================== /* コピー */ static void _copy_text(client_ex *p,const char *text,uint32_t serial) { printf("-- copy text: \"%s\"\n", text); //前のソースを解放 _source_release(p); //内部データに文字列をコピー p->text_size = strlen(text); p->copy_text = (char *)malloc(p->text_size); memcpy(p->copy_text, text, p->text_size); //このクライアントがデータを持っていることを通知する p->data_source = wl_data_device_manager_create_data_source(p->data_device_manager); wl_data_source_offer(p->data_source, "text/plain;charset=utf-8"); wl_data_source_offer(p->data_source, "UTF8_STRING"); wl_data_source_add_listener(p->data_source, &g_data_source_listener, p); wl_data_device_set_selection(p->data_device, p->data_source, serial); } /* 貼り付け */ static void _paste(client_ex *p) { int fd[2]; if(!p->data_offer) { printf("-- paste: no data\n"); return; } //現在、データを受信中 if(p->poll[1].fd != -1) return; //データの受信を要求 if(pipe2(fd, O_CLOEXEC) == -1) return; wl_data_offer_receive(p->data_offer, "text/plain;charset=utf-8", fd[1]); close(fd[1]); p->poll[1].fd = fd[0]; p->recv_cursize = 0; } /* 貼り付け時のデータ受信 */ static void _data_recv_handle(client_ex *p,int events) { int fd,ret = 0; fd = p->poll[1].fd; printf("@ poll | "); if(events & POLLIN) printf("POLLIN "); if(events & POLLHUP) printf("POLLHUP "); printf("\n"); //受信するデータがある場合 if(events & POLLIN) { //最後に NULL 文字を追加するので、読み込みサイズは 1 byte 減らす 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) { //受信データがなくなったら終了 close(fd); p->poll[1].fd = -1; //NULL 文字を追加 p->recv_buf[p->recv_cursize] = 0; printf("<paste text>\n"); puts(p->recv_buf); } else { //データが続く場合、常に 1 KB 以上の空き容量を作っておく 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 _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; } } /* キー押し */ static void _keyboard_press(client *cl,uint32_t key,uint32_t serial) { client_ex *p = (client_ex *)cl; char m[64]; switch(key) { //コピー case KEY_C: snprintf(m, 64, "copytext-%u", serial); _copy_text(p, m, serial); break; //貼り付け case KEY_V: _paste(p); break; //所有権放棄 case KEY_R: if(p->data_source) { wl_data_device_set_selection(p->data_device, NULL, serial); _source_release(p); } break; //ESC キーで終了 case KEY_ESC: p->b.finish_loop = 1; break; } } /* イベントループ */ static void _event_loop(client_ex *p) { //display イベント用 p->poll[0].fd = wl_display_get_fd(p->b.display); p->poll[0].events = POLLIN; //貼り付けデータ取得用 // 初期状態ではデータがないため、fd = -1。 // fd が負の値の場合、poll() 時に無視される。 p->poll[1].fd = -1; p->poll[1].events = POLLIN; //ループ while(!p->b.finish_loop) { wl_display_flush(p->b.display); if(poll(p->poll, 2, -1) < 0) break; //イベントを処理 if(p->poll[0].revents & POLLIN) wl_display_dispatch(p->b.display); //データ受信 if(p->poll[1].revents) _data_recv_handle(p, p->poll[1].revents); } } /* client_ex 破棄 */ static void _clientex_destroy(client *cl) { client_ex *p = (client_ex *)cl; free(p->recv_buf); //wl_data_offer if(p->data_offer) wl_data_offer_destroy(p->data_offer); //ソースデータ _source_release(p); //wl_data_device 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 wl_data_device_manager_destroy(p->data_device_manager); } /* main */ 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_KEYBOARD; p->b.registry_global = _registry_global; p->b.func_keyboard_key_press = _keyboard_press; 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; //ウィンドウ win = toplevel_create(CLIENT(p), 256, 256, NULL); imagebuf_fill(win->sf.img, 0xffff0000); // _event_loop(p); //解放 toplevel_destroy(win); client_destroy(CLIENT(p)); return 0; }
wl_data_device_manager
クリップボードや D&D の処理を行う場合は、wl_data_device_manager をバインドします。
2025年2月時点での最大バージョンは「3」のため、今回は ver 3 までを使います。
wl_data_device_manager の子として、wl_data_device, wl_data_source が作成できます。
2025年2月時点での最大バージョンは「3」のため、今回は ver 3 までを使います。
//破棄 void wl_data_device_manager_destroy(struct wl_data_device_manager *wl_data_device_manager);
wl_data_device_manager の子として、wl_data_device, wl_data_source が作成できます。
wl_data_device
wl_data_device_manager がバインドできたら、wl_data_device を作成します。
wl_data_device は、クリップボードの管理や、D&D の操作を行うためのインターフェイスです。
そのため、この2つが両方ともバインドされている状態で、wl_data_device_manager_get_data_device() を実行する必要があります。
wl_data_device_manager のバインドと同時に wl_data_device を作成したい場合、すでに wl_seat がバインドされている必要がありますが、wl_registry:global イベントでは、wl_data_device_manager と wl_seat が来る順番は決まっていません。
そのため、wl_seat より先に wl_data_device_manager が来た場合は、wl_data_device_manager のバインドと同時に wl_data_device を作成することはできません。
よって、wl_data_device を作成するタイミングは、以下の2通りになります。
今回の場合は、前者の方法で作成しています。
Wayland では、クリップボードのことを「セレクション (selection)」と呼びます。
selection イベントでは、サーバーから任意のタイミングで、wl_data_offer のポインタが送られてきます。
これは、現在セレクションにセットされているデータを管理するためのインターフェイスです。
これを使用することで、クリップボードにあるデータの種類を取得したり、データを要求したりすることができます。
クリップボードからデータを読み込みたいクライアントは、selection イベントで送られてきた wl_data_offer を保持しておき、次の selection イベントが来た時に、前回の古い wl_data_offer を破棄する必要があります。
具体的には、以下のような時に来ます。
つまり、現在アクティブなクライアントに対しては、クリップボードのデータが変更されたタイミングで、常に通知されてくるということです。
クリップボードのソースデータを保持しているクライアントが、セレクションの所有権を放棄した時 (主にプログラムの終了時) に来ます。
wl_data_offer のポインタは、次の selection イベントで、新しい wl_data_offer が送られてくるまで有効となります。
前回の selection イベントで取得した wl_data_offer は、wl_data_offer_destroy() で破棄する必要があります。
また、プログラムの終了時に、最後の selection イベントで取得した wl_data_offer がある場合は、破棄します。
data_offer イベントは、サーバー側で新しい wl_data_offer が作成された時に来ます。
このイベント内では、基本的に wl_data_offer のハンドラ設定を行います。
クリップボードの場合は、以下のような順でイベントが来ます。
wl_data_offer:offer イベントでは、クリップボードのデータの種類を、MIME タイプで取得できます。
wl_data_offer:offer イベントは、wl_data_device:data_offer の直後に生成されるため、selection イベント内で wl_data_offer のハンドラ設定を行っても、offer イベントは処理できません。
そのため、data_offer イベント内でハンドラ設定を行う必要があります。
wl_data_device は、クリップボードの管理や、D&D の操作を行うためのインターフェイスです。
//wl_data_device の作成 struct wl_data_device *wl_data_device_manager_get_data_device( struct wl_data_device_manager *wl_data_device_manager, struct wl_seat *seat); //破棄 void wl_data_device_destroy(struct wl_data_device *wl_data_device); //解放 (ver 2) void wl_data_device_release(struct wl_data_device *wl_data_device); //ハンドラ設定 int wl_data_device_add_listener(struct wl_data_device *wl_data_device, const struct wl_data_device_listener *listener, void *data); 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); }; //data_offer, selection 以外は D&D で使用する
作成時の注意点
wl_data_device を作成するためには、wl_data_device_manager と wl_seat が必要になります。そのため、この2つが両方ともバインドされている状態で、wl_data_device_manager_get_data_device() を実行する必要があります。
wl_data_device_manager のバインドと同時に wl_data_device を作成したい場合、すでに wl_seat がバインドされている必要がありますが、wl_registry:global イベントでは、wl_data_device_manager と wl_seat が来る順番は決まっていません。
そのため、wl_seat より先に wl_data_device_manager が来た場合は、wl_data_device_manager のバインドと同時に wl_data_device を作成することはできません。
よって、wl_data_device を作成するタイミングは、以下の2通りになります。
- すべての wl_registry:global イベントが終了した後に行う。
- global イベント内で、wl_data_device_manager と wl_seat の2つが作成できた時点で行う。
今回の場合は、前者の方法で作成しています。
selection イベント
void (*selection)(void *data, struct wl_data_device *data_device, struct wl_data_offer *id);
Wayland では、クリップボードのことを「セレクション (selection)」と呼びます。
selection イベントでは、サーバーから任意のタイミングで、wl_data_offer のポインタが送られてきます。
これは、現在セレクションにセットされているデータを管理するためのインターフェイスです。
これを使用することで、クリップボードにあるデータの種類を取得したり、データを要求したりすることができます。
クリップボードからデータを読み込みたいクライアントは、selection イベントで送られてきた wl_data_offer を保持しておき、次の selection イベントが来た時に、前回の古い wl_data_offer を破棄する必要があります。
イベントが来るタイミング
selection イベントは、「クライアントがキーボードフォーカスを受け取る直前」と「クライアントにキーボードフォーカスがある状態で、新しいデータが設定された時 (またはデータがなくなった時)」に来ます。具体的には、以下のような時に来ます。
- クライアントプログラムを起動して、最初のウィンドウがアクテイブになった時。
- 自身のクライアント以外のウィンドウがアクティブな状態で、自身のクライアントのウィンドウがアクティブになった時。
- クライアントのウィンドウがアクティブな状態で、新しいクリップボードデータが設定された時。
つまり、現在アクティブなクライアントに対しては、クリップボードのデータが変更されたタイミングで、常に通知されてくるということです。
wl_data_offer のポインタ
送られてきた wl_data_offer のポインタが NULL の場合は、現在クリップボードにデータがない状態です。クリップボードのソースデータを保持しているクライアントが、セレクションの所有権を放棄した時 (主にプログラムの終了時) に来ます。
wl_data_offer のポインタは、次の selection イベントで、新しい wl_data_offer が送られてくるまで有効となります。
前回の selection イベントで取得した wl_data_offer は、wl_data_offer_destroy() で破棄する必要があります。
また、プログラムの終了時に、最後の selection イベントで取得した wl_data_offer がある場合は、破棄します。
data_offer イベント
void (*data_offer)(void *data, struct wl_data_device *data_device, struct wl_data_offer *id);
data_offer イベントは、サーバー側で新しい wl_data_offer が作成された時に来ます。
このイベント内では、基本的に wl_data_offer のハンドラ設定を行います。
クリップボードの場合は、以下のような順でイベントが来ます。
wl_data_device:data_offer -> wl_data_offer ハンドラ設定 wl_data_offer:offer (複数) -> MIME タイプの取得 wl_data_device:selection -> wl_data_offer の保持と破棄
wl_data_offer:offer イベントでは、クリップボードのデータの種類を、MIME タイプで取得できます。
wl_data_offer:offer イベントは、wl_data_device:data_offer の直後に生成されるため、selection イベント内で wl_data_offer のハンドラ設定を行っても、offer イベントは処理できません。
そのため、data_offer イベント内でハンドラ設定を行う必要があります。
wl_data_offer
wl_data_offer は、クライアントからは作成できません。
サーバーで作成されたものを、クライアントがイベントで受け取るという形になります。
offer イベント以外は、D&D で使います。
wl_data_device:data_offer イベントの直後に、wl_data_offer でデータを受信する際に指定できる MIME タイプが、一つずつ通知されてきます。
MIME タイプは、読み込むデータの形式を表します。
UTF-8 のテキストであれば、"text/plain;charset=utf-8" となります。
ソースデータを持っている側が、複数の MIME タイプでのデータ送信に対応している場合は、複数回送られてきます。
サーバーで作成されたものを、クライアントがイベントで受け取るという形になります。
//破棄 void wl_data_offer_destroy(struct wl_data_offer *wl_data_offer); //ハンドラ設定 int wl_data_offer_add_listener(struct wl_data_offer *data_offer, const struct wl_data_offer_listener *listener, void *data); 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 イベント以外は、D&D で使います。
offer イベント
void (*offer)(void *data, struct wl_data_offer *data_offer, const char *mime_type);
wl_data_device:data_offer イベントの直後に、wl_data_offer でデータを受信する際に指定できる MIME タイプが、一つずつ通知されてきます。
MIME タイプは、読み込むデータの形式を表します。
UTF-8 のテキストであれば、"text/plain;charset=utf-8" となります。
ソースデータを持っている側が、複数の MIME タイプでのデータ送信に対応している場合は、複数回送られてきます。
実際の動作を見てみる
ここまでの内容から、実際にクライアントが受け取るイベントを確認してみます。
起動時、またはウィンドウがアクティブになった時に、以下のイベントが来ます。
クリップボードデータがない時は、selection イベントで、offer = NULL となります。
wl_data_device の selection と data_offer イベントで渡される、wl_data_offer のポインタ値は同じです。
テキストデータは、基本的に UTF-8 文字列で扱われますが、上記を見ればわかるように、色々な形式に対応しています。
"text/plain" や "text/plain;charset=utf-8" が2回来ていますが、あまり気にしなくても良いです。
起動時、またはウィンドウがアクティブになった時に、以下のイベントが来ます。
===== クリップボードデータがない時 wl_data_device # selection | offer:(nil) ===== テキストデータがある時 wl_data_device # selection | offer:(nil) wl_data_device # data_offer | offer:0x5bac2d54df00 wl_data_offer # offer | type:"text/plain" wl_data_offer # offer | type:"text/plain;charset=utf-8" wl_data_offer # offer | type:"STRING" wl_data_offer # offer | type:"text/plain" wl_data_offer # offer | type:"TEXT" wl_data_offer # offer | type:"COMPOUND_TEXT" wl_data_offer # offer | type:"UTF8_STRING" wl_data_offer # offer | type:"text/plain;charset=utf-8" wl_data_offer # offer | type:"application/x-gtk-text-buffer-rich-text" wl_data_offer # offer | type:"GTK_TEXT_BUFFER_CONTENTS" wl_data_device # selection | offer:0x5bac2d54df00
クリップボードデータがない時は、selection イベントで、offer = NULL となります。
wl_data_device の selection と data_offer イベントで渡される、wl_data_offer のポインタ値は同じです。
テキストデータは、基本的に UTF-8 文字列で扱われますが、上記を見ればわかるように、色々な形式に対応しています。
"text/plain" や "text/plain;charset=utf-8" が2回来ていますが、あまり気にしなくても良いです。
MIME タイプについて
「STRING、TEXT、COMPOUND_TEXT、UTF8_STRING」は、X11 のセレクションで使われていた名前です。
Wayland 上で X11 プログラムを動作させるための互換として登録されています。
Wayland 上で X11 プログラムを動作させるための互換として登録されています。
クリップボードについて
Wayland では、クリップボードの扱いは、基本的に X11 と似たような構造になっています。
基本的に、デスクトップ側がクリップボードデータを保持するのではなく、コピー操作を行った各クライアントが、自身でソースデータを保持する形になっています。
この方が、ソースデータを様々な形式で扱えるという利点があります。
データを持つクライアントは、セレクションの所有権を持つことで、サーバーに対して、自身がクリップボードデータを持っていることを通知します。
データを持つクライアントは、常にそのデータを保持し、他のクライアントがクリップボードデータを要求してきたら、サーバーがそれを仲介して、クライアント同士が直接データをやりとりするという形になります。
なお、「クリップボードマネージャ」のアプリが存在する場合は、クリップボードマネージャが、データを持つクライアントに対してデータを要求して、そのデータを保持し、自身が代わりにセレクションの所有者となって、データを要求してくるクライアントに対応する、といった処理を行う場合があります。
基本的に、デスクトップ側がクリップボードデータを保持するのではなく、コピー操作を行った各クライアントが、自身でソースデータを保持する形になっています。
この方が、ソースデータを様々な形式で扱えるという利点があります。
データを持つクライアントは、セレクションの所有権を持つことで、サーバーに対して、自身がクリップボードデータを持っていることを通知します。
データを持つクライアントは、常にそのデータを保持し、他のクライアントがクリップボードデータを要求してきたら、サーバーがそれを仲介して、クライアント同士が直接データをやりとりするという形になります。
なお、「クリップボードマネージャ」のアプリが存在する場合は、クリップボードマネージャが、データを持つクライアントに対してデータを要求して、そのデータを保持し、自身が代わりにセレクションの所有者となって、データを要求してくるクライアントに対応する、といった処理を行う場合があります。
ソース側のデータの保持について
「クリップボードにデータをコピーする」という処理を行う場合、ソースデータを持つクライアントは、プログラムが起動している間は、常に自身でデータを保持しておき、他のクライアントがデータを要求してきたら、それに対応してデータを送信する必要があります。
データは基本的に、複数形式でやりとりできるようになっています。
例えば、データを持っているクライアントが、任意形式で画像のデータを保持していて、BMP, PNG, JPEG のいずれかの形式でデータを渡せるようになっている場合、貼り付けを行いたい他のクライアントは、その中から一つの形式を選択して、データの送信を要求します。
そうすると、サーバーは、現在データを持っているクライアントに対して、データの送信を要求するので、その要求をイベントで受け取ったソース側は、指定された形式で、受信側にデータを送信します。
このように、データが要求された時は、要求された任意の形式にエンコードして送信する必要があるため、クリップボードデータを持つ側は、常に素となるデータを保持しておく必要があります。
データは基本的に、複数形式でやりとりできるようになっています。
例えば、データを持っているクライアントが、任意形式で画像のデータを保持していて、BMP, PNG, JPEG のいずれかの形式でデータを渡せるようになっている場合、貼り付けを行いたい他のクライアントは、その中から一つの形式を選択して、データの送信を要求します。
そうすると、サーバーは、現在データを持っているクライアントに対して、データの送信を要求するので、その要求をイベントで受け取ったソース側は、指定された形式で、受信側にデータを送信します。
このように、データが要求された時は、要求された任意の形式にエンコードして送信する必要があるため、クリップボードデータを持つ側は、常に素となるデータを保持しておく必要があります。
サーバーやクリップボードマネージャによるデータ保持
以上のようにしてクリップボードを扱う場合、デメリットとして、データを持つクライアントが終了してしまうと、データ自体が存在しなくなってしまうため、その後は、データが取得できなくなってしまいます。
テキストの場合は、特に使用される頻度が高いので、データを持つクライアントが終了しても、コピーされたテキストを引き続き貼り付けられるようにした方が便利です。
そのため、デスクトップによっては、クライアントが終了しても、クリップボードデータを維持できるように、クリップボードデータが変更された時に、常にデータを要求してそれを保持しておき、データを持つクライアントが終了した時に、自身が代わりにセレクションの所有者になる、といったことを行っている場合があります。
もしくは、クリップボードマネージャが存在する場合も、同じように、クリップボードマネージャがクリップボードのデータを保持します。
GNOME で、今回のプログラムを実行した後、テキストエディタでテキストをコピーし、テキストエディタを終了した後、プログラムのウィンドウをアクティブにすると、以下のようにイベントが来ます。
selection で offer = NULL になっているので、クリップボードのデータは一度空になりますが、その直後に再び selection が来るので、ここでデスクトップがセレクション所有者に成り代わり、保持した以前のテキストが貼り付けられるようになっています。
ただし、データの形式は UTF-8 文字列のみとなっています。
このような動作は、データが一般的な形式である場合のみ有効です。
特殊な形式であれば維持されることはないので、注意してください。
テキストの場合は、特に使用される頻度が高いので、データを持つクライアントが終了しても、コピーされたテキストを引き続き貼り付けられるようにした方が便利です。
そのため、デスクトップによっては、クライアントが終了しても、クリップボードデータを維持できるように、クリップボードデータが変更された時に、常にデータを要求してそれを保持しておき、データを持つクライアントが終了した時に、自身が代わりにセレクションの所有者になる、といったことを行っている場合があります。
もしくは、クリップボードマネージャが存在する場合も、同じように、クリップボードマネージャがクリップボードのデータを保持します。
GNOME で、今回のプログラムを実行した後、テキストエディタでテキストをコピーし、テキストエディタを終了した後、プログラムのウィンドウをアクティブにすると、以下のようにイベントが来ます。
wl_data_device # selection | offer:(nil) wl_data_device # data_offer | offer:0x5bbb78f67d00 wl_data_offer # offer | type:"text/plain;charset=utf-8" wl_data_device # selection | offer:0x5bbb78f67d00
selection で offer = NULL になっているので、クリップボードのデータは一度空になりますが、その直後に再び selection が来るので、ここでデスクトップがセレクション所有者に成り代わり、保持した以前のテキストが貼り付けられるようになっています。
ただし、データの形式は UTF-8 文字列のみとなっています。
このような動作は、データが一般的な形式である場合のみ有効です。
特殊な形式であれば維持されることはないので、注意してください。
wl_data_source
wl_data_source は、wl_data_device_manager から作成できます。
これは、クライアントが、クリップボードのソースデータを持ちたい場合に使います。
つまり、クリップボードに「コピーする」という処理を行いたい時に使います。
wl_data_source の send イベントは、ソースデータを持つクライアントに対して、データの送信が要求された時に呼ばれます。
クライアントは、指定された MIME タイプの形式でエンコードしたデータを、fd のファイルディスクリプタに送信する必要があります。
unistd.h の write() で書き込みを行い、最後に close() で閉じます。
文字列を送信する場合は、終端のヌル文字を付けずに送信したほうが良いでしょう。
データを受信したクライアント側は、ヌル文字は付いていないものと想定して、データを扱う必要があります。
cancelled イベントは、セットされている wl_data_source がキャンセルされた時に呼ばれます。
クリップボードの場合は、クライアントがクリップボードデータを持っている状態で、他のクライアントが新しいクリップボードデータをセットした時に呼ばれます。
つまり、その wl_data_source はもう使用しないので、破棄する必要がある、ということです。
このイベント内では、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); //破棄 void wl_data_source_destroy(struct wl_data_source *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); }; //send, cancelled 以外は D&D で使用する //MIME タイプ追加 void wl_data_source_offer(struct wl_data_source *wl_data_source, const char *mime_type); //(wl_data_device) クリップボードのデータとしてセット void wl_data_device_set_selection(struct wl_data_device *wl_data_device, struct wl_data_source *source, uint32_t serial);
処理
まず、wl_data_device_manager から wl_data_source を作成します。
新しいデータを作成するするたびに作り直す必要があります。
次に、wl_data_source_offer() で、データの送信時に対応する MIME タイプを追加します。
一つのデータで複数の形式に対応するなら、複数回実行します。
そして、wl_data_source_add_listener() でハンドラを設定します。
最後に、wl_data_device_set_selection() で、wl_data_source を、新しいクリップボードのデータとしてセットします。
この時点で、クライアントがセレクションの所有権を取得することになります。
source に NULL を指定すると、セレクションの所有権を放棄します。
つまり、クライアントがもうクリップボードデータを持たないということをサーバーに通知します。
serial は、wl_pointer や wl_keyboard などで、serial 引数を持つイベントがある場合、そこで一番最後に渡されたシリアル値です。
キー押しやボタン押しによって、「コピー」の動作を行った場合は、その時のイベントの値を使います。
新しいデータを作成するするたびに作り直す必要があります。
次に、wl_data_source_offer() で、データの送信時に対応する MIME タイプを追加します。
一つのデータで複数の形式に対応するなら、複数回実行します。
そして、wl_data_source_add_listener() でハンドラを設定します。
最後に、wl_data_device_set_selection() で、wl_data_source を、新しいクリップボードのデータとしてセットします。
この時点で、クライアントがセレクションの所有権を取得することになります。
source に NULL を指定すると、セレクションの所有権を放棄します。
つまり、クライアントがもうクリップボードデータを持たないということをサーバーに通知します。
serial は、wl_pointer や wl_keyboard などで、serial 引数を持つイベントがある場合、そこで一番最後に渡されたシリアル値です。
キー押しやボタン押しによって、「コピー」の動作を行った場合は、その時のイベントの値を使います。
X11 プログラム内での貼り付け
xwayland を使って、Wayland 上で X11 プログラムを動かしている場合は、X11 で使われる名前 ("UTF8_STRING" など) を MIME タイプとして設定しておかないと、X11 プログラム内でのテキストの貼り付けが行なえません。
「"text/plain;charset=utf-8"」だけでは、UTF-8 テキストとして認識されないので、注意が必要です。
(Wayland プログラム同士の場合は、普通の MIME タイプでやりとりできます)
しかし、ウィンドウの見た目だけでは、Wayland と X11 のどちらで起動しているのかは判断できません。
GTK+3、Qt5 以降は Wayland に対応しているので、それ以前のものを使用して作られている場合は、X11 で起動していると思っていいでしょう。
「"text/plain;charset=utf-8"」だけでは、UTF-8 テキストとして認識されないので、注意が必要です。
(Wayland プログラム同士の場合は、普通の MIME タイプでやりとりできます)
しかし、ウィンドウの見た目だけでは、Wayland と X11 のどちらで起動しているのかは判断できません。
GTK+3、Qt5 以降は Wayland に対応しているので、それ以前のものを使用して作られている場合は、X11 で起動していると思っていいでしょう。
コピー時のイベント
今回のプログラムを GNOME 上で実行し、'C' キーでコピーの動作を行った場合のイベントは、以下のようになります (2025年2月時点)。
自身のクライアントがセレクションの所有権を取得した場合も、wl_data_device:selection イベントが来ます。
その直後に、wl_data_source:send イベントで、データの要求が行われています。
おそらく、クライアントが終了した時に備えて、デスクトップ側でテキストデータを保持するためのものです。
-- copy text: "copytext-414" wl_data_device # data_offer | offer:0x5be5bff76560 wl_data_offer # offer | type:"UTF8_STRING" wl_data_offer # offer | type:"text/plain;charset=utf-8" wl_data_device # selection | offer:0x5be5bff76560 wl_data_source # send | mime_type:"text/plain;charset=utf-8", fd:5 wl_data_source # send | mime_type:"UTF8_STRING", fd:5 wl_data_source # send | mime_type:"UTF8_STRING", fd:5
自身のクライアントがセレクションの所有権を取得した場合も、wl_data_device:selection イベントが来ます。
その直後に、wl_data_source:send イベントで、データの要求が行われています。
おそらく、クライアントが終了した時に備えて、デスクトップ側でテキストデータを保持するためのものです。
send イベント
void (*send)(void *data, struct wl_data_source *wl_data_source, const char *mime_type, int32_t fd);
wl_data_source の send イベントは、ソースデータを持つクライアントに対して、データの送信が要求された時に呼ばれます。
クライアントは、指定された MIME タイプの形式でエンコードしたデータを、fd のファイルディスクリプタに送信する必要があります。
unistd.h の write() で書き込みを行い、最後に close() で閉じます。
文字列を送信する場合は、終端のヌル文字を付けずに送信したほうが良いでしょう。
データを受信したクライアント側は、ヌル文字は付いていないものと想定して、データを扱う必要があります。
cancelled イベント
void (*cancelled)(void *data, struct wl_data_source *wl_data_source);
cancelled イベントは、セットされている wl_data_source がキャンセルされた時に呼ばれます。
クリップボードの場合は、クライアントがクリップボードデータを持っている状態で、他のクライアントが新しいクリップボードデータをセットした時に呼ばれます。
つまり、その wl_data_source はもう使用しないので、破棄する必要がある、ということです。
このイベント内では、wl_data_source と、それに関連するクリップボードのデータを破棄します。
クリップボードからの貼り付け
「貼り付け」を行うために、クリップボードからデータを取得したい場合、wl_data_offer のポインタが必要です。
wl_data_device:selection イベントで取得したポインタを使用します。
ポインタが NULL の場合は、クリップボードデータが存在していないため、取得できません。
なお、データを受信するには、unistd.h の read() を使って、ファイルディスクリプタから読み込む必要があります。
今回は、一度に読み込むのではなく、イベントループに組み込む形で実装しています。
クリップボードデータを持っているクライアントに、指定した MIME タイプでデータを送信するように要求します。
fd には、自身がデータを取得するための、書き込み用のファイルディスクリプタを指定します。
なお、サーバー側は、MIME タイプの判別などは行わないため、要求された MIME タイプを判定するのは、送信側の処理となります。
対応していない MIME タイプを要求した場合でも、送信自体がキャンセルされることはありません。
今回の場合は、クリップボードにどんな形式のデータがあろうと、常に「UTF-8 テキスト」として読み込む形にしていますが、本来は、wl_data_offer:offer イベント時に、現在のクリップボードデータが扱える MIME タイプを記録しておいて、そこから読み込む形式を判別する必要があります。
書き込み用の fd は、送信側がデータを送る際に使われますが、こちら側ではもう使用しないので、閉じることができます。
その後、読み込み用の fd を使って、順次データの読み込みを行い、書き込み側で fd が閉じられたら、そこでデータが終了したということなので、読み込みを終了します。
今回は、pipe に O_CLOEXEC フラグを付けるために、pipe2() 関数を使っています。
weston でも GTK+ でも、このフラグは付けられているので、指定しておいた方が良さそうです。
wl_data_device:selection イベントで取得したポインタを使用します。
ポインタが NULL の場合は、クリップボードデータが存在していないため、取得できません。
なお、データを受信するには、unistd.h の read() を使って、ファイルディスクリプタから読み込む必要があります。
今回は、一度に読み込むのではなく、イベントループに組み込む形で実装しています。
データを要求する
void wl_data_offer_receive(struct wl_data_offer *wl_data_offer, const char *mime_type, int32_t fd);
クリップボードデータを持っているクライアントに、指定した MIME タイプでデータを送信するように要求します。
fd には、自身がデータを取得するための、書き込み用のファイルディスクリプタを指定します。
なお、サーバー側は、MIME タイプの判別などは行わないため、要求された MIME タイプを判定するのは、送信側の処理となります。
対応していない MIME タイプを要求した場合でも、送信自体がキャンセルされることはありません。
今回の場合は、クリップボードにどんな形式のデータがあろうと、常に「UTF-8 テキスト」として読み込む形にしていますが、本来は、wl_data_offer:offer イベント時に、現在のクリップボードデータが扱える MIME タイプを記録しておいて、そこから読み込む形式を判別する必要があります。
データの受信
データを受信する場合は、パイプを使って fd を2つ作成し、その書き込み用の fd を wl_data_offer_receive() に渡して、すぐに close() で閉じます。書き込み用の fd は、送信側がデータを送る際に使われますが、こちら側ではもう使用しないので、閉じることができます。
その後、読み込み用の fd を使って、順次データの読み込みを行い、書き込み側で fd が閉じられたら、そこでデータが終了したということなので、読み込みを終了します。
今回は、pipe に O_CLOEXEC フラグを付けるために、pipe2() 関数を使っています。
weston でも GTK+ でも、このフラグは付けられているので、指定しておいた方が良さそうです。
イベントループ
今回は、poll() を使って、Wayland のイベントとクリップボードデータの受信を待っていますが、select や epoll を使っても構いません。
wl_data_offer_receive() の実行後、wl_display_flush() を実行すると、その後すぐにデータを受信することもできますが、データが大きい場合、すべてを受信するのに時間がかかる場合があるので、イベントループ内に組み込んで、データがある時に読み込んでいくといいでしょう。
wl_display_get_fd() で、Wayland ディスプレイ用のファイルディスクリプタを取得できます。
これを使うと、イベントが存在する場合、入力がある状態となるので、何らかのイベントが起こるまで待つことができます。
なお、poll を行う前に、wl_display_flush() を行って、クライアントのリクエストをサーバーに送信しないと、全くイベントが起こらない状態となるので、永遠に待つことになります。
wl_data_offer_receive() の実行後、wl_display_flush() を実行すると、その後すぐにデータを受信することもできますが、データが大きい場合、すべてを受信するのに時間がかかる場合があるので、イベントループ内に組み込んで、データがある時に読み込んでいくといいでしょう。
wl_display_get_fd() で、Wayland ディスプレイ用のファイルディスクリプタを取得できます。
これを使うと、イベントが存在する場合、入力がある状態となるので、何らかのイベントが起こるまで待つことができます。
なお、poll を行う前に、wl_display_flush() を行って、クライアントのリクエストをサーバーに送信しないと、全くイベントが起こらない状態となるので、永遠に待つことになります。
データの受信
クリップボードデータの受信においては、送信側がデータを送信した時 (受信側がデータが読み込めるようになった時) と、送信側で fd が閉じられた時に poll を抜けてくるので、その時に、データの受信や読み込みの終了処理を行います。
データは、read() で受信します。
poll が返った時に POLLHUP が ON の場合は、送信側で fd が閉じられた状態です。
送信側で fd が閉じられたとしても、受信側で fd を閉じない限り、読み込みはそのまま継続できます。
送信側で fd が閉じられて、かつ読み込んだデータがない場合は、データが終了したということになります。
その後は close() で fd を閉じて、実際に受信したデータを処理します。
データは、read() で受信します。
poll が返った時に POLLHUP が ON の場合は、送信側で fd が閉じられた状態です。
送信側で fd が閉じられたとしても、受信側で fd を閉じない限り、読み込みはそのまま継続できます。
送信側で fd が閉じられて、かつ読み込んだデータがない場合は、データが終了したということになります。
その後は close() で fd を閉じて、実際に受信したデータを処理します。