読者です 読者をやめる 読者になる 読者になる

プログラムdeタマゴ

nodamushiの著作物は、文章、画像、プログラムにかかわらず全てUnlicenseです

JavaFXのTooltipの挙動を変えるTooltipBehavior作った話

JavaFX JAVA

 JavaFXでTooltipを使ったことがあるだろうか?
 うん、みなまで言わなくてもいい。

  使いにくいよね


 ざっと欠点を挙げるとするならば、

  1. 表示されるまでが遅い
  2. 表示されるまでの時間を変えられない
  3. 表示時間が短い
  4. 表示時間を変えられない
  5. フォーカス持っていないウィンドウでもポップアップする。ついでにウィンドウが一番手前に来る


 屋上へ行こうぜ…久しぶりに…キレちまったよ…な状態になってしまったのは一番最後の項目。
 この症状は複数のWindowを一つのApplicationで開いているときに起こる。(なお、他のプロセスのウィンドウより前に来ることはないっぽい。)
f:id:nodamushi:20150805231216p:plain:w250

f:id:nodamushi:20150805231241p:plain:w250

 これを解消する方法がどうしてもわからんかった。次善策として、フォーカス持っていないウィンドウではせめてポップアップしないようにしたい。


自作TooltipBehaviorを作ってみることにした

 JavaFXにおいて、Tooltipの挙動を決定しているのはTooltipBehaviorというクラスです。
 このTooltipBehaviorはTooltipのシングルトンな内部クラスで、唯一のTooltipBehaviorが色々とEventHandlerを登録したり、今どれが表示されてて、どれが表示されようとしているのかとかを管理しています。
 基本的な処理はこんなかんじ。

  • マウスがホバーしたらactivateタイマーを起動して、時間差でポップアップさせる
  • ポップアップしたら、他に表示されているTooltipは非表示にする
  • ポップアップしたらhideタイマーを起動し、時間差でポップアップを非表示にする
  • マウスが対象ノードから出た場合は、activate,hideタイマーは停止し、現在対象のTooltipが表示中の場合はleftタイマーを起動して時間差でポップアップを非表示にする。
  • マウスプレスされたら全てのタイマーを止め、Tooltipを消す


 この動作を基本として、次の機能を持つ自作TooltipBehaviorを作ることにした。

  1. Tooltip以外のPopupWindowにも使える
  2. フォーカスを持たないウィンドウからもポップアップするかどうか変更できる
  3. 「時間差」を変更することが出来る
  4. 一つのTooltipインスタンスを使い回すことが出来る
  5. 複数の自作TooltipBehaviorインスタンスをまたいで一つだけTooltipが表示できるようにする

というわけで、なんかできた

 成果がこちら。なお、JavaFX8以上が対象。
TooltipBehavior/SinglePopupBehavior.java at master · nodamushi/TooltipBehavior · GitHub
TooltipBehavior/TooltipBehaviorBase.java at master · nodamushi/TooltipBehavior · GitHub
TooltipBehavior/TooltipBehavior.java at master · nodamushi/TooltipBehavior · GitHub

 著作権は放棄してる(パブリックドメイン)のでどなたでも利用改変して問題ありません。
※なお、一部Tooltipの動作がオリジナルのTooltipBehaviorが保持するマウス座標の値に依存している部分があるので、オリジナルのTooltibBehaviorを使わずにTooltipを利用すると、おかしな挙動を示す場合があると思います。
 ただし、その場面はポップアップしているときに、テキストの内容を変更した場合だけのようなので、基本的には問題ないんじゃね、たぶん。



 これらのデモ実行のコードはこちら。
TooltipBehavior/test at master · nodamushi/TooltipBehavior · GitHub

 自作TooltipBehaviorの使い方は至って極シンプルで、最初にTooltipBehaviorのインスタンスを用意しておく以外はTooltip.installを使うのと変わらない

final TooltipBehavior behavior = new TooltipBehavior();
//マウスが乗ってから0.1秒後に表示
behavior.setOpenDuration(new Duration(100));
//ずっと表示
behavior.setHideDuration(Duration.INDEFINITE);
//マウスが放れてから0.3秒後に非表示
behavior.setLeftDuration(new Duration(300));

//………中略………

