3

This is a cross-post: http://mail.openjdk.java.net/pipermail/openjfx-dev/2015-January/016437.html

Sometimes when I'm setting the divider positions of a SplitPane programmatically my value gets overridden again by some JavaFX internal layout code.

For debugging purposes I've added a listener to the position property of the divider and have thrown a RuntimeException to see the stack trace. It looks like this:

at 
com.sun.javafx.binding.ExpressionHelper$Generic.fireValueChangedEvent(ExpressionHelper.java:361)
        at 
com.sun.javafx.binding.ExpressionHelper.fireValueChangedEvent(ExpressionHelper.java:81)
        at 
javafx.beans.property.DoublePropertyBase.fireValueChangedEvent(DoublePropertyBase.java:106)
        at 
javafx.beans.property.DoublePropertyBase.markInvalid(DoublePropertyBase.java:113)
        at 
javafx.beans.property.DoublePropertyBase.set(DoublePropertyBase.java:146)
        at javafx.scene.control.SplitPane$Divider.setPosition(SplitPane.java:486)
        at 
com.sun.javafx.scene.control.skin.SplitPaneSkin.setAbsoluteDividerPos(SplitPaneSkin.java:310)
        at 
com.sun.javafx.scene.control.skin.SplitPaneSkin.setupContentAndDividerForLayout(SplitPaneSkin.java:502)
        at 
com.sun.javafx.scene.control.skin.SplitPaneSkin.layoutChildren(SplitPaneSkin.java:817)
        at javafx.scene.control.Control.layoutChildren(Control.java:589)
        at javafx.scene.Parent.layout(Parent.java:1074)
        at javafx.scene.Parent.layout(Parent.java:1080)
        at javafx.scene.Parent.layout(Parent.java:1080)
        at javafx.scene.Parent.layout(Parent.java:1080)
        at javafx.scene.Parent.layout(Parent.java:1080)
        at javafx.scene.Parent.layout(Parent.java:1080)
        at javafx.scene.Parent.layout(Parent.java:1080)
        at javafx.scene.Parent.layout(Parent.java:1080)
        at javafx.scene.Parent.layout(Parent.java:1080)
        at javafx.scene.Scene.doLayoutPass(Scene.java:532)
        at javafx.scene.Scene$ScenePulseListener.pulse(Scene.java:2363)
        at com.sun.javafx.tk.Toolkit.lambda$runPulse$28(Toolkit.java:314)
        at com.sun.javafx.tk.Toolkit$$Lambda$230/25595560.run(Unknown Source)
        at java.security.AccessController.doPrivileged(Native Method)
        at com.sun.javafx.tk.Toolkit.runPulse(Toolkit.java:313)
        at com.sun.javafx.tk.Toolkit.firePulse(Toolkit.java:340)
        at com.sun.javafx.tk.quantum.QuantumToolkit.pulse(QuantumToolkit.java:451)
        at com.sun.javafx.tk.quantum.QuantumToolkit.pulse(QuantumToolkit.java:431)
        at 
com.sun.javafx.tk.quantum.QuantumToolkit.lambda$runToolkit$363(QuantumToolkit.java:298)
        at 
com.sun.javafx.tk.quantum.QuantumToolkit$$Lambda$59/174792896.run(Unknown 
Source)
        at 
com.sun.glass.ui.InvokeLaterDispatcher$Future.run(InvokeLaterDispatcher.java:95)
        at com.sun.glass.ui.gtk.GtkApplication._runLoop(Native Method)
        at 
com.sun.glass.ui.gtk.GtkApplication.lambda$null$45(GtkApplication.java:126)
        at com.sun.glass.ui.gtk.GtkApplication$$Lambda$55/1472148546.run(Unknown 
Source)
        at java.lang.Thread.run(Thread.java:745)

It doesn't seem to get triggered by some code of mine (at least not directly according to the stack trace).

I tried several things including setting the prefWidth/ prefHeight of the items to the expected value, but I couldn't stop the SplitPane to effectively "hide" the first and the third (last) item during a re-layout.

The two dividers changed during the layoutChildren call as follows:

Divider position changed: old value: 0.10857763300760044, new value: 0.004343105320304018

Divider position changed: old value: 0.8914223669923995, new value: 0.995656894679696

I had a look at the com.sun.javafx.scene.control.skin.SplitPaneSkin (source code provided with the JDK), but unfortunatly it's not easy there to see how the areas are calculated in this situation.

Why is this re-layout happing here? Why does it override the values I've set for the divider positions? How can stop this?

I've tried to write a SSCCE but I couldn't reproduce the issue with a simple sample yet. (The issue happens in a Skin implementation of a custom control.)

Update

The 3 items (subclasses of Controls) of the SplitPane in question (orientation: vertical) have the following min and max heights:

