/*
* 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
* {@link javax.swing.table.TableRowSorter 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, {@link
* javax.swing.table.TableRowSorter 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, {@link
* javax.swing.table.TableRowSorter 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
* {@code setModelWrapper}. The {@code 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 {@code
* DefaultRowSorter} without specifying a {@code ModelWrapper}.
*
*
* The maximum number of sort keys is enforced by
*
*
* 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
* 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.
*
* @param RowFilter
* @see javax.swing.table.TableRowSorter
* @see javax.swing.table.DefaultTableModel
* @see java.text.Collator
* @since 1.6
*/
public abstract class DefaultRowSorterDefaultRowSorter
.
*/
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(ModelWrappertoggleSortOrder
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 column
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 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
*/
public void setSortKeys(List extends SortKey> sortKeys) {
ListsetMaxSortKeys(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.
* 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.
*
* @param max the maximum number of sort keys
* @throws IllegalArgumentException if max
< 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 (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.
*
* @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 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.
* sortKeys
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
* 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.
*
* @param column the index of the column to test, in terms of the
* underlying model
* @throws IndexOutOfBoundsException if column
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.
*/
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
.
*
* @param column the index of the column the Comparator
is
* to be used for, in terms of the underlying model
* @param comparator the Comparator
to use
* @throws IndexOutOfBoundsException if column
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.
*
* @param column the column to fetch the Comparator
for, in
* terms of the underlying model
* @return the Comparator
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.EntryTableModelEvent
. 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;
ListDefaultRowSorter.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.
*
* @param ModelWrapper
.
*/
protected ModelWrapper() {
}
/**
* Returns the underlying model that this Model
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 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.
*
* @param row the row index
* @param column the column index
* @return the value at the specified index as a String
* @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
.
*
* @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