X11: クリップボード (4) - 管理

クリップボードの収集
クリップボードの内容を常に収集して保存するようなアプリケーションを作りたい場合、以下の方法があります。

  • セレクション所有者が変更されたら、データを要求して保存した後、自身が所有者になる。
    以降、データの要求はこのアプリケーションで処理する。
  • XFIXES 拡張機能で、セレクション所有者が変更された時のイベントを受信し、クリップボードデータが変更されたら、常にデータを要求して保存する。
    セレクション所有者は変更されないので、クリップボードは引き続き元のクライアントが管理する。

前者は、XFIXES 拡張機能が実装される前の古いやり方です。
所有者のクライアントが終了しても、データを維持できるのが利点ですが、この場合は、特定のターゲットしかサポートされなくなるので、元のセレクション所有者による自由なターゲット変換が行えなくなります。

XFIXES 拡張機能を使う場合、所有者は変更されないので、所有者による自由なターゲット変換が行えます。
拡張機能が使える場合は、こちらを使った方が良いでしょう。
プログラム
XFIXES 拡張機能で、CLIPBOARD セレクションの所有者が変わった時のイベントを受け取ります。
閉じるボタンで終了します。

<X11/extensions/Xfixes.h> のインクルードと、-lXfixes のリンクが必要です。

$ cc -o run d20-xfixes.c util.c -lX11 -lXfixes

<d20-xfixes.c>
#include <stdio.h>
#include <X11/Xlib.h>
#include <X11/extensions/Xfixes.h>
#include "util.h"

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

    set_display(disp);

    if(!XFixesQueryExtension(disp, &event_base, &error_base))
    {
        printf("unsupported XFIXES\n");
        XCloseDisplay(disp);
        return 1;
    }

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

    //イベント選択

    XFixesSelectSelectionInput(disp, win,
        GET_ATOM("CLIPBOARD"),
        XFixesSetSelectionOwnerNotifyMask
            | XFixesSelectionWindowDestroyNotifyMask
            | XFixesSelectionClientCloseNotifyMask);

    //イベント

    XMapWindow(disp, win);

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

        if(event_quit(&ev)) break;

        if(ev.type == event_base + XFixesSelectionNotify)
        {
            pev = (XFixesSelectionNotifyEvent *)&ev;
            
            printf("[XFixesSelectionNotify] subtype(%d) owner(0x%lx) "
                "timestamp(0x%lx) selection_timestamp(0x%lx)\n",
                pev->subtype, pev->owner, pev->timestamp,
                pev->selection_timestamp);
        }
    }

    XCloseDisplay(disp);

    return 0;
}

クリップボードにコピーした時、イベントが来るのを確認してください。
所有者ウィンドウの破棄や、そのクライアントの終了時にもイベントが来ます。

XFIXES 拡張機能には、コアプロトコルで実現できない、ちょっとした機能が集まっています。
セレクション関連以外にも、いくつか機能があります。
関数
Bool XFixesQueryExtension(Display *dpy, int *event_base_return, int *error_base_return);

void XFixesSelectSelectionInput(Display *dpy, Window win, Atom selection, unsigned long eventMask);

XFixesQueryExtension は、XFIXES 拡張機能がサポートされている場合、True が返ります。
引数にはそれぞれ、拡張機能のイベントタイプのベース値と、エラーコードのベース値が返ります。

XFixesSelectSelectionInput は、ウィンドウに対して、セレクション関連のイベントマスクを選択します。
selection は、対象のセレクションを指定します。
eventMask は、以下の値です。

XFixesSetSelectionOwnerNotifyMaskXSetSelectionOwner() で所有者が変化した時。
subtype = XFixesSetSelectionOwnerNotify
XFixesSelectionWindowDestroyNotifyMask所有者ウィンドウが破棄された時。
subtype = XFixesSelectionWindowDestroyNotify
XFixesSelectionClientCloseNotifyMask所有者のクライアントが閉じられた時。
subtype = XFixesSelectionClientCloseNotify
XFixesSelectionNotify イベント
typedef struct {
    int type;
    unsigned long serial;
    Bool send_event;
    Display *display;
    Window window;
    int subtype;
    Window owner;
    Atom selection;
    Time timestamp;
    Time selection_timestamp;
} XFixesSelectionNotifyEvent;

実際のイベントタイプは、XFixesQueryExtension() で取得した event_base + XFixesSelectionNotify です。

