JavaFX それを起動したボタンなどの近くにダイアログを表示する

Alertなどのダイアログや自作のモーダルタイプのStageでもそうなのだが、黙っているとスクリーンの中央に表示される。
これを、それをオープンすることになったボタン等の近くに表示したいのだが、モーダルの場合、showAndWait()の前にはそのウインドウサイズがわからないし、
あらかじめPlatform.runLater()をかけてからshowAndWait()を行うと、一瞬中央に表示されてから移動してしまう。
かなりトリッキーだが、以下で解決。

Sometimes you want Window(Dialog or Stage) should be shown around a button which opened that Window.
So users can easily click the buttons in the Window without moving pointer long distance on the screen.
But you can’t get size of the window before showAndWait() and you can’t do anything after showAndWait().
Even if you use Platform.runLater(), it seems that the window will be shown at center of the screen in a short time,
and then it will be moved to the desired position.
Very tricky but the following code is the solution.

import javafx.beans.property.*;
import javafx.beans.value.*;
import javafx.geometry.*;
import javafx.scene.*;
import javafx.scene.control.*;
import javafx.stage.*;

public abstract class RelocateAroundNode {

  public static class ForStage extends RelocateAroundNode {
    private Stage stage;
    public ForStage(Node node, Stage stage) {
      super(node, stage.widthProperty(), stage.heightProperty());
      this.stage = stage;
      if (stage.isShowing()) {
        relocateWindow();
        return;
      }
      listenWindowSizeChange();
    }
    protected void setX(double x) { stage.setX(x); }  
    protected void setY(double y) { stage.setY(y); }  
  }

  public static class ForDialog extends RelocateAroundNode {
    private Dialog<?> dialog;
    public ForDialog(Node node, Dialog<?> dialog) {
      super(node, dialog.widthProperty(), dialog.heightProperty());
      this.dialog = dialog;
      if (dialog.isShowing()) {
        relocateWindow();
        return;
      }
      listenWindowSizeChange();
    }
    protected void setX(double x) { dialog.setX(x); }  
    protected void setY(double y) { dialog.setY(y); }  
  }

  private Node node;
  private ReadOnlyDoubleProperty windowWidthProperty;
  private ReadOnlyDoubleProperty windowHeightProperty;
  private ChangeListener<Number>changeListener = null;

  protected abstract void setX(double x);
  protected abstract void setY(double y);

  protected RelocateAroundNode(Node node, ReadOnlyDoubleProperty width, ReadOnlyDoubleProperty height) {
    this.node = node;
    this.windowWidthProperty = width;
    this.windowHeightProperty = height;
  }

  protected void listenWindowSizeChange() {
    changeListener = new ChangeListener<Number>() {
      public void changed(ObservableValue<? extends Number> observable, Number oldValue, Number newValue) {
        windowSizeChanged();
      }    
    };
    windowWidthProperty.addListener(changeListener);
    windowHeightProperty.addListener(changeListener);
  }

  protected void windowSizeChanged() {
    if (Double.isNaN(windowWidthProperty.get())) return;
    if (Double.isNaN(windowHeightProperty.get())) return;
    relocateWindow();    
    windowWidthProperty.removeListener(changeListener);
    windowHeightProperty.removeListener(changeListener);
    changeListener = null;
  }

  protected void relocateWindow() {
    getScreenSize();

    double windowWidth = windowWidthProperty.get();
    double windowHeight = windowHeightProperty.get();

    Bounds nodeB = node.localToScreen(node.getBoundsInLocal());
    double nodeCenterX = (nodeB.getMinX() + nodeB.getMaxX()) / 2;
    double nodeCenterY = (nodeB.getMinY() + nodeB.getMaxY()) / 2;

    double windowNewX = nodeCenterX - windowWidth / 2;
    double windowNewY = nodeCenterY - windowHeight / 2;

    windowNewX = Math.max(0, windowNewX);
    windowNewX = Math.min(windowNewX,  screenWidth - windowWidth);

    windowNewY = Math.max(0, windowNewY);
    windowNewY = Math.min(windowNewY,  screenHeight - windowHeight);

    setX(windowNewX);
    setY(windowNewY); 
  }

  private static void getScreenSize() {
    if (vb == null) {
      vb = Screen.getPrimary().getVisualBounds();
      screenWidth = vb.getWidth();
      screenHeight = vb.getHeight();
    }    
  }

  private static Rectangle2D vb;
  private static double screenWidth;
  private static double screenHeight;
}


以下のように使用する。

Usage is the following.

      Alert alert = new Alert(AlertType.WARNING, "ok?", ButtonType.OK, ButtonType.CANCEL);
      alert.initOwner(button.getScene().getWindow());
      new RelocateAroundNode.ForDialog(button, alert);
      alert.showAndWait();