JavaFX / TextFormatterを使用する

## 環境

- JDK 1.8.0_121
- JavaFX Scene Builder 8.3.0

## TextFormatterとは
`TextFormatter<T>`はテキスト入力コントロールにおける「変換と変更」を取り扱うクラスで、`TextField`や`TextArea`といったテキスト入力コントロールにセットして使います。以下の役割を持ちます。

- 型`T`のデータモデルを保持する
- テキスト表現と本来の型`T`の相互変換を扱う
- テキスト入力の状態変更であるChangeを加工する（フィルター）

## データモデルの保持

TextFormatterは型引数`T`で指定した型のデータモデルを保持します。

`getValue()`で取り出し、`valueProperty()`でプロパティを取得できるのでプロパティバインディングも行えます。また、この`value`とテキスト入力コントロールの`text`はどちらを書き換えても相互に追従して変わります。

## StringConverter

例えば、Integer型のデータを`TextField`で編集しようと思えば一旦テキスト表現に変換して表示する必要があります。また、編集後の値をIntegerとして扱うにはテキストのパースをする必要があります。StringConverterはそういった型の相互変換の枠組みを提供するインタフェースです。

JavaFXは一般的な型用にビルトインStringConverterを既に用意してくれているのでここから選んで使用することができます。
[パッケージ javafx.util.converter](https://docs.oracle.com/javase/jp/8/javafx/api/javafx/util/converter/package-summary.html)

## フィルター
### TextFormatter.Change
`TextFormatter.Change`はテキスト入力コントロールの状態変更を保持するクラスです。テキスト入力コントロールが**操作される度**に`Change`が生成されて`TextFormatter`に渡されます。以下の情報を持ちます。

- 変更前後の選択範囲（キャレットとアンカーの位置）
- 変更で取り除かれるテキストの範囲
- 変更後に差し込まれるテキスト
- 変更の種類（追加、削除、置き換え）
- 変更前後の全体テキスト

[TextFormatter.Change (JavaFX): API Document](https://docs.oracle.com/javase/jp/8/javafx/api/javafx/scene/control/TextFormatter.Change.html)

### フィルターとは

TextFormatterはこの`Change`を加工する機能を持ちます。つまり、`Change`の気に入らない変更を他の変更に差し替えることができます。TextFormatterではこの操作をフィルターと呼んでおり、`Change`を受け取り`Change`を返す関数型インタフェースとして実装することが出来ます。

`UnaryOperator<TextFormatter.Change> filter;`



### 選択範囲について

選択範囲はアンカーとキャレットの間、と定義されています。キャレットとは入力を行う現在位置のことですが、これが選択範囲の最初にある場合と最後にある場合では意味合いが異なるので注意が必要です。

![a.png](https://qiita-image-store.s3.amazonaws.com/0/168976/9faee2cc-39ef-5865-a94e-0e0c3bde00e3.png)


## TextFormatterの使用例

TextFormatterはStringConverter,フィルターのいずれか、もしくは両方を渡して作成します。
[TextFormatter (JavaFX): API Document](https://docs.oracle.com/javase/jp/8/javafx/api/javafx/scene/control/TextFormatter.html)

以下サンプルです。

### 日本円の通貨を編集するテキストフィールド

Numberを通貨として表示するビルトインクラス`CurrencyStringConverter`を使用してみます。

```java:例
TextFormatter<Number> currencyFormatter = new TextFormatter<>(
        new CurrencyStringConverter(Locale.JAPAN), 
        0, 
        change -> {
    // 選択範囲は1文字目より前にできない
    change.setAnchor(Math.max(1, change.getAnchor()));
    change.setCaretPosition(Math.max(1, change.getCaretPosition()));
    // テキスト変更範囲は1文字目より前にできない
    change.setRange(Math.max(1, change.getRangeStart()), Math.max(1, change.getRangeEnd()));
    return change;
});
textField1.setTextFormatter(currencyFormatter);
```
![b.png](https://qiita-image-store.s3.amazonaws.com/0/168976/5fd62c4d-0486-d0ee-de7d-b23b85a794a7.png)

「￥」が常に先頭に表示されています。エンターキーが押された時にCurrencyStringConverterは "￥100000" → 100000 → "￥100,000" といった相互変換を行っており、100000が`TextFormatter`の`value`にセットされます。

`value`のおかげでテキストフィールドの表現とデータモデルを切り離して取り扱うことができます。以下は数値が30000以下の場合に文字を赤くする例です。

```java:例
currencyFormatter.valueProperty().addListener((o, oldValue, newValue) -> {
    textField1.setStyle(newValue.intValue() <= 30000 ? 
            "-fx-text-fill: red" : "-fx-text-fill: black");
});
```

![9.png](https://qiita-image-store.s3.amazonaws.com/0/168976/6b585c1b-a5bd-7c4c-7db0-d6db5bdabec5.png)


また、この`TextFormatter`にはフィルターを渡しており、１文字目である「￥」を選択、削除、変更することが出来なくしてあります。以下はCtrl-Aを押して全選択したところです。

![c.png](https://qiita-image-store.s3.amazonaws.com/0/168976/c29c4c11-f7ed-7ecd-cce8-1e78cc78e042.png)

### 数字しか入力できないテキストフィールド

```java:例
Pattern notNumberPattern = Pattern.compile("[^0-9]+");
TextFormatter<String> lowerFormatter = new TextFormatter<>(change -> {
    String newStr = notNumberPattern.matcher(change.getText()).replaceAll("");
    int diffcount = change.getText().length() - newStr.length();
    change.setAnchor(change.getAnchor() - diffcount);
    change.setCaretPosition(change.getCaretPosition() - diffcount);
    change.setText(newStr);
    return change;
});
textField2.setTextFormatter(lowerFormatter);
```
この`TextFormatter`はフィルターのみ渡されており、変更差分テキストを毎回数字のみに置き換える仕組みになっています。以下は「2017-04-01T12:00:00」と入力した後のテキストフィールドです。

![e.png](https://qiita-image-store.s3.amazonaws.com/0/168976/79bccc63-375a-32a9-3da5-0295684789b8.png)

フィルターは更新ごとに変換を行うので、キー入力だけでなくコピー＆ペーストや`TextField::setText`による値設定でも機能します。

### IPv4アドレスを編集するテキストフィールド

１バイトごとに数値入力可能なIPv4アドレスフィールドを作ってみました。数字以外を編集することが出来ず、「.」をタイプすると一つ右のバイトを選択します。

![f.png](https://qiita-image-store.s3.amazonaws.com/0/168976/3dc8b591-7e11-b2aa-59c8-e06917bb93fb.png)

[Inet4AddressFormatter.java](https://github.com/monolith52/Snippets/blob/master/src/main/java/textformatter/Inet4AddressFormatter.java)

```java:例
textField3.setTextFormatter(new Inet4AddressFormatter(null));
```

変更差分からユーザーのマウス操作やキー操作を判断するのには限界があります。手の込んだものを作りたい場合は別のアプローチも検討したほうがよさそうです。


## TextFormatterの別の使い道

フィルターはテキストフィールドの変更部分だけを一括で知ることができて便利なので、テキストフィールドのChangeListenerの代わりとして手軽に使われることも多いようです。


## 参考
[ゆっちのBlog: JDK8u40 で追加された Formatted Text も試してみた。](http://yucchi.jp/blog/?p=1960)
[ソース: GitHub](https://github.com/monolith52/Snippets/tree/master/src/main/java/textformatter)
[FXLM: GitHub](https://github.com/monolith52/Snippets/blob/master/src/main/resources/textformatter/Main.fxml)