I am making a sodoku solver program in Java with the JavaFX library. The program incorporates an interactive sodoku board consisting of a series of TextField
s in a GridPane
. The board looks like this:
Right now, the cursor is in the top left most TextField
. If the field had text in it, the user would be able to move the cursor through the text by using the arrow keys. However, I want the user to be able to use the arrow keys to navigate to a different TextField
. The issue is, the field is in "typing mode" (I don't know the official terminology) so the arrow keys only move the cursor to a different point in the text, but otherwise it stays in the same field.
This is what I mean:
Pretend that line I drew is the cursor. Right now, if I click the left arrow key, the cursor will move to the left of the 1, but I want it to move the the TextField
on the left, instead. If I click the down arrow key, nothing happens because there is no text below the 1 for the cursor to navigate to, but I want it to move to the TextField
below, instead.
The code for the GridPane
is this:
TextField[][] squares = new TextField[9][9];
GridPane grid = new GridPane();
for (int i = 0; i < 9; i++) {
for (int j = 0; j < 9; j++) {
squares[i][j] = new TextField();
squares[i][j].setPrefHeight(8);
squares[i][j].setPrefWidth(25);
grid.add(squares[i][j], j, i);
}
}
grid.setAlignment(Pos.CENTER);
The squares
array is for me to have access to individual TextField
s in the GridPane
.
Any suggestions for how I can fix this?
To avoid the focused TextField
from handling arrow keys at all you need to intercept the KeyEvent
before it reaches said TextField
. This can be accomplished by adding an event filter to the GridPane
and consuming the event as appropriate. If you're not sure why this works you can check out the JavaFX: Handling Events tutorial.
Then you can use Node#requestFocus()
to programmatically change the focused node.
I also recommend setting the prefColumnCount
of each TextField
rather than trying to set the preferred dimensions manually. That way the preferred dimensions are computed based on the font size.
Here's an example:
import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.TextField;
import javafx.scene.control.TextFormatter;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.GridPane;
import javafx.scene.text.Font;
import javafx.stage.Stage;
public class App extends Application {
private TextField[][] fields;
@Override
public void start(Stage primaryStage) {
GridPane grid = new GridPane();
grid.setHgap(3);
grid.setVgap(3);
grid.setPadding(new Insets(5));
grid.setAlignment(Pos.CENTER);
grid.addEventFilter(KeyEvent.KEY_PRESSED, this::handleArrowNavigation);
fields = new TextField[9][9];
for (int i = 0; i < 9; i++) {
for (int j = 0; j < 9; j++) {
fields[i][j] = createTextField();
grid.add(fields[i][j], j, i);
}
}
primaryStage.setScene(new Scene(grid));
primaryStage.show();
}
private void handleArrowNavigation(KeyEvent event) {
Node source = (Node) event.getSource(); // the GridPane
Node focused = source.getScene().getFocusOwner();
if (event.getCode().isArrowKey() && focused.getParent() == source) {
int row = GridPane.getRowIndex(focused);
int col = GridPane.getColumnIndex(focused);
// Switch expressions were standardized in Java 14
switch (event.getCode()) {
case LEFT -> fields[row][Math.max(0, col - 1)].requestFocus();
case RIGHT -> fields[row][Math.min(8, col + 1)].requestFocus();
case UP -> fields[Math.max(0, row - 1)][col].requestFocus();
case DOWN -> fields[Math.min(8, row + 1)][col].requestFocus();
}
event.consume();
}
}
private TextField createTextField() {
TextField field = new TextField();
// Rather than setting the pref sizes manually this will
// compute the pref sizes based on the font size.
field.setPrefColumnCount(1);
field.setFont(Font.font(20));
field.setTextFormatter(
new TextFormatter<>(
change -> {
// Only allow the text to be empty or a single digit between 1-9
if (change.getControlNewText().matches("[1-9]?")) {
// Without this the text goes "off screen" to the left. This also
// seems to have the added benefit of selecting the just-entered
// text, which makes replacing it a simple matter of typing another
// digit.
change.setCaretPosition(0);
return change;
}
return null;
}));
return field;
}
}
The above also adds a TextFormatter
to each TextField
to show a way to limit the text to digits between 1 and 9. Note the arrow navigation does not "wrap around" when it reaches the end of a row or column. You can of course modify the code to implement this, if desired.
You may want to consider creating a model for the game. That way the business logic is not tied directly to JavaFX UI objects. When you update the model it would notify the view (possibly via a "view model", depending on the architecture) and the view will update itself accordingly.