Having a couple evenings to kill in a hotel room, and needing to do a bit of coding to keep myself sane, I wrote some UI and keyboard usability improvements to JNN, James Gosling's RSS reader (screenshot in blog). I hope you'll agree the results are pretty slick. 

I got involved in JNN a year ago, when I downloaded it, and noticed it had some "border build-up" problems in the UI - it was a little ugly. And since I'd been doing some similar fixes for NetBeans, I spent a Saturday afternoon putting together some patches, and ended up being a committer on the project.

JNN is just a handy RSS reader - it's become sort of my news source when I'm online. And I'd done a bunch of work over the last year making NetBeans look really nice on the macintosh, so I figured I'd apply those skills to JNN and make it seem a little more at home on the macintosh desktop. Here's a screenshot (yes, this is a pure Java app!):


It's not quite as pretty on Windows or Linux yet - but if I can tear myself away from my mac, it should be possible to do something similar there - or maybe someone else will. Some of the inspiration for the shadows and the color of the RSS list came from iTunes.

I mentioned improvements in keyboard navigability - here's what's new:

  • Ctrl (Command on mac) - up/down will navigate RSS feeds - no need to tab to the list
  • Up/Down arrows navigate messages even if keyboard focus is in the message area
  • Enter from anywhere will open the top link of the current message in a browser - no mouse needed
  • +/- will adjust the font size

Now for how the shadows and rounded outlines work: Only two things actually changed in the UI - using the system propertyapple.awt.brushMetalLook to set the main window background, and using a custom Border class on the mac. The painting is all Java2D to do the shadows and such. The shadows are simply taking a RoundedRectangle2D.Double and painting it repeatedly with lighter colors, while setting the clipping area so it doesn't paint at the bottom of the rounded rectangle - and then doing the same thing for the bottom with lighter colors. The point being that getting these nice effects is really quite simple - here's the painting code that does it:

    private static final int ARC = 22;

    public void paintBorder(Component component, Graphics g, int a,
                                    int b, int c, int d) {

        Graphics2D g2d = (Graphics2D) g;
        RoundRectangle2D.Double r =
            new RoundRectangle2D.Double (a+(ARC / 2), b+(ARC/2), c-ARC, d-ARC,
            ARC, ARC);

        if (component instanceof JScrollPane) {
            component = ((JScrollPane) component).getViewport().getView();
        Color bg = component.getBackground();

        Color outer = UIManager.getColor("control");


        g2d.setColor (outer);
        g2d.fillRect (a, b, c, d);

        g2d.setPaint (bg);

        Shape clip = g2d.getClip();

        Rectangle withoutBottom = new Rectangle (a, b, c, d - ARC);

        if (clip != null) {
            Area area = new Area (clip);
            area.intersect(new Area(withoutBottom));
            g.setClip (area);
        } else {
            g.setClip (withoutBottom);

        double amt = 0.65d;
        int blu = 5;

        int[] colors = new int[] { 125, 160, 180, 215, 232, 242 };
        int red = bg.getRed();
        int green = bg.getGreen();
        int blue = bg.getBlue();
        double ttl = 0;
        Color first = null;
        for (int i=0; i < colors.length; i++) {
            g2d.translate (0d, amt);
            ttl += amt;
            int ra = red - (255 - colors[i]);
            int ga = green - (255 - colors[i]);
            int ba = blue - (255 - colors[i]) + blu;
            Color col = new Color (ra, ga, ba);
            g2d.setPaint (col);
            if (i == colors.length /2) {
                first = col;
        g2d.translate (0d, -ttl);

        Rectangle bottom = new Rectangle (a, b + (d - ARC), c, ARC/2);

        if (clip != null) {
            Area area = new Area (clip);
            area.intersect(new Area(bottom));
            g.setClip (area);
        } else {
            g.setClip (bottom);
        g2d.translate (0, -amt);
        g2d.setPaint (new GradientPaint (0, (b+d)-ARC, first, 0, b+(d-(ARC/2)), Color.WHITE));

        Composite comp = g2d.getComposite();
        g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.5f));

        g2d.translate (0, -amt);
        g2d.setPaint (new GradientPaint (0, (b+d)-ARC, first, 0, b+(d-(ARC/2)), Color.WHITE));

        g2d.setClip (clip);
        g2d.translate (0, amt * 2);
        g2d.setComposite (comp);

    private static final Map HINTS = new HashMap();
    static {
        HINTS.put (RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);