ORIGINAL DRAFT

The Swing Icon interface is surprisingly powerful given it’s amazing simplicity. With it, you can create icons programmatically or perform various operations on them, join them together in different ways and present them easily in various existing components. This month, we’ll develop a component that lets you present a set of icons and cycle through the list through various states. This is especially useful when managing user interfaces with crowded sets of elements that need to reflect states visually for editing or informational purposes. Examples might include a list of log entries that use a colored icon to reflect the severity of each item, or perhaps a table with a column that that reflects the editable, normal, included or excluded state of each row.

Figure 1: Compound, decorated Icons in a
JLabel with JIcon showing multiple state navigation.

Figure 1: Compound, decorated Icons in a JLabel with JIcon showing multiple state navigation.

We won’t spend time demonstrating how JIcon can be used as a table, list or tree cell renderer and/or editor. Suffice it to say that this is not especially difficult. We won’t have room enough to accomplish our goals if we try to do too much. Instead our focus will be on building a simple components that cycles through Icon elements, using a model that you can easily listen to for change events, making it easy to apply this solution to stand-alone use or for handling cell editor/renderer components.

In the process of developing this project, I’ve implement a few Icon utility classes you can download at www.javapro.com. The CompoundIcon class allows you to join any two Icons, either side-by-side or one above the other. The DecoratorIcon allows you to overlay one icon over another, as demonstrated in Figure 1 (with the shortcut decoration). The FilterIcon allows you to apply an image filter to the icon before it gets drawn. Let’s take a brief look at how each functions before moving on.

The Icon interface, provided as part of the Swing collection, is very simple:

public interface Icon
{
  public int getIconHeight(); 
  public int getIconWidth();
  public void paintIcon(Component c, Graphics g, int x, int y);
}

As long at the calling component knows the width and height of the Icon, it can handle layout management well enough. When the time comes to draw the icon, the painIcon method gets called. Most of the time, all you need to do is use the graphic context and draw the image at the specified x and y position, keeping the drawing within the width and height returned by the getIconWidth and getIconHeight methods. Sometimes it’s useful to have access to the component in which the icon is being drawn, using Component getBackground method to match the background color for example.

Given this interface, it’s easy to construct Icons from images. In fact, Swing provides the ImageIcon for this purpose. It’s also easy to draw icons on-the-fly. I’ve provided a CheckBoxIcon class that does just that, drawing a box, either empty, with a check mark or with an X drawn within. We’ll use these to demonstrate multiple states. If you take a look at Figure 1, you’ll see it in use at the bottom left. If you click this icon, it will rotate through each of the possible states. In each state, as separate instance of the CheckBoxIcon is used. The JIcon component manages the states.

Let’s take a whirlwind tour of the three icon implementations that act more like containers. They are designed to enable you to group, overlay or adjust icons and treat them as a single Icon. The CompoundIcon, for example, can be used to place multiple icons in a standard Swing component that normally handles only a single Icon, such as JLabel and JButton.

The CompoundIcon class expects four constructor arguments; two icons to be grouped together, an integer that defines the orientation, and an integer specifying the gap between them for spacing. The orientation is taken from the SwingConstants interface and must be either VERTICAL or HORIZONTAL, otherwise we throw a IllegalArgumentException. The getIconWidth and getIconHeight methods return the largest value from the two icons if the orientation is opposite and the total from the two icons and the gap if the orientation matches the method’s function. Painting the icons is a simple matter of calling their paintIcon methods with the x and y positions in the right location.

The DecoratedIcon allows you to overlay a second icon above another. This is a function that lets you decorate a large icon with a smaller one in one 9 possible positions; TOP, BOTTOM or CENTER vertically and LEFT, RIGHT or CENTER horizontally. The constructor expects two Icon instances and a vertical and horizontal alignment. The alignments are checked for validity and you’ll get IllegalArgumentException if the alignments are bad or if the decoration icon is larger than the decorated icon. The getIconWidth and getIconHeight methods return the width and height for the decorated icon, the larger of the two. The paintIcon method simply draws the decorated icon first, and then the decoration icon right on top is the specified position.

The FilterIcon class applies an ImageFilter to a specified Icon instance. It does this in the constructor and caches the resulting image for later drawing. I chose to use the ImageFilter, which is part of the older AWT because the more recent BufferedImageFilter implementations provided with Java 2D can still be used (BufferedImageFilter implements the ImageFilter interface). The rest of the class does little more than return the created image’s width and height and draw the image at the x and y location when the paintIcon method is called.

The three classes I’ve just talked about give you the power to display compound, decorated and/or filtered icons in existing Swing components. These Icon implementations can be nested to achieve any combination of grouping or image adjustment you need. Unfortunately, none of the Swing components provide much in the way of multiple state views, so let’s take a look at the implementation of a component that lets you handle multiple icons to represent states that are relevant to your application.

Figure 2: JIcon and supporting classes. This
project includes a number of useful Icon implementations that let you nest or apply image filters to
other icon instances.

Figure 2: JIcon and supporting classes. This project includes a number of useful Icon implementations that let you nest or apply image filters to other icon instances.

Let’s take a look at a couple of key classes in the JIcon implementation; our implementation of the IconListModel and the JIcon class itself. The IconListModel is an interface that stores multiple icons for use in JIcon. It manages a mutable list of Icon instances and the current selection state in the form of a current index into the list. An implementation of the IconListModel interface must also send events to any registered ChangeListener to reflect changes in state or the content of the Icon list so that views, like JIcon can immediately reflect these changes. Note that IconListModel extends the Icon interface.

public interface IconListModel
  extends Icon
{
  public int getIconCount();
  public int getCurrentIndex();
  public Icon getIcon(int index);
  public void setIcons(Icon[] icon);
  public void addIcon(Icon icon);
  public void removeIcon(Icon icon);
  public void setCurrentIcon(int index);
  public void addChangeListener(ChangeListener listener);
  public void removeChangeListener(ChangeListener listener);
}

Given this interface, it’s easy to see what needs to be done to implement it. Listing 1 shows the code for IconList, which does just that. We use two ArrayList instances - one to manage the list of ChangeListener instances and the other to manage the list of Icon instances. We cache the width and height value to return these when the icon getIconWidth and getIconHeight methods are called. The width and height are calculated as the largest value from the icons in the list by the calculateSize method, which is called whenever the list content changes.

We also keep a current icon and index value to reflect the state of the current selection. The selected icon is draw when the paintIcon method is called in the Icon interface. We provide a setIcons method to set the content of the entire list, if necessary. The second of two constructor uses this to set the list on initialization. The other constructor is empty and requires populating the list in a separate step. Calling the addIcon or removeIcon methods causes the calculateSize and fireChangeEvent methods to be called. We use a utility method called capIndex to ensure the current index value never exceeds the list size. The fireChangeEvent, addChangeListener and removeChangeListener manage the list of registered ChangeListeners.

Listing 2 shows the JIcon class itself. By default we use a single pixel EmptyBorder to surround the icon, so that we can reflect gaining the focus with a blue LineBorder. You can call the setFocusable method to enable this behavior. By default, we handle mouse events but don’t accept the focus. The three constructors allow you to create an empty JIcon, one with a single Icon or a JIcon with an initial Icon list. In each case, the initComponent method is called to set up esthetic options and listeners.

JIcon implements four listeners; ChangeListener, FocusListener, MouseListener and KeyListener. ChangeListener events are used to trigger a repaint of the view. We use a method called resetSize because changes in the model may affect the preferred and minimum size values returned by the component. The actual size values are cached by the IconList model implementation, which calculates them only when icons are added or removed, so the preferred and minimum size values are updated based on the current getIconWidht and getIconHeight method values, adjusted for any existing border insets.

The FocusListener focusGained and focusLost methods are used to change the border if necessary. We check to see if that’s true by calling the standard isFocusable method. note that both the setFocusable and isFocusable methods were introduced in Java 1.4. You’ll have modify the code to use the isFocusTraversible method if you need to be backward-compatible.

The KeyListener and MouseListener methods respond to user input. For the KeyListener, we respond to space bar presses by calling a utility method called nextIcon. The mousePressed event does the same thing but first requests the focus if appropriate. In both cases, we call repaint to refresh the view. I’ve provided both a nextIcon and prevIcon method in the implementation, though only the nextIcon method is used.

The nextIcon method fetches the current index value from the model and increments the index, wrapping around to the first value if it goes past the end of the list. The reverse is true of the prevIcon method, decrementing the index and wrapping around to the end of the list. The prevIcon method is never used in this implementation, but you might want to allow the user to move backward through the list with the backspace key and the right mouse button, for example.

That’s about it for the JIcon class, other than accessors to let you get and set the model, add, remove and set icons in the list and the setBorder method, which we override to call the resetSize method if the border is not null. The setModel method does a small amount of extra work to unregister any previous model as a ChangeListener before registering with the new model. The net effect is a component that lets you cycle through a list of icons, either directly by setting the model’s index value programmatically, or via user key or mouse events.

The JIcon component fills a void in Swing by supporting the management of a list of Icons that can reflect a state in a visual and editable manner. JIcon can benefit from the many compound Icon implementations we’ve developed to permit more complex views without additional implementation complexity, but those implementations can also be applied to any component that uses instances of the Icon interface, such as JLabel and JButton. JIcon manages state, but the IconListModel also implements the Icon interface and could be used in other components. I hope these techniques and examples serve you well in your hunt for improved usability.

Listing 1

import java.awt.*;
import java.util.*;
import java.util.List;
import javax.swing.*;
import javax.swing.event.*;

public class IconList
  implements IconListModel
{
  protected ArrayList listeners = new ArrayList();
  protected ArrayList icons = new ArrayList();
  protected int width, height;
  protected Icon icon;
  protected int index;
  
  public IconList() {}
  
  public IconList(Icon[] iconList)
  {
    setIcons(iconList);
  }
  
  public int getCurrentIndex()
  {
    return index;
  }
  
  public void setIcons(Icon[] iconList)
  {
    icons.clear();
    for (int i = 0; i < iconList.length; i++)
    {
      icons.add(iconList[i]);
    }
    setCurrentIcon(0);
    calculateSize();
    fireChangeEvent();
  }
  
  public void addIcon(Icon icon)
  {
    icons.add(icon);
    setCurrentIcon(capIndex(icons.size()));
    calculateSize();
    fireChangeEvent();
  }
  
  public void removeIcon(Icon icon)
  {
    icons.remove(icon);
    setCurrentIcon(capIndex(index));
    calculateSize();
    fireChangeEvent();
  }
  
  public int getIconCount()
  {
    return icons.size();
  }
  
  public Icon getIcon(int index)
  {
    return (Icon)icons.get(index);
  }
  
  public void setCurrentIcon(int index)
  {
    if (index < 0 || index > getIconCount() - 1)
    {
      throw new IllegalArgumentException(
        "invalid index value");
    }
    icon = (Icon)icons.get(index);
    this.index = index;
    fireChangeEvent();
  }
  
  protected int capIndex(int index)
  {
    if (index >= icons.size())
    {
      return icons.size() - 1;
    }
    return index;
  }
  
  protected void calculateSize()
  {
    width = 0;
    height = 0;
    for (int i = 0; i < getIconCount(); i++)
    {
      Icon icon = (Icon)icons.get(i);
      width = Math.max(width, icon.getIconWidth());
      height = Math.max(height, icon.getIconHeight());
    }
  }

  public int getIconWidth()
  {
    return width;
  }
  
  public int getIconHeight()
  {
    return height;
  }
  
  public void paintIcon(Component c, Graphics g, int x, int y)
  {
    icon.paintIcon(c, g, x, y);
  }
  
  protected void fireChangeEvent()
  {
    ChangeEvent event = new ChangeEvent(this);
    ArrayList list = (ArrayList)listeners.clone();
    for (int i = 0; i < list.size(); i++)
    {
      ChangeListener listener = (ChangeListener)list.get(i);
      listener.stateChanged(event);
    }
  }
  
  public void addChangeListener(ChangeListener listener)
  {
    listeners.add(listener);
  }
  
  public void removeChangeListener(ChangeListener listener)
  {
    listeners.remove(listener);
  }
}

