mrPeaceExporter
mrPeaceExporter

Reputation: 31

JavaFX TreeTableView exception when sorting with multiple rows selected

I'm learning JavaFX right now and I can't seem to get one thing right. Basically what I'm trying to do is a TreeTableView with multiple selection, which works fine until I try to sort the list.

Here's the code (Example 15-1 TreeTableView with One Column from http://docs.oracle.com/javase/8/javafx/user-interface-tutorial/tree-table-view.htm#CJAEIFDC):

import javafx.application.Application;
import javafx.beans.property.ReadOnlyStringWrapper;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.control.TreeTableColumn;
import javafx.scene.control.TreeTableColumn.CellDataFeatures;
import javafx.scene.control.TreeItem;
import javafx.scene.control.TreeTableView;
import javafx.stage.Stage;

public class TreeTableViewSample extends Application {

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

    @Override
    public void start(Stage stage) {
        stage.setTitle("Tree Table View Samples");
        final Scene scene = new Scene(new Group(), 200, 400);
        Group sceneRoot = (Group)scene.getRoot();  

        //Creating tree items
        final TreeItem<String> childNode1 = new TreeItem<>("Child Node 1");
        final TreeItem<String> childNode2 = new TreeItem<>("Child Node 2");
        final TreeItem<String> childNode3 = new TreeItem<>("Child Node 3");

        //Creating the root element
        final TreeItem<String> root = new TreeItem<>("Root node");
        root.setExpanded(true);   

        //Adding tree items to the root
        root.getChildren().setAll(childNode1, childNode2, childNode3);        

        //Creating a column
        TreeTableColumn<String,String> column = new TreeTableColumn<>("Column");
        column.setPrefWidth(150);   

        //Defining cell content
        column.setCellValueFactory((CellDataFeatures<String, String> p) -> 
            new ReadOnlyStringWrapper(p.getValue().getValue()));  

        //Creating a tree table view
        final TreeTableView<String> treeTableView = new TreeTableView<>(root);
        TreeTableView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE); //Setting SelectionMode to MULTIPLE
        treeTableView.getColumns().add(column);
        treeTableView.setPrefWidth(152);
        treeTableView.setShowRoot(true);             
        sceneRoot.getChildren().add(treeTableView);
        stage.setScene(scene);
        stage.show();
    }     
}

I added this line:

TreeTableView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE); //Setting SelectionMode to MULTIPLE

Everything works fine but when I select multiple rows and try to sort the columns only the active row (last selected) remains selected.

The console gives me this output when sorting:

Exception in thread "JavaFX Application Thread" java.lang.IndexOutOfBoundsException
    at com.sun.javafx.scene.control.ReadOnlyUnbackedObservableList.subList(Unknown Source)
    at javafx.collections.ListChangeListener$Change.getAddedSubList(Unknown Source)
    at javafx.scene.control.TreeTableView$TreeTableViewArrayListSelectionModel.handleSelectedCellsListChangeEvent(Unknown Source)
    at javafx.scene.control.TreeTableView$TreeTableViewArrayListSelectionModel.access$2100(Unknown Source)
    at javafx.scene.control.TreeTableView.sort(Unknown Source)
    at javafx.scene.control.TreeTableView.doSort(Unknown Source)
    at javafx.scene.control.TreeTableView.lambda$new$115(Unknown Source)
    at javafx.scene.control.TreeTableView$$Lambda$99/1473718685.onChanged(Unknown Source)
    at com.sun.javafx.collections.ListListenerHelper$Generic.fireValueChangedEvent(Unknown Source)
    at com.sun.javafx.collections.ListListenerHelper.fireValueChangedEvent(Unknown Source)
    at javafx.collections.ObservableListBase.fireChange(Unknown Source)
    at javafx.collections.ListChangeBuilder.commit(Unknown Source)
    at javafx.collections.ListChangeBuilder.endChange(Unknown Source)
    at javafx.collections.ObservableListBase.endChange(Unknown Source)
    at javafx.collections.ModifiableObservableListBase.setAll(Unknown Source)
    at javafx.collections.ObservableListBase.setAll(Unknown Source)
    at com.sun.javafx.scene.control.skin.TableColumnHeader.sortColumn(Unknown Source)
    at com.sun.javafx.scene.control.skin.TableColumnHeader.lambda$static$55(Unknown Source)
    at com.sun.javafx.scene.control.skin.TableColumnHeader$$Lambda$152/863692449.handle(Unknown Source)
    at com.sun.javafx.event.CompositeEventHandler.dispatchBubblingEvent(Unknown Source)
    at com.sun.javafx.event.EventHandlerManager.dispatchBubblingEvent(Unknown Source)
    at com.sun.javafx.event.EventHandlerManager.dispatchBubblingEvent(Unknown Source)
    at com.sun.javafx.event.CompositeEventDispatcher.dispatchBubblingEvent(Unknown Source)
    at com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(Unknown Source)
    at com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(Unknown Source)
    at com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(Unknown Source)
    at com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(Unknown Source)
    at com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(Unknown Source)
    at com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(Unknown Source)
    at com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(Unknown Source)
    at com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(Unknown Source)
    at com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(Unknown Source)
    at com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(Unknown Source)
    at com.sun.javafx.event.EventUtil.fireEventImpl(Unknown Source)
    at com.sun.javafx.event.EventUtil.fireEvent(Unknown Source)
    at javafx.event.Event.fireEvent(Unknown Source)
    at javafx.scene.Scene$MouseHandler.process(Unknown Source)
    at javafx.scene.Scene$MouseHandler.access$1500(Unknown Source)
    at javafx.scene.Scene.impl_processMouseEvent(Unknown Source)
    at javafx.scene.Scene$ScenePeerListener.mouseEvent(Unknown Source)
    at com.sun.javafx.tk.quantum.GlassViewEventHandler$MouseEventNotification.run(Unknown Source)
    at com.sun.javafx.tk.quantum.GlassViewEventHandler$MouseEventNotification.run(Unknown Source)
    at java.security.AccessController.doPrivileged(Native Method)
    at com.sun.javafx.tk.quantum.GlassViewEventHandler.lambda$handleMouseEvent$350(Unknown Source)
    at com.sun.javafx.tk.quantum.GlassViewEventHandler$$Lambda$224/2145564822.get(Unknown Source)
    at com.sun.javafx.tk.quantum.QuantumToolkit.runWithoutRenderLock(Unknown Source)
    at com.sun.javafx.tk.quantum.GlassViewEventHandler.handleMouseEvent(Unknown Source)
    at com.sun.glass.ui.View.handleMouseEvent(Unknown Source)
    at com.sun.glass.ui.View.notifyMouse(Unknown Source)
    at com.sun.glass.ui.win.WinApplication._runLoop(Native Method)
    at com.sun.glass.ui.win.WinApplication.lambda$null$145(Unknown Source)
    at com.sun.glass.ui.win.WinApplication$$Lambda$36/2117255219.run(Unknown Source)
    at java.lang.Thread.run(Unknown Source)

