4

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?

user1803551
  • 12,965
  • 5
  • 47
  • 74
Brian Nieves
  • 338
  • 2
  • 11
  • 2
    I think you found a bug. If you use 2 in `.add(2, yourTab)`, you will see that the new `Tab` is in a position one less than the position it's supposed to be in. – SedJ601 Dec 03 '17 at 08:41
  • Could you explain how that results in the behavior? I don't see how the selected tab affects their positions, because either way the first tab should be removed and the list updated, resulting in tab 2 being displayed (and selected) at the front, followed by tab 1 being placed behind it. Also if the width didn't matter, how come it changes the behavior? Edit: Yay bugs... – Brian Nieves Dec 03 '17 at 08:42
  • It has to be a bug. If you print the actual location of the Tab after you add it again it will show a different from the actual visual place. Now if you call `tabPane.autosize();` at the end of the key event it will fix the locations. – JKostikiadis Dec 03 '17 at 13:01
  • seems to work in fx9 – kleopatra Dec 05 '17 at 14:17
  • @kleopatra Did `TabPaneSkin` change its asynchronous behavior? – user1803551 Dec 05 '17 at 22:10
  • your example works as expected, that's all I can say ;) – kleopatra Dec 05 '17 at 22:59
  • @kleopatra it's not my example, but OK, I'll look at the source for 9. – user1803551 Dec 06 '17 at 18:58
  • @user1803551 well, if I don't adress any of the commentors, I intend to talk to the auhor of the post that I comment ;) In other words: the example in the question simply works as expected in fx9 – kleopatra Dec 06 '17 at 23:27
  • That's interesting for sure. I'm glad if that's the case, but for my project we're using 8 for sure. Thanks for the information though, and also the workaround below works wonders. Thanks @user1803551 ! – Brian Nieves Dec 07 '17 at 06:39
  • The bug is fixed in Java 11. – user1803551 Jan 13 '18 at 21:19

1 Answers1

2

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.

user1803551
  • 12,965
  • 5
  • 47
  • 74