net.sf.cuf.ui.table.TableSorter Maven / Gradle / Ivy
package net.sf.cuf.ui.table;
import javax.swing.event.TableModelEvent;
import javax.swing.table.TableModel;
/**
* A sorter for TableModels. The sorter has a model (conforming to TableModel)
* and itself implements TableModel. TableSorter does not store or copy
* the data in the TableModel, instead it maintains an array of
* integers which it keeps the same size as the number of rows in its
* model. When the model changes it notifies the sorter that something
* has changed eg. "rowsAdded" so that its internal array of integers
* can be reallocated. As requests are made of the sorter (like
* getValueAt(row, col) it redirects them to its model via the mapping
* array. That way the TableSorter appears to hold another copy of the table
* with the rows in a different order. The sorting algorthm used is stable
* which means that it does not move around rows when its comparison
* function returns 0 to denote that they are equivalent.
*
* Taken originally from "The Java Tutorial" by Sun Microsystems.
* Original Source was version 1.5 12/17/97 author Philip Milne.
*
* @author Jörg Eichhorn, sd&m AG
*/
public class TableSorter extends TableMap
{
/**
* Mapping from unsorted to sorted indices as follows:
* indexes[unsorted] = sorted
*
* We initialize indexes
always in the construcotr,
* so it'll never be null.
*/
private int[] mIndexes;
/**
* Holds information about the columns to sort.
*/
private TableSortInfo mSortingColumns = new TableSortInfo();
/**
* Create new TableSorter for given table model.
*
* @param pModel table model holding data to be displayed
*/
public TableSorter(final TableModel pModel)
{
setModel(pModel);
}
/**
* Set a new TableModel.
*
* @param pModel the new TableModel
*/
public void setModel(final TableModel pModel)
{
super.setModel(pModel);
// we initialize our sorting to "no sorting"
mIndexes = new int[pModel.getRowCount()];
for (int row = 0; row < mIndexes.length; row++)
{
mIndexes[row]= row;
}
}
/**
* Map view-index of table row to model-index.
*
* @param pRow row-index from view
* @return row-index for model
*/
public int getModelRow(final int pRow)
{
return mIndexes[pRow];
}
/**
* Map model-index of table row to view-index.
*
* @param pRow row-index from model
* @return row-index for view
*/
public int getViewRow(final int pRow)
{
for (int i = 0; i < mIndexes.length; i++)
{
if (mIndexes[i] == pRow) return i;
}
return -1;
}
/**
* Check if given column is a sorted column.
*
* @param pColumn index of the column to check
* @return given column is a sorted column
*/
public boolean isSorted(final int pColumn)
{
return mSortingColumns.isSorted(pColumn);
}
/**
* Callback from TableModel.
* Our superclass is a TableModelListener, so we are too.
* This method overrides the implementation of our superclass.
*
* @param pEvent tableModelEvent from tableModel
*/
public void tableChanged(final TableModelEvent pEvent)
{
// get type of change
int type = pEvent.getType();
// get first and last row that changed
int firstRow = pEvent.getFirstRow();
int lastRow = pEvent.getLastRow();
// get column that changed
int column = pEvent.getColumn();
// reacting to INSERTs and DELETEs
if (type == TableModelEvent.INSERT || type == TableModelEvent.DELETE)
{
// FirstRow and lastRow MUST be the same.
// ReallocateIndexesForInsertDelete only works for one row at a time.
// This is because every single change in the number of rows will affect indices in array indexes
// and it's vastly complicated to handle changes in multiple rows at once.
// This is even more complicated with MULTIPLE_INTERVAL_SELECTION.
if (firstRow != lastRow) throw new IllegalArgumentException("firstRow and lastRow must be equal for INSERT or DELETE");
reallocateIndexesForInsertDelete(type, firstRow);
// no automatic sort after INSERT or DELETE
}
// reacting to UPDATEs
if (type == TableModelEvent.UPDATE)
{
if (isSorted(column))
{
// throw away all sorting info
mSortingColumns.clear();
}
if (column == TableModelEvent.ALL_COLUMNS)
{
// will forget about current sorting
reallocateIndexesForUpdate();
}
sort();
}
// propagate change to superclass.
super.tableChanged(pEvent);
}
/*
----------------------------------------------------------------------
TableModel methods to set and get data.
The mapping only affects the contents of the data rows.
Pass all requests to these rows through the mapping array: "indexes".
----------------------------------------------------------------------
*/
/**
* Returns an attribute value for the cell at aRow and aColumn.
*
* @param pRow the row whose value is to be looked up
* @param pColumn the column whose value is to be looked up
* @return the value Object at the specified cell
*/
public Object getValueAt(final int pRow, final int pColumn)
{
checkModel();
//System.out.println("TableSorter.getValueAt Mapping " + pRow + " to " + indexes[pRow]);
return mModel.getValueAt(mIndexes[pRow], pColumn);
}
/**
* Sets an attribute value for the record in the cell at
* aRow and aColumn. aValue is the new value.
*
* @param pValue the new value
* @param pRow the row whose value is to be changed
* @param pColumn the column whose value is to be changed
*/
public void setValueAt(final Object pValue, final int pRow, final int pColumn)
{
checkModel();
//System.out.println("TableSorter.setValueAt Mapping " + pRow + " to " + indexes[pRow]);
mModel.setValueAt(pValue, mIndexes[pRow], pColumn);
}
/*
----------------------------------------------------------------------
Set up sorting info and start sorting.
----------------------------------------------------------------------
*/
/**
* Sort data according to specified column.
* The sorting will be done ascending.
*
* @param pColumn the modelindex of the column
*/
public void sortByColumn(final int pColumn)
{
sortByColumn(pColumn, true);
}
/**
* Sort data according to specified column.
* Sort will be performed immediately with respect to all columns previously added.
* Adding an already set sorting anew will move its criteria downwards in priority.
*
* To start a fresh sort call resetColumns() first.
*
* @param pColumn the modelindex of the column
* @param pAscending the sorting direction
*/
public void sortByColumn(final int pColumn, final boolean pAscending)
{
// remember sorting criteria
mSortingColumns.sortByColumn(pColumn, pAscending);
// execute sort
sort();
// inform all listeners of the change in this tableModel
fireTableDataChanged();
}
/**
* Drop the sorting criteria for this table model but keep the current row order.
*/
public void dropSorting()
{
mSortingColumns.clear();
fireTableDataChanged();
}
/**
* Reset all previous set sorting info.
* Will also reset result of last sorting.
*/
public void resetColumns()
{
// throw away all sorting info
mSortingColumns.clear();
tableChanged(new TableModelEvent(this));
}
/**
* Get current sorting info.
*
* @return unmodifiable TableSortInfo object
*/
public TableSortInfo getSortInfo()
{
return mSortingColumns.cloneImmutable();
}
/*
----------------------------------------------------------------------
Internal sorting stuff.
----------------------------------------------------------------------
*/
/**
* Check, if we're still consistent with our tablemodel holding data to be displayed.
*/
private void checkModel()
{
if (mIndexes.length != mModel.getRowCount())
{
throw new IllegalStateException("Sorter not informed of a change in model.");
}
}
/**
* Sort data in tablemodel according to columns set up via sortByColumn.
*
* This method is made protected
as a hook for different sorting implementations.
*/
protected void sort()
{
checkModel();
shuttlesort(mIndexes.clone(), mIndexes, 0, mIndexes.length);
}
/**
* Do sorting via a shuttlesort.
*
* This is a home-grown implementation which we have not had time
* to research - it may perform poorly in some circumstances. It
* requires twice the space of an in-place algorithm and makes
* NlogN assigments shuttling the values between the two
* arrays. The number of compares appears to vary between N-1 and
* NlogN depending on the initial order but the main reason for
* using it here is that, unlike qsort, it is stable.
*
* @param pFrom int-array containing original indices
* @param pTo int-array containing sorted indices
* @param pLow lower bound of sorting-interval
* @param pHigh upper bound of sorting-interval
*/
private void shuttlesort(final int[] pFrom, final int[] pTo, final int pLow, final int pHigh)
{
if (pHigh - pLow < 2)
{
return;
}
int middle = (pLow + pHigh) / 2;
shuttlesort(pTo, pFrom, pLow, middle);
shuttlesort(pTo, pFrom, middle, pHigh);
int p = pLow;
int q = middle;
/* This is an optional short-cut; at each recursive call,
check to see if the elements in this subset are already
ordered. If so, no further comparisons are needed; the
sub-array can just be copied. The array must be copied rather
than assigned otherwise sister calls in the recursion might
get out of sinc. When the number of elements is three they
are partitioned so that the first set, [pLow, mid), has one
element and and the second, [mid, pHigh), has two. We skip the
optimisation when the number of elements is three or less as
the first compare in the normal merge will produce the same
sequence of steps. This optimisation seems to be worthwhile
for partially ordered lists but some analysis is needed to
find out how the performance drops to Nlog(N) as the initial
order diminishes - it may drop very quickly. */
if (pHigh - pLow >= 4 && compare(pFrom[middle - 1], pFrom[middle]) <= 0)
{
System.arraycopy(pFrom, pLow, pTo, pLow, pHigh - pLow);
return;
}
// A normal merge.
for (int i = pLow; i < pHigh; i++)
{
if (q >= pHigh || (p < middle && compare(pFrom[p], pFrom[q]) <= 0))
{
pTo[i] = pFrom[p++];
}
else
{
pTo[i] = pFrom[q++];
}
}
}
/**
* Compare two rows of tablemodel holding data with respect to given column.
*
* This method is made protected
as a hook for different sorting implementations.
*
* @param pRow1 first row of compare
* @param pRow2 second row of compare
* @param pColumn column to use for compare
* @return negative value, if row1 < row2. positive value, if row1 > row2. zero, if row1 == row2.
*/
protected int compareRowsByColumn(final int pRow1, final int pRow2, final int pColumn)
{
// Get data from tablemodel holding data.
Object o1 = mModel.getValueAt(pRow1, pColumn);
Object o2 = mModel.getValueAt(pRow2, pColumn);
// Check objects for null.
if (o1 == null || o2 == null)
{
return (o1 != null ? 1 : (o2 != null ? -1 : 0));
}
// Objects are not null
if (o1 instanceof Comparable)
{
return ((Comparable)o1).compareTo(o2);
}
else
{
// try using stringrep if not comparable
String s1 = o1.toString();
String s2 = o2.toString();
return s1.compareTo(s2);
}
}
/**
* Compare two rows of tablemodel holding data.
* Compares rows with respect to information in sortingColumns.
*
* This method is made protected
as a hook for different sorting implementations.
*
* @param pRow1 first row of compare
* @param pRow2 second row of compare
* @return negative value, if row1 < row2. positive value, if row1 > row2. zero, if row1 == row2.
*/
protected int compare(final int pRow1, final int pRow2)
{
// mSortingColumns contains columns to use for compare.
for (int level = 0; level < mSortingColumns.size(); level++)
{
// compare by column in sortingcolumns.
int result = compareRowsByColumn(pRow1, pRow2, mSortingColumns.getColumn(level));
if (result != 0)
{
// if result != 0 rows are not equal and we're done.
// maybe we have to change sign of result because of desired direction.
return mSortingColumns.isAscending(level) ? result : -result;
}
// rows were equal, so try next column in sortingcolumns if any left.
}
// default return is: rows are equal.
return 0;
}
/**
* One row inserted or deleted in Tablemodel holding data to be displayed.
* Array indexes, which holds mapping from sorted to unsorted columns, needs to be reallocted.
*
* @param pType type of change in Tablemodel (Type of TablemodelEvent)
* @param pRow row that changed (modelindex)
*/
private void reallocateIndexesForInsertDelete(final int pType, final int pRow)
{
// direction of change in indexes meaning shrinking or growth of array
int direction = 0;
// would even be possible to set direction = pType, but more correct is:
if (pType == TableModelEvent.DELETE) direction = -1; // one entry less
if (pType == TableModelEvent.INSERT) direction = +1; // one entry more
// Set up a new array of indexes with the right number of elements
// for the new data model.
int[] newindexes = new int[mIndexes.length + direction];
// place model-index of changed row at end of new array
if (newindexes.length > 0)
{
newindexes[newindexes.length - 1] = pRow;
}
// adjustment with respect to old array meaning old array was smaller or larger than new one.
int adjust = 0;
// Run over array indexes and setup new array newindexes.
for (int row = 0; row < mIndexes.length; row++)
{
if (mIndexes[row] == pRow)
{
// we hit row that changed:
// remember how to adjust mapping of indices from old array to new array
adjust = direction;
if (adjust > 0)
{
// new array is larger than old one:
// have to insert new row-index right here
newindexes[row] = pRow;
// original value now one arrayelement further to right.
newindexes[row + adjust] = mIndexes[row] + direction;
}
}
else if (row + adjust >= 0)
{
// map old values to new ones, and map them to right position
newindexes[row + adjust] = mIndexes[row] + (mIndexes[row] > pRow ? direction : 0);
}
}
// use new array, forget about the old one.
mIndexes = newindexes;
}
/**
* Tablemodel holding data to be displayed got updated.
* Array indexes, which holds mapping from sorted to unsorted columns, needs to be reallocated.
* Will forget last sorting and replaces it with identity-mapping.
*/
private void reallocateIndexesForUpdate()
{
// Set up a new array of indexes with the right number of elements
// for the new data model.
int[] newindexes;
if (mModel.getRowCount() == mIndexes.length)
{
// old array indexes can be reused for this model
newindexes = mIndexes;
}
else
{
// we need a new array indexes
newindexes = new int[mModel.getRowCount()];
}
// Create identity-mapping
for (int row = 0; row < newindexes.length; row++)
{
newindexes[row] = row;
}
// use new array, forget about the old one.
mIndexes = newindexes;
}
}