JavaFX の Clipboard API を使う

# 概要
Java には [java.awt.datatransfer.Clipboard](https://docs.oracle.com/javase/jp/8/docs/api/java/awt/datatransfer/Clipboard.html) という、クリップボードを操作する用のクラスが用意されています。詳細はひしだまさんが書いていらっしゃいますので、そちらの記事をお読みください。

[ひしだま's 技術メモページ - クリップボード](http://www.ne.jp/asahi/hishidama/home/tech/java/clipboard.html)

私は JavaFX に入っている方の Clipboard API である [javafx.scene.input.Clipboard](https://docs.oracle.com/javase/jp/8/javafx/api/javafx/scene/input/Clipboard.html) を調べることにしました。

## 実行環境

| Java SE | 1.8.0_u131
|:---|:---
| OS | Windows 10
| IDE | IntelliJ IDEA 2017.1.3

----

# [javafx.scene.input.Clipboard](https://docs.oracle.com/javase/jp/8/javafx/api/javafx/scene/input/Clipboard.html)

> 切取り、コピー、貼付けなどの操作中にデータを配置できる、オペレーティング・システムのクリップボードを表します。 

まあ、クリップボード関連のあれこれができるクラスということでしょう。

----

# Let's try!
試しにコードを書いてみます。

```java:実行すると例外を出して終了するコード
import javafx.scene.input.Clipboard;

import java.util.concurrent.atomic.AtomicLong;
import java.util.stream.Stream;

public class Main {
    public static void main(String[] args) {
        final Clipboard cb = Clipboard.getSystemClipboard();
        System.out.println(cb.getString());;
    }
}
```

## 失敗
パッケージ階層からわかる通り、javafx.scene.input.Clipboard は JavaFX のクラスなので、通常のスレッドからは利用できません。

```
Exception in thread "main" java.lang.IllegalStateException: This operation is permitted on the event thread only; currentThread = main
	at com.sun.glass.ui.Application.checkEventThread(Application.java:443)
	at com.sun.glass.ui.ClipboardAssistance.<init>(ClipboardAssistance.java:40)
	at com.sun.javafx.tk.quantum.QuantumToolkit.getSystemClipboard(QuantumToolkit.java:1200)
	at javafx.scene.input.Clipboard.getSystemClipboardImpl(Clipboard.java:413)
	at javafx.scene.input.Clipboard.getSystemClipboard(Clipboard.java:178)
	at jp.toastkid.sandbox.Sandbox.main(Sandbox.java:10)
```

というわけで、通常のアプリケーションでクリップボード操作をしたい時は java.awt.datatransfer.Clipboard を使いましょう。~~おしまい~~

## 動作するコード例
JavaFX アプリケーションであれば使えるので、main メソッドのクラスを javafx.application.Application のサブクラスにします。

```java:正常に動作するコード
import javafx.application.Application;
import javafx.scene.input.Clipboard;
import javafx.stage.Stage;

public class MainFx extends Application {
    public static void main(String[] args) {
        Application.launch(SandboxFx.class);
    }

    @Override
    public void start(Stage primaryStage) throws Exception {
        Clipboard cb = Clipboard.getSystemClipboard();
        System.out.println(cb.getString());
    }
}
```

このコードを動かすと、現在クリップボードに保持されている文字列が出力されます。

```java
final Clipboard cb = Clipboard.getSystemClipboard();
System.out.println(cb.getString());
```

----

# メソッドについて

## getContentTypes()
保持しているコンテンツの種別を取得します。

```java:getContentTypes()
System.out.println(cb.getContentTypes());
```

### テキストの場合

```
[[text/uri-list], [JAVA_DATAFLAVOR:application/x-java-jvm-local-objectref; class=com.intellij.codeInsight.editorActions.FoldingData], [text/plain], [ms-stuff/oem-text], [text/rtf], [text/html], [ms-stuff/locale]]
```

### 画像ファイルの場合

```
[[FileName], [message/external-body;access-type=clipboard;index=1;size=28453;name="650contribution.png"], [AsyncFlag], [DataObjectAttributesRequiringElevation], [application/x-java-file-list, java.file-list], [FileNameW], [ms-stuff/preferred-drop-effect], [message/external-body;access-type=clipboard;index=2;size=220376;name="sample_images_1.jpg"], [text/uri-list], [message/external-body;access-type=clipboard;index=0;size=119242;name="sample_images_2.jpg"], [Shell Object Offsets], [Shell IDList Array], [DataObjectAttributes]]
```

### インターネット上の画像の場合

```
[[text/uri-list], [application/x-java-rawimage], [text/_moz_htmlinfo], [text/html;cf=49474], [application/x-java-file-list, java.file-list], [text/_moz_htmlcontext], [application/x-moz-nativeimage], [text/html], [application/x-moz-file-promise-url], [application/x-moz-file-promise-dest-filename], [cf17], [ms-stuff/preferred-drop-effect]]
```


## hasXX()
保持しているコンテンツ種別を判定します。

```java:hasXX()を試すコード
System.out.println(cb.hasString());
System.out.println(cb.hasUrl());
System.out.println(cb.hasHtml());
System.out.println(cb.hasRtf());
System.out.println(cb.hasImage());
System.out.println(cb.hasFiles());
```

### 実行結果
T: true
F: false

| Method name | Text | Local image file | Web image file | URL on Browser
|:---|:---|:---|:---|:---
| hasString() | T | F | F | T |
| hasUrl()    | T | T | T | F |
| hasHtml()   | T | F | T | F |
| hasRtf()    | T | F | F | F |
| hasImage()  | F | F | T | F |
| hasFiles()  | F | T | T | F |


## setContent(Map<DataFormat, Object>)
値を取り出すだけでなく、差し込むことも可能です。差し込みには Map と [DataFormat](https://docs.oracle.com/javase/jp/8/javafx/api/javafx/scene/input/DataFormat.html) を使います。Dat**a**Format です。名前にご注意ください。

### 単一の値

```java
final Map<DataFormat, Object> content = new HashMap<>();
content.put(DataFormat.PLAIN_TEXT, "Orange");
cb.setContent(content);
```

アプリケーションの実行後にテキストエディタ上で Ctrl+V を押すと "Orange" というテキストがペーストされます。

### 複数の値
では複数の値を content に持たせたらどうなるでしょうか？

```java
final Map<DataFormat, Object> content = new HashMap<>();
content.put(DataFormat.URL, "https://www.yahoo.co.jp");
content.put(DataFormat.PLAIN_TEXT, "Toast");
cb.setContent(content);
```

アプリケーションの実行後にテキストエディタ上でCtrl+V を押すと "Toast" というテキストがペーストされます。1つしかクリップボードには保持できないようです。

## clear()
こちらの Clipboard API には clear メソッドがあります。

```java:Clipboard.clear()
System.out.println(clipboard.getString());
clipboard.clear();
System.out.println(clipboard.getString());
```

```:実行結果
holding text
null
```

----

# この API を使って何ができるか？
JavaFX の API なので、用途も必然的に GUI との組み合わせになります。

## 1. 今クリップボードに持っている値を見せる
まあ、そのままですね。

## 2. エディタアプリケーションでのマルチクリップボードの実装
マルチクリップボードというのはその名の通り複数の値をクリップボードに保持しておける機能で、UNIX のテキストエディタには標準的に備わっている機能だそうです。この機能について迂闊に人に話したらどうなるか、『[プロダクティブ・プログラマ](https://www.oreilly.co.jp/books/9784873114026/)』にこんな話が載っていました。

> サンダル履きの男に「俺は高校の時からもう20年もそんなの使ってるよ」的な話を1時間くらい聞かされるはめになるかもしれません

## 3. クリップボード監視アプリケーションの実装
例えば、画像が保持されたらそれを自動でファイルに保存するとか、URL だったら WebView のウィンドウを出すとか……

----
# 簡単なクリップボード監視アプリケーションの実装
というわけで、ごく簡単なものを実装してみました。ソースコードは GitHub の下記リポジトリに置いてあります。

https://github.com/toastkidjp/clipboard_observer_app/tree/qiita

## スクリーンショット
![sample.png](https://qiita-image-store.s3.amazonaws.com/0/102004/a3fd1b06-0cf6-d3bf-40f8-c6a34503ea8b.png)

クリップボードに保持された文字列か画像をウィンドウに表示して、それらをクリックするとクリップボードに再保持される、という雑な GUI アプリケーションです。

## クリップボードのイベント監視
javafx.scene.input.Clipboard にはイベントリスナー等の反応的な API が用意されていないので、RxJava を使って1秒間隔で中身を確認するという方法で実装しました。

```java:クリップボードを監視するObservable
Observable.interval(1, TimeUnit.SECONDS)
          .observeOn(JavaFxScheduler.platform())
          .map(l -> getStringOrEmpty())
          .filter(str -> !str.isEmpty())
          .subscribe(stringConsumer, Throwable::printStackTrace);
```

### RxJavaFX の [JavaFxScheduler](https://github.com/ReactiveX/RxJavaFX/blob/2.1.0/src/main/java/io/reactivex/rxjavafx/schedulers/JavaFxScheduler.java)
[RxJavaFX](https://github.com/ReactiveX/RxJavaFX/tree/2.1.0) で用意されている RxJava の Scheduler で、これを使うと JavaFX アプリケーションスレッドで RxJava 中の処理を実行させることができます。Consumer 等であれば標準で用意されている [Platform.runLater(Runnable)](https://docs.oracle.com/javase/jp/8/javafx/api/javafx/application/Platform.html#runLater-java.lang.Runnable-) を使えばよいのですが、 map や filter のように値を返さないといけない場合は、この JavaFxScheduler があると便利です。

### RxJava2 の map等での null 非許容
RxJava2 では map で null を return すると NullPointerException が発生します。

```
java.lang.NullPointerException: The mapper function returned a null value.
	at io.reactivex.internal.functions.ObjectHelper.requireNonNull(ObjectHelper.java:39)
	at io.reactivex.internal.operators.observable.ObservableMap$MapObserver.onNext(ObservableMap.java:59)
	at io.reactivex.internal.operators.observable.ObservableObserveOn$ObserveOnObserver.drainNormal(ObservableObserveOn.java:200)
	at io.reactivex.internal.operators.observable.ObservableObserveOn$ObserveOnObserver.run(ObservableObserveOn.java:252)
	at io.reactivex.rxjavafx.schedulers.JavaFxScheduler$JavaFxWorker$QueuedRunnable.run(JavaFxScheduler.java:87)
	at io.reactivex.rxjavafx.schedulers.JavaFxScheduler$JavaFxWorker.run(JavaFxScheduler.java:158)
	at com.sun.javafx.application.PlatformImpl.lambda$null$173(PlatformImpl.java:295)
	at java.security.AccessController.doPrivileged(Native Method)
	at com.sun.javafx.application.PlatformImpl.lambda$runLater$174(PlatformImpl.java:294)
	at com.sun.glass.ui.InvokeLaterDispatcher$Future.run(InvokeLaterDispatcher.java:95)
	at com.sun.glass.ui.win.WinApplication._runLoop(Native Method)
	at com.sun.glass.ui.win.WinApplication.lambda$null$148(WinApplication.java:191)
	at java.lang.Thread.run(Thread.java:748)
```

これを回避するために Empty オブジェクトを用意しています。

```java
private static final Image EMPTY_IMAGE = new WritableImage(1, 1);
```

----

# まとめ
JavaFX の Clipboard API の紹介と、これを用いた簡単なアプリケーションの実装について説明しました。 JavaFX アプリケーションを作るのでないなら、こちらではなく [java.awt.datatransfer.Clipboard](https://docs.oracle.com/javase/jp/8/docs/api/java/awt/datatransfer/Clipboard.html) を使いましょう。JavaFX の Clipboard API には clear() メソッドがあります。