property: height min: -1.0 ; max: -1.0
property: height min: -1.0 ; max: -1.0
property: height min: -1.0 ; max: -1.0

AFAIK this is the value of Region.USE_COMPUTED_SIZE.

I changed the max value to Double.MAX_VALUE:

property: height min: -1.0 ; max: 1.7976931348623157E308 
property: height min: -1.0 ; max: 1.7976931348623157E308 
property: height min: -1.0 ; max: 1.7976931348623157E308

but I'm still getting the same issue.

Update 2

I finally managed to create a SSCCE:

package splitpanelayoutissue;

import javafx.application.Application;
import javafx.geometry.Orientation;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.control.SplitPane;
import javafx.scene.control.Tab;
import javafx.scene.control.TabPane;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;

public class SplitPaneLayoutIssue extends Application {

    private static final double DIVIDER_POSITION_0 = 0.10834236186348863;
    private static final double DIVIDER_POSITION_1 = 0.8916576381365114;

    @Override
    public void start(Stage primaryStage) {
        SplitPane outerSplitPane = createOuterSplitPane();

        StackPane root = new StackPane();
        root.getChildren().add(outerSplitPane);

        Scene scene = new Scene(root, 1500, 1000);

        primaryStage.setTitle("SplitPane Layout Issue");
        primaryStage.setScene(scene);
        primaryStage.show();
    }

    private SplitPane createOuterSplitPane() {
        SplitPane outerSplitPane = createSplitPane("outerSplitPane");

        TabPane leftTabPane = addTabPane(outerSplitPane, "Left");

        SplitPane innerSplitPane = createInnerSplitPane();
        outerSplitPane.getItems().add(innerSplitPane);

        TabPane rightTabPane = addTabPane(outerSplitPane, "Right");

        SplitPane.setResizableWithParent(leftTabPane, Boolean.FALSE);
        SplitPane.setResizableWithParent(rightTabPane, Boolean.FALSE);
        SplitPane.setResizableWithParent(innerSplitPane, Boolean.TRUE);

        setDividerPositions(outerSplitPane, innerSplitPane);

        leftTabPane.getTabs().get(0).setOnClosed(event -> {
            // the following line causes com.sun.javafx.scene.control.skin.SplitPaneSkin.Content.getArea() 
            // of all 3 items return 0, even though setDividerPositions is called before the next SplitPane.layoutChildren call
            innerSplitPane.getItems().setAll(innerSplitPane.getItems().get(0),innerSplitPane.getItems().get(1),innerSplitPane.getItems().get(2));

            outerSplitPane.getItems().setAll(innerSplitPane, rightTabPane);
            setDividerPositions(outerSplitPane, innerSplitPane);
        });

        return outerSplitPane;
    }

    private SplitPane createSplitPane(String name) {
        SplitPane splitPane = new SplitPane() {

            @Override
            protected void layoutChildren() {
                System.out.println("Calling relayout on " + name + "...");
                com.sun.javafx.scene.control.skin.SplitPaneSkin skin = (com.sun.javafx.scene.control.skin.SplitPaneSkin) getSkin();
                super.layoutChildren();
            }

        };
        return splitPane;
    }

    private void setDividerPositions(SplitPane outerSplitPane, SplitPane innerSplitPane) {
        System.out.println("Set divider positions...");
        System.out.println("Set divider position 0 of outerSplitPane...");
        outerSplitPane.setDividerPosition(0, outerSplitPane.getItems().size() == 3 ? DIVIDER_POSITION_0
                : DIVIDER_POSITION_1);
        if (outerSplitPane.getItems().size() == 3) {
            System.out.println("Set divider position 1 of outerSplitPane...");
            outerSplitPane.setDividerPosition(1, DIVIDER_POSITION_1);
        }
        System.out.println("Set divider position 0 of innerSplitPane...");
        innerSplitPane.setDividerPosition(0, DIVIDER_POSITION_0);
        System.out.println("Set divider position 1 of innerSplitPane...");
        innerSplitPane.setDividerPosition(1, DIVIDER_POSITION_1);
    }

    private TabPane addTabPane(SplitPane splitPane, String label) {
        TabPane tabPane = new TabPane();
//        tabPane.setFocusTraversable(false);
        Tab tab = new Tab(label);
        tab.setContent(new Label(label));
        tabPane.getTabs().add(tab);
        splitPane.getItems().add(tabPane);
        return tabPane;
    }

