Action とショートカットを設定できる Label を作る

# 概要
JavaFX の標準で用意されている Label には Action やキーボードショートカットが設定できません。文字を表示するためだけの Control なので当たり前といえばそうなのですが、どうしても設定したい事情(後述)があったので、どうにかならないかと調べてみたところ、自分で Label を継承して定義するしかないとわかりました。

## 実行環境

| Java | SE 1.8.0_102
|:---|:---
| OS | Windows 10
| Eclipse | Mars 4.5.2
| JFoenix | 1.0.0

----

# 実装
2つクラスを作る必要があります。実装全体はリンク先(GitHub Repository)をご覧ください。

1. Label を継承するクラス……[MenuLabel](https://github.com/toastkidjp/menu_label/blob/qiita/src/main/java/jp/toastkid/menu_label/MenuLabel.java)
2. 1 の Skin を定義するクラス……[MenuLabelSkin](https://github.com/toastkidjp/menu_label/blob/qiita/src/main/java/jp/toastkid/menu_label/MenuLabelSkin.java)

以下、ポイントを紹介します。

## 1. Label を継承するクラス……[MenuLabel](https://github.com/toastkidjp/menu_label/blob/qiita/src/main/java/jp/toastkid/menu_label/MenuLabel.java)

### 2つのコピペ
コピペなので何も工夫しておらずコメントのしようもないです。

- [Action を使うための部分](https://github.com/toastkidjp/menu_label/blob/qiita/src/main/java/jp/toastkid/menu_label/MenuLabel.java#L26-L55)……Buttonクラスからコピペ
- [キーボードショートカットを使うための部分](https://github.com/toastkidjp/menu_label/blob/qiita/src/main/java/jp/toastkid/menu_label/MenuLabel.java#L61-L76)……MenuItemクラスからコピペ

### Skin の指定
設定されるキーボードショートカットを scene のキーボードショートカット一覧に登録するため、2のCustomスキンを指定します。

```java
    @Override
    protected Skin<?> createDefaultSkin() {
        return new MenuLabelSkin(this);
    }
```

### 引数なしコンストラクタ
FXML からは引数なしコンストラクタが呼ばれるので、忘れずに定義しておきます。

```java
    public MenuLabel() {
        this("");
    }

    public MenuLabel(final String text) {
        super(text);
        setOnMouseClicked(event -> getOnAction().handle(new ActionEvent()));
    }
```

## 2. 1 の Skin を定義するクラス……[MenuLabelSkin](https://github.com/toastkidjp/menu_label/blob/qiita/src/main/java/jp/toastkid/menu_label/MenuLabelSkin.java)
表示するテキストの調整(ショートカットの追加表示)と、キーボードショートカットの登録に必要でした。

https://github.com/toastkidjp/menu_label/blob/qiita/src/main/java/jp/toastkid/menu_label/MenuLabelSkin.java#L32-L38

下記のコードをご覧になるとわかる通り、Skin を設定する label オブジェクトが Accelerator(キーボードショートカット)を持っている場合は表示テキストにショートカットを追加し、持っているショートカットを scene オブジェクトの Accelerators という Map に登録します。

```java:キーボードショートカット関連の処理
if (label.getAccelerator() == null) {
    return;
}

label.setText(label.getText() + "\t|\t" + label.getAccelerator().getDisplayText());
getSkinnable().getScene().getAccelerators()
             .put(label.getAccelerator(), () -> label.getOnAction().handle(new ActionEvent()));
```

ショートカットの表示は、 MenuItem のコードを辿っていくとよりきれいに表示させる実装方法が見つかったのですが、今回やった限りでは上手くいかなかったので、上記の実装で妥協しています。

# おもちゃ
今回作成した MenuLabel を使ったおもちゃのスクリーンショットは下記の通りです。

## スクリーンショット
![screenshot1.png](https://qiita-image-store.s3.amazonaws.com/0/102004/7dec74eb-a1c5-bc40-7ab3-3c5f71854c5a.png)

起動直後はこんな感じで、Hello の領域をクリックするか、 Ctrl+H を入力すると

![screenshot2.png](https://qiita-image-store.s3.amazonaws.com/0/102004/bb632cc2-eed2-1b5d-e9d4-d253e243a248.png)

このようにポップアップが出ます。

このやり方を知っていれば、どんな Control にも Action とキーボードショートカットを設定できるようになります。どんな時に役立つのかはさっぱりわかりません。

# なぜこんなものを作ってしまったのか ~ どうしても設定したい事情
普通に考えたら Button や MenuItem を使えばよいので、今回やったことはまったく無意味に思えるでしょう。

JavaFX で [Material design](https://material.google.com/) を実装するためのライブラリで [JFoenix](http://jfoenix.com/) というものがあります。先ほどのサンプルアプリケーションで使ったポップアップはこのライブラリの JFXSnackbar というコンポーネントを使いました。この JFoenix には ListView を拡張した JFXListView というものがありまして、これは Labeled を継承した Control を要素に用いることができます。これと JFXTabPane(TabPane の拡張)を組み合わせて、 Material designっぽい Menu を作ってみようと考えて、実装してみたところ、2つ問題が発生しました。そう、Action を設定できないこと、キーボードショートカットを設定できないことです。Menu の代替として使うにはその2つが必須でした。

Action だけなら Button を使えば、見た目はおかしいですが何とか実現できます。しかし Button はキーボードショートカットを設定できないので、それであれば Label を拡張してしまおう、という判断に至りました。

![screenshot3.png](https://qiita-image-store.s3.amazonaws.com/0/102004/bbe60790-d2aa-9352-32bd-42655d69aa83.png)


# 備考
この JFXTabPane を、例えば Drawer の下に入れた(隠した)場合、起動直後はキーボードショートカットが使えません。どうやら Skin オブジェクトの初期化はそのコンポーネントが最初に表示されたタイミングで実行されるようです。それでは Menu の代替として使えることにならないので、対策を考えてみましたが、どうもなさそうでしたので、手動で JFXTabPane が持っている ```MenuLabel``` を全件調べて ```Accelerator``` を put する、という方法を採りました。

```java
@FXML
private TabPane menuTabs;

......

final ObservableMap<KeyCombination, Runnable> accelerators
            = this.stage.getScene().getAccelerators();
menuTabs.getTabs().forEach(tab -> {
        @SuppressWarnings("unchecked")
        final JFXListView<ActionLabel> labels = (JFXListView<ActionLabel>) tab.getContent();
        labels.getItems().forEach(l -> {
            if (l.getAccelerator() == null) {
                return;
            }
            accelerators.put(l.getAccelerator(), () -> l.getOnAction().handle(new ActionEvent()));
        });
});
```

# リンク
- [ソースコード全体](https://github.com/toastkidjp/menu_label/tree/qiita)
- [JavaFX で Snackbar を表示する](http://qiita.com/y_q1m/items/ea4b686a8abbd104bc96)……今回使ったポップアップ表示用のコンポーネントについてはこちらの記事をご覧くださいますと幸いです。