クリップボード
ここでは、クリップボードの文字列のコピーと貼り付けを行います。
ウィンドウがアクティブな状態で、キーボードのキーを押して、操作を行います。
<12_clipboard.c>
$ cc -o test 12_clipboard.c client.c imagebuf.c -lwayland-client -lrt
ウィンドウがアクティブな状態で、キーボードのキーを押して、操作を行います。
C | コピー |
---|---|
P | 貼り付け |
R | クリップボード解放 |
ESC | プログラム終了 |
<12_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.h> #include <wayland-client.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; //受信バッファ確保サイズ }ClientEx; //------------- /* ソースデータ解放 */ static void _source_release(ClientEx *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) { //D&D } /* データが要求された時 */ static void _data_source_send(void *data, struct wl_data_source *source, const char *mime_type, int32_t fd) { ClientEx *p = (ClientEx *)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((ClientEx *)data); } void _data_source_dnd_drop_performed(void *data, struct wl_data_source *source) { //D&D } void _data_source_dnd_finished(void *data, struct wl_data_source *source) { //D&D } void _data_source_action(void *data, struct wl_data_source *source, uint32_t dnd_action) { //D&D } 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) { //D&D } static void _data_offer_action(void *data, struct wl_data_offer *offer, uint32_t dnd_action) { //D&D } 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) { //D&D } static void _device_leave(void *data, struct wl_data_device *data_device) { //D&D } static void _device_motion(void *data, struct wl_data_device *data_device, uint32_t time, wl_fixed_t x, wl_fixed_t y) { //D&D } static void _device_drop(void *data, struct wl_data_device *data_device) { //D&D } /* クリップボード用の wl_data_offer が送られてくる */ static void _device_selection(void *data, struct wl_data_device *data_device, struct wl_data_offer *offer) { ClientEx *p = (ClientEx *)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(ClientEx *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(ClientEx *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(ClientEx *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); } } } //----------------------- // wl_keyboard //----------------------- static void _keyboard_keymap(void *data, struct wl_keyboard *keyboard, uint32_t format, int32_t fd, uint32_t size) { //キーマップ処理はしない } static void _keyboard_enter(void *data, struct wl_keyboard *keyboard, uint32_t serial, struct wl_surface *surface, struct wl_array *keys) { } static void _keyboard_leave(void *data, struct wl_keyboard *keyboard, uint32_t serial, struct wl_surface *surface) { } /* キー */ static void _keyboard_key(void *data, struct wl_keyboard *keyboard, uint32_t serial, uint32_t time, uint32_t key, uint32_t state) { ClientEx *p = (ClientEx *)data; char m[64]; if(state != WL_KEYBOARD_KEY_STATE_PRESSED) return; switch(key) { //コピー case KEY_C: snprintf(m, 64, "copytext-%u", serial); _copy_text(p, m, serial); break; //貼り付け case KEY_P: _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 _keyboard_modifiers(void *data, struct wl_keyboard *keyboard, uint32_t serial, uint32_t mods_depressed, uint32_t mods_latched, uint32_t mods_locked, uint32_t group) { } static void _keyboard_repeat_info(void *data, struct wl_keyboard *keyboard, int32_t rate, int32_t delay) { } static const struct wl_keyboard_listener g_keyboard_listener = { _keyboard_keymap, _keyboard_enter, _keyboard_leave, _keyboard_key, _keyboard_modifiers, _keyboard_repeat_info }; //----------------------- // 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; } } //-------------------- /* イベントループ */ static void _event_loop(ClientEx *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; //display にイベントがある 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); } } /* ClientEx 破棄 */ static void _clientex_destroy(Client *cl) { ClientEx *p = (ClientEx *)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); } //------------------- 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_KEYBOARD; p->b.registry_global = _registry_global; p->b.keyboard_listener = &g_keyboard_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; //ウィンドウ win = Window_create(CLIENT(p), 256, 256, NULL); ImageBuf_fill(win->img, 0xffff0000); Window_update(win); // _event_loop(p); //解放 Window_destroy(win); Client_destroy(CLIENT(p)); return 0; }
解説
「クリップボード」と「ドラッグ&ドロップ」の操作は、同じインターフェイスを使います。
以下の4つのオブジェクトを使います。
以下の4つのオブジェクトを使います。
wl_data_device_manager | wl_data_* の管理 |
---|---|
wl_data_devie | メインとなる操作を行う |
wl_data_source | クライアントが持つソースデータの管理 |
wl_data_offer | データの操作 |
wl_data_device_manager
まずは、wl_data_device_manager をバインドします。
これは、wl_data_device と wl_data_source を作成するのに使います。
使用する最大バージョンは「3」とします。
wl_data_device は ver 2 で wl_data_device_release() 関数が追加されているので、破棄時に destroy/release どちらを使うのかを判断するために、バージョンの値を記録しておきます。
これは、wl_data_device と wl_data_source を作成するのに使います。
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; }
使用する最大バージョンは「3」とします。
wl_data_device は ver 2 で wl_data_device_release() 関数が追加されているので、破棄時に destroy/release どちらを使うのかを判断するために、バージョンの値を記録しておきます。
wl_data_device
wl_data_device_manager がバインドできたら、wl_data_device を作成します。
よって、この2つをバインドした後で、wl_data_device_manager_get_data_device() を実行する必要があります。
しかし、wl_registry:global イベントでは、2つのうちどちらが先に来るかわからないので、すべての global イベントが終了して、wl_data_device_manager と wl_seat の2つが確実に作成できた後に実行するか、global イベント時のそれぞれで、片方がすでに作成されているかを確認して、両方作成できた段階で実行するかの、2通りの方法で実行してください。
data_offer と selection 以外は、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); ## ハンドラ設定 int wl_data_device_add_listener( struct wl_data_device *wl_data_device, const struct wl_data_device_listener *listener, void *data);
wl_data_device の作成タイミング
wl_data_device を作成するためには、wl_data_device_manager と wl_seat が必要です。よって、この2つをバインドした後で、wl_data_device_manager_get_data_device() を実行する必要があります。
しかし、wl_registry:global イベントでは、2つのうちどちらが先に来るかわからないので、すべての global イベントが終了して、wl_data_device_manager と wl_seat の2つが確実に作成できた後に実行するか、global イベント時のそれぞれで、片方がすでに作成されているかを確認して、両方作成できた段階で実行するかの、2通りの方法で実行してください。
wl_data_device のイベント
void (*data_offer)(void *data, struct wl_data_device *wl_data_device, struct wl_data_offer *id); void (*enter)(void *data, struct wl_data_device *wl_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 *wl_data_device); void (*motion)(void *data, struct wl_data_device *wl_data_device, uint32_t time, wl_fixed_t x, wl_fixed_t y); void (*drop)(void *data, struct wl_data_device *wl_data_device) void (*selection)(void *data, struct wl_data_device *wl_data_device, struct wl_data_offer *id);
data_offer と selection 以外は、D&D 時に使われるので、今回は説明を省略します。
wl_data_device : selection イベント
Wayland では、クリップボードのことを「selection」と呼びます。
このイベントでは、サーバーから、現在クリップボード上にあるデータの情報となる wl_data_offer のポインタが送られてきます。
この値で、現在クリップボードにデータがあるか、ないかがわかります。
また、このポインタを使うことで、クリップボードのデータを受信することができます。
「ウィンドウがアクティブな状態で、新しいクリップボードデータが設定された時」に来ます。
具体的には、以下のような時に来ます。
NULL 以外の場合は、クリップボードにデータが存在します。
ここで渡された wl_data_offer のポインタは、次の selection イベントで、新しい wl_data_offer のポインタ値が送られてくるまで、有効となります。
よって、selection イベント時は、wl_data_offer のポインタ値を保存しておく必要があります。
また、前回の selection イベントで取得した wl_data_offer は、wl_data_offer_destroy() で破棄する必要があります。
プログラムの終了時には、selection イベントで保存した wl_data_offer が残っている場合、破棄します。
このイベントでは、サーバーから、現在クリップボード上にあるデータの情報となる wl_data_offer のポインタが送られてきます。
この値で、現在クリップボードにデータがあるか、ないかがわかります。
また、このポインタを使うことで、クリップボードのデータを受信することができます。
来るタイミング
「ウィンドウがアクティブになる直前」と、「ウィンドウがアクティブな状態で、新しいクリップボードデータが設定された時」に来ます。
具体的には、以下のような時に来ます。
- クライアントプログラムを起動して、最初のウィンドウがアクテイブになった時。
- 他のウィンドウがアクティブな状態で、クライアントのウィンドウがアクティブになった時。
- クライアントのウィンドウがアクティブな状態で、新しいクリップボードデータが設定された時。
wl_data_offer のポインタ
送られてきた wl_data_offer のポインタが NULL の場合は、現在クリップボードにデータがない状態です。NULL 以外の場合は、クリップボードにデータが存在します。
ここで渡された wl_data_offer のポインタは、次の selection イベントで、新しい wl_data_offer のポインタ値が送られてくるまで、有効となります。
よって、selection イベント時は、wl_data_offer のポインタ値を保存しておく必要があります。
また、前回の selection イベントで取得した wl_data_offer は、wl_data_offer_destroy() で破棄する必要があります。
プログラムの終了時には、selection イベントで保存した wl_data_offer が残っている場合、破棄します。
wl_data_device : data_offer イベント
サーバー側で新しい wl_data_offer が作成された時に来ます。
新しいクリップボードデータが設定された時や、ウィンドウがアクティブになる直前に来ます。
クリップボードの場合は、このイベントの直後に selection イベントが来て、同じ wl_data_offer ポインタ値が送られてきます。
また、同時に、wl_data_offer の offer イベントも生成されます。
このイベント内では、基本的に、wl_data_offer のハンドラ設定を行います。
wl_data_offer_add_listener() を使ってイベントハンドラを設定することで、クリップボードのデータの種類を MIME タイプで取得できます。
新しいクリップボードデータが設定された時や、ウィンドウがアクティブになる直前に来ます。
クリップボードの場合は、このイベントの直後に selection イベントが来て、同じ wl_data_offer ポインタ値が送られてきます。
また、同時に、wl_data_offer の offer イベントも生成されます。
このイベント内では、基本的に、wl_data_offer のハンドラ設定を行います。
wl_data_offer_add_listener() を使ってイベントハンドラを設定することで、クリップボードのデータの種類を MIME タイプで取得できます。
wl_data_offer
wl_data_offer は、クライアント側では作成できません。
サーバーで作成されたものを、クライアントがイベントで受け取るという形になります。
wl_data_offer のイベントでは、データの種類などの情報を取得できます。
主に、ソースデータの情報が送られてきます。
offer イベント以外は、D&D で使います。
サーバーで作成されたものを、クライアントがイベントで受け取るという形になります。
wl_data_offer のイベントでは、データの種類などの情報を取得できます。
wl_data_offer のイベント
void (*offer)(void *data, struct wl_data_offer *wl_data_offer, const char *mime_type); void (*source_actions)(void *data, struct wl_data_offer *wl_data_offer, uint32_t source_actions); void (*action)(void *data, struct wl_data_offer *wl_data_offer, uint32_t dnd_action);
主に、ソースデータの情報が送られてきます。
offer イベント以外は、D&D で使います。
wl_data_offer : offer イベント
クリップボードの場合は、クライアントがクリップボードデータを受信する時に、指定することが可能な MIME タイプが、一つずつ通知されてきます。
MIME タイプは文字列で指定され、UTF-8 のテキストなら、"text/plain;charset=utf-8" となります。
データを持っている側が複数の MIME タイプの送信に対応している場合は、受信する側が取得したい MIME タイプを要求して、送信側がその形式でデータを送信する、という形になります。
MIME タイプは文字列で指定され、UTF-8 のテキストなら、"text/plain;charset=utf-8" となります。
データを持っている側が複数の MIME タイプの送信に対応している場合は、受信する側が取得したい MIME タイプを要求して、送信側がその形式でデータを送信する、という形になります。
実際の動作を見てみる
では、実際にクライアントが受け取るイベントを確認してみます。
起動時、またはウィンドウがアクティブになった時に以下のイベントが来ます。
クリップボードデータがない時は、selection イベントで offer に NULL が送られてきます。
wl_data_device の selection と data_offer イベントで渡される wl_data_offer のポインタ値は同じです。
offer イベント時に渡される MIME タイプで、現在クリップボードにあるデータがどのような形式で受信できるかを取得できます。
「STRING、TEXT、COMPOUND_TEXT、UTF8_STRING」は、X11 で使われていた名前です。
Wayland 上で X11 プログラムを動作させるための互換として登録されています。
起動時、またはウィンドウがアクティブになった時に以下のイベントが来ます。
#====== クリップボードデータがない時 wl_data_device # selection | offer:(nil) #====== クリップボードデータがある時 (GNOME) wl_data_device # data_offer | offer:0x564c57299cb0 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" wl_data_offer # offer | type:"COMPOUND_TEXT" wl_data_offer # offer | type:"UTF8_STRING" 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:0x564c57299cb0
クリップボードデータがない時は、selection イベントで offer に NULL が送られてきます。
wl_data_device の selection と data_offer イベントで渡される wl_data_offer のポインタ値は同じです。
offer イベント時に渡される MIME タイプで、現在クリップボードにあるデータがどのような形式で受信できるかを取得できます。
MIME タイプについて
上記のように、データが UTF-8 テキストの場合、色々な形式で受信を行うことができます。「STRING、TEXT、COMPOUND_TEXT、UTF8_STRING」は、X11 で使われていた名前です。
Wayland 上で X11 プログラムを動作させるための互換として登録されています。
クリップボードについて
Wayland では、クリップボードの扱いは、基本的に X11 と似たような構造になっています。
サーバー側がクリップボードデータを保持するのではなく、データを持つクライアントがクリップボードデータの所有権を持っている間は、そのクライアントが常にデータを保持し、他のクライアントがクリップボードデータを要求してきたら、サーバーが仲介して、クライアント同士が直接データをやりとりするという形になります。
なお、「クリップボードマネージャ」が存在する場合は、クリップボードマネージャがデータを保持して、データを要求してくるクライアントに対応する、という場合もあります。
サーバー側がクリップボードデータを保持するのではなく、データを持つクライアントがクリップボードデータの所有権を持っている間は、そのクライアントが常にデータを保持し、他のクライアントがクリップボードデータを要求してきたら、サーバーが仲介して、クライアント同士が直接データをやりとりするという形になります。
なお、「クリップボードマネージャ」が存在する場合は、クリップボードマネージャがデータを保持して、データを要求してくるクライアントに対応する、という場合もあります。
ソース側のデータの保持について
クリップボードにデータをコピーする場合、ソースデータを持つクライアントは、プログラムが起動している間は常に自身でデータを保持しておき、他のクライアントがデータを要求してきたら、それに対応してデータを送信する必要があります。
データは基本的に複数形式でやりとりできるようになっているので、例えば、クリップボードデータを持っているクライアントが画像のデータを保持していて、BMP, PNG, JPEG のいずれかでデータを渡せるようになっている場合、貼り付けを行いたい他のクライアントは、その中からいずれかを選択して、サーバーに対しデータの送信を要求します。
そうすると、サーバーは現在データを持っているクライアントに対して、データの送信を要求するので、その要求を受け取ったソース側は、指定された形式で受信側にデータを送信します。
このように、データが要求された時は、要求された形式にエンコードして送信する必要があるため、クリップボードデータを持つ側は、常に素となるデータを保持しておく必要があります。
データは基本的に複数形式でやりとりできるようになっているので、例えば、クリップボードデータを持っているクライアントが画像のデータを保持していて、BMP, PNG, JPEG のいずれかでデータを渡せるようになっている場合、貼り付けを行いたい他のクライアントは、その中からいずれかを選択して、サーバーに対しデータの送信を要求します。
そうすると、サーバーは現在データを持っているクライアントに対して、データの送信を要求するので、その要求を受け取ったソース側は、指定された形式で受信側にデータを送信します。
このように、データが要求された時は、要求された形式にエンコードして送信する必要があるため、クリップボードデータを持つ側は、常に素となるデータを保持しておく必要があります。
サーバーやクリップボードマネージャによるデータ保持
このようなクリップボード動作の場合は、データを持つクライアントが終了してしまうと、データ自体が存在しなくなってしまうため、その後はクリップボードのデータが取得できなくなってしまいます。
テキストの場合は特に使用頻度が高いので、ソース側が終了しても、コピーされたデータを使えた方が便利です。
そのため、サーバーによっては、クライアントが終了してもクリップボードデータを維持できるように、クリップボードデータが設定された時に、ソース側にデータを要求して、サーバー内で常に現在のクリップボードのデータを保持している場合があります。
もしくは、クリップボードマネージャが存在する場合にも、同じように、クリップボードマネージャ側がクリップボードのデータを保持します。
データを持つクライアントが稼働している間は、通常通りクライアントが処理をし、クライアントが終了した場合は、サーバーやクリップボードマネージャが、代わりにクリップボードデータの所有者となって、取得したデータを元にデータを送信する、というように動作します。
ただし、この場合、すべてのフォーマットのデータに対応しているとは限りません。
UTF-8 テキストデータであれば、ほぼ対応していると思いますが、一般的でないフォーマットの場合、どのように扱われるかはわかりません。
テキストの場合は特に使用頻度が高いので、ソース側が終了しても、コピーされたデータを使えた方が便利です。
そのため、サーバーによっては、クライアントが終了してもクリップボードデータを維持できるように、クリップボードデータが設定された時に、ソース側にデータを要求して、サーバー内で常に現在のクリップボードのデータを保持している場合があります。
もしくは、クリップボードマネージャが存在する場合にも、同じように、クリップボードマネージャ側がクリップボードのデータを保持します。
データを持つクライアントが稼働している間は、通常通りクライアントが処理をし、クライアントが終了した場合は、サーバーやクリップボードマネージャが、代わりにクリップボードデータの所有者となって、取得したデータを元にデータを送信する、というように動作します。
ただし、この場合、すべてのフォーマットのデータに対応しているとは限りません。
UTF-8 テキストデータであれば、ほぼ対応していると思いますが、一般的でないフォーマットの場合、どのように扱われるかはわかりません。
クリップボードにデータをコピーする
クリップボードにデータをコピーしたい場合の手順です。
まず、wl_data_device_manager_create_data_source() で、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 などのイベントで、最後に渡されたシリアル値です。
コピー直後に wl_data_source:send が呼ばれ、weston サーバーがデータを受信しています。
解放時は、サーバーが保持しているデータを元に、クリップボード所有権が切り替わっています。
これにより、クライアント終了後も同じテキストを貼り付けることができます。
コピー直後に wl_data_source:send が呼ばれるまでは同じですが、その後に新しい wl_data_offer が作成され、selection が再度呼ばれています。
この動作の意味はわかりません。
解放時は、offer が NULL になり、そのまま、クリップボードデータがない状態となります。
GNOME のサーバー自体はクリップボードデータを保持していないことになります。
クリップボードのソースデータとして使う場合、dnd_drop_performed 以降は使いませんが、使用するバージョンに合わせてすべてのハンドラを設定する必要があります。
(使用しないハンドラを NULL に設定することはできない)
指定された MIME タイプの形式で、データを送信する必要があります。
渡された fd に対して、write() で書き込みを行って、最後に close() で閉じます。
文字列の場合、最後の NULL 文字は付けずに送信したほうが良いでしょう。
データを受信したクライアント側は、NULL 文字が付いていないと想定してデータを扱う必要があります。
クリップボードの場合は、クライアントがクリップボードデータを持っている状態で、他のクライアントがクリップボードデータをセットした時に呼ばれます。
このイベント内では、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 破棄 void wl_data_source_destroy(struct wl_data_source *wl_data_source); ## MIME タイプ追加 void wl_data_source_offer(struct wl_data_source *wl_data_source, const char *mime_type); ## 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); ## クリップボードのデータとしてセット 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_create_data_source() で、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 などのイベントで、最後に渡されたシリアル値です。
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 で起動していると思っていいでしょう。
コピー時の動作
weston
▼ コピー時 -- copy text: "copytext-22" wl_data_device # data_offer | offer:0x55e2c721ed00 wl_data_offer # offer | type:"text/plain;charset=utf-8" wl_data_offer # offer | type:"UTF8_STRING" wl_data_device # selection | offer:0x55e2c721ed00 wl_data_source # send | mime_type:"text/plain;charset=utf-8", fd:5 ▼ 解放時 wl_data_device # selection | offer:(nil) wl_data_device # data_offer | offer:0x55e2c721ed50 wl_data_offer # offer | type:"text/plain;charset=utf-8" wl_data_device # selection | offer:0x55e2c721ed50
コピー直後に wl_data_source:send が呼ばれ、weston サーバーがデータを受信しています。
解放時は、サーバーが保持しているデータを元に、クリップボード所有権が切り替わっています。
これにより、クライアント終了後も同じテキストを貼り付けることができます。
GNOME
▼ コピー時 -- copy text: "copytext-265" wl_data_device # data_offer | offer:0x5640daf5bca0 wl_data_offer # offer | type:"UTF8_STRING" wl_data_offer # offer | type:"text/plain;charset=utf-8" wl_data_device # selection | offer:0x5640daf5bca0 wl_data_source # send | mime_type:"text/plain;charset=utf-8", fd:5 wl_data_device # data_offer | offer:0x5640daf5bd20 wl_data_offer # offer | type:"UTF8_STRING" wl_data_offer # offer | type:"text/plain;charset=utf-8" wl_data_device # selection | offer:0x5640daf5bd20 ▼ 解放時 wl_data_device # selection | offer:(nil)
コピー直後に wl_data_source:send が呼ばれるまでは同じですが、その後に新しい wl_data_offer が作成され、selection が再度呼ばれています。
この動作の意味はわかりません。
解放時は、offer が NULL になり、そのまま、クリップボードデータがない状態となります。
GNOME のサーバー自体はクリップボードデータを保持していないことになります。
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);
クリップボードのソースデータとして使う場合、dnd_drop_performed 以降は使いませんが、使用するバージョンに合わせてすべてのハンドラを設定する必要があります。
(使用しないハンドラを NULL に設定することはできない)
send イベント
クリップボードのデータの送信が要求された時に呼ばれます。指定された MIME タイプの形式で、データを送信する必要があります。
渡された fd に対して、write() で書き込みを行って、最後に close() で閉じます。
文字列の場合、最後の NULL 文字は付けずに送信したほうが良いでしょう。
データを受信したクライアント側は、NULL 文字が付いていないと想定してデータを扱う必要があります。
cancelled イベント
wl_data_source のデータがキャンセルされた時に呼ばれます。クリップボードの場合は、クライアントがクリップボードデータを持っている状態で、他のクライアントがクリップボードデータをセットした時に呼ばれます。
このイベント内では、wl_data_source と、それに関連するクリップボードのデータを解放する必要があります。
クリップボードからの貼り付け
クリップボードからデータを取得するには、wl_data_device の selection イベントで渡される、wl_data_offer のポインタが必要です。
ポインタが NULL の場合は、クリップボードデータが存在していないため、取得できません。
なお、データを受信するためには、read() を使って fd から読み込む必要がありますが、今回は、イベントループ内に組み込んで、poll で入力を待って読み込む形にしてあります。
wl_data_offer_receive() を使うと、クリップボードデータを持っている対象に対して、指定した MIME タイプでデータを送信するように要求します。
fd は、書き込み用のファイルディスクリプタを指定します。
なお、mime_type に、wl_data_offer に登録されていない MIME タイプを指定した場合は、Wayland 側で MIME タイプの判定などは行われません。
どんな文字列であろうと、そのまま send イベントが送信されるので、MIME タイプの判定は、データを送信する側が行う必要があります。
今回の場合は、受信するデータを「UTF-8 テキスト」に限定していますが、本来は、wl_data_offer:offer イベント時に、受信可能な MIME タイプを記録しておいて、クリップボードデータの種類を判別する必要があります。
データを受信する場合は、パイプを使って fd を2つ作成し、書き込み用の fd を wl_data_offer_receive() に渡して、すぐに close() で閉じます。
その後、読み込み用の fd を使って、順次データの読み込みを行い、書き込み側で fd が閉じられたら、そこでデータが終了したということなので、読み込みを終了します。
今回は、pipe に O_CLOEXEC フラグを付けるために、pipe2() 関数を使っています。
weston でも GTK+ でもこのフラグは付けられているので、指定しておいた方が良さそうです。
ポインタが NULL の場合は、クリップボードデータが存在していないため、取得できません。
なお、データを受信するためには、read() を使って fd から読み込む必要がありますが、今回は、イベントループ内に組み込んで、poll で入力を待って読み込む形にしてあります。
データを要求する
void wl_data_offer_receive(struct wl_data_offer *wl_data_offer, const char *mime_type, int32_t fd);
wl_data_offer_receive() を使うと、クリップボードデータを持っている対象に対して、指定した MIME タイプでデータを送信するように要求します。
fd は、書き込み用のファイルディスクリプタを指定します。
なお、mime_type に、wl_data_offer に登録されていない MIME タイプを指定した場合は、Wayland 側で MIME タイプの判定などは行われません。
どんな文字列であろうと、そのまま send イベントが送信されるので、MIME タイプの判定は、データを送信する側が行う必要があります。
今回の場合は、受信するデータを「UTF-8 テキスト」に限定していますが、本来は、wl_data_offer:offer イベント時に、受信可能な MIME タイプを記録しておいて、クリップボードデータの種類を判別する必要があります。
データを受信する場合は、パイプを使って fd を2つ作成し、書き込み用の fd を wl_data_offer_receive() に渡して、すぐに close() で閉じます。
その後、読み込み用の fd を使って、順次データの読み込みを行い、書き込み側で fd が閉じられたら、そこでデータが終了したということなので、読み込みを終了します。
今回は、pipe に O_CLOEXEC フラグを付けるために、pipe2() 関数を使っています。
weston でも GTK+ でもこのフラグは付けられているので、指定しておいた方が良さそうです。
イベントループ
今回は、poll() を使って、Wayland のイベントとクリップボードデータの受信を待っていますが、select や epoll を使っても構いません。
クリップボードのデータの場合は、wl_data_offer_receive() の後、wl_display_dispatch() を実行すると、その後すぐに受信することはできますが、D&D データの場合は、イベントを順番通り処理しつつ受信を行っていかないと、正しく処理が行えないので、データの受信はイベントループ内に組み込む必要があります。
wl_display_get_fd() で、Wayland ディスプレイ用の fd を取得できます。
これを使うと、イベントが存在する時に入力がある状態となり、イベントが起こるまで待つことができます。
なお、poll を行う前に、wl_display_flush() でクライアントの要求をサーバーに送信しないと、全くイベントが起こらないので、永遠に待つことになります。
クリップボードのデータの場合は、wl_data_offer_receive() の後、wl_display_dispatch() を実行すると、その後すぐに受信することはできますが、D&D データの場合は、イベントを順番通り処理しつつ受信を行っていかないと、正しく処理が行えないので、データの受信はイベントループ内に組み込む必要があります。
wl_display_get_fd() で、Wayland ディスプレイ用の fd を取得できます。
これを使うと、イベントが存在する時に入力がある状態となり、イベントが起こるまで待つことができます。
なお、poll を行う前に、wl_display_flush() でクライアントの要求をサーバーに送信しないと、全くイベントが起こらないので、永遠に待つことになります。
データの受信
クリップボードデータの受信では、送信側がデータを送信した時 (受信側がデータが読み込めるようになった時) と、送信側で fd が閉じられた時に poll を抜けてくるので、その時にデータの受信と読み込みの終了処理を行います。
データは、recv() で受信します。
POLLHUP が ON の場合は、送信側で fd が閉じられた状態です。
送信側で fd が閉じられても、受信側で fd をクローズしない限り、読み込みはそのまま継続できます。
送信側で fd が閉じられて、かつ読み込むデータがない場合は、データが終了したということになります。
close() で fd を閉じて、受信したデータを処理します。
データは、recv() で受信します。
POLLHUP が ON の場合は、送信側で fd が閉じられた状態です。
送信側で fd が閉じられても、受信側で fd をクローズしない限り、読み込みはそのまま継続できます。
送信側で fd が閉じられて、かつ読み込むデータがない場合は、データが終了したということになります。
close() で fd を閉じて、受信したデータを処理します。