Sea Glass version 0.1

December 4, 2009

Sea Glass version 0.1 is ready for you to try! This is a very preliminary version of the look and feel, and is missing artwork for tabs and component focused states. Also note that the artwork will likely change as we move forward based on your feedback.

Don’t expect rendering of components to be perfect in this release — text alignment, for example, might not be quite right. The goal with this very early preview is to get feedback on fundamental aspects of the look and feel (e.g. colors).

You can download seaglass-0.1.jar or check out the Google Code Download page.

You can download the demo (based on Laffy) here.

Advertisement

JavaOne 2010?

December 3, 2009

Anyone care to venture a guess as to whether there will be a JavaOne 2010? I know it’s pointless to speculate, but it sure is fun! Here are some facts to inform your guess:

Conference Year Call for Papers Conference Dates
2008 Oct. 26th – Nov. 16th, 2007 May 6th – 9th, 2008
2009 Nov. 19th – Dec. 19th, 2008 June 2nd – 5th, 2009
2010 ? June 22nd – 25th, 2010

According to the Moscone Center’s website, JavaOne 2010 has been scheduled for June 22nd – 25th, 2010. There hasn’t been a call for papers yet, but maybe that’s because the conference is a little later than usual.

What do you think?


As promised, here is the code to create the iTunes navigation header button. It’s not a perfect replica, but it’s as close as is practical.

iTunes uses hand drawn artwork, which is not easy to replicate in code. The inner shadows, for example, are simulated in the my code and look decent, but are not a perfect facsimile of the original. These subtle details are almost invisible when you look at the component, but without these details, the component looks cheap and amateurish.

Some highlights of what’s going on in the code:

  • Inner shadow simulation on the left, top and bottom sides of the selected button.
  • Upward pointing shadow under the text.

See the comments in the code provide further explanation of these items. To actually create the iTunes navigation header component, you can adapt the code from my last post, with TriAreaComponent.

public class ITunesHeaderButtonUI extends BasicButtonUI {

    private static Color TEXT_COLOR = Color.WHITE;
    private static Color TEXT_SHADOW_COLOR = Color.BLACK;

    // the gradient colors for when the button is selected.
    private static Color SELECTED_BACKGROUND_COLOR_1 = new Color(0x141414);
    private static Color SELECTED_BACKGROUND_COLOR_2 = new Color(0x1e1e1e);
    private static Color SELECTED_BACKGROUND_COLOR_3 = new Color(0x191919);
    private static Color SELECTED_BACKGROUND_COLOR_4 = new Color(0x1e1e1e);

    // the border colors for the button.
    private static Color SELECTED_TOP_BORDER = new Color(0x030303);
    private static Color SELECTED_BOTTOM_BORDER = new Color(0x292929);

    // the border colors between buttons.
    private static Color LEFT_BORDER = new Color(255,255,255,21);
    private static Color RIGHT_BORDER = new Color(0,0,0,125);

    private static final Color SELECTED_INNER_SHADOW_COLOR_1 = new Color(0x161616);
    private static final Color SELECTED_INNER_SHADOW_COLOR_2 = new Color(0x171717);
    private static final Color SELECTED_INNER_SHADOW_COLOR_3 = new Color(0x191919);

    @Override
    protected void installDefaults(AbstractButton button) {
        super.installDefaults(button);
        button.setBackground(new Color(0,0,0,0));
        button.setOpaque(false);
    }

    @Override
    public void paint(Graphics g, JComponent c) {
        // if the button is selected, paint the special background now.
        // if it is not selected paint the left and right highlight border.
        AbstractButton button = (AbstractButton) c;
        if (button.isSelected()) {
            paintButtonSelected(g, button);
        } else {
            // paint the border and border highlight if the button isn't
            // selected.
            g.setColor(LEFT_BORDER);
            g.drawLine(0, 1, 0, button.getHeight()-2);
            g.setColor(RIGHT_BORDER);
            g.drawLine(button.getWidth()-1, 1,
                    button.getWidth()-1, button.getHeight()-2);
        }

        super.paint(g, c);
    }

    @Override
    protected void paintText(Graphics g, AbstractButton button,
                             Rectangle textRect, String text) {
        // we need to override the paintText method so that we can paint
        // the text shadow. the paintText method in BasicButtonUI pulls
        // the color to use from the foreground property -- there is no
        // way to change this during the painting process without causing
        // an infinite sequence of events, so we must implement our own 
        // text painting.

        FontMetrics fontMetrics = g.getFontMetrics(button.getFont());
        int mnemonicIndex = button.getDisplayedMnemonicIndex();

        // paint the shadow text.
        g.setColor(TEXT_SHADOW_COLOR);
        BasicGraphicsUtils.drawStringUnderlineCharAt(g, text, mnemonicIndex,
                textRect.x + getTextShiftOffset(),
                textRect.y + fontMetrics.getAscent() + getTextShiftOffset() - 1);

        // paint the actual text.
        g.setColor(TEXT_COLOR);
        BasicGraphicsUtils.drawStringUnderlineCharAt(g, text, mnemonicIndex,
                textRect.x + getTextShiftOffset(),
                textRect.y + fontMetrics.getAscent() + getTextShiftOffset());
    }

    /**
     * Paints the selected buttons state, also used as the pressed state.
     */
    private void paintButtonSelected(Graphics graphics, AbstractButton button) {
        // calculate the middle of the area to paint.
        int midY = button.getHeight()/2;

        Paint topPaint = new GradientPaint(0, 0, SELECTED_BACKGROUND_COLOR_1,
                0, midY, SELECTED_BACKGROUND_COLOR_2);
        ((Graphics2D) graphics).setPaint(topPaint);
        graphics.fillRect(0, 0, button.getWidth(), midY);

        // paint the top half of the background with the corresponding
        // gradient.
        Paint bottomPaint =
                new GradientPaint(0, midY + 1, SELECTED_BACKGROUND_COLOR_3,
                        0, button.getHeight(), SELECTED_BACKGROUND_COLOR_4);
        ((Graphics2D) graphics).setPaint(bottomPaint);
        graphics.fillRect(0, midY, button.getWidth(), button.getHeight());

        // draw the top and bottom border.
        graphics.setColor(SELECTED_TOP_BORDER);
        graphics.drawLine(0, 0, button.getWidth(), 0);
        graphics.setColor(SELECTED_BOTTOM_BORDER);
        graphics.drawLine(0, button.getHeight() - 1,
                button.getWidth(), button.getHeight() - 1);

        // paint the outter part of the inner shadow.
        graphics.setColor(SELECTED_INNER_SHADOW_COLOR_1);
        graphics.drawLine(0, 1, 0, button.getHeight()-2);
        graphics.drawLine(0, 1, button.getWidth(), 1);
        graphics.drawLine(button.getWidth()-1, 1,
                button.getWidth()-1, button.getHeight()-2);

        // paint the middle part of the inner shadow.
        graphics.setColor(SELECTED_INNER_SHADOW_COLOR_2);
        graphics.drawLine(1, 1, 1, button.getHeight()-2);
        graphics.drawLine(0, 2, button.getWidth(), 2);
        graphics.drawLine(button.getWidth()-2, 1,
                button.getWidth()-2, button.getHeight()-2);

        // paint the inner part of the inner shadow.
        graphics.setColor(SELECTED_INNER_SHADOW_COLOR_3);
        graphics.drawLine(2, 1, 2, button.getHeight()-2);
        graphics.drawLine(0, 3, button.getWidth(), 3);
        graphics.drawLine(button.getWidth()-3, 1,
                button.getWidth()-3, button.getHeight()-2);
    }

    @Override
    protected void paintButtonPressed(Graphics graphics, AbstractButton button) {
        paintButtonSelected(graphics, button);
    }
}

Sea Glass artwork update

November 23, 2009

I haven’t had a whole lot of time recently, but I’ve updated the color palette (see the original colors back here) and drawn some new artwork for the Sea Glass look and feel. Kathryn has been plugging away on the implementation, working hard towards an initial release, which is still a ways off.

I’ll let the images do the rest of the talking!






Note that we haven’t settled on the color scheme for the Windows window chrome — suggestions and requests are welcome.

itunes_navigation_barRecently, someone asked me what the best way to create the iTunes navigation header (seen in the iTunes music store — the black shiny bar at the top) would be. Here was my response:

The navigation header’s most prominent feature is it’s multi-stop gradient. There are four colors used in the gradient as illustrated below:

itunes_navigation_bar_gradient

But to really capture the subtleties of the navigation header we need to look closer at the top and bottom of the component, where you’ll notice an inner shadow, and an inner glow. The inner shadow and inner glow are what make the component visually interesting.

itunes_navigation_bar_inner_shadows

I chose to hard code the inner shadow and inner glow sizes to 3 pixels (top) and 2 pixels (bottom) rather than produce the real effects. Real inner shadows and glows aren’t straight forward to create (I talked about them here), and are computationally expensive to recompute because they aren’t currently computed on the graphics card. So I decided to hard code the inner shadow and inner glow colors to the exact colors seen in iTunes. You could figure out their grayscale and alpha values to make them reusable — I’ll leave that as an exercise for the reader.

Here’s the code:

public class ITunesNavigationHeader extends JComponent {

    // the hard-coded preferred height. ideally this would be derived
    // from the font size.
    private static int HEADER_HEIGHT = 25;

    // the background colors used in the multi-stop gradient.
    private static Color BACKGROUND_COLOR_1 = new Color(0x393939);
    private static Color BACKGROUND_COLOR_2 = new Color(0x2e2e2e);
    private static Color BACKGROUND_COLOR_3 = new Color(0x232323);
    private static Color BACKGROUND_COLOR_4 = new Color(0x282828);

    // the color to use for the top and bottom border.
    private static Color BORDER_COLOR = new Color(0x171717);

    // the inner shadow colors on the top of the header.
    private static Color TOP_SHADOW_COLOR_1 = new Color(0x292929);
    private static Color TOP_SHADOW_COLOR_2 = new Color(0x353535);
    private static Color TOP_SHADOW_COLOR_3 = new Color(0x383838);

    // the inner shadow colors on the bottom of the header.
    private static Color BOTTOM_SHADOW_COLOR_1 = new Color(0x2c2c2c);
    private static Color BOTTOM_SHADOW_COLOR_2 = new Color(0x363636);

    @Override
    public Dimension getPreferredSize() {
        return new Dimension(-1, HEADER_HEIGHT);
    }

    @Override
    protected void paintComponent(Graphics g) {
        Graphics2D graphics = (Graphics2D) g.create();

        // calculate the middle of the area to paint.
        int midY = getHeight()/2;

        // paint the top half of the background with the corresponding
        // gradient. note that if we were using Java 6, we could use a
        // LinearGradientPaint with multiple stops.
        Paint topPaint = new GradientPaint(0, 0, BACKGROUND_COLOR_1,
                0, midY, BACKGROUND_COLOR_2);
        graphics.setPaint(topPaint);
        graphics.fillRect(0, 0, getWidth(), midY);

        // paint the top half of the background with the corresponding
        // gradient.
        Paint bottomPaint = new GradientPaint(0, midY + 1, BACKGROUND_COLOR_3,
                0, getHeight(), BACKGROUND_COLOR_4);
        graphics.setPaint(bottomPaint);
        graphics.fillRect(0, midY, getWidth(), getHeight());

        // draw the top inner shadow.
        graphics.setColor(TOP_SHADOW_COLOR_1);
        graphics.drawLine(0, 1, getWidth(), 1);
        graphics.setColor(TOP_SHADOW_COLOR_2);
        graphics.drawLine(0, 2, getWidth(), 2);
        graphics.setColor(TOP_SHADOW_COLOR_3);
        graphics.drawLine(0, 3, getWidth(), 3);

        // draw the bottom inner shadow.
        graphics.setColor(BOTTOM_SHADOW_COLOR_1);
        graphics.drawLine(0, getHeight() - 3, getWidth(), getHeight() - 3);
        graphics.setColor(BOTTOM_SHADOW_COLOR_2);
        graphics.drawLine(0, getHeight() - 2, getWidth(), getHeight() - 2);

        // draw the top and bottom border.
        graphics.setColor(BORDER_COLOR);
        graphics.drawLine(0, 0, getWidth(), 0);
        graphics.drawLine(0, getHeight() - 1, getWidth(), getHeight() - 1);

        graphics.dispose();
    }
}

jide_box
If you’re a JIDE customer, you’ll be happy to know that they’ve incorporated my blog article on Creating a better JTable back into their tables (see their news release here).

My article points out a number of things you can do to get better parity with native tables. Tables tend to permeate applications, so it’s important that they look good. If you haven’t read the article, go check it out and pull in the better JTable code into your codebase!

