Everyone has seen tooltips: the mouse enters a node, stays still for a while, and then a tooltip is displayed. JavaFX does have tooltips. They are easy to use, but not very flexible. For example, you cannot control the delay before the tooltip is shown or for how long it is shown. If you want to roll your own tooltip implementation, the first step is to detect when and where the mouse stays still.
For touch devices, JavaFX has the TOUCH_STATIONARY
event. There’s no such event for mouse, so we are going to simulate it by means of available mouse events.
Plan
Detect all mouse events on a node. When a MOUSE_MOVED
event hasn’t been followed by any mouse event for 1 second, we conclude that the mouse is stationary. When the next mouse event arrives, we conclude that the mouse stopped being stationary. We are going to create a ReactFX event stream that emits the mouse position when the mouse becomes stationary and null
when it stops being stationary.
Solution
1
2
3
4
5
6
7
8
9
10
11
12
EventStream<MouseEvent> mouseEvents = eventsOf(node, MouseEvent.ANY);
EventStream<Point2D> stationaryPositions = mouseEvents
.successionEnds(Duration.ofSeconds(1))
.filter(e -> e.getEventType() == MouseEvent.MOUSE_MOVED)
.map(e -> new Point2D(e.getX(), e.getY()));
EventStream<Void> stoppers = mouseEvents.supply((Void) null);
EventStream<Either<Point2D, Void>> stationaryEvents =
stationaryPositions.or(stoppers)
.distinct();
- Line 1 creates a stream of all mouse events on
node
. - Line 4 keeps only events that haven’t been followed by another one for 1 second.
- Line 5 further filters the stream to contain only mouse moves.
This means that, for example, if the last event is a click and then the mouse stays still, it is not detected as mouse being stationary. Note that this is consistent with how tooltips work—the tooltip is not displayed after the node is clicked. - Line 6 converts mouse events to mouse positions.
- Line 8 declares that any mouse event is a reason to end the stationary state.
- Line 11 includes both types of events (
started being stationary
andstopped being stationary
) in one stream. - Line 12 filters out repeating
stopped being stationary
events.
Highlights
- There are no mutable variables to track state, like
lastMousePosition
orisStationary
. Any use of timers to measure the 1 second delay is completely hidden as well. All the variables above are effectively final and all state is managed by the event streams. - There is no CPU overhead when no one is actually subscribed to
stationaryEvents
. This is due to the lazy nature of event streams. Only when someone subscribes tostationaryEvents
does this propagate all the way down and an event handler is registered for mouse events on the node.
Usage
stationaryEvents.subscribe(either -> either.exec(
pos -> showTooltipAt(pos),
stop -> hideTooltip()
));
Optionally: Dispatch MouseStationaryEvent
s on a node
You may want to use JavaFX’s standard addEventHandler
method to detect when the mouse starts and stops being stationary.
Let’s define MouseStationaryEvent
with the following API:
public class MouseStationaryEvent extends InputEvent {
public static final EventType<MouseStationaryEvent> ANY;
public static final EventType<MouseStationaryEvent> MOUSE_STATIONARY_BEGIN;
public static final EventType<MouseStationaryEvent> MOUSE_STATIONARY_END;
/** Creates a new event of type MOUSE_STATIONARY_BEGIN. */
static final MouseStationaryEvent beginAt(Point2D screenPos);
/** Creates a new event of type MOUSE_STATIONARY_END. */
static final MouseStationaryEvent end();
public Point2D getPosition();
public Point2D getScenePosition();
public Point2D getScreenPosition();
}
Implementation of this class is not very interesting, but is provided here for completeness.
Now, to start dispatching MouseStationaryEvent
s for a node, you simply do this:
EventStream<Either<Point2D, Void>> stationaryEvents = // defined above
stationaryEvents.<Event>map(either -> either.unify(
pos -> MouseStationaryEvent.beginAt(node.localToScreen(pos)),
stop -> MouseStationaryEvent.end()))
.subscribe(evt -> Event.fireEvent(node, evt));
The unify
operator converts a stream of either
to a stream of a single type Point2D
or Void
MouseStationaryEvent
. Point2D
s are converted to a MouseStaionaryEvent
of type MOUSE_STATIONARY_BEGIN
and Void
s are converted to a MouseStationaryEvent
of type MOUSE_STATIONARY_END
.
Usage
node.addEventHandler(MOUSE_STATIONARY_BEGIN, e -> {
showTooltipAt(e.getScreenPosition());
});
node.addEventHandler(MOUSE_STATIONARY_END, e -> {
hideTooltip();
});
Convenient helper class
Finally, here is a helper class combining all of the above that you can use in your projects.
Usage
// detect stationary events on a node after 1 second delay
MouseStationaryHelper helper =
new MouseStationaryHelper(node, Duration.ofSeconds(1));
helper.events().subscribe(either -> either.exec(
pos -> showTooltipAt(pos),
stop -> hideTooltip()
));
// If you would rather use standard JavaFX way,
// start dispatching MouseStationaryEvents on the node.
helper.install();
node.addEventHandler(MOUSE_STATIONARY_BEGIN, e -> {
showTooltipAt(e.getScreenPosition());
});
node.addEventHandler(MOUSE_STATIONARY_END, e -> {
hideTooltip();
});
// optionally, stop dispatching MouseStationaryEvents
helper.uninstall();