Listing 2

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

public class JIcon extends JPanel
  implements ChangeListener, FocusListener,
    MouseListener, KeyListener
{
  protected IconListModel icons = new IconList();
  
  protected static final Border
    BLANK_BORDER = new EmptyBorder(1, 1, 1, 1);
  protected static final Border
    FOCUS_BORDER = new LineBorder(Color.blue);
  
  public JIcon()
  {
    initComponent();
  }
  
  public JIcon(Icon icon)
  {
    initComponent();
    addIcon(icon);
  }

  public JIcon(Icon[] iconList)
  {
    initComponent();
    setIcons(iconList);
  }
  
  protected void initComponent()
  {
    setOpaque(true);
    setFocusable(false);
    addKeyListener(this);
    addMouseListener(this);
    addFocusListener(this);
    setBorder(BLANK_BORDER);
    icons.addChangeListener(this);
  }
  
  public IconListModel getModel()
  {
    return icons;
  }
  
  public void setModel(IconListModel icons)
  {
    if (icons != null)
    {
      icons.removeChangeListener(this);
    }
    icons.addChangeListener(this);
    resetSize();
  }

  public void addIcon(Icon icon)
  {
    icons.addIcon(icon);
  }
  
  public void setIcons(Icon[] iconList)
  {
    icons.setIcons(iconList);
  }
  
  public void removeIcon(Icon icon)
  {
    icons.removeIcon(icon);
  }
  
  public void nextIcon()
  {
    int index = icons.getCurrentIndex() + 1;
    if (index >= icons.getIconCount())
    {
      index = 0;
    }
    icons.setCurrentIcon(index);
  }
  
  public void prevIcon()
  {
    int index = icons.getCurrentIndex() - 1;
    if (index < 0)
    {
      index = icons.getIconCount() - 1;
    }
    icons.setCurrentIcon(index);
  }
  
  public void setBorder(Border border)
  {
    super.setBorder(border);
    if (border != null) resetSize();
  }
  
  protected void resetSize()
  {
    Insets insets = getInsets();
    int w = icons.getIconWidth();
    int h = icons.getIconHeight();
    w += (insets.left + insets.right);
    h += (insets.top + insets.bottom);
    Dimension size = new Dimension(w, h);
    setPreferredSize(size);
    setMinimumSize(size);
    doLayout();
    repaint();
  }
  
  public void stateChanged(ChangeEvent event)
  {
    resetSize();
  }
  
  public void focusGained(FocusEvent event)
  {
    if (isFocusable())
    {
      setBorder(FOCUS_BORDER);
    }
  }
  
  public void focusLost(FocusEvent event)
  {
    if (isFocusable())
    {
      setBorder(BLANK_BORDER);
    }
  }
  
  public void mousePressed(MouseEvent event)
  {
    if (isFocusable())
    {
      requestFocus();
    }
    nextIcon();
    repaint();
  }
  
  public void mouseReleased(MouseEvent event) {}
  public void mouseClicked(MouseEvent event) {}
  public void mouseEntered(MouseEvent event) {}
  public void mouseExited(MouseEvent event) {}
  
  public void keyPressed(KeyEvent event)
  {
    int code = event.getKeyCode();
    if (code == KeyEvent.VK_SPACE)
    {
      nextIcon();
      repaint();
    }
  }
  
  public void keyReleased(KeyEvent event) {}
  public void keyTyped(KeyEvent event) {}
  
  public void paintComponent(Graphics g)
  {
    if (isOpaque())
    {
      int w = getSize().width;
      int h = getSize().height;
      g.setColor(getBackground());
      g.fillRect(0, 0, w, h);
    }
    Insets insets = getInsets();
    icons.paintIcon(this, g,
      insets.left, insets.top);
  }
}