/*
This file belongs to the Servoy development and deployment environment, Copyright (C) 1997-2010 Servoy BV
This program is free software; you can redistribute it and/or modify it under
the terms of the GNU Affero General Public License as published by the Free
Software Foundation; either version 3 of the License, or (at your option) any
later version.
This program 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 Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License along
with this program; if not, see http://www.gnu.org/licenses or write to the Free
Software Foundation,Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
*/
package com.servoy.j2db.util;
/**
* 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.
*
*/
import java.awt.event.InputEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.Date;
import java.util.Vector;
import javax.swing.JTable;
import javax.swing.event.TableModelEvent;
import javax.swing.table.JTableHeader;
import javax.swing.table.TableColumnModel;
import javax.swing.table.TableModel;
public class TableSorter extends TableMap
{
int indexes[];
Vector sortingColumns = new Vector();
boolean ascending = true;
int compares;
boolean reallocateIndexesOnUpdate = true;
// use of a last sorted column, in order to invert the sort order
private int lastSortedColumn = -1;
private boolean lastSortOrder = true;
public TableSorter()
{
indexes = new int[0]; // For consistency.
}
public TableSorter(TableModel model)
{
setModel(model);
}
public void setReallocateIndexesOnUpdate(boolean reallocateIndexesOnUpdate)
{
this.reallocateIndexesOnUpdate = reallocateIndexesOnUpdate;
}
@Override
public void setModel(TableModel model)
{
super.setModel(model);
reallocateIndexes();
}
public int compareRowsByColumn(int row1, int row2, int column)
{
Class type = model.getColumnClass(column);
TableModel data = model;
// Check for nulls
Object o1 = data.getValueAt(row1, column);
Object o2 = data.getValueAt(row2, column);
// If both values are null return 0
if (o1 == null && o2 == null)
{
return 0;
}
else if (o1 == null)
{ // Define null less than everything.
return -1;
}
else if (o2 == null)
{
return 1;
}
/*
* We copy all returned values from the getValue call in case an optimised model is reusing one object to return many values. The Number subclasses in the JDK
* are immutable and so will not be used in this way but other subclasses of Number might want to do this to save space and avoid unnecessary heap allocation.
*/
if (type.getSuperclass() == java.lang.Number.class)
{
Number n1 = (Number)data.getValueAt(row1, column);
double d1 = n1.doubleValue();
Number n2 = (Number)data.getValueAt(row2, column);
double d2 = n2.doubleValue();
if (d1 < d2) return -1;
else if (d1 > d2) return 1;
else return 0;
}
else if (type == java.util.Date.class)
{
Date d1 = (Date)data.getValueAt(row1, column);
long n1 = d1.getTime();
Date d2 = (Date)data.getValueAt(row2, column);
long n2 = d2.getTime();
if (n1 < n2) return -1;
else if (n1 > n2) return 1;
else return 0;
}
else if (type == String.class)
{
String s1 = (String)data.getValueAt(row1, column);
String s2 = (String)data.getValueAt(row2, column);
int result = s1.compareTo(s2);
if (result < 0) return -1;
else if (result > 0) return 1;
else return 0;
}
else if (type == Boolean.class)
{
Boolean bool1 = (Boolean)data.getValueAt(row1, column);
boolean b1 = bool1.booleanValue();
Boolean bool2 = (Boolean)data.getValueAt(row2, column);
boolean b2 = bool2.booleanValue();
if (b1 == b2) return 0;
else if (b1) // Define false < true
return 1;
else return -1;
}
else
{
Object v1 = data.getValueAt(row1, column);
String s1 = v1.toString();
Object v2 = data.getValueAt(row2, column);
String s2 = v2.toString();
int result = s1.compareTo(s2);
if (result < 0) return -1;
else if (result > 0) return 1;
else return 0;
}
}
public int compare(int row1, int row2)
{
compares++;
for (int level = 0; level < sortingColumns.size(); level++)
{
Integer column = (Integer)sortingColumns.elementAt(level);
int result = compareRowsByColumn(row1, row2, column.intValue());
if (result != 0) return ascending ? result : -result;
}
return 0;
}
public void reallocateIndexes()
{
int rowCount = model.getRowCount();
// Set up a new array of indexes with the right number of elements
// for the new data model.
indexes = new int[rowCount];
// Initialise with the identity mapping.
for (int row = 0; row < rowCount; row++)
indexes[row] = row;
}
@Override
public void tableChanged(TableModelEvent e)
{
Debug.trace("Sorter: tableChanged"); //$NON-NLS-1$
if (e.getType() != TableModelEvent.UPDATE || reallocateIndexesOnUpdate)
{
reallocateIndexes();
}
super.tableChanged(e);
}
public void checkModel()
{
if (indexes.length != model.getRowCount())
{
System.err.println("Sorter not informed of a change in model."); //$NON-NLS-1$
}
}
public void sort(Object sender)
{
checkModel();
compares = 0;
// n2sort();
// qsort(0, indexes.length-1);
shuttlesort(indexes.clone(), indexes, 0, indexes.length);
Debug.trace("Compares: " + compares); //$NON-NLS-1$s
}
public void n2sort()
{
for (int i = 0; i < getRowCount(); i++)
{
for (int j = i + 1; j < getRowCount(); j++)
{
if (compare(indexes[i], indexes[j]) == -1)
{
swap(i, j);
}
}
}
}
// 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.
public void shuttlesort(int from[], int to[], int low, int high)
{
if (high - low < 2)
{
return;
}
int middle = (low + high) / 2;
shuttlesort(to, from, low, middle);
shuttlesort(to, from, middle, high);
int p = low;
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, [low, mid), has one element and and the second, [mid, high),
* 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 (high - low >= 4 && compare(from[middle - 1], from[middle]) <= 0)
{
for (int i = low; i < high; i++)
{
to[i] = from[i];
}
return;
}
// A normal merge.
for (int i = low; i < high; i++)
{
if (q >= high || (p < middle && compare(from[p], from[q]) <= 0))
{
to[i] = from[p++];
}
else
{
to[i] = from[q++];
}
}
}
public void swap(int i, int j)
{
int tmp = indexes[i];
indexes[i] = indexes[j];
indexes[j] = tmp;
}
// The mapping only affects the contents of the data rows.
// Pass all requests to these rows through the mapping array: "indexes".
@Override
public Object getValueAt(int aRow, int aColumn)
{
checkModel();
return model.getValueAt(indexes[aRow], aColumn);
}
/**
* @see com.servoy.j2db.util.TableMap#isCellEditable(int, int)
*/
@Override
public boolean isCellEditable(int row, int column)
{
checkModel();
return model.isCellEditable(indexes[row], column);
}
@Override
public void setValueAt(Object aValue, int aRow, int aColumn)
{
checkModel();
if (indexes != null && aRow < indexes.length && indexes.length > 0) model.setValueAt(aValue, indexes[aRow], aColumn);
}
public void sortByColumn(int column)
{
sortByColumn(column, true);
}
public void sortByColumn(int column, boolean ascending)
{
this.ascending = ascending;
sortingColumns.removeAllElements();
sortingColumns.addElement(new Integer(column));
sort(this);
super.tableChanged(new TableModelEvent(this));
}
// There is no-where else to put this.
// Add a mouse listener to the Table to trigger a table sort
// when a column heading is clicked in the JTable.
MouseAdapter listMouseListener;
public void addMouseListenerToHeaderInTable(JTable table)
{
final TableSorter sorter = this;
final JTable tableView = table;
tableView.setColumnSelectionAllowed(false);
listMouseListener = new MouseAdapter()
{
@Override
public void mouseClicked(MouseEvent e)
{
TableColumnModel columnModel = tableView.getColumnModel();
int viewColumn = columnModel.getColumnIndexAtX(e.getX());
int column = tableView.convertColumnIndexToModel(viewColumn);
if (e.getClickCount() == 1 && column != -1)
{
Debug.trace("Sorting ..."); //$NON-NLS-1$
int shiftPressed = e.getModifiers() & InputEvent.SHIFT_MASK;
boolean ascending;
if (lastSortedColumn == column && column != -1)
{
ascending = !lastSortOrder;
}
else
{
ascending = shiftPressed == 0;
}
lastSortedColumn = column;
lastSortOrder = ascending;
sorter.sortByColumn(column, ascending);
}
}
};
JTableHeader th = tableView.getTableHeader();
th.removeMouseListener(listMouseListener); //safety prevent multiple adds
th.addMouseListener(listMouseListener);
}
public void removeMouseListenerToHeaderInTable(JTable table)
{
JTableHeader th = table.getTableHeader();
th.removeMouseListener(listMouseListener);
}
/**
* @param row
* @return
*/
public int getRealRowIndex(int row)
{
return indexes[row];
}
}