X11: クリップボード (2) - 貼り付け

貼り付け
ユーザーによって「貼り付け」コマンドが実行されたことにより、クリップボードから読み込んだテキストなどを貼り付けたい場合、まずは、CLIPBOARD セレクションに TARGETS ターゲットを要求します。

SelectionNotify イベントが来たら、プロパティにアトムのリストがセットされているので、その中に、必要なデータのタイプが存在するかを確認します。

もしくは、必要なデータタイプのターゲットを直接指定し、変換できなかったら、他の形式を試すという方法もあります。
注意点
TARGETS を読み込んだ後、実際のデータを要求するまでの間に、他のクライアントによって、セレクション所有者 (またはクリップボードのデータ) が変更される可能性が 0 ではないので、注意してください。
テキストデータ
テキストを貼り付けたい場合は、以下のターゲットが使用できます。

UTF8_STRINGUTF-8 文字列
STRINGISO8859-1 によって定義された文字セットと、タブと改行 (0x0a)
COMPOUND_TEXT複数のエンコーディングに対応した文字列
TEXTセレクション所有者が、変換時にエンコーディングを選択する。
プロパティを読み込んだ時に返されるタイプで判別する。
(主に STRING か COMPOUND_TEXT)

ただし、クリップボードデータを所有しているアプリケーションによって、対応している形式は異なるため、優先順位を付けた上で、上記のいずれかで読み込めるようにしてください。

基本的には、UTF8_STRING > COMPOUND_TEXT > TEXT > STRING の順で選択すれば良いと思います。

アプリケーションによっては、「text/plain」「text/plain;charset=utf-8」などの MIME タイプにも対応しています。
変換の要求
必要なデータのターゲットを選択できたら、XConvertSelection() で、そのターゲットをセレクションに要求します。

X ウィンドウ
データを受け取るためには、X ウィンドウが必要です。
表示されているウィンドウでなくても良いので、例えば、グループリーダーのウィンドウなどを使っても構いません。

ウィンドウマネージャによって管理されているウィンドウの場合、デスクトップに関する色々なプロパティが設定されるため、プロパティの重複を回避するためにも、マップされていないウィンドウを使うのは良い方法です。

プロパティ
データは、ウィンドウのプロパティに設定されます。
データを受け取る側で、そのウィンドウに存在しない、適当な名前のプロパティを指定する必要があります。
SelectionNotify イベント
データの要求後、セレクションの所有者がいなかった場合や、変換に失敗した場合は、SelectionNotify イベントの property が None になっています。
この場合は、データの取得に失敗したものとして扱ってください。

property が None でない場合は、ターゲットデータへの変換に成功し、プロパティにデータがセットされている状態になっています。
XGetWindowProperty() で、requestor ウィンドウの property プロパティからデータを読み込みます。

データを読み込んだ後は、そのプロパティを削除してください。
XDeleteProperty(disp, window, property) で削除できます。
SelectionNotify イベントが来るまで待つ
例えば、Ctrl+V で貼り付けを行う場合、V キーが押されて ControlMask が ON の時に XConvertSelection() を実行しますが、キーが押されたその瞬間にデータを取得できるわけではないので、Ctrl+V が押されてから SelectionNotify イベントを受け取るまでの間に、何かしらのイベントがあった場合、貼り付けるカーソル位置が移動したりするようなことがあるかもしれません。

そのため、基本的には、貼り付けコマンドが実行された時点で、データを取得する必要があります。

そのためには、XConvertSelection() 後、所有者または X サーバーによって SelectionNotify イベントが生成されるまで、待つ必要があります。

ConnectionNumber(display) マクロで、X サーバーとの接続が行われているファイルディスクリプタを取得することができるので、そのファイルディスクリプタを使って、poll() で X サーバーから受信が来るまでブロックすることができます。

ファイルディスクリプタが読み込み可能になった場合は、サーバーからイベントが送られてきたということなので、poll() を抜けた後、XCheckTypedEvent() などでイベントキューを検索し、SelectionNotify イベントが来ているかどうかを確認します。

イベントが来た場合は、そのイベントを読み込んで、ループを終了し、プロパティからデータを読み込んで、そのデータを貼り付けます。

なお、SelectionNotify イベントを待つループを永遠に行うことはできないので、一定時間経ってもイベントが来なかった場合 (所有者がクラッシュした場合など) は、失敗したものとして扱う必要があります。

