X11: クリップボード (1)

セレクション
クライアント間の通信手段として、X には「セレクション」という概念があります。

セレクションには、所有者 (ウィンドウ) が存在し、一つのセレクションには、常に一つの所有者 (または所有者なし) がいます。

任意のセレクションから、任意のデータを取得したいクライアントは、現在のセレクション所有者に対して、欲しいデータのタイプを示すターゲットを指定して、データを要求します。
データは、ウィンドウのプロパティを介することで、取得できます。

  • 各セレクションは、アトムで識別されます。
  • データを所持するクライアントは、XSetSelectionOwner 関数で、セレクションの所有権を取得します。
  • セレクションからデータが欲しいクライアントは、XConvertSelection 関数で、現在のセレクション所有者に対してデータの変換を要求します。
    その後、現在のセレクション所有者であるクライアントが、SelectionRequest イベントを受け取ります。
  • セレクションの所有者は、SelectionRequest イベントで渡された値を元に、自身が所持しているデータを、要求されたターゲットのデータに変換して、要求者によって指定されたウィンドウの指定プロパティに、データをセットします。
    その後、XSendEvent 関数で、要求者に対して SelectionNotify イベントを送信することで、データがセットされたことを通知します。
  • データの要求者は、SelectionNotify イベントを受け取った時に、XGetWindowProperty 関数を使って、自身のウィンドウのプロパティから、データを読み込みます。

セレクションは、主にクリップボードや D&D で使用します。
クリップボード
クリップボードは、セレクションを使うことで実装することができます。

ICCCM では、PRIMARY, SECONDARY, CLIPBOARD の3つの標準セレクションが定義されているので、それを使います。

PRIMARYマウスの中ボタンによる貼り付けで使われます。
範囲が選択された時は常に PRIMARY に内容をセットし、貼り付ける時は中ボタンを押します。
SECONDARY基本的に使用しません
CLIPBOARD通常のクリップボードとして扱い、ユーザーがコピー/貼り付けを行うことで操作します。

  • コピーコマンドが存在しない場合は PRIMARY のみを使用し、CLIPBOARD は使わない。
  • マウスの中ボタンでは PRIMARY のデータを貼り付ける必要があり、CLIPBOARD は使わない。
  • 切り取り/コピーコマンドは、常に CLIPBOARD に対して、現在選択されているデータを設定する。
  • 切り取り/コピーコマンドは、選択範囲がない場合でも、常に CLIPBOARD と PRIMARY の両方を設定する必要がある。
  • 貼り付けコマンドでは、PRIMARY ではなく、CLIPBOARD のデータを貼り付ける必要がある。
  • 選択範囲が解除された場合、PRIMARY のデータはそのままにすること。
    (以前のデータをそのまま残して、貼り付けられるようにする)
クライアントの終了時
X のクリップボードは、データを所持するクライアントと、データを取得するクライアント間で、直接データをやりとりする形の実装となるため、クリップボードデータを所持しているクライアントが終了した場合、現在のクリップボードの内容は消滅する形になります。

これを解決するためには、クライアントの終了時に、クリップボードを管理しているクライアントにデータを渡して、所有権を引き継いでもらうか、クリップボードの内容を常に監視して収集するようなアプリケーションを使う必要があります。
セレクション関数
セレクションの所有者の変更
void XSetSelectionOwner(Display *display, Atom selection, Window owner, Time time);

指定したセレクションの所有者を、指定ウィンドウに設定します。

selectionセレクションの名前のアトム
owner所有権をセットするウィンドウ。
None で所有者をなしにする。
timeタイムスタンプ or CurrentTime

タイムスタンプが、セレクションの最終変更時刻よりも早い、または現在の X サーバー時刻よりも遅い場合は、何もしません。

新しい所有者 (None 含む) が、現在の所有者と同じではなく、現在の所有者が None でない場合、現在の所有者に対して SelectionClear イベントが送信されます。

所有者のクライアントが終了した、または指定ウィンドウが破棄された場合、所有者は自動的に None になります。
現在の所有者の取得
Window XGetSelectionOwner(Display *display, Atom selection);

指定セレクションの現在の所有者のウィンドウを返します。存在しない場合、None が返ります。
セレクションデータの要求
void XConvertSelection(Display *display, Atom selection,
    Atom target, Atom property, Window requestor, Time time);

指定したセレクションに対して、指定したターゲットへのデータの変換を要求します。

