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.