JavaFX doesn't really encourage a functionality where you register an event handler with a control, and then inspect low-level details of the event (such as the mouse coordinates) in order to deduce semantic information about the event. The preferred approach is to register listeners with individual contained nodes in the scene graph. For example, whereas in Swing you might register a listener with a JList
and then locationToIndex(...)
method to get the index of the item in the JList
, in JavaFX you are encouraged to register listeners with the individual ListCell
s instead of the ListView
itself.
So the "idiomatic" way to do this is probably to create a TextFlow
and add individual Text
s to it. To determine which Text
is clicked, you would register listeners with each of the individual texts.
You could create a reusable class to encapsulate this functionality, of course, exposing as much API of the enclosed TextFlow
as you need.
Here's a fairly basic example:
import java.util.ArrayList;
import java.util.List;
import javafx.application.Application;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.event.Event;
import javafx.event.EventType;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.input.InputEvent;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.StackPane;
import javafx.scene.text.Text;
import javafx.scene.text.TextFlow;
import javafx.stage.Stage;
public class ClickableTextExample extends Application {
@Override
public void start(Stage primaryStage) {
ClickableText text = new ClickableText("The quick brown fox jumped over the lazy dog.");
text.asNode().addEventHandler(TextClickEvent.CLICKED, e -> {
System.out.println("Click on "+e.getCharacter()+" at "+e.getIndex());
});
StackPane root = new StackPane(text.asNode());
Scene scene = new Scene(root, 350, 120);
primaryStage.setScene(scene);
primaryStage.show();
}
public static class ClickableText {
private final StringProperty text = new SimpleStringProperty();
public StringProperty textProperty() {
return text ;
}
public String getText() {
return textProperty().get();
}
public void setText(String text) {
textProperty().set(text);
}
private final TextFlow textFlow ;
public ClickableText(String text) {
textFlow = new TextFlow();
textProperty().addListener((obs, oldText, newText) ->
rebuildText(newText));
setText(text);
}
public Node asNode() {
return textFlow ;
}
private void rebuildText(String text) {
List<Text> textNodes = new ArrayList<>();
for (int i = 0; i < text.toCharArray().length; i++) {
char c = text.charAt(i);
Text textNode = new Text(Character.toString(c));
textNodes.add(textNode);
registerListener(textNode, i);
}
textFlow.getChildren().setAll(textNodes);
}
private void registerListener(Text text, int index) {
text.addEventHandler(MouseEvent.MOUSE_CLICKED, e -> {
TextClickEvent event = new TextClickEvent(text.getText().charAt(0), index, textFlow, textFlow);
Event.fireEvent(textFlow, event);
});
}
}
public static class TextClickEvent extends InputEvent {
private final char c ;
private final int index ;
public static final EventType<TextClickEvent> CLICKED = new EventType<TextClickEvent>(InputEvent.ANY);
public TextClickEvent(char c, int index, Node source, Node target) {
super(source, target, CLICKED);
this.c = c ;
this.index = index ;
}
public char getCharacter() {
return c ;
}
public int getIndex() {
return index ;
}
}
public static void main(String[] args) {
launch(args);
}
}
Another solution, which you may or may not regard as a hack, would be to use a non-editable text field and style it to look like a Text
(or Label
). Then you can check the caretPosition
after the user clicks.
This code is based on Copiable Label/TextField/LabeledText in JavaFX
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.layout.HBox;
import javafx.scene.control.TextField;
import javafx.stage.Stage;
public class TextClicked extends Application {
public static void main(String[] args) {
launch(args);
}
@Override
public void start(Stage primaryStage) throws Exception {
final TextField textNode = new TextField("Hello, world!");
textNode.setEditable(false);
textNode.getStyleClass().add("clickable-text");
textNode.setOnMouseClicked(event -> {
final int idx = Math.max(0, textNode.getCaretPosition() - 1);
System.out.println("Character clicked: " + textNode.getText().charAt(idx));
});
Scene scene = new Scene(new HBox(textNode));
scene.getStylesheets().add("clickable-text.css");
primaryStage.setScene(scene);
primaryStage.show();
}
}
and then
clickable-text.css:
.clickable-text, .clickable-text:focused {
-fx-background-color: transparent ;
-fx-background-insets: 0px ;
}