/*
 * Copyright (c) 2010, 2018, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */

package javafx.scene.control;

import com.sun.javafx.collections.NonIterableChange;
import static javafx.scene.control.SelectionMode.SINGLE;

import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

import com.sun.javafx.scene.control.MultipleAdditionAndRemovedChange;
import com.sun.javafx.scene.control.ReadOnlyUnbackedObservableList;
import com.sun.javafx.scene.control.SelectedItemsReadOnlyObservableList;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.collections.ObservableListBase;
import javafx.util.Callback;

import javafx.util.Pair;


An abstract class that implements more of the abstract MultipleSelectionModel abstract class. However, this class is package-protected and not intended for public use.
Type parameters:
  • <T> – The type of the underlying data model for the UI control.
/** * An abstract class that implements more of the abstract MultipleSelectionModel * abstract class. However, this class is package-protected and not intended * for public use. * * @param <T> The type of the underlying data model for the UI control. */
abstract class MultipleSelectionModelBase<T> extends MultipleSelectionModel<T> {
* Constructors * *
/*********************************************************************** * * * Constructors * * * **********************************************************************/
public MultipleSelectionModelBase() { selectedIndexProperty().addListener(valueModel -> { // we used to lazily retrieve the selected item, but now we just // do it when the selection changes. This is hardly likely to be // expensive, and we still lazily handle the multiple selection // cases over in MultipleSelectionModel. setSelectedItem(getModelItem(getSelectedIndex())); }); selectedIndices = new SelectedIndicesList(); selectedItems = new SelectedItemsReadOnlyObservableList<T>(selectedIndices, () -> getItemCount()) { @Override protected T getModelItem(int index) { return MultipleSelectionModelBase.this.getModelItem(index); } }; }
* Observable properties * *
/*********************************************************************** * * * Observable properties * * * **********************************************************************/
/* * We only maintain the values of the selectedIndex and selectedIndices * properties. The value of the selectedItem and selectedItems properties * is determined on-demand. We fire the SELECTED_ITEM and SELECTED_ITEMS * property change events whenever the related SELECTED_INDEX or * SELECTED_INDICES properties change. * * This means that the cost of the ListViewSelectionModel is cheap in most * cases, assuming that the end-consumer isn't calling getSelectedItems * too aggressively. Of course, this is only an issue when the ListViewModel * is being populated by some remote, expensive to query data source. * * In addition, we do not provide ObservableLists for the selected indices or the * selected items properties, as this would allow the API consumer to add * observers to these ObservableLists. This would make life tougher as we would * then be forced to keep these ObservableLists in-sync at all times, which for * the selectedItems ObservableList, would require potentially a lot of work and * memory. Instead, we return a List, and allow for changes to these Lists * to be observed through the SELECTED_INDICES and SELECTED_ITEMS * properties. */ final SelectedIndicesList selectedIndices; @Override public ObservableList<Integer> getSelectedIndices() { return selectedIndices; } private final ObservableListBase<T> selectedItems; @Override public ObservableList<T> getSelectedItems() { return selectedItems; }
* Internal field * *
/*********************************************************************** * * * Internal field * * * **********************************************************************/
ListChangeListener.Change selectedItemChange; /*********************************************************************** * * * Public selection API * * * **********************************************************************/
Returns the number of items in the data model that underpins the control. An example would be that a ListView selection model would likely return listView.getItems().size(). The valid range of selectable indices is between 0 and whatever is returned by this method.
Returns:the number of items in the data model that underpins the control
/** * Returns the number of items in the data model that underpins the control. * An example would be that a ListView selection model would likely return * <code>listView.getItems().size()</code>. The valid range of selectable * indices is between 0 and whatever is returned by this method. * @return the number of items in the data model that underpins the control */
protected abstract int getItemCount();
Returns the item at the given index. An example using ListView would be listView.getItems().get(index).
Params:
  • index – The index of the item that is requested from the underlying data model.
Returns:Returns null if the index is out of bounds, or an element of type T that is related to the given index.
/** * Returns the item at the given index. An example using ListView would be * <code>listView.getItems().get(index)</code>. * * @param index The index of the item that is requested from the underlying * data model. * @return Returns null if the index is out of bounds, or an element of type * T that is related to the given index. */
protected abstract T getModelItem(int index); protected abstract void focus(int index); protected abstract int getFocusedIndex(); static class ShiftParams { private final int clearIndex; private final int setIndex; private final boolean selected; ShiftParams(int clearIndex, int setIndex, boolean selected) { this.clearIndex = clearIndex; this.setIndex = setIndex; this.selected = selected; } public final int getClearIndex() { return clearIndex; } public final int getSetIndex() { return setIndex; } public final boolean isSelected() { return selected; } } // package only void shiftSelection(int position, int shift, final Callback<ShiftParams, Void> callback) { shiftSelection(Arrays.asList(new Pair<>(position, shift)), callback); } void shiftSelection(List<Pair<Integer, Integer>> shifts, final Callback<ShiftParams, Void> callback) { int selectedIndicesCardinality = selectedIndices.size(); // number of true bits if (selectedIndicesCardinality == 0) return; int selectedIndicesSize = selectedIndices.bitsetSize(); // number of bits reserved int[] perm = new int[selectedIndicesSize]; Arrays.fill(perm, -1); // sort the list so that we iterate from highest position to lowest position Collections.sort(shifts, (s1, s2) -> Integer.compare(s2.getKey(), s1.getKey())); final int lowestShiftPosition = shifts.get(shifts.size() - 1).getKey(); // make a copy of the selectedIndices before so we can compare to it afterwards BitSet selectedIndicesCopy = (BitSet) selectedIndices.bitset.clone(); startAtomic(); for (Pair<Integer, Integer> shift : shifts) { doShift(shift, callback, perm); } stopAtomic(); // strip out all useless -1 default values from the perm array final int[] prunedPerm = Arrays.stream(perm).filter(value -> value > -1).toArray(); final boolean hasSelectionChanged = prunedPerm.length > 0; // This ensure that the selection remains accurate when a shift occurs. final int selectedIndex = getSelectedIndex(); if (selectedIndex >= lowestShiftPosition && selectedIndex > -1) { // sum up the total shift, where the position is less than or equal // to the previously selected index int totalShift = shifts.stream() .filter(shift -> shift.getKey() <= selectedIndex) .mapToInt(shift -> shift.getValue()) .sum(); // Fix for RT-38787: we used to not enter this block if // selectedIndex + shift resulted in a value less than zero, whereas // now we just set the newSelectionLead to zero in that instance. // There exists unit tests that cover this. final int newSelectionLead = Math.max(0, selectedIndex + totalShift); setSelectedIndex(newSelectionLead); // added the selectedIndices call for RT-30356. // changed to check if hasPermutated, and to call select(..) for RT-40010. // This forces the selection event to go through the system and fire // the necessary events. if (hasSelectionChanged) { selectedIndices.set(newSelectionLead, true); } else { select(newSelectionLead); } // removed due to RT-27185 // focus(newSelectionLead); } if (hasSelectionChanged) { // work out what indices were removed and added BitSet removed = (BitSet) selectedIndicesCopy.clone(); removed.andNot(selectedIndices.bitset); BitSet added = (BitSet) selectedIndices.bitset.clone(); added.andNot(selectedIndicesCopy); selectedIndices.reset(); selectedIndices.callObservers(new MultipleAdditionAndRemovedChange<>( added.stream().boxed().collect(Collectors.toList()), removed.stream().boxed().collect(Collectors.toList()), selectedIndices )); } } private void doShift(Pair<Integer, Integer> shiftPair, final Callback<ShiftParams, Void> callback, int[] perm) { final int position = shiftPair.getKey(); final int shift = shiftPair.getValue(); // with no check here, we get RT-15024 if (position < 0) return; if (shift == 0) return; int idx = (int) Arrays.stream(perm).filter(value -> value > -1).count(); int selectedIndicesSize = selectedIndices.bitsetSize() - idx; // number of bits reserved if (shift > 0) { for (int i = selectedIndicesSize - 1; i >= position && i >= 0; i--) { boolean selected = selectedIndices.isSelected(i); if (callback == null) { selectedIndices.clear(i); selectedIndices.set(i + shift, selected); } else { callback.call(new ShiftParams(i, i + shift, selected)); } if (selected) { perm[idx++] = i + 1; } } selectedIndices.clear(position); } else if (shift < 0) { for (int i = position; i < selectedIndicesSize; i++) { if ((i + shift) < 0) continue; if ((i + 1 + shift) < position) continue; boolean selected = selectedIndices.isSelected(i + 1); if (callback == null) { selectedIndices.clear(i + 1); selectedIndices.set(i + 1 + shift, selected); } else { callback.call(new ShiftParams(i + 1, i + 1 + shift, selected)); } if (selected) { perm[idx++] = i; } } } } void startAtomic() { selectedIndices.startAtomic(); } void stopAtomic() { selectedIndices.stopAtomic(); } boolean isAtomic() { return selectedIndices.isAtomic(); } @Override public void clearAndSelect(int row) { if (row < 0 || row >= getItemCount()) { clearSelection(); return; } final boolean wasSelected = isSelected(row); // RT-33558 if this method has been called with a given row, and that // row is the only selected row currently, then this method becomes a no-op. if (wasSelected && getSelectedIndices().size() == 1) { // before we return, we double-check that the selected item // is equal to the item in the given index if (getSelectedItem() == getModelItem(row)) { return; } } // firstly we make a copy of the selection, so that we can send out // the correct details in the selection change event. // We remove the new selection from the list seeing as it is not removed. BitSet selectedIndicesCopy = new BitSet(); selectedIndicesCopy.or(selectedIndices.bitset); selectedIndicesCopy.clear(row); List<Integer> previousSelectedIndices = new SelectedIndicesList(selectedIndicesCopy); // RT-32411 We used to call quietClearSelection() here, but this // resulted in the selectedItems and selectedIndices lists never // reporting that they were empty. // makeAtomic toggle added to resolve RT-32618 startAtomic(); // then clear the current selection clearSelection(); // and select the new row select(row); stopAtomic(); // fire off a single add/remove/replace notification (rather than // individual remove and add notifications) - see RT-33324 ListChangeListener.Change<Integer> change; /* * getFrom() documentation: * If wasAdded is true, the interval contains all the values that were added. * If wasPermutated is true, the interval marks the values that were permutated. * If wasRemoved is true and wasAdded is false, getFrom() and getTo() should * return the same number - the place where the removed elements were positioned in the list. */ if (wasSelected) { change = ControlUtils.buildClearAndSelectChange(selectedIndices, previousSelectedIndices, row); } else { int changeIndex = Math.max(0, selectedIndices.indexOf(row)); change = new NonIterableChange.GenericAddRemoveChange<>( changeIndex, changeIndex+1, previousSelectedIndices, selectedIndices); } selectedIndices.callObservers(change); } @Override public void select(int row) { if (row == -1) { clearSelection(); return; } if (row < 0 || row >= getItemCount()) { return; } boolean isSameRow = row == getSelectedIndex(); T currentItem = getSelectedItem(); T newItem = getModelItem(row); boolean isSameItem = newItem != null && newItem.equals(currentItem); boolean fireUpdatedItemEvent = isSameRow && ! isSameItem; // focus must come first so that we have the anchors set appropriately focus(row); if (! selectedIndices.isSelected(row)) { if (getSelectionMode() == SINGLE) { startAtomic(); quietClearSelection(); stopAtomic(); } selectedIndices.set(row); } setSelectedIndex(row); if (fireUpdatedItemEvent) { setSelectedItem(newItem); } } @Override public void select(T obj) { // if (getItemCount() <= 0) return; if (obj == null && getSelectionMode() == SelectionMode.SINGLE) { clearSelection(); return; } // We have no option but to iterate through the model and select the // first occurrence of the given object. Once we find the first one, we // don't proceed to select any others. Object rowObj = null; for (int i = 0, max = getItemCount(); i < max; i++) { rowObj = getModelItem(i); if (rowObj == null) continue; if (rowObj.equals(obj)) { if (isSelected(i)) { return; } if (getSelectionMode() == SINGLE) { quietClearSelection(); } select(i); return; } } // if we are here, we did not find the item in the entire data model. // Even still, we allow for this item to be set to the give object. // We expect that in concrete subclasses of this class we observe the // data model such that we check to see if the given item exists in it, // whilst SelectedIndex == -1 && SelectedItem != null. setSelectedIndex(-1); setSelectedItem(obj); } @Override public void selectIndices(int row, int... rows) { if (rows == null || rows.length == 0) { select(row); return; } /* * Performance optimisation - if multiple selection is disabled, only * process the end-most row index. */ int rowCount = getItemCount(); if (getSelectionMode() == SINGLE) { quietClearSelection(); for (int i = rows.length - 1; i >= 0; i--) { int index = rows[i]; if (index >= 0 && index < rowCount) { selectedIndices.set(index); select(index); break; } } if (selectedIndices.isEmpty()) { if (row > 0 && row < rowCount) { selectedIndices.set(row); select(row); } } } else { selectedIndices.set(row, rows); IntStream.concat(IntStream.of(row), IntStream.of(rows)) .filter(index -> index >= 0 && index < rowCount) .reduce((first, second) -> second) .ifPresent(lastIndex -> { setSelectedIndex(lastIndex); focus(lastIndex); setSelectedItem(getModelItem(lastIndex)); }); } } @Override public void selectAll() { if (getSelectionMode() == SINGLE) return; if (getItemCount() <= 0) return; final int rowCount = getItemCount(); final int focusedIndex = getFocusedIndex(); // set all selected indices to true clearSelection(); selectedIndices.set(0, rowCount, true); if (focusedIndex == -1) { setSelectedIndex(rowCount - 1); focus(rowCount - 1); } else { setSelectedIndex(focusedIndex); focus(focusedIndex); } } @Override public void selectFirst() { if (getSelectionMode() == SINGLE) { quietClearSelection(); } if (getItemCount() > 0) { select(0); } } @Override public void selectLast() { if (getSelectionMode() == SINGLE) { quietClearSelection(); } int numItems = getItemCount(); if (numItems > 0 && getSelectedIndex() < numItems - 1) { select(numItems - 1); } } @Override public void clearSelection(int index) { if (index < 0) return; // TODO shouldn't directly access like this // TODO might need to update focus and / or selected index/item boolean wasEmpty = selectedIndices.isEmpty(); selectedIndices.clear(index); if (! wasEmpty && selectedIndices.isEmpty()) { clearSelection(); } } @Override public void clearSelection() { quietClearSelection(); if (! isAtomic()) { setSelectedIndex(-1); focus(-1); } } private void quietClearSelection() { selectedIndices.clear(); } @Override public boolean isSelected(int index) { // Note the change in semantics here - we used to check to ensure that // the index is less than the item count, but now simply ensure that // it is less than the length of the selectedIndices bitset. This helps // to resolve issues such as RT-26721, where isSelected(int) was being // called for indices that exceeded the item count, as a TreeItem (e.g. // the root) was being collapsed. // if (index >= 0 && index < getItemCount()) { if (index >= 0 && index < selectedIndices.bitsetSize()) { return selectedIndices.isSelected(index); } return false; } @Override public boolean isEmpty() { return selectedIndices.isEmpty(); } @Override public void selectPrevious() { int focusIndex = getFocusedIndex(); if (getSelectionMode() == SINGLE) { quietClearSelection(); } if (focusIndex == -1) { select(getItemCount() - 1); } else if (focusIndex > 0) { select(focusIndex - 1); } } @Override public void selectNext() { int focusIndex = getFocusedIndex(); if (getSelectionMode() == SINGLE) { quietClearSelection(); } if (focusIndex == -1) { select(0); } else if (focusIndex != getItemCount() -1) { select(focusIndex + 1); } }
* Private implementation * *
/*********************************************************************** * * * Private implementation * * * **********************************************************************/
class SelectedIndicesList extends ReadOnlyUnbackedObservableList<Integer> { private final BitSet bitset; private int lastGetIndex = -1; private int lastGetValue = -1; // Fix for RT-20945 (and numerous other issues!) private int atomicityCount = 0; // @Override // public void callObservers(Change<Integer> c) { // throw new RuntimeException("callObservers unavailable"); // } public SelectedIndicesList() { this(new BitSet()); } public SelectedIndicesList(BitSet bitset) { this.bitset = bitset; } boolean isAtomic() { return atomicityCount > 0; } void startAtomic() { atomicityCount++; } void stopAtomic() { atomicityCount = Math.max(0, atomicityCount - 1); } // Returns the selected index at the given index. // e.g. if our selectedIndices are [1,3,5], then an index of 2 will return 5 here. @Override public Integer get(int index) { final int itemCount = size(); if (index < 0 || index >= itemCount) { throw new IndexOutOfBoundsException(index + " >= " + itemCount); } if (index == (lastGetIndex + 1) && lastGetValue < itemCount) { // we're iterating forward in order, short circuit for // performance reasons (RT-39776) lastGetIndex++; lastGetValue = bitset.nextSetBit(lastGetValue + 1); return lastGetValue; } else if (index == (lastGetIndex - 1) && lastGetValue > 0) { // we're iterating backward in order, short circuit for // performance reasons (RT-39776) lastGetIndex--; lastGetValue = bitset.previousSetBit(lastGetValue - 1); return lastGetValue; } else { for (lastGetIndex = 0, lastGetValue = bitset.nextSetBit(0); lastGetValue >= 0 || lastGetIndex == index; lastGetIndex++, lastGetValue = bitset.nextSetBit(lastGetValue + 1)) { if (lastGetIndex == index) { return lastGetValue; } } } return -1; } public void set(int index) { if (!isValidIndex(index) || isSelected(index)) { return; } _beginChange(); bitset.set(index); int indicesIndex = indexOf(index); _nextAdd(indicesIndex, indicesIndex + 1); _endChange(); } private boolean isValidIndex(int index) { return index >= 0 && index < getItemCount(); } public void set(int index, boolean isSet) { if (isSet) { set(index); } else { clear(index); } } public void set(int index, int end, boolean isSet) { _beginChange(); if (isSet) { bitset.set(index, end, isSet); int indicesIndex = indexOf(index); int span = end - index; _nextAdd(indicesIndex, indicesIndex + span); } else { // TODO handle remove bitset.set(index, end, isSet); } _endChange(); } public void set(int index, int... indices) { if (indices == null || indices.length == 0) { set(index); } else { // we reduce down to the minimal number of changes possible // by finding all contiguous indices, of all indices that are // not already selected, and which are in the valid range startAtomic(); List<Integer> sortedNewIndices = IntStream.concat(IntStream.of(index), IntStream.of(indices)) .distinct() .filter(this::isValidIndex) .filter(this::isNotSelected) .sorted() .boxed() .peek(this::set) // we also set here, but it's atomic! .collect(Collectors.toList()); stopAtomic(); final int size = sortedNewIndices.size(); if (size == 0) { // no-op } else if (size == 1) { _beginChange(); int _index = sortedNewIndices.get(0); int indicesIndex = indexOf(_index); _nextAdd(indicesIndex, indicesIndex + 1); _endChange(); } else { _beginChange(); int pos = 0; int start = 0; int end = 0; // starting from pos, we keep going until the value is // not the next value int startValue = sortedNewIndices.get(pos++); start = indexOf(startValue); end = start + 1; int endValue = startValue; while (pos < size) { int previousEndValue = endValue; endValue = sortedNewIndices.get(pos++); ++end; if (previousEndValue != (endValue - 1)) { _nextAdd(start, end); start = end; continue; } // special case for when we get to the point where the loop is about to end // and we have uncommitted changes to fire. if (pos == size) { _nextAdd(start, start + pos); } } _endChange(); } } } public void clear() { _beginChange(); List<Integer> removed = bitset.stream().boxed().collect(Collectors.toList()); bitset.clear(); _nextRemove(0, removed); _endChange(); } public void clear(int index) { if (!bitset.get(index)) return; _beginChange(); bitset.clear(index); _nextRemove(index, index); _endChange(); } // public void clearAndSelect(int index) { // if (index < 0 || index >= getItemCount()) { // clearSelection(); // return; // } // // final boolean wasSelected = isSelected(index); // // // RT-33558 if this method has been called with a given row, and that // // row is the only selected row currently, then this method becomes a no-op. // if (wasSelected && getSelectedIndices().size() == 1) { // // before we return, we double-check that the selected item // // is equal to the item in the given index // if (getSelectedItem() == getModelItem(index)) { // return; // } // } // // List<Integer> removed = bitset.stream().boxed().collect(Collectors.toList()); // boolean isSelected = removed.contains(index); // if (isSelected) { // removed.remove((Object)index); // } // // if (removed.isEmpty()) { // set(index); // } // // bitset.clear(); // bitset.set(index); // _beginChange(); // if (isSelected) { // _nextRemove(0, removed); // } else { // _nextAdd(0, 1); // _nextRemove(0, removed); // } // _endChange(); // } public boolean isSelected(int index) { return bitset.get(index); } public boolean isNotSelected(int index) { return !isSelected(index); }
Returns number of true bits in BitSet
/** Returns number of true bits in BitSet */
@Override public int size() { return bitset.cardinality(); }
Returns the number of bits reserved in the BitSet
/** Returns the number of bits reserved in the BitSet */
public int bitsetSize() { return bitset.size(); } @Override public int indexOf(Object obj) { reset(); return super.indexOf(obj); } @Override public boolean contains(Object o) { if (o instanceof Number) { Number n = (Number) o; int index = n.intValue(); return index >= 0 && index < bitset.length() && bitset.get(index); } return false; } public void reset() { this.lastGetIndex = -1; this.lastGetValue = -1; } @Override public void _beginChange() { if (!isAtomic()) { super._beginChange(); } } @Override public void _endChange() { if (!isAtomic()) { super._endChange(); } } @Override public final void _nextUpdate(int pos) { if (!isAtomic()) { nextUpdate(pos); } } @Override public final void _nextSet(int idx, Integer old) { if (!isAtomic()) { nextSet(idx, old); } } @Override public final void _nextReplace(int from, int to, List<? extends Integer> removed) { if (!isAtomic()) { nextReplace(from, to, removed); } } @Override public final void _nextRemove(int idx, List<? extends Integer> removed) { if (!isAtomic()) { nextRemove(idx, removed); } } @Override public final void _nextRemove(int idx, Integer removed) { if (!isAtomic()) { nextRemove(idx, removed); } } @Override public final void _nextPermutation(int from, int to, int[] perm) { if (!isAtomic()) { nextPermutation(from, to, perm); } } @Override public final void _nextAdd(int from, int to) { if (!isAtomic()) { nextAdd(from, to); } } } }