All Downloads are FREE. Search and download functionalities are using the official Maven repository.

javafx.scene.control.MultipleSelectionModelBase Maven / Gradle / Ivy

/*
 * Copyright (c) 2010, 2013, 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.MappingChange;
import com.sun.javafx.collections.NonIterableChange;
import static javafx.scene.control.SelectionMode.SINGLE;

import java.util.AbstractList;
import java.util.ArrayList;
import java.util.BitSet;
import java.util.Collections;
import java.util.List;

import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.collections.ListChangeListener.Change;
import javafx.util.Callback;

import com.sun.javafx.scene.control.ReadOnlyUnbackedObservableList;


/**
 * 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  The type of the underlying data model for the UI control.
 */
abstract class MultipleSelectionModelBase extends MultipleSelectionModel {

    /***********************************************************************
     *                                                                     *
     * Constructors                                                        *
     *                                                                     *
     **********************************************************************/

    public MultipleSelectionModelBase() {
        selectedIndexProperty().addListener(new InvalidationListener() {
            @Override public void invalidated(Observable 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 BitSet();

        selectedIndicesSeq = new ReadOnlyUnbackedObservableList() {
            @Override public Integer get(int index) {
                if (index < 0 || index >= getItemCount()) return -1;

                for (int pos = 0, val = selectedIndices.nextSetBit(0);
                    val >= 0 || pos == index;
                    pos++, val = selectedIndices.nextSetBit(val+1)) {
                        if (pos == index) return val;
                }

                return -1;
            }

            @Override public int size() {
                return selectedIndices.cardinality();
            }

            @Override public boolean contains(Object o) {
                if (o instanceof Number) {
                    Number n = (Number) o;
                    int index = n.intValue();

                    return index > 0 && index < selectedIndices.length() &&
                            selectedIndices.get(index);
                }

                return false;
            }
        };
        
        final MappingChange.Map map = new MappingChange.Map() {
            @Override public T map(Integer f) {
                return getModelItem(f);
            }
        };
        
        selectedIndicesSeq.addListener(new ListChangeListener() {
            @Override public void onChanged(final Change c) {
                // when the selectedIndices ObservableList changes, we manually call
                // the observers of the selectedItems ObservableList.
                selectedItemsSeq.callObservers(new MappingChange(c, map, selectedItemsSeq));
                c.reset();
            }
        });


        selectedItemsSeq = new ReadOnlyUnbackedObservableList() {
            @Override public T get(int i) {
                int pos = selectedIndicesSeq.get(i);
                return getModelItem(pos);
            }

            @Override public int size() {
                return selectedIndices.cardinality();
            }
        };
    }



    /***********************************************************************
     *                                                                     *
     * 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 BitSet selectedIndices;
    private final ReadOnlyUnbackedObservableList selectedIndicesSeq;
    @Override public ObservableList getSelectedIndices() {
        return selectedIndicesSeq;
    }
//    private void setSelectedIndices(BitSet rows) {
//        this.selectedIndices.clear();
//        this.selectedIndices.or(rows);
//    }

    private final ReadOnlyUnbackedObservableList selectedItemsSeq;
    @Override public ObservableList getSelectedItems() {
        return selectedItemsSeq;
    }



    /***********************************************************************
     *                                                                     *
     * Internal field                                                      *
     *                                                                     *
     **********************************************************************/

    // Fix for RT-20945
    boolean makeAtomic = false;


    /***********************************************************************
     *                                                                     *
     * 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.
     */
    protected abstract int getItemCount();
    
    /**
     * Returns the item at the given index. An example using ListView would be
     * listView.getItems().get(index).
     * 
     * @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 callback) {
        // with no check here, we get RT-15024
        if (position < 0) return;
        if (shift == 0) return;
        
        int selectedIndicesCardinality = selectedIndices.cardinality(); // number of true bits
        if (selectedIndicesCardinality == 0) return;
        
        int selectedIndicesSize = selectedIndices.size();   // number of bits reserved 
        
        int[] perm = new int[selectedIndicesSize];
        int idx = 0;
        
        if (shift > 0) {
            for (int i = selectedIndicesSize - 1; i >= position && i >= 0; i--) {
                boolean selected = selectedIndices.get(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.get(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;
                }
            }
        }
        
        // This ensure that the selection remains accurate when a shift occurs.
        if (getFocusedIndex() >= position && getFocusedIndex() > -1 && getFocusedIndex() + shift > -1) {
            final int newFocus = getFocusedIndex() + shift;
            setSelectedIndex(newFocus);
 
            // removed due to RT-27185
            // focus(newFocus);
        }
         
        selectedIndicesSeq.callObservers(
                new NonIterableChange.SimplePermutationChange(
                        0, 
                        selectedIndicesCardinality, 
                        perm, 
                        selectedIndicesSeq));
    }

    @Override public void clearAndSelect(int row) {
        // clear out all other selection quietly - so that we don't fire events
        quietClearSelection();

        // and select
        select(row);
    }

    @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;

        if (! selectedIndices.get(row)) {
            if (getSelectionMode() == SINGLE) {
                quietClearSelection();
            }
            selectedIndices.set(row);
        }

        setSelectedIndex(row);
        focus(row);
        
        int changeIndex = selectedIndicesSeq.indexOf(row);
        selectedIndicesSeq.callObservers(new NonIterableChange.SimpleAddChange(changeIndex, changeIndex+1, selectedIndicesSeq));
        
        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.
        setSelectedItem(obj);
    }

    @Override public void selectIndices(int row, int... rows) {
        if (rows == null) {
            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);
                }
            }

            selectedIndicesSeq.callObservers(new NonIterableChange.SimpleAddChange(0, 1, selectedIndicesSeq));
        } else {
            final List actualSelectedRows = new ArrayList();
            
            int lastIndex = -1;
            if (row >= 0 && row < rowCount) {
                lastIndex = row;
                if (! selectedIndices.get(row)) {
                    selectedIndices.set(row);
                    actualSelectedRows.add(row);
                }
            }

            for (int i = 0; i < rows.length; i++) {
                int index = rows[i];
                if (index < 0 || index >= rowCount) continue;
                lastIndex = index;
                
                if (! selectedIndices.get(index)) {
                    selectedIndices.set(index);
                    actualSelectedRows.add(index);
                }
            }

            if (lastIndex != -1) {
                setSelectedIndex(lastIndex);
                focus(lastIndex);
                setSelectedItem(getModelItem(lastIndex));
            }

            // need to come up with ranges based on the actualSelectedRows, and
            // then fire the appropriate number of changes. We also need to
            // translate from a desired row to select to where that row is 
            // represented in the selectedIndices list. For example,
            // we may have requested to select row 5, and the selectedIndices
            // list may therefore have the following: [1,4,5], meaning row 5
            // is in position 2 of the selectedIndices list
            Change change = createRangeChange(selectedIndicesSeq, actualSelectedRows);
            selectedIndicesSeq.callObservers(change);
        }
    }
    
    static Change createRangeChange(final ObservableList list, final List addedItems) {
        Change change = new Change(list) {
            private final int[] EMPTY_PERM = new int[0];
            private final int addedSize = addedItems.size(); 
            
            private boolean invalid = true;
            
            private int pos = 0;
            private int from = pos;
            private int to = pos;
            
            @Override public int getFrom() {
                checkState();
                return from;
            }

            @Override public int getTo() {
                checkState();
                return to;
            }

            @Override public List getRemoved() {
                checkState();
                return Collections.emptyList();
            }

            @Override protected int[] getPermutation() {
                checkState();
                return EMPTY_PERM;
            }
            
            @Override public int getAddedSize() {
                return to - from;
            }

            @Override public boolean next() {
                if (pos >= addedSize) return false;
                
                // starting from pos, we keep going until the value is
                // not the next value
                from = pos;
                int startValue = addedItems.get(pos++);
                int endValue = startValue;
                while (pos < addedSize) {
                    int previousEndValue = endValue;
                    endValue = addedItems.get(pos++);
                    if (previousEndValue != (endValue - 1)) {
                        break;
                    }
                }
                to = pos;
                
                if (invalid) {
                    invalid = false;
                    return true; 
                }
                
                // we keep going until we've represented all changes!
                return pos < addedSize;
            }

            @Override public void reset() {
                invalid = true;
                pos = 0;
            }
            
            private void checkState() {
                if (invalid) {
                    throw new IllegalStateException("Invalid Change state: next() must be called before inspecting the Change.");
                }
            }
            
        };
        return change;
    }

    @Override public void selectAll() {
        if (getSelectionMode() == SINGLE) return;

        quietClearSelection();
        if (getItemCount() <= 0) return;

        int rowCount = getItemCount();

        // set all selected indices to true
        quietClearSelection();
        selectedIndices.set(0, (int) rowCount, true);
        selectedIndicesSeq.callObservers(new NonIterableChange.SimpleAddChange(0, (int) rowCount, selectedIndicesSeq));

        int focusedIndex = getFocusedIndex();
        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();
        }

        selectedIndicesSeq.callObservers(
                new NonIterableChange.GenericAddRemoveChange(index, index+1, 
                Collections.singletonList(index), selectedIndicesSeq));
    }

    @Override public void clearSelection() {
        if (! makeAtomic) {
            setSelectedIndex(-1);
            focus(-1);
        }

        if (! selectedIndices.isEmpty()) {
            List removed = new AbstractList() {
                final BitSet clone = (BitSet) selectedIndices.clone();

                @Override public Integer get(int index) {
                    return clone.nextSetBit(index);
                }

                @Override public int size() {
                    return clone.cardinality();
                }
            };

            quietClearSelection();
            
            selectedIndicesSeq.callObservers(
                    new NonIterableChange.GenericAddRemoveChange(0, 0, 
                    removed, selectedIndicesSeq));
        }
    }

    private void quietClearSelection() {
        selectedIndices.clear();
    }

    @Override public boolean isSelected(int index) {
        if (index >= 0 && index < getItemCount()) {
            return selectedIndices.get(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);
        }
    }
    
    
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy