I want to use a JavaFX TextArea as though it were exactly like a multi-line TextField. In other words, when I press [Tab] I want to cycle to the next control on the form and when I press [Enter] I want the Key.Event to go to the defaultButton control (rather than be consumed by the TextArea).
The default behavior for TextArea is that [Tab] gets inserted into the TextArea and [Enter] inserts a new-line character.
I know that I need to use EventFilters to get the behavior that I want, but I'm getting it all wrong. I don't want the TextArea to consume these events ... I just want it to let them "go right on by".
The solution here displays two text areas and a default button. When the user presses the tab key, the focus moves to the next control down. When the user presses the enter key, the default button is fired.
To achieve this behavior:
The code makes use of features implemented in Java 8, so Java 8 is required to execute it.
import javafx.application.Application;
import static javafx.application.Application.launch;
import javafx.beans.value.*;
import javafx.collections.ObservableList;
import javafx.event.*;
import javafx.scene.*;
import javafx.scene.control.*;
import static javafx.scene.input.KeyCode.ENTER;
import static javafx.scene.input.KeyCode.TAB;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.VBox;
import javafx.stage.*;
public class TextAreaTabAndEnterHandler extends Application {
final Label status = new Label();
public static void main(String[] args) { launch(args); }
@Override public void start(final Stage stage) {
final TextArea textArea1 = new TabAndEnterIgnoringTextArea();
final TextArea textArea2 = new TabAndEnterIgnoringTextArea();
final Button defaultButton = new Button("OK");
defaultButton.setDefaultButton(true);
defaultButton.setOnAction(new EventHandler<ActionEvent>() {
@Override public void handle(ActionEvent event) {
status.setText("Default Button Pressed");
}
});
textArea1.textProperty().addListener(new ClearStatusListener());
textArea2.textProperty().addListener(new ClearStatusListener());
VBox layout = new VBox(10);
layout.setStyle("-fx-background-color: cornsilk; -fx-padding: 10px;");
layout.getChildren().setAll(
textArea1,
textArea2,
defaultButton,
status
);
stage.setScene(
new Scene(layout)
);
stage.show();
}
class ClearStatusListener implements ChangeListener<String> {
@Override public void changed(ObservableValue<? extends String> observable, String oldValue, String newValue) {
status.setText("");
}
}
class TabAndEnterIgnoringTextArea extends TextArea {
final TextArea myTextArea = this;
TabAndEnterIgnoringTextArea() {
addEventFilter(KeyEvent.KEY_PRESSED, new TabAndEnterHandler());
}
class TabAndEnterHandler implements EventHandler<KeyEvent> {
private KeyEvent recodedEvent;
@Override public void handle(KeyEvent event) {
if (recodedEvent != null) {
recodedEvent = null;
return;
}
Parent parent = myTextArea.getParent();
if (parent != null) {
switch (event.getCode()) {
case ENTER:
if (event.isControlDown()) {
recodedEvent = recodeWithoutControlDown(event);
myTextArea.fireEvent(recodedEvent);
} else {
Event parentEvent = event.copyFor(parent, parent);
myTextArea.getParent().fireEvent(parentEvent);
}
event.consume();
break;
case TAB:
if (event.isControlDown()) {
recodedEvent = recodeWithoutControlDown(event);
myTextArea.fireEvent(recodedEvent);
} else {
ObservableList<Node> children = parent.getChildrenUnmodifiable();
int idx = children.indexOf(myTextArea);
if (idx >= 0) {
for (int i = idx + 1; i < children.size(); i++) {
if (children.get(i).isFocusTraversable()) {
children.get(i).requestFocus();
break;
}
}
for (int i = 0; i < idx; i++) {
if (children.get(i).isFocusTraversable()) {
children.get(i).requestFocus();
break;
}
}
}
}
event.consume();
break;
}
}
}
private KeyEvent recodeWithoutControlDown(KeyEvent event) {
return new KeyEvent(
event.getEventType(),
event.getCharacter(),
event.getText(),
event.getCode(),
event.isShiftDown(),
false,
event.isAltDown(),
event.isMetaDown()
);
}
}
}
}
An alternate solution would be to implement your own customized skin for TextArea which includes new key handling behavior. I believe that such a process would be more complicated than the solution presented here.
Update
One thing I didn't really like about my original solution to this problem was that once the Tab or Enter key was consumed, there was no way to trigger their default processing. So I updated the solution such that if the user holds the control key down when pressing Tab or Enter, the default Tab or Enter operation will be performed. This updated logic allows the user to insert a new line or tab space into the text area by pressing CTRL+Enter or CTRL+Tab.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With