I have written the following code in my JavaFX main class:
package jfxTest;
import java.util.Random;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.TextArea;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
public class Main extends Application {
public static void main(String[] args) {
Application.launch(args);
}
private static int selectionIndex = 0;
private static TextArea textArea;
@Override
public void start(Stage primaryStage) {
System.out.println("Starting JavaFX Window...");
StackPane rootPane = new StackPane();
textArea = new TextArea();
textArea.setText("TEST");
textArea.setEditable(false);
createRandomOptions(8);
textArea.addEventFilter(KeyEvent.KEY_PRESSED, event -> {
if (event.getCode().equals(KeyCode.UP)) {
select(selectionIndex <= 0 ? 0 : selectionIndex - 1);
}
if (event.getCode().equals(KeyCode.DOWN)) {
String[] lines = textArea.getText().split("\n");
select(selectionIndex >= lines.length - 1 ? lines.length - 1 : selectionIndex + 1);
}
});
rootPane.getChildren().add(textArea);
Scene scene = new Scene(rootPane, 900, 500);
primaryStage.setScene(scene);
primaryStage.show();
System.out.println("Created window.");
System.out.println("\nNow press up and down keys to navigate:\n"
+ "Notice, that although a new selection is displayed in the console, it is not\n"
+ "highlighted in the window itself. Even a call to getSelectedText() works fine.\n");
}
/**
* Creates a bunch of random options in textArea.
* @param newOptionCount
*/
private static void createRandomOptions(int newOptionCount) {
for (int i = 0; i < newOptionCount; i++) {
textArea.appendText("\nSEL" + (new Random().nextInt(10000)));
}
}
private static void select(int newSelectionIndex) {
String[] lines = textArea.getText().split("\n");
System.out.println("New selection index: " + newSelectionIndex);
selectionIndex = newSelectionIndex;
// Determine selection indexes
int selectionStart = 0;
int selectionEnd = 0;
for (int i = 0; i < newSelectionIndex; i++) {
selectionStart += lines[i].length() + 1;
}
selectionEnd = selectionStart + lines[newSelectionIndex].length();
// Does not work. Selection does need to be applied twice by the user to be actually highlighted.
textArea.selectRange(selectionStart, selectionEnd);
System.out.println("Selected text: " + textArea.getSelectedText() + "\n");
}
}
I know it looks like a mess and is not exactly clean, but you should get the point.
Now the problem is as follows:
When I navigate in the TextArea
with the arrow keys (specifically UP and DOWN), the selection only becomes visible, when it is applied twice by the user(only at the very top and bottom), although, a call to selectRange()
is made everytime.
Why is this? Is it a bug in the JavaFX library, or am I missing something?
I have searched the problem a few times already, but I've found no results yet. I haven't tested this using AWT Swing yet, but I am quite confident, that it would work fine using it.
My Java is OpenJDK 19 with JavaFX version 19.
It is never a particularly good idea to add low-level event handling (such as mouse and keyboard event handlers) to high-level UI controls. You always stand the risk of interfering with the built-in functionality of the controls, or of the built-in functionality interfering with your event handlers resulting in not giving you the functionality you want.
In your case, the latter is happening: the TextArea
already implements changing the selection when keys are pressed. If the arrow keys are pressed with no modifier key, then the selection will be cleared. This default behavior is occurring after your key press is invoked, so the default behavior is the behavior you see.
You can see what is happening by adding
textArea.selectionProperty().addListener((obs, oldSel, newSel) ->
System.out.printf("Selection change: %s -> %s%n", oldSel, newSel)
);
Note you can workaround this by adding event.consume()
to both the if() { ... }
blocks in your key event handler, but I don't think this is a particularly good solution: the solutions below are better imho.
It looks like you are simply using the wrong control here. If you have a non-editable TextArea
in which you are regarding each line as a distinct item, then it sounds like you are trying to re-implement a ListView
, which already implements things like selection of the individual items. You can reimplement your example simple with:
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.ListView;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
import java.util.Random;
public class Main extends Application {
public static void main(String[] args) {
Application.launch(args);
}
private ListView<String> listView;
@Override
public void start(Stage primaryStage) {
System.out.println("Starting JavaFX Window...");
StackPane rootPane = new StackPane();
listView = new ListView<>();
listView.getItems().add("TEST");
createRandomOptions(8);
rootPane.getChildren().add(listView);
Scene scene = new Scene(rootPane, 900, 500);
primaryStage.setScene(scene);
primaryStage.show();
System.out.println("Created window.");
System.out.println("\nNow press up and down keys to navigate:\n"
+ "Notice, that although a new selection is displayed in the console, it is not\n"
+ "highlighted in the window itself. Even a call to getSelectedText() works fine.\n");
}
/**
* Creates a bunch of random options in textArea.
* @param newOptionCount
*/
private void createRandomOptions(int newOptionCount) {
for (int i = 0; i < newOptionCount; i++) {
listView.getItems().add("SEL" + (new Random().nextInt(10000)));
}
}
}
If you really want to modify what is selected in a text area, use a TextFormatter
with a filter that modifies the selection. This would looks something like this (though, again, I think this is just reinventing the wheel). Note this also behaves the way you (presumably) want on mouse clicks:
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.IndexRange;
import javafx.scene.control.TextArea;
import javafx.scene.control.TextFormatter;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
import java.util.Random;
public class Main extends Application {
public static void main(String[] args) {
Application.launch(args);
}
private TextArea textArea;
@Override
public void start(Stage primaryStage) {
System.out.println("Starting JavaFX Window...");
StackPane rootPane = new StackPane();
textArea = new TextArea();
textArea.selectionProperty().addListener((obs, oldSel, newSel) -> System.out.printf("Selection change: %s -> %s%n", oldSel, newSel));
textArea.setText("TEST");
textArea.setEditable(false);
createRandomOptions(8);
textArea.setTextFormatter(new TextFormatter<String>(this::modifySelection));
rootPane.getChildren().add(textArea);
Scene scene = new Scene(rootPane, 900, 500);
primaryStage.setScene(scene);
primaryStage.show();
System.out.println("Created window.");
System.out.println("\nNow press up and down keys to navigate:\n"
+ "Notice, that although a new selection is displayed in the console, it is not\n"
+ "highlighted in the window itself. Even a call to getSelectedText() works fine.\n");
}
private TextFormatter.Change modifySelection(TextFormatter.Change change) {
IndexRange selection = change.getSelection();
String[] lines = change.getControlNewText().split("\n");
int lineStart = 0 ;
for (String line : lines) {
int lineEnd = lineStart + line.length();
if (lineStart <= selection.getStart() && lineEnd >= selection.getStart()) {
change.setAnchor(lineStart);
}
if (lineStart <= selection.getEnd() && lineEnd >= selection.getEnd()) {
change.setCaretPosition(lineEnd);
}
lineStart += line.length() + 1; // +1 to account for line terminator itself
}
return change;
}
/**
* Creates a bunch of random options in textArea.
* @param newOptionCount
*/
private void createRandomOptions(int newOptionCount) {
for (int i = 0; i < newOptionCount; i++) {
textArea.appendText("\nSEL" + (new Random().nextInt(10000)));
}
}
}