Indicate table sort
Sorting table rows is a common task in Swing applications.
The following is an extended example of a reusable scheme for adding a sort icon to table column headers. It uses three classes.
SortOrder is a type-safe enumeration
for the two directions of sorting (ascending and descending) :
package hirondelle.stocks.table; /** * Enumeration class for the two directions which a sort may take. */ public enum SortOrder { DESCENDING("Descending"), ASCENDING("Ascending"); @Override public String toString() { return fName; } /** * Return the opposite <tt>SortOrder</tt> from <tt>this</tt> one. */ public SortOrder toggle(){ return (this == ASCENDING ? DESCENDING : ASCENDING); } // PRIVATE // private final String fName; private SortOrder(String aName){ fName = aName; } }
SortBy is a data-centric class which holds two items :
- a SortOrder
- an index representing a JTable column.
package hirondelle.stocks.table; import hirondelle.stocks.util.HashCodeUtil; import hirondelle.stocks.util.EqualsUtil; /** * Data-centric, immutable value class representing both the direction * of a sort and the index of its column. * * <P><tt>SortBy</tt> does not have any knowledge of table contents, so it may be * used with any sorted table. */ public final class SortBy { /** * Constructor. * * @param aSortOrder satisfies <tt>aSortOrder!=null</tt> and denotes ascending * or descending order. * @param aColumn satisfies <tt>aColumn >= 0</tt> and is the index of a * table column. */ public SortBy (SortOrder aSortOrder, int aColumn) { fSortOrder = aSortOrder; fColumn = aColumn; validateState(); } /** * Return the sense of the sort, either ascending or descending. */ public SortOrder getOrder() { return fSortOrder; } /** * Return the column index identifying the sort, or {@link #NO_SELECTED_COLUMN} * in the case of {@link #NONE}. */ public int getColumn() { return fColumn; } /** * A special <tt>SortBy</tt> which represents the absence of any sort. */ public static final SortBy NONE = new SortBy(); /** * The special value of the column index used by {@link #NONE}. */ public static final int NO_SELECTED_COLUMN = -1; /** * Represent this object as a <tt>String</tt> - intended * for logging purposes only. */ @Override public String toString() { StringBuilder result = new StringBuilder(); String newLine = System.getProperty("line.separator"); result.append( this.getClass().getName() ); result.append(" {"); result.append(newLine); result.append(" fSortOrder = ").append(fSortOrder).append(newLine); result.append(" fColumn = ").append(fColumn).append(newLine); result.append("}"); result.append(newLine); return result.toString(); } @Override public boolean equals( Object aThat ) { if ( this == aThat ) return true; if ( !(aThat instanceof SortBy) ) return false; SortBy that = (SortBy)aThat; return EqualsUtil.areEqual(this.fSortOrder, that.fSortOrder) && EqualsUtil.areEqual(this.fColumn, that.fColumn) ; } @Override public int hashCode() { int result = HashCodeUtil.SEED; result = HashCodeUtil.hash( result, fSortOrder ); result = HashCodeUtil.hash( result, fColumn ); return result; } // PRIVATE // private final SortOrder fSortOrder; private final int fColumn; private void validateState() { boolean hasValidState = (fSortOrder!=null) && (fColumn >= 0); if ( !hasValidState ) throw new IllegalArgumentException(this.toString()); } /** * Constructor used only for the special case of no sort at all. * * <P>Note that this constructor does not perform the validations done by the public * constructor, and is thus not subject to the same restrictions. */ private SortBy(){ fSortOrder = SortOrder.DESCENDING; fColumn = NO_SELECTED_COLUMN; } }
Finally, TableSortIndicator
- listens for clicks on column headers
- manages an icon indicating sort column and sort direction
- toggles direction of this icon when clicked repeatedly
- notifies listeners of new sort selections
package hirondelle.stocks.table; import java.util.*; import javax.swing.*; import javax.swing.table.*; import java.awt.*; import java.awt.event.*; import hirondelle.stocks.util.Consts; import hirondelle.stocks.util.Args; /** * Places an up or down icon in a table column header, as an indicator of the * primary sort. * *<P>This class does not do any sorting of the underlying rows - it merely indicates * the identity and direction of the sorted column. * * <P>The user changes the indicated sort by simply clicking on a column header. * The initial click always indicates a descending sort. * A re-click on the same column will toggle the indicated direction. * * <P>Listeners to this class are notified when the sort has changed, and * use {@link #getSortBy} to retrieve the new sort, and then perform the actual * sorting. Example : <pre> TableSortIndicator fSortIndicator = new TableSortIndicator(table, upIcon, downIcon); fSortIndicator.addObserver(this); //when the user clicks a column header, fSortIndicator will notify //registered observers, who will call getSortBy to fetch the new sort. //.. public void update(Observable aObservable, Object aData) { //extract column and (asc|desc) from fSortIndicator SortBy sortBy = fSortIndicator.getSortBy(); //...perform the actual sorting } </pre> * Instead of using a mouse click, the sort can be set programmatically as well; this * is useful for reflecting a sort selected through a preferences dialog. Example : <pre> TableSortIndicator fSortIndicator = new TableSortIndicator(table, upIcon, downIcon); fSortIndicator.addObserver(this); fSortIndicator.setSortBy( sortByPreference ) ; //setSortBy calls the update method </pre> */ final class TableSortIndicator extends Observable { /** * Constructor. * * @param aTable receives indication of table sort ; if it has any custom * header renderers, they will be overwritten by this class. * @param aUpIcon placed in column header to indicate ascending sort. * @param aDownIcon placed in column header to indicate descending sort. */ TableSortIndicator(JTable aTable, Icon aUpIcon, Icon aDownIcon) { Args.checkForNull(aUpIcon); Args.checkForNull(aDownIcon); fTable = aTable; fUpIcon = aUpIcon; fDownIcon = aDownIcon; fCurrentSort = SortBy.NONE; initHeaderClickListener(); initHeaderRenderers(); assert getRenderer(0) != null : "Ctor - renderer 0 is null."; } /** * Return the identity of column having the primary sort, and the direction * of its sort. */ SortBy getSortBy(){ return fCurrentSort; } /** * Change the sort programmatically, instead of through a user click. * * <P>If there is a user preference for sort, it is passed to this method. * Notifies listeners of the change to the sort. */ void setSortBy( SortBy aTargetSort ){ validateIdx( aTargetSort.getColumn() ); initHeaderRenderers(); assert getRenderer(0) != null : "setSortBy - renderer 0 is null."; fTargetSort = aTargetSort; if ( fCurrentSort == SortBy.NONE ){ setInitialHeader(); } else if ( fCurrentSort.getColumn() == fTargetSort.getColumn() && fCurrentSort.getOrder() != fTargetSort.getOrder() ) { toggleIcon(); } else { updateTwoHeaders(); } synchCurrentSortWithSelectedSort(); notifyAndPaint(); } // PRIVATE // private final JTable fTable; private final Icon fUpIcon; private final Icon fDownIcon; /** * The sort as currently displayed to the user, representing the end result of a * previous user request. */ private SortBy fCurrentSort; /** * A new sort to be processed, whose origin is either a user preference or a * a mouse click. * * Once fTargetSort is processed, fCurrentSort is assigned to fTargetSort. */ private SortBy fTargetSort; private static final SortOrder fDEFAULT_SORT_ORDER = SortOrder.DESCENDING; /** * Return true only if the index is in the range 0..N-1, where N is the * number of columns in fTable. */ private boolean isValidColumnIdx(int aColumnIdx) { return 0 <= aColumnIdx && aColumnIdx <= fTable.getColumnCount()-1 ; } private void validateIdx(int aSelectedIdx) { if ( ! isValidColumnIdx(aSelectedIdx) ) { throw new IllegalArgumentException("Column index is out of range: " + aSelectedIdx); } } /** * Called both upon construction and by {@link #setSortBy}. * * If fireTableStructureChanged is called, then the original headers are lost, and * this method must be called in order to restore them. */ private void initHeaderRenderers(){ /* * Attach a default renderer explicitly to all columns. This is a * workaround for the unusual fact that TableColumn.getHeaderRenderer returns * null in the default case; Sun did this as an optimization for tables with * very large numbers of columns. As well, there is only one default renderer * instance which is reused by each column. * See http://developer.java.sun.com/developer/bugParade/bugs/4276838.html for * further information. */ for (int idx=0; idx < fTable.getColumnCount(); ++idx) { TableColumn column = fTable.getColumnModel().getColumn(idx); column.setHeaderRenderer( new Renderer(fTable.getTableHeader()) ); assert column.getHeaderRenderer() != null : "Header Renderer is null"; } } private Renderer getRenderer(int aColumnIdx) { TableColumn column = fTable.getColumnModel().getColumn(aColumnIdx); return (Renderer)column.getHeaderRenderer(); } private void initHeaderClickListener() { fTable.getTableHeader().addMouseListener( new MouseAdapter() { public void mouseClicked(MouseEvent event) { int selectedIdx = fTable.getColumnModel().getColumnIndexAtX(event.getX()); processClick( selectedIdx ); } }); } /** * Update the display of table headers to reflect a new sort, as indicated by a * mouse click performed by the user on a column header. * * If <tt>aSelectedIdx</tt> is the column which already has the sort indicator, * then toggle the indicator to its opposite state (up -> down, down -> up). * If <tt>aSelectedIdx</tt> does not already display a sort indicator, then * add a down indicator to it, and remove the indicator from the fCurrentSort * column, if present. */ private void processClick(int aSelectedIdx){ validateIdx( aSelectedIdx ); if ( fCurrentSort.getColumn() == aSelectedIdx ) { fTargetSort = new SortBy( fCurrentSort.getOrder().toggle(), aSelectedIdx); } else { fTargetSort = new SortBy(fDEFAULT_SORT_ORDER , aSelectedIdx); } if ( fCurrentSort == SortBy.NONE ){ setInitialHeader(); } if ( fCurrentSort.getColumn() == fTargetSort.getColumn() ) { toggleIcon(); } else { updateTwoHeaders(); } synchCurrentSortWithSelectedSort(); notifyAndPaint(); } private void notifyAndPaint(){ setChanged(); notifyObservers(); fTable.getTableHeader().resizeAndRepaint(); } private void setInitialHeader(){ if ( fTargetSort.getOrder() == SortOrder.DESCENDING ){ getRenderer( fTargetSort.getColumn() ).setIcon(fDownIcon); } else { getRenderer( fTargetSort.getColumn() ).setIcon(fUpIcon); } } /** * Flip the direction of the icon (up->down or down->up). */ private void toggleIcon(){ Renderer renderer = getRenderer(fCurrentSort.getColumn()); if ( fCurrentSort.getOrder() == SortOrder.ASCENDING ) { renderer.setIcon(fDownIcon); } else { renderer.setIcon(fUpIcon); } } /** * Change the fCurrentSort column to having no icon, and change the fTargetSort * column to having a down icon. */ private void updateTwoHeaders() { getRenderer(fCurrentSort.getColumn()).setIcon(null); getRenderer(fTargetSort.getColumn()).setIcon(fDownIcon); } private void synchCurrentSortWithSelectedSort(){ fCurrentSort = fTargetSort; } /** * Renders a column header with an icon. * * This class duplicates the default header behavior, but there does * not seem to be any other option, since such an object does not seem * to be available from JTableHeader. */ private final class Renderer extends DefaultTableCellRenderer { Renderer(JTableHeader aTableHeader){ setHorizontalAlignment(JLabel.CENTER); setForeground(aTableHeader.getForeground()); setBackground(aTableHeader.getBackground()); setBorder(UIManager.getBorder("TableHeader.cellBorder")); fTableHeader = aTableHeader; } public Component getTableCellRendererComponent( JTable aTable, Object aValue, boolean aIsSelected, boolean aHasFocus, int aRowIdx, int aColumnIdx ) { setText((aValue == null) ? Consts.EMPTY_STRING : aValue.toString()); setFont(fTableHeader.getFont()); return this; } private JTableHeader fTableHeader; } }
See Also :
Would you use this technique?
|
|