所有者がいる場合、X サーバーはその所有者に SelectionRequest イベントを送信します。
所有者が存在しない場合、X サーバーは、要求元のクライアントに対して、SelectionNotify イベント (property = None) を生成します。

target要求するデータのタイプ
propertyウィンドウのプロパティ。
requestor ウィンドウのこのプロパティに対してデータがセットされる。
None の場合、所有者が任意のプロパティを選択する。
requestor要求者のウィンドウ
timeタイムスタンプ or CurrentTime
セレクションイベント
セレクションイベントに関しては、イベントマスクは必要ありません。常にイベントが送られてきます。
SelectionClear
typedef struct {
 int           type;
 unsigned long serial;
 Bool          send_event;
 Display       *display;
 Window        window;
 Atom          selection;
 Time          time;
} XSelectionClearEvent;

セレクションの所有者が、所有権を失った時に来ます。

window は、所有権を失ったウィンドウ。
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;

セレクションの所有者に対して、XConvertSelection() でデータが要求された時に来ます。

所有者は、requestor ウィンドウの property プロパティに、target で示されたタイプでデータをセットした後、XSendEvent() で、requestor ウィンドウに対して SelectionNotify イベントを送信する必要があります。
(データの変換に失敗した場合は、イベントの property を None に設定します。成功しても失敗しても、常に送信する必要があります)

SelectionRequest の property が None の場合は、データをセットするプロパティを所有者が選択して、送信する SelectionNotify イベントの property に、実際にデータをセットしたプロパティを指定する必要があります。
SelectionNotify
typedef struct {
 int           type;
 unsigned long serial;
 Bool          send_event;
 Display       *display;
 Window        requestor;
 Atom          selection;
 Atom          target;
 Atom          property;
 Time          time;
} XSelectionEvent;

XConvertSelection() 後、そのセレクションのデータ変換が完了した時、または変換に失敗した時に来ます。

セレクションの所有者がいなかった場合や、指定されたターゲットで所有者がデータ変換できなかった場合などは、property = None となり、失敗扱いになります。

property が None でない場合は、ウィンドウのプロパティに、実際にデータがセットされているということなので、requestor ウィンドウの property プロパティから、データを読み込みます。

time は、XConvertSelection() 時に指定されたタイムスタンプが、そのまま渡されます (CurrentTime の場合は 0)。
セレクションのターゲット
セレクションのデータ要求時に指定するターゲットは、要求するデータのタイプを示します。

セレクション所有者は、少なくとも以下のターゲットをサポートしている必要があります (サポートしていなくても動作する場合はあります)。

TARGETS(type = "ATOM", format = 32)
変換に成功する (サポートしている) ターゲットのアトムのリスト。
TARGETS も含む、すべてのリストが含まれること。
MULTIPLE(type = "ATOM_PAIR", format = 32)
複数のターゲットを一度に取得するための、ターゲットとプロパティのアトムのペアリスト。
TIMESTAMP(type = "INTEGER", format = 32)
所有者が、所有権を取得するために使用したタイムスタンプ。
MULTIPLE
MULTIPLE ターゲットは、複数のターゲットを一度に取得したい時に使います。

一度の XConvertSelection() で、複数のプロパティに値を設定することができるので、X サーバーとクライアント間の転送量を減らせる上、同じタイミングで値を取得することができます (複数回データを要求すると、その間に所有者やデータが変わる可能性がある)。

  • データの要求者は、取得したい実際のターゲットと、そのデータをセットするプロパティの、2つのアトムをペアにした配列を用意し、type = "ATOM_PAIR", format = 32 で、任意のプロパティに設定します。
    この時、プロパティの値に None を指定することはできません (所有者が任意のプロパティを選択することはできない)。
  • XConvertSelection() で、target = "MULTIPLE", property = アトムペアを設定したプロパティに指定して、セレクションにデータを要求します。
  • セレクションの所有者は、SelectionRequest イベントを受け取った時、target = "MULTIPLE"、property != None の場合、指定されたプロパティから、ターゲットとプロパティのリストを読み込んで、それぞれのデータを各プロパティに保存します。
  • リスト内の一部のターゲットで変換に失敗した場合、リストが設定されているプロパティ内の、データをセットするプロパティの値が None に置き換えられます。
  • すべてのデータのセットが終わると、要求者に対して SelectionNotify イベントが来るので、各プロパティからデータを読み込みます。
