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).
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?
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.
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.
Removing a tab calls removeTabs
, which is outlined below:
GROW
),
requestLayout
method, which itself is invoked asynchronously,NONE
),
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.
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).
Some synchronization on tabHeaderArea
.