Search code examples
javafxkeyevent

JavaFX KeyEvent doesn't do anything


So I have a little program where you can fly your spaceship between planets and I want to use the arrow keys to rotate the ship. First I tried adding the key listener to the panel and the ship did rotate, but only when I pressed ctrl/alt. Then I tried adding the key listener to the scene instead (which people say is the right thing to do because it doesn't depend on focus), but although the function that rotates the ship is called, you can't see anything on the screen.

The main class:

@Override
public void start(Stage stage) {
    try {
        FXMLLoader loader = new FXMLLoader(getClass().getResource("Main.fxml"));
        Parent root = loader.load();
        Scene scene = new Scene(root, 700, 500);
        Controller controller = new Controller();
        loader.setController(controller);
             
        scene.getStylesheets().add(getClass().getResource("application.css").toExternalForm());
        stage.setScene(scene);
        stage.setFullScreen(true);
        stage.show();
        controller.setScene(scene);
    } catch(Exception e) {
        e.printStackTrace();
    }
        
} 

The controller:

    @FXML
    private BorderPane contentPane;
    
    private OrbiterPanel orbiterPanel = new PerfectCirclePanel(this);
    private Scene scene;

    @Override
    public void initialize(URL arg0, ResourceBundle arg1) {
        contentPane.setCenter(orbiterPanel);
    }

    public void setScene(Scene scene) {
        this.scene = scene;
        scene.addEventFilter(KeyEvent.KEY_PRESSED, e -> keyStrokesPressed(e));
        scene.addEventFilter(KeyEvent.KEY_RELEASED, e -> keyStrokesReleased(e));
    } 
    
    private void keyStrokesPressed(KeyEvent e) {
        switch(e.getCode()) {
        case LEFT -> {
            orbiterPanel.leftPressed();
        }
        case RIGHT -> {
            orbiterPanel.rightPressed();
        }
        default -> {}
        }   
    }
    
    private void keyStrokesReleased(KeyEvent e) {
        switch(e.getCode()) {
        case LEFT -> {
            orbiterPanel.leftReleased();
        }
        case RIGHT -> {
            orbiterPanel.rightReleased();
        }
        default -> {}
        }
    } 

The superclass of all my "worlds/maps":

protected AtomicReference<Spaceship> shipReference = new AtomicReference<>();
    
    protected RotateTransition rotatingTransition;
    
    public OrbiterPanel() {
        rotatingTransition = new RotateTransition();
        rotatingTransition.setCycleCount(RotateTransition.INDEFINITE);
        rotatingTransition.setInterpolator(Interpolator.LINEAR);
    }
    
    protected void rightPressed() {
        if(rotatingTransition.getStatus().equals(Status.RUNNING)) return;
        rotatingTransition.stop();
        rotatingTransition.setNode(shipReference.get());
        rotatingTransition.setByAngle(360);
        rotatingTransition.setDuration(Duration.seconds(2));
        rotatingTransition.play();
    }
    
    protected void leftPressed() {
        if(rotatingTransition.getStatus().equals(Status.RUNNING)) return;
        rotatingTransition.stop();
        rotatingTransition.setNode(shipReference.get());
        rotatingTransition.setByAngle(-360);
        rotatingTransition.setDuration(Duration.seconds(2));
        rotatingTransition.play();
    }
    
    protected void rightReleased() {
        rotatingTransition.stop();
    }
    
    protected void leftReleased() {
        rotatingTransition.stop();
    } 

PerfectCirclePanel, a subclass of OrbiterPanel doesn't really do much besides displaying the spaceship and a planet. FXML:

<BorderPane prefHeight="607.0" prefWidth="877.0" xmlns="http://javafx.com/javafx/17" xmlns:fx="http://javafx.com/fxml/1" fx:controller="application.Controller">
   <center>
      <BorderPane fx:id="contentPane" prefHeight="704.0" prefWidth="963.0" style="-fx-background-color: black;">
      </BorderPane>
   </center>
</BorderPane>

Solution

  • There are two different ways to connect a controller to the UI defined by an FXML file.

    Either:

    1. Specify the controller class by providing a fx:controller attribute to the root element of the FXML. In this case, by default, a controller instance will be created from that class by invoking its no-argument constructor, and that instance will be the controller associated with the UI.

    Or:

    1. Call setController(...) on the FXMLLoader instance.

    When you call loader.load(), if a controller exists, any @FXML-annotated fields in the controller will be initialized from the corresponding FXML elements, and then the initialize() method will be called.

    In your code, you create two Controller instances: one is created from the fx:controller attribute, and associated with the UI. The other you create "by hand" in the Java code. The latter is not connected to the UI (the @FXML-annotated fields are not injected, and initialize() is not invoked), because you call setController(...) after calling load().

    Because there are two Controller instances, there are two OrbiterPanel instances. The one created from the fx:controller attribute is the one displayed in the UI (because that's the one referenced when initialize() is invoked). The one created from the Controller instance you create in code is not displayed; however that is the one referenced by your event handler.

    Remove the fx:controller attribute, and move the call to setController() before the call to load().