2

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();
    }
}
Sai Dandem
  • 8,229
  • 11
  • 26
  • a) you should not change standard behavior of standard controls, that might be the reason F4 handling is buried so deeply inside the bowels b) consume does _not_ prevent event delivery to _sibling_ handlers (those on the same node) of the same phase and for all super eventTypes - it's up to them to do with it whatever they want, be it consumed or not. The other way round: consume prevents delivery across _levels_ of the hierarchy (and implicitly reaching the bubbling phase) – kleopatra Oct 25 '19 at 08:35

1 Answers1

2

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();
  }

}
Slaw
  • 37,820
  • 8
  • 53
  • 80
  • wicked and +1 :) Wondering how you would _process other functions for F4_ (assuming the "other" should happen on the target combo) with this approach? – kleopatra Oct 26 '19 at 13:36
  • 1
    actually, I think the F4 handling in the skin is a left-over from very old days: there's a keyMapping in ComboBoxBaseBehavior, so the interference in skin isn't needed - in the current dev, I can comment the lines without producing any failing tests ... someone might consider filing a bug ;) All the keyHandling in combo-related skins is ... suboptimal. Trying to cleanup a bit but it's _really_ a mess. – kleopatra Oct 26 '19 at 14:16
  • Considering we're only intercepting when F4 is _released_ I believe you could still add an event handler for when F4 is _pressed_. Also, if the F4 handler is, for instance, an accelerator on the scene then you should be able to return the event (instead of null) and the event will enter the bubbling phase. If all else fails, I _suppose_ you could always put the F4 handling in the `EventDispatcher`. – Slaw Oct 26 '19 at 14:45
  • 1
    yeah, you are probably right, thanks - gave up too early ;) Anyway [filed an issue](https://bugs.openjdk.java.net/browse/JDK-8233040) and took it, that _should_ be easy to fix (at least the tests seem to run... but then, test coverage is not optimal, *sigh) – kleopatra Oct 26 '19 at 14:50
  • 1
    And I also suspect the F4 handling in the skin is a relic of the past. When I first read this question I looked at the behavior, couldn't find the code the OP was talking about, then realized they meant the skin and was like... huh? I also find it strange that an event _filter_ is used rather than a handler, but maybe that's necessary. – Slaw Oct 26 '19 at 14:51
  • Thanks for the solution. Indeed I too thought and worked on EventDispatcher for this issue after re-reading [my OP and your answer](https://stackoverflow.com/questions/51015476/javafx-difference-between-eventdispatcher-and-eventfilter) (exactly one and half year back ;) ). Just to let you know, instead of returning null, I am now returning the same "event", to let the "handlers" of F4 to run in the heirarchy. BTW @kleopatra, thanks for all your efforts in looking into the issue :) – Sai Dandem Oct 27 '19 at 21:26
  • 1
    as of openjfx14, [the issue is fixed](https://bugs.openjdk.java.net/browse/JDK-8233040) :) – kleopatra Nov 05 '19 at 15:31