    private SplitPane createInnerSplitPane() {
        SplitPane innerSplitPane = createSplitPane("innerSplitPane");
        innerSplitPane.setOrientation(Orientation.VERTICAL);

        TabPane topTabPane = addTabPane(innerSplitPane, "Top");
        TabPane centerTabPane = addTabPane(innerSplitPane, "Center");
        TabPane bottomTabPane = addTabPane(innerSplitPane, "Bottom");

        SplitPane.setResizableWithParent(topTabPane, Boolean.FALSE);
        SplitPane.setResizableWithParent(bottomTabPane, Boolean.FALSE);

        SplitPane.setResizableWithParent(centerTabPane, Boolean.TRUE);

        return innerSplitPane;
    }

    /**
     * @param args the command line arguments
     */
    public static void main(String[] args) {
        launch(args);
    }

}

If you close the left tab, then the top and bottom tab get hidden (size 0), although the divider positions are set before the layoutChildren() call (see output).

I get this situation because I need to reset the items of the splitPane in my code. Also note that the items have different resizableWithParent properties.

Update 3

Here is a smaller SSCCE with only one single SplitPane. When you press the "Grow width" button you'll see the same effect.

package splitpanelayoutissue;

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

public class SplitPaneLayoutIssue2 extends Application {

    private static final double DIVIDER_POSITION_0 = 0.10834236186348863;
    private static final double DIVIDER_POSITION_1 = 0.8916576381365114;

    @Override
    public void start(Stage primaryStage) {
        SplitPane splitPane = createSplitPane();

        StackPane root = new StackPane();
        root.getChildren().add(splitPane);

        Scene scene = new Scene(root, 1500, 1000);

        primaryStage.setTitle("SplitPane Layout Issue");
        primaryStage.setScene(scene);
        primaryStage.show();
    }

    private SplitPane createSplitPane(String name) {
        SplitPane splitPane = new SplitPane() {

            @Override
            protected void layoutChildren() {
                System.out.println("Calling relayout on " + name + "...");
                com.sun.javafx.scene.control.skin.SplitPaneSkin skin = (com.sun.javafx.scene.control.skin.SplitPaneSkin) getSkin();
                super.layoutChildren();
            }

        };
        return splitPane;
    }

    private void setDividerPositions(SplitPane innerSplitPane) {
        System.out.println("Set divider positions...");
        System.out.println("Set divider position 0 of innerSplitPane...");
        innerSplitPane.setDividerPosition(0, DIVIDER_POSITION_0);
        System.out.println("Set divider position 1 of innerSplitPane...");
        innerSplitPane.setDividerPosition(1, DIVIDER_POSITION_1);
    }

    private TabPane addTabPane(SplitPane splitPane, String label) {
        TabPane tabPane = new TabPane();
//        tabPane.setFocusTraversable(false);
        Tab tab = new Tab(label);
        tab.setContent(new Label(label));
        tabPane.getTabs().add(tab);
        splitPane.getItems().add(tabPane);
        return tabPane;
    }

    private SplitPane createSplitPane() {
        SplitPane splitPane = createSplitPane("splitPane");
        splitPane.setOrientation(Orientation.VERTICAL);

        TabPane topTabPane = addTabPane(splitPane, "Top");

        TabPane centerTabPane = new TabPane();
//        tabPane.setFocusTraversable(false);
        Tab tab = new Tab("Center");
        Button button = new Button("Grow width");
        tab.setContent(button);
        button.setOnAction(event -> {
            System.out.println("Growing window width...");
            // the following line causes com.sun.javafx.scene.control.skin.SplitPaneSkin.Content.getArea() 
            // of all 3 items return 0, even though setDividerPositions is called before the next SplitPane.layoutChildren call
            // Without this line the code works fine.
            splitPane.getItems().setAll(splitPane.getItems().get(0), splitPane.getItems().get(1), splitPane.getItems().get(2));

            // This line is needed to trigger the execution path in question
            splitPane.getScene().getWindow().setWidth(splitPane.getScene().getWindow().getWidth() + 20.0);
            setDividerPositions(splitPane);
        });
        centerTabPane.getTabs().add(tab);

        splitPane.getItems().add(centerTabPane);

        TabPane bottomTabPane = addTabPane(splitPane, "Bottom");

        setDividerPositions(splitPane);
        SplitPane.setResizableWithParent(topTabPane, Boolean.FALSE);
        SplitPane.setResizableWithParent(bottomTabPane, Boolean.FALSE);

        SplitPane.setResizableWithParent(centerTabPane, Boolean.TRUE);

        return splitPane;
    }

    /**
     * @param args the command line arguments
     */
    public static void main(String[] args) {
        launch(args);
    }

}

Is this a JavaFX bug or something I can influence somehow?

