Substance custom table cell renderers

I use the Substance Look & Feel in most of my Java Swing GUIs these days. It’s a very well engineered, mature, configurable implementation of a Swing look & feel which can transform the appearance of any Swing GUI. The author Kirill Grouchnikov has done a great job with it over the years and I’d like to thank him for his work on this project (and his other projects such as the Trident animation library and Flamingo component suite).

Although Substance is extremely configurable, one area that I’ve had problems with in the past is that of custom table cell renderers. Substance provides default renderers for common cell value classes such as strings, numbers, booleans etc. which covers most use cases. However, if you want to customise the cell renderer behaviour and still benefit from all of the Substance effects such as row striping, selection highlighting animation etc. you’re quite limited because Substance enforces that your custom renderer is sub-classed from the SubstanceDefaultTableCellRenderer, which itself sub-classes DefaultTableCellRenderer. This is fine as long as your custom renderer is happy to be, effectively, a sub-class of JLabel, but for anything involving custom painting it’s no good.

This is one area that I think Substance could possibly be improved to make it a bit more extensible and flexible.

One specific situation I encountered this problem with recently was where I wanted to display a mini bar-chart in a table cell. The actual custom bar component was a trivial amount of custom painting but I couldn’t see an easy way of getting this into a Substance table cell renderer. I tried creating a simple cell renderer which extended the bar component and implemented the obligatory TableCellRenderer interface, and although this worked to a degree, I lost all of the nice Substance effects mentioned above by virtue of the fact that the renderer wasn’t derived from SubstanceDefaultTableCellRenderer. I managed to get row background striping back quite easily by calling the SubstanceStripingUtils.applyStripedBackground() method but that wasn’t good enough.

Then I had a mini flash of inspiration triggered when I noticed that Substance provided a default renderer for icons…

Because the Substance default renderer is based on a label, I could set the icon property to an implementation of the javax.swing.Icon interface to get my custom painting “injected” into the renderer. The steps involved were:

  1. Make my custom bar component implement the Icon interface. This was simply a case of moving my custom painting code from the paintComponent method to paintIcon and also providing implementations of the getIconWidth and getIconHeight methods.
  2. Create a simple SubstanceDefaultTableCellRenderer sub-class which wraps an instance of my bar component. In the renderer constuctor I call setIcon to attach the bar component to the renderer. The renderer also needs to override the two setBounds methods so that it can set the size of the wrapped bar component, otherwise it wouldn’t resize with the table cell! Oh, and of course an overridden setValue method needs to update the wrapped bar component value as appropriate.

As a result, my two classes looked something like this:

BarIconComponent

package com.purplegem.substanceplay;

import javax.swing.*;
import java.awt.*;

/**
 * Simple custom component which renders a mini bar chart.
 * Implements the Icon interface so it can be used where an Icon can.
 */
public class BarIconComponent extends JComponent implements Icon {

  private double value = 0.5d;

  public BarIconComponent() {
    setSize(40, 20);
  }

  public double getValue() {
    return value;
  }

  public void setValue(double value) {
    this.value = value;
  }

  @Override
  public void paintIcon(Component c, Graphics g, int x, int y) {
    Graphics2D g2 = (Graphics2D) g;
    g2.setColor(Color.GREEN);
    g2.fillRect(0, 0, (int) (getIconWidth() * value), getIconHeight());
  }

  @Override
  public int getIconWidth() {
    return getWidth();
  }

  @Override
  public int getIconHeight() {
    return getHeight();
  }

  @Override
  protected void paintComponent(Graphics g) {
    paintIcon(this, g, 0, 0);
  }

}

BarCellRenderer

package com.purplegem.substanceplay;

import org.pushingpixels.substance.api.renderers.SubstanceDefaultTableCellRenderer;

import java.awt.*;

/**
 * Custom Substance table cell renderer which renders a mini bar
 * chart using the wrapped BarIconComponent instance
 */
public class BarCellRenderer extends SubstanceDefaultTableCellRenderer {

  private BarIconComponent barComponent = new BarIconComponent();

  public BarCellRenderer() {
    // attach our bar component as an icon
    setIcon(barComponent);
  }

  @Override
  public void setBounds(int x, int y, int width, int height) {
    super.setBounds(x, y, width, height);
    // ensure we update the size of the wrapped bar component
    barComponent.setSize(width, height);
  }

  @Override
  public void setBounds(Rectangle r) {
    super.setBounds(r);
    // ensure we update the size of the wrapped bar component
    barComponent.setSize((int) r.getWidth(), (int) r.getHeight());
  }

  @Override
  protected void setValue(Object value) {
    if (value != null) {
      // update the value of the bar component for this particular table cell
      barComponent.setValue((Double) value);
    }
  }

}

Once I’d implemented all of this I then had exactly the result I was expecting – a mini bar chart in a table cell with all the expected Substance table cell effects 🙂

It’s nice to tell the user what’s happening sometimes…

As much as I like Android and my HTC Desire, there are still a few niggles that need ironing out. One case in point is a problem I’ve had for the last week or so and only just got round to investigating and finding a solution for.

For some reason, Android Marketplace on my Desire stopped working for me. I could browse, search and choose to download apps, but they just wouldn’t get beyond the “Starting download…” step. I tried cancelling the downloads and restarting, with no success. I also tried switching between WiFi and 3G, toggling the radios on and off, restarting the phone – several times. No success.

A similar problem happened to me a couple of months ago when I switched my Google account from “googlemail.com” to “gmail.com”once it had become available again in the UK. I did a lot of investigation into this problem at the time and found that a lot of people were being affected. If I switched back to googlemail.com everything worked fine again. I tried removing the old googlemail.com Google account from my phone so that I could replace it with a gmail.com account instead, but it wouldn’t have it. In the end I backed up my apps and data to SD card, reset the phone to factory settings and started afresh with a gmail.com Google account. Once I’d restored my apps and data I was back in business – everything worked just fine.

At the time I remembered seeing references to the Google Talk service so this time round I tried starting the Talk app – and it just died immediately. No error message or other information. Nothing. Tried again – same thing. So, I used the very handy aLogcat app to view the system logs just in case the were any clues in there. And lo and behold, there was an exception message stating that the Talk app couldn’t be started due to a lack of device storage space!

Now, I’d not really paid much attention to the low space notification that I’d been seeing for the last few weeks, because I knew that I was close to the limit of the measly built-in storage in the Desire. But I didn’t for a minute think that this could be the cause of my Marketplace problems. After deleting some unused apps and trying Talk again, it worked! I went to the Marketplace and that worked too! Bingo.

So, a few things to note from this experience…

  1. Don’t ignore the low space notifications any more – it could affect more than you think!
  2. Count the days until Android 2.2 is rolled out by HTC to the Desire (when I’ll be able to install apps to the SD card)
  3. Wonder why on earth app developers don’t think it’s important to tell users what’s happening when something they don’t expect happens

On this last point, why does the Talk app not show an error dialog informing the user why it can’t start? And likewise, why does the Android Marketplace app not tell me why it can’t download items. From a UI design perspective this is inexcusable. App developers – take note!