2

In my JavaFX app I have a TableView with multiple columns, one of which displays data in a graphical form. To do this I have created a CanvasCell object that creates and manages its own Canvas to deal with the drawing. The drawing part works just fine.

I'd now like to put Tooltips over some regions within the Canvas/Cell. There may be multiple Tooltips per Cell (which prevents me from adding the Tooltip at the Cell level) and they should only trigger in specific regions of the graph. However, I'm not managing to get it functioning at all. I don't seem to understand the interactions of Display Node hierarchy well enough (read "at all") to be able to place the Tooltip anywhere where it will actually work.

Documentation for JavaFX is sparse and Google + SO has come up blank for all searches that I've tried. Is there anyone who knows how to do this sort of thing or should I just write it off as "not an option" for now.

For info, the CanvasCell calls a draw() function inside an extended Canvas object on updateItem(). The code in which I've tried to create a Tooltip sits inside that draw() function and looks like:

    Rectangle rect = new Rectangle(leftVal, topVal, width, height);
    gc.fillRect(rect.getX(), rect.getY(), rect.getWidth(), rect.getHeight());
    Tooltip tooltip = new Tooltip("Tooltip Text");
    Tooltip.install(rect, tooltip);

but that code was written more in hope than anything else and doesn't generate anything useful in the interface.

If someone can point me in the right direction, I will be very grateful.

Bignose
  • 141
  • 9
  • You'll have to listen to mouse events on the canvas and keep track of when it enters and exits certain regions of the canvas. If the mouse enters a region and doesn't move for some arbitrary time then display the tooltip at the mouse's location. When the mouse exits the region hide the tooltip. – Slaw Dec 12 '18 at 11:08

2 Answers2

4

If you don't need the timing control illustrated here, you can simply install the Tooltip on the enclosing Canvas and leverage Shape::contains to condition the text as shown below.

node.setOnMouseMoved(e -> {
    tooltips.forEach((color, bounds) -> {
        if (bounds.contains(e.getX(), e.getY())) {
            tooltip.setText(color.toString());
        }
    });
});

As suggested here, Java 9 and later provide control over Tooltip timing via the properties showDelay and showDuration.

A similar approach is illustrated here for Swing.

image

import javafx.application.Application;
import javafx.scene.shape.Rectangle;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.paint.Color;
import javafx.scene.control.Tooltip;
import javafx.scene.layout.StackPane;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.stage.Stage;

import java.util.HashMap;
import java.util.Map;

/**
 * @see https://stackoverflow.com/a/53785468/230513
 * @see https://stackoverflow.com/a/53753537/230513
 */
public class CanvasTooltipDemo extends Application {

    @Override
    public void start(Stage stage) throws Exception {
        StackPane root = new StackPane();
        Scene sc = new Scene(root, 400, 400);
        stage.setScene(sc);
        Canvas canvas = new Canvas(200, 200);
        root.getChildren().add(canvas);

        Map<Color, Rectangle> tooltips = new HashMap<>();
        tooltips.put(Color.RED, new Rectangle(0, 0, 100, 100));
        tooltips.put(Color.BLUE, new Rectangle(100, 0, 100, 100));
        tooltips.put(Color.YELLOW, new Rectangle(0, 100, 100, 100));
        tooltips.put(Color.GREEN, new Rectangle(100, 100, 100, 100));
        GraphicsContext gc = canvas.getGraphicsContext2D();
        tooltips.forEach((color, bounds) -> {
            gc.setFill(color);
            gc.fillRect(bounds.getX(), bounds.getY(), bounds.getWidth(), bounds.getHeight());
        });

        setToolTips(canvas, tooltips);
        stage.show();
    }

    private void setToolTips(Node node, Map<Color, Rectangle> tooltips) {
        Tooltip tooltip = new Tooltip();
        Tooltip.install(node, tooltip);
        node.setOnMouseMoved(e -> {
            tooltips.forEach((color, bounds) -> {
                if (bounds.contains(e.getX(), e.getY())) {
                    tooltip.setText(color.toString());
                }
            });
        });
        node.setOnMouseExited(e -> {
            tooltip.hide();
        });
    }

    public static void main(String[] args) {
        Application.launch(args);
    }
}
trashgod
  • 203,806
  • 29
  • 246
  • 1,045