Sea Glass look and feel

October 1, 2009

I’ve teamed up with Kathryn Huxtable on a new look and feel for Java we’re calling “Sea Glass”. The look was inspired by the following image:
sea_glass
I’ve been working on the artwork, while Kathryn has been doing the hard part of converting the art into an actual Java look and feel. We’ve only just begun the project, so there is much yet to be done, but I’d like to share the following glimpses of the artwork with you:
controls
scroll_bars
The colors will likely be tweaked as we go, but the images above give you a feel for what we’re going for. I’ll provide a link for the project shortly, which will be open source.

If you have comments or suggestions, be sure to leave them below.

hud_slider_without_tickshud_slider_with_ticks
Someone recently requested I add a HUD style slider to the widget set, which I quickly added and is now available in the latest Mac Widgets for Java developer build. Creating a custom SliderUI delegate is pretty easy, as BasicSliderUI has all the right hooks. You can specify the thumb size, tick size, track bounds, thumb location and much more by overriding corresponding methods. This is a refreshing change from the complicated process of creating a custom scroll bar UI delegate (which I talked about in detail in part 1, part 2, and part 3 of the “Skinning a scroll bar” series).

As I’ve noted in previous HUD style component posts (here and here), you may notice a bit of redundant code between the posts. I’ve done this to keep the blog entries independent, but the actual Mac Widgets for Java code uses a utility class to do most of the HUD painting.

Finally, for this widget to look “correct”, it must be added to a HUD style window, which I talked about here.

public class HudSliderUI extends BasicSliderUI {

    private static final int SLIDER_KNOB_WIDTH = 11;
    private static final int SLIDER_KNOB_HEIGHT_NO_TICKS = 11;
    private static final int SLIDER_KNOB_HEIGHT_WITH_TICKS = 13;
    private static final int TRACK_HEIGHT = 4;

    private static final Color TRACK_BACKGROUND_COLOR = new Color(143, 147, 144, 100);
    private static final Color TRACK_BORDER_COLOR = new Color(255, 255, 255, 200);

    private static final Color TOP_SLIDER_KNOB_COLOR = new Color(0x555555);
    private static final Color BOTTOM_SLIDER_KNOB_COLOR = new Color(0x393939);
    private static final Color TOP_SLIDER_KNOB_PRESSED_COLOR = new Color(0xb0b2b6);
    private static final Color BOTTOM_SLIDER_KNOB_PRESSED_COLOR = new Color(0x86888b);

    public static final Color BORDER_COLOR = new Color(0xc5c8cf);

    private static final Color LIGHT_SHADOW_COLOR = new Color(0, 0, 0, 145);
    private static final Color DARK_SHADOW_COLOR = new Color(0, 0, 0, 50);

    private static final ShapeProvider NO_TICKS_SHAPE_PROVIDER =
            createCircularSliderKnobShapeProvider();

    private static final ShapeProvider TICKS_SHAPE_PROVIDER =
            createPointedSliderKnobShapeProvider();

    public HudSliderUI(JSlider b) {
        super(b);
    }

    @Override
    protected void installDefaults(JSlider slider) {
        super.installDefaults(slider);
        slider.setOpaque(false);
    }

    @Override
    protected Dimension getThumbSize() {
        int sliderKnobHeight = slider.getPaintTicks()
                ? SLIDER_KNOB_HEIGHT_WITH_TICKS : SLIDER_KNOB_HEIGHT_NO_TICKS;
        return new Dimension(SLIDER_KNOB_WIDTH, sliderKnobHeight);
    }

    @Override
    public void paintThumb(Graphics graphics) {
        Paint paint = createSliderKnobButtonPaint(isDragging(), thumbRect.height);
        ShapeProvider shapeProvider = slider.getPaintTicks()
                ? TICKS_SHAPE_PROVIDER : NO_TICKS_SHAPE_PROVIDER;
        paintHudControlBackground((Graphics2D) graphics, thumbRect, shapeProvider,
                paint);
    }

    @Override
    public void paintTrack(Graphics graphics) {
        Graphics2D graphics2d = (Graphics2D) graphics;
        graphics2d.setRenderingHint(
                RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);

        double trackY = slider.getHeight()/2.0 - TRACK_HEIGHT/2.0;
        RoundRectangle2D track = new RoundRectangle2D.Double(
                0, trackY, slider.getWidth()-1, TRACK_HEIGHT - 1, 4, 2);

        graphics.setColor(TRACK_BACKGROUND_COLOR);
        graphics2d.fill(track);
        graphics2d.setColor(TRACK_BORDER_COLOR);
        graphics2d.draw(track);
    }

    @Override
    protected int getTickLength() {
        return 5;
    }

    @Override
    protected void calculateThumbLocation() {
        super.calculateThumbLocation();

        // if this is a horizontal style slider and we're drawing a pointy style thumb
        // then shift the thumb down three pixels.
        if ( slider.getOrientation() == JSlider.HORIZONTAL
                && slider.getPaintTicks()) {
            thumbRect.y += 3;
        } else  {
            // TODO handle vertical slider.
        }
    }

    @Override
    protected void calculateTickRect() {
        super.calculateTickRect();

        // if this is a horizontal style slider, shift the ticks down one pixel so that
        // they aren't right up against the track.
        if ( slider.getOrientation() == JSlider.HORIZONTAL ) {
            tickRect.y += 1;
        } else  {
            // TODO handle vertical slider.
        }
    }

    @Override
    protected void paintMajorTickForHorizSlider(Graphics g, Rectangle tickBounds, int x) {
        g.setColor(Color.WHITE);
        super.paintMajorTickForHorizSlider(g, tickBounds, x);
    }

    @Override
    public void setThumbLocation(int x, int y) {
        super.setThumbLocation(x, y);
        // repaint the whole slider -- it's easier than trying to figure out
        // whats dirty, especially since the thumb will be drawn outside of the
        // thumbRect (the shadow part).
        slider.repaint();
    }

    @Override
    public void paintFocus(Graphics g) {
        // don't paint focus.
    }

    private static Paint createSliderKnobButtonPaint(boolean isPressed, int height) {
        // grab the top and bottom gradient colors based on the pressed state.
        Color topColor = isPressed
                ? TOP_SLIDER_KNOB_PRESSED_COLOR : TOP_SLIDER_KNOB_COLOR;
        Color bottomColor = isPressed
                ? BOTTOM_SLIDER_KNOB_PRESSED_COLOR : BOTTOM_SLIDER_KNOB_COLOR;
        // compenstate for the two pixel shadow drawn below the slider thumb.
        int bottomY = height - 2;
        return new GradientPaint(0, 0, topColor, 0, bottomY, bottomColor);
    }

    /**
     * Creates a simple circle.
     */
    private static ShapeProvider createCircularSliderKnobShapeProvider() {
        return new ShapeProvider() {
            public Shape createShape(double x, double y, double width, double height) {
                return new Ellipse2D.Double(x, y, width, height);
            }
        };
    }

    /**
     * Cerates a pointy slider thumb shape that looks roughly like this:
     *     +----+
     *    /      \
     *    +      +
     *    |      |
     *    +      +
     *      \  /
     *       \/
     */
    private static ShapeProvider createPointedSliderKnobShapeProvider() {
        return new ShapeProvider() {
            public Shape createShape(double x, double y, double width, double height) {
                float xFloat = (float) x;
                float yFloat = (float) y;
                float widthFloat = (float) width;
                float heightFloat = (float) height;

                // draw the thumb shape based on the given height and width.
                GeneralPath path = new GeneralPath();
                // move in two pixels so that we can curve down to the next point.
                path.moveTo(xFloat + 2.0f, yFloat);
                // curve down to the second point.
                path.curveTo(xFloat + 0.25f, yFloat + 0.25f, xFloat - 0.25f,
                        yFloat + 2.0f, xFloat, yFloat + 2.0f);
                // move straight down to the next point.
                path.lineTo(xFloat, yFloat + heightFloat/1.60f);
                // move down and right to form the left half of the pointy section.
                path.lineTo(xFloat + widthFloat/2, yFloat + heightFloat);
                // move up and right to form the right half of the pointy section.
                path.lineTo(xFloat + widthFloat, yFloat + heightFloat/1.60f);
                // move straight up to the point to curve from.
                path.lineTo(xFloat + widthFloat, yFloat + 2.0f);
                // curve up and right to the top of the thumb.
                path.curveTo(xFloat + widthFloat - 0.25f, yFloat + 2.0f,
                        xFloat + widthFloat - 0.25f, yFloat + 0.25f,
                        xFloat + widthFloat - 2.0f, yFloat);
                path.closePath();

                return path;
            }
        };
    }

