按 F4 时禁用 ComboBox 下拉菜单

Disable ComboBox dropdown on hitting F4

目前,JavaFX 提供了一种功能,可以在按 F4 时下拉组合框。我们想禁用该功能并处理 F4 的其他功能。一开始我认为这很简单。我的想法是,我将添加一个按键事件过滤器,并在按下 F4 时使用它。

但不幸的是那没有用!!经查,发现ComboBoxPopupControl中有一段代码处理按键事件,设置为KeyEvent.ANY过滤器。奇怪的是他们消费 showing/hiding 之后的事件。

部分代码如下:

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

这让我完全无能为力,因为现在我无法仅通过消耗 filters/handlers 来控制事件链的这一部分。 None 以下过滤器帮助我停止显示下拉菜单。

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

我可以阻止它的唯一方法是在其父级上使用事件并且不允许委托给 ComboBox。但这绝对是一种开销,整个应用程序中已经有数十个组合框,而且还会有更多。

我的问题是: 为什么他们实现了一个紧密集成的功能,不允许用户禁用它?

有没有我可以在 ComboBox 级别实施的替代方案,以在按下 F4 时停止 showing/hiding 下拉菜单。

我尝试了以下方法使其工作。但我不确定我能在多大程度上依赖基于时间轴的解决方案:(

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

正如@kleopatra 在问题评论中提到的,消耗一个事件不会停止其在同一阶段的相同 "level" 内的传播。换句话说,所有注册到 ComboBox 的事件过滤器(对于 EventType 及其超类型)仍然会收到通知,即使其中一个使用了该事件。然后还有更改控件的默认行为的问题,这可能是您的最终用户意想不到的,并且不被欣赏。

如果您仍想更改控件的行为,并且发现在祖先上使用事件不令人满意,您可以在自定义 EventDispatcher 而不是事件过滤器中拦截事件:

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

}