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 @@ -250,19 +250,15 @@ // and then determine all row and columns which must be selected int minRow = Math.min(anchor.getRow(), row); int maxRow = Math.max(anchor.getRow(), row); - int minColumn = Math.min(anchor.getColumn(), column); - int maxColumn = Math.max(anchor.getColumn(), column); + TableColumnBase minColumn = anchor.getColumn() < column ? anchor.getTableColumn() : tableColumn; + TableColumnBase maxColumn = anchor.getColumn() >= column ? anchor.getTableColumn() : tableColumn; if (sm.isCellSelectionEnabled()) { // clear selection, but maintain the anchor 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 @@ -461,24 +461,31 @@ * TableRow class might pick up the need to set an entire row to be * selected. */ + if (! isInCellSelectionMode()) return; if (isEmpty()) return; - if (getIndex() == -1 || getTableView() == null) return; - if (getTableView().getSelectionModel() == null) return; - - boolean isSelected = isInCellSelectionMode() && - getTableView().getSelectionModel().isSelected(getIndex(), getTableColumn()); + + final TableView tableView = getTableView(); + if (getIndex() == -1 || tableView == null) return; + + TableSelectionModel sm = tableView.getSelectionModel(); + if (sm == null) 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; + if (! isInCellSelectionMode()) 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()); + boolean isFocused = fm != null && + fm.isFocused(getIndex(), getTableColumn()); setFocused(isFocused); } @@ -510,9 +517,10 @@ } private boolean isInCellSelectionMode() { - return getTableView() != null && - getTableView().getSelectionModel() != null && - getTableView().getSelectionModel().isCellSelectionEnabled(); + TableView tableView = getTableView(); + if (tableView == null) return false; + TableSelectionModel sm = tableView.getSelectionModel(); + return 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,13 @@ public abstract void selectBelowCell(); /** + * Selects the cells in the range (minRow, minColumn) to (maxRow, maxColumn), + * inclusive. + */ + public abstract void selectRange(int minRow, TableColumnBase minColumn, + int maxRow, TableColumnBase maxColumn); + + /** * 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,18 @@ */ public abstract void clearSelection(int row, TableColumn column); + /** {@inheritDoc} */ + @Override public void selectRange(int minRow, TableColumnBase minColumn, + int maxRow, TableColumnBase maxColumn) { + final int minColumnIndex = tableView.getVisibleLeafIndex((TableColumn)minColumn); + final int maxColumnIndex = tableView.getVisibleLeafIndex((TableColumn)maxColumn); + for (int _row = minRow; _row <= maxRow; _row++) { + for (int _col = minColumnIndex; _col <= maxColumnIndex; _col++) { + select(_row, tableView.getVisibleLeafColumn(_col)); + } + } + } + /*********************************************************************** @@ -1792,8 +1799,7 @@ public TableViewArrayListSelectionModel(final TableView tableView) { super(tableView); this.tableView = tableView; -// this.selectedIndicesBitSet = new BitSet(); - + updateItemCount(); cellSelectionEnabledProperty().addListener(new InvalidationListener() { @@ -1803,8 +1809,7 @@ } }); - selectedCells = FXCollections.>observableArrayList(); - selectedCells.addListener(new ListChangeListener>() { + selectedCellsMap = new SelectedCellsMap<>(new ListChangeListener>() { @Override public void onChanged(final Change> c) { handleSelectedCellsListChangeEvent(c); } @@ -1822,11 +1827,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 +1917,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 +1935,7 @@ } + /*********************************************************************** * * * Internal properties * @@ -1973,10 +1979,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 +2049,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 +2087,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 +2127,7 @@ quietClearSelection(); } - if (! selectedCells.contains(pos)) { - selectedCells.add(pos); - } + selectedCellsMap.add(pos); updateSelectedIndex(row); focus(row, column); @@ -2188,7 +2192,7 @@ } } - if (selectedCells.isEmpty()) { + if (selectedCellsMap.isEmpty()) { if (row > 0 && row < rowCount) { select(row); } @@ -2200,16 +2204,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 +2216,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 +2247,7 @@ indices.add(tp); } } - selectedCells.setAll(indices); + selectedCellsMap.setAll(indices); if (tp != null) { select(tp.getRow(), tp.getTableColumn()); @@ -2271,7 +2258,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 +2271,51 @@ } } + @Override public void selectRange(int minRow, TableColumnBase minColumn, + int maxRow, TableColumnBase maxColumn) { + makeAtomic = true; + + if (getSelectionMode() == SelectionMode.SINGLE) { + quietClearSelection(); + select(maxRow, maxColumn); + return; + } + + final int itemCount = getItemCount(); + final boolean isCellSelectionEnabled = isCellSelectionEnabled(); + + final int minColumnIndex = tableView.getVisibleLeafIndex((TableColumn)minColumn); + final int maxColumnIndex = tableView.getVisibleLeafIndex((TableColumn)maxColumn); + + for (int _row = minRow; _row <= maxRow; _row++) { + for (int _col = minColumnIndex; _col <= maxColumnIndex; _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, (TableColumn)maxColumn); + + final int startChangeIndex = selectedCellsMap.indexOf(new TablePosition(tableView, minRow, (TableColumn)minColumn)); + final int endChangeIndex = selectedCellsMap.indexOf(new TablePosition(tableView, maxRow, (TableColumn)maxColumn)); + handleSelectedCellsListChangeEvent(new NonIterableChange.SimpleAddChange<>(startChangeIndex, endChangeIndex + 1, selectedCellsSeq)); + } + @Override public void clearSelection(int index) { clearSelection(index, null); } @@ -2296,7 +2328,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 +2348,7 @@ } private void quietClearSelection() { - selectedCells.clear(); + selectedCellsMap.clear(); } @Override public boolean isSelected(int index) { @@ -2328,22 +2360,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() { diff --git a/modules/controls/src/main/java/javafx/scene/control/TreeTableCell.java b/modules/controls/src/main/java/javafx/scene/control/TreeTableCell.java --- a/modules/controls/src/main/java/javafx/scene/control/TreeTableCell.java +++ b/modules/controls/src/main/java/javafx/scene/control/TreeTableCell.java @@ -448,27 +448,32 @@ * TableRow class might pick up the need to set an entire row to be * selected. */ + if (! isInCellSelectionMode()) return; if (isEmpty()) return; final TreeTableView tv = getTreeTableView(); - if (getIndex() == -1 || getTreeTableView() == null) return; - if (tv.getSelectionModel() == null) return; - - boolean isSelected = isInCellSelectionMode() && - tv.getSelectionModel().isSelected(getIndex(), getTableColumn()); + if (getIndex() == -1 || tv == null) return; + + TreeTableView.TreeTableViewSelectionModel sm = tv.getSelectionModel(); + if (sm == null) return; + + boolean isSelected = sm.isSelected(getIndex(), getTableColumn()); if (isSelected() == isSelected) return; updateSelected(isSelected); } private void updateFocus() { + if (! isInCellSelectionMode()) return; + final TreeTableView tv = getTreeTableView(); if (getIndex() == -1 || tv == null) return; - if (tv.getFocusModel() == null) return; - - boolean isFocused = isInCellSelectionMode() && - tv.getFocusModel() != null && - tv.getFocusModel().isFocused(getIndex(), getTableColumn()); + + TreeTableView.TreeTableViewFocusModel fm = tv.getFocusModel(); + if (fm == null) return; + + boolean isFocused = fm != null && + fm.isFocused(getIndex(), getTableColumn()); setFocused(isFocused); } @@ -501,10 +506,10 @@ } private boolean isInCellSelectionMode() { - TreeTableView treeTable = getTreeTableView(); - return treeTable != null && - treeTable.getSelectionModel() != null && - treeTable.getSelectionModel().isCellSelectionEnabled(); + TreeTableView tv = getTreeTableView(); + if (tv == null) return false; + TreeTableView.TreeTableViewSelectionModel sm = tv.getSelectionModel(); + return sm != null && sm.isCellSelectionEnabled(); } /* diff --git a/modules/controls/src/main/java/javafx/scene/control/TreeTableView.java b/modules/controls/src/main/java/javafx/scene/control/TreeTableView.java --- a/modules/controls/src/main/java/javafx/scene/control/TreeTableView.java +++ b/modules/controls/src/main/java/javafx/scene/control/TreeTableView.java @@ -29,6 +29,7 @@ import com.sun.javafx.collections.NonIterableChange; import com.sun.javafx.collections.annotations.ReturnsUnmodifiableCollection; +import com.sun.javafx.scene.control.SelectedCellsMap; import javafx.beans.property.DoubleProperty; import javafx.css.CssMetaData; import javafx.css.PseudoClass; @@ -1923,6 +1924,18 @@ return getFocusedCell().getRow(); } + /** {@inheritDoc} */ + @Override public void selectRange(int minRow, TableColumnBase,?> minColumn, + int maxRow, TableColumnBase,?> maxColumn) { + final int minColumnIndex = treeTableView.getVisibleLeafIndex((TreeTableColumn)minColumn); + final int maxColumnIndex = treeTableView.getVisibleLeafIndex((TreeTableColumn)maxColumn); + for (int _row = minRow; _row <= maxRow; _row++) { + for (int _col = minColumnIndex; _col <= maxColumnIndex; _col++) { + select(_row, treeTableView.getVisibleLeafColumn(_col)); + } + } + } + /*********************************************************************** @@ -1982,9 +1995,8 @@ this.treeTableView.rootProperty().addListener(weakRootPropertyListener); updateTreeEventListener(null, treeTableView.getRoot()); - - selectedCells = FXCollections.>observableArrayList(); - selectedCells.addListener(new ListChangeListener>() { + + selectedCellsMap = new SelectedCellsMap<>(new ListChangeListener>() { @Override public void onChanged(final ListChangeListener.Change> c) { handleSelectedCellsListChangeEvent(c); @@ -2003,11 +2015,11 @@ selectedCellsSeq = new ReadOnlyUnbackedObservableList>() { @Override public TreeTablePosition get(int i) { - return selectedCells.get(i); + return selectedCellsMap.get(i); } @Override public int size() { - return selectedCells.size(); + return selectedCellsMap.size(); } }; } @@ -2133,11 +2145,11 @@ final int clearIndex = param.getClearIndex(); TreeTablePosition oldTP = null; if (clearIndex > -1) { - for (int i = 0; i < selectedCells.size(); i++) { - TreeTablePosition tp = selectedCells.get(i); + for (int i = 0; i < selectedCellsMap.size(); i++) { + TreeTablePosition tp = selectedCellsMap.get(i); if (tp.getRow() == clearIndex) { oldTP = tp; - selectedCells.remove(i); + selectedCellsMap.remove(tp); break; } } @@ -2146,8 +2158,8 @@ if (oldTP != null && param.isSelected()) { TreeTablePosition newTP = new TreeTablePosition( treeTableView, param.getSetIndex(), oldTP.getTableColumn()); - - selectedCells.add(newTP); + + selectedCellsMap.add(newTP); } return null; @@ -2169,9 +2181,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; @@ -2219,7 +2231,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(); @@ -2254,12 +2266,10 @@ TreeTablePosition pos = new TreeTablePosition<>(getTreeTableView(), row, (TreeTableColumn)column); - if (! selectedCells.contains(pos)) { - if (getSelectionMode() == SelectionMode.SINGLE) { - quietClearSelection(); - } - selectedCells.add(pos); + if (getSelectionMode() == SelectionMode.SINGLE) { + quietClearSelection(); } + selectedCellsMap.add(pos); // setSelectedIndex(row); updateSelectedIndex(row); @@ -2328,7 +2338,7 @@ } } - if (selectedCells.isEmpty()) { + if (selectedCellsMap.isEmpty()) { if (row > 0 && row < rowCount) { select(row); } @@ -2340,16 +2350,7 @@ if (row >= 0 && row < rowCount) { TreeTablePosition pos = new TreeTablePosition(getTreeTableView(), 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++) { - TreeTablePosition selectedCell = selectedCells.get(j); - if (selectedCell.getRow() == row) { - match = true; - break; - } - } + boolean match = selectedCellsMap.isSelected(row, -1); if (! match) { positions.add(pos); lastIndex = row; @@ -2361,22 +2362,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++) { - TreeTablePosition 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 TreeTablePosition pos = new TreeTablePosition(getTreeTableView(), index, null); positions.add(pos); } - selectedCells.addAll(positions); + selectedCellsMap.addAll(positions); if (lastIndex != -1) { select(lastIndex); @@ -2401,7 +2394,7 @@ indices.add(tp); } } - selectedCells.setAll(indices); + selectedCellsMap.setAll(indices); if (tp != null) { select(tp.getRow(), tp.getTableColumn()); @@ -2412,7 +2405,7 @@ for (int i = 0; i < getRowCount(); i++) { indices.add(new TreeTablePosition<>(getTreeTableView(), i, null)); } - selectedCells.setAll(indices); + selectedCellsMap.setAll(indices); int focusedIndex = getFocusedIndex(); if (focusedIndex == -1) { @@ -2425,6 +2418,51 @@ } } + @Override public void selectRange(int minRow, TableColumnBase,?> minColumn, + int maxRow, TableColumnBase,?> maxColumn) { + makeAtomic = true; + + if (getSelectionMode() == SelectionMode.SINGLE) { + quietClearSelection(); + select(maxRow, maxColumn); + return; + } + + final int itemCount = getItemCount(); + final boolean isCellSelectionEnabled = isCellSelectionEnabled(); + + final int minColumnIndex = treeTableView.getVisibleLeafIndex((TreeTableColumn)minColumn); + final int maxColumnIndex = treeTableView.getVisibleLeafIndex((TreeTableColumn)maxColumn); + + for (int _row = minRow; _row <= maxRow; _row++) { + for (int _col = minColumnIndex; _col <= maxColumnIndex; _col++) { + // begin copy/paste of select(int, column) method (with some + // slight modifications) + if (_row < 0 || _row >= itemCount) return; + + final TreeTableColumn column = treeTableView.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; + + TreeTablePosition pos = new TreeTablePosition<>(treeTableView, _row, column); + + selectedCellsMap.add(pos); + // end copy/paste + } + } + makeAtomic = false; + + // fire off events + updateSelectedIndex(maxRow); + focus(maxRow, (TreeTableColumn)maxColumn); + + final int startChangeIndex = selectedCellsMap.indexOf(new TreeTablePosition(treeTableView, minRow, (TreeTableColumn)minColumn)); + final int endChangeIndex = selectedCellsMap.indexOf(new TreeTablePosition(treeTableView, maxRow, (TreeTableColumn)maxColumn)); + handleSelectedCellsListChangeEvent(new NonIterableChange.SimpleAddChange<>(startChangeIndex, endChangeIndex + 1, selectedCellsSeq)); + } + @Override public void clearSelection(int index) { clearSelection(index, null); } @@ -2436,7 +2474,7 @@ for (TreeTablePosition 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); @@ -2456,7 +2494,7 @@ } private void quietClearSelection() { - selectedCells.clear(); + selectedCellsMap.clear(); } @Override public boolean isSelected(int index) { @@ -2482,7 +2520,7 @@ } @Override public boolean isEmpty() { - return selectedCells.isEmpty(); + return selectedCellsMap.isEmpty(); } @Override public void selectPrevious() {