package com.explodingpixels.widgets;
import java.awt.Component;
import java.awt.Graphics;
import javax.swing.CellRendererPane;
import javax.swing.JComponent;
import javax.swing.JTable;
import javax.swing.JViewport;
import javax.swing.border.AbstractBorder;
import javax.swing.border.Border;
import javax.swing.table.JTableHeader;
import javax.swing.table.TableCellRenderer;
public class TableHeaderUtils {
private static final CellRendererPane CELL_RENDER_PANE = new CellRendererPane();
public static final int NO_COLUMN = -1;
private static final String PRESSED_COLUMN_KEY = "EPJTableHeader.pressedColumn";
private static final String SELECTED_COLUMN_KEY = "EPJTableHeader.selectedColumn";
private static final String SORT_DIRECTION_KEY = "EPJTableHeader.sortDirection";
private TableHeaderUtils() {
// no constructor - utility class.
}
/**
* Creates a component that paints the table's header background.
*
* @param table the {@link javax.swing.JTable} to create the corner component for.
* @return a {@link javax.swing.JComponent} that paints the given table's table header background.
*/
public static JComponent createCornerComponent(final JTable table) {
return new JComponent() {
@Override
protected void paintComponent(Graphics g) {
paintHeader(g, table, 0, getWidth());
}
};
}
/**
* Installs a custom {@link javax.swing.border.Border} on the given table's {@link javax.swing.table.JTableHeader} that paints any
* blank area to the right of the last column header with the {@code JTableHeader}'s background.
*
* @param table the {@link javax.swing.JTable} from which to get the {@code JTableHeader} to paint the
* empty column header space for.
*/
public static void makeHeaderFillEmptySpace(JTable table) {
table.getTableHeader().setBorder(createTableHeaderEmptyColumnPainter(table));
}
/**
* Paints the given JTable's table default header background at given
* x for the given width.
*
* @param graphics the {@link java.awt.Graphics} to paint into.
* @param table the table that the header belongs to.
* @param x the x coordinate of the table header.
* @param width the width of the table header.
*/
public static void paintHeader(Graphics graphics, JTable table, int x, int width) {
TableCellRenderer renderer = table.getTableHeader().getDefaultRenderer();
Component component = renderer.getTableCellRendererComponent(
table, "", false, false, -1, table.getColumnCount());
component.setBounds(0, 0, width, table.getTableHeader().getHeight());
((JComponent) component).setOpaque(false);
CELL_RENDER_PANE.paintComponent(graphics, component, null, x, 0,
width, table.getTableHeader().getHeight(), true);
}
/**
* Creates a {@link javax.swing.border.Border} that paints any empty space to the right of the last column header
* in the given {@link javax.swing.JTable}'s {@link javax.swing.table.JTableHeader}.
*/
private static Border createTableHeaderEmptyColumnPainter(final JTable table) {
return new AbstractBorder() {
@Override
public void paintBorder(Component c, Graphics g, int x, int y, int width, int height) {
// if this JTableHeader is parented in a JViewport, then paint the table header
// background to the right of the last column if neccessary.
JViewport viewport = (JViewport) table.getParent();
if (viewport != null && table.getWidth() < viewport.getWidth()) {
int startX = table.getWidth();
int emptyColumnWidth = viewport.getWidth() - table.getWidth();
paintHeader(g, table, startX, emptyColumnWidth);
}
}
};
}
// Support for making column headers selected and sorted. /////////////////////////////////////
/**
* Get's the pressed column header for the given {@link javax.swing.table.JTableHeader}.
*
* @param tableHeader the {@code JTableHeader} to determine the pressed column header for.
* @return the column model index of the pressed column header, or {@link #NO_COLUMN} if no
* column's header has been pressed.
* @see #isColumnPressed(javax.swing.table.JTableHeader, int)
*/
public static int getPressedColumn(JTableHeader tableHeader) {
Object pressedColumnValue = tableHeader.getClientProperty(PRESSED_COLUMN_KEY);
return pressedColumnValue != null && pressedColumnValue instanceof Integer
? ((Integer) pressedColumnValue) : NO_COLUMN;
}
/**
* {@code true} if the given column model index is pressed in the given {@link javax.swing.table.JTableHeader}.
*
* @param tableHeader the {@code JTableHeader} to use when determining if the given column is
* pressed.
* @param columnModelIndex the column model index to determine the pressed state of.
* @return {@code true} if the given column is pressed.
* @see #getPressedColumn(javax.swing.table.JTableHeader)
* @see #setPressedColumn(javax.swing.table.JTableHeader, int)
*/
public static boolean isColumnPressed(JTableHeader tableHeader, int columnModelIndex) {
int pressedColumn = getPressedColumn(tableHeader);
return pressedColumn >= 0 && pressedColumn == columnModelIndex;
}
/**
* Sets the given column for the given {@link javax.swing.table.JTableHeader} as pressed. Calling this method
* installs a hint, which can be used by renders to draw the correct column pressed state.
* Renders can use {@link #isColumnPressed(javax.swing.table.JTableHeader, int)} to determine
* what state to draw the header for the given index in.
* header state.
*
* @param tableHeader the {@code JTableHeader} to set the pressed state for.
* @param columnModelIndex the column model index of the pressed column.
*/
public static void setPressedColumn(JTableHeader tableHeader, int columnModelIndex) {
tableHeader.putClientProperty(PRESSED_COLUMN_KEY, columnModelIndex);
}
/**
* Get's the selected column header for the given {@link javax.swing.table.JTableHeader}.
*
* @param tableHeader the {@code JTableHeader} to determine the selected column header for.
* @return the column model index of the selected column header, or {@link #NO_COLUMN} if no
* column's header has been pressed.
* @see #isColumnSelected(javax.swing.table.JTableHeader, int)
*/
public static int getSelectedColumn(JTableHeader tableHeader) {
Object selectedColumnValue = tableHeader.getClientProperty(SELECTED_COLUMN_KEY);
return selectedColumnValue != null && selectedColumnValue instanceof Integer
? ((Integer) selectedColumnValue) : NO_COLUMN;
}
/**
* {@code true} if the given column model index is selected in the given {@link javax.swing.table.JTableHeader}.
* <p/>
* Note that there is no direct way to set wheter a column is selected or not. Selection is a
* by-product of set the sort direction on a column.
*
* @param tableHeader the {@code JTableHeader} to use when determining if the given column is
* selected.
* @param columnModelIndex the column model index to determine the selected state of.
* @return {@code true} if the given column is selected.
* @see #getSelectedColumn(javax.swing.table.JTableHeader)
* @see #setSortDirection(javax.swing.table.JTableHeader, int, com.explodingpixels.widgets.TableUtils.SortDirection)
*/
public static boolean isColumnSelected(JTableHeader tableHeader, int columnModelIndex) {
int selectedColumn = getSelectedColumn(tableHeader);
return selectedColumn >= 0 && selectedColumn == columnModelIndex;
}
/**
* Get's the pressed column header for the given {@link javax.swing.table.JTableHeader}.
*
* @param tableHeader the {@code JTableHeader} containing the column to determine the sort
* direction for.
* @param columnModelIndex the column model index to determine the sort direction for.
* @return the {@link com.explodingpixels.widgets.TableUtils.SortDirection} of the given
* column index.
* @see #setSortDirection(javax.swing.table.JTableHeader, int, com.explodingpixels.widgets.TableUtils.SortDirection)
* @see #toggleSortDirection(javax.swing.table.JTableHeader, int)
*/
public static TableUtils.SortDirection getSortDirection(JTableHeader tableHeader, int columnModelIndex) {
Object sortDirection = tableHeader.getClientProperty(SORT_DIRECTION_KEY);
boolean isColumnSelected = isColumnSelected(tableHeader, columnModelIndex);
return sortDirection != null && sortDirection instanceof String && isColumnSelected
? TableUtils.SortDirection.find((String) sortDirection) : TableUtils.SortDirection.NONE;
}
/**
* Toggles the sort direction of the given column model index in the given {@link javax.swing.table.JTableHeader}.
* This also makes the given column selected. If the given column has no sort order set
* ({@link com.explodingpixels.widgets.TableUtils.SortDirection#NONE}) then the new sort order
* will be {@link com.explodingpixels.widgets.TableUtils.SortDirection#ASCENDING}. Otherwise
* the new sort order will be {@link com.explodingpixels.widgets.TableUtils.SortDirection#DESCENDING}
*
* @param tableHeader the {@code JTableHeader} of the column to toggle the sort order for.
* @param columnModelIndex the column model index to toggle the sort order for.
* @return the new sort order of the column.
*/
public static TableUtils.SortDirection toggleSortDirection(JTableHeader tableHeader, int columnModelIndex) {
// grab the previously selected column as well as the sort direction for the given column, if either exist.
int oldSelectedColumn = getSelectedColumn(tableHeader);
TableUtils.SortDirection oldSortDirection = getSortDirection(tableHeader, columnModelIndex);
// if the old selected column is different from the given column, or the old sort direction is set to none,
// then the new sort direction is "ascending".
// else the new sort direction is "descending.
TableUtils.SortDirection newSortDirection;
if (oldSelectedColumn != columnModelIndex || oldSortDirection == TableUtils.SortDirection.NONE) {
newSortDirection = TableUtils.SortDirection.ASCENDING;
} else {
newSortDirection =
oldSortDirection == TableUtils.SortDirection.ASCENDING
? TableUtils.SortDirection.DESCENDING : TableUtils.SortDirection.ASCENDING;
}
// set the new sort direction on the given column model index.
setSortDirection(tableHeader, columnModelIndex, newSortDirection);
return newSortDirection;
}
private static void setSortDirection(JTableHeader tableHeader, int columnModelIndex,
TableUtils.SortDirection sortDirection) {
tableHeader.putClientProperty(SELECTED_COLUMN_KEY, columnModelIndex);
tableHeader.putClientProperty(SORT_DIRECTION_KEY, sortDirection.getValue());
}
}