package org.aitools.programd.interfaces.graphical; 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; import org.aitools.util.runtime.DeveloperError; /** * 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. * * @author Philip Milne * @author <a href="mailto:noel@aitools.org">Noel Bush</a> */ public class TableSorter extends TableMap { /** * */ private static final long serialVersionUID = 1L; int indexes[]; Vector<Integer> sortingColumns = new Vector<Integer>(); boolean ascending = true; int compares; /** * Creates a new TableSorter. */ public TableSorter() { this.indexes = new int[0]; } /** * @param modelToSet */ public TableSorter(TableModel modelToSet) { this.setModel(modelToSet); } /** * Adds a mouse listener to the header of the given table to trigger a table sort when a column heading is clicked in * the JTable. * * @param table the table to whose header to add a mouse listener. */ public void addMouseListenerToHeaderInTable(JTable table) { final TableSorter sorter = this; final JTable tableView = table; tableView.setColumnSelectionAllowed(false); MouseAdapter 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) { int shiftPressed = e.getModifiers() & InputEvent.SHIFT_MASK; sorter.sortByColumn(column, shiftPressed == 0); } } }; JTableHeader th = tableView.getTableHeader(); th.addMouseListener(listMouseListener); } /** * Checks that the model is valid (has not been changed without informing the sorter). */ public void checkModel() { if (this.indexes.length != this.model.getRowCount()) { throw new DeveloperError("TableSorter model is not valid.", new IllegalStateException( "Sorter not informed of a change in model.")); } } /** * Compares two rows. * * @param row1 one row * @param row2 another row * @return the result of comparing the two rows */ public int compare(int row1, int row2) { this.compares++; for (int level = 0; level < this.sortingColumns.size(); level++) { Integer column = this.sortingColumns.elementAt(level); int result = this.compareRowsByColumn(row1, row2, column.intValue()); if (result != 0) { return this.ascending ? result : -result; } } return 0; } /** * Compares the given rows in the given column * * @param row1 one row * @param row2 the other row * @param column the column in which to compare * @return an indicator of the comparison of the two columns (?) */ public int compareRowsByColumn(int row1, int row2, int column) { Class<?> type = this.model.getColumnClass(column); TableModel data = this.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; } // Define null less than everything. else if (o1 == null) { 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; } // Define false < true. else if (b1) { 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; } } } /** * @see org.aitools.programd.interfaces.graphical.TableMap#getValueAt(int, int) */ @Override public Object getValueAt(int aRow, int aColumn) { this.checkModel(); return this.model.getValueAt(this.indexes[aRow], aColumn); } /** * Sorts the table according to an n2 algorithm. */ public void n2sort() { for (int i = 0; i < this.getRowCount(); i++) { for (int j = i + 1; j < this.getRowCount(); j++) { if (this.compare(this.indexes[i], this.indexes[j]) == -1) { this.swap(i, j); } } } } /** * Sets up a new array of indices with the correct number of elements for the new data model, and initializes with the * identity mapping. */ public void reallocateIndexes() { int rowCount = this.model.getRowCount(); /* * Set up a new array of indexes with the right number of elements for the new data model. */ this.indexes = new int[rowCount]; // Initialise with the identity mapping. for (int row = 0; row < rowCount; row++) { this.indexes[row] = row; } } /** * @see org.aitools.programd.interfaces.graphical.TableMap#setModel(javax.swing.table.TableModel) */ @Override public synchronized void setModel(TableModel modelToSet) { super.setModel(modelToSet); this.reallocateIndexes(); } /** * @see org.aitools.programd.interfaces.graphical.TableMap#setValueAt(java.lang.Object, int, int) */ @Override public void setValueAt(Object aValue, int aRow, int aColumn) { this.checkModel(); this.model.setValueAt(aValue, this.indexes[aRow], aColumn); } /** * 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 from ? * @param to ? * @param low ? * @param high ? */ public void shuttlesort(int from[], int to[], int low, int high) { if (high - low < 2) { return; } int middle = (low + high) / 2; this.shuttlesort(to, from, low, middle); this.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 && this.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 && this.compare(from[p], from[q]) <= 0) { to[i] = from[p++]; } else { to[i] = from[q++]; } } } // The mapping only affects the contents of the data rows. // Pass all requests to these rows through the mapping array: "indexes". /** * */ public void sort() { this.checkModel(); this.compares = 0; this.shuttlesort(this.indexes.clone(), this.indexes, 0, this.indexes.length); } /** * Sorts the table by the specified column. * * @param column the column by which to sort */ public void sortByColumn(int column) { this.sortByColumn(column, true); } /** * Sorts by the specified column, either ascending or descending. * * @param column the column by which to sort * @param ascendingSetting whether to sort ascending */ public void sortByColumn(int column, boolean ascendingSetting) { this.ascending = ascendingSetting; this.sortingColumns.removeAllElements(); this.sortingColumns.addElement(new Integer(column)); this.sort(); super.tableChanged(new TableModelEvent(this)); } /** * Swaps the cells indicated by <code>i</code> and <code>j</code>. * * @param i a cell * @param j a cell */ public void swap(int i, int j) { int tmp = this.indexes[i]; this.indexes[i] = this.indexes[j]; this.indexes[j] = tmp; } /** * @see org.aitools.programd.interfaces.graphical.TableMap#tableChanged(javax.swing.event.TableModelEvent) */ @Override public void tableChanged(TableModelEvent e) { this.reallocateIndexes(); super.tableChanged(e); } }