2

I have the same solution as per @Slaw suggested. My idea is to make it more centralized so that you can pass your node and its regions you want to show the tooltips.

In the below demo, you can use the setToolTips() as static utitlity method for multiple nodes.

Note: some part of the logic is referred from Tooltip core implementation ;)

import javafx.animation.Animation;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.application.Application;
import javafx.geometry.Bounds;
import javafx.geometry.Rectangle2D;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.Tooltip;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
import javafx.util.Duration;

import java.util.HashMap;
import java.util.Map;

public class MultiTooltipDemo extends Application {
    private double lastMouseX;
    private double lastMouseY;
    private static int TOOLTIP_XOFFSET = 10;
    private static int TOOLTIP_YOFFSET = 7;

    @Override
    public void start(Stage stage) throws Exception {
        StackPane root = new StackPane();
        Scene sc = new Scene(root, 600, 600);
        stage.setScene(sc);
        stage.show();

        StackPane box1 = new StackPane();
        box1.setMaxSize(200, 200);
        box1.setStyle("-fx-background-color:red, blue, yellow, green; -fx-background-insets: 0 100 100 0, 0 0 100 100, 100 100 0 0, 100 0 0 100;");
        root.getChildren().add(box1);

        Map<String, Rectangle2D> tooltips = new HashMap<>();
        tooltips.put("I am red", new Rectangle2D(0, 0, 100, 100));
        tooltips.put("I am blue", new Rectangle2D(100, 0, 100, 100));
        tooltips.put("I am yellow", new Rectangle2D(0, 100, 100, 100));
        tooltips.put("I am green", new Rectangle2D(100, 100, 100, 100));
        setToolTips(box1, tooltips);

    }

    private void setToolTips(Node node, Map<String, Rectangle2D> tooltips) {
        Duration openDelay = Duration.millis(1000);
        Duration hideDelay = Duration.millis(5000);
        Tooltip toolTip = new Tooltip();

        Timeline hideTimer = new Timeline();
        hideTimer.getKeyFrames().add(new KeyFrame(hideDelay));
        hideTimer.setOnFinished(event -> {
            toolTip.hide();
        });

        Timeline activationTimer = new Timeline();
        activationTimer.getKeyFrames().add(new KeyFrame(openDelay));
        activationTimer.setOnFinished(event -> {
            Bounds nodeScreenBounds = node.localToScreen(node.getBoundsInLocal());
            double nMx = nodeScreenBounds.getMinX();
            double nMy = nodeScreenBounds.getMinY();
            toolTip.setText("");
            tooltips.forEach((str, bounds) -> {
                double mnX = nMx + bounds.getMinX();
                double mnY = nMy + bounds.getMinY();
                double mxX = mnX + bounds.getWidth();
                double mxY = mnY + bounds.getHeight();
                if (lastMouseX >= mnX && lastMouseX <= mxX && lastMouseY >= mnY && lastMouseY <= mxY) {
                    toolTip.setText(str);
                }
            });
            if (!toolTip.getText().isEmpty()) {
                toolTip.show(node.getScene().getWindow(), lastMouseX + TOOLTIP_XOFFSET, lastMouseY + TOOLTIP_YOFFSET);
                hideTimer.playFromStart();
            }
        });

        node.setOnMouseMoved(e -> {
            double buffPx = 2;
            double eX = e.getScreenX();
            double eY = e.getScreenY();
            // Not hiding for slight mouse movements while tooltip is showing
            if (hideTimer.getStatus() == Animation.Status.RUNNING) {
                if (lastMouseX - buffPx <= eX && lastMouseX + buffPx >= eX && lastMouseY - buffPx <= eY && lastMouseY + buffPx >= eY) {
                    return;
                }
            }
            lastMouseX = e.getScreenX();
            lastMouseY = e.getScreenY();
            toolTip.hide();
            hideTimer.stop();
            activationTimer.playFromStart();
        });

        node.setOnMouseExited(e -> {
            toolTip.hide();
            activationTimer.stop();
            hideTimer.stop();
        });
    }

    public static void main(String[] args) {
        Application.launch(args);
    }
}
Sai Dandem
  • 8,229
  • 11
  • 26