final Rectangle r = createNode(size);
//色をツールチップで表示する
final Tooltip tooltip = new Tooltip(r.getFill().toString());
//インストール Tooltip.install(r,tooltip)と同じ
behavior.install(r, tooltip);


 BehaviorTestを実行してみるとこんな画面が出てくる。Tooltip.installを用いたときと(表示スピードが違う以外)同じ動作をする。
f:id:nodamushi:20150805235025p:plain



 BehaviorTestでは色のついたRectagleを作るたびにTooltipも生成している。
 今回はたかが色コードのテキストを表示しているだけなのに、無駄だよね。できるなら一つのTooltipで表示できた方が良いはずだ。


 というわけで、それができるよ、と言うことを示すのがBehaviorTest2。

    //表示前にTooltipの内容をColorの値に更新させる
    behavior.setPopupUpdater((final Tooltip t,final Node n)->{
      if(n instanceof Rectangle){
        t.setText(((Rectangle)n).getFill().toString());
      }
    });

    for(int y=0;y<l;y++) {
      for(int x=0;x<l;x++){
        final Rectangle r = createNode(size);
        g.add(r, x, y);

        //インストール Tooltipは一つのインスタンスを使い回す
        //(TooltipBehaviorがデフォルトのTooltipを持っている)
        behavior.install(r);
      }
    }


 上のように、BehaviorTest2では、Rectangleを作るたびにTooltipを作成すると言うことはしていない。
 代わりに、setPopupUpdaterというメソッドに渡しているラムダ式の中でTooltipの文字列を更新しているっぽい。まぁ、要するにCellと同じ考え方だね。
 むろん、これもちゃんと同じように動作します。

f:id:nodamushi:20150805235834p:plain

 Tooltipを更新するフックの他に、表示するかどうかのフックも用意してあるので、結構自由度が高くて個人的には使いやすいです。




 ところで、自作TooltipBehaviorはオリジナルのTooltipBehaviorと違い、Singletonではないので、複数のBehaviorを定義できます。
 なので、複数のBehaviorを同時に利用すると、こんなことになっちゃったりします。

f:id:nodamushi:20150806000425p:plain


 こりゃ困りまんねん。
 というわけで、自作TooltipBehaviorではGroupという概念で、複数のTooltipBehaviorにまたがっても一つしかTooltipを表示させないように出来ます。

    final TooltipBehavior behavior1 = new TooltipBehavior();
    final TooltipBehavior behavior2 = new TooltipBehavior();

    //BehaviorGroupに登録すると、一つのBehaviorGroupに登録された
    //Behaviorの中では、ポップアップするTooltipが常に一つになります
    final BehaviorGroup group = new BehaviorGroup();
    group.addAll(behavior1,behavior2);

 これをしておくと、問題なく表示されるようになります。

f:id:nodamushi:20150806000712p:plain

 
 


CSSで指定できた方がよくね?

 ここまで作って、欲が出たのか、CSSで指定したくなった。だって、Behavior単位で時間管理するより、Tooltip単位で時間管理できた方が楽に決まってる。

 というわけで、表示までの時間等をCSSで指定できるようにプロパティを追加したTooltipのサブクラスを……… 

    private final class CSSBridge extends PopupControl.CSSBridge {
        private Tooltip tooltip = Tooltip.this;

        CSSBridge() {
            super();
            setAccessibleRole(AccessibleRole.TOOLTIP);
        }
    }

(Tooltip.javaより引用)

 ( ゚д゚)作れねえええええええ CSSBridgeが見えねぇええ




 リフレクションに頼るかどうか悩んだ末、Tooltipを丸コピして、必要なプロパティやBehaviorを追加することにした

TooltipBehavior/NTooltip.java at master · nodamushi/TooltipBehavior · GitHub


 うん、まぁ、ねぇ。一応たぶん、動くんだけどね。著作権とかライセンス的にどうなんだろうね、これね
 こいつのSkinに至っては、package名と名前以外完全にコピーだからね。やばいね。よって、NTooltipとNTooltipSkinの著作権は放棄してないよ。たぶん、OpenJDKのライセンスに沿うんだと思うよ。




 というわけで、最後はちょっとあれな感じだけど、結構便利なものが出来たんじゃないかと思ってるよ。