データの変換に思いの外時間が掛かって、ループ終了後に SelectionNotify イベントが来るような場合もあるので、注意してください。
プログラム
左ボタンで、クリップボードから、UTF8_STRING, COMPOUND_TEXT, STRING, TEXT のぞれぞれのターゲットを取得します。
左ボタンが押された時に、poll() を行ってイベントを待っています。

閉じるボタンで終了します。

※タイムアウト時間を取得するために、clock_gettime() を使っています。Linux 以外では使えない場合があるので、注意してください。

$ cc -o run d18-clipb2.c util.c -lX11

<d18-clipb2.c>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <poll.h>
#include <errno.h>
#include <X11/Xlib.h>
#include <X11/Xatom.h>
#include <X11/Xutil.h>
#include "util.h"

enum
{
    _ATOM_CLIPBOARD,
    _ATOM_MY_SELECTION,
    _ATOM_TARGETS,
    _ATOM_UTF8_STRING,
    _ATOM_COMPOUND_TEXT,
    _ATOM_TEXT,

    _ATOM_NUM
};

const char *g_names[] = {
    "CLIPBOARD", "_MY_SELECTION_", "TARGETS", "UTF8_STRING",
    "COMPOUND_TEXT", "TEXT"
};

Atom atoms[_ATOM_NUM];

/* t1 - t2 の ms 秒を取得 */

static long _get_diff_time(struct timespec *t1,struct timespec *t2)
{
    long sec,ns;

    sec = t1->tv_sec - t2->tv_sec;
    
    if(t1->tv_nsec >= t2->tv_nsec)
        ns = t1->tv_nsec - t2->tv_nsec;
    else
    {
        sec--;
        ns = 1000 * 1000 * 1000 - t2->tv_nsec + t1->tv_nsec;
    }

    return sec * (1000 * 1000) + ns / 1000;
}

/* SelectionNotify イベントを受信 */

static int _recv_selection_notify(Display *disp,XEvent *ev)
{
    struct pollfd fds;
    struct timespec ts,tsend;
    int ret;
    long ms;

    fds.fd = ConnectionNumber(disp);
    fds.events = POLLIN;

    //タイムアウト終了時間 (+1s)

    clock_gettime(CLOCK_MONOTONIC, &tsend);

    tsend.tv_sec += 1;

    //

    while(1)
    {
        //キュー内にあるか (削除 & ブロックなし)

        if(XCheckTypedEvent(disp, SelectionNotify, ev))
            break;

        //タイムアウトまでの時間

        clock_gettime(CLOCK_MONOTONIC, &ts);

        ms = _get_diff_time(&tsend, &ts);
        if(ms < 0) return 1;

        //イベントを受信するまで待つ

        ret = poll(&fds, 1, ms);

        if(ret == 0)
            return 1; //タイムアウト
        else if(ret == -1)
            return 1; //エラー
    }

    return 0;
}

/* 指定ターゲットでテキスト読み込み */

static int _read_text(Display *disp,Window win,Atom target,const char *target_name)
{
    XEvent ev;
    XTextProperty tp;
    Atom type;
    char *buf,**list,*name;
    int size,num;

    printf("--- [%s] ---\n", target_name);

    XConvertSelection(disp, atoms[_ATOM_CLIPBOARD],
        target, atoms[_ATOM_MY_SELECTION], win, CurrentTime);

    if(_recv_selection_notify(disp, &ev))
    {
        printf("<no SelectionNotify>\n");
        return 1;
    }

    if(ev.xselection.property == None)
    {
        printf("<failed>\n");
        return 1;
    }

    //プロパティ読み込み

    buf = (char *)read_prop8(win, atoms[_ATOM_MY_SELECTION], &type, &size);

    XDeleteProperty(disp, win, atoms[_ATOM_MY_SELECTION]);
    
    if(!buf)
    {
        printf("<no property>\n");
        return 1;
    }

    //各タイプごと

    if(type == atoms[_ATOM_COMPOUND_TEXT])
    {
        //COMPOUND_TEXT

        tp.value = (unsigned char *)buf;
        tp.encoding = type;
        tp.format = 8;
        tp.nitems = size;

        if(XmbTextPropertyToTextList(disp, &tp, &list, &num) == Success)
        {
            printf("<COMPOUND_TEXT>\n%s\n", *list);
            XFreeStringList(list);
        }
    }
    else if(type == XA_STRING)
    {
        printf("<STRING>\n%s\n", buf);
    }
    else if(type == atoms[_ATOM_UTF8_STRING])
    {
        printf("<UTF8_STRING>\n%s\n", buf);
    }
    else
    {
        name = XGetAtomName(disp, type);
        if(name)
        {
            printf("<%s>\n%s\n", name, buf);
            XFree(name);
        }
    }

    free(buf);

    return 0;
}