    /**
     * Paints a HUD style background in the given shape. This includes a drop shadow
     * which will be drawn under the shape to be painted. The shadow will be draw
     * outside the given bounds.
     * @param graphics the {@code Graphics2D} context to draw in.
     * @param bounds the bounds to paint in.
     * @param shapeProvider the delegate to request the {@link Shape} from.
     * @param paint the {@link Paint} to use to fill the {@code Shape}.
     */
    public static void paintHudControlBackground(
            Graphics2D graphics, Rectangle bounds, ShapeProvider shapeProvider,
            Paint paint) {

        graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
                RenderingHints.VALUE_ANTIALIAS_ON);

        int x = bounds.x;
        int y = bounds.y;
        int width = bounds.width;
        int height = bounds.height;

        // paint the first (further away) part of the drop shadow.
        graphics.setColor(LIGHT_SHADOW_COLOR);
        graphics.draw(shapeProvider.createShape(x, y, width - 1, height));
        // paint the second (closer) part of the drop shadow.
        graphics.setColor(DARK_SHADOW_COLOR);
        graphics.draw(shapeProvider.createShape(x, y, width - 1, height + 1));

        // fill the HUD shape.
        graphics.setPaint(paint);
        graphics.fill(shapeProvider.createShape(x, y + 1, width, height - 1));
        // stroke the HUD shape.
        graphics.setColor(BORDER_COLOR);
        graphics.draw(shapeProvider.createShape(x, y, width - 1, height - 1));
    }

    /**
     * An interface for specifying a shape to paint and draw a drop shadown under.
     */
    public interface ShapeProvider {
        Shape createShape(double x, double y, double width, double height);
    }
}

new_client_properties_focusednew_client_properties_unfocused
There are a few new client properties on the Mac, that haven’t yet been documented (though I’ve been assured that they’re safe to use). The great thing about the way Apple has been using client properties on the Mac, is that they make it easy for you to get closer to being a great Mac app, while not breaking your fidelity on other platforms. They’re really great if you want to fit in on your target platform, whereas in Mac Widgets for Java, I’m aiming for always looking like a Mac app.

The screenshots above show the new client properties that give you access to SourceList style selection painters (demo’d in the source code below). Note that these painters accurately pick up whether the user is using Aqua or Graphite — that’s a big bonus.

Here’s a listing of the new client properties:

List.sourceListBackgroundPainter
List.sourceListSelectionBackgroundPainter
List.sourceListFocusedSelectionBackgroundPainter
List.evenRowBackgroundPainter
List.oddRowBackgroundPainter

Here’s a little bit of code that puts a few of the new SourceList client properties to work (seen above):

public class NewClientProperties {

    /**
     * Create a SourceList style JList.
     */
    private static JList createMacSourceList() {
        JList list = new SourceList();
        // install a custom renderer that wraps the already installed renderer.
        list.setCellRenderer(new CustomListCellRenderer(list.getCellRenderer()));
        return list;
    }

    /**
     * A custom JList that renders like a Mac SourceList.
     */
    public static class SourceList extends JList {

        public SourceList() {
            // make the component non-opaque so that we can paint the background in
            // paintComponent.
            setOpaque(false);
        }

        @Override
        protected void paintComponent(Graphics g) {
            // paint the background of the component using the special Mac border
            // painter.
            Border backgroundPainter =
                    UIManager.getBorder("List.sourceListBackgroundPainter");
            backgroundPainter.paintBorder(this, g, 0, 0, getWidth(), getHeight());
            super.paintComponent(g);
        }
    }

