X11: クリップボード (3) - コピー

クリップボードにコピー
クリップボードにデータをコピーするということは、クライアントのウィンドウが、セレクションの所有者になるということです。

クリップボードのデータは、セレクションの所有権を持っている間、その所有者のクライアントが保持します。

他のクライアントによってデータが要求されて、SelectionRequest イベントが来るたびに、保持しているデータを、要求されたターゲットに変換して、それを、指定されたウィンドウの指定されたプロパティにセットします。

その後、XSendEvent() で、要求者に対して SelectionNotify イベントを送信すれば、データがセットされたことを要求者に通知できます。
注意点
  • クリップボードデータが変更されるたびに、XSetSelectionOwner() でセレクションの所有権を取得する必要があります。
    (すでに自身が所有者であっても、毎回所有権を取得する必要があります。これは、クリップボードの内容が変更されたかどうかという判断を行うために必要です)
  • 自身が所有者である時にデータを要求した場合、自分のクライアントに対して SelectionRequest イベントが来ます。
    自身が所有者になっている場合は、XConvertSelection() を行わずに、保持しているデータを直接使って、貼り付けることもできます。
所有者のウィンドウ
セレクションの所有権を取得するためには、X ウィンドウが必要になります。

通常のウィンドウである必要はないので、グループリーダーのような、非表示ウィンドウを使うことができます。

特に、クライアントが複数のウィンドウを管理する場合、ダイアログなどの一時的なウィンドウ上でコピーを実行して、そのウィンドウで所有権を取得した場合、ウィンドウが破棄されると、セレクションの所有者が自動で「なし」になるので、結果として、クリップボードデータが破棄される形になってしまいます。

セレクションの所有権を取得する場合は、グループリーダーウィンドウなど、アプリケーションに対して1つ作成されている、共通のウィンドウを使用した方が良いでしょう。
所有権を失った時
他のクライアントが XSetSelectionOwner() を実行して所有権を取得したり、現在の所有者のウィンドウが破棄されたり、所有者ウィンドウを作成したクライアントが終了した場合は、セレクションの所有権が他のクライアントに移るか、「なし」になります。

その場合、以前の所有者に対して、SelectionClear イベントが来ます。
このイベントが来た時は、セレクションの所有権を失ったということなので、保持しているクリップボードデータを破棄する (解放する) 必要があります。

※XSetSelectionOwner() 時、すでに同じウィンドウが所有者になっている場合は、SelectionClear イベントは来ません。
よって、クリップボードデータが変更された時に、自身で再度所有権を取得する場合、SelectionClear イベントで現在のクリップボードデータが破棄されるようなことは起こりません。
SelectionRequest イベント
セレクション所有者に対して、データ変換の要求が来た時、SelectionRequest イベントが来ます。

typedef struct {
 int           type;
 unsigned long serial;
 Bool          send_event;
 Display       *display;
 Window        owner;
 Window        requestor;
 Atom          selection;
 Atom          target;
 Atom          property;
 Time          time;
} XSelectionRequestEvent;
property
XSelectionRequestEvent の property が None の場合、XConvertSelection() で property が None に指定されています。

この場合、所有者が、データを保存するプロパティを、任意のものに選択する必要があります。
面倒であれば、target と同じアトムでも構いません。
プロパティにデータをセット
自身が所有しているクリップボードデータを、target で指定されたタイプで、requestor ウィンドウの property プロパティに、XChangeProperty() を使ってセットします。

target が TARGETS, TIMESTAMP, MULTIPLE などの場合、それぞれに対応する各データをセットします。

TARGETS(type = "ATOM", format = 32)
変換に成功する (サポートしている) ターゲットのアトムのリスト。
MULTIPLE(type = "ATOM_PAIR", format = 32)
property から、ターゲットとプロパティのアトムのリストを読み込んで、各プロパティに指定ターゲットのデータをセットする。
個別に失敗した場合は、リスト内のプロパティの値を None にする。
TIMESTAMP(type = "INTEGER", format = 32)
所有者が、所有権を取得するために使用したタイムスタンプ。
SelectionNotify イベントを送信
SelectionRequest イベントが来た場合、データの変換に成功しても失敗しても、必ず要求元に SelectionNotify イベントを送信する必要があります。
MULTIPLE の場合は、すべてのデータをセットした後に、1度だけ実行します。

