JavaFX - Fixed Popups
I am currently working on a small autocompletion text field. I want the component to show a small popup with completion possibilities whenever any are available at the current cursor location.
I thought "That's gotta be easy, just call new Popup()
and invoke
show()
... well, it didn't work too well. The popup would
initially show correctly, but also not hide clicking anywhere. This is due
to the fact you have to set `autoHide` to `true`.
After setting autoHide
, it would however hide when you move the window or
click in some area that can't receive focus anyway. This means the text field
would still have focus, but no popup showing. This is quite annoying, as this
means we can't bind the popup visiblity to the focus. While a workaround would
be to listen to KeyEvent
and MouseEvent
to re-show
the popup, this is janky.
Time to make my own popup I guess? I found an interesting post guiding me the right way.
This solution would allow me to freely place any component inside of a
Pane
. The only issue with this solution, is that it changes
the bounds of the popups parent. So our hierarchy Would Be
Pane(TextField, Popup)
.
To solve the layouting issue, the following rough setup is required:
final var textField = new TextField("I am a TextField");
final var popup = new Label("I am a Popup");
popup.setVisible(false);
popup.setManaged(false);
final var pane = new Pane();
pane.getChildren().addAll(textField, popup);
pane.maxHeightProperty().bind(textField.heightProperty());
pane.maxWidthProperty().bind(textField.widthProperty());
showPopup();
Calling setManaged(false)
on the popup
will prevent
it from being layouted, but also prevent it from expanding the parent in size.
Since the parent is now bound to the size of the textField
, we
won't be pushing other components in the layout.
However, since popup
isn't being layouted, it does not have a position.
While the methods setLayoutX
and setLayoutY
exist, they
have no effect, as we are effectively not part of a layout. Instead we need to
call setTranslateY
and setTranslateX
:
private void showPopup() {
popup.setVisible(true);
final var textFieldBounds = textField.getBoundsInParent();
popup.setTranslateY(textFieldBounds.getMaxY());
popup.setTranslateX(textFieldBounds.getMinX());
...
}
In order to show the text field, we need to always bring all components in its
hierarchy to the front, as the popup will otherwise render partially behind
other components in any nested layout. However, calling
Node#toFront
will actually reposition the nodes. Instead, we set
the viewOrder
to a negative value for the full node hierarchy:
private void showPopup() {
...
toFront(popup);
}
private void toFront(Node node) {
node.setViewOrder(-1);
if (node.getParent() != null) {
toFront(node.getParent());
}
}
I hope someone can make use of this wild hack 😀