package com.explodingpixels.widgets;
import java.awt.Color;
import java.awt.Cursor;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import javax.swing.BorderFactory;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.JViewport;
import javax.swing.table.TableColumnModel;
/**
* A collection of utility methods to be used with {@link javax.swing.JTable}.
*/
public class TableUtils {
private TableUtils() {
// no constructor - utility class.
}
/**
* Add's striping to the background of the given {@link javax.swing.JTable}. The actual striping is
* installed on the containing {@link javax.swing.JScrollPane}'s {@link javax.swing.JViewport}, so if this table is not
* added to a {@code JScrollPane}, then no stripes will be painted. This method can be called
* before the given table is added to a scroll pane, though, as a {@link java.beans.PropertyChangeListener}
* will be installed to handle "ancestor" changes.
*
* @param table the table to paint row stripes for.
* @param stipeColor the color of the stripes to paint.
*/
public static void makeStriped(JTable table, Color stipeColor) {
table.addPropertyChangeListener("ancestor",
createAncestorPropertyChangeListener(table, stipeColor));
// install a listener to cause the whole table to repaint when a column is resized. we do
// this because the extended grid lines may need to be repainted. this could be cleaned up,
// but for now, it works fine.
for (int i = 0; i < table.getColumnModel().getColumnCount(); i++) {
table.getColumnModel().getColumn(i).addPropertyChangeListener(
createAncestorPropertyChangeListener(table, stipeColor)
);
}
}
private static PropertyChangeListener createAncestorPropertyChangeListener(
final JTable table, final Color stipeColor) {
return new PropertyChangeListener() {
public void propertyChange(PropertyChangeEvent event) {
// indicate that the parent of the JTable has changed.
parentDidChange(table, stipeColor);
}
};
}
private static void parentDidChange(JTable table, Color stipeColor) {
// if the parent of the table is an instance of JViewport, and that JViewport's parent is
// a JScrollpane, then install the custom BugFixedViewportLayout.
if (table.getParent() instanceof JViewport
&& table.getParent().getParent() instanceof JScrollPane) {
JScrollPane scrollPane = (JScrollPane) table.getParent().getParent();
scrollPane.setViewportBorder(
new StripedViewportBorder(scrollPane.getViewport(), table, stipeColor));
scrollPane.getViewport().setOpaque(false);
scrollPane.setCorner(JScrollPane.UPPER_RIGHT_CORNER,
TableHeaderUtils.createCornerComponent(table));
scrollPane.setBorder(BorderFactory.createEmptyBorder());
}
}
// Sort support. //////////////////////////////////////////////////////////////////////////////
/**
* An enumeration representing the sort order of a table column.
*/
public enum SortDirection {
NONE(""), ASCENDING("ascending"), DESCENDING("descending");
private final String fValue;
SortDirection(String value) {
fValue = value;
}
String getValue() {
return fValue;
}
static SortDirection find(String value) {
for (SortDirection sortDirection : values()) {
if (sortDirection.getValue().equals(value)) {
return sortDirection;
}
}
throw new IllegalArgumentException("No sort direction found for " + value);
}
}
/**
* An interface that will be notified when sorting of a {@link javax.swing.JTable} should occur.
*
* @see TableUtils#makeSortable(javax.swing.JTable, com.explodingpixels.widgets.TableUtils.SortDelegate)
*/
public interface SortDelegate {
/**
* Called when a table should sort its' rows based on the given column.
*
* @param columnModelIndex the column model index to base the sorting on.
* @param sortDirection the direction to sort the table's rows in.
*/
void sort(int columnModelIndex, SortDirection sortDirection);
}
/**
* Installs a listener on the given {@link javax.swing.JTable}'s {@link javax.swing.table.JTableHeader},
* which will notify the given {@link com.explodingpixels.widgets.TableUtils.SortDelegate} when the user clicks the header
* and thus wishes to sort. The listener will also call
* {@link TableHeaderUtils#toggleSortDirection(javax.swing.table.JTableHeader, int)}
* and {@link com.explodingpixels.widgets.TableHeaderUtils#setPressedColumn(javax.swing.table.JTableHeader, int)}
* which will install hints for header renders to render the column headers in the
* appropriate state.
*
* @param table the table so install the {@code SortDelegate} on.
* @param sortDelegate the delegate to notify when sorting should be performed.
*/
public static void makeSortable(JTable table, SortDelegate sortDelegate) {
validateSortDelegate(sortDelegate);
MouseListener mouseListener = new ColumnHeaderMouseListener(table, sortDelegate);
table.getTableHeader().addMouseListener(mouseListener);
}
private static void validateSortDelegate(SortDelegate sortDelegate) {
if (sortDelegate == null) {
throw new IllegalArgumentException("The given SortDelegate cannot be null.");
}
}
private static class ColumnHeaderMouseListener extends MouseAdapter {
private final JTable fTable;
private final SortDelegate fSortDelegate;
private boolean fMouseEventIsPerformingPopupTrigger = false;
private ColumnHeaderMouseListener(JTable fTable, SortDelegate fSortDelegate) {
this.fTable = fTable;
this.fSortDelegate = fSortDelegate;
}
public void mouseClicked(MouseEvent mouseEvent) {
if (shouldProcessMouseClicked()) {
final TableColumnModel columnModel = fTable.getColumnModel();
int columnViewIndex = columnModel.getColumnIndexAtX(mouseEvent.getX());
int columnModelIndex = fTable.convertColumnIndexToModel(columnViewIndex);
SortDirection sortDirection =
TableHeaderUtils.toggleSortDirection(fTable.getTableHeader(), columnModelIndex);
fSortDelegate.sort(columnModelIndex, sortDirection);
fTable.getTableHeader().repaint();
}
}
private boolean shouldProcessMouseClicked() {
return !fMouseEventIsPerformingPopupTrigger && isNotResizeCursor();
}
private boolean isNotResizeCursor() {
return fTable.getTableHeader().getCursor() != Cursor.getPredefinedCursor(Cursor.E_RESIZE_CURSOR);
}
public void mousePressed(MouseEvent mouseEvent) {
fMouseEventIsPerformingPopupTrigger = mouseEvent.isPopupTrigger();
if (isNotResizeCursor()) {
final TableColumnModel columnModel = fTable.getColumnModel();
int viewColumnIndex = columnModel.getColumnIndexAtX(mouseEvent.getX());
int columnModelIndex = fTable.convertColumnIndexToModel(viewColumnIndex);
TableHeaderUtils.setPressedColumn(fTable.getTableHeader(), columnModelIndex);
fTable.getTableHeader().repaint();
}
}
@Override
public void mouseReleased(MouseEvent e) {
TableHeaderUtils.setPressedColumn(fTable.getTableHeader(), TableHeaderUtils.NO_COLUMN);
fTable.getTableHeader().repaint();
}
}
}