int main(int argc,char **argv)
{
    Display *disp;
    Window win;
    XEvent ev;
    
    disp = XOpenDisplay(NULL);
    if(!disp) return 1;

    set_display(disp);
    init_locale();

    XInternAtoms(disp, (char **)g_names, _ATOM_NUM, False, atoms);

    win = create_test_window2(disp, 200, 200, 0, ButtonPress);

    //イベント

    XMapWindow(disp, win);

    while(1)
    {
        XNextEvent(disp, &ev);

        if(event_quit(&ev)) break;

        switch(ev.type)
        {
            case ButtonPress:
                if(ev.xbutton.button == Button1)
                {
                    _read_text(disp, win, atoms[_ATOM_UTF8_STRING], "UTF8_STRING");
                    _read_text(disp, win, atoms[_ATOM_COMPOUND_TEXT], "COMPOUND_TEXT");
                    _read_text(disp, win, XA_STRING, "STRING");
                    _read_text(disp, win, atoms[_ATOM_TEXT], "TEXT");
                }
                break;
        }
    }

    XCloseDisplay(disp);

    return 0;
}
解説
TARGETS に UTF8_STRING, STRING が含まれる
--- [UTF8_STRING] ---
<UTF8_STRING>
イベントを受信する
--- [COMPOUND_TEXT] ---
<failed>
--- [STRING] ---
<STRING>
イベントを受信する
--- [TEXT] ---
<failed>

TARGETS に UTF8_STRING, STRING が含まれている場合に実行した時は、上記のようになります (Geany でテキストをコピー)。

COMPOUND_TEXT, TEXT は property = None になるので、変換に失敗しています。

ここでは、STRING が UTF-8 文字列になっていることに注意してください。
本来はラテン文字しか含まれていないはずですが、アプリによっては、厳格なエンコーディングになっていない可能性があります。
UTF8_STRING, COMPOUND_TEXT, TEXT, STRING が含まれる
--- [UTF8_STRING] ---
<UTF8_STRING>
SelectionNotify イベント
--- [COMPOUND_TEXT] ---
<COMPOUND_TEXT>
SelectionNotify イベント
--- [STRING] ---
<STRING>
SelectionNotify \u30a4\u30d9\u30f3\u30c8
--- [TEXT] ---
<COMPOUND_TEXT>
SelectionNotify イベント

FireFox でテキストをコピーした場合、上記のようになります。

TEXT ターゲットは、実際には COMPOUND_TEXT タイプに変換されています。

STRING の場合、日本語が \uXXXX で表記されています。これは、Unicode による1文字表現となります (UTF-16)。
ちなみに、U+FFFF を超える文字については、\UXXXXXXXX となります (UTF-32)。
ファイラでファイルをコピー
--- [UTF8_STRING] ---
<UTF8_STRING>
file:///usr/include/ar.h
--- [COMPOUND_TEXT] ---
<no property>
--- [STRING] ---
<STRING>
file:///usr/include/ar.h
--- [TEXT] ---
<TEXT>
file:///usr/include/ar.h

pcmanfm-qt でファイルをコピーした場合、上記のようになります。

TARGETS には、これらのターゲットが含まれていないにもかかわらず、変換できています。
クリップボードは基本的にテキストデータとして扱うことが多いので、これらのターゲットは常に対応している場合もあるようです。

COMPOUND_TEXT は、property != None となりますが、実際にはプロパティにデータがセットされていません。
TEXT では、プロパティ上の実際のタイプは TEXT になっています。
まとめ
クリップボードデータを所有しているクライアントが、どのようにデータを変換するかは、各アプリケーションによって異なる場合があります。

テキストであれば、基本的に UTF8_STRING ターゲットを使えば問題ありませんが、古いアプリケーションでは対応していない可能性もあります。
STRING にも対応しているアプリは多いですが、実際のエンコーディングがどうなっているかは不透明です。
COMPOUND_TEXT と TEXT は、対応していない場合もあります。

所有者が対応していないターゲットの場合、property = None で失敗する場合もあれば、実際にはプロパティにデータがセットされていない場合もあるので、データを受け取る側は、柔軟に対応する必要があります。