/* * 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 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 SortKeys, in order. * If two objects are equal (the Comparator for the * column returns 0) the next SortKey is used. If no * SortKeys 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 occurring 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 Comparators 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: Comparators by * column, current sort order, and whether each column is sortable. To * find the default Comparators, 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: Comparators 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}. *

* 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 the type of the model * @param the type of the identifier passed to the RowFilter * @see javax.swing.table.TableRowSorter * @see javax.swing.table.DefaultTableModel * @see java.text.Collator * @since 1.6 */ public abstract class DefaultRowSorter extends RowSorter { /** * 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 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 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 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 DefaultRowSorter. */ 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 modelWrapper) { if (modelWrapper == null) { throw new IllegalArgumentException( "modelWrapper most be non-null"); } ModelWrapper 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 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 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. * * @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 sortKeys) { List 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(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 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. * * @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. *

* This method triggers a sort. * * @param filter the filter used to determine what entries should be * included */ public void setRowFilter(RowFilter filter) { this.filter = filter; sort(); } /** * Returns the filter that determines which rows, if any, should * be hidden from view. * * @return the filter */ public RowFilter 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 keys = new ArrayList(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 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 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 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.Entry 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 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 { Comparator c = (Comparator)sortComparators[counter]; result = c.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 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. */ 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 added = new ArrayList(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 updated = new ArrayList(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 * Objects and Strings. This allows * implementations to provide a custom string * converter to be used instead of invoking toString on the * object. * * @param the type of the underlying model * @param the identifier supplied to the filter * @since 1.6 * @see RowFilter * @see RowFilter.Entry */ protected abstract static class ModelWrapper { /** * Creates a new 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 { /** * 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 { 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); } } }