Search code examples
javafxpopupjavafx-8textfieldkeyevent

JavaFx: TextField KeyEvents are not triggered


I am implmenting a cusom component where the user can type into a TextField and a TableView is pops up then the user can look for items in that table.

I have a problem with the standard KeyEvents of the TextFieldlike Ctrl+A or Home. After the pane with the TableView pops up, those key events are not working anymore. I have checked if the TextField lost the focus but it doesn't, and if I set an EventFilter to see what is happening, it shows that those events are triggered but I see no effect on UI. Even the popup's setHideOnEscape is not working.

Here is a simple code to verify it:

The fxml:

<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.scene.control.TextField?>
<?import javafx.scene.layout.AnchorPane?>
<AnchorPane xmlns="http://javafx.com/javafx"
            xmlns:fx="http://javafx.com/fxml"
            fx:controller="textField.Controller">
    <TextField fx:id="textField" prefWidth="400"/>
</AnchorPane>

The controller:

public class Controller implements Initializable {

    @FXML
    private TextField textField;


    @Override
    public void initialize(URL location, ResourceBundle resources) {

        Popup popUp = new Popup();
        TableView<Object> table = new TableView<>();

        table.prefWidthProperty().bind(textField.widthProperty());
        popUp.getContent().add(table);
        popUp.setHideOnEscape(true);
        popUp.setAutoHide(true);


        // To see if the KeyEvent is triggered
        textField.addEventFilter(KeyEvent.ANY, System.out::println);

        textField.setOnKeyTyped(event -> {
            if(!popUp.isShowing()){
                popUp.show(
                        textField.getScene().getWindow(),
                        textField.getScene().getWindow().getX()
                                + textField.localToScene(0, 0).getX()
                                + textField.getScene().getX(),
                        textField.getScene().getWindow().getY()
                                + textField.localToScene(0, 0).getY()
                                + textField.getScene().getY()
                                + textField.getHeight() - 1);
            }
        });
    }
}

And the main:

public class Main extends Application {

    public void start(Stage primaryStage) throws Exception {
        FXMLLoader loader = new FXMLLoader(getClass().getResource("View.fxml"));
        AnchorPane pane = loader.load();
        primaryStage.setScene(new Scene(pane, 800, 600));
        primaryStage.show();
    }

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

Here is the console output

KeyEvent [source = TextField[id=textField, styleClass=text-input text-field], target = TextField[id=textField, styleClass=text-input text-field], eventType = KEY_PRESSED, consumed = false, character =  , text = a, code = A]
KeyEvent [source = TextField[id=textField, styleClass=text-input text-field], target = TextField[id=textField, styleClass=text-input text-field], eventType = KEY_TYPED, consumed = false, character = a, text = , code = UNDEFINED]
KeyEvent [source = TextField[id=textField, styleClass=text-input text-field], target = TextField[id=textField, styleClass=text-input text-field], eventType = KEY_RELEASED, consumed = false, character =  , text = a, code = A]
KeyEvent [source = TextField[id=textField, styleClass=text-input text-field], target = TextField[id=textField, styleClass=text-input text-field], eventType = KEY_PRESSED, consumed = false, character =  , text = , code = CONTROL, controlDown, shortcutDown]
KeyEvent [source = TextField[id=textField, styleClass=text-input text-field], target = TextField[id=textField, styleClass=text-input text-field], eventType = KEY_TYPED, consumed = false, character = , text = , code = UNDEFINED, controlDown, shortcutDown]
KeyEvent [source = TextField[id=textField, styleClass=text-input text-field], target = TextField[id=textField, styleClass=text-input text-field], eventType = KEY_RELEASED, consumed = false, character =  , text = a, code = A, controlDown, shortcutDown]
KeyEvent [source = TextField[id=textField, styleClass=text-input text-field], target = TextField[id=textField, styleClass=text-input text-field], eventType = KEY_RELEASED, consumed = false, character =  , text = , code = CONTROL]

Any idea why are those events consumed even if it says they aren't?


Solution

  • The reason that the textField doesn't react to some/all "standard function keys" is that their KEY_PRESSED type never reaches it - they are redirected to the table in the popup and most/all of them consumed by the table.

    A naive first approximation would be to set the table's focusTraversable property to false: this effectively prevents all keys from being delivered to it. Real-world requirements might be a bit less simplistic, in that some of those should reach the table while others should bubble up to the textField.

    This can be achieved by a custom EventDispatcher (on the table) which examines all keyEvents and decides which to deliver/not to the table's original dispatcher. A code snippet where interceptor is a predicate used for the decision (at the end there's complete working example for convenience):

    private BasicEventDispatcher original;
    private Predicate<Event> interceptor;
    
    @Override
    public Event dispatchEvent(Event event, EventDispatchChain tail) {
        if (!interceptor.test(event)) {
            event = original.dispatchCapturingEvent(event);
            if (event.isConsumed()) {
                return null;
            }
        }
        event = tail.dispatchEvent(event);
        if (event != null && !interceptor.test(event)) {
            event = original.dispatchBubblingEvent(event);
            if (event.isConsumed()) {
                return null;
            }
        }
        return event;
    }
    

    It's usage: if f.i. we want LEFT and RIGT be bubbled up to that textField, while all others should be handled normally by the table

    List<KeyCode> toIntercept = List.of(KeyCode.LEFT, KeyCode.RIGHT);
    Predicate<Event> interceptor = e -> {
        if (e instanceof KeyEvent) {
            return toIntercept.contains(((KeyEvent) e).getCode());
        }
        return false;
    };
    table.setEventDispatcher(new InterceptingEventDispatcher(
            (BasicEventDispatcher) table.getEventDispatcher(), interceptor));
    

    A complete example to play with:

    public class ViewPopupApplication extends Application {
    
        public static class InterceptingEventDispatcher implements EventDispatcher {
            private BasicEventDispatcher original;
            private Predicate<Event> interceptor;
    
            public InterceptingEventDispatcher(BasicEventDispatcher original, Predicate<Event> interceptor) {
                this.original = original;
                this.interceptor = interceptor;
            }
    
            @Override
            public Event dispatchEvent(Event event, EventDispatchChain tail) {
                if (!interceptor.test(event)) {
                    event = original.dispatchCapturingEvent(event);
                    if (event.isConsumed()) {
                        return null;
                    }
                }
                event = tail.dispatchEvent(event);
                if (event != null && !interceptor.test(event)) {
                    event = original.dispatchBubblingEvent(event);
                    if (event.isConsumed()) {
                        return null;
                    }
                }
                return event;
            }
    
        }
    
        private Parent createContent() {
            TableView<Locale> table = new TableView<>(FXCollections.observableArrayList(Locale.getAvailableLocales()));
            // just to see that right/left are intercepted while up/down are handled
            table.getSelectionModel().setCellSelectionEnabled(true);
    
            TableColumn<Locale, String> country = new TableColumn<>("Country");
            country.setCellValueFactory(new PropertyValueFactory<>("displayCountry"));
            TableColumn<Locale, String> language = new TableColumn<>("Language");
            language.setCellValueFactory(new PropertyValueFactory<>("displayLanguage"));
            table.getColumns().addAll(country, language);
            // disables default focus traversal
            //  table.setFocusTraversable(false);
    
            // decide which keys to intercept
            List<KeyCode> toIntercept = List.of(KeyCode.LEFT, KeyCode.RIGHT);
            Predicate<Event> interceptor = e -> {
                if (e instanceof KeyEvent) {
                    return toIntercept.contains(((KeyEvent) e).getCode());
                }
                return false;
            };
            table.setEventDispatcher(new InterceptingEventDispatcher(
                    (BasicEventDispatcher) table.getEventDispatcher(), interceptor));
    
            TextField textField = new TextField("something to show");
            textField.setPrefColumnCount(20);
            textField.setText("something to see");
    
            table.prefWidthProperty().bind(textField.widthProperty());
            Popup popUp = new Popup();
            popUp.getContent().add(table);
    
            textField.setOnKeyTyped(event -> {
                if(!popUp.isShowing()){
                    popUp.show(
                            textField.getScene().getWindow(),
                            textField.getScene().getWindow().getX()
                                    + textField.localToScene(0, 0).getX()
                                    + textField.getScene().getX(),
                            textField.getScene().getWindow().getY()
                                    + textField.localToScene(0, 0).getY()
                                    + textField.getScene().getY()
                                    + textField.getHeight() - 1);
                }
            });
    
            BorderPane content = new BorderPane(textField);
            return content;
        }
    
        @Override
        public void start(Stage stage) throws Exception {
            stage.setScene(new Scene(createContent()));
            stage.show();
        }
    
        public static void main(String[] args) {
            launch(args);
        }
    
    }