Search code examples
javajavafxparentchildrenpane

Load existing FXML from parent pane


I've been working on an application which has a mainPane pane, which stores multiple smaller panes within it.

I initially started by adding a listener for each button pressed, upon a specific button being pressed a correlating pane should appear. This worked fine, until I realised I was adding a new instance of the pane with each button press and ended up losing the progress on the initially selected panes somewhere in the background.

I since attempted to try and load the existing panes from the getChildren() method of the main parent pane, however I haven't had much success with this. I tried to find the solution everywhere on here, but I couldn't find anything relevant to my problem, or if I had spotted something I may have not understood it if at all.

My question is as follows: If I can add a child pane to the parent pane to start with using mainPane.getChildren().add(root), I then added a simple Boolean switch to be set if the pane is added and if a pane of this type has already been added - try and get it to show with the mainPane.getChildren().get(intArray[0]).toFront();. However, when adding this to all the other button handlers the .toFront() method seems to be showing the panes in the order they were added.

So if I initially press buttons trackerButton, progressButton, communityButton, blockerButton - and I press the trackerButton 4 times the pane will show -> trackerPane, progressPane,communityPane,blockerPane and then loop over these again if I keep pressing it.

Is there a cleaner way in which I can implement this, so that if I press a button multiple times, it solely loads it correlating pane, but doesn't add a new instance of it with the mainPane.getChildren().add(root); ?

Any help will be greatly appreciated, I have pasted my code below:

public class MainController {

private Client client;

@FXML
TrackerPaneController trackerPaneController;

@FXML
ProgressController progressController;

@FXML
CommunityController communityController;

@FXML
BlockerController blockerController;

@FXML
SettingsController settingsController;

@FXML
Label userLabel;

@FXML
Button logoutButton;

@FXML
Button trackerButton;

@FXML
Button progressButton;

@FXML 
Button communityButton;

@FXML
Button blockerButton;

@FXML
Button settingsButton;

@FXML
Pane mainPane;

Integer[] intArray = new Integer[4];
final Boolean[] booleanArray = {true,true,true,true,true};


@FXML
private void handlePanelButtonAction(ActionEvent event) throws IOException {


    if(event.getSource() == trackerButton) {

        if(booleanArray[0]) {


        FXMLLoader trackerLoader = new FXMLLoader(getClass().getResource("/view/TrackerPane.fxml"));
        Parent root = (Parent) trackerLoader.load();

        trackerPaneController = trackerLoader.getController();
        trackerPaneController.setClient(client);
        mainPane.getChildren().add(root);

        intArray[0] = mainPane.getChildren().indexOf(root);

        }

        mainPane.getChildren().get(intArray[0]).toFront();
        booleanArray[0]=false;

    } else if(event.getSource() == progressButton) {

        if(booleanArray[1]) {
        FXMLLoader loader = new FXMLLoader(getClass().getResource("/view/ProgressPane.fxml"));
        Parent root = (Parent) loader.load();
        progressController = loader.getController();
        progressController.setClient(client);
        mainPane.getChildren().add(root);
        intArray[1] = mainPane.getChildren().indexOf(root);

        }

        mainPane.getChildren().get(intArray[1]).toFront();
        booleanArray[1]=false;

    } else if(event.getSource() == communityButton) {

        if(booleanArray[2]) {
        FXMLLoader loader = new FXMLLoader(getClass().getResource("/view/CommunityPane.fxml"));
        Parent root = (Parent) loader.load();
        communityController = loader.getController();
        communityController.setClient(client);
        mainPane.getChildren().add(root);
        intArray[2] = mainPane.getChildren().indexOf(root);

        }

        mainPane.getChildren().get(intArray[2]).toFront();
        booleanArray[2]=false;

    } else if(event.getSource() == blockerButton) {

        if(booleanArray[3]) {
        FXMLLoader loader = new FXMLLoader(getClass().getResource("/view/BlockerPane.fxml"));
        Parent root = (Parent) loader.load();
        blockerController = loader.getController();
        blockerController.setClient(client);
        mainPane.getChildren().add(root);
        intArray[3] = mainPane.getChildren().indexOf(root);

        }

        mainPane.getChildren().get(intArray[3]).toFront();
        booleanArray[3]=false;

    } else if(event.getSource() == settingsButton) {

        if(booleanArray[4]) {
        FXMLLoader loader = new FXMLLoader(getClass().getResource("/view/SettingsPane.fxml"));
        Parent root = (Parent) loader.load();
        settingsController = loader.getController();
        settingsController.setClient(client);
        mainPane.getChildren().add(root);
        intArray[4] = mainPane.getChildren().indexOf(root);

        }

        mainPane.getChildren().get(intArray[4]).toFront();
        booleanArray[4]=false;

    } else if(event.getSource() == logoutButton) {

        client.logout();
        Platform.exit();
    }
}

}

I feel like the solution is very simple for someone with the right skillset, however I am still very new to JavaFX and any help will be greatly appreciated.

Kind Regards


Solution

  • The problem is that calling toFront on a Node results in the node being moved to the last index of the parent layout. This results in some indices becoming wrong in some cases. To fix this simply store the Node, not the index in your data structure.

    Also note thet basically all loading code looks the same except for some minor differences. You should avoid repeating code since it makes your code harder to maintain. The repeated code could easily be removed by adding a interface to your controllers.

    @FXML
    private void handlePanelButtonAction(ActionEvent event) throws IOException {
        if(event.getSource() == trackerButton) {
            ...
        } else if(event.getSource() == progressButton) {
            ...
        } else if(event.getSource() == communityButton) {
            ...
        } else if(event.getSource() == blockerButton) {
            ...
        } else if(event.getSource() == settingsButton) {
            ...
        } else if(event.getSource() == logoutButton) {
            ...
        }
    }
    

    Don't do something like this. If the logic is completely different, a Button should receive it's own event handler method. Even if the code is similar enough, you should avoid checking reference equality of the source with some of your nodes. Instead you can store information in the userData or the properties of the Node.

    Recommendation

    Implement the following interface with your controllers:

    public interface ClientContainer {
        void setClient(Client client);
    }
    

    Initialize the userData of your buttons with the URLs:

    public class MainController {
    
        ...
    
        private void setResource(Node node, String resource) {
            URL url = getClass().getResource(resource);
            if (url == null) {
                throw new IllegalArgumentException("Resource not available: " + resource);
            }
            node.setUserData(url);
        }
    
        private static URL getResource(Object source) {
            return (URL) ((Node) source).getUserData();
        }
    
        @FXML
        private void initialize() {
            setResource(trackerButton, "/view/TrackerPane.fxml");
            setResource(progressButton, "/view/ProgressPane.fxml");
            setResource(communityButton, "/view/CommunityPane.fxml");
            setResource(blockerButton, "/view/BlockerPane.fxml");
            setResource(settingsButton, "/view/SettingsPane.fxml");
        }
    }
    

    Use this data and a Map to avoid reloading the scenes. (You also need to move the code for handling the logoutButton click to a different method.)

    private final Map<URL, Node> loadedContent = new HashMap<>();
    
    @FXML
    private void handlePanelButtonAction(ActionEvent event) {
        loadedContent.computeIfAbsent(getResource(event.getSource()), url -> {
            try {
                FXMLLoader loader = new FXMLLoader(url);
                Node result = loader.load();
                ClientContainer controller = loader.getController();
                controller.setClient(client);
                mainPane.getChildren().add(result);
                return result;
            } catch (IOException ex) {
                throw new RuntimeException(ex);
            }
        }).toFront();
    }