Wednesday, July 19, 2017

Simulating a macOS Sheet Window in JavaFX

Simulating a macOS Sheet Window in JavaFX



For those who enjoy programming in JavaFX but appreciate the elegance of the macOS interface, one item that will promptly be noticed by its absence in JavaFX is the drop-down or sheet window that has been a common feature of macOS system software. 

Here is a screen shot of the feature within a macOS program not written in Java:




It is a nice feature when trying to obtain additional information from the user but want to maintain the context of a primary window. Though dialog windows in JavaFX can be made modal, they still "float" above the main or primary window and can be moved around, and can create a cluttered look. The elegance of the macOS sheet window is that it is attached to the primary window, and simply "slides" out of view when the user is finished, leaving the user where he or she left off before initiating the additional process.

Fortunately, because of the animation features of JavaFX, this can be simulated fairly easily and look similar to macOS by using the TranslateTransition method.

Below is the code showing how to obtain this effect.

Assuming you have a JavaFX program and a primary window, the sheet window is called from the primary window usually by the user pushing a button, or selecting some other option from the primary window:

The following code is from the controller of the primary window. The call to the window and the translate transition are placed into the button's action event:

@FXML
    private void buttonAction(ActionEvent e) {
    try {
           //Obtain the form you wish to use as a sheet window:
            String url = "SheetWindowForm.fxml";
            InputStream fxmlStream = getClass().getResourceAsStream(url);
            FXMLLoader loader = new FXMLLoader();
            AnchorPane anchorPane = (AnchorPane) loader.load(fxmlStream);
            
            // get the "owning window" for the sheet:
            Button source = (Button) e.getSource();
            Stage owningWindow = (Stage) source.getScene().getWindow();
            // create the sheet window
            Stage sheetWindow = new Stage(StageStyle.TRANSPARENT);
            sheetWindow.initModality(Modality.APPLICATION_MODAL);
            // insures the sheet window "sticks" to its owner if owner window moved
            sheetWindow.initOwner(owningWindow);
            // then create  scene
            Scene scene = new Scene(anchorPane);
            scene.setFill(Color.TRANSPARENT);
            sheetWindow.setScene(scene);
            sheetWindow.show();
            double delta = (owningWindow.getWidth() - sheetWindow.getWidth()) / 2;
            // set window sheet just below window's title bar
            sheetWindow.setY(source.getScene().getWindow().getY() + source.getScene().getY());
            // center sheet horizontally in parent window
            sheetWindow.setX(source.getScene().getWindow().getX() + source.getScene().getX() +          delta);
            // Set up a translate transition to "drop" sourceWindow
            TranslateTransition transitDown = new TranslateTransition(Duration.seconds(0.5),
                    anchorPane);
            transitDown.setFromY(-scene.getHeight());
            transitDown.setToY(-12);
            transitDown.setCycleCount(1);
            transitDown.setAutoReverse(false);
            transitDown.play();
            owningWindow.requestFocus();
            owningWindow.toFront();
 } 
catch (Exception ex) {
            Logger.getLogger(ProviderListController.class.getName()).log(Level.SEVERE, null, ex);
        
}


To "draw up" the sheet when the user is finished with it, you implement a similar process in the action event of a button on the sheet window inside the controller for that window:

@FXML
    private void buttonAction(ActionEvent e) {
  
        Scene scene = ((Node) (e.getSource())).getScene();
        AnchorPane view = (AnchorPane) (scene.getRoot());
        // Set up a translate transition to "raise" window
        TranslateTransition transitUp = new TranslateTransition(Duration.seconds(0.5), view);
        transitUp.setFromY(-12);
        transitUp.setToY(-scene.getHeight());
        transitUp.setCycleCount(1);
        transitUp.setAutoReverse(false);
        transitUp.play();
        Node source = (Node) e.getSource();
        Stage owningWindow = (Stage) source.getScene().getWindow();
        transitUp.setOnFinished((ActionEvent closeDialogEvent) -> {
            owningWindow.close();
        });
    }