JavaFx 11 ListView consumes ESCAPE key pressed event even if is not in editing state

492 views Asked by At

I have a problem with JavaFx ListView component. I'm using popup with TextField and ListView inside of VBox. When TextField is in focus, I can normally close this popup window pressing the Esc key on the keyboard, but when ListView item is in focus popup stays open, nothing happens.

Minimal reproducible example:

package sample;

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.ButtonType;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.Dialog;
import javafx.scene.control.Label;
import javafx.scene.control.ListView;
import javafx.scene.control.MenuItem;
import javafx.scene.control.TextField;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

public class Main extends Application {

   @Override
   public void start(Stage primaryStage) throws Exception {
      MenuItem rightClickItem = new MenuItem("CLICK!");
      rightClickItem.setOnAction(a -> showdialog());
      ContextMenu menu = new ContextMenu(rightClickItem);

      Label text = new Label("Right Click on me");

      text.setContextMenu(menu);

      StackPane root = new StackPane(text);
      Scene scene = new Scene(root, 300, 250);

      primaryStage.setTitle("RightClick MenuItem And Dialog");
      primaryStage.setScene(scene);
      primaryStage.show();
   }

   private void showdialog() {
      Dialog<ButtonType> dialog = new Dialog<>();
      dialog.getDialogPane().getButtonTypes().add(ButtonType.CANCEL);
      VBox vBox = new VBox();
      ListView listView = new ListView();
      listView.getItems().add("Item 1");
      listView.getItems().add("Item 2");
      vBox.getChildren().add(new TextField());
      vBox.getChildren().add(listView);

      vBox.addEventHandler(KeyEvent.KEY_PRESSED, keyEvent -> System.err.println("Key pressed: " + keyEvent.getCode()));

      dialog.getDialogPane().setContent(vBox);

      dialog.showAndWait();
   }


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

It seems to me that Esc key is consumed in ListView, and this cause a problem with closing a popup.

Just to mention, I'm using zulu-11.0.8 JDKFx version.

2

There are 2 answers

0
kleopatra On BEST ANSWER

It seems to me that Esc key is consumed in ListView, and this cause a problem with closing a popup.

That's indeed the problem - happens with all controls that have a consuming KeyMapping to ESCAPE added by their respective Behavior (f.i. also for a TextField with TextFormatter).

There is no clean way to interfere with it (Behavior and InputMap didn't yet make to move into public api). The way to hack around is to remove the KeyMapping from the Behavior's inputMap. Beware: you must be allowed to go dirty, that is use internal api and use reflection!

The steps:

  • grab the control's skin (available after the control is added to the scenegraph)
  • reflectively access the skin's behavior
  • remove the keyMapping from the behavior's inputMap

Example code snippet:

private void tweakInputMap(ListView listView) {
    ListViewSkin<?> skin = (ListViewSkin<?>) listView.getSkin();
    // use your favorite utility method to reflectively access the private field
    ListViewBehavior<?> listBehavior = (ListViewBehavior<?>) FXUtils.invokeGetFieldValue(
            ListViewSkin.class, skin, "behavior");
    InputMap<?> map = listBehavior.getInputMap();
    Optional<Mapping<?>> mapping = map.lookupMapping(new KeyBinding(KeyCode.ESCAPE));
    map.getMappings().remove(mapping.get());
}

It's usage:

listView.skinProperty().addListener(ov -> {
    tweakInputMap(listView);
});
0
Rich On

To avoid using private API, you can use an event filter that, if the ListView is not editing, copies the Escape key event and fires it on the parent. From there, the copied event can propagate to be useful in other handlers such as closing a popup.

Also, if you need this on all ListViews in your application, you can do it in a derived class of ListViewSkin and set that as the -fx-skin for .list-view in your CSS file.

      listView.addEventFilter( KeyEvent.KEY_PRESSED, keyEvent -> {
         if( keyEvent.getCode() == KeyCode.ESCAPE && !keyEvent.isAltDown() && !keyEvent.isControlDown()
               && !keyEvent.isMetaDown() && !keyEvent.isShiftDown()
         ) {
            if( listView.getEditingIndex() == -1 ) {
               // Not editing.
               final Parent parent = listView.getParent();
               parent.fireEvent( keyEvent.copyFor( parent, parent ) );
            }
            keyEvent.consume();
         }
      } );