diff --git a/modules/controls/src/main/java/com/sun/javafx/scene/control/SelectedCellsMap.java b/modules/controls/src/main/java/com/sun/javafx/scene/control/SelectedCellsMap.java new file mode 100644 --- /dev/null +++ b/modules/controls/src/main/java/com/sun/javafx/scene/control/SelectedCellsMap.java @@ -0,0 +1,206 @@ +/* + * Copyright (c) 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 com.sun.javafx.scene.control; + +import javafx.collections.FXCollections; +import javafx.collections.ListChangeListener; +import javafx.collections.ObservableList; +import javafx.scene.control.TablePositionBase; + +import java.util.*; + +/** + * Implementation code used by the TableSelectionModel implementations. In short + * this code exists to speed up some common use cases which were incredibly + * slow in the old approach. The old approach essentially required a lot of + * iterating through the selectedCells list. The new approach is to keep this + * list for what it is good for (representing selection order primarily), and + * introduce a Map to speed up the slow parts - namely looking + * up whether a given row/column intersection is selected or not. + * + * Note that a map that contains an empty bitset is used to represent that the + * row is selected. + * + * Refer to RT-33442 for more information on this issue. + */ +// T == TablePosition +public class SelectedCellsMap { + private final ObservableList selectedCells; + + private final Map selectedCellBitSetMap; + + public SelectedCellsMap(final ListChangeListener listener) { + selectedCells = FXCollections.observableArrayList(); + selectedCells.addListener(listener); + + selectedCellBitSetMap = new TreeMap<>(new Comparator() { + @Override public int compare(Integer o1, Integer o2) { + return o1.compareTo(o2); + } + }); + } + + public int size() { + return selectedCells.size(); + } + + public T get(int i) { + if (i < 0) { + return null; + } + return selectedCells.get(i); + } + + public void add(T tp) { + final int row = tp.getRow(); + final int columnIndex = tp.getColumn(); + + // update the bitset map + BitSet bitset; + if (! selectedCellBitSetMap.containsKey(row)) { + bitset = new BitSet(); + selectedCellBitSetMap.put(row, bitset); + } else { + bitset = selectedCellBitSetMap.get(row); + } + + if (columnIndex >= 0) { + boolean isAlreadySet = bitset.get(columnIndex); + bitset.set(columnIndex); + + if (! isAlreadySet) { + // add into the list + selectedCells.add(tp); + } + } else { + // FIXME slow path (for now) + if (! selectedCells.contains(tp)) { + selectedCells.add(tp); + } + } + } + + public void addAll(Collection cells) { + // update bitset + for (T tp : cells) { + final int row = tp.getRow(); + final int columnIndex = tp.getColumn(); + + // update the bitset map + BitSet bitset; + if (! selectedCellBitSetMap.containsKey(row)) { + bitset = new BitSet(); + selectedCellBitSetMap.put(row, bitset); + } else { + bitset = selectedCellBitSetMap.get(row); + } + + if (columnIndex < 0) { + continue; + } + + bitset.set(columnIndex); + } + + // add into the list + selectedCells.addAll(cells); + } + + public void setAll(Collection cells) { + // update bitset + selectedCellBitSetMap.clear(); + for (T tp : cells) { + final int row = tp.getRow(); + final int columnIndex = tp.getColumn(); + + // update the bitset map + BitSet bitset; + if (! selectedCellBitSetMap.containsKey(row)) { + bitset = new BitSet(); + selectedCellBitSetMap.put(row, bitset); + } else { + bitset = selectedCellBitSetMap.get(row); + } + + if (columnIndex < 0) { + continue; + } + + bitset.set(columnIndex); + } + + // add into the list + selectedCells.setAll(cells); + } + + public void remove(T tp) { + final int row = tp.getRow(); + final int columnIndex = tp.getColumn(); + + // update the bitset map + if (selectedCellBitSetMap.containsKey(row)) { + BitSet bitset = selectedCellBitSetMap.get(row); + + if (columnIndex >= 0) { + bitset.clear(columnIndex); + } + + if (bitset.isEmpty()) { + selectedCellBitSetMap.remove(row); + } + } + + // update list + selectedCells.remove(tp); + } + + public void clear() { + // update bitset + selectedCellBitSetMap.clear(); + + // update list + selectedCells.clear(); + } + + public boolean isSelected(int row, int columnIndex) { + if (columnIndex < 0) { + return selectedCellBitSetMap.containsKey(row); + } else { + return selectedCellBitSetMap.containsKey(row) ? selectedCellBitSetMap.get(row).get(columnIndex) : false; + } + } + + public int indexOf(T tp) { + return selectedCells.indexOf(tp); + } + + public boolean isEmpty() { + return selectedCells.isEmpty(); + } + + public ObservableList getSelectedCells() { + return selectedCells; + } +} \ No newline at end of file diff --git a/modules/controls/src/main/java/com/sun/javafx/scene/control/behavior/TableCellBehaviorBase.java b/modules/controls/src/main/java/com/sun/javafx/scene/control/behavior/TableCellBehaviorBase.java --- a/modules/controls/src/main/java/com/sun/javafx/scene/control/behavior/TableCellBehaviorBase.java +++ b/modules/controls/src/main/java/com/sun/javafx/scene/control/behavior/TableCellBehaviorBase.java @@ -258,11 +258,7 @@ sm.clearSelection(); // and then perform the selection - for (int _row = minRow; _row <= maxRow; _row++) { - for (int _col = minColumn; _col <= maxColumn; _col++) { - sm.select(_row, getVisibleLeafColumn(_col)); - } - } + sm.selectRange(minRow, minColumn, maxRow, maxColumn); } else { // To prevent RT-32119, we make a copy of the selected indices // list first, so that we are not iterating and modifying it diff --git a/modules/controls/src/main/java/javafx/scene/control/TableCell.java b/modules/controls/src/main/java/javafx/scene/control/TableCell.java --- a/modules/controls/src/main/java/javafx/scene/control/TableCell.java +++ b/modules/controls/src/main/java/javafx/scene/control/TableCell.java @@ -462,23 +462,31 @@ * selected. */ if (isEmpty()) return; - if (getIndex() == -1 || getTableView() == null) return; - if (getTableView().getSelectionModel() == null) return; - - boolean isSelected = isInCellSelectionMode() && - getTableView().getSelectionModel().isSelected(getIndex(), getTableColumn()); - if (isSelected() == isSelected) return; - updateSelected(isSelected); + final TableView tableView = getTableView(); + if (getIndex() == -1 || tableView == null) return; + + TableSelectionModel sm = tableView.getSelectionModel(); + if (sm == null) return; + + if (! isInCellSelectionMode()) return; + + boolean _isSelected = sm.isSelected(getIndex(), getTableColumn()); + if (isSelected() == _isSelected) return; + + updateSelected(_isSelected); } private void updateFocus() { - if (getIndex() == -1 || getTableView() == null) return; - if (getTableView().getFocusModel() == null) return; + final TableView tableView = getTableView(); + if (getIndex() == -1 || tableView == null) return; + + final TableViewFocusModel fm = tableView.getFocusModel(); + if (fm == null) return; boolean isFocused = isInCellSelectionMode() && - getTableView().getFocusModel() != null && - getTableView().getFocusModel().isFocused(getIndex(), getTableColumn()); + fm != null && + fm.isFocused(getIndex(), getTableColumn()); setFocused(isFocused); } @@ -510,9 +518,9 @@ } private boolean isInCellSelectionMode() { - return getTableView() != null && - getTableView().getSelectionModel() != null && - getTableView().getSelectionModel().isCellSelectionEnabled(); + TableView tableView = getTableView(); + TableSelectionModel sm = tableView.getSelectionModel(); + return tableView != null && sm != null && sm.isCellSelectionEnabled(); } /* diff --git a/modules/controls/src/main/java/javafx/scene/control/TableSelectionModel.java b/modules/controls/src/main/java/javafx/scene/control/TableSelectionModel.java --- a/modules/controls/src/main/java/javafx/scene/control/TableSelectionModel.java +++ b/modules/controls/src/main/java/javafx/scene/control/TableSelectionModel.java @@ -82,6 +82,15 @@ public abstract void selectBelowCell(); /** + * Selects the cells in the range (minRow, minColumn) to (maxRow, maxColumn), + * inclusive. + */ + public void selectRange(int minRow, int minColumn, int maxRow, int maxColumn) { + // FIXME make abstract + // no-op + } + + /** * A boolean property used to represent whether the table is in * row or cell selection modes. By default a table is in row selection * mode which means that individual cells can not be selected. Setting diff --git a/modules/controls/src/main/java/javafx/scene/control/TableView.java b/modules/controls/src/main/java/javafx/scene/control/TableView.java --- a/modules/controls/src/main/java/javafx/scene/control/TableView.java +++ b/modules/controls/src/main/java/javafx/scene/control/TableView.java @@ -26,15 +26,10 @@ package javafx.scene.control; import java.lang.ref.WeakReference; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Comparator; -import java.util.HashMap; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Set; +import java.util.*; import com.sun.javafx.scene.control.Logging; +import com.sun.javafx.scene.control.SelectedCellsMap; import javafx.beans.DefaultProperty; import javafx.beans.InvalidationListener; import javafx.beans.Observable; @@ -1690,6 +1685,9 @@ */ public abstract void clearSelection(int row, TableColumn column); + /** {@inheritDoc} */ + @Override public abstract void selectRange(int minRow, int minColumn, int maxRow, int maxColumn); + /*********************************************************************** @@ -1792,8 +1790,7 @@ public TableViewArrayListSelectionModel(final TableView tableView) { super(tableView); this.tableView = tableView; -// this.selectedIndicesBitSet = new BitSet(); - + updateItemCount(); cellSelectionEnabledProperty().addListener(new InvalidationListener() { @@ -1803,8 +1800,7 @@ } }); - selectedCells = FXCollections.>observableArrayList(); - selectedCells.addListener(new ListChangeListener>() { + selectedCellsMap = new SelectedCellsMap<>(new ListChangeListener>() { @Override public void onChanged(final Change> c) { handleSelectedCellsListChangeEvent(c); } @@ -1822,11 +1818,11 @@ selectedCellsSeq = new ReadOnlyUnbackedObservableList>() { @Override public TablePosition get(int i) { - return selectedCells.get(i); + return selectedCellsMap.get(i); } @Override public int size() { - return selectedCells.size(); + return selectedCellsMap.size(); } }; @@ -1912,9 +1908,9 @@ * * **********************************************************************/ - // the only 'proper' internal observableArrayList, selectedItems and selectedIndices + // the only 'proper' internal data structure, selectedItems and selectedIndices // are both 'read-only and unbacked'. - private final ObservableList> selectedCells; + private final SelectedCellsMap> selectedCellsMap; // used to represent the _row_ backing data for the selectedCells private final ReadOnlyUnbackedObservableList selectedItems; @@ -1930,6 +1926,7 @@ } + /*********************************************************************** * * * Internal properties * @@ -1973,10 +1970,10 @@ if (position < 0) return; if (shift == 0) return; - List> newIndices = new ArrayList>(selectedCells.size()); + List> newIndices = new ArrayList>(selectedCellsMap.size()); - for (int i = 0; i < selectedCells.size(); i++) { - final TablePosition old = selectedCells.get(i); + for (int i = 0; i < selectedCellsMap.size(); i++) { + final TablePosition old = selectedCellsMap.get(i); final int oldRow = old.getRow(); final int newRow = oldRow < position ? oldRow : oldRow + shift; @@ -2043,7 +2040,7 @@ // (6) quietClearSelection(); - selectedCells.setAll(newIndices); + selectedCellsMap.setAll(newIndices); selectedCellsSeq.callObservers(new NonIterableChange.SimpleAddChange<>(0, newIndices.size(), selectedCellsSeq)); if (oldSelectedIndex >= 0 && oldSelectedIndex < itemCount) { @@ -2081,7 +2078,7 @@ // firstly we make a copy of the selection, so that we can send out // the correct details in the selection change event - List> previousSelection = new ArrayList<>(selectedCells); + List> previousSelection = new ArrayList<>(selectedCellsMap.getSelectedCells()); // then clear the current selection clearSelection(); @@ -2121,9 +2118,7 @@ quietClearSelection(); } - if (! selectedCells.contains(pos)) { - selectedCells.add(pos); - } + selectedCellsMap.add(pos); updateSelectedIndex(row); focus(row, column); @@ -2188,7 +2183,7 @@ } } - if (selectedCells.isEmpty()) { + if (selectedCellsMap.isEmpty()) { if (row > 0 && row < rowCount) { select(row); } @@ -2200,16 +2195,7 @@ if (row >= 0 && row < rowCount) { TablePosition tp = new TablePosition(getTableView(), row, null); - // refer to the multi-line comment below for the justification for the following - // code. - boolean match = false; - for (int j = 0; j < selectedCells.size(); j++) { - TablePosition selectedCell = selectedCells.get(j); - if (selectedCell.getRow() == row) { - match = true; - break; - } - } + boolean match = selectedCellsMap.isSelected(row, -1); if (! match) { positions.add(tp); lastIndex = row; @@ -2221,22 +2207,14 @@ if (index < 0 || index >= rowCount) continue; lastIndex = index; - // we need to manually check all selected cells to see whether this index is already - // selected. This is because selectIndices is inherently row-based, but there may - // be a selected cell where the column is non-null. If we were to simply do a - // selectedCells.contains(pos), then we would not find the match and duplicate the - // row selection. This leads to bugs such as RT-29930. - for (int j = 0; j < selectedCells.size(); j++) { - TablePosition selectedCell = selectedCells.get(j); - if (selectedCell.getRow() == index) continue outer; - } + if (selectedCellsMap.isSelected(index, -1)) continue outer; // if we are here then we have successfully gotten through the for-loop above TablePosition pos = new TablePosition(getTableView(), index, null); positions.add(pos); } - - selectedCells.addAll(positions); + + selectedCellsMap.addAll(positions); if (lastIndex != -1) { select(lastIndex); @@ -2260,7 +2238,7 @@ indices.add(tp); } } - selectedCells.setAll(indices); + selectedCellsMap.setAll(indices); if (tp != null) { select(tp.getRow(), tp.getTableColumn()); @@ -2271,7 +2249,7 @@ for (int i = 0; i < getItemCount(); i++) { indices.add(new TablePosition<>(getTableView(), i, null)); } - selectedCells.setAll(indices); + selectedCellsMap.setAll(indices); int focusedIndex = getFocusedIndex(); if (focusedIndex == -1) { @@ -2284,6 +2262,47 @@ } } + public void selectRange(int minRow, int minColumn, int maxRow, int maxColumn) { + makeAtomic = true; + + if (getSelectionMode() == SelectionMode.SINGLE) { + quietClearSelection(); + select(maxRow, tableView.getVisibleLeafColumn(maxColumn)); + return; + } + + final int itemCount = getItemCount(); + final boolean isCellSelectionEnabled = isCellSelectionEnabled(); + + for (int _row = minRow; _row <= maxRow; _row++) { + for (int _col = minColumn; _col <= maxColumn; _col++) { + // begin copy/paste of select(int, column) method (with some + // slight modifications) + if (_row < 0 || _row >= itemCount) return; + + final TableColumn column = tableView.getVisibleLeafColumn(_col); + + // if I'm in cell selection mode but the column is null, I don't want + // to select the whole row instead... + if (column == null && isCellSelectionEnabled) return; + + TablePosition pos = new TablePosition<>(tableView, _row, column); + + selectedCellsMap.add(pos); + // end copy/paste + } + } + makeAtomic = false; + + // fire off events + updateSelectedIndex(maxRow); + focus(maxRow, tableView.getVisibleLeafColumn(maxColumn)); + + final int startChangeIndex = selectedCellsMap.indexOf(new TablePosition(tableView, minRow, tableView.getVisibleLeafColumn(minColumn))); + final int endChangeIndex = selectedCellsMap.indexOf(new TablePosition(tableView, maxRow, tableView.getVisibleLeafColumn(maxColumn))); + handleSelectedCellsListChangeEvent(new NonIterableChange.SimpleAddChange<>(startChangeIndex, endChangeIndex + 1, selectedCellsSeq)); + } + @Override public void clearSelection(int index) { clearSelection(index, null); } @@ -2296,7 +2315,7 @@ for (TablePosition pos : getSelectedCells()) { if ((! csMode && pos.getRow() == row) || (csMode && pos.equals(tp))) { - selectedCells.remove(pos); + selectedCellsMap.remove(pos); // give focus to this cell index focus(row); @@ -2316,7 +2335,7 @@ } private void quietClearSelection() { - selectedCells.clear(); + selectedCellsMap.clear(); } @Override public boolean isSelected(int index) { @@ -2328,22 +2347,15 @@ // When in cell selection mode, we currently do NOT support selecting // entire rows, so a isSelected(row, null) // should always return false. - if (isCellSelectionEnabled() && (column == null)) return false; - - for (TablePosition tp : getSelectedCells()) { - boolean columnMatch = ! isCellSelectionEnabled() || - (column == null && tp.getTableColumn() == null) || - (column != null && column.equals(tp.getTableColumn())); - - if (tp.getRow() == row && columnMatch) { - return true; - } - } - return false; + final boolean isCellSelectionEnabled = isCellSelectionEnabled(); + if (isCellSelectionEnabled && column == null) return false; + + int columnIndex = tableView.getVisibleLeafIndex(column); + return selectedCellsMap.isSelected(row, columnIndex); } @Override public boolean isEmpty() { - return selectedCells.isEmpty(); + return selectedCellsMap.isEmpty(); } @Override public void selectPrevious() {