JavaFX tooltip styleable parent bug

Few days ago I found problem with tooltip styling.
I wanted to show tooltip on demand (when TextField get focus) and I also wanted to style this tooltip using custom css like this:

1
2
3
4
5
6
7
8
9
.label-with-tooltip .tooltip {
-fx-effect: dropshadow(three-pass-box, rgba(0,0,0,0.8), 10, 0, 0, 0);
-fx-font-weight: bold;
-fx-padding: 5;
-fx-border-width:1;
-fx-background-color: #FBEFEF;
-fx-text-fill: #cc0033;
-fx-border-color:#cc0033;
}

I got result that when I hovered mouse on TextField then my style was applied correctly but when I show tooltip on focus event using tooltip.show(...) then tooltip didn’t get my style.

This is due to getStyleableParent method of Tooltip class with is responsible for style “propagation”. In Tooltip class getStyleableParent looks like this:

1
2
3
public Styleable getStyleableParent() {
return BEHAVIOR.hoveredNode;
}

BEHAVIOR is static class responsible for properly showing and hiding tooltip when mouse is over node. hoveredNode property is set in mouse move event handler on the node that have installed tooltip supported by BEHAVIOR class. Therefore, when you use show method to manually show tooltip then BEHAVIOR.hoveredNode property is not set and also you don’t have chance to change it, because BEHAVIOR is private class.

I found solution by overriding getStyleableParent and returning ownerNode which is set when you show tooltip using method of Tooltip class:

1
2
3
public void show(Node ownerNode, double anchorX, double anchorY) {
...
}

Here is example application where you see how it works:

JavaFX tooltip fix example

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
public class Main extends Application {
public static class TooltipFix extends Tooltip {
public TooltipFix(String text) {
super(text);
}
@Override
public Styleable getStyleableParent() {
Styleable styleableParent = super.getStyleableParent();
// if default behavior is not returning styleable parent
// then return owner node
if (styleableParent != null) {
return styleableParent;
} else {
return getOwnerNode();
}
}
}
@Override
public void start(Stage primaryStage) throws Exception{
GridPane gridPane = new GridPane();
Parent root = gridPane;
primaryStage.setTitle("Hello World");
primaryStage.setScene(new Scene(root, 300, 275));
primaryStage.show();
primaryStage.getScene().getStylesheets().clear();
primaryStage.getScene().getStylesheets().add(getClass().getResource("style.css").toExternalForm());
Label lblTooltipBug = new Label("Tooltip bugged");
lblTooltipBug.getStyleClass().add("label-with-tooltip");
// original tooltip
Tooltip tooltipBug = new Tooltip("I'm broken");
lblTooltipBug.setTooltip(tooltipBug);
gridPane.add(lblTooltipBug, 0, 0);
Label lblTooltipFix = new Label("Tooltip fixed");
lblTooltipFix.getStyleClass().add("label-with-tooltip");
gridPane.add(lblTooltipFix, 0, 1);
// fixed tooltip
TooltipFix tooltipFix = new TooltipFix("I'm repaired");
lblTooltipFix.setTooltip(tooltipFix);
Button btnShowTooltip = new Button("Show tooltips");
btnShowTooltip.setOnAction(event -> {
showTooltip(lblTooltipBug);
showTooltip(lblTooltipFix);
});
gridPane.add(btnShowTooltip, 0, 2);
gridPane.setVgap(20);
gridPane.setAlignment(Pos.CENTER);
}
public static void showTooltip(Label label) {
if (label != null && label.getTooltip() != null) {
// offset
Point2D point = label.localToScene(100, 0);
label.getTooltip().setAutoHide(true);
label.getTooltip().show(label, point.getX()
+ label.getScene().getX() + label.getScene().getWindow().getX(), point.getY()
+ label.getScene().getY() + label.getScene().getWindow().getY());
}
}
public static void main(String[] args) {
launch(args);
}
}