The popup menus in Swing application are rather standard. Put a fewJMenuItems (most probably with icons), spice them up with an assorted selection of JSeparators, and your plain boring popup is ready for the outside world to see.

The immediate solution to creating custom menu items is, of course, using some lively look'n'feel scheme. However, you don't have to go that far to bring your popup menu alive. There are two things you can do - use HTML text and create custom background image. The following example illustrates both techniques.

First, here is a sample popup menu:
menu.jpg

The zoomed-in version shows some interesting details on the underlying visual artifacts:
menuBig.jpg 
  • The text of the header entry is painted twice (black shadow makes the white text stand out).
  • The text of regular entries is HTML andanti-aliased.
  • The gradient on the header entry and the overlaid squares will be discussed below.
The first and the simpler thing to do - make menu items with HTML text. Simply provide valid HTML construct to the constructor (orsetText() function) of JMenuItem:
JMenuItem myItem = new JMenuItem("<html><b>Increase</b> font size</html>");
The more interesting part is providing custom rendering for your menu items. The obvious approach is to extend theJMenuItem class and override thepaintComponent(Graphics g) function: 
@Override
protected final void paintComponent(Graphics g) {
   Graphics2D graphics = (Graphics2D) g;
   graphics.setColor(Color.red);
   graphics.fillRect(0,0,this.getWidth(), this.getHeight());
   super.paintComponent(graphics);
}
Of course, the fillRect function can be replaced bydrawImage for painting your background image before the super implementation is called. However, this approach fails when the corresponding menu item is selected - the background is filled with grey color (or the color specified inUIDefaults / L&F specific setting). In this case, you may completely override the painting function, usingHTMLEditorKit, HTMLDocument andHTML.Attribute classes to correctly display both the background image and the HTML text. Of course, special attention has to be paid to icons and accelerator strings. The code given below goes halfway in this direction - it draws only the background image and simple (non-HTML) text.

Let's call our class HeaderMenuItem
public final class HeaderMenuItem extends JMenuItem 
The constructor is straightforward, making this item disabled: 
   
public HeaderMenuItem(String text) {
   super(text);
   this.setEnabled(false);
}
The main functionality lies in the paintComponentoverriden function. It sets anti-aliasing hint on the graphics context, paints the background image (with the corresponding dimension) and paints the text with shadow: 
@Override
protected final void paintComponent(Graphics g) {
   Graphics2D graphics = (Graphics2D) g;
   graphics.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING,
    RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
   // paint background image
   graphics.drawImage(ImageCreator.getGradientCubesImage(
        this.getWidth(), this.getHeight(), 
        ImageCreator.mainMidColor,
        ImageCreator.mainUltraDarkColor, 
        (int) (0.7 * this.getWidth()),
    (int) (0.9 * this.getWidth())), 0, 0, null);
   graphics.setFont(HEADER_FONT);
   // place the text slightly to the left of the center
   int x = (this.getWidth() - graphics.getFontMetrics().stringWidth(
    this.getText())) / 4;
   int y = (int)(graphics.getFontMetrics().getLineMetrics(
    this.getText(), graphics).getHeight());

   // paint the text with black shadow
   graphics.setColor(Color.black);
   graphics.drawString(this.getText(), x+1, y+1);
   graphics.setColor(Color.white);
   graphics.drawString(this.getText(), x, y);
}
Where the default font is 
private static final Font HEADER_FONT = new Font("Arial", Font.BOLD, 14);
And the ImageCreator class defines color constants 
public static final Color mainMidColor = new Color(0, 64, 196);
public static final Color mainUltraDarkColor = new Color(0, 0, 64);
and the getGradientCubesImage function. This function is pretty straightforward: 
public static BufferedImage getGradientCubesImage(int width, 
   int height, Color leftColor, Color rightColor, 
   int transitionStart, int transitionEnd) {
First it fills the image with background. All pixels to the left of the transitionStart column are painted withleftColor, all pixels to the right of thetransitionEnd column are painted withrightColor, and the pixels in the transition area are filled with the gradient: 
BufferedImage image = new BufferedImage(width, height,
   BufferedImage.TYPE_INT_ARGB);

Graphics2D graphics = (Graphics2D) image.getGraphics();
graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
   RenderingHints.VALUE_ANTIALIAS_ON);