Thanks in advance for any help.

Upvotes: 3

Views: 1567

Answers (1)

Denis
Denis

Reputation: 567

I've stumbled on your post when encountered the same issue yesterday. It's pity nobody replied to you, and I decided to investigate myself. It's still quite unbelievable that a bug in such a common functionality could go unnoticed and unfixed for years...

Because it looks to me like a bug in JavaFX, in the TreeTableView.TreeTableViewArrayListSelectionModel. There's a handler that updates the selection in response to changes in the data model:

        private EventHandler<TreeItem.TreeModificationEvent<S>> treeItemListener = new EventHandler<TreeItem.TreeModificationEvent<S>>() {
        @Override public void handle(TreeItem.TreeModificationEvent<S> e) {

            if (getSelectedIndex() == -1 && getSelectedItem() == null) return;
            <...>

At some point (line 2421) it handles the sorting case (i.e., permutation):

               } else if (e.wasPermutated()) {
                // This handles the sorting case where nothing was added or
                // removed, but the location of the selected index / item
                // has likely changed. This was added to fix RT-30156 and
                // unit tests exist to prevent it from regressing.
                quietClearSelection();
                select(oldSelectedItem);
            } else if (e.wasAdded()) {

This code tries to re-select the correct item based on its contents (and not the line number that might have changed during sorting). But unfortunately it fails to consider the multiple selection case, leaving only one item selected after the sorting.

The crash (exception) happens because TreeTableView's sort() method seems to address the same issue (restore the correct selection once the sorting is done), by saving the the selection indices before and after the sorting, and issuing a permutation event:

    final List<TreeTablePosition<S,?>> prevState = new ArrayList<>(getSelectionModel().getSelectedCells());
    final int itemCount = prevState.size();

    <...>

            final TreeTableViewArrayListSelectionModel<S> sm = (TreeTableViewArrayListSelectionModel<S>) getSelectionModel();
            final ObservableList<TreeTablePosition<S, ?>> newState = sm.getSelectedCells();

            List<TreeTablePosition<S, ?>> removed = new ArrayList<>();
            for (int i = 0; i < itemCount; i++) {
                TreeTablePosition<S, ?> prevItem = prevState.get(i);
                if (!newState.contains(prevItem)) {
                    removed.add(prevItem);
                }
            }

            if (!removed.isEmpty()) {
                // the sort operation effectively permutates the selectedCells list,
                // but we cannot fire a permutation event as we are talking about
                // TreeTablePosition's changing (which may reside in the same list
                // position before and after the sort). Therefore, we need to fire
                // a single add/remove event to cover the added and removed positions.
                ListChangeListener.Change<TreeTablePosition<S, ?>> c = new NonIterableChange.GenericAddRemoveChange<>(0, itemCount, removed, newState);
                sm.handleSelectedCellsListChangeEvent(c);
            }

In the before-last line, onIterableChange.GenericAddRemoveChange object is created while assuming that newState list is having itemCount elements (while newState will always contain 1 element as explained above), and it crashes when trying to get itemCount elements from it.

Now, what can you do about it? To cleanly fix it, you'd need either to

  1. subclass the TreeTableView and override sort() method, or
  2. provide your own implementation of SelectionModel (probably based on the TreeTableViewArrayListSelectionModel implementation) with TreeTableView.SetSelectionModel()

Both methods are not simple, as the code heavily uses privately accessible members. With the first solution, you'll also face an issue with the FXMLLoader that only can create TreeTableView (and not your subclass MyTreeTableView), but you should be able to create the object manually.

I think I will stick to the following workaround, clearing the selection whenever the table gets sorted (doesn't happen a lot in my case):

 myTreeTable.setOnSort(event -> { 
     if(myTreeTable.getSelectionModel().getSelectedIndices().size() > 1) 
         myTreeTable.getSelectionModel().clearSelection(); 
 });

Hope this helps!

Upvotes: 10

Related Questions