subtype何が原因で所有者が変更されたか。
イベントマスクの表をご覧ください。
owner現在の所有者。または None
timestampイベントが生成された時間
selection_timestampXSetSelectionOwner() 時に渡されたタイムスタンプ。
ウィンドウ破棄や閉じられた時は 0。
クリップボードマネージャ
クリップボード所有者のクライアントが終了する時、「クリップボードマネージャ」にデータを引き渡すことで、クライアントが終了した後も、引き続き同じクリップボードデータを貼り付けることができるようになります。

ここで言うところのクリップボードマネージャは、クリップボードデータを収集するような機能があるわけではなく、単に破棄されようとしているクリップボードデータを引き受ける役目を持っています。

※クリップボードマネージャと謳われているようなアプリは、基本的に、クリップボードデータを常に収集したりするような機能があるだけで、以下で説明しているような、CLIPBOARD_MANAGER セレクションを実装しているものではありません。
CLIPBOARD_MANAGER セレクション
クリップボードマネージャは、CLIPBOARD_MANAGER セレクションの所有権を持っています。

XGetSelectionOwner() で CLIPBOARD_MANAGER の所有者を取得した時、戻り値が None でなければ、クリップボードマネージャが存在します。

クリップボードマネージャは、SAVE_TARGETS ターゲットの変換に対応している必要があります。
SAVE_TARGETS ターゲット
終了時に、クリップボードデータをウィンドウマネージャに引き渡す CLIPBOARD 所有者は、TARGETS ターゲットが要求された時に、SAVE_TARGETS をリストに含める必要があります。
これは、CLIPBOARD_MANAGER によって、ただの目印として使われます。
手順
クリップボード所有者のクライアントが終了する時、以下の手順で、データをクリップボードマネージャに引き渡します。

クリップボードマネージャが存在するか
XGetSelectionOwner() で CLIPBOARD_MANAGER セレクションの所有者がいるかどうかを確認し、所有者がいない場合は、何もせずに終了します。

SAVE_TARGETS ターゲットを要求
XConvertSelection() で、CLIPBOARD_MANAGER セレクションに対して、SAVE_TARGETS ターゲットの変換を要求します。

ただし、その前に、データの保存先のプロパティに対して、クリップボードマネージャに保存したい各ターゲットのアトムのリストをセットします (type = "ATOM", format = 32)。
文字列なら、UTF8_STRING や STRING です。CLIPBOARD 所有者が変換可能なターゲットである必要があります。

XConvertSelection() 時に property 引数を None にした場合は、クリップボードマネージャが CLIPBOARD セレクションに対して TARGETS を要求し、不要なターゲットを除外した上で、現在の所有者でサポートされているターゲットが選択されます。

TARGET_SIZES ターゲット
CLIPBOARD 所有者が、TARGETS で TARGET_SIZES ターゲットをサポートしている場合、クリップボードマネージャは、CLIPBOARD セレクションに対して TARGET_SIZES ターゲットを要求します。

CLIPBOARD 所有者は、type = "ATOM", format = 32 のデータをプロパティにセットします。
2つの値をペアにします。最初の値はターゲットのアトムで、2番目の値は整数です。

データを保存する各ターゲットと、そのデータの変換後のバイトサイズを指定します。
副作用ターゲット (実際のデータはなく、type = "NULL" で長さ 0 のプロパティがセットされる) の場合は -1、変換後のサイズが決められない場合は 0 を指定します。

これにより、クリップボードマネージャが大きなサイズのデータを保存したくない場合、そのターゲットの保存を拒否するか、ユーザーに対して、保存の可否を確認することがあります。

データの変換
その後、クリップボードマネージャは、CLIPBOARD セレクションに対して、SAVE_TARGETS で取得したそれぞれのターゲットのデータ変換を要求します。
これにより、クリップボードマネージャで、各ターゲットのデータが保存されます。

その後、クリップボードマネージャーが CLIPBOARD セレクションの所有権を取得します。
以降は、クリップボードマネージャがクリップボードデータを所持する形になります。

元のクリップボード所有者が SelectionNotify イベントを受け取った時、データはすべて変換されたということになるので、その後、クライアントを終了させることができます。
(データの転送が終わらないうちに終了してはいけません)

※クリップボードマネージャは、各ターゲットタイプごとにデータを保持するため、柔軟なデータ変換は行えません。
文字列 (UTF8_STRING) など、一般的なデータを保存する時だけ使用してください。