Disable ComboBox dropdown on hitting F4

Viewed 254

Currently JavaFX provides a feature to dropdown the comboBox on hitting F4. We want to disable that feature and process other functions for F4. On first instance I thought this is pretty straight forward. My idea is that I will add a key event filter and consume it when F4 is pressed.

But unfortunately that didnt worked !! Upon investigation, I noticed that there is a part of code in ComboBoxPopupControl to handle the key event, which is set as KeyEvent.ANY filter. The strange part is that they consume the event after showing/hiding.

The part of code is as below:

private void handleKeyEvent(KeyEvent ke, boolean doConsume) {
        // When the user hits the enter or F4 keys, we respond before
        // ever giving the event to the TextField.
        if (ke.getCode() == KeyCode.ENTER) {
            setTextFromTextFieldIntoComboBoxValue();

            if (doConsume && comboBoxBase.getOnAction() != null) {
                ke.consume();
            } else {
                forwardToParent(ke);
            }
        } else if (ke.getCode() == KeyCode.F4) {
            if (ke.getEventType() == KeyEvent.KEY_RELEASED) {
                if (comboBoxBase.isShowing()) comboBoxBase.hide();
                else comboBoxBase.show();
            }
            ke.consume(); // we always do a consume here (otherwise unit tests fail)
        }
    }

This makes me totally helpless, as now I can no more control this part of event chain by merely consuming the filters/handlers. None of the below filters helped me to stop showing the dropdown.

comboBox.addEventFilter(KeyEvent.ANY, e -> {
    if (e.getCode() == KeyCode.F4) {
        e.consume(); // Didn't stopped showing the drop down
    }
});
comboBox.addEventFilter(KeyEvent.KEY_PRESSED, e -> {
    if (e.getCode() == KeyCode.F4) {
        e.consume(); // Didn't stopped showing the drop down
    }
});
comboBox.addEventFilter(KeyEvent.KEY_RELEASED, e -> {
    if (e.getCode() == KeyCode.F4) {
        e.consume(); // Didn't stopped showing the drop down
    }
});

The only way I can stop it is to consume the event on its parent and not allowing to delegate to ComboBox. But this is definetly an overhead, with already tens of ComboBox(es) across the application and many more to come.

My question is: Why did they implemented a feature which is tightly integrated not allowing the user to disable it?

Is there any alternate that I can implement on ComboBox level to stop showing/hiding the dropdown when F4 is pressed.

I tried the below approach to make it work. But I am not sure how much i can rely on Timeline based solution :(

import javafx.animation.Animation;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.application.Application;
import javafx.collections.FXCollections;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.ComboBox;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.HBox;
import javafx.stage.Stage;
import javafx.util.Duration;

public class ComboBoxF4_Demo extends Application {
    Timeline f4PressedTimeline = new Timeline(new KeyFrame(Duration.millis(100), e1 -> {
    }));

    @Override
    public void start(Stage stage) throws Exception {
        HBox root = new HBox();
        root.setSpacing(15);
        root.setPadding(new Insets(25));
        root.setAlignment(Pos.CENTER);
        Scene scene = new Scene(root, 600, 600);
        stage.setScene(scene);

        final ComboBox<String> comboBox = new ComboBox<String>() {
            @Override
            public void show() {
                if (f4PressedTimeline.getStatus() != Animation.Status.RUNNING) {
                    super.show();
                }
            }
        };
        comboBox.setItems(FXCollections.observableArrayList("One", "Two", "Three"));
        comboBox.addEventFilter(KeyEvent.ANY, e -> {
            if (e.getCode() == KeyCode.F4) {
                if (e.getEventType() == KeyEvent.KEY_RELEASED) {
                    f4PressedTimeline.playFromStart();
                }
            }
        });

        // NONE OF THE BELOW FILTERS WORKED :(
        /*comboBox.addEventFilter(KeyEvent.ANY, e -> {
            if (e.getCode() == KeyCode.F4) {
                e.consume(); // Didn't stopped showing the drop down
            }
        });
        comboBox.addEventFilter(KeyEvent.KEY_PRESSED, e -> {
            if (e.getCode() == KeyCode.F4) {
                e.consume(); // Didn't stopped showing the drop down
            }
        });
        comboBox.addEventFilter(KeyEvent.KEY_RELEASED, e -> {
            if (e.getCode() == KeyCode.F4) {
                e.consume(); // Didn't stopped showing the drop down
            }
        });
        */
        root.getChildren().addAll(comboBox);
        stage.show();
    }
}
1 Answers

As mentioned by @kleopatra in the question comments, consuming an event does not stop its propagation within the same "level" of the same phase. In other words, all the event filters registered with the ComboBox (for the EventType and its supertypes) will still be notified even if one of them consumes the event. Then there's also the problem of changing the default behavior of controls which may be unexpected, and unappreciated, by your end users.

If you still want to change the control's behavior, and you don't find consuming the event on an ancestor satisfactory, you can intercept the event in a custom EventDispatcher instead of an event filter:

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.ComboBox;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;

public class App extends Application {

  @Override
  public void start(Stage primaryStage) {
    var comboBox = new ComboBox<String>();
    for (int i = 0; i < 20; i++) {
      comboBox.getItems().add("Item #" + i);
    }
    comboBox.getSelectionModel().select(0);

    var oldDispatcher = comboBox.getEventDispatcher();
    comboBox.setEventDispatcher((event, tail) -> {
      if (event.getEventType() == KeyEvent.KEY_RELEASED
          && ((KeyEvent) event).getCode() == KeyCode.F4) {
        return null; // returning null indicates the event was consumed
      }
      return oldDispatcher.dispatchEvent(event, tail);
    });

    primaryStage.setScene(new Scene(new StackPane(comboBox), 500, 300));
    primaryStage.show();
  }

}
Related