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(); } } }