简体   繁体   中英

JavaFX TabPane sorting tabs creates havoc

JavaFX TabPane has a very strange behavior if you try to sort the tabs when there is not enough space to show all the tabs.

More precisely, the tab selection button (round button with a down facing arrow on the right of the tab pane header) that should show a drop-down list with all the tabs, doesn't show anything .

I've created a small test to reproduce the problem. Just click several times (till there is not enough space for all the tabs) on "Add new tab & sort" (or several times on "Add new tab" and then "Sort tabs"), and then click on the tab selection button in the top right corner... Just to see that it shows nothing at all!

在此处输入图片说明

Note that resizing the window to make all tabs fit, and then resizing it back so the tab selection button appears again, solves the issue.

Here is the code to reproduce. I'm using jdk1.8.0_92 . Looks like a JDK bug?

public class TabPaneTest extends Application {

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

int i = 1;

@Override
public void start(Stage primaryStage) throws Exception {
    TabPane tabPane = new TabPane();

    tabPane.getTabs().add(new Tab("My beautiful tab " + i, new TextArea("pane " + (i++))));

    Button add = new Button("Add new tab");
    add.setOnAction(event -> {
        tabPane.getTabs().add(new Tab("My beautiful tab " + i, new TextArea("pane " + (i++))));
    });

    Button addSort = new Button("Add new tab & sort");
    addSort.setOnAction(event -> {
        tabPane.getTabs().add(new Tab("My beautiful tab " + i, new TextArea("pane " + (i++))));
        tabPane.getTabs().sort((o1, o2) -> o2.getText().compareTo(o1.getText()));
    });
    Button sort = new Button("Sort tabs");
    sort.setOnAction(event -> {
        tabPane.getTabs().sort((o1, o2) -> o2.getText().compareTo(o1.getText()));
    });


    VBox vbox = new VBox(tabPane, new HBox(add, addSort, sort));

    primaryStage.setScene(new Scene(vbox));
    primaryStage.setWidth(400);
    primaryStage.setHeight(300);
    primaryStage.show();
}
}

There is a ListChangeListener in the TabPaneSkin for the Tab list, but somehow it's not working on sorting the list, as you already mentioned.

As a workaround you can put the tabs in a new list and apply it to the TabPane after it is sorted:

 List<Tab> tabs = new ArrayList(tabPane.getTabs());
 tabs.sort((o1, o2) -> o2.getText().compareTo(o1.getText()));
 tabPane.getTabs().clear();
 tabPane.getTabs().setAll(tabs);

I think I found the problem in the TabPaneSkin class' method removeTabs : it removes the entries from the tabHeaderArea.controlButtons.popup :

                // remove the menu item from the popup menu
            ContextMenu popupMenu = tabHeaderArea.controlButtons.popup;
            TabMenuItem tabItem = null;
            if (popupMenu != null) {
                for (MenuItem item : popupMenu.getItems()) {
                    tabItem = (TabMenuItem) item;
                    if (tab == tabItem.getTab()) {
                        break;
                    }
                    tabItem = null;
                }
            }
            if (tabItem != null) {
                tabItem.dispose();
                popupMenu.getItems().remove(tabItem);
            }
            // end of removing menu item

This is a problem because:

  1. the opposite method addTabs does not do the opposite (ie, does not add the items into the popup menu, and
  2. tabHeaderArea.controlButtons.popup manages its entries by itself, by subscribing to changes in the tabPane.getTabs() :

      tabPane.getTabs().addListener((ListChangeListener<Tab>) c -> setupPopupMenu()); 

So they both remove the items from the popup menu, but because setupPopupMenu is called before removeTabs , the entries are not re-added back when the tabs re-added with addTabs .

I removed the above lines from removeTabs method, and it works just fine.

Will submit a bug to JDK...

UPDATE:

Submitted a bug report at http://Bugs.java.com (review ID JI-9038050), but have little hope that it will be fixed (my last submitted bug report in September 2015 is still "pending").

In the meantime the ugly workaround (thanks to @jns) is to remove all the tabs, sort them, and then add back:

        List<Tab> tabs = Lists.newArrayList(tabPane.getTabs());
        tabs.sort((o1, o2) -> o2.getText().compareTo(o1.getText()));
        tabPane.getTabs().clear();
        tabPane.getTabs().setAll(tabs);

It is ugly, because you can actually see the tabs going away and then coming back.

UPDATE 2:

A nicer workaround (thanks again @jns) is to determine the correct position of the new tab before the insertion:

    Comparator<Tab> comparator = (o1, o2) -> o2.getText().compareTo(o1.getText());
    Button addSort = new Button("Add new tab, sorted");
    addSort.setOnAction(event -> {
        Tab newTab = new Tab("My beautiful tab " + i, new TextArea("pane " + (i++)));

        // THIS IS WRONG! See UPDATE 3 below:
        // int pos = Math.max(0, Collections.binarySearch(tabPane.getTabs(), newTab, comparator));
        tabPane.getTabs().add(pos, newTab);
    });

This only works obviously if the sorting order does not change every time a new tab is inserted. If you need to sort existing tabs with another sorting order, you will still need to remove all tabs, sort, then re-add (see workaround 1).

UPDATE 3:

It turns out that Java's binarySearch is only searching for exact match, and not returning the lower bound as I (seasoned C++ developer ;) would expect... So you need to put in place the following atrocity to find the insertion point:

    <...>
    int pos = 0;
    while(pos < tabPane.getTabs().size() && tabPane.getTabs().get(pos).getText().compareTo(newTab.getText()) < 0) {
        pos++;
    }
    tabPane.getTabs().add(pos, newTab);
    <...>

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM