Tooltips are a great place to attach additional help information for users. However, you probably, like me, often forget about them in your application. Usually, tooltips are limited only to the title of an element, but you probably know that you can attach more content to them to make your application more pleasant for users. When I got the idea, I started to think about how I could do this, and how to implement it for my application in JavaFx. Then I came to this idea with a tooltip with delayed content.
The plan
The idea is quite simple - when a tooltip is displayed long enough, I want to display additional content with more detailed information. There are also a couple more issues I’d like to address during this implementation - like, positioning the tooltip below the pointed element, and optionally attaching information about the keyboard shortcut that will activate this element. So let’s create a class skeleton that allows me to do this all.
public class PositionedTooltip extends Tooltip {
public PositionedTooltip position(Position tooltipPosition) {
// TODO ....
return this;
}
public PositionedTooltip text(String text, KeyCombination keyCombination) {
// TODO ....
return this;
}
public PositionedTooltip delayedContent(long waitTimeMillis, Node delayedContent) {
// TODO ...
return this;
}
public enum Position {
BOTTOM, RIGHT, TOP
}
}
So, I have these 3 methods that support my needs. As you can see, all of them return the current instance of the object. This approach will make it easier to construct tooltips by chain invocation like this:
new PositionedTooltip()
.text("Some action", new KeyCodeCombination(KeyCode.F1))
.position(PositionedTooltip.Position.BOTTOM)
.delayedContent(1000, new Label("Extra info"));
Method delayedContent
accepts time in milliseconds, and Node
. I used here a Node, because sometimes I may need to display more advanced information - like text with images.
The last one is positioning. I’ve created here enum with some variants of positioning. This enum will help me in the future to create a custom implementation for calculating positions for each of its variants.
Starting implementation
To implement this component, I’ll need also a couple more variables, where I could keep the state of the tooltip. So, I’ll add it right away:
public class PositionedTooltip extends Tooltip {
private final static PauseTransition DELAYED_CONTENT_TIMER = new PauseTransition();
private final BorderPane layout = new BorderPane();
private Position tooltipPosition = Position.BOTTOM;
private Node delayedContent;
private Duration delayedContentTime;
//....
}
Starting from the top - DELAYED_CONTENT_TIMER
- this is a static field, that will show additional content after a configured period. “Why static?” - you may ask. It seems that there is no need to create a separate timer for each instance - there will be only one tooltip visible for the entire application. To show another tooltip, the current tooltip will disappear. I just only need to remember to reset this timer while showing a new tooltip.
Other variables hold information about the layout of a tooltip, settings for the display position, and optional values for the delayed content and the time that should pass before the content is displayed.
So, let’s move on to implementation. I will start with the implementation of methods, that were already defined earlier.
// ....
public PositionedTooltip position(Position tooltipPosition) {
this.tooltipPosition = tooltipPosition;
return this;
}
public PositionedTooltip delayedContent(long waitTimeMillis, Node delayedContent) {
this.delayedContent = delayedContent;
this.delayedContentTime = Duration.millis(waitTimeMillis);
return this;
}
public PositionedTooltip text(String text, KeyCombination keyCombination) {
Node top;
if (keyCombination != null) {
Label shortcutLabel = new Label("(" + keyCombination.getDisplayText() + ")");
BorderPane.setMargin(shortcutLabel, new Insets(0, 0, 0, 5));
top = new BorderPane(null, null, shortcutLabel, null, new Label(text));
} else {
top = new Label(text);
}
layout.setTop(top);
return this;
}
//....
All three implementations are quite easy, mostly just set passed attributes inside the tooltip class. The last method has two variants - create a simple label to display, or create a node with a label and assigned keyboard shortcut on the right side. So now let’s move to the main part.
//...
public PositionedTooltip(Control owner) {
setShowDelay(Duration.ZERO);
setGraphic(layout);
setShowDuration(Duration.INDEFINITE);
owner.setTooltip(this);
setOnShowing(s -> {
layout.setCenter(null);
DELAYED_CONTENT_TIMER.stop();
Bounds bounds = owner.localToScreen(owner.getBoundsInLocal());
Point2D position = tooltipPosition.findPosition(bounds);
setX(position.getX());
setY(position.getY());
if (delayedContent != null) {
DELAYED_CONTENT_TIMER.setOnFinished(e -> {
layout.setCenter(delayedContent);
});
DELAYED_CONTENT_TIMER.setDuration(delayedContentTime);
DELAYED_CONTENT_TIMER.playFromStart();
}
});
}
// ....
As you may have noticed, in the implementation I encountered one main problem - to compute a tooltip position, I need to have access to the owner of a tooltip. I’ve decided to pass it through the constructor. The first couple of lines initialize some default settings for the tooltip (I want to show the tooltip immediately, and I want to it not disappear after a couple of seconds, then I set the layout to a tooltip, and assign it to the passed component. The last operation in this constructor is to set the behavior for the tooltip for showing.
Showing behavior is composed of three steps. The first one is to reset the tooltip before showing (remove delayed content if needed and reset the global tooltip timer). The second step is to compute the position of the showing tooltip. I needed here to introduce an additional method in the Position
enum. The third step is to set up the timer if for current tooltip is defined with some additional delayed content. Operation is quite easy - when the time runs out, just place delayed content inside the tooltip.
The last missing piece of code is about computing the tooltip coordinates where the tooltip should be shown. To do this, I need to improve enum implementation, for example like this:
public enum Position {
BOTTOM {
@Override
Point2D findPosition(Bounds bounds) {
return new Point2D(bounds.getMinX(), bounds.getMaxY());
}
},
TOP {
@Override
Point2D findPosition(Bounds bounds) {
return new Point2D(bounds.getMinX(), bounds.getMinY() - 40);
}
},
RIGHT {
@Override
Point2D findPosition(Bounds bounds) {
return new Point2D(bounds.getMaxX(), bounds.getMinY());
}
};
Point2D findPosition(Bounds bounds) {
return null;
}
}
Polishing
So, as you can see, each Tooltip positioning variant has its own implementation to find the right coordinates. I also noticed an annoying issue with tooltips in JavaFx. They appear even if the current window does not have focus (for example it is somewhere in the background, and we move the cursor above it). Here is a little workaround for this:
@Override
protected void show() {
if (getOwnerWindow().isFocused()) {
super.show();
}
}
To simplify various usages I can add a couple more methods to this class, for easier read and use of the code:
// ....
public static PositionedTooltip addTo(Control owner) {
return new PositionedTooltip(owner);
}
public PositionedTooltip text(String text) {
return text(text, null);
}
// ....
I can use any JavaFx Node as delayed content of the tooltip, but it will be easier to use a simple dedicated class for the most common use. An example implementation is below. I know it’s not the best choice because I have hardcoded styles inside the implementation instead of using external CSS styling, but it’s just an example.
public class TextContent extends VBox {
private static final Font defaultFont = Font.font(10);
private static final String defaultLabelStyle = "-fx-border-color: #ddd; -fx-border-width: 1 0 0 0; -fx-border-style: dotted; -fx-padding: 5 0 0 0;";
public TextContent(String text) {
Label detailsLabel = new Label(text);
detailsLabel.setFont(defaultFont);
setPadding(new Insets(5, 0, 0, 0));
detailsLabel.setStyle(defaultLabelStyle);
getChildren().add(detailsLabel);
}
}
And it looks like that’s it when it comes to implementing such a tooltip.
Here are some examples of usage of this class, to show it simplicity:
// example 1
PositionedTooltip.addTo(button1)
.text("Click me");
// example 2
PositionedTooltip.addTo(button2)
.text("Click me 2", new KeyCodeCombination(KeyCode.F5))
.position(PositionedTooltip.Position.RIGHT)
.delayedContent(2000, new TextContent("Some button details."));
Here you can download complete source code: