JavaFX Enter key issue on ComboBox

Viewed 194

I am encountering an issue with the "ENTER" key event propagation in ComboBox.

I have a form(VBox) which submits or perform validation when it receives "ENTER" key released event. The user will enter the details by tabbing between fields and hitting UP/DOWN keys for selection in combo box (Just to minimize the mouse interaction). So far everything works fine.

When the user tabs to ComboBox and press "DOWN" key, the ComboBox popup is shown. And user can select the options using UP/DOWN keys. After choosing an option, user will press the "ENTER" key to close the popup. Here I want the "ENTER" event to only close the popup but not to submit the form. If the popup not opened, then "ENTER" key on ComboBox should submit the form.

I tried to fix this by consuming the events on ComboBox. But that didn't work. So two questions::

Can anyone let me know "how to prevent the 'ENTER' key event propagation to VBox, only when the ComboBox is showing"?

And also can anyone let me know how come the event is propagated to "VBox" handler, in spite of consuming event on ComboBox filter/handler?

Below is the example show casing the issue:

import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.ComboBox;
import javafx.scene.control.ListView;
import javafx.scene.control.TextField;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;

public class ComboBoxEnterKeyIssue extends Application {

    private DateFormat df = new SimpleDateFormat("hh:mm:ss");

    @Override
    public void start(Stage primaryStage) throws Exception {
        TextField textField1 = new TextField();
        textField1.setMaxWidth(150);

        ComboBox<String> comboBox = new ComboBox<>();
        comboBox.setPrefWidth(150);
        comboBox.getItems().addAll("Item 1", "Item 2", "Item 3", "Item 4", "Item 5");
        comboBox.addEventFilter(KeyEvent.KEY_RELEASED, ke -> {
            if (ke.getCode() == KeyCode.DOWN && !comboBox.isShowing()) {
                comboBox.show();
                ke.consume();
            } else if (ke.getCode() == KeyCode.ENTER) {
                ke.consume(); // Has no effect !!
            }
        });
        comboBox.addEventHandler(KeyEvent.KEY_RELEASED, ke -> {
            if (ke.getCode() == KeyCode.ENTER) {
                ke.consume(); // Has no effect !!
            }
        });

        TextField textField2 = new TextField();
        textField2.setMaxWidth(150);

        ListView<String> output = new ListView<>();
        VBox.setVgrow(output, Priority.ALWAYS);

        VBox pane = new VBox(textField1, comboBox, textField2, output);
        pane.setPadding(new Insets(10));
        pane.setSpacing(10);
        pane.setOnKeyReleased(ke -> {
            if (ke.getCode() == KeyCode.ENTER) {
                output.getItems().add(df.format(new Date()) + " :: Enter key released on pane...");
                // Form submission/validation is performed here !!
            }
        });

        Scene scene = new Scene(pane, 500, 400);
        primaryStage.setScene(scene);
        primaryStage.setTitle("ComboBox enter key issue");
        primaryStage.show();
    }
}
1 Answers

So I came with this nasty solution to differentiate the enter event. The fix is as below:

  • Created a custom combo skin to indentify the listView and set a flag when an "enter" key is pressed on listview.
  • In the Custom combo event dispatcher,check the flag if the enter is pressed while the popup is opened and then consume the event in dispatcher.

The question is still open for any better ideas.

import com.sun.javafx.scene.control.skin.ComboBoxListViewSkin;
import javafx.application.Application;
import javafx.event.EventDispatcher;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.ComboBox;
import javafx.scene.control.ListView;
import javafx.scene.control.Skin;
import javafx.scene.control.TextField;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;

public class ComboBoxEnterKeyIssue extends Application {

    private DateFormat df = new SimpleDateFormat("hh:mm:ss");

    @Override
    public void start(Stage primaryStage) throws Exception {
        TextField textField1 = new TextField();
        textField1.setMaxWidth(150);

        MyComboBox<String> comboBox = new MyComboBox<>();
        comboBox.setPrefWidth(150);
        comboBox.getItems().addAll("Item 1", "Item 2", "Item 3", "Item 4", "Item 5");
        comboBox.addEventFilter(KeyEvent.KEY_RELEASED, ke -> {
            if (ke.getCode() == KeyCode.DOWN && !comboBox.isShowing()) {
                comboBox.show();
                ke.consume();
            } else if (ke.getCode() == KeyCode.ENTER) {
                ke.consume(); // Has no effect !!
            }
        });
        comboBox.addEventHandler(KeyEvent.KEY_RELEASED, ke -> {
            if (ke.getCode() == KeyCode.ENTER) {
                ke.consume(); // Has no effect !!
            }
        });

        TextField textField2 = new TextField();
        textField2.setMaxWidth(150);

        ListView<String> output = new ListView<>();
        VBox.setVgrow(output, Priority.ALWAYS);

        VBox pane = new VBox(textField1, comboBox, textField2, output);
        pane.setPadding(new Insets(10));
        pane.setSpacing(10);
        pane.setOnKeyReleased(ke -> {
            if (ke.getCode() == KeyCode.ENTER) {
                output.getItems().add(df.format(new Date()) + " :: Enter key released on pane...");
                // Form submission/validation is performed here !!
            }
        });

        Scene scene = new Scene(pane, 500, 400);
        primaryStage.setScene(scene);
        primaryStage.setTitle("ComboBox enter key issue");
        primaryStage.show();
    }

    class MyComboBox<T> extends ComboBox<T> {
        boolean hiddenByEnter = false;

        public MyComboBox() {
            init();
        }

        public void setHiddenByEnter(boolean hiddenByEnter) {
            this.hiddenByEnter = hiddenByEnter;
        }

        private void init() {
            final EventDispatcher initial = getEventDispatcher();
            setEventDispatcher((event, tail) -> {
                // Consuming the event only if the popup is hidden by 'enter' key event.
                if (event instanceof KeyEvent && ((KeyEvent) event).getCode() == KeyCode.ENTER && hiddenByEnter) {
                    hiddenByEnter = false;
                    return null;
                }
                return initial.dispatchEvent(event, tail);
            });
        }

        @Override
        protected Skin<?> createDefaultSkin() {
            return new MyComboBoxSkin<>(this);
        }
    }

    class MyComboBoxSkin<T> extends ComboBoxListViewSkin<T> {

        private ListView<T> listView;

        public MyComboBoxSkin(MyComboBox<T> comboBox) {
            super(comboBox);
            /* Identifying the "ENTER" event on listView in popup, to differentiate the events. */
            getPopup().showingProperty().addListener((obs, old, showing) -> {
                if (showing) {
                    comboBox.setHiddenByEnter(false);
                    if (listView == null) {
                        listView = (ListView) getPopup().getScene().getRoot().lookup(".list-view");
                        listView.addEventFilter(KeyEvent.KEY_PRESSED, ke -> {
                            if (ke.getCode() == KeyCode.ENTER) {
                                comboBox.setHiddenByEnter(true);
                            }
                        });
                    }
                }
            });
        }
    }
}
Related