/*******************************************************************************
* Breakout Cave Survey Visualizer
*
* Copyright (C) 2014 James Edwards
*
* jedwards8 at fastmail dot fm
*
* This program is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License as published by the Free Software
* Foundation; either version 2 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 General Public License for more
* details.
*
* You should have received a copy of the GNU General Public License along with
* this program; if not, write to the Free Software Foundation, Inc., 51
* Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*******************************************************************************/
package org.andork.swing.table;
import java.text.Collator;
import java.util.Arrays;
import java.util.Comparator;
import java.util.function.Consumer;
import javax.swing.JTable;
import javax.swing.event.TableModelEvent;
import javax.swing.event.TableModelListener;
import javax.swing.table.AbstractTableModel;
import javax.swing.table.DefaultTableModel;
import javax.swing.table.TableModel;
import javax.swing.table.TableStringConverter;
import org.andork.swing.AnnotatingRowSorter;
import org.andork.swing.FromEDT;
import org.andork.swing.OnEDT;
import org.andork.swing.async.Subtask;
/**
* An implementation of <code>RowSorter</code> that provides sorting and
* filtering using a <code>TableModel</code>. The following example shows adding
* sorting to a <code>JTable</code>:
*
* <pre>
* TableModel myModel = createMyTableModel();
* JTable table = new JTable(myModel);
* table.setRowSorter(new AnnotatingTableRowSorter(myModel));
* </pre>
*
* This will do all the wiring such that when the user does the appropriate
* gesture, such as clicking on the column header, the table will visually sort.
* <p>
* <code>JTable</code>'s row-based methods and <code>JTable</code>'s selection
* model refer to the view and not the underlying model. Therefore, it is
* necessary to convert between the two. For example, to get the selection in
* terms of <code>myModel</code> you need to convert the indices:
*
* <pre>
* int[] selection = table.getSelectedRows();
* for (int i = 0; i < selection.length; i++) {
* selection[i] = table.convertRowIndexToModel(selection[i]);
* }
* </pre>
*
* Similarly to select a row in <code>JTable</code> based on a coordinate from
* the underlying model do the inverse:
*
* <pre>
* table.setRowSelectionInterval(table.convertRowIndexToView(row),
* table.convertRowIndexToView(row));
* </pre>
* <p>
* The previous example assumes you have not enabled filtering. If you have
* enabled filtering <code>convertRowIndexToView</code> will return -1 for
* locations that are not visible in the view.
* <p>
* <code>AnnotatingTableRowSorter</code> uses <code>Comparator</code>s for doing
* comparisons. The following defines how a <code>Comparator</code> is chosen
* for a column:
* <ol>
* <li>If a <code>Comparator</code> has been specified for the column by the
* <code>setComparator</code> method, use it.
* <li>If the column class as returned by <code>getColumnClass</code> is
* <code>String</code>, use the <code>Comparator</code> returned by
* <code>Collator.getInstance()</code>.
* <li>If the column class implements <code>Comparable</code>, use a
* <code>Comparator</code> that invokes the <code>compareTo</code> method.
* <li>If a <code>TableStringConverter</code> has been specified, use it to
* convert the values to <code>String</code>s and then use the
* <code>Comparator</code> returned by <code>Collator.getInstance()</code>.
* <li>Otherwise use the <code>Comparator</code> returned by
* <code>Collator.getInstance()</code> on the results from calling
* <code>toString</code> on the objects.
* </ol>
* <p>
* In addition to sorting <code>AnnotatingTableRowSorter</code> provides the
* ability to filter. A filter is specified using the <code>setFilter</code>
* method. The following example will only show rows containing the string
* "foo":
*
* <pre>
* TableModel myModel = createMyTableModel();
* AnnotatingTableRowSorter sorter = new AnnotatingTableRowSorter(myModel);
* sorter.setRowFilter(RowFilter.regexFilter(".*foo.*"));
* JTable table = new JTable(myModel);
* table.setRowSorter(sorter);
* </pre>
* <p>
* If the underlying model structure changes (the
* <code>modelStructureChanged</code> method is invoked) the following are reset
* to their default values: <code>Comparator</code>s by column, current sort
* order, and whether each column is sortable. The default sort order is natural
* (the same as the model), and columns are sortable by default.
* <p>
* <code>AnnotatingTableRowSorter</code> has one formal type parameter: the type
* of the model. Passing in a type that corresponds exactly to your model allows
* you to filter based on your model without casting. Refer to the documentation
* of <code>RowFilter</code> for an example of this.
* <p>
* <b>WARNING:</b> <code>DefaultTableModel</code> returns a column class of
* <code>Object</code>. As such all comparisons will be done using
* <code>toString</code>. This may be unnecessarily expensive. If the column
* only contains one type of value, such as an <code>Integer</code>, you should
* override <code>getColumnClass</code> and return the appropriate
* <code>Class</code>. This will dramatically increase the performance of this
* class.
*
* @param <M>
* the type of the model, which must be an implementation of
* <code>TableModel</code>
* @see javax.swing.JTable
* @see javax.swing.RowFilter
* @see javax.swing.table.DefaultTableModel
* @see java.text.Collator
* @see java.util.Comparator
* @since 1.6
*/
public class AnnotatingTableRowSorter<M extends TableModel> extends AnnotatingRowSorter<M, Integer> {
public static abstract class AbstractTableModelCopier<M extends AbstractTableModel> extends ModelCopier<M> {
protected static Class<?>[] getColumnClasses(TableModel model) {
Class<?>[] result = new Class[model.getColumnCount()];
for (int i = 0; i < model.getColumnCount(); i++) {
result[i] = model.getColumnClass(i);
}
return result;
}
protected static Object[] getColumnIdentifiers(TableModel model) {
Object[] result = new Object[model.getColumnCount()];
for (int i = 0; i < model.getColumnCount(); i++) {
result[i] = model.getColumnName(i);
}
return result;
}
public void copyInBackground(final M src, final M dest, final int step, Subtask subtask) {
class ChangeHandler implements TableModelListener {
boolean changed = false;
@Override
public void tableChanged(TableModelEvent e) {
changed = true;
}
}
final ChangeHandler changeHandler = new ChangeHandler();
new OnEDT() {
@Override
public void run() throws Throwable {
src.addTableModelListener(changeHandler);
}
};
try {
int row = 0;
while (row < src.getRowCount()) {
final int finalRow = row;
int[] progress = new FromEDT<int[]>() {
@Override
public int[] run() throws Throwable {
if (changeHandler.changed) {
changeHandler.changed = false;
return new int[] { 0, src.getRowCount() };
}
int nextRow;
for (nextRow = finalRow; nextRow < Math.min(src.getRowCount(),
finalRow + step); nextRow++) {
copyRow(src, nextRow, dest);
}
return new int[] { nextRow, src.getRowCount() };
}
}.result();
row = progress[0];
if (subtask != null) {
if (subtask.isCanceling()) {
return;
}
subtask.setTotal(progress[1]);
subtask.setCompleted(progress[0]);
}
}
} finally {
new OnEDT() {
@Override
public void run() throws Throwable {
src.removeTableModelListener(changeHandler);
}
};
}
}
@Override
public void copyRow(M src, int row, M dest) {
for (int column = 0; column < Math.min(src.getColumnCount(), dest.getColumnCount()); column++) {
dest.setValueAt(src.getValueAt(row, column), row, column);
}
}
}
private static class ComparableComparator implements Comparator {
@Override
@SuppressWarnings("unchecked")
public int compare(Object o1, Object o2) {
return ((Comparable) o1).compareTo(o2);
}
}
public static class DefaultTableModelCopier extends AbstractTableModelCopier<DefaultTableModel> {
@Override
public DefaultTableModel createEmptyCopy(DefaultTableModel model) {
return new DefaultTableModelCopy(getColumnClasses(model), getColumnIdentifiers(model), model.getRowCount());
}
}
@SuppressWarnings("serial")
private static class DefaultTableModelCopy extends DefaultTableModel {
/**
*
*/
private static final long serialVersionUID = -3193148495164558957L;
Class<?>[] columnClasses;
public DefaultTableModelCopy(Class<?>[] columnClasses, Object[] columnIdentifiers, int rowCount) {
super(columnIdentifiers, rowCount);
this.columnClasses = Arrays.copyOf(columnClasses, columnClasses.length);
}
@Override
public Class<?> getColumnClass(int columnIndex) {
return columnClasses[columnIndex];
}
}
/**
* Implementation of AnnotatingRowSorter.ModelWrapper that delegates to a
* TableModel.
*/
private class TableRowSorterModelWrapper extends ModelWrapper<M, Integer> {
M tableModel;
public TableRowSorterModelWrapper(M tableModel) {
super();
this.tableModel = tableModel;
}
@Override
public int getColumnCount() {
return tableModel == null ? 0 : tableModel.getColumnCount();
}
@Override
public Integer getIdentifier(int index) {
return index;
}
@Override
public M getModel() {
return tableModel;
}
@Override
public int getRowCount() {
return tableModel == null ? 0 : tableModel.getRowCount();
}
@Override
public String getStringValueAt(int row, int column) {
TableStringConverter converter = getStringConverter();
if (converter != null) {
// Use the converter
String value = converter.toString(
tableModel, row, column);
if (value != null) {
return value;
}
return "";
}
// No converter, use getValueAt followed by toString
Object o = getValueAt(row, column);
if (o == null) {
return "";
}
String string = o.toString();
if (string == null) {
return "";
}
return string;
}
@Override
public Object getValueAt(int row, int column) {
return tableModel.getValueAt(row, column);
}
}
/**
* Comparator that uses compareTo on the contents.
*/
private static final Comparator COMPARABLE_COMPARATOR = new ComparableComparator();
/**
* For toString conversions.
*/
private TableStringConverter stringConverter;
/**
* Creates a <code>AnnotatingTableRowSorter</code> with an empty model.
*/
public AnnotatingTableRowSorter(JTable table, Consumer<Runnable> sortRunner) {
this(table, (M) table.getModel(), sortRunner);
}
/**
* Creates a <code>AnnotatingTableRowSorter</code> using <code>model</code>
* as the underlying <code>TableModel</code>.
*
* @param model
* the underlying <code>TableModel</code> to use,
* <code>null</code> is treated as an empty model
*/
public AnnotatingTableRowSorter(JTable table, M model, Consumer<Runnable> sortRunner) {
super(table, sortRunner);
setModel(model);
}
@Override
protected org.andork.swing.AnnotatingRowSorter.ModelWrapper<M, Integer> createModelWrapper(M model) {
return new TableRowSorterModelWrapper(model);
}
/**
* Returns the <code>Comparator</code> for the specified column. If a
* <code>Comparator</code> has not been specified using the
* <code>setComparator</code> method a <code>Comparator</code> will be
* returned based on the column class (
* <code>TableModel.getColumnClass</code>) of the specified column. If the
* column class is <code>String</code>, <code>Collator.getInstance</code> is
* returned. If the column class implements <code>Comparable</code> a
* private <code>Comparator</code> is returned that invokes the
* <code>compareTo</code> method. Otherwise
* <code>Collator.getInstance</code> is returned.
*
* @throws IndexOutOfBoundsException
* {@inheritDoc}
*/
@Override
public Comparator<?> getComparator(int column) {
Comparator comparator = super.getComparator(column);
if (comparator != null) {
return comparator;
}
Class columnClass = getModel().getColumnClass(column);
if (columnClass == String.class) {
return Collator.getInstance();
}
if (Comparable.class.isAssignableFrom(columnClass)) {
return COMPARABLE_COMPARATOR;
}
return Collator.getInstance();
}
/**
* Returns the object responsible for converting values from the model to
* strings.
*
* @return object responsible for converting values to strings.
*/
public TableStringConverter getStringConverter() {
return stringConverter;
}
/**
* Sets the object responsible for converting values from the model to
* strings. If non-<code>null</code> this is used to convert any object
* values, that do not have a registered <code>Comparator</code>, to
* strings.
*
* @param stringConverter
* the object responsible for converting values from the model to
* strings
*/
public void setStringConverter(TableStringConverter stringConverter) {
this.stringConverter = stringConverter;
}
/**
* {@inheritDoc}
*
* @throws IndexOutOfBoundsException
* {@inheritDoc}
*/
@Override
protected boolean useToString(int column) {
Comparator comparator = super.getComparator(column);
if (comparator != null) {
return false;
}
Class columnClass = getModel().getColumnClass(column);
if (columnClass == String.class) {
return false;
}
if (Comparable.class.isAssignableFrom(columnClass)) {
return false;
}
return true;
}
}