2

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 TextFields in a GridPane. The board looks like this:

The Sodoku Board

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:

enter image description here

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 TextFields in the GridPane.

Any suggestions for how I can fix this?

qwerty
  • 810
  • 1
  • 9
  • 26

2 Answers2

3

You need to set the event handler to move on arrow key press as you can see below look at the setTextHandler function there is no error handling I just wrote this up to give you an idea of what you should be doing it is called from the loop when you create the TextFields from there it checks for an arrow key press and from there it will .requestFocus() from the next TextField

public class Main extends Application {

    private TextField[][] squares;

    @Override
    public void start(Stage primaryStage) throws Exception {
        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);
                setTextHandler(squares[i][j], i, j);
                grid.add(squares[i][j], j, i);
            }
        }
        grid.setAlignment(Pos.CENTER);

        Scene scene = new Scene(grid);
        primaryStage.setScene(scene);
        primaryStage.show();
    }

    private void setTextHandler(TextField textField, int i, int j){
        textField.setOnKeyPressed(keyEvent -> {
            System.out.println(keyEvent.getCode());
            if(keyEvent.getCode().isArrowKey()) {
                if (keyEvent.getCode() == KeyCode.UP) {
                    squares[i-1][j].requestFocus();

                } else if (keyEvent.getCode() == KeyCode.DOWN) {
                    squares[i+1][j].requestFocus();

                } else if (keyEvent.getCode() == KeyCode.LEFT) {
                    squares[i][j-1].requestFocus();

                } else if (keyEvent.getCode() == KeyCode.RIGHT) {
                    squares[i][j+1].requestFocus();
                }
            }
        });

    }
}
Matt
  • 3,052
  • 1
  • 17
  • 30
3

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.

Slaw
  • 37,820
  • 8
  • 53
  • 80
  • This worked perfectly, but I wasn't able to use those lambda expressions embedded in the switch statement. I'm using Java 14, so I'm curious why it didn't work. Either way, I rewrote it as a standard switch statement and it functioned as intended. – qwerty Jul 08 '20 at 15:24
  • 1
    Glad to help. As for your error, I can only think to suggest to double-check you're using Java 14 for your project and that your IDE is configured correctly (e.g. in IntelliJ make sure the "project language level" is set to Java 14). – Slaw Jul 08 '20 at 15:28
  • How would I create this GridPane using FXML? There's no way to put a Java for loop in an XML file, is there? – qwerty Jul 14 '20 at 15:02
  • 1
    No, the FXML format does not support loop constructs. When something is overly repetitive or is dynamic you should implement that part in the FXML controller. In your case, adding the fields in the `initialize` method would probably be best. – Slaw Jul 14 '20 at 15:14
  • Do you know where I can find information online about building a model for the game? I don't really understand how that works. – qwerty Aug 04 '20 at 18:38
  • Not sure I ever had any one source of information. Over time I've just continued reading about different architectures such as MVC, MVP, MVVM, and others, as well as event-driven programming and patterns such as the state pattern. Since you're making a game you can also check out [Introduction to JavaFX for Game Development](https://gamedevelopment.tutsplus.com/tutorials/introduction-to-javafx-for-game-development--cms-23835), though I think that article is geared more towards real-time games. – Slaw Aug 08 '20 at 01:09