0

In the following minimal example when the parents of tabs and tabs2 are printing out, they are both null.

From this question I've come to understand because while the two TabPanes have been added to the SplitPane, because the skin of the TabPanes hasn't been created yet, getScene and getParent will return null.

So the question is, how can I get access to their parents at the point in code where I am trying to print it out? I presume I need to force the creation of the skin?

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.SplitPane;
import javafx.scene.control.TabPane;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;


public class MainApp extends Application {

    @Override
    public void start(Stage stage) throws Exception {
        Button button = new Button("click");
        StackPane root = new StackPane(button);

        button.setOnAction( event -> {
            SplitPane sp = new SplitPane();
            TabPane tabs1 = new TabPane();
            TabPane tabs2 = new TabPane();
            sp.getItems().addAll(tabs1, tabs2);
            root.getChildren().add(sp);

            System.out.println(tabs1.getParent());
            System.out.println(tabs2.getParent());
        });

        Scene scene = new Scene(root);
        stage.setTitle("JavaFX and Gradle");
        stage.setScene(scene);
        stage.show();
}

public static void main(String[] args) {
    launch(args);
}
}
AlwaysNeedingHelp
  • 1,851
  • 3
  • 21
  • 29
  • what do you _really_ want to achieve? it doesn't make sense to add items to a splitpane and trying to access the parent without adding the splitpane to the scenegraph – kleopatra Sep 12 '21 at 20:42
  • I'm writing an algorithm to rebuild a tree of stackpanes/tabpanes that was previously saved by the user. I explain it a bit more here: https://github.com/panemu/tiwulfx-dock/issues/4#issuecomment-917671525 I run into the problem where my algorithm is trying to do some operation requiring a TabPanes parent but because it has needed to move that same TabPane into some other SplitPane when rebuilding the UI structure its parent is null. – AlwaysNeedingHelp Sep 12 '21 at 21:00
  • @kleopatra I created an example program here to demonstrate exactly where it fails: https://github.com/SKeeneCode/dock-test – AlwaysNeedingHelp Sep 12 '21 at 22:08
  • [mcve] please .. here not anywhere else ;) Note that this is _not_ a personal support forum but a community effort to build a knowledge base - questions without context (the reference will get lost over time) will not be overly helpful to future readers – kleopatra Sep 13 '21 at 09:31

2 Answers2

2

May be instead of trying to rely on the node rendering, you can consider trying to change your logic by keeping references to parent nodes. That way you dont need to rely on node rendering.

Below are the changes i tried and it works as expected.

#Change 1:

Add the below instance variable to DetachableTabPane.java, to let know in which split pane this is set. That way you dont need to loop through all the nodes, to find the parent SplitPane.

private TabSplitPane parentSplitPane;

#Change 2:

Create a custom SplitPane that register itself to its children. So you dont need to worry to set it everytime when you add a DetachableTabPane to SplitPane.

class TabSplitPane extends SplitPane {
    public TabSplitPane() {
        getItems().addListener((ListChangeListener) e -> {
            if (e.next()) {
                e.getAddedSubList().stream().filter(o -> o instanceof DetachableTabPane).forEach(tp -> ((DetachableTabPane) tp).parentSplitPane = TabSplitPane.this);
                e.getRemoved().stream().filter(o -> o instanceof DetachableTabPane).forEach(tp -> ((DetachableTabPane) tp).parentSplitPane = null);
            }
        });
    }
}

# Change 3:

Update the placeTab method as below. This way you directly deal with the SplitPane instance associated to the DetachableTabPane and not worrying about when the node(s) will be rendered.

