summaryrefslogtreecommitdiffstats
path: root/source/client/swing/TableSorter.java
diff options
Diffstat (limited to 'source/client/swing/TableSorter.java')
-rw-r--r--source/client/swing/TableSorter.java432
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);
+ }
+}
+