SelectionNotify イベントの各値は、基本的に SelectionRequest イベントの値をそのままコピーしますが、property に関しては、変換に成功した場合は実際にデータをセットしたプロパティを、失敗した場合は None をセットします。
(serial, send_event, display の値は XSendEvent 関数によってセットされるので、値は設定しなくて構いません)

XSendEvent 関数の引数は、以下のようになります。

XSendEvent(disp, ev.xselectionrequest.requestor, False, 0, &event);

送信先ウィンドウには要求者のウィンドウを指定し、event_mask は 0 にします。
event_mask が 0 の場合、送信先ウィンドウを作成したクライアントにイベントが送られます。
プログラム
左ボタンで、CLIPBOARD セレクションの所有権を取得します (コピーコマンド扱い)。
他のクライアントで貼り付けを行った場合、UTF8_STRING ターゲットにより、"テストABCDEFG_" のテキストが貼り付けられます。

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

<d19-clipb3.c>
#include <stdio.h>
#include <string.h>
#include <X11/Xlib.h>
#include <X11/Xatom.h>
#include "util.h"

enum
{
    _ATOM_CLIPBOARD,
    _ATOM_MY_SELECTION,
    _ATOM_ATOM_PAIR,
    _ATOM_TARGETS,
    _ATOM_TIMESTAMP,
    _ATOM_MULTIPLE,
    _ATOM_UTF8_STRING,

    _ATOM_NUM
};

const char *g_names[] = {
    "CLIPBOARD", "_MY_SELECTION_", "ATOM_PAIR",
    "TARGETS", "TIMESTAMP", "MULTIPLE", "UTF8_STRING"
};

Atom atoms[_ATOM_NUM];
Time g_timestamp = 0;

const char *copy_string = "テストABCDEFG_";

/* 各ターゲットをセット */

static int _set_property(Window requestor,Atom property,Atom target)
{
    if(target == atoms[_ATOM_TARGETS])
    {
        //TARGETS
        //(TARGETS, TIMESTAMP, MULTIPLE, UTF8_STRING)
        
        XChangeProperty(g_disp.disp, requestor, property,
            XA_ATOM, 32,
            PropModeReplace, (unsigned char *)&atoms[_ATOM_TARGETS], 4);

        return 0;
    }
    else if(target == atoms[_ATOM_TIMESTAMP])
    {
        //TIMESTAMP
        
        XChangeProperty(g_disp.disp, requestor, property,
            XA_INTEGER, 32,
            PropModeReplace, (unsigned char *)&g_timestamp, 1);

        return 0;
    }
    else if(target == atoms[_ATOM_UTF8_STRING])
    {
        //UTF8_STRING
        
        XChangeProperty(g_disp.disp, requestor, property,
            atoms[_ATOM_UTF8_STRING], 8,
            PropModeReplace, (unsigned char *)copy_string, strlen(copy_string));

        return 0;
    }

    return 1;
}

/* MULTIPLE */