    /**
     * A custom ListCellRenderrer that wraps a delegate renderer.
     */
    public static class CustomListCellRenderer extends JPanel
            implements ListCellRenderer {

        private ListCellRenderer fDelegate;
        private boolean fIsSelected;
        private boolean fIsFocused;

        public CustomListCellRenderer(ListCellRenderer delegate) {
            this.setOpaque(false);
            this.setLayout(new BorderLayout());
            this.setBorder(BorderFactory.createEmptyBorder(1,5,1,5));
            fDelegate = delegate;
        }

        public Component getListCellRendererComponent(
                JList list, Object value, int index,boolean isSelected,
                boolean cellHasFocus) {

            this.removeAll();
            // remember the isSelected and cellHasFocus state so that we can use those
            // values in the paintComponent method.
            fIsSelected = isSelected;
            fIsFocused = cellHasFocus;
            // call the delegate renderer
            JComponent component = (JComponent) fDelegate.getListCellRendererComponent(
                    list, value, index, isSelected, false);
            // make the delegate rendere non-opqaue so that the background shows through.
            component.setOpaque(false);
            this.add(component, BorderLayout.CENTER);

            return this;
        }

        @Override
        protected void paintComponent(Graphics g) {
            super.paintComponent(g);
            // if the item was selected, then paint the custom Mac selection background.
            if (fIsSelected) {
                Border backgroundPainter = fIsFocused
                        ? UIManager.getBorder("List.sourceListFocusedSelectionBackgroundPainter")
                        : UIManager.getBorder("List.sourceListSelectionBackgroundPainter");
                backgroundPainter.paintBorder(this, g, 0, 0, getWidth(), getHeight());
            }
        }
    }

    public static void main(String[] args) {

        JList list = createMacSourceList();
        list.setListData(new String[]{
                "BMW", "Chevy", "Dodge", "Infiniti", "Nissan", "Porsche"});

        JScrollPane scrollPane = new JScrollPane(list);
        scrollPane.setBorder(BorderFactory.createEmptyBorder());

        JFrame frame = new JFrame();
        frame.add(scrollPane);
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setSize(200,200);
        frame.setLocationRelativeTo(null);
        frame.setVisible(true);
    }
}

SeparatorList
I’ve been espousing the power of Glazed Lists for a long time, and won’t be stopping any time soon! I recently needed a JList that visually grouped items into categories. GL made this super simple.

Here’s how you can create a SeparatorList, which will auto-magically insert separators into your JList for you. We’ll create a simple list of items that we want grouped by their first letter.

public class SeparatorListTest {

    /**
     * Creates a {@link Comparator} that compares the first letter of two given strings.
     */
    private static Comparator createComparator() {
        return new Comparator() {
            public int compare(String stringOne, String stringTwo) {
                return stringOne.substring(0,1).compareTo(stringTwo.substring(0,1));
            }
        };
    }

    /**
     * Creates a renderer that can render both separators and regular items.
     */
    private static ListCellRenderer createListCellRenderer() {
        return new DefaultListCellRenderer() {
            @Override
            public Component getListCellRendererComponent(
                    JList list, Object value, int index, boolean isSelected, 
                    boolean cellHasFocus) {
                
                // call the super renderer to take care of setting the foreground and
                // background colors.
                JLabel label = (JLabel) super.getListCellRendererComponent(
                        list, value, index, isSelected, cellHasFocus);

                // if the item being renderered is a separator, then bold it, and shift
                //    in slightly.
                // else if the item being rendered is an actual list item, make it plain
                //    and shift it in more.
                if (value instanceof SeparatorList.Separator) {
                    SeparatorList.Separator separator = (SeparatorList.Separator) value;
                    label.setText(separator.getGroup().get(0).toString().substring(0,1));
                    label.setFont(label.getFont().deriveFont(Font.BOLD));
                    label.setBorder(BorderFactory.createEmptyBorder(0,5,0,0));
                } else {
                    label.setFont(label.getFont().deriveFont(Font.PLAIN));
                    label.setBorder(BorderFactory.createEmptyBorder(0,15,0,0));
                }

                return label;
            }
        };
    }

    public static void main(String[] args) {
        // create a list of items.
        EventList rawList = GlazedLists.eventListOf(
                "apple", "appricot", "acorn", "blueberry", "coconut", "chesnut", "grape");
        // create a SeparatorList based on the raw list of items using the "first-letter"
        // comparator to group them.
        SeparatorList separatorList =
                new SeparatorList(rawList, createComparator(), 0, 1000);

        JList list = new JList(new EventListModel(separatorList));
        list.setCellRenderer(createListCellRenderer());
        JScrollPane scrollPane = new JScrollPane(list);
        scrollPane.setBorder(null);

        JFrame frame = new JFrame();
        frame.add(scrollPane, BorderLayout.CENTER);
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setSize(200,200);
        frame.setLocationRelativeTo(null);
        frame.setVisible(true);
    }
}