Make javafx choicebox select item based on keyboard

Viewed 168

According to https://wiki.openjdk.java.net/display/OpenJFX/ChoiceBox+User+Experience+Documentation

Any Alpha-numeric Keys should Jumps to the first item in the list with that letter or letters in the Selected state

However, this doesn't seem to be the case at all, typing when the widget is selected does not appear to do anything.

Here is my minimal reproducible example:

package com.techniqab.testchoicebox;

import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.control.ChoiceBox;
import javafx.scene.layout.Pane;
import javafx.stage.Stage;

import java.net.URL;
import java.nio.file.Paths;


public class TestchoiceboxApplication extends Application {

    @Override
    public void start(Stage primaryStage) throws Exception{
        URL url = Paths.get("./src/main/resources/sample.fxml").toUri().toURL();

        Parent root = FXMLLoader.load(url);
        primaryStage.setTitle("Hello World");
        primaryStage.setScene(new Scene(root, 300, 275));
        primaryStage.show();

        var cb = new ChoiceBox();
        cb.getItems().add("a");
        cb.getItems().add("b");

        ((Pane) root).getChildren().add(cb);
    }


    public static void main(String[] args) {
        launch(args);
    }
}
<?xml version="1.0" encoding="UTF-8"?>

<?import java.lang.*?>
<?import java.util.*?>
<?import javafx.scene.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>

<AnchorPane xmlns="http://javafx.com/javafx"
            xmlns:fx="http://javafx.com/fxml"
            fx:controller="com.techniqab.testchoicebox.TestchoiceboxApplication"
            prefHeight="400.0" prefWidth="600.0">

</AnchorPane>
module testchoicebox {
    requires javafx.controls;
    requires javafx.graphics;
    requires javafx.fxml;
    exports com.techniqab.testchoicebox;
    opens com.techniqab.testchoicebox to javafx.graphics;
}
2 Answers

This seems like it reinvents something that should work...and obviously one would want to subclass the ChoiceBox and add this behavior rather than the hack i've done here as a POC... but this works

import javafx.application.Application;
import javafx.event.EventHandler;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.control.ChoiceBox;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.Pane;
import javafx.stage.Stage;

import java.net.URL;
import java.nio.file.Paths;
import java.time.Duration;
import java.time.Instant;
import java.time.LocalTime;


public class TestchoiceboxApplication extends Application {
    private String typed="";
    private Boolean gotIt=false;
    private Instant lastKeyTime = Instant.now();

    @Override
    public void start(Stage primaryStage) throws Exception{
        URL url = Paths.get("./src/main/resources/sample.fxml").toUri().toURL();

        Parent root = FXMLLoader.load(url);
        primaryStage.setTitle("Hello World");
        primaryStage.setScene(new Scene(root, 300, 275));
        primaryStage.show();

        var cb = new ChoiceBox();
        cb.getItems().add("a");
        cb.getItems().add("b");
        cb.getItems().add("ab");
        cb.getItems().add("aa");
        cb.getItems().add("c");
        cb.getItems().add("cba");


        EventHandler<KeyEvent> keyEventHandler =
                new EventHandler<KeyEvent>() {
                    public void handle(final KeyEvent keyEvent) {
                        if (keyEvent.getCode() == KeyCode.UNDEFINED) {
                            return;
                        }
                        keyEvent.consume();

                        var  now = Instant.now();
                        if (Duration.between(lastKeyTime, now).getSeconds()>3) {
                            //reset search after timeout
                            typed="";
                        }
                        lastKeyTime = now;

                        typed = typed + keyEvent.getCharacter();
                        var items = cb.getItems();
                        int nMatch = 0;
                        int i=0;
                        for (var item: items) {
                            if (item.toString().startsWith(typed)) {
                                if (nMatch==0) {
                                    cb.getSelectionModel().select(item);
                                }
                                nMatch++;
                                i++;
                            }
                        }
                        if (nMatch>1) {
                            cb.show();
                            return;
                        } else {
                            if (nMatch == 1) {
                                cb.hide();
                                return;
                            }
                        }

                        // key didn't match
                        typed =  keyEvent.getCharacter();
                        cb.show();
                        nMatch = 0;
                        for (var item: items) {
                            if (item.toString().startsWith(typed)) {
                                if (nMatch==0) {
                                    cb.getSelectionModel().select(item);
                                }
                                nMatch++;
                            }
                        }
                        if (nMatch>1) {
                            cb.show();
                            return;
                        } else {
                            if (nMatch == 1) {
                                cb.hide();
                                return;
                            }
                        }
                        typed = "";
                    };
                };

        cb.setOnKeyTyped(keyEventHandler);
        ((Pane) root).getChildren().add(cb);
    }


    public static void main(String[] args) {
        launch(args);
    }
}

While the ChoiceBox User Experience Documentation is normative, this simpler variation jumps to the first item in the list having the letter typed. It also responds to the Enter and Return keys.

import javafx.application.Application;
import javafx.event.EventHandler;
import javafx.scene.Scene;
import javafx.scene.control.ChoiceBox;
import javafx.scene.input.KeyEvent;
import javafx.stage.Stage;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.geometry.Insets;
import javafx.scene.input.KeyCode;
import javafx.scene.layout.StackPane;

/**
 * https://stackoverflow.com/a/69591397/230513
 */
public class KeyTest extends Application {

    @Override
    public void start(Stage primaryStage) throws Exception {
        primaryStage.setTitle("Key Test");
        ObservableList<String> items = FXCollections.observableArrayList(
            "a10", "b42", "c33", "a11", "b22", "c333", "d0");
        FXCollections.sort(items);
        ChoiceBox cb = new ChoiceBox(items);
        EventHandler<KeyEvent> keyEventHandler = (var e) -> {
            if (e.getCode() == KeyCode.ENTER) {
                return;
            }
            e.consume();
            for (var item : cb.getItems()) {
                if (item.toString().startsWith(e.getCharacter())) {
                    cb.getSelectionModel().select(item);
                    cb.show();
                    break;
                }
            }
        };
        cb.setOnKeyTyped(keyEventHandler);
        StackPane root = new StackPane();
        StackPane.setMargin(cb, new Insets(16, 16, 16, 16));
        root.getChildren().add(cb);
        primaryStage.setScene(new Scene(root));
        primaryStage.show();
    }

    public static void main(String[] args) {
        launch(args);
    }
}

For multi-key prefix selection, as described here, also consider PrefixSelectionChoiceBox or PrefixSelectionComboBox included in ControlsFX.

Related