TreeView - Certain TreeItems are not allowed to be

2020-02-15 05:37发布

问题:

I have created a Treeview (javafx), it looks like:

I want now, that only the "Tour"-TreeItems be selectable. But I don't know how.

I have tried it with a ChangeListener, but I can only with it refresh the content of a Tab (TabPane)...the refresh works fine...but the "Delivery"-TreeItems can be selected :(

code:

public void showTours(List<Tour> pTours) {

    treeViewPane.getSelectionModel().selectedItemProperty().addListener(treeItemChangeListener);

    TreeItem tTreeRoot = new TreeItem<>("Root", new ImageView(Icons.getIcon24("truck_blue.png")));
    tTreeRoot.setExpanded(true);
    treeViewPane.setRoot(tTreeRoot);

    for (Tour tTour : pTours) {

        TreeItem<Object> tTourItem = new TreeItem<>(tTour);
        tTreeRoot.getChildren().add(tTourItem);

        if (tTour.getDeliveries() != null) {
            for (Delivery tDelivery : tTour.getDeliveries()) {

                TreeItem<Object> tDeliveryItem = new TreeItem<>(tDelivery);
                tTourItem.getChildren().add(tDeliveryItem);
            }
        }
    }
}

private final ChangeListener<TreeItem> treeItemChangeListener = (observable, oldValue, newValue) -> {

    if (newValue != null && newValue.getValue() instanceof Tour){
        Tour selectedTour = (Tour) newValue.getValue();
        reloadTabContent(selectedTour);
    }
};

I hope you can help me. If you can show me example code, I will be really happy :)

Thank you

回答1:

Modifying the selection behavior in any controls in JavaFX seems to be a bit of a pain; but the "proper" way to do this is to define a custom selection model for the tree. The easiest way to do this is to wrap the default selection model, and delegate the method calls to it, vetoing selection if the selection index is for an item which shouldn't be selected.

It's a good idea to select something whenever possible when a select method is called, as otherwise keyboard navigation will break.

Here is an implementation:

