JavaFX ListChangeListener: getPermutation() not working

Viewed 61

I use ListChangeListener to listen to changes in Tab Pane.

private final TabPane tabBar = new TabPane();
...
tabBar.getTabs().addListener(this::tabsChanged);

I'm trying to listen to tab move events with the following code:

 private void tabsChanged(ListChangeListener.Change<? extends Tab> change) {
    while (change.next()) {
        if (change.wasPermutated()) {
            for (int i = change.getFrom(); i < change.getTo(); i++) {
                System.out.println(i + " -> " + change.getPermutation(i));
            }
        }
    }
}

As JavaFX documentation says:

In order to get the new position of an element, you must call: change.getPermutation(oldIndex). Returns: the new index of the same element.

But in my case change.getPermutation(i) always returns just i.

For example, I have 4 tabs. Their indexes are: 0, 1, 2, 3.

Then I move the 4th tab to the first position.

I expect the following output:

0 -> 1
1 -> 2
2 -> 3
3 -> 0

But I get:

0 -> 0
1 -> 1
2 -> 2
3 -> 3

How can I make it work as I need?

1 Answers

As already noted in the comments: the behavior you observe is a bug just reported as JDK-8278062 - the doc and your expectation based on the doc is correct, the notification (implemented in the internal class TabObservableList) is wrong.

Normally, if we want to find the newIndex, a listChangeListener would do something like:

for (int oldIndex = c.getFrom();  oldIndex < c.getTo(); ++oldIndex) {
   int newIndex = c.getPermutation(oldIndex);
   ...
}

To work around the issue, we could manually keep a copy of the tabs, lookup the tab at the old index and find its new index in the re-ordered tabs:

for (int oldIndex = c.getFrom();  oldIndex < c.getTo(); ++oldIndex) {
   Tab tab = copy.get(oldIndex);
   int newIndex = c.getList().indexOf(tab);
   ...
}
// update the copy

Or we could have some fun and implement a TransformationList around the original tabs that does the work for us :) It jumps in when it detects a permutation and fires the correct notification. Note that the only internal class used below is SourceChangeAdapter, we either need to relax encapsulation or c&p its content (it is doing nothing but pass on notifications on behalf of the wrapper)

public class TabObservableListWrapper extends TransformationList<Tab, Tab> {
    // copy of source used to build the correct permutation
    private ObservableList<Tab> copy = FXCollections.observableArrayList();

    public TabObservableListWrapper(ObservableList<Tab> source) {
        super(source);
        updateCopy();
    }

    @Override
    protected void sourceChanged(Change<? extends Tab> c) {
        // TBD: cope with a change that has
        // - a mixture of permutation and other subchanges
        // - multiple subchanges of type permutation
        boolean isPermutation = false;
        // check if the change is a permutation
        while (c.next()) {
            if (c.wasPermutated()) {
                isPermutation = true;
                break;
            }
        }
        c.reset();
        if (isPermutation) {
            beginChange();
            updatePermutation(c);
            endChange();
        } else {
            // assuming other change type notifications are correct, just delegate
            fireChange(new SourceAdapterChange<>(this, c));
        }
        // keep copy sync'ed to source
        updateCopy();
    }

    /**
     * Converts the incorrect permutation notification from source
     * into a correct one and let super fire the appropriate change.
     *
     * Note: this method must be called inside a begin/endChange block.
     * @param c a change with a single subChange of type wasPermutated
     */
    private void updatePermutation(Change<? extends Tab> c) {
        c.next();
        int from = c.getFrom();
        int to = c.getTo();
        int permSize = to - from;
        int[] perm = new int[permSize];
        // fill the perm
        for(int i = 0; i < permSize; i++) {
            int oldIndex = from + i;
            Tab tab = copy.get(oldIndex);
            perm[i] = c.getList().indexOf(tab);
        }
        nextPermutation(from, to, perm);
    }

    // keep copy sync'ed
    private void updateCopy() {
        copy.setAll(getSource());
    }

    // implement public methods by delegating 1:1 to source
    @Override
    public int getSourceIndex(int index) {
        return index;
    }

    @Override
    public int getViewIndex(int index) {
        return index;
    }

    @Override
    public Tab get(int index) {
        return getSource().get(index);
    }

    @Override
    public int size() {
        return getSource().size();
    }

}

To use, wrap it around a tabPane's tab list and listen to the wrapper instead of directly to original list, something like:

TabObservableListWrapper wrapper = new TabObservableListWrapper(tabPane.getTabs());
wrapper.addListener((ListChangeListener<Tab>)change -> {
    while (change.next()) {
        if (change.wasPermutated()) {
            System.out.println("from wrapper:");
            for (int oldIndex = change.getFrom(); oldIndex < change.getTo(); oldIndex++) {
                System.out.println(oldIndex + " -> " + change.getPermutation(oldIndex));
            }
        }
    }
});
Related