/*
* Copyright (c) 2005, 2006, 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 RowSorter
that provides sorting and
filtering around a grid-based data model.
Beyond creating and installing a RowSorter
, you very rarely need to interact with one directly. Refer to TableRowSorter
for a concrete implementation of RowSorter
for JTable
.
Sorting is done based on the current SortKey
s, in order.
If two objects are equal (the Comparator
for the
column returns 0) the next SortKey
is used. If no
SortKey
s remain or the order is UNSORTED
, then
the order of the rows in the model is used.
Sorting of each column is done by way of a Comparator
that you can specify using the setComparator
method.
If a Comparator
has not been specified, the
Comparator
returned by
Collator.getInstance()
is used on the results of
calling toString
on the underlying objects. The
Comparator
is never passed null
. A
null
value is treated as occuring before a
non-null
value, and two null
values are
considered equal.
If you specify a Comparator
that casts its argument to
a type other than that provided by the model, a
ClassCastException
will be thrown when the data is sorted.
In addition to sorting, DefaultRowSorter
provides the
ability to filter rows. Filtering is done by way of a
RowFilter
that is specified using the
setRowFilter
method. If no filter has been specified all
rows are included.
By default, rows are in unsorted order (the same as the model) and
every column is sortable. The default Comparator
s are documented in the subclasses (for example, TableRowSorter
).
If the underlying model structure changes (the
modelStructureChanged
method is invoked) the following
are reset to their default values: Comparator
s by
column, current sort order, and whether each column is sortable. To
find the default Comparator
s, see the concrete implementation (for example, TableRowSorter
). The default sort order is unsorted (the same as the model), and columns are sortable by default.
If the underlying model structure changes (the
modelStructureChanged
method is invoked) the following
are reset to their default values: Comparator
s by column,
current sort order and whether a column is sortable.
DefaultRowSorter
is an abstract class. Concrete subclasses must provide access to the underlying data by invoking setModelWrapper
. The setModelWrapper
method must be invoked soon after the constructor is called, ideally from within the subclass's constructor. Undefined behavior will result if you use a
DefaultRowSorter
without specifying a ModelWrapper
.
DefaultRowSorter
has two formal type parameters. The
first type parameter corresponds to the class of the model, for example
DefaultTableModel
. The second type parameter
corresponds to the class of the identifier passed to the
RowFilter
. Refer to TableRowSorter
and
RowFilter
for more details on the type parameters.
Type parameters: See Also: Since: 1.6
/**
* 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 occuring 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.
/**
* Whether or not we resort on TableModelEvent.UPDATEs.
*/
private boolean sortsOnUpdates;
View (JTable) -> model.
/**
* View (JTable) -> model.
*/
private Row[] viewToModel;
model -> view (JTable)
/**
* model -> view (JTable)
*/
private int[] modelToView;
Comparators specified by column.
/**
* Comparators specified by column.
*/
private Comparator[] comparators;
Whether or not the specified column is sortable, by column.
/**
* Whether or not the specified column is sortable, by column.
*/
private boolean[] isSortable;
Cached SortKeys for the current sort.
/**
* Cached SortKeys for the current sort.
*/
private SortKey[] cachedSortKeys;
Cached comparators for the current sort
/**
* Cached comparators for the current sort
*/
private Comparator[] sortComparators;
Developer supplied Filter.
/**
* 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.
/**
* Value passed to the filter. The same instance is passed to the
* filter for different rows.
*/
private FilterEntry filterEntry;
The sort keys.
/**
* The sort keys.
*/
private List<SortKey> sortKeys;
Whether or not to use getStringValueAt. This is indexed by column.
/**
* 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.
/**
* 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.
/**
* Maximum number of sort keys.
*/
private int maxSortKeys;
Provides access to the data we're sorting/filtering.
/**
* 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).
/**
* 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 DefaultRowSorter
.
/**
* 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.
Params: - modelWrapper – the model wrapper responsible for providing the
data that gets sorted and filtered
Throws: - IllegalArgumentException – if
modelWrapper
is null
/**
* 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.
Returns: the model wrapper responsible for providing the data that
gets sorted and filtered
/**
* 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.
Returns: the underlying model
/**
* 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 toggleSortOrder
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.
Params: - column – the column to enable or disable sorting on, in terms
of the underlying model
- sortable – whether or not the specified column is sortable
Throws: - IndexOutOfBoundsException – if
column
is outside
the range of the model
See Also:
/**
* 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.
Params: - column – the column to check sorting for, in terms of the
underlying model
Throws: - IndexOutOfBoundsException – if column is outside
the range of the underlying model
Returns: true if the column is 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 List
; subsequent changes to the supplied List
do not effect this DefaultRowSorter
. If the sort keys have changed this triggers a sort. Params: - sortKeys – the new
SortKeys
; null
is a shorthand for specifying an empty list,
indicating that the view should be unsorted
Throws: - IllegalArgumentException – if any of the values in
sortKeys
are null or have a column index outside
the range of the model
/**
* 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 non-null List
. If you need to change the sort keys, make a copy of the returned List
, mutate the copy and invoke setSortKeys
with the new list. Returns: the current sort order
/**
* 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
setMaxSortKeys(2)
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 setMaxSortKeys
, column 1 no longer has an
effect on the order.
The maximum number of sort keys is enforced by
toggleSortOrder
. You can specify more sort
keys by invoking setSortKeys
directly and they will
all be honored. However if toggleSortOrder
is subsequently
invoked the maximum number of sort keys will be enforced.
The default value is 3.
Params: - max – the maximum number of sort keys
Throws: - IllegalArgumentException – if
max
< 1
/**
* 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.
Returns: the maximum number of sort keys
/**
* 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 (rowsUpdated
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.
Params: - sortsOnUpdates – whether or not to sort on update events
/**
* 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.
Returns: whether or not to sort when the model is updated
/**
* 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 null
indicates all values from the model should be
included.
RowFilter
's include
method is passed an
Entry
that wraps the underlying model. The number
of columns in the Entry
corresponds to the
number of columns in the ModelWrapper
. The identifier
comes from the ModelWrapper
as well.
This method triggers a sort.
Params: - filter – the filter used to determine what entries should be
included
/**
* 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.
Returns: the filter
/**
* 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.
Params: - column – index of the column to make the primary sorted column,
in terms of the underlying model
Throws: - IndexOutOfBoundsException – {@inheritDoc}
See Also:
/**
* 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}
/**
* {@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}
/**
* {@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.
/**
* 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 sortKeys
list
indicates that the view should unsorted, the same as the model.
See Also: - setRowFilter
- setSortKeys
/**
* 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.
/**
* 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.
/**
* 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.
/**
* 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.
/**
* 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.
/**
* 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
ModelWrapper.getStringValueAt
will be used, otherwise
ModelWrapper.getValueAt
will be used. It is up to
subclasses, such as TableRowSorter
, to honor this value
in their ModelWrapper
implementation.
Params: - column – the index of the column to test, in terms of the
underlying model
Throws: - IndexOutOfBoundsException – if
column
is not valid
/**
* 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 unsetFirst
is true, all indices in modelToView are
first set to -1.
/**
* 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 Comparator
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 sort
.
Params: - column – the index of the column the
Comparator
is
to be used for, in terms of the underlying model - comparator – the
Comparator
to use
Throws: - IndexOutOfBoundsException – if
column
is outside
the range of the underlying model
/**
* 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 Comparator
for the specified
column. This will return null
if a Comparator
has not been specified for the column.
Params: - column – the column to fetch the
Comparator
for, in
terms of the underlying model
Throws: - IndexOutOfBoundsException – if column is outside
the range of the underlying model
Returns: the Comparator
for the specified column
/**
* 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}
/**
* {@inheritDoc}
*/
public int getViewRowCount() {
if (viewToModel != null) {
// When filtering this may differ from getModelWrapper().getRowCount()
return viewToModel.length;
}
return getModelWrapper().getRowCount();
}
{@inheritDoc}
/**
* {@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}
/**
* {@inheritDoc}
*/
public void modelStructureChanged() {
allChanged();
modelRowCount = getModelWrapper().getRowCount();
}
{@inheritDoc}
/**
* {@inheritDoc}
*/
public void allRowsChanged() {
modelRowCount = getModelWrapper().getRowCount();
sort();
}
{@inheritDoc}
Throws: - IndexOutOfBoundsException – {@inheritDoc}
/**
* {@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}
/**
* {@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}
/**
* {@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}
/**
* {@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.
/**
* 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.
/**
* Whether not we are filtering/sorting.
*/
private boolean isTransformed() {
return (viewToModel != null);
}
Insets new set of entries.
Params: - toAdd – the Rows to add, sorted
- current – the array to insert the items into
/**
* 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
TableModelEvent
. If this returns false, assume the
event was dealt with and no further processing needs to happen.
/**
* 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");
}
}
DefaultRowSorter.ModelWrapper
is responsible for providing
the data that gets sorted by DefaultRowSorter
. You
normally do not interact directly with ModelWrapper
.
Subclasses of DefaultRowSorter
provide an
implementation of ModelWrapper
wrapping another model.
For example,
TableRowSorter
provides a ModelWrapper
that
wraps a TableModel
.
ModelWrapper
makes a distinction between values as
Object
s and String
s. This allows
implementations to provide a custom string
converter to be used instead of invoking toString
on the
object.
Type parameters: - <M> – the type of the underlying model
- <I> – the identifier supplied to the filter
See Also: Since: 1.6
/**
* <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 ModelWrapper
.
/**
* Creates a new <code>ModelWrapper</code>.
*/
protected ModelWrapper() {
}
Returns the underlying model that this Model
is
wrapping.
Returns: the underlying model
/**
* 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.
Returns: the number of columns in the model
/**
* 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.
Returns: the number of rows in the model
/**
* 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.
Params: - row – the row index
- column – the column index
Throws: - IndexOutOfBoundsException – if the indices are outside
the range of the model
Returns: the value at the specified index
/**
* 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 String
at the specified
index. This implementation uses toString
on
the result from getValueAt
(making sure
to return an empty string for null values). Subclasses that
override this method should never return null.
Params: - row – the row index
- column – the column index
Throws: - IndexOutOfBoundsException – if the indices are outside
the range of the model
Returns: the value at the specified index as a String
/**
* 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
RowFilter.Entry
that is passed to the
RowFilter
.
Params: - row – the row to return the identifier for, in terms of
the underlying model
See Also: Returns: the identifier
/**
* 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.
/**
* 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
/**
* 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.
/**
* 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);
}
}
}