プログラム
左クリックで、MULTIPLE ターゲットを使って、CLIPBOARD セレクションの TARGETS, TIMESTAMP を表示します。
その他のボタンで、CLIPBOARD セレクションから UTF8_STRING 文字列を取得して表示します。

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

$ cc -o run d17-clipb1.c util.c -lX11

<d17-clipb1.c>
#include <stdio.h>
#include <stdlib.h>
#include <X11/Xlib.h>
#include <X11/Xatom.h>
#include "util.h"

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

    _ATOM_NUM
};

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

int main(int argc,char **argv)
{
    Display *disp;
    Window win;
    XEvent ev;
    Atom atoms[_ATOM_NUM],multi[4];
    char *str;
    int size;
    
    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)
                {
                    //MULTIPLE 用のアトムペア
                    multi[0] = multi[1] = atoms[_ATOM_TARGETS];
                    multi[2] = multi[3] = atoms[_ATOM_TIMESTAMP];

                    XChangeProperty(disp, win, atoms[_ATOM_MY_SELECTION],
                        atoms[_ATOM_ATOM_PAIR], 32, PropModeReplace,
                        (unsigned char *)multi, 4);

                    //MULTIPLE を要求
                    XConvertSelection(disp, atoms[_ATOM_CLIPBOARD],
                        atoms[_ATOM_MULTIPLE], atoms[_ATOM_MY_SELECTION],
                        win, ev.xbutton.time);
                }
                else
                {
                    //UTF8_STRING を要求
                    XConvertSelection(disp, atoms[_ATOM_CLIPBOARD],
                        atoms[_ATOM_UTF8_STRING], atoms[_ATOM_MY_SELECTION],
                        win, ev.xbutton.time);
                }

                printf("* XConvertSelection: time(0x%lx)\n", ev.xbutton.time);
                break;

            case SelectionNotify:
                printf("[SelectionNotify] requestor(0x%lx) property(%lu) time(0x%lx)\n",
                    ev.xselection.requestor, ev.xselection.property,
                    ev.xselection.time);

                if(ev.xselection.property == None) break;

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

                    put_prop_atoms(win, atoms[_ATOM_TARGETS], "TARGETS");
                    put_prop_num(win, atoms[_ATOM_TIMESTAMP], XA_INTEGER, "TIMESTAMP");

                    XDeleteProperty(disp, win, atoms[_ATOM_TARGETS]);
                    XDeleteProperty(disp, win, atoms[_ATOM_TIMESTAMP]);
                    XDeleteProperty(disp, win, atoms[_ATOM_MY_SELECTION]);
                }
                else if(ev.xselection.target == atoms[_ATOM_UTF8_STRING])
                {
                    //UTF8_STRING

                    str = read_prop8(win, atoms[_ATOM_MY_SELECTION], &size);
                    if(str)
                    {
                        printf("--- UTF8_STRING (size:%d) ---\n%s\n-----------\n", size, str);
                        free(str);
                    }

                    XDeleteProperty(disp, win, atoms[_ATOM_MY_SELECTION]);
                }
                break;
        }
    }

    XCloseDisplay(disp);

    return 0;
}
解説
MULTIPLE ターゲット
* XConvertSelection: time(0x11d21c5)
[SelectionNotify] requestor(0x1c00002) property(665) time(0x11d21c5)
<TARGETS> (6) TIMESTAMP, TARGETS, MULTIPLE, SAVE_TARGETS, UTF8_STRING, STRING, 
<TIMESTAMP> 18493619,

CLIPBOARD セレクションに所有者がいる状態で、MULTIPLE ターゲットを要求した場合、上記のようになります。
UTF8_STRING, STRING のターゲットがあるので、クリップボードに文字列が設定されていることになります。

<TARGETS> (14) TIMESTAMP, TARGETS, MULTIPLE, SAVE_TARGETS, text/html, text/_moz_htmlcontext,
 text/_moz_htmlinfo, UTF8_STRING, COMPOUND_TEXT, TEXT, STRING, text/plain;charset=utf-8,
 text/plain, text/x-moz-url-priv,

なお、FireFox で HTML ページのテキストをコピーしてみると、上記のようになります。
通常の文字列に加えて、各 MIME タイプでデータを取得することができます。

<TARGETS> (8) x-special/gnome-copied-files, text/uri-list, text/x-moz-url,
 text/plain, TARGETS, MULTIPLE, TIMESTAMP, SAVE_TARGETS,

ファイラでファイルをコピーしてみると、上記のようになります。

他にも、色々なものをコピーして、確認してみてくだだい。