Search code examples
javajavafxtabsjavafx-2tabpanel

JavaFX TabPane tabs don't update position


I noticed that when adding and deleting tabs from a TabPane, it fails to match the position of the order of tabs in the underlying list. This only happens when at least one tab is hidden entirely due to the width of the parent. Here's some code that replicates the issue:

public class TabPaneTester extends Application {
    @Override
    public void start(Stage primaryStage) throws Exception {
        Scene scene = sizeScene();
        primaryStage.setMinHeight(200);
        primaryStage.setWidth(475);
        primaryStage.setScene(scene);
        primaryStage.show();
    }

    private Scene sizeScene(){
        TabPane tabPane = new TabPane();
        tabPane.setTabMinWidth(200);
        tabPane.getTabs().addAll(newTabs(3));
        Scene scene = new Scene(tabPane);
        scene.setOnKeyPressed(e -> tabPane.getTabs().add(1, tabPane.getTabs().remove(0)));
        return scene;
    }

    private static Tab[] newTabs(int numTabs){
        Tab[] tabs = new Tab[numTabs];
        for(int i = 0; i < numTabs; i++) {
            Label label = new Label("Tab Number " + (i + 1));
            Tab tab = new Tab();
            tab.setGraphic(label);
            tabs[i] = tab;
        }
        return tabs;
    }

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

}

When you press a key, it removes the first tab (at index 0) and puts it back at index 1, effectively swapping the first two tabs. However, when run the tabs don't actually visually swap (even though the tab switcher menu does switch their position).

Tab position off

If you change the width of the screen to include even a pixel of the third tab that was hidden (replace 475 with 500), it works as intended. Any clues as to how to fix this?


Solution

  • This is indeed a bug and I couldn't find it reported in the public JIRA it is now reported at https://bugs.openjdk.java.net/browse/JDK-8193495.

    All my analysis is based on the code in TabPaneSkin if you want to have a look yourself.

    Summary

    The problem arises when you remove and then add the tab "too quickly". When a tab is removed, asynchronous calls are made during the removal process. If you make another change such as adding a tab before the async calls finish (or at least "finish enough"), then the change procedure sees the pane at an invalid state.

    Details

    Removing a tab calls removeTabs, which is outlined below:

    1. Various internal removal methods are called.
    2. Then it checks if closing should be animated.
      • If yes (GROW),
        1. an animation queues a call to a requestLayout method, which itself is invoked asynchronously,
        2. and the animations starts (asynchronously) and the method returns.
      • If not (NONE),
        1. requestLayout is called immediately and the method returns.

    The time during which the pane is at an invalid state is the time from when the call returns until requestLayout returns (on another thread). This duration is equivalent to the duration of requestLayout plus the duration of the animation (if there is one), which is ANIMATION_SPEED = 150[ms]. Invoking addTabs during this time can cause undesired effects because the data needed to properly add the tab is not ready yet.

    Workaround

    Add an artificial pause between the calls:

    ObservableList<Tab> tabs = tabPane.getTabs();
    PauseTransition p = new PauseTransition(Duration.millis(150 + 20));
    scene.setOnKeyPressed(e -> {
        Tab remove = tabs.remove(0);
        p.setOnFinished(e2 -> tabs.add(1, remove));
        p.play();
    });
    

    This is enough time for the asynchronous calls to return (don't call the KeyPressed handler too quickly in succession because you will remove the tabs faster than they can be added). You can turn off the removal animation with

    tabPane.setStyle("-fx-close-tab-animation: NONE;");
    

    which allows you to decrease the pause duration. On my machine 15 was safe (here you can also call the KeyPressed handler quickly in succession because of the short delay).

    Possible fix

    Some synchronization on tabHeaderArea.