ツールチップ

昨日のエントリーに比べるとずいぶんと地味ですが、今日はロールオーバー時のツールチップを作ってみました。

例のごとくソースファイルサンプルはそれぞれリンク先から確認ください。

インターフェイス

数少ないJavaFXのエントリーの中からid:bluepapa32のこちらのエントリーにインスパイアされたのでインターフェイスを工夫してみました。

Node をドラッグできるようにするために、シーングラフの構造を変更しなくてはならない

これは自分でも先日実装したDrragableも同じ問題を抱えており、既存のノードを後からドラック可能にすることが難しいという問題があります。JavaFXではmixinが可能ですが、クラス構造は静的であり、動的にクラス構造を変えることは出来ないので、うまくラッピングするようなインターフェイスで機能追加が出来ることが望ましいわけです。
そこで次のようにしてツールチップを表示する機能を追加できるようにデザインしました。

var button:Button = Button {
    layoutY: 30
    text: "登録"
}
ToolTip.extend {
    node: button
    text: "データベースに登録"
};

extendはクラス

本来であればextend は関数(メソッド)として定義する方が正しいでしょうが、ここではあえてクラスとしています。その理由はクラスの場合、JavaFXの柔軟な宣言的文法が記述できるからです。実際のクラス定義は以下の通りです。パラメータとして指定されたノードのマウスイベント関数をこっそりとラップしているだけです。

public class extend {
    /** 拡張するノード */
    public-init var node:Node;
    public var text:String;
    public var font:Font = Font.DEFAULT;
    public var fill:Paint = Color.LIGHTBLUE;
    var toolTip:ToolTip = ToolTip {
        node: node
        text: bind text
        font: bind font
        fill: bind fill
    }
    var nodeOnMouseEntered: function(e:MouseEvent): Void;
    var nodeOnMouseExited: function(e:MouseEvent): Void;
    postinit {
        nodeOnMouseEntered = node.onMouseEntered;
        node.onMouseEntered = function(e:MouseEvent): Void {
            toolTip.visible = true;
            nodeOnMouseEntered(e);
        }
        nodeOnMouseExited = node.onMouseExited;
        node.onMouseExited = function(e:MouseEvent): Void {
            toolTip.visible = false;
            nodeOnMouseExited(e);
        }
    }
}

つまり、各種のデフォルト値を設定でき、必要な分だけこのクラスの初期化パラメータとして渡せるわけです。各パラメータは可変長でデフォルト値のある引数のような役割を果たすことになります。
拡張処理を以下のようにしても構わないのですが、ToolTip本来の機能とノードにToolTipを追加するという機能を分離した方が良いと思われます。

var button:Button = Button {
    layoutY: 30
    text: "登録"
}
ToolTip {
    node: button
    text: "データベースに登録"
};

また、ToolTip.fxにあえて小文字で開始するpublic クラスを定義することで、ToolTipクラスの静的メソッド風に使えるようになるわけです。

ツールチップの表示

ツールチップ自体はそれ自体がシーングラフ(ノード)で最も全面に表示する必要があります。したがって、ルート(Scene)に追加していますが、この時の座標計算にはNode.localToSceneが便利です。

    override function create():Node {
        Group {
            layoutX: bind node.localToScene(node.layoutX + node.layoutBounds.width + 10, node.layoutY - rectNode.height + 5).x
            layoutY: bind node.localToScene(node.layoutX + node.layoutBounds.width + 10, node.layoutY - rectNode.height + 5).y
            content: [rectNode, textNode]
        }
    }