How to swap screens in a JavaFX application in the controller class?

If there are 3 files in a JavaFX project; an FXML file, a controller for the FXML, and an application class; how can the controller respond to a button click (which works perfectly fine) with changing the screen on that click (which is normally done with stage.setScreen())? I have no reference to the stage (which is passed to the application class's start(Stage)).

Application sample:

public class JavaFXApplication4 extends Application {

    @Override
    public void start(Stage stage) throws Exception {
        Parent root = FXMLLoader.load(getClass().getResource("Sample.fxml"));

        Scene scene = new Scene(root);

        stage.setScene(scene);
        stage.show();
    }

    /**
     * The main() method is ignored in correctly deployed JavaFX application.
     * main() serves only as fallback in case the application can not be
     * launched through deployment artifacts, e.g., in IDEs with limited FX
     * support. NetBeans ignores main().
     *
     * @param args the command line arguments
     */
    public static void main(String[] args) {
        launch(args);
    }
}

FXML sample:

<?xml version="1.0" encoding="UTF-8"?>

<?import java.lang.*?>
<?import java.util.*?>
<?import javafx.scene.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>

<AnchorPane id="AnchorPane" prefHeight="200.0" prefWidth="320.0"  xmlns:fx="http://javafx.com/fxml" fx:controller="javafxapplication4.SampleController">
  <children>
  <Button id="button" fx:id="nextScreen" layoutX="126.0" layoutY="90.0" onAction="#handleButtonAction" text="Next Screen" />
  <Label fx:id="label" layoutX="126.0" layoutY="120.0" minHeight="16.0" minWidth="69.0" />
  </children>
</AnchorPane>

Controller sample:

public class SampleController implements Initializable {

    @FXML
    private Label label;

    @FXML
    private void handleButtonAction(ActionEvent event) {
        System.out.println("You clicked me!");
        label.setText("Hello World!");

        // Here I want to swap the screen!
    }

    @Override
    public void initialize(URL url, ResourceBundle rb) {
        // ...
    }    
}

Solution 1:

@FXML
private void handleButtonAction(ActionEvent event) {
    System.out.println("You clicked me!");
    label.setText("Hello World!");
    //Here I want to swap the screen!

    Stage stageTheEventSourceNodeBelongs = (Stage) ((Node)event.getSource()).getScene().getWindow();
    // OR
    Stage stageTheLabelBelongs = (Stage) label.getScene().getWindow();
    // these two of them return the same stage
    // Swap screen
    stage.setScene(new Scene(new Pane()));
}

Solution 2:

I found this old question while getting into Java and trying to solve the same thing. Since I wanted the scenes to remember the content between the switches, I couldn't use the accepted answer, because when switching between the scenes it instantiates them again (loosing their previous state).

Anyways the accepted answer and the answer to the similar question gave me a hints on how to switch the scenes without loosing their states. The main idea is to inject instance of the scene into another controller, so that the controller doesn't need to instantiate a fresh scene over and over again but can use already existing instance (with its state).

So here is the main class that instantiates the scenes:

public class Main extends Application {

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

    @Override
    public void start(Stage primaryStage) throws Exception {
        // getting loader and a pane for the first scene. 
        // loader will then give a possibility to get related controller
        FXMLLoader firstPaneLoader = new FXMLLoader(getClass().getResource("firstLayout.fxml"));
        Parent firstPane = firstPaneLoader.load();
        Scene firstScene = new Scene(firstPane, 300, 275);

        // getting loader and a pane for the second scene
        FXMLLoader secondPageLoader = new FXMLLoader(getClass().getResource("secondLayout.fxml"));
        Parent secondPane = secondPageLoader.load();
        Scene secondScene = new Scene(secondPane, 300, 275);

        // injecting second scene into the controller of the first scene
        FirstController firstPaneController = (FirstController) firstPaneLoader.getController();
        firstPaneController.setSecondScene(secondScene);

        // injecting first scene into the controller of the second scene
        SecondController secondPaneController = (SecondController) secondPageLoader.getController();
        secondPaneController.setFirstScene(firstScene);

        primaryStage.setTitle("Switching scenes");
        primaryStage.setScene(firstScene);
        primaryStage.show();
    }
}

And here are the both controllers:

public class FirstController {

    private Scene secondScene;

    public void setSecondScene(Scene scene) {
        secondScene = scene;
    }

    public void openSecondScene(ActionEvent actionEvent) {
        Stage primaryStage = (Stage)((Node)actionEvent.getSource()).getScene().getWindow();
        primaryStage.setScene(secondScene);
    }
}

yep, second one looks the same (some logic could probably be shared, but the current state is enough as a proof of concept)

public class SecondController {

    private Scene firstScene;

    public void setFirstScene(Scene scene) {
        firstScene = scene;
    }

    public void openFirstScene(ActionEvent actionEvent) {    
        Stage primaryStage = (Stage)((Node)actionEvent.getSource()).getScene().getWindow();
        primaryStage.setScene(firstScene);
    }
}