| /* | |
| * Copyright (c) 1997, 2018, Oracle and/or its affiliates. All rights reserved. | |
| * | |
| * Redistribution and use in source and binary forms, with or without | |
| * modification, are permitted provided that the following conditions | |
| * are met: | |
| * | |
| * - Redistributions of source code must retain the above copyright | |
| * notice, this list of conditions and the following disclaimer. | |
| * | |
| * - Redistributions in binary form must reproduce the above copyright | |
| * notice, this list of conditions and the following disclaimer in the | |
| * documentation and/or other materials provided with the distribution. | |
| * | |
| * - Neither the name of Oracle nor the names of its | |
| * contributors may be used to endorse or promote products derived | |
| * from this software without specific prior written permission. | |
| * | |
| * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS | |
| * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, | |
| * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR | |
| * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR | |
| * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, | |
| * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, | |
| * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR | |
| * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF | |
| * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING | |
| * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS | |
| * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| */ | |
| /* | |
| * This source code is provided to illustrate the usage of a given feature | |
| * or technique and has been deliberately simplified. Additional steps | |
| * required for a production-quality application, such as security checks, | |
| * input validation and proper error handling, might not be present in | |
| * this sample code. | |
| */ | |
| import javax.swing.table.TableModel; | |
| import javax.swing.event.TableModelEvent; | |
| import java.awt.event.MouseAdapter; | |
| import java.awt.event.MouseEvent; | |
| import java.awt.event.InputEvent; | |
| import java.util.ArrayList; | |
| import java.util.Date; | |
| import java.util.List; | |
| import javax.swing.JTable; | |
| import javax.swing.table.JTableHeader; | |
| import javax.swing.table.TableColumnModel; | |
| /** | |
| * 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 | |
| */ | |
| @SuppressWarnings("serial") | |
| public final class TableSorter extends TableMap { | |
| int[] indexes; | |
| List<Integer> sortingColumns = new ArrayList<Integer>(); | |
| boolean ascending = true; | |
| int compares; | |
| public TableSorter() { | |
| indexes = new int[0]; // For consistency. | |
| } | |
| public TableSorter(TableModel model) { | |
| setModel(model); | |
| } | |
| @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 = sortingColumns.get(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) { | |
| System.out.println("Sorter: tableChanged"); | |
| reallocateIndexes(); | |
| super.tableChanged(e); | |
| } | |
| public void checkModel() { | |
| if (indexes.length != model.getRowCount()) { | |
| System.err.println("Sorter not informed of a change in model."); | |
| } | |
| } | |
| public void sort(Object sender) { | |
| checkModel(); | |
| compares = 0; | |
| // n2sort(); | |
| // qsort(0, indexes.length-1); | |
| shuttlesort(indexes.clone(), indexes, 0, indexes.length); | |
| 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 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) { | |
| System.arraycopy(from, low, to, low, high - low); | |
| 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); | |
| } | |
| @Override | |
| 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.clear(); | |
| sortingColumns.add(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. | |
| 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) { | |
| System.out.println("Sorting ..."); | |
| int shiftPressed = e.getModifiersEx() & InputEvent.SHIFT_DOWN_MASK; | |
| boolean ascending = (shiftPressed == 0); | |
| sorter.sortByColumn(column, ascending); | |
| } | |
| } | |
| }; | |
| JTableHeader th = tableView.getTableHeader(); | |
| th.addMouseListener(listMouseListener); | |
| } | |
| } |