Sexy Swing App – iTunes Table Header
May 11, 2008
iTunes has a unique, clean, clearly identifiable table header. Its built from few simple gradients and border colors.
It’s easy to replicate the iTunes table header in Swing by creating a TableCellRenderer that extends EmphasizedLabel (demonstrated in the Part 1 of this blog series). This renderer can then be installed as the default renderer for the table’s JTableHeader.
In order to get the selected column state, driven by user mouse events, we add a mouse listener to the table’s header component in the renderer’s constructor. This lets us render all five different states:
- Unselected, focused
- Unselected, pressed, focused
- Selected, focused
- Selected, pressed, focused
- Unfocused
Here’s the code for the custom table header renderer:
public class ITunesTableHeaderRenderer extends EmphasizedLabel
implements TableCellRenderer{
private JTable fTable;
private int fSelectedColumn = -1;
private int fPressedColumn = -1;
private Color fTopColor = new Color(0xdbdbdb);
private Color fBottomColor = new Color(0xbbbbbb);
public static final Color ITUNES_TABLE_HEADER_LEFT_UNSELECTED_BORDER_COLOR =
new Color(0xd9d9d9);
public static final Color ITUNES_TABLE_HEADER_LEFT_PRESSED_UNSELECTED_BORDER_COLOR =
new Color(0xc0c0c0);
public static final Color ITUNES_TABLE_HEADER_LEFT_SELECTED_BORDER_COLOR =
new Color(0xabbbce);
public static final Color ITUNES_TABLE_HEADER_RIGHT_UNSELECTED_BORDER_COLOR =
new Color(0x9c9c9c);
public static final Color ITUNES_TABLE_HEADER_RIGHT_SELECTED_BORDER_COLOR =
new Color(0x8a97a6);
public static final Color ITUNES_TABLE_HEADER_BOTTOM_BORDER_COLOR =
new Color(0x555555);
public static final Color ITUNES_TABLE_HEADER_UNSELECTED_TOP_COLOR =
new Color(0xdbdbdb);
public static final Color ITUNES_TABLE_HEADER_UNSELECTED_BOTTOM_COLOR =
new Color(0xbbbbbb);
public static final Color ITUNES_TABLE_HEADER_SELECTED_TOP_COLOR =
new Color(0xc2cfdd);
public static final Color ITUNES_TABLE_HEADER_SELECTED_BOTTOM_COLOR =
new Color(0x7d93b2);
public static final Color ITUNES_TABLE_HEADER_PRESSED_UNSELECTED_TOP_COLOR =
new Color(0xc4c4c4);
public static final Color ITUNES_TABLE_HEADER_PRESSED_UNSELECTED_BOTTOM_COLOR =
new Color(0x959595);
public static final Color ITUNES_TABLE_HEADER_PRESSED_SELECTED_TOP_COLOR =
new Color(0x96b7cb);
public static final Color ITUNES_TABLE_HEADER_PRESSED_SELECTED_BOTTOM_COLOR =
new Color(0x536b90);
ITunesTableHeaderRenderer() {
setOpaque(false);
setFont(UIManager.getFont("Table.font").deriveFont(Font.BOLD, 11.0f));
}
ITunesTableHeaderRenderer(JTable table) {
this();
table.getTableHeader().addMouseListener(new HeaderClickHandler());
fTable = table;
}
public Component getTableCellRendererComponent(
JTable table, Object value, boolean isSelected, boolean hasFocus,
int row, int column) {
// determine if the window has focus.
Window window = SwingUtilities.getWindowAncestor(fTable);
boolean windowHasFocus = window != null && window.isFocused();
int modelColumn = fTable.convertColumnIndexToModel(column);
setText(value.toString());
Border leftSpacerBorder = BorderFactory.createCompoundBorder(
BorderFactory.createMatteBorder(0,1,0,0,
getLeftBorderColor(modelColumn, windowHasFocus)),
BorderFactory.createEmptyBorder(1,4,0,4));
Border bottomRightBorder = BorderFactory.createCompoundBorder(
BorderFactory.createMatteBorder(0,0,1,0,
ITUNES_TABLE_HEADER_BOTTOM_BORDER_COLOR),
BorderFactory.createMatteBorder(0,0,0,1,
getRightBorderColor(modelColumn, windowHasFocus)));
setBorder(BorderFactory.createCompoundBorder(
bottomRightBorder, leftSpacerBorder));
fTopColor = getTopColor(modelColumn, windowHasFocus);
fBottomColor = getBottomColor(modelColumn, windowHasFocus);
return this;
}
/**
* Gets the color to use as the top gradient color. This color takes into account window
* focus as well as the selection state of the column.
*/
private Color getTopColor(int column, boolean windowHasFocus) {
Color retVal;
if (!windowHasFocus) {
retVal = ITUNES_TABLE_HEADER_UNSELECTED_TOP_COLOR;
} else if (column == fSelectedColumn && column == fPressedColumn) {
retVal = ITUNES_TABLE_HEADER_PRESSED_SELECTED_TOP_COLOR;
} else if (column == fSelectedColumn) {
retVal = ITUNES_TABLE_HEADER_SELECTED_TOP_COLOR;
} else if (column == fPressedColumn) {
retVal = ITUNES_TABLE_HEADER_PRESSED_UNSELECTED_TOP_COLOR;
} else {
retVal = ITUNES_TABLE_HEADER_UNSELECTED_TOP_COLOR;
}
return retVal;
}
/**
* Gets the color to use as the bottom gradient color. This color takes into account window
* focus as well as the selection state of the column.
*/
private Color getBottomColor(int column, boolean windowHasFocus) {
Color retVal;
if (!windowHasFocus) {
retVal = ITUNES_TABLE_HEADER_UNSELECTED_BOTTOM_COLOR;
} else if (column == fSelectedColumn && column == fPressedColumn) {
retVal = ITUNES_TABLE_HEADER_PRESSED_SELECTED_BOTTOM_COLOR;
} else if (column == fSelectedColumn) {
retVal = ITUNES_TABLE_HEADER_SELECTED_BOTTOM_COLOR;
} else if (column == fPressedColumn) {
retVal = ITUNES_TABLE_HEADER_PRESSED_UNSELECTED_BOTTOM_COLOR;
} else {
retVal = ITUNES_TABLE_HEADER_UNSELECTED_BOTTOM_COLOR;
}
return retVal;
}
private Color getLeftBorderColor(int column, boolean windowHasFocus) {
Color retVal;
if (!windowHasFocus) {
retVal = ITUNES_TABLE_HEADER_LEFT_UNSELECTED_BORDER_COLOR;
} else if (column == fSelectedColumn && column == fPressedColumn) {
retVal = ITUNES_TABLE_HEADER_LEFT_SELECTED_BORDER_COLOR;
} else if (column == fSelectedColumn) {
retVal = ITUNES_TABLE_HEADER_LEFT_SELECTED_BORDER_COLOR;
} else if (column == fPressedColumn) {
retVal = ITUNES_TABLE_HEADER_LEFT_PRESSED_UNSELECTED_BORDER_COLOR;
} else {
retVal = ITUNES_TABLE_HEADER_LEFT_UNSELECTED_BORDER_COLOR;
}
return retVal;
}
private Color getRightBorderColor(int column, boolean windowHasFocus) {
return windowHasFocus && column == fSelectedColumn
? ITUNES_TABLE_HEADER_RIGHT_SELECTED_BORDER_COLOR
: ITUNES_TABLE_HEADER_RIGHT_UNSELECTED_BORDER_COLOR;
}
@Override
protected void paintComponent(Graphics g) {
Graphics2D graphics = (Graphics2D) g.create();
Paint paint = new GradientPaint(
0, 0, fTopColor, 0, getHeight(), fBottomColor);
graphics.setPaint(paint);
graphics.fillRect(0,0,getWidth(),getHeight());
graphics.dispose();
super.paintComponent(g);
}
private class HeaderClickHandler extends MouseAdapter {
private boolean mouseEventIsPerformingPopupTrigger = false;
public void mouseClicked(MouseEvent mouseEvent) {
// if the MouseEvent is popping up a context menu, do not sort
if (mouseEventIsPerformingPopupTrigger) return;
// if the cursor indicates we're resizing columns, do not sort
if (fTable.getTableHeader().getCursor() == Cursor.getPredefinedCursor(Cursor.E_RESIZE_CURSOR)) {
return;
}
final TableColumnModel columnModel = fTable.getColumnModel();
int viewColumn = columnModel.getColumnIndexAtX(mouseEvent.getX());
fSelectedColumn = fTable.convertColumnIndexToModel(viewColumn);
fTable.getTableHeader().repaint();
}
public void mousePressed(MouseEvent mouseEvent) {
this.mouseEventIsPerformingPopupTrigger = mouseEvent.isPopupTrigger();
if (fTable.getTableHeader().getCursor() != Cursor.getPredefinedCursor(Cursor.E_RESIZE_CURSOR)) {
final TableColumnModel columnModel = fTable.getColumnModel();
int viewColumn = columnModel.getColumnIndexAtX(mouseEvent.getX());
fPressedColumn = fTable.convertColumnIndexToModel(viewColumn);
fTable.getTableHeader().repaint();
}
}
@Override
public void mouseReleased(MouseEvent e) {
fPressedColumn = -1;
fTable.getTableHeader().repaint();
}
}
}
To install this renderer, simply set the default renderer on the JTable’s JTableHeader like this:
table.getTableHeader().setDefaultRenderer(new ITunesTableHeaderRenderer(this));
There’s one last bit of subtlety to handle (and subtly is the name of the game!), namely the upper right hand corner of the enclosing JScrollPane. By default, that corner will be blank white, which sticks out like a sore thumb. To paint that area with the appropriate gradient, you can do either of the following:
- Call jscrollPane.setCorner(JScrollPane.UPPER_RIGHT_CORNER, new ITunesTableHeaderRenderer())
- Override the configureEnclosingScrollPane method of JTable, should you be extending that class (this is the technique I used):
@Override protected void configureEnclosingScrollPane() { super.configureEnclosingScrollPane(); Container p = getParent(); if (p instanceof JViewport) { Container gp = p.getParent(); if (gp instanceof JScrollPane) { JScrollPane scrollPane = (JScrollPane)gp; ITunesTableHeaderRenderer renderer = new ITunesTableHeaderRenderer(); renderer.setBorder(BorderFactory.createMatteBorder(0,0,1,0, ITUNES_TABLE_HEADER_BOTTOM_BORDER_COLOR)); scrollPane.setCorner(JScrollPane.UPPER_RIGHT_CORNER, renderer); } } }
The latter technique is a little more self encapsulated, though not quite as clean.
May 15, 2008 at 11:56 am
Cool. I like the hole Sexy Swing App (most works on Tiger, too).
May 15, 2008 at 12:40 pm
We need more Sexy Swing Apps!
May 15, 2008 at 12:59 pm
I noticed something:
The table header is not automatically repainted when the window looses the focus — the selected column is still highlighted.
I’ll handle it with a window listener.
May 15, 2008 at 1:05 pm
Interesting – on Leopard using Java 1.5.0_13 it seems to repaint for me. I was under the impression that a full repaint of the hierarchy occurs when the focus state of a window changes.
What OS are you using?
May 15, 2008 at 11:55 pm
Your right Felix…in a stand-alone app, the table header (and in particular the selected header) doesn’t repaint when focus is lost. For some reason, in my demo app I didn’t see that behavior. I’ll look into it.
May 17, 2008 at 8:31 pm
Great!
May 25, 2008 at 6:54 pm
I fixed it for me now:
Add a window listener to the parent window and repaint the table header when the window is focused/unfocused (you need to get the window later, not in the constructor; best I worked out is in the getTableCellRendererComponent-method).
This works fine for me.
By the way: On Tiger, this also includes Terminal apps.
May 25, 2008 at 11:55 pm
Hi Felix,
I also added the fix – I still haven’t figured out why it *was* working in my demo app.
I also looked into the JTable code, to see how it was changing the selection color based on focus. It uses a plain focus listener, which works when the component in question bases its rendering only on *it’s own* focus state. In our case, we needed to base the rendering on the parent window’s focus state.
As you mention, it isn’t possible to get the parent frame at construction time, so we have to be a little more creative. I created a factory that hands out a Window that repaints itself on focus events – the rest takes care of itself.
-Ken
May 28, 2008 at 5:37 pm
Hi,
maybe your solution is better than mine — the table header is the first painted component when the window shows (I’ve got quite a complex layout, so painting takes at least a quarter second on my ppc) because it gets its own rendering process (that’s what I think is the reason). Now everyone sees the great table header :-) before they see the rest.
Felix
July 6, 2008 at 1:25 am
THis is pretty nifty, but you need to alter the mouseClicked and mousePressed methods in HeaderClickHandler to test if column selection is allowed in the table. Otherwise is highlights a clicked column heading even when column selection is turned off.
As is, you have in mouseClicked…
final TableColumnModel columnModel = fTable.getColumnModel();
int viewColumn = columnModel.getColumnIndexAtX(mouseEvent.getX());
fSelectedColumn = fTable.convertColumnIndexToModel(viewColumn);
That would be better as…
final TableColumnModel columnModel = fTable.getColumnModel();
if (columnModel.getColumnSelectionAllowed ( ))
{
int viewColumn = columnModel.getColumnIndexAtX(mouseEvent.getX());
fSelectedColumn = fTable.convertColumnIndexToModel(viewColumn);
}
July 6, 2008 at 10:48 am
Hmmm…I don’t know if you’d want to prevent sorting if columns weren’t allowed to be selected. Column selection relates to selecting all the cells in a column, where as clicking the header indicates that a sort operation should be performed. These seem like two distinct things to me.
July 7, 2008 at 6:52 pm
Just to be clear… In my case I have a particular table where sorting is not allowed, nor is column selection. Consequently, I do not want _anything_ to happen if the user clicks on a column heading. And that includes changing the color of the header. The stock default header renderer behaves the way I want, and adding the if-then clauses that i suggest causes ITunesTableHeaderRenderer to do so also.
As for why that’s the case, it’s actually a tree-table with just a few columns. Sorting is automatic, and row selection is preferred.
August 13, 2008 at 7:13 am
Hi i read you post at java-dev mailing list, and i tried to use your header renderer, and do some changes, but i don’t know for some reason the sort arrow never drawn even when i just copy paste your code… does it suppose to happen or just in my case?
Can anybody help me with sorter arrow drawing?
thanks…
August 13, 2008 at 10:43 am
I should have pointed out that the table sorting and the sorting arrows were provided by Glazed Lists. If your interested in using Glazed Lists sorting capabilities, check out this demo code.
Sorry about the confusion.
August 27, 2008 at 5:48 am
thanks…
but I finally able to figure out how to draw sorter arrow with your table header, for some reason my table just need ordinary sorting ability and it’s quite easier to just use jdk6 auto sorter ability…
thanks… nice project and awesome Swing tips… can’t wait for your new tips…
August 27, 2008 at 12:10 pm
Glad you got it working. I’ll talk to the Glazed Lists guys about maybe integrating with the JDK 6 sort header rendering (not that that would have helped you).
October 17, 2008 at 12:23 pm
This table header rendering code is great , its something Ive wanted to achieve for quite some time. I actually use Swingx and JXTables so I’ve made modification to your code so that it works with a JXTable with in built sorting on Java 1.5.
Screenshot: http://www.jthink.net/jaikoz/jsp/code/jaikozusingmacwidgets.jpg
Code: http://www.jthink.net/jaikoz/jsp/code/JaikozColumnHeaderRenderer.java
October 17, 2008 at 12:30 pm
Looks great Paul! I’ll take a look at your changes and see if I can incorporate them back in. Also note that I’m currently working on iTunes style scroll bars which may be of interest to you.
-Ken
October 17, 2008 at 12:45 pm
The key changes are in getTableCellRendererComponent, and I simplified the HeaderClickHandler so that it just identified mouse presses and didnt deal with the sorting.
One additional thing I did do is change your Painter code to implement Swingx painters instead then I could use the same Painter to paint the background of a JXLabel used as the header for my browser genre/artist/album list. So unlike others comments on the list I would actually prefer the dependencies on swingx to be kept so your code can be eaily used in situations that might not have ocurred to you, but I can see why the dependency has been removed.