diff options
Diffstat (limited to 'source/client/swing/TableSorter.java')
| -rw-r--r-- | source/client/swing/TableSorter.java | 432 |
1 files changed, 432 insertions, 0 deletions
diff --git a/source/client/swing/TableSorter.java b/source/client/swing/TableSorter.java new file mode 100644 index 0000000..f8b2a40 --- /dev/null +++ b/source/client/swing/TableSorter.java @@ -0,0 +1,432 @@ +package itunes.client.swing; + +/** + * 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. + * + * @version 1.5 12/17/97 + * @author Philip Milne + */ + +import java.util.*; + +import javax.swing.table.TableModel; +import javax.swing.event.TableModelEvent; + +// Imports for picking up mouse events from the JTable. + +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.awt.event.InputEvent; +import javax.swing.JTable; +import javax.swing.JTextField; +import javax.swing.table.JTableHeader; +import javax.swing.table.TableColumnModel; + +public class TableSorter extends TableMap { + int indexes[]; + + protected int numElements; + + int fullIndex[]; + Vector sortingColumns = new Vector(); + boolean ascending = true; + int compares; + protected int currColumn; + protected boolean currAscending; + protected String filter; + + public TableSorter() { + + indexes = new int[0]; // for consistency + currColumn = -1; + currAscending = true; + checkModel(); + } + + public TableSorter(TableModel model) { + setModel(model); + currColumn = -1; + currAscending = true; + } + + public void setModel(TableModel model) { + super.setModel(model); + filter = ""; + reallocateIndexes(); + loadFullIndex(); + } + public void reFilter(JTextField field) { + filter = field.getText().toLowerCase(); + decideFiltering(); + super.tableChanged(new TableModelEvent(this)); + } + protected void decideFiltering() + { + if (filter.equals("")) + loadFullIndex(); + else + findFilter(); + } + protected boolean presentInRow(String[] tokens, int row) + { + String artist = getArtistAt(row).toLowerCase(); + String album = getAlbumAt(row).toLowerCase(); + String song = getSongAt(row).toLowerCase(); + for (int i = 0; i < tokens.length; i++){ + if (artist.indexOf(tokens[i])==-1 && album.indexOf(tokens[i])==-1 && song.indexOf(tokens[i])==-1){ + return false; + } + } + return true; + } + public int getFullIndexRow(int filteredRow) + { + if(filteredRow < 0 || filteredRow >= numElements) + { + return -1; + } + return indexes[filteredRow]; + } + public int getFilteredIndexRow(int fullRow) + { + for(int i = 0; i < numElements; i++) + { + if(indexes[i] == fullRow) + { + return i; + } + } + return -1; + } + public String getArtistAt(int row) { + checkModel(); + return ((SongTableModel)model).getArtistAt(indexes[row]); + } + + public String getAlbumAt(int row) { + checkModel(); + return ((SongTableModel)model).getAlbumAt(indexes[row]); + } + + public String getSongAt(int row) { + checkModel(); + return ((SongTableModel)model).getSongNameAt(indexes[row]); + } + protected void findFilter() + { + indexes = fullIndex; + int temp[] = new int[indexes.length]; + int j = 0; + + String[] tokens = filter.split(" +"); + for (int i = 0; i < indexes.length; i++){ + if (presentInRow(tokens, i)){ + temp[j] = indexes[i]; + ++j; + } + } + indexes = temp; + numElements = j; + } + public void loadFullIndex() + { + indexes = fullIndex; + numElements = indexes.length; + } + 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 (column == 4) { //rating + if (s1.length() > s2.length()) { + return 1; + } else if (s1.length() < s2.length()) { + return -1; + } else { + return 0; + } + } + + if (s1.matches(" *") && !s2.matches(" *")) { + return 1; + } else if (s2.matches(" *") && !s1.matches(" *")) { + return -1; + } + 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 int getRowCount() + { + return numElements; + } + public void reallocateIndexes() { + + numElements = model.getRowCount(); + + // Set up a new array of indexes with the right number of elements + // for the new data model. + fullIndex = new int[numElements]; + + // Initialise with the identity mapping. + for (int row = 0; row < numElements; row++) { + fullIndex[row] = row; + } + + } + + public void tableChanged(TableModelEvent e) { + //System.out.println("Sorter: tableChanged"); + reallocateIndexes(); + decideFiltering(); + super.tableChanged(e); + } + + public void checkModel() { + if (indexes.length != model.getRowCount()) { + System.out.println("Sorter not informed of a change in model."); + } + } + + public void sort(Object sender) { + checkModel(); + + compares = 0; + // n2sort(); + // qsort(0, indexes.length-1); + shuttlesort((int[])indexes.clone(), indexes, 0, numElements); + //System.out.println("Compares: "+compares); + } + + 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". + + public Object getValueAt(int aRow, int aColumn) { + checkModel(); + return model.getValueAt(indexes[aRow], aColumn); + } + + public void setValueAt(Object aValue, int aRow, int aColumn) { + checkModel(); + 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, 0, numElements - 1, TableModelEvent.ALL_COLUMNS, + TableModelEvent.UPDATE)); + } + + // 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. + public void addMouseListenerToHeaderInTable(JTable table) { + final TableSorter sorter = this; + final JTable tableView = table; + tableView.setColumnSelectionAllowed(false); + MouseAdapter listMouseListener = new MouseAdapter() { + 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) { + //System.out.println("Sorting ..."); + // int shiftPressed = e.getModifiers()&InputEvent.SHIFT_MASK; + //boolean ascending = (shiftPressed == 0); + if (column == sorter.currColumn) { + sorter.currAscending = !sorter.currAscending; + } else { + sorter.currAscending = true; + } + sorter.currColumn = column; + sorter.sortByColumn(column, sorter.currAscending); + } + } + }; + JTableHeader th = tableView.getTableHeader(); + th.addMouseListener(listMouseListener); + } +} + |