import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
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 CustomTreeSelectionModelExample extends Application {

    @Override
    public void start(Stage primaryStage) {
        TreeItem<Object> root = new TreeItem<>("Root");
        for (int i = 1 ; i <= 5 ; i++) {
            TreeItem<Object> item = new TreeItem<>(new Tour("Tour "+i));
            for (int j = 1 ; j <= 5; j++) {
                Delivery delivery = new Delivery("Delivery "+j);
                item.getChildren().add(new TreeItem<>(delivery));
            }
            root.getChildren().add(item);
        }
        TreeView<Object> tree = new TreeView<>();
        tree.setSelectionModel(new TourSelectionModel(tree.getSelectionModel(), tree));
        tree.setRoot(root);

        primaryStage.setScene(new Scene(new BorderPane(tree), 400, 400));
        primaryStage.show();
    }

    public static class TourSelectionModel extends MultipleSelectionModel<TreeItem<Object>> {

        private final MultipleSelectionModel<TreeItem<Object>> selectionModel ;
        private final TreeView<Object> tree ;

        public TourSelectionModel(MultipleSelectionModel<TreeItem<Object>> selectionModel, TreeView<Object> tree) {
            this.selectionModel = selectionModel ;
            this.tree = tree ;
            selectionModeProperty().bindBidirectional(selectionModel.selectionModeProperty());
        }

        @Override
        public ObservableList<Integer> getSelectedIndices() {
            return selectionModel.getSelectedIndices() ;
        }

        @Override
        public ObservableList<TreeItem<Object>> getSelectedItems() {
            return selectionModel.getSelectedItems() ;
        }

        @Override
        public void selectIndices(int index, int... indices) {

            List<Integer> indicesToSelect = Stream.concat(Stream.of(index), IntStream.of(indices).boxed())
                    .filter(i -> tree.getTreeItem(i).getValue() instanceof Tour)
                    .collect(Collectors.toList());


            if (indicesToSelect.isEmpty()) {
                return ;
            }
            selectionModel.selectIndices(indicesToSelect.get(0), 
                    indicesToSelect.stream().skip(1).mapToInt(Integer::intValue).toArray());

        }

        @Override
        public void selectAll() {
            List<Integer> indicesToSelect = IntStream.range(0, tree.getExpandedItemCount())
                    .filter(i -> tree.getTreeItem(i).getValue() instanceof Tour)
                    .boxed()
                    .collect(Collectors.toList());
            if (indicesToSelect.isEmpty()) {
                return ;
            }
            selectionModel.selectIndices(0, 
                    indicesToSelect.stream().skip(1).mapToInt(Integer::intValue).toArray());
        }

        @Override
        public void selectFirst() {
            IntStream.range(0, tree.getExpandedItemCount())
                .filter(i -> tree.getTreeItem(i).getValue() instanceof Tour)
                .findFirst()
                .ifPresent(selectionModel::select);
        }

        @Override
        public void selectLast() {
            IntStream.iterate(tree.getExpandedItemCount() - 1, i -> i - 1)
                .limit(tree.getExpandedItemCount())
                .filter(i -> tree.getTreeItem(i).getValue() instanceof Tour)
                .findFirst()
                .ifPresent(selectionModel::select);
        }

        @Override
        public void clearAndSelect(int index) {
            int toSelect = index ;
            int direction = selectionModel.getSelectedIndex() < index ? 1 : -1 ;
            while (toSelect >= 0 && toSelect < tree.getExpandedItemCount() && ! (tree.getTreeItem(toSelect).getValue() instanceof Tour)) {
                toSelect = toSelect + direction  ;
            }
            if (toSelect >= 0 && toSelect < tree.getExpandedItemCount()) {
                selectionModel.clearAndSelect(toSelect);
            }
        }

        @Override
        public void select(int index) {
            int toSelect = index ;
            int direction = selectionModel.getSelectedIndex() < index ? 1 : -1 ;
            while (toSelect >= 0 && toSelect < tree.getExpandedItemCount() && ! (tree.getTreeItem(toSelect).getValue() instanceof Tour)) {
                toSelect = toSelect + direction  ;
            }
            if (toSelect >= 0 && toSelect < tree.getExpandedItemCount()) {
                selectionModel.select(toSelect);
            }
        }

        @Override
        public void select(TreeItem<Object> obj) {
            if (obj.getValue() instanceof Tour) {
                selectionModel.select(obj);
            }
        }

        @Override
        public void clearSelection(int index) {
            selectionModel.clearSelection(index);
        }

        @Override
        public void clearSelection() {
            selectionModel.clearSelection();
        }

        @Override
        public boolean isSelected(int index) {
            return selectionModel.isSelected(index);
        }

        @Override
        public boolean isEmpty() {
            return selectionModel.isEmpty();
        }

        @Override
        public void selectPrevious() {
            int current = selectionModel.getSelectedIndex() ;
            if (current > 0) {
                IntStream.iterate(current - 1, i -> i - 1).limit(current)
                    .filter(i -> tree.getTreeItem(i).getValue() instanceof Tour)
                    .findFirst()
                    .ifPresent(selectionModel::select);
            }
        }

        @Override
        public void selectNext() {
            int current = selectionModel.getSelectedIndex() ;
            if (current < tree.getExpandedItemCount() - 1) {
                IntStream.range(current + 1, tree.getExpandedItemCount())
                    .filter(i -> tree.getTreeItem(i).getValue() instanceof Tour)
                    .findFirst()
                    .ifPresent(selectionModel::select);
            }
        }

    }

    public static class Tour {

        private final String name ;

        public Tour(String name) {
            this.name = name ;
        }

        public String getName() {
            return name ;
        }

        @Override
        public String toString() {
            return getName();
        }

    }

    public static class Delivery {
        private final String name;

        public Delivery(String name) {
            this.name = name;
        }

        public String getName() {
            return name;
        }

        @Override
        public String toString() {
            return getName();
        }
    }

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


回答2:

I modified the selection-model that James_D posted by making it a bit more generic so that you can specify a custom filter. The implementation is:

public class FilteredTreeViewSelectionModel<S> extends MultipleSelectionModel<TreeItem<S>> {

    private final TreeView<S> treeView;
    private final MultipleSelectionModel<TreeItem<S>> selectionModel;
    private final TreeItemSelectionFilter<S> filter;

    public FilteredTreeViewSelectionModel(
            TreeView<S> treeView,
            MultipleSelectionModel<TreeItem<S>> selectionModel, 
            TreeItemSelectionFilter<S> filter) {
        this.treeView = treeView;
        this.selectionModel = selectionModel;
        this.filter = filter;
        selectionModeProperty().bindBidirectional(selectionModel.selectionModeProperty());
    }

    @Override
    public ObservableList<Integer> getSelectedIndices() {
        return this.selectionModel.getSelectedIndices();
    }

    @Override
    public ObservableList<TreeItem<S>> getSelectedItems() {
        return this.selectionModel.getSelectedItems();
    }

    private int getRowCount() {
        return this.treeView.getExpandedItemCount();
    }

    @Override
    public boolean isSelected(int index) {
        return this.selectionModel.isSelected(index);
    }

    @Override
    public boolean isEmpty() {
        return this.selectionModel.isEmpty();
    }

    @Override
    public void select(int index) {
        // If the row is -1, we need to clear the selection.
        if (index == -1) {
            this.selectionModel.clearSelection();
        } else if (index >= 0 && index < getRowCount()) {
            // If the tree-item at the specified row-index is selectable, we
            // forward select call to the internal selection-model.
            TreeItem<S> treeItem = this.treeView.getTreeItem(index);
            if (this.filter.isSelectable(treeItem)) {
                this.selectionModel.select(index);
            }
        }
    }

    @Override
    public void select(TreeItem<S> treeItem) {
        if (treeItem == null) {
            // If the provided tree-item is null, and we are in single-selection
            // mode we need to clear the selection.
            if (getSelectionMode() == SelectionMode.SINGLE) {
                this.selectionModel.clearSelection();
            }
            // Else, we just forward to the internal selection-model so that
            // the selected-index can be set to -1, and the selected-item
            // can be set to null.
            else {
                this.selectionModel.select(null);
            }
        } else if (this.filter.isSelectable(treeItem)) {
            this.selectionModel.select(treeItem);
        }
    }

    @Override
    public void selectIndices(int index, int... indices) {
        // If we have no trailing rows, we forward to normal row-selection.
        if (indices == null || indices.length == 0) {
            select(index);
            return;
        }

        // Filter indices so that we only end up with those indices whose
        // corresponding tree-items are selectable.
        int[] filteredIndices = IntStream.concat(IntStream.of(index), Arrays.stream(indices)).filter(indexToCheck -> {
            TreeItem<S> treeItem = treeView.getTreeItem(indexToCheck);
            return (treeItem != null) && filter.isSelectable(treeItem);
        }).toArray();

        // If we have indices left, we proceed to forward to internal selection-model.
        if (filteredIndices.length > 0) {
            int newIndex = filteredIndices[0];
            int[] newIndices = Arrays.copyOfRange(filteredIndices, 1, filteredIndices.length);
            this.selectionModel.selectIndices(newIndex, newIndices);
        }
    }

    @Override
    public void clearAndSelect(int index) {
        // If the index is out-of-bounds we just clear and return.
        if (index < 0 || index >= getRowCount()) {
            clearSelection();
            return;
        }

        // Get tree-item at index.
        TreeItem<S> treeItem = this.treeView.getTreeItem(index);

        // If the tree-item at the specified row-index is selectable, we forward
        // clear-and-select call to the internal selection-model.
        if (this.filter.isSelectable(treeItem)) {
            this.selectionModel.clearAndSelect(index);
        }
        // Else, we just do a normal clear-selection call.
        else {
            this.selectionModel.clearSelection();
        }
    }

    @Override
    public void selectAll() {
        int rowCount = getRowCount();

        // If we are in single-selection mode, we exit prematurely as
        // we cannot select all rows.
        if (getSelectionMode() == SelectionMode.SINGLE) {
            return;
        }

        // If we only have a single index to select, we forward to the
        // single-index select-method.
        if (rowCount == 1) {
            select(0);
        }
        // Else, if we have more than one index available, we construct an array
        // of all the indices and forward to the selectIndices-method.
        else if (rowCount > 1) {
            int index = 0;
            int[] indices = IntStream.range(1, rowCount).toArray();
            selectIndices(index, indices);
        }
    }

    @Override
    public void clearSelection(int index) {
        this.selectionModel.clearSelection(index);
    }

    @Override
    public void clearSelection() {
        this.selectionModel.clearSelection();
    }

    @Override
    public void selectFirst() {
        Optional<TreeItem<S>> firstItem = IntStream.range(0, getRowCount()).
            mapToObj(this.treeView::getTreeItem).
            filter(this.filter::isSelectable).
            findFirst();
        firstItem.ifPresent(this.selectionModel::select);
    }

    @Override
    public void selectLast() {
        int rowCount = getRowCount();
        Optional<TreeItem<S>> lastItem = IntStream.iterate(rowCount - 1, i -> i - 1).
            limit(rowCount).
            mapToObj(this.treeView::getTreeItem).
            filter(this.filter::isSelectable).
            findFirst();
        lastItem.ifPresent(this.selectionModel::select);
    }

    private int getFocusedIndex() {
        FocusModel<TreeItem<S>> focusModel = this.treeView.getFocusModel();
        return (focusModel == null) ? -1 : focusModel.getFocusedIndex();
    }

    @Override
    public void selectPrevious() {
        int focusIndex = getFocusedIndex();
        // If we have nothing selected, wrap around to the last index.
        int startIndex = (focusIndex == -1) ? getRowCount() : focusIndex;
        if (startIndex > 0) {
            Optional<TreeItem<S>> previousItem = IntStream.iterate(startIndex - 1, i -> i - 1).
                limit(startIndex).
                mapToObj(this.treeView::getTreeItem).
                filter(this.filter::isSelectable).
                findFirst();
            previousItem.ifPresent(this.selectionModel::select);
        }
    }

    @Override
    public void selectNext() {
        // If we have nothing selected, starting at -1 will work out correctly
        // because we'll search from 0 onwards.
        int startIndex = getFocusedIndex();
        if (startIndex < getRowCount() - 1) {
            Optional<TreeItem<S>> nextItem = IntStream.range(startIndex + 1, getRowCount()).
                mapToObj(this.treeView::getTreeItem).
                filter(this.filter::isSelectable).
                findFirst();
            nextItem.ifPresent(this.selectionModel::select);
        }
    }
}

I changed the selectIndex(int) method as this method should just forward the index-based selection to its internal selection-model if the filter permits. I disagree with the while loop logic as you explicitly pass the index to be selected to this method in the hopes that it can select it. The expected behaviour should be that it should ignore the select if the filter doesn't allow it. I also fleshed out the method by adding a catch for the index == -1 case as we need to clear selection when this happens.

The select(TreeItem) method was also changed quite a bit by checking for a null parameter and handling this separately so that if we are in single-selection mode we need to clear the selection, otherwise we call select(null) so that the internal selection-model handles it correctly. If we do have a tree-item we just check against filter and pass through to the internal selection-model.

The selectIndices(int, int[]) method is also different in that it should handle the case where the indices-array could be null or of length 0. If this is the case the select(index) method should be called.

I implemented the clearAndSelect(int) method a bit differently compared to the other approach. I do the boundary checks at the beginning to see if we need to call clearSelection() immediately. Else, I check if the TreeItem at the index is selectable via the filter. If it is we forward to the internal selection-model, else we just clear. I also disagree with the while-loop approach here that was done in the other implementation.

There is actually a bug with the selectPrevious() and selectNext() methods of James_D's implementation. If nothing is selected you need to snap to the last index when calling selectPrevious(). The opposite is true for selectFirst() where you need to snap to the first index if nothing is selected. You then work from these new indices to find the first item that is permitted by the filter. You also need to work with the focus-index and not the selected-index. You can see this behaviour if you look at the MultipleSelectionModelBase class for reference.

The TreeItemSelectionFilter is specified as:

public interface TreeItemSelectionFilter<S> {

    public boolean isSelectable(TreeItem<S> treeItem);
}

For your particular case you can then wire it all together as:

....
MultipleSelectionModel<TreeItem<Object>> selectionModel = tree.getSelectionModel();
TreeItemSelectionFilter<Object> filter = treeItem -> treeItem.getValue() instanceof Tour;
FilteredTreeViewSelectionModel<Object> filteredSelectionModel = new FilteredTreeViewSelectionModel<>(tree, selectionModel, filter);
tree.setSelectionModel(filteredSelectionModel);
....

I've uploaded the source-code of an example application here so that you can easily test the behavior of the FilteredTreeViewSelectionModel for yourself. Compare it with the default selection-model and see if you are satisfied with the behavior.