public void placeTab(Tab tab, Pos pos) {
    boolean addToLast = pos == Pos.CENTER_RIGHT || pos == Pos.BOTTOM_CENTER;
    DetachableTabPane dt = detachableTabPaneFactory.create(this);
    dt.getTabs().add(tab);

    Orientation requestedOrientation = Orientation.HORIZONTAL;
    if (pos == Pos.BOTTOM_CENTER || pos == Pos.TOP_CENTER) {
        requestedOrientation = Orientation.VERTICAL;
    }

    TabSplitPane targetSplitPane = parentSplitPane;
    // If there is no splitPane parent... Create one!!
    if (targetSplitPane == null) {
        targetSplitPane = new TabSplitPane();
        targetSplitPane.setOrientation(requestedOrientation);

        Pane parent = (Pane) getParent();
        int indexInParent = parent.getChildren().indexOf(DetachableTabPane.this);
        parent.getChildren().remove(DetachableTabPane.this);
        if (addToLast) {
            targetSplitPane.getItems().addAll(DetachableTabPane.this, dt);
        } else {
            targetSplitPane.getItems().addAll(dt, DetachableTabPane.this);
        }
        parent.getChildren().add(indexInParent, targetSplitPane);

    } 
    //  If the orientation is changed... create a new split pane.
    else if (targetSplitPane.getOrientation() != requestedOrientation) {
        TabSplitPane parent = targetSplitPane;
        int indexInParent = parent.getItems().indexOf(DetachableTabPane.this);
        parent.getItems().remove(DetachableTabPane.this);

        targetSplitPane = new TabSplitPane();
        targetSplitPane.setOrientation(requestedOrientation);
        if (addToLast) {
            targetSplitPane.getItems().addAll(DetachableTabPane.this, dt);
        } else {
            targetSplitPane.getItems().addAll(dt, DetachableTabPane.this);
        }
        parent.getItems().add(indexInParent, targetSplitPane);

    } else {
        if (addToLast) {
            parentSplitPane.getItems().add(dt);
        } else {
            int indexInParent = targetSplitPane.getItems().indexOf(DetachableTabPane.this);
            parentSplitPane.getItems().add(indexInParent, dt);
        }
    }
}
Sai Dandem
  • 8,229
  • 11
  • 26
  • What a remarkably creative solution! Tried it out and it works. I also found calling applyCss() on the innerSplitPane also solves it, although that forces a CSS pass where as this solution avoids it. While that is a one-liner looking at the documentation it appears it can be quite computationally expensive so I should probably favor this solution. Thank you again for taking time to come up with a solution! – AlwaysNeedingHelp Sep 13 '21 at 14:39
1

Applying CSS and generating a layout pass will force the creation of any nodes managed by the CSS or the control's skin.

The layout pass application can be for the entire scene graph if you apply it to the root of the scene graph. Or just the part that has changed (is dirty) if applied to a subtree of the scene graph.

Here is an example based on the code in your question:

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;

public class MainApp extends Application {

    @Override
    public void start(Stage stage) throws Exception {
        Button button = new Button("click");
        StackPane root = new StackPane(button);

        button.setOnAction(event -> {
            SplitPane sp = new SplitPane();
            TabPane tabs1 = new TabPane();
            TabPane tabs2 = new TabPane();
            sp.getItems().addAll(tabs1, tabs2);
            root.getChildren().add(sp);

            // force a layout pass.
            sp.applyCss();
            sp.layout();

            System.out.println(tabs1.getParent());
            System.out.println(tabs2.getParent());
        });

        Scene scene = new Scene(root);
        stage.setTitle("JavaFX and Gradle");
        stage.setScene(scene);
        stage.show();
    }

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

With these changes, the code which outputs the parent node of the tab panes prints out a non-null value (a reference to the enclosing split-pane skin). It is important to note that it is not a reference to the enclosing SplitPane control instance, but merely the visual representation of the control (the skin). This is an important distinction. It answers your question, but the answer may not be as directly applicable to your application as you had thought it might be.

Advice on this approach

The rest here is background info and generic advice on API usage in JavaFX, so ignore if not relevant to your particular case.

In general, I'd advise keeping references and structures for the relationships between important items separately, rather than relying on the parent/child relationships of nodes. That way you can deal with things at a higher level (e.g. the control classes themselves) rather than underlying nodes for skins and nodes which are created within the skin (which are fragile relationships and may change between JavaFX versions and skin implementations, breaking your code).

For controls, they usually have APIs to describe their relationships independent of the scene graph. Making use of these where you can is preferred to querying scene graph relationships for things like parent/child relationships. For example, you can get all the children of a split pane using the [splitPane.getItems()] call. Or, all of the tabs in a TabPane using the [tabPane.getTabs()] call. Note, a tab isn't even a node, so using the scene graph to try to find or manage it isn't appropriate. Similar object relationships outside of the scene graph are maintained for other controls, like menu items. Complex controls such as color pickers can have combo boxes that feature pop-ups that even have nodes in different pop-up windows featuring entirely different scene graphs from the parent nodes.

Usually, if you use FXML, the important references are injected into the application code within the controller, so there isn't additional work you need to do. If you are dynamically changing the structure of the existing scene graph (which it sounds like you are doing), then you might need to do some more work and create some custom data structures to manage references and sync them up with what is in the scene graph.

jewelsea
  • 150,031
  • 14
  • 366
  • 406