static int _set_multiple(Window requestor,Atom property)
{
    Atom *buf,*ps;
    int i,num;

    buf = (Atom *)read_prop32(requestor, property,
        atoms[_ATOM_ATOM_PAIR], &num);
    if(!buf) return 1;

    ps = buf;

    for(i = 0; i < num / 2; i++, ps += 2)
        _set_property(requestor, ps[1], ps[0]);

    XFree(buf);

    return 0;
}

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

    set_display(disp);

    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)
                {
                    g_timestamp = ev.xbutton.time;
                    
                    XSetSelectionOwner(disp, atoms[_ATOM_CLIPBOARD], win, g_timestamp);
                }
                break;
            case SelectionClear:
                printf("[SelectionClear] time(0x%lx)\n", ev.xselectionclear.time);
                break;
            case SelectionRequest:
                printf("[SelectionRequest] requestor(0x%lx) time(0x%lx) target(",
                    ev.xselectionrequest.requestor, ev.xselectionrequest.time);

                put_atom_name(ev.xselectionrequest.target);
                printf(")\n");

                //所有者がプロパティを選択
                if(ev.xselectionrequest.property == None)
                    ev.xselectionrequest.property = ev.xselectionrequest.target;

                if(ev.xselectionrequest.target == atoms[_ATOM_MULTIPLE])
                {
                    //MULTIPLE

                    if(_set_multiple(ev.xselectionrequest.requestor,
                        ev.xselectionrequest.property))
                        ev.xselectionrequest.property = None;
                }
                else
                {
                    if(_set_property(ev.xselectionrequest.requestor,
                        ev.xselectionrequest.property,
                        ev.xselectionrequest.target))
                    {
                        ev.xselectionrequest.property = None;
                    }
                }

                //SelectionNotify を送る

                evs.type = SelectionNotify;
                evs.requestor = ev.xselectionrequest.requestor;
                evs.selection = ev.xselectionrequest.selection;
                evs.target = ev.xselectionrequest.target;
                evs.property = ev.xselectionrequest.property;
                evs.time = ev.xselectionrequest.time;

                XSendEvent(disp, evs.requestor, False, 0, (XEvent *)&evs);
                break;
        }
    }

    XCloseDisplay(disp);

    return 0;
}
解説
所有権取得時
左ボタンで所有権を取得した瞬間、以下のように、どこかのクライアントから TARGETS や UTF8_STRING が要求されています (環境によります)。

[SelectionRequest] requestor(0x1600081) time(0x139e5d9) target(TARGETS)
[SelectionRequest] requestor(0x600000) time(0x0) target(UTF8_STRING)

xprop コマンドで、各ウィンドウのプロパティを調べてみると、以下のようになっています。

$ xprop -id 0x1600081
WM_NAME(STRING) = "Qt Clipboard Requestor Window"
_NET_WM_NAME(UTF8_STRING) = "Qt Clipboard Requestor Window"

$ xprop -id 0x600000
FCITX_X11_SEL_PRIMARY(UTF8_STRING) = "0x600000"
FCITX_X11_SEL_CLIPBOARD(UTF8_STRING) = "0x600000

どうやら、Qt がクリップボードのデータを要求するためのウィンドウと、fcitx 関連のウィンドウのようです。

fcitx の「クリップボード」アドオンは、クリップボードを常に監視して文字列を収集しているため、他のクライアントが所有権を取得すると (クリップボードデータの内容が変更されると)、すぐに UTF8_STRING を要求して、データが取得されています。
XFIXES 拡張機能
XFIXES 拡張機能を使うと、セレクションの所有者が変更された時に、イベントを受け取ることができます。

クリップボードを監視したいアプリケーションは、この機能を使って、クリップボードデータが変更された時にデータを収集するような処理を行うことができます。
貼り付けを確認
プログラムウィンドウが所有権を取得している状態で、テキストエディタなどで貼り付けを行った場合、"テストABCDEFG_" の文字列が貼り付けられるのを確認してください。

[SelectionRequest] requestor(0xe00039) time(0x41e5f5) target(TARGETS)
[SelectionRequest] requestor(0xe00039) time(0x41e5f5) target(text/plain;charset=utf-8)
[SelectionRequest] requestor(0xe00039) time(0x41e5f5) target(UTF8_STRING)

このような形で、複数のターゲットが要求されます。
所有権を失った時
他のクライアントがコピーを行った場合、CLIPBOARD の所有権が変更されるため、プログラムのクライアントに SelectionClear イベントが来ます。
まとめ
プログラムでは一応 MULTIPLE ターゲットも処理していますが、基本的にはあまり使われることはありません。

テキストデータが要求される場合は、最初に TARGETS が要求された後に、UTF8_STRING や "text/plain;charset=utf-8" が要求されるか、TARGETS は要求されずに、直接 UTF8_STRING などが要求される場合がほとんどです。

TIMESTAMP と MULTIPLE ターゲットをサポートしなくても、実際は動作する場合が多いです。