Search code examples
javafx-8treetableview

JavaFX TreeTable select children when parent is selected and remove selection from parent


I have a TreeTable which has maximum depth of 2, e.g.

  • fooType

    -foo

    -foo

If I select fooType I want the program to select automatically all child-items AND deselect the parent item. But when I try this, I always get an IndexOutOfBoundsException.

myTreeTable.getSelectionModel().selectedItemProperty().addListener((obs, ov, nv) -> {
  if (nv.getValue() instanceof fooType) {
    myTreeTable.getSelectionModel().clearSelection(myTreeTable.getSelectionModel().getSelectedIndex());
    if (!nv.isExpanded()) {
      nv.setExpanded(true);
    }
    ObservableList<TreeItem<IfooTreeItem>> children = nv.getChildren();
    for (TreeItem<IfooTreeItem> item : children) {
      annotationsTreeTable.getSelectionModel().select(item);
    }
  }
});

Multi selection mode is enabled.

Any help appreciated.


Solution

  • In JavaFX, you are not allowed to change an ObservableList while an existing change to that list is being processed. (Whether or not this is a sensible rule is open to debate, nevertheless, it is a rule.)

    The selection model keeps ObservableLists of both the selected indices and the selected items. Part of the processing of changes in those lists is to call listeners on the selected item and selected index. Consequently, you can't change the selection from a listener on the selection itself.

    The "proper" way to do this would be to provide your own implementation of the selection model. This is a bit of a pain, as there are a lot of methods to implement, and their use is not well documented. Here is an example, though this is intended as a starting point and is not intended to be production quality:

    import java.util.ArrayList;
    import java.util.Arrays;
    import java.util.List;
    import java.util.stream.Stream;
    
    import javafx.application.Application;
    import javafx.collections.ObservableList;
    import javafx.scene.Scene;
    import javafx.scene.control.MultipleSelectionModel;
    import javafx.scene.control.SelectionMode;
    import javafx.scene.control.TreeItem;
    import javafx.scene.control.TreeView;
    import javafx.scene.layout.BorderPane;
    import javafx.stage.Stage;
    
    public class TreeSelectionExample extends Application {
    
        @Override
        public void start(Stage primaryStage) {
            TreeView<String> tree = new TreeView<>();
            TreeItem<String> root = new TreeItem<>();
            tree.setRoot(root);
            tree.setShowRoot(false);
    
            root.getChildren().add(new TreeItem<>("Item 1"));
            root.getChildren().add(new TreeItem<>("Item 2"));
    
            root.getChildren().forEach(item -> 
                Stream.of("A", "B").map(s -> new TreeItem<String>(item.getValue()+s))
                .forEach(item.getChildren()::add));
    
            MultipleSelectionModel<TreeItem<String>> defaultSelectionModel = tree.getSelectionModel() ;
            defaultSelectionModel.setSelectionMode(SelectionMode.MULTIPLE);
    
            tree.setSelectionModel(new MultipleSelectionModel<TreeItem<String>>() {
    
                {
                    setSelectionMode(SelectionMode.MULTIPLE);
                }
    
                @Override
                public ObservableList<Integer> getSelectedIndices() {
                    return defaultSelectionModel.getSelectedIndices();
                }
    
                @Override
                public ObservableList<TreeItem<String>> getSelectedItems() {
                    return defaultSelectionModel.getSelectedItems();
                }
    
                @Override
                public void selectRange(int start, int end) {
                    System.out.println("selectRange("+start+", "+end+")");
                    List<TreeItem<String>> items = new ArrayList<>();
                    for (int i = start; i < end; i++) {
                        items.add(tree.getTreeItem(i));
                    }
                    for (int i = start ; i > end; i--) {
                        items.add(tree.getTreeItem(i));
                    }
                    items.forEach(this::select);
                }
    
                @Override
                public void selectIndices(int index, int... indices) {
                    System.out.println("select("+index+", "+Arrays.toString(indices)+")");
                    TreeItem<String> item = tree.getTreeItem(index);
                    if (item.isLeaf()) {
                        defaultSelectionModel.select(item);;
                    } else {
                        List<TreeItem<String>> leaves = new ArrayList<>();
                        findLeavesAndExpand(item, leaves);
                        for (TreeItem<String> leaf : leaves) {
                            defaultSelectionModel.select(leaf);
                        }
                    }
                    for (int i : indices) {
                        item = tree.getTreeItem(i);
                        if (item.isLeaf()) {
                            defaultSelectionModel.select(item);;                        
                        } else {
                            List<TreeItem<String>> leaves = new ArrayList<>();
                            findLeavesAndExpand(item, leaves);
                            for (TreeItem<String> leaf : leaves) {
                                defaultSelectionModel.select(leaf);
                            }
                        }
                    }
                }
    
                @Override
                public void selectAll() {
                    System.out.println("selectAll()");
                    List<TreeItem<String>> leaves = new ArrayList<>();
                    findLeavesAndExpand(tree.getRoot(), leaves);
                    for (TreeItem<String> leaf : leaves) {
                        defaultSelectionModel.select(leaf);
                    }
                }
    
                @Override
                public void selectFirst() {
                    System.out.println("selectFirst()");
                    TreeItem<String> firstLeaf ;
                    for (firstLeaf = tree.getRoot(); ! firstLeaf.isLeaf(); firstLeaf = firstLeaf.getChildren().get(0)) ;
                    defaultSelectionModel.select(firstLeaf);
                }
    
                @Override
                public void selectLast() {
                    System.out.println("selectLast()");
                    TreeItem<String> lastLeaf ;
                    for (lastLeaf = tree.getRoot(); ! lastLeaf.isLeaf(); 
                            lastLeaf = lastLeaf.getChildren().get(lastLeaf.getChildren().size()-1)) ;
                    defaultSelectionModel.select(lastLeaf);
                }
    
                @Override
                public void clearAndSelect(int index) {
                    TreeItem<String> item = tree.getTreeItem(index);
                    defaultSelectionModel.clearSelection();
                    if (item.isLeaf()) {
                        defaultSelectionModel.select(item);
                    } else {
                        List<TreeItem<String>> leaves = new ArrayList<>();
                        findLeavesAndExpand(item, leaves);
                        for (TreeItem<String> leaf : leaves) {
                            defaultSelectionModel.select(leaf);
                        }
                    }
                }
    
                @Override
                public void select(int index) {
                    System.out.println("select("+index+")");
                    select(tree.getTreeItem(index));
                }
    
                @Override
                public void select(TreeItem<String> item) {
                    System.out.println("select("+item.getValue()+")");
                    if (item.isLeaf()) {
                        defaultSelectionModel.select(item);
                    } else {
                        List<TreeItem<String>> leaves = new ArrayList<>();
                        findLeavesAndExpand(item, leaves);
                        for (TreeItem<String> leaf : leaves) {
                            defaultSelectionModel.select(leaf);
                        }                    
                    }
                }
    
                @Override
                public void clearSelection(int index) {
                    defaultSelectionModel.clearSelection(index);
                }
    
                @Override
                public void clearSelection() {
                    defaultSelectionModel.clearSelection();
                }
    
                @Override
                public boolean isSelected(int index) {
                    return defaultSelectionModel.isSelected(index);
                }
    
                @Override
                public boolean isEmpty() {
                    return defaultSelectionModel.isEmpty();
                }
    
                @Override
                public void selectPrevious() {
                    // TODO Auto-generated method stub
                    // not sure on implementation needed here
                }
    
                @Override
                public void selectNext() {
                    System.out.println("selectNext()");
                    // TODO Auto-generated method stub
                    // not sure on implementation needed here
                }
    
                private void findLeavesAndExpand(TreeItem<String> node, List<TreeItem<String>> leaves) {
                    if (node.isLeaf()) {
                        leaves.add(node);
                    } else {
                        node.setExpanded(true);
                        for (TreeItem<String> child : node.getChildren()) {
                            findLeavesAndExpand(child, leaves);
                        }
                    }
                }
    
            });
    
            primaryStage.setScene(new Scene(new BorderPane(tree), 400, 400));
            primaryStage.show();
        }
    
        public static void main(String[] args) {
            launch(args);
        }
    }