/* |
|
* Copyright (c) 2005, 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 javax.swing; |
|
import java.text.Collator; |
|
import java.util.ArrayList; |
|
import java.util.Arrays; |
|
import java.util.Collections; |
|
import java.util.Comparator; |
|
import java.util.List; |
|
import javax.swing.SortOrder; |
|
/** |
|
* An implementation of <code>RowSorter</code> that provides sorting and |
|
* filtering around a grid-based data model. |
|
* Beyond creating and installing a <code>RowSorter</code>, you very rarely |
|
* need to interact with one directly. Refer to |
|
* {@link javax.swing.table.TableRowSorter TableRowSorter} for a concrete |
|
* implementation of <code>RowSorter</code> for <code>JTable</code>. |
|
* <p> |
|
* Sorting is done based on the current <code>SortKey</code>s, in order. |
|
* If two objects are equal (the <code>Comparator</code> for the |
|
* column returns 0) the next <code>SortKey</code> is used. If no |
|
* <code>SortKey</code>s remain or the order is <code>UNSORTED</code>, then |
|
* the order of the rows in the model is used. |
|
* <p> |
|
* Sorting of each column is done by way of a <code>Comparator</code> |
|
* that you can specify using the <code>setComparator</code> method. |
|
* If a <code>Comparator</code> has not been specified, the |
|
* <code>Comparator</code> returned by |
|
* <code>Collator.getInstance()</code> is used on the results of |
|
* calling <code>toString</code> on the underlying objects. The |
|
* <code>Comparator</code> is never passed <code>null</code>. A |
|
* <code>null</code> value is treated as occurring before a |
|
* non-<code>null</code> value, and two <code>null</code> values are |
|
* considered equal. |
|
* <p> |
|
* If you specify a <code>Comparator</code> that casts its argument to |
|
* a type other than that provided by the model, a |
|
* <code>ClassCastException</code> will be thrown when the data is sorted. |
|
* <p> |
|
* In addition to sorting, <code>DefaultRowSorter</code> provides the |
|
* ability to filter rows. Filtering is done by way of a |
|
* <code>RowFilter</code> that is specified using the |
|
* <code>setRowFilter</code> method. If no filter has been specified all |
|
* rows are included. |
|
* <p> |
|
* By default, rows are in unsorted order (the same as the model) and |
|
* every column is sortable. The default <code>Comparator</code>s are |
|
* documented in the subclasses (for example, {@link |
|
* javax.swing.table.TableRowSorter TableRowSorter}). |
|
* <p> |
|
* If the underlying model structure changes (the |
|
* <code>modelStructureChanged</code> method is invoked) the following |
|
* are reset to their default values: <code>Comparator</code>s by |
|
* column, current sort order, and whether each column is sortable. To |
|
* find the default <code>Comparator</code>s, see the concrete |
|
* implementation (for example, {@link |
|
* javax.swing.table.TableRowSorter TableRowSorter}). The default |
|
* sort order is unsorted (the same as the model), and columns are |
|
* sortable by default. |
|
* <p> |
|
* If the underlying model structure changes (the |
|
* <code>modelStructureChanged</code> method is invoked) the following |
|
* are reset to their default values: <code>Comparator</code>s by column, |
|
* current sort order and whether a column is sortable. |
|
* <p> |
|
* <code>DefaultRowSorter</code> is an abstract class. Concrete |
|
* subclasses must provide access to the underlying data by invoking |
|
* {@code setModelWrapper}. The {@code setModelWrapper} method |
|
* <b>must</b> be invoked soon after the constructor is |
|
* called, ideally from within the subclass's constructor. |
|
* Undefined behavior will result if you use a {@code |
|
* DefaultRowSorter} without specifying a {@code ModelWrapper}. |
|
* <p> |
|
* <code>DefaultRowSorter</code> has two formal type parameters. The |
|
* first type parameter corresponds to the class of the model, for example |
|
* <code>DefaultTableModel</code>. The second type parameter |
|
* corresponds to the class of the identifier passed to the |
|
* <code>RowFilter</code>. Refer to <code>TableRowSorter</code> and |
|
* <code>RowFilter</code> for more details on the type parameters. |
|
* |
|
* @param <M> the type of the model |
|
* @param <I> the type of the identifier passed to the <code>RowFilter</code> |
|
* @see javax.swing.table.TableRowSorter |
|
* @see javax.swing.table.DefaultTableModel |
|
* @see java.text.Collator |
|
* @since 1.6 |
|
*/ |
|
public abstract class DefaultRowSorter<M, I> extends RowSorter<M> { |
|
/** |
|
* Whether or not we resort on TableModelEvent.UPDATEs. |
|
*/ |
|
private boolean sortsOnUpdates; |
|
/** |
|
* View (JTable) -> model. |
|
*/ |
|
private Row[] viewToModel; |
|
/** |
|
* model -> view (JTable) |
|
*/ |
|
private int[] modelToView; |
|
/** |
|
* Comparators specified by column. |
|
*/ |
|
private Comparator[] comparators; |
|
/** |
|
* Whether or not the specified column is sortable, by column. |
|
*/ |
|
private boolean[] isSortable; |
|
/** |
|
* Cached SortKeys for the current sort. |
|
*/ |
|
private SortKey[] cachedSortKeys; |
|
/** |
|
* Cached comparators for the current sort |
|
*/ |
|
private Comparator[] sortComparators; |
|
/** |
|
* Developer supplied Filter. |
|
*/ |
|
private RowFilter<? super M,? super I> filter; |
|
/** |
|
* Value passed to the filter. The same instance is passed to the |
|
* filter for different rows. |
|
*/ |
|
private FilterEntry filterEntry; |
|
/** |
|
* The sort keys. |
|
*/ |
|
private List<SortKey> sortKeys; |
|
/** |
|
* Whether or not to use getStringValueAt. This is indexed by column. |
|
*/ |
|
private boolean[] useToString; |
|
/** |
|
* Indicates the contents are sorted. This is used if |
|
* getSortsOnUpdates is false and an update event is received. |
|
*/ |
|
private boolean sorted; |
|
/** |
|
* Maximum number of sort keys. |
|
*/ |
|
private int maxSortKeys; |
|
/** |
|
* Provides access to the data we're sorting/filtering. |
|
*/ |
|
private ModelWrapper<M,I> modelWrapper; |
|
/** |
|
* Size of the model. This is used to enforce error checking within |
|
* the table changed notification methods (such as rowsInserted). |
|
*/ |
|
private int modelRowCount; |
|
/** |
|
* Creates an empty <code>DefaultRowSorter</code>. |
|
*/ |
|
public DefaultRowSorter() { |
|
sortKeys = Collections.emptyList(); |
|
maxSortKeys = 3; |
|
} |
|
/** |
|
* Sets the model wrapper providing the data that is being sorted and |
|
* filtered. |
|
* |
|
* @param modelWrapper the model wrapper responsible for providing the |
|
* data that gets sorted and filtered |
|
* @throws IllegalArgumentException if {@code modelWrapper} is |
|
* {@code null} |
|
*/ |
|
protected final void setModelWrapper(ModelWrapper<M,I> modelWrapper) { |
|
if (modelWrapper == null) { |
|
throw new IllegalArgumentException( |
|
"modelWrapper most be non-null"); |
|
} |
|
ModelWrapper<M,I> last = this.modelWrapper; |
|
this.modelWrapper = modelWrapper; |
|
if (last != null) { |
|
modelStructureChanged(); |
|
} else { |
|
// If last is null, we're in the constructor. If we're in |
|
// the constructor we don't want to call to overridable methods. |
|
modelRowCount = getModelWrapper().getRowCount(); |
|
} |
|
} |
|
/** |
|
* Returns the model wrapper providing the data that is being sorted and |
|
* filtered. |
|
* |
|
* @return the model wrapper responsible for providing the data that |
|
* gets sorted and filtered |
|
*/ |
|
protected final ModelWrapper<M,I> getModelWrapper() { |
|
return modelWrapper; |
|
} |
|
/** |
|
* Returns the underlying model. |
|
* |
|
* @return the underlying model |
|
*/ |
|
public final M getModel() { |
|
return getModelWrapper().getModel(); |
|
} |
|
/** |
|
* Sets whether or not the specified column is sortable. The specified |
|
* value is only checked when <code>toggleSortOrder</code> is invoked. |
|
* It is still possible to sort on a column that has been marked as |
|
* unsortable by directly setting the sort keys. The default is |
|
* true. |
|
* |
|
* @param column the column to enable or disable sorting on, in terms |
|
* of the underlying model |
|
* @param sortable whether or not the specified column is sortable |
|
* @throws IndexOutOfBoundsException if <code>column</code> is outside |
|
* the range of the model |
|
* @see #toggleSortOrder |
|
* @see #setSortKeys |
|
*/ |
|
public void setSortable(int column, boolean sortable) { |
|
checkColumn(column); |
|
if (isSortable == null) { |
|
isSortable = new boolean[getModelWrapper().getColumnCount()]; |
|
for (int i = isSortable.length - 1; i >= 0; i--) { |
|
isSortable[i] = true; |
|
} |
|
} |
|
isSortable[column] = sortable; |
|
} |
|
/** |
|
* Returns true if the specified column is sortable; otherwise, false. |
|
* |
|
* @param column the column to check sorting for, in terms of the |
|
* underlying model |
|
* @return true if the column is sortable |
|
* @throws IndexOutOfBoundsException if column is outside |
|
* the range of the underlying model |
|
*/ |
|
public boolean isSortable(int column) { |
|
checkColumn(column); |
|
return (isSortable == null) ? true : isSortable[column]; |
|
} |
|
/** |
|
* Sets the sort keys. This creates a copy of the supplied |
|
* {@code List}; subsequent changes to the supplied |
|
* {@code List} do not effect this {@code DefaultRowSorter}. |
|
* If the sort keys have changed this triggers a sort. |
|
* |
|
* @param sortKeys the new <code>SortKeys</code>; <code>null</code> |
|
* is a shorthand for specifying an empty list, |
|
* indicating that the view should be unsorted |
|
* @throws IllegalArgumentException if any of the values in |
|
* <code>sortKeys</code> are null or have a column index outside |
|
* the range of the model |
|
*/ |
|
public void setSortKeys(List<? extends SortKey> sortKeys) { |
|
List<SortKey> old = this.sortKeys; |
|
if (sortKeys != null && sortKeys.size() > 0) { |
|
int max = getModelWrapper().getColumnCount(); |
|
for (SortKey key : sortKeys) { |
|
if (key == null || key.getColumn() < 0 || |
|
key.getColumn() >= max) { |
|
throw new IllegalArgumentException("Invalid SortKey"); |
|
} |
|
} |
|
this.sortKeys = Collections.unmodifiableList( |
|
new ArrayList<SortKey>(sortKeys)); |
|
} |
|
else { |
|
this.sortKeys = Collections.emptyList(); |
|
} |
|
if (!this.sortKeys.equals(old)) { |
|
fireSortOrderChanged(); |
|
if (viewToModel == null) { |
|
// Currently unsorted, use sort so that internal fields |
|
// are correctly set. |
|
sort(); |
|
} else { |
|
sortExistingData(); |
|
} |
|
} |
|
} |
|
/** |
|
* Returns the current sort keys. This returns an unmodifiable |
|
* {@code non-null List}. If you need to change the sort keys, |
|
* make a copy of the returned {@code List}, mutate the copy |
|
* and invoke {@code setSortKeys} with the new list. |
|
* |
|
* @return the current sort order |
|
*/ |
|
public List<? extends SortKey> getSortKeys() { |
|
return sortKeys; |
|
} |
|
/** |
|
* Sets the maximum number of sort keys. The number of sort keys |
|
* determines how equal values are resolved when sorting. For |
|
* example, assume a table row sorter is created and |
|
* <code>setMaxSortKeys(2)</code> is invoked on it. The user |
|
* clicks the header for column 1, causing the table rows to be |
|
* sorted based on the items in column 1. Next, the user clicks |
|
* the header for column 2, causing the table to be sorted based |
|
* on the items in column 2; if any items in column 2 are equal, |
|
* then those particular rows are ordered based on the items in |
|
* column 1. In this case, we say that the rows are primarily |
|
* sorted on column 2, and secondarily on column 1. If the user |
|
* then clicks the header for column 3, then the items are |
|
* primarily sorted on column 3 and secondarily sorted on column |
|
* 2. Because the maximum number of sort keys has been set to 2 |
|
* with <code>setMaxSortKeys</code>, column 1 no longer has an |
|
* effect on the order. |
|
* <p> |
|
* The maximum number of sort keys is enforced by |
|
* <code>toggleSortOrder</code>. You can specify more sort |
|
* keys by invoking <code>setSortKeys</code> directly and they will |
|
* all be honored. However if <code>toggleSortOrder</code> is subsequently |
|
* invoked the maximum number of sort keys will be enforced. |
|
* The default value is 3. |
|
* |
|
* @param max the maximum number of sort keys |
|
* @throws IllegalArgumentException if <code>max</code> < 1 |
|
*/ |
|
public void setMaxSortKeys(int max) { |
|
if (max < 1) { |
|
throw new IllegalArgumentException("Invalid max"); |
|
} |
|
maxSortKeys = max; |
|
} |
|
/** |
|
* Returns the maximum number of sort keys. |
|
* |
|
* @return the maximum number of sort keys |
|
*/ |
|
public int getMaxSortKeys() { |
|
return maxSortKeys; |
|
} |
|
/** |
|
* If true, specifies that a sort should happen when the underlying |
|
* model is updated (<code>rowsUpdated</code> is invoked). For |
|
* example, if this is true and the user edits an entry the |
|
* location of that item in the view may change. The default is |
|
* false. |
|
* |
|
* @param sortsOnUpdates whether or not to sort on update events |
|
*/ |
|
public void setSortsOnUpdates(boolean sortsOnUpdates) { |
|
this.sortsOnUpdates = sortsOnUpdates; |
|
} |
|
/** |
|
* Returns true if a sort should happen when the underlying |
|
* model is updated; otherwise, returns false. |
|
* |
|
* @return whether or not to sort when the model is updated |
|
*/ |
|
public boolean getSortsOnUpdates() { |
|
return sortsOnUpdates; |
|
} |
|
/** |
|
* Sets the filter that determines which rows, if any, should be |
|
* hidden from the view. The filter is applied before sorting. A value |
|
* of <code>null</code> indicates all values from the model should be |
|
* included. |
|
* <p> |
|
* <code>RowFilter</code>'s <code>include</code> method is passed an |
|
* <code>Entry</code> that wraps the underlying model. The number |
|
* of columns in the <code>Entry</code> corresponds to the |
|
* number of columns in the <code>ModelWrapper</code>. The identifier |
|
* comes from the <code>ModelWrapper</code> as well. |
|
* <p> |
|
* This method triggers a sort. |
|
* |
|
* @param filter the filter used to determine what entries should be |
|
* included |
|
*/ |
|
public void setRowFilter(RowFilter<? super M,? super I> filter) { |
|
this.filter = filter; |
|
sort(); |
|
} |
|
/** |
|
* Returns the filter that determines which rows, if any, should |
|
* be hidden from view. |
|
* |
|
* @return the filter |
|
*/ |
|
public RowFilter<? super M,? super I> getRowFilter() { |
|
return filter; |
|
} |
|
/** |
|
* Reverses the sort order from ascending to descending (or |
|
* descending to ascending) if the specified column is already the |
|
* primary sorted column; otherwise, makes the specified column |
|
* the primary sorted column, with an ascending sort order. If |
|
* the specified column is not sortable, this method has no |
|
* effect. |
|
* |
|
* @param column index of the column to make the primary sorted column, |
|
* in terms of the underlying model |
|
* @throws IndexOutOfBoundsException {@inheritDoc} |
|
* @see #setSortable(int,boolean) |
|
* @see #setMaxSortKeys(int) |
|
*/ |
|
public void toggleSortOrder(int column) { |
|
checkColumn(column); |
|
if (isSortable(column)) { |
|
List<SortKey> keys = new ArrayList<SortKey>(getSortKeys()); |
|
SortKey sortKey; |
|
int sortIndex; |
|
for (sortIndex = keys.size() - 1; sortIndex >= 0; sortIndex--) { |
|
if (keys.get(sortIndex).getColumn() == column) { |
|
break; |
|
} |
|
} |
|
if (sortIndex == -1) { |
|
// Key doesn't exist |
|
sortKey = new SortKey(column, SortOrder.ASCENDING); |
|
keys.add(0, sortKey); |
|
} |
|
else if (sortIndex == 0) { |
|
// It's the primary sorting key, toggle it |
|
keys.set(0, toggle(keys.get(0))); |
|
} |
|
else { |
|
// It's not the first, but was sorted on, remove old |
|
// entry, insert as first with ascending. |
|
keys.remove(sortIndex); |
|
keys.add(0, new SortKey(column, SortOrder.ASCENDING)); |
|
} |
|
if (keys.size() > getMaxSortKeys()) { |
|
keys = keys.subList(0, getMaxSortKeys()); |
|
} |
|
setSortKeys(keys); |
|
} |
|
} |
|
private SortKey toggle(SortKey key) { |
|
if (key.getSortOrder() == SortOrder.ASCENDING) { |
|
return new SortKey(key.getColumn(), SortOrder.DESCENDING); |
|
} |
|
return new SortKey(key.getColumn(), SortOrder.ASCENDING); |
|
} |
|
/** |
|
* {@inheritDoc} |
|
* |
|
* @throws IndexOutOfBoundsException {@inheritDoc} |
|
*/ |
|
public int convertRowIndexToView(int index) { |
|
if (modelToView == null) { |
|
if (index < 0 || index >= getModelWrapper().getRowCount()) { |
|
throw new IndexOutOfBoundsException("Invalid index"); |
|
} |
|
return index; |
|
} |
|
return modelToView[index]; |
|
} |
|
/** |
|
* {@inheritDoc} |
|
* |
|
* @throws IndexOutOfBoundsException {@inheritDoc} |
|
*/ |
|
public int convertRowIndexToModel(int index) { |
|
if (viewToModel == null) { |
|
if (index < 0 || index >= getModelWrapper().getRowCount()) { |
|
throw new IndexOutOfBoundsException("Invalid index"); |
|
} |
|
return index; |
|
} |
|
return viewToModel[index].modelIndex; |
|
} |
|
private boolean isUnsorted() { |
|
List<? extends SortKey> keys = getSortKeys(); |
|
int keySize = keys.size(); |
|
return (keySize == 0 || keys.get(0).getSortOrder() == |
|
SortOrder.UNSORTED); |
|
} |
|
/** |
|
* Sorts the existing filtered data. This should only be used if |
|
* the filter hasn't changed. |
|
*/ |
|
private void sortExistingData() { |
|
int[] lastViewToModel = getViewToModelAsInts(viewToModel); |
|
updateUseToString(); |
|
cacheSortKeys(getSortKeys()); |
|
if (isUnsorted()) { |
|
if (getRowFilter() == null) { |
|
viewToModel = null; |
|
modelToView = null; |
|
} else { |
|
int included = 0; |
|
for (int i = 0; i < modelToView.length; i++) { |
|
if (modelToView[i] != -1) { |
|
viewToModel[included].modelIndex = i; |
|
modelToView[i] = included++; |
|
} |
|
} |
|
} |
|
} else { |
|
// sort the data |
|
Arrays.sort(viewToModel); |
|
// Update the modelToView array |
|
setModelToViewFromViewToModel(false); |
|
} |
|
fireRowSorterChanged(lastViewToModel); |
|
} |
|
/** |
|
* Sorts and filters the rows in the view based on the sort keys |
|
* of the columns currently being sorted and the filter, if any, |
|
* associated with this sorter. An empty <code>sortKeys</code> list |
|
* indicates that the view should unsorted, the same as the model. |
|
* |
|
* @see #setRowFilter |
|
* @see #setSortKeys |
|
*/ |
|
public void sort() { |
|
sorted = true; |
|
int[] lastViewToModel = getViewToModelAsInts(viewToModel); |
|
updateUseToString(); |
|
if (isUnsorted()) { |
|
// Unsorted |
|
cachedSortKeys = new SortKey[0]; |
|
if (getRowFilter() == null) { |
|
// No filter & unsorted |
|
if (viewToModel != null) { |
|
// sorted -> unsorted |
|
viewToModel = null; |
|
modelToView = null; |
|
} |
|
else { |
|
// unsorted -> unsorted |
|
// No need to do anything. |
|
return; |
|
} |
|
} |
|
else { |
|
// There is filter, reset mappings |
|
initializeFilteredMapping(); |
|
} |
|
} |
|
else { |
|
cacheSortKeys(getSortKeys()); |
|
if (getRowFilter() != null) { |
|
initializeFilteredMapping(); |
|
} |
|
else { |
|
createModelToView(getModelWrapper().getRowCount()); |
|
createViewToModel(getModelWrapper().getRowCount()); |
|
} |
|
// sort them |
|
Arrays.sort(viewToModel); |
|
// Update the modelToView array |
|
setModelToViewFromViewToModel(false); |
|
} |
|
fireRowSorterChanged(lastViewToModel); |
|
} |
|
/** |
|
* Updates the useToString mapping before a sort. |
|
*/ |
|
private void updateUseToString() { |
|
int i = getModelWrapper().getColumnCount(); |
|
if (useToString == null || useToString.length != i) { |
|
useToString = new boolean[i]; |
|
} |
|
for (--i; i >= 0; i--) { |
|
useToString[i] = useToString(i); |
|
} |
|
} |
|
/** |
|
* Resets the viewToModel and modelToView mappings based on |
|
* the current Filter. |
|
*/ |
|
private void initializeFilteredMapping() { |
|
int rowCount = getModelWrapper().getRowCount(); |
|
int i, j; |
|
int excludedCount = 0; |
|
// Update model -> view |
|
createModelToView(rowCount); |
|
for (i = 0; i < rowCount; i++) { |
|
if (include(i)) { |
|
modelToView[i] = i - excludedCount; |
|
} |
|
else { |
|
modelToView[i] = -1; |
|
excludedCount++; |
|
} |
|
} |
|
// Update view -> model |
|
createViewToModel(rowCount - excludedCount); |
|
for (i = 0, j = 0; i < rowCount; i++) { |
|
if (modelToView[i] != -1) { |
|
viewToModel[j++].modelIndex = i; |
|
} |
|
} |
|
} |
|
/** |
|
* Makes sure the modelToView array is of size rowCount. |
|
*/ |
|
private void createModelToView(int rowCount) { |
|
if (modelToView == null || modelToView.length != rowCount) { |
|
modelToView = new int[rowCount]; |
|
} |
|
} |
|
/** |
|
* Resets the viewToModel array to be of size rowCount. |
|
*/ |
|
private void createViewToModel(int rowCount) { |
|
int recreateFrom = 0; |
|
if (viewToModel != null) { |
|
recreateFrom = Math.min(rowCount, viewToModel.length); |
|
if (viewToModel.length != rowCount) { |
|
Row[] oldViewToModel = viewToModel; |
|
viewToModel = new Row[rowCount]; |
|
System.arraycopy(oldViewToModel, 0, viewToModel, |
|
0, recreateFrom); |
|
} |
|
} |
|
else { |
|
viewToModel = new Row[rowCount]; |
|
} |
|
int i; |
|
for (i = 0; i < recreateFrom; i++) { |
|
viewToModel[i].modelIndex = i; |
|
} |
|
for (i = recreateFrom; i < rowCount; i++) { |
|
viewToModel[i] = new Row(this, i); |
|
} |
|
} |
|
/** |
|
* Caches the sort keys before a sort. |
|
*/ |
|
private void cacheSortKeys(List<? extends SortKey> keys) { |
|
int keySize = keys.size(); |
|
sortComparators = new Comparator[keySize]; |
|
for (int i = 0; i < keySize; i++) { |
|
sortComparators[i] = getComparator0(keys.get(i).getColumn()); |
|
} |
|
cachedSortKeys = keys.toArray(new SortKey[keySize]); |
|
} |
|
/** |
|
* Returns whether or not to convert the value to a string before |
|
* doing comparisons when sorting. If true |
|
* <code>ModelWrapper.getStringValueAt</code> will be used, otherwise |
|
* <code>ModelWrapper.getValueAt</code> will be used. It is up to |
|
* subclasses, such as <code>TableRowSorter</code>, to honor this value |
|
* in their <code>ModelWrapper</code> implementation. |
|
* |
|
* @param column the index of the column to test, in terms of the |
|
* underlying model |
|
* @throws IndexOutOfBoundsException if <code>column</code> is not valid |
|
*/ |
|
protected boolean useToString(int column) { |
|
return (getComparator(column) == null); |
|
} |
|
/** |
|
* Refreshes the modelToView mapping from that of viewToModel. |
|
* If <code>unsetFirst</code> is true, all indices in modelToView are |
|
* first set to -1. |
|
*/ |
|
private void setModelToViewFromViewToModel(boolean unsetFirst) { |
|
int i; |
|
if (unsetFirst) { |
|
for (i = modelToView.length - 1; i >= 0; i--) { |
|
modelToView[i] = -1; |
|
} |
|
} |
|
for (i = viewToModel.length - 1; i >= 0; i--) { |
|
modelToView[viewToModel[i].modelIndex] = i; |
|
} |
|
} |
|
private int[] getViewToModelAsInts(Row[] viewToModel) { |
|
if (viewToModel != null) { |
|
int[] viewToModelI = new int[viewToModel.length]; |
|
for (int i = viewToModel.length - 1; i >= 0; i--) { |
|
viewToModelI[i] = viewToModel[i].modelIndex; |
|
} |
|
return viewToModelI; |
|
} |
|
return new int[0]; |
|
} |
|
/** |
|
* Sets the <code>Comparator</code> to use when sorting the specified |
|
* column. This does not trigger a sort. If you want to sort after |
|
* setting the comparator you need to explicitly invoke <code>sort</code>. |
|
* |
|
* @param column the index of the column the <code>Comparator</code> is |
|
* to be used for, in terms of the underlying model |
|
* @param comparator the <code>Comparator</code> to use |
|
* @throws IndexOutOfBoundsException if <code>column</code> is outside |
|
* the range of the underlying model |
|
*/ |
|
public void setComparator(int column, Comparator<?> comparator) { |
|
checkColumn(column); |
|
if (comparators == null) { |
|
comparators = new Comparator[getModelWrapper().getColumnCount()]; |
|
} |
|
comparators[column] = comparator; |
|
} |
|
/** |
|
* Returns the <code>Comparator</code> for the specified |
|
* column. This will return <code>null</code> if a <code>Comparator</code> |
|
* has not been specified for the column. |
|
* |
|
* @param column the column to fetch the <code>Comparator</code> for, in |
|
* terms of the underlying model |
|
* @return the <code>Comparator</code> for the specified column |
|
* @throws IndexOutOfBoundsException if column is outside |
|
* the range of the underlying model |
|
*/ |
|
public Comparator<?> getComparator(int column) { |
|
checkColumn(column); |
|
if (comparators != null) { |
|
return comparators[column]; |
|
} |
|
return null; |
|
} |
|
// Returns the Comparator to use during sorting. Where as |
|
// getComparator() may return null, this will never return null. |
|
private Comparator getComparator0(int column) { |
|
Comparator comparator = getComparator(column); |
|
if (comparator != null) { |
|
return comparator; |
|
} |
|
// This should be ok as useToString(column) should have returned |
|
// true in this case. |
|
return Collator.getInstance(); |
|
} |
|
private RowFilter.Entry<M,I> getFilterEntry(int modelIndex) { |
|
if (filterEntry == null) { |
|
filterEntry = new FilterEntry(); |
|
} |
|
filterEntry.modelIndex = modelIndex; |
|
return filterEntry; |
|
} |
|
/** |
|
* {@inheritDoc} |
|
*/ |
|
public int getViewRowCount() { |
|
if (viewToModel != null) { |
|
// When filtering this may differ from getModelWrapper().getRowCount() |
|
return viewToModel.length; |
|
} |
|
return getModelWrapper().getRowCount(); |
|
} |
|
/** |
|
* {@inheritDoc} |
|
*/ |
|
public int getModelRowCount() { |
|
return getModelWrapper().getRowCount(); |
|
} |
|
private void allChanged() { |
|
modelToView = null; |
|
viewToModel = null; |
|
comparators = null; |
|
isSortable = null; |
|
if (isUnsorted()) { |
|
// Keys are already empty, to force a resort we have to |
|
// call sort |
|
sort(); |
|
} else { |
|
setSortKeys(null); |
|
} |
|
} |
|
/** |
|
* {@inheritDoc} |
|
*/ |
|
public void modelStructureChanged() { |
|
allChanged(); |
|
modelRowCount = getModelWrapper().getRowCount(); |
|
} |
|
/** |
|
* {@inheritDoc} |
|
*/ |
|
public void allRowsChanged() { |
|
modelRowCount = getModelWrapper().getRowCount(); |
|
sort(); |
|
} |
|
/** |
|
* {@inheritDoc} |
|
* |
|
* @throws IndexOutOfBoundsException {@inheritDoc} |
|
*/ |
|
public void rowsInserted(int firstRow, int endRow) { |
|
checkAgainstModel(firstRow, endRow); |
|
int newModelRowCount = getModelWrapper().getRowCount(); |
|
if (endRow >= newModelRowCount) { |
|
throw new IndexOutOfBoundsException("Invalid range"); |
|
} |
|
modelRowCount = newModelRowCount; |
|
if (shouldOptimizeChange(firstRow, endRow)) { |
|
rowsInserted0(firstRow, endRow); |
|
} |
|
} |
|
/** |
|
* {@inheritDoc} |
|
* |
|
* @throws IndexOutOfBoundsException {@inheritDoc} |
|
*/ |
|
public void rowsDeleted(int firstRow, int endRow) { |
|
checkAgainstModel(firstRow, endRow); |
|
if (firstRow >= modelRowCount || endRow >= modelRowCount) { |
|
throw new IndexOutOfBoundsException("Invalid range"); |
|
} |
|
modelRowCount = getModelWrapper().getRowCount(); |
|
if (shouldOptimizeChange(firstRow, endRow)) { |
|
rowsDeleted0(firstRow, endRow); |
|
} |
|
} |
|
/** |
|
* {@inheritDoc} |
|
* |
|
* @throws IndexOutOfBoundsException {@inheritDoc} |
|
*/ |
|
public void rowsUpdated(int firstRow, int endRow) { |
|
checkAgainstModel(firstRow, endRow); |
|
if (firstRow >= modelRowCount || endRow >= modelRowCount) { |
|
throw new IndexOutOfBoundsException("Invalid range"); |
|
} |
|
if (getSortsOnUpdates()) { |
|
if (shouldOptimizeChange(firstRow, endRow)) { |
|
rowsUpdated0(firstRow, endRow); |
|
} |
|
} |
|
else { |
|
sorted = false; |
|
} |
|
} |
|
/** |
|
* {@inheritDoc} |
|
* |
|
* @throws IndexOutOfBoundsException {@inheritDoc} |
|
*/ |
|
public void rowsUpdated(int firstRow, int endRow, int column) { |
|
checkColumn(column); |
|
rowsUpdated(firstRow, endRow); |
|
} |
|
private void checkAgainstModel(int firstRow, int endRow) { |
|
if (firstRow > endRow || firstRow < 0 || endRow < 0 || |
|
firstRow > modelRowCount) { |
|
throw new IndexOutOfBoundsException("Invalid range"); |
|
} |
|
} |
|
/** |
|
* Returns true if the specified row should be included. |
|
*/ |
|
private boolean include(int row) { |
|
RowFilter<? super M, ? super I> filter = getRowFilter(); |
|
if (filter != null) { |
|
return filter.include(getFilterEntry(row)); |
|
} |
|
// null filter, always include the row. |
|
return true; |
|
} |
|
@SuppressWarnings("unchecked") |
|
private int compare(int model1, int model2) { |
|
int column; |
|
SortOrder sortOrder; |
|
Object v1, v2; |
|
int result; |
|
for (int counter = 0; counter < cachedSortKeys.length; counter++) { |
|
column = cachedSortKeys[counter].getColumn(); |
|
sortOrder = cachedSortKeys[counter].getSortOrder(); |
|
if (sortOrder == SortOrder.UNSORTED) { |
|
result = model1 - model2; |
|
} else { |
|
// v1 != null && v2 != null |
|
if (useToString[column]) { |
|
v1 = getModelWrapper().getStringValueAt(model1, column); |
|
v2 = getModelWrapper().getStringValueAt(model2, column); |
|
} else { |
|
v1 = getModelWrapper().getValueAt(model1, column); |
|
v2 = getModelWrapper().getValueAt(model2, column); |
|
} |
|
// Treat nulls as < then non-null |
|
if (v1 == null) { |
|
if (v2 == null) { |
|
result = 0; |
|
} else { |
|
result = -1; |
|
} |
|
} else if (v2 == null) { |
|
result = 1; |
|
} else { |
|
result = sortComparators[counter].compare(v1, v2); |
|
} |
|
if (sortOrder == SortOrder.DESCENDING) { |
|
result *= -1; |
|
} |
|
} |
|
if (result != 0) { |
|
return result; |
|
} |
|
} |
|
// If we get here, they're equal. Fallback to model order. |
|
return model1 - model2; |
|
} |
|
/** |
|
* Whether not we are filtering/sorting. |
|
*/ |
|
private boolean isTransformed() { |
|
return (viewToModel != null); |
|
} |
|
/** |
|
* Insets new set of entries. |
|
* |
|
* @param toAdd the Rows to add, sorted |
|
* @param current the array to insert the items into |
|
*/ |
|
private void insertInOrder(List<Row> toAdd, Row[] current) { |
|
int last = 0; |
|
int index; |
|
int max = toAdd.size(); |
|
for (int i = 0; i < max; i++) { |
|
index = Arrays.binarySearch(current, toAdd.get(i)); |
|
if (index < 0) { |
|
index = -1 - index; |
|
} |
|
System.arraycopy(current, last, |
|
viewToModel, last + i, index - last); |
|
viewToModel[index + i] = toAdd.get(i); |
|
last = index; |
|
} |
|
System.arraycopy(current, last, viewToModel, last + max, |
|
current.length - last); |
|
} |
|
/** |
|
* Returns true if we should try and optimize the processing of the |
|
* <code>TableModelEvent</code>. If this returns false, assume the |
|
* event was dealt with and no further processing needs to happen. |
|
*/ |
|
private boolean shouldOptimizeChange(int firstRow, int lastRow) { |
|
if (!isTransformed()) { |
|
// Not transformed, nothing to do. |
|
return false; |
|
} |
|
if (!sorted || (lastRow - firstRow) > viewToModel.length / 10) { |
|
// We either weren't sorted, or to much changed, sort it all |
|
sort(); |
|
return false; |
|
} |
|
return true; |
|
} |
|
private void rowsInserted0(int firstRow, int lastRow) { |
|
int[] oldViewToModel = getViewToModelAsInts(viewToModel); |
|
int i; |
|
int delta = (lastRow - firstRow) + 1; |
|
List<Row> added = new ArrayList<Row>(delta); |
|
// Build the list of Rows to add into added |
|
for (i = firstRow; i <= lastRow; i++) { |
|
if (include(i)) { |
|
added.add(new Row(this, i)); |
|
} |
|
} |
|
// Adjust the model index of rows after the effected region |
|
int viewIndex; |
|
for (i = modelToView.length - 1; i >= firstRow; i--) { |
|
viewIndex = modelToView[i]; |
|
if (viewIndex != -1) { |
|
viewToModel[viewIndex].modelIndex += delta; |
|
} |
|
} |
|
// Insert newly added rows into viewToModel |
|
if (added.size() > 0) { |
|
Collections.sort(added); |
|
Row[] lastViewToModel = viewToModel; |
|
viewToModel = new Row[viewToModel.length + added.size()]; |
|
insertInOrder(added, lastViewToModel); |
|
} |
|
// Update modelToView |
|
createModelToView(getModelWrapper().getRowCount()); |
|
setModelToViewFromViewToModel(true); |
|
// Notify of change |
|
fireRowSorterChanged(oldViewToModel); |
|
} |
|
private void rowsDeleted0(int firstRow, int lastRow) { |
|
int[] oldViewToModel = getViewToModelAsInts(viewToModel); |
|
int removedFromView = 0; |
|
int i; |
|
int viewIndex; |
|
// Figure out how many visible rows are going to be effected. |
|
for (i = firstRow; i <= lastRow; i++) { |
|
viewIndex = modelToView[i]; |
|
if (viewIndex != -1) { |
|
removedFromView++; |
|
viewToModel[viewIndex] = null; |
|
} |
|
} |
|
// Update the model index of rows after the effected region |
|
int delta = lastRow - firstRow + 1; |
|
for (i = modelToView.length - 1; i > lastRow; i--) { |
|
viewIndex = modelToView[i]; |
|
if (viewIndex != -1) { |
|
viewToModel[viewIndex].modelIndex -= delta; |
|
} |
|
} |
|
// Then patch up the viewToModel array |
|
if (removedFromView > 0) { |
|
Row[] newViewToModel = new Row[viewToModel.length - |
|
removedFromView]; |
|
int newIndex = 0; |
|
int last = 0; |
|
for (i = 0; i < viewToModel.length; i++) { |
|
if (viewToModel[i] == null) { |
|
System.arraycopy(viewToModel, last, |
|
newViewToModel, newIndex, i - last); |
|
newIndex += (i - last); |
|
last = i + 1; |
|
} |
|
} |
|
System.arraycopy(viewToModel, last, |
|
newViewToModel, newIndex, viewToModel.length - last); |
|
viewToModel = newViewToModel; |
|
} |
|
// Update the modelToView mapping |
|
createModelToView(getModelWrapper().getRowCount()); |
|
setModelToViewFromViewToModel(true); |
|
// And notify of change |
|
fireRowSorterChanged(oldViewToModel); |
|
} |
|
private void rowsUpdated0(int firstRow, int lastRow) { |
|
int[] oldViewToModel = getViewToModelAsInts(viewToModel); |
|
int i, j; |
|
int delta = lastRow - firstRow + 1; |
|
int modelIndex; |
|
int last; |
|
int index; |
|
if (getRowFilter() == null) { |
|
// Sorting only: |
|
// Remove the effected rows |
|
Row[] updated = new Row[delta]; |
|
for (j = 0, i = firstRow; i <= lastRow; i++, j++) { |
|
updated[j] = viewToModel[modelToView[i]]; |
|
} |
|
// Sort the update rows |
|
Arrays.sort(updated); |
|
// Build the intermediary array: the array of |
|
// viewToModel without the effected rows. |
|
Row[] intermediary = new Row[viewToModel.length - delta]; |
|
for (i = 0, j = 0; i < viewToModel.length; i++) { |
|
modelIndex = viewToModel[i].modelIndex; |
|
if (modelIndex < firstRow || modelIndex > lastRow) { |
|
intermediary[j++] = viewToModel[i]; |
|
} |
|
} |
|
// Build the new viewToModel |
|
insertInOrder(Arrays.asList(updated), intermediary); |
|
// Update modelToView |
|
setModelToViewFromViewToModel(false); |
|
} |
|
else { |
|
// Sorting & filtering. |
|
// Remove the effected rows, adding them to updated and setting |
|
// modelToView to -2 for any rows that were not filtered out |
|
List<Row> updated = new ArrayList<Row>(delta); |
|
int newlyVisible = 0; |
|
int newlyHidden = 0; |
|
int effected = 0; |
|
for (i = firstRow; i <= lastRow; i++) { |
|
if (modelToView[i] == -1) { |
|
// This row was filtered out |
|
if (include(i)) { |
|
// No longer filtered |
|
updated.add(new Row(this, i)); |
|
newlyVisible++; |
|
} |
|
} |
|
else { |
|
// This row was visible, make sure it should still be |
|
// visible. |
|
if (!include(i)) { |
|
newlyHidden++; |
|
} |
|
else { |
|
updated.add(viewToModel[modelToView[i]]); |
|
} |
|
modelToView[i] = -2; |
|
effected++; |
|
} |
|
} |
|
// Sort the updated rows |
|
Collections.sort(updated); |
|
// Build the intermediary array: the array of |
|
// viewToModel without the updated rows. |
|
Row[] intermediary = new Row[viewToModel.length - effected]; |
|
for (i = 0, j = 0; i < viewToModel.length; i++) { |
|
modelIndex = viewToModel[i].modelIndex; |
|
if (modelToView[modelIndex] != -2) { |
|
intermediary[j++] = viewToModel[i]; |
|
} |
|
} |
|
// Recreate viewToModel, if necessary |
|
if (newlyVisible != newlyHidden) { |
|
viewToModel = new Row[viewToModel.length + newlyVisible - |
|
newlyHidden]; |
|
} |
|
// Rebuild the new viewToModel array |
|
insertInOrder(updated, intermediary); |
|
// Update modelToView |
|
setModelToViewFromViewToModel(true); |
|
} |
|
// And finally fire a sort event. |
|
fireRowSorterChanged(oldViewToModel); |
|
} |
|
private void checkColumn(int column) { |
|
if (column < 0 || column >= getModelWrapper().getColumnCount()) { |
|
throw new IndexOutOfBoundsException( |
|
"column beyond range of TableModel"); |
|
} |
|
} |
|
/** |
|
* <code>DefaultRowSorter.ModelWrapper</code> is responsible for providing |
|
* the data that gets sorted by <code>DefaultRowSorter</code>. You |
|
* normally do not interact directly with <code>ModelWrapper</code>. |
|
* Subclasses of <code>DefaultRowSorter</code> provide an |
|
* implementation of <code>ModelWrapper</code> wrapping another model. |
|
* For example, |
|
* <code>TableRowSorter</code> provides a <code>ModelWrapper</code> that |
|
* wraps a <code>TableModel</code>. |
|
* <p> |
|
* <code>ModelWrapper</code> makes a distinction between values as |
|
* <code>Object</code>s and <code>String</code>s. This allows |
|
* implementations to provide a custom string |
|
* converter to be used instead of invoking <code>toString</code> on the |
|
* object. |
|
* |
|
* @param <M> the type of the underlying model |
|
* @param <I> the identifier supplied to the filter |
|
* @since 1.6 |
|
* @see RowFilter |
|
* @see RowFilter.Entry |
|
*/ |
|
protected abstract static class ModelWrapper<M,I> { |
|
/** |
|
* Creates a new <code>ModelWrapper</code>. |
|
*/ |
|
protected ModelWrapper() { |
|
} |
|
/** |
|
* Returns the underlying model that this <code>Model</code> is |
|
* wrapping. |
|
* |
|
* @return the underlying model |
|
*/ |
|
public abstract M getModel(); |
|
/** |
|
* Returns the number of columns in the model. |
|
* |
|
* @return the number of columns in the model |
|
*/ |
|
public abstract int getColumnCount(); |
|
/** |
|
* Returns the number of rows in the model. |
|
* |
|
* @return the number of rows in the model |
|
*/ |
|
public abstract int getRowCount(); |
|
/** |
|
* Returns the value at the specified index. |
|
* |
|
* @param row the row index |
|
* @param column the column index |
|
* @return the value at the specified index |
|
* @throws IndexOutOfBoundsException if the indices are outside |
|
* the range of the model |
|
*/ |
|
public abstract Object getValueAt(int row, int column); |
|
/** |
|
* Returns the value as a <code>String</code> at the specified |
|
* index. This implementation uses <code>toString</code> on |
|
* the result from <code>getValueAt</code> (making sure |
|
* to return an empty string for null values). Subclasses that |
|
* override this method should never return null. |
|
* |
|
* @param row the row index |
|
* @param column the column index |
|
* @return the value at the specified index as a <code>String</code> |
|
* @throws IndexOutOfBoundsException if the indices are outside |
|
* the range of the model |
|
*/ |
|
public String getStringValueAt(int row, int column) { |
|
Object o = getValueAt(row, column); |
|
if (o == null) { |
|
return ""; |
|
} |
|
String string = o.toString(); |
|
if (string == null) { |
|
return ""; |
|
} |
|
return string; |
|
} |
|
/** |
|
* Returns the identifier for the specified row. The return value |
|
* of this is used as the identifier for the |
|
* <code>RowFilter.Entry</code> that is passed to the |
|
* <code>RowFilter</code>. |
|
* |
|
* @param row the row to return the identifier for, in terms of |
|
* the underlying model |
|
* @return the identifier |
|
* @see RowFilter.Entry#getIdentifier |
|
*/ |
|
public abstract I getIdentifier(int row); |
|
} |
|
/** |
|
* RowFilter.Entry implementation that delegates to the ModelWrapper. |
|
* getFilterEntry(int) creates the single instance of this that is |
|
* passed to the Filter. Only call getFilterEntry(int) to get |
|
* the instance. |
|
*/ |
|
private class FilterEntry extends RowFilter.Entry<M,I> { |
|
/** |
|
* The index into the model, set in getFilterEntry |
|
*/ |
|
int modelIndex; |
|
public M getModel() { |
|
return getModelWrapper().getModel(); |
|
} |
|
public int getValueCount() { |
|
return getModelWrapper().getColumnCount(); |
|
} |
|
public Object getValue(int index) { |
|
return getModelWrapper().getValueAt(modelIndex, index); |
|
} |
|
public String getStringValue(int index) { |
|
return getModelWrapper().getStringValueAt(modelIndex, index); |
|
} |
|
public I getIdentifier() { |
|
return getModelWrapper().getIdentifier(modelIndex); |
|
} |
|
} |
|
/** |
|
* Row is used to handle the actual sorting by way of Comparable. It |
|
* will use the sortKeys to do the actual comparison. |
|
*/ |
|
// NOTE: this class is static so that it can be placed in an array |
|
private static class Row implements Comparable<Row> { |
|
private DefaultRowSorter sorter; |
|
int modelIndex; |
|
public Row(DefaultRowSorter sorter, int index) { |
|
this.sorter = sorter; |
|
modelIndex = index; |
|
} |
|
public int compareTo(Row o) { |
|
return sorter.compare(modelIndex, o.modelIndex); |
|
} |
|
} |
|
} |