RichTextFX でリッチなテキスト編集機能を実装する

この記事は [JavaFX Advent Calendar 2016](http://qiita.com/advent-calendar/2016/javafx) の14日目です。昨日は @sk44_ さんの「[javafx-maven-plugin でネイティブ用アイコンを設定する](http://d.hatena.ne.jp/hagi44/20161213/1481557645)」でした。あすは @khasunuma さんの「JavaFX から Payara Micro API を呼び出す際の注意点」です。

# 概要
JavaFX でエディタを実装する際、標準で用意されている TextArea の表現力や拡張性の微妙さに物足りなさを感じたことはありませんか？

- フォントの調整が難しい＆すべての文字が同じフォントでないといけない
- 標準では行番号表示ができない
- シンタックスハイライトができない

この記事では JavaFX でエディタ機能を持つツールを作りたい方、あるいは作ったことのある方にお勧めしたいライブラリ "RichTextFX" を紹介します。

----
# [RichTextFX](https://github.com/TomasMikula/RichTextFX) とは
Tomas Mikula氏が開発した、リッチなテキスト編集コンポーネントを提供するライブラリです。GitHub リポジトリにデモコードや使い方があります。

## ライセンス
[BSD 2-Clause License と GPLv2 クラスパス例外のデュアルライセンス](https://github.com/TomasMikula/RichTextFX/blob/master/LICENSE)です。

## 要件
[javafx.scene.text.TextFlow](https://docs.oracle.com/javase/jp/8/javafx/api/javafx/scene/text/TextFlow.html) を利用しているため、 JDK 8 以上が必要です。

## RichTextFX を導入して可能なこと
1. 行番号表示
2. シンタックスハイライト
3. フォント変更

----
# まず導入する

## 環境
今回の導入で用いた環境は下記の通りです。

| Java SE | 1.8.0_102
|:---|:---
| OS | Windows 10
| Gradle | 3.0


### お試し用コード
ごく簡単なコードとして Groovy の FizzBuzz を用います。

```groovy:FizzBuzz.groovy
(1..100)
  .collect{it % 15 == 0 ? "FizzBuzz" :  it % 3 ==0 ? "Fizz" : it % 5 == 0 ? "Buzz" : it}
  .forEach{print it + ", "}
```


## 修正前
![ss2016_1.png](https://qiita-image-store.s3.amazonaws.com/0/102004/0549c94f-ed89-5bd5-80a5-0c63cecf2b21.png)

## [まず置き換える](https://github.com/toastkidjp/javafx_advent2015/commit/7c181376d75739a03ea19677e713498409a14f39)

### Gradle の依存を追加
dependencies に下記の1行を追加します。

```diff:build.gradle
dependencies {
+  compile 'org.fxmisc.richtext:richtextfx:0.7-M2'
......
}
```

### FXML の修正
標準の TextArea とは完全に別物として実装されているため、互換性がありません。注意してください。

#### import の追加

```xml
<?import org.fxmisc.richtext.CodeArea?>
```

#### TextArea タグを CodeArea に置換
Tooltip が使えないので注意してください。

```diff
-        <TextArea fx:id="scripterInput" prefHeight="550.0" prefWidth="500.0">
-          <tooltip><Tooltip text="input script." /></tooltip>
-        </TextArea>
+        <CodeArea fx:id="scripterInput" prefHeight="550.0" prefWidth="500.0" />
```

### ソースコードの修正
Controller クラスも修正が必要です。

#### import の修正

```diff
-import javafx.scene.control.TextArea;
+import org.fxmisc.richtext.CodeArea;
```

#### TextArea を CodeArea に置換
TextArea と CodeArea に互換性はありませんので置き換えが必要です。

```diff
     @FXML
-    public TextArea scripterInput;
+    public CodeArea scripterInput;

     @FXML
-    public TextArea scripterOutput;
+    public CodeArea scripterOutput;
```

#### setText を replaceText に変更
メソッド名も異なるので注意してください。

```diff
-        scripterOutput.setText(result);
+        scripterOutput.replaceText(result);
```

## 修正後のスクリーンショット
素の状態だとコードハイライトも行番号表示もなく、折り返しもされません。

![ss2016_2.png](https://qiita-image-store.s3.amazonaws.com/0/102004/6b6aeb99-f4cd-b306-4f19-9dd43824909c.png)

----
# 行番号を表示する
続いて、行番号を表示してみましょう。

## import の追加

```diff
+import org.fxmisc.richtext.LineNumberFactory;
```

## ParagraphGraphicFactory の指定
各 CodeArea オブジェクトに対し、下記の通り ParagraphGraphicFactory を指定します。

```java:setParagraphGraphicFactoryで行番号表示をセット
scripterOutput.setParagraphGraphicFactory(LineNumberFactory.get(scripterInput));
```

## 修正後のスクリーンショット
![rtfx1.png](https://qiita-image-store.s3.amazonaws.com/0/102004/e1915af4-fa23-73f9-f84d-aa4bc738bca6.png)

このように、行番号が表示されます。

![rtfx2.png](https://qiita-image-store.s3.amazonaws.com/0/102004/29e8af02-eb6e-f7b5-627a-a1e78ae6a37a.png)

試しに改行してみれば、改行した分だけ行数が表示されています。

![rtfx3.png](https://qiita-image-store.s3.amazonaws.com/0/102004/8790bb2f-81b3-2cfe-6523-cc2b428bf607.png)

100行を超えた場合はちゃんと3桁で表示されます。

----
# コードハイライト
CodeArea では、少し実装が必要ですが、特定の書式・キーワードでハイライトさせることが可能です。このライブラリの特徴として、非同期でのハイライティングが挙げられます。編集内容をリアルタイムでハイライトすることが可能です。実装には [ReactFX](https://github.com/TomasMikula/ReactFX) という、同じ Tomas Mikula 氏の開発した Reactive Streams のライブラリが用いられています。

プログラミング言語 Java での実装例は下記に用意されています。

https://github.com/TomasMikula/RichTextFX/blob/master/richtextfx-demos/src/main/java/org/fxmisc/richtext/demo/JavaKeywordsAsync.java

## Groovy のコードハイライトを実装
せっかくなので今回は Groovy での例を実装してみましょう。

### Groovy のキーワード一覧を作成
下記の57種類だそうです。識者の方、間違っていたらご指摘ください。

```groovy:Groovy's&nbsp;keywords
abstract
as
assert
boolean
break
byte
case
catch
char
class
const
continue
def
default
do
double
else
enum
extends
false
final
finally
float
for
goto
if
implements
import
in
instanceof
int
interface
long
native
new
null
package
private
protected
public
return
short
static
strictfp
super
switch
synchronized
this
threadsafe
throw
throws
transient
true
try
void
volatile
while
```

これをテキストファイルに保存しておきます。

### キーワード着色用のCSSファイルを用意
今回は[公式のサンプル](https://github.com/TomasMikula/RichTextFX/blob/2b4ba4378cfe27facfd86051771946ed9c0dc691/richtextfx-demos/src/main/resources/org/fxmisc/richtext/demo/java-keywords.css)をそのまま使わせてもらいます。

```css:キーワード用CSS
.keyword {
    -fx-fill: purple;
    -fx-font-weight: bold;
}
.semicolon {
    -fx-font-weight: bold;
}
.paren {
    -fx-fill: firebrick;
    -fx-font-weight: bold;
}
.bracket {
    -fx-fill: darkgreen;
    -fx-font-weight: bold;
}
.brace {
    -fx-fill: teal;
    -fx-font-weight: bold;
}
.string {
    -fx-fill: blue;
}

.comment {
    -fx-fill: cadetblue;
}

.paragraph-box:has-caret {
    -fx-background-color: #f2f9fc;
}
```

この CSS ファイルはどこに置いてもよいですが、今回は src/main/resources の直下に置きます。

#### ベースとなる Highlight クラスを定義
ハイライト処理実装で共通して使うメソッドを持たせたクラスを先に定義しておきます。デモコード [JavaKeywordsAsync.java](https://github.com/TomasMikula/RichTextFX/blob/2b4ba4378cfe27facfd86051771946ed9c0dc691/richtextfx-demos/src/main/java/org/fxmisc/richtext/demo/JavaKeywordsAsync.java) から、ハイライト処理をする部分だけを抜き出して、さらに共通処理だけを abstract クラスとして定義します。

##### 実装例: Highlight.java
https://github.com/toastkidjp/javafx_advent2015/blob/v2016/src/main/java/jp/toastkid/highlight/Highlight.java

###### highlight メソッド
非同期処理を実行するメソッドです。アプリケーションからはこのメソッドを呼び出します。

#### Groovy 個別の Highlight クラスを作成
先の Highlight クラスを継承して、Groovy の文法のハイライトを実行するクラスを作ります。

##### 実装例: GroovyHighlight.java
https://github.com/toastkidjp/javafx_advent2015/blob/v2016/src/main/java/jp/toastkid/highlight/GroovyHighlight.java

###### makePattern メソッド
先に用意したキーワードファイルからキーワードを読み込み、コードハイライト用の正規表現を構築するメソッドです。キーワード以外の正規表現は事前に定義してあります。下記を各言語用に変更すれば、それぞれの言語用の Highlight クラスを作成可能です。

```java:コードハイライト用の正規表現
    private static final String PAREN_PATTERN     = "\\(|\\)";
    private static final String BRACE_PATTERN     = "\\{|\\}";
    private static final String BRACKET_PATTERN   = "\\[|\\]";
    private static final String SEMICOLON_PATTERN = "\\;";
    private static final String STRING_PATTERN    = "\"([^\"\\\\]|\\\\.)*\"";
    private static final String COMMENT_PATTERN   = "//[^\n]*" + "|" + "/\\*(.|\\R)*?\\*/";
```

###### computeHighlighting メソッド
CodeArea の text を取得し、コードハイライトを施せる形式に変換するメソッドです。

#### Controller クラスに処理を追加
initialize メソッドの最後の行に下記を追加してください。

```java:initialize()
new GroovyHighlight(scripterInput).highlight();
```

さらに、先ほど用意したキーワード着色用 CSS ファイルの URI を Scene オブジェクトの stylesheets に追加してください。

```java:src/main/resources直下の"keywords.css"をstylesheetsに追加
final ObservableList<String> stylesheets = thisStage.getScene().getStylesheets();
stylesheets.add(getClass().getClassLoader().getResource("keywords.css").toExternalForm());
```

これで仕込みは完了です。

### 修正後のスクリーンショット
試しに下記の FizzBuzz コードを入力してみましょう。

```groovy:FizzBuzz
int i = 0

(1..100)
  .collect{it % 15 == 0 
    ? "FizzBuzz" 
    : it % 3 ==0 
      ? "Fizz" 
      : it % 5 == 0 
        ? "Buzz" 
        : it
  }
  .forEach{print it + ", "}
```

![rtfx4.png](https://qiita-image-store.s3.amazonaws.com/0/102004/172d0991-4316-5028-2533-af4a4d3ae4cc.png)


ご覧のように、int は赤っぽい文字に、コメントは薄緑色に、 " で囲われている文字列リテラルは青になっています。

### 実装サンプル
その他のコードハイライトの簡単な実装例は下記のリポジトリに置きましたので、ご興味があればご覧ください(アプリケーション本体は入っておりません)。

https://github.com/toastkidjp/simple_highlight

----
# (追記) 日本語入力の不具合について
……と、ここまで推してきた RichTextFX ですが、実際のアプリケーションに組み込む前にこの記事を書いていて、後ほど見過ごせないバグに気付いてしまいましたので追記します。

## 何がおかしいか？
まず、下記の画像をご覧ください。テキストエリアが3つ並んだウィンドウが表示されていて、そのうち左のテキストエリア(RichTextFX の CodeArea)に「となりの」と入力し、変換候補を表示している状態です。

![rtfx1.png](https://qiita-image-store.s3.amazonaws.com/0/102004/2c6b5614-595f-c2f0-2f7e-2c80d3836ce7.png)

何か違和感はありましたか？そう、入力したテキストと変換候補が画面左上に出ていますね。なかなかお間抜けな状態です……これは RichTextFX のテキストコントロールクラスが [javafx.scene.control.TextInputControl](https://docs.oracle.com/javase/jp/8/javafx/api/javafx/scene/control/TextInputControl.html) を実装していないことが原因らしいです。 ~~作者の方はマルチバイト文字の入力が必要なかったのでしょうかね……~~

## ほかに誰もおかしいと思わなかったのか？
もちろんそんなことはなく、指摘して改善策を提案している方が Issue を作成していました。

[How to insert text using an Input Method #146](https://github.com/TomasMikula/RichTextFX/issues/146)

## InputMethodRequests を実装する

上記の Issue を見ると 「InputMethodRequests を実装して解決した」とのことでしたので、それで改善できるならやってみようと思い、実装してみたのがこちらの [EditorInputMethodRequests](https://github.com/toastkidjp/RichTextFX_verification/blob/verification/src/main/java/jp/toastkid/rtfx_verification/EditorInputMethodRequests.java) です。SNAPSHOT の 1.0.0 で追加された getCaretBounds() というメソッドを使えば割と簡単に実装できました。

```java
codeArea.setInputMethodRequests(new EditorInputMethodRequests(improved));
```

まあ、それよりも [setOnInputMethodTextChanged の中身](https://github.com/toastkidjp/RichTextFX_verification/blob/verification/src/main/java/jp/toastkid/rtfx_verification/Controller.java#L42-L90) を実装する方が大変だったのですが……

```java
codeArea.setOnInputMethodTextChanged(this::handleInputMethodEvent);
```

## 改善後
これらの修正を適用したのが中央のテキストエリアです。これにより

![rtfx2.png](https://qiita-image-store.s3.amazonaws.com/0/102004/4074d620-9a24-dac4-640c-63ed5bdd9db6.png)

ちゃんと入力テキストがキャレットの位置に表示され、変換候補もその下に表示されています。これでようやく最低限のエディタとして使えるレベルになりました。

### 終わらない改善の日々
……と思ったらそんなことはありませんでした。

![rtfx4.png](https://qiita-image-store.s3.amazonaws.com/0/102004/ac7aa790-24bc-6319-8cda-3845f758d6af.png)

右のテキストエリアは JavaFX 標準の TextArea です。入力テキストの未確定部分が下線できょうちょうされていて、変換候補の範囲が青背景で強調されています。このレベルにならないと実用にはきついのですが、現在ここまでの機能は実装できていません。もし興味がございましたら、下記3クラスのコードを参考にチャレンジしてみると面白いかもしれません。

1. [TextInputControlSkin](http://grepcode.com/file/repo1.maven.org/maven2/net.java.openjfx.backport/openjfx-78-backport/1.8.0-ea-b96.1/com/sun/javafx/scene/control/skin/TextInputControlSkin.java)
1. [TextFieldSkin](http://grepcode.com/file/repo1.maven.org/maven2/net.java.openjfx.backport/openjfx-78-backport/1.8.0-ea-b96.1/com/sun/javafx/scene/control/skin/TextFieldSkin.java)
1. [TextAreaSkin](http://grepcode.com/file/repo1.maven.org/maven2/net.java.openjfx.backport/openjfx-78-backport/1.8.0-ea-b96.1/com/sun/javafx/scene/control/skin/TextAreaSkin.java)

## 1.0.0-SNAPSHOT を使う際の注意
なお、2017年2月6日時点で RichTextFX の 1.0.0-SNAPSHOT を使うには、build.gradle の repositories に ```https://oss.sonatype.org/content/repositories/snapshots/``` のリポジトリ指定の追加が必要です。

```groovy:build.gradle
repositories {
    mavenCentral()
    maven {
        url 'https://oss.sonatype.org/content/repositories/snapshots/'
    }
}
```


## サンプルアプリケーションのリポジトリ(GitHub)
https://github.com/toastkidjp/RichTextFX_verification/tree/verification

----
# JavaFX 9以降
JavaFX Advent Calendar 2016 の @nodamushi さんの記事「[JavaFX9が良い感じになってきた件](http://nodamushi.hatenablog.com/entry/2016/12/09/015033)」によると、JavaFX 9 では行番号表示のできる TextArea の実装が標準の API だけで可能になるそうです。期待が高まります。

----
# まとめ
JavaFX でリッチなテキスト編集機能を実装するのに役立つライブラリ RichTextFX を、ごく簡単に紹介致しました。公式のリポジトリには XMLエディタやフォントサイズ変更といった、今回紹介できなかったさまざまなデモコードが用意されています。
Markdown エディタや HTML エディタを開発されている方や、あるいは JavaFX で開発したプレゼンテーションツールにサンプルコードを載せたい方は、このライブラリの導入を検討してみるとよいかもしれません。

----
# 参考

## RichTextFX CSS Reference Guide
https://github.com/TomasMikula/RichTextFX/wiki/RichTextFX-CSS-Reference-Guide

## 今回のソースコード
- [今回の修正差分](https://github.com/toastkidjp/javafx_advent2015/compare/ebf7020e54c5af4aeeb44fbe564768fd5284a5fb...dbdbe55deaae571924b3f5748bc53a9871d4c62e)
- [今回改修したコード全体](https://github.com/toastkidjp/javafx_advent2015/tree/v2016)