GradientPaint gradient = new GradientPaint(transitionStart, 0,
   leftColor, transitionEnd, 0, rightColor);
graphics.setPaint(gradient);
graphics.fillRect(transitionStart, 0, 
   transitionEnd - transitionStart, height);

graphics.setColor(leftColor);
graphics.fillRect(0, 0, transitionStart, height);

graphics.setColor(rightColor);
graphics.fillRect(transitionEnd, 0, width - transitionEnd, height);
Now, the transition area is covered with squares of the predefined size. We compute how many rows and columns of squares we have to paint in order to completely cover the transition area: 
public static final int CUBE_DIMENSION = 5;
The computation itself: 
int cubeCountY = height / ImageCreator.CUBE_DIMENSION;
int cubeCountX = 1 + (transitionEnd - transitionStart)
  / ImageCreator.CUBE_DIMENSION;
int cubeStartY = (height % ImageCreator.CUBE_DIMENSION) / 2;
int cubeStartX = transitionStart
   - (ImageCreator.CUBE_DIMENSION - 
   ((transitionEnd - transitionStart) % ImageCreator.CUBE_DIMENSION));
Now, we iterate over cube rows and columns. A random decision is made - if random number is less than 0.5, no cube is drawn at that location: 
for (int col = 0; col < cubeCountX; col++) {
   for (int row = 0; row < cubeCountY; row++) {
     // decide if we should put a cube
     if (Math.random() < 0.5)
        continue;
Now, a semi-random color is chosen. The base color is interpolated according to the current X position in the transition area. This color is then perturbed using a random number: 
// Make semi-random choice of color. It should lie
// close to the interpolated color, but still appear
// random
double coef = 1.0 - (((double) col / (double) cubeCountX) + 
  0.9 * (Math.random() - 0.5));
coef = Math.max(0.0, coef);
coef = Math.min(1.0, coef);
The interpolated color is computed: 
// Compute RGB components
int r = (int) (coef * leftColor.getRed() + (1.0 - coef)
   * rightColor.getRed());
int g = (int) (coef * leftColor.getGreen() + (1.0 - coef)
   * rightColor.getGreen());
int b = (int) (coef * leftColor.getBlue() + (1.0 - coef)
   * rightColor.getBlue());
And the corresponding square is filled: 
// fill cube
graphics.setColor(new Color(r, g, b));
graphics.fillRect(cubeStartX + col * ImageCreator.CUBE_DIMENSION, 
   cubeStartY + row * ImageCreator.CUBE_DIMENSION,
   ImageCreator.CUBE_DIMENSION,
   ImageCreator.CUBE_DIMENSION);
The last thing - the border of the square is painted with slightly brighter color (10% closer to the pure white): 
// draw cube's border in slightly brighter color
graphics.setColor(new Color(
   255 - (int) (0.95 * (255 - r)),
   255 - (int) (0.9 * (255 - g)),
   255 - (int) (0.9 * (255 - b))));
graphics.drawRect(cubeStartX + col * ImageCreator.CUBE_DIMENSION, 
   cubeStartY + row * ImageCreator.CUBE_DIMENSION,
   ImageCreator.CUBE_DIMENSION,
   ImageCreator.CUBE_DIMENSION);
The last thing you can do with your regular menu item - draw it in anti-aliased text. All you have to do is extend theJMenuItem class and override thepaintComponent function: 
@Override
protected final void paintComponent(Graphics g) {
   Graphics2D graphics = (Graphics2D) g;
   Object oldHint = graphics.getRenderingHint(
      RenderingHints.KEY_TEXT_ANTIALIASING);
   graphics.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING,
      RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
   super.paintComponent(graphics);
   graphics.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING,
      oldHint);
}
Don't forget to restore the state of the graphics context.