Puce
  • 37,247
  • 13
  • 80
  • 152
  • 1
    It's pretty much impossible to find an answer unless you can figure out an SSCCE, (as you know). `SplitPane`s respect the `min`/`max` `width`/`height` properties of their contents, so if you are manipulating those, that may be a possible cause. – James_D Jan 15 '15 at 02:30
  • not surprisingly, there is nothing we can do, if it is introduced/somehow depends by that custom component, as you well know and @James_D already noted ;-) In your shoes I would strip down the custom to the barest minimum - just a control with a colored background and its custom skin with the same sizing characteristics as the real thing. Then add/remove all the other parts one by one until you hit the reason. Simple Ol' Bug Huntin' :-) – kleopatra Jan 15 '15 at 10:29
  • just seeing the gtk in the stack trace, is it Linux? How does it behave on other OS? Which jdk exactly? – kleopatra Jan 15 '15 at 10:31
  • @James_D I know, it's hard to help without a SSCCE and I'm still working on this. But I think I basically have to understand how SplitPaneSkin.layoutChildren works and I was hoping someone could shed some light. Not every call to layoutChildren changes the divider positions, but sometimes they change. What is the algorithm there (roughly)? – Puce Jan 15 '15 at 13:18
  • I had a look at the source for `SplitPaneSkin` and it would take a while for me to figure out how it all worked. As far as I could tell (from a fairly cursory look), it respects the `dividerPositions` assigned to the split pane unless they violate the `minWidth` or `maxWidth` of the content. – James_D Jan 15 '15 at 19:38
  • @kleopatra I think in this case (there are three custom controls working together) it's easier to go for a SSCCE, but even this is not easy. Regarding gtk: I'm running Kubuntu 14.04 and Oracle JDK 1.8.0_25. But the gtk and toolkit parts are just about threading and managing the relayouting pulse I think. But I will see if I can check it on a different OS as well. – Puce Jan 15 '15 at 22:03
  • a possible relation of the min/max constraint (as @James_D suggested) is what you also got from the openjdk mailinglist .. jsut saying :-) – kleopatra Jan 15 '15 at 23:45
  • Can you log the min/max width of the content nodes of the split pane in your listener for the divider positions? That would at least help understand if that's the issue... – James_D Jan 16 '15 at 17:01
  • @James_D I don't think I'm changing any min/ max values of the content nodes, but still it's a good idea. I will update the question once I have the data. – Puce Jan 16 '15 at 17:12
  • @kleopatra I'm aware of this, but since I'm not really changing the min/ max values, this information alone doesn't get me any further, I think. – Puce Jan 16 '15 at 22:18
  • @kleopatra It behaves in the same way on Windows 8.1, with the difference that it's easier to debug (KDE sometimes freezes during debugging :-( ) – Puce Feb 01 '15 at 12:55

3 Answers3

2

I think the important part of the stack trace is this:

com.sun.javafx.scene.control.skin.SplitPaneSkin.setAbsoluteDividerPos(SplitPaneSkin.java:310) at com.sun.javafx.scene.control.skin.SplitPaneSkin.setupContentAndDividerForLayout(SplitPaneSkin.java:502) at com.sun.javafx.scene.control.skin.SplitPaneSkin.layoutChildren(SplitPaneSkin.java:817)

As you can see the JavaFX default layout mechanism (layoutChildren()) is implemented for the SplitPane in a way that can affect the position of the divider. the setupContentAndDividerForLayout() method is called in some internal if statements in the layoutChildren() method. Therefore I would debug this method and check why setupContentAndDividerForLayout() is called. by doing so you can find the reason why the SplitPane thinks it must change the position of the divider.

Hendrik Ebbers
  • 2,570
  • 19
  • 34
1

It seems to be something introduced lately into JavaFX. I tested this on Windows 7 with both Java 8u20 (b26) and Java 8u40ea (early adopter) (b23).

Using your SSCCE, I see that with Java 8u20, if I resize the window's height to be smaller and then click on Grow Width, the upper and lower tabs remain the same height they were before.

If I do the same thing with Java 8u40ea, the upper and lower tabs get their own heights resized to be smaller when I resize the window to be smaller and subsequently click on the button. If I resize the window to be tall again, the tabs once again resize to be large (as before) when I click on the button.

Sounds like something to open with the JavaFX guys; I personally submitted a SSCCE just a couple of days ago, and they are very responsive.

Ahmed
  • 590
  • 8
  • 19
  • By the way, I'm not sure my answer is worth any bounty at all, but I just wanted to answer your question of whether it appears to be a bug. Good luck! – Ahmed Feb 02 '15 at 09:04
  • Thanks for the additional hints. Unfortunately I cannot split the bounty. – Puce Feb 07 '15 at 16:45
0

As it seems to be a JavaFX issue, I filed an issue here: https://javafx-jira.kenai.com/browse/RT-40031

Update:

The issue could be reproduced but was already fixed in the upcoming JavaFX 8u40 release.

Puce
  • 37,247
  • 13
  • 80
  • 152