ORIGINAL DRAFT

Certain patterns are frequent in user interface development. Among them is the need for radio button groups. It’s also common that radio and check box groupings are associated with subcomponents that are enabled only when the radio or check box is selected. These can even be nested. One of the best visual cues to help the user understand the implications is simple indentation. This month, we’re going to develop a simple framework for handling indented list item presentation and selection. The list may be presented as a bulleted lists with icons or with incrementing numbers. Radio and check box lists can have selected items. We’ll even throw in a new SpaceLayout manager that spreads components apart with equal separation, vertically or horizontally, and a JTextArea subclass that acts like a multi-line JLabel.

One of our objectives is to abstract the list item selection process for groups of radio buttons and check boxes. Our design makes use the Swing ListSelectionModel and associated ListSelectionEvent and ListSelectionListener class and interface. This gives us a pretty healthy paradigm to work with when button groups are involved. You can think of a radio button group as a single selection list and a check box group as a multiple selection list. By changing the ListSelectionModel state when buttons are selected or deselected, we have a complete model for the button group that helps us better respond to events. All we need to do is listen for item change events from the buttons and update the ListSelectionModel accordingly.

Figure 1: JBulletList Class Diagram.

Figure 1: JBulletList Class Diagram.

The elements we want to implement are simple enough in principle. Figure 1 shows the classes we’ll be implementing. There are 4 types of list entries, all of which derive from an abstract base class called BasicListEntry. The are 4 associated list panels, which extend the JBulletList class. For convenience, JBulletList supports a number of static factory methods for creating all the different variants from the same class, though you can certainly call individual constructors as you see fit. Each type of list panel uses its associated list entry class to construct the elements we’ll need. Lets take a look at the entries first.

List Entries

Listing 1 shows the code for BasicListEntry. We keep track of various instance variables which hold the components for each of the bullet, the label and a component associated with the entry that can be enabled or disabled. The position value holds a flag, declared as constants in JBulletList,indicating whether the associated component will be shown BELOW or to the RIGHT of the label entry. The width value indicates the bullet item width, which has to be coordinated with other entries to make sure they all align properly. Finally, we keep references to the westPanel and eastPanel as instance variables to make it easier to update components after the panels have been constructed.

The main construction takes place in a method called initListEntry to avoid problems setting things up in subclasses before the super constructor can be called. The actual constructor merely passes its arguments directly to the initListEntry method. The initListEntry method stores object references, sets up panels and populates the components. There is a static northPanel method that pushes the label entry to the north part of a border layout panel. Making it static allows it to be called in the constructor. The other methods support recursing through components to enable or disable children of the associated content component, as well as setting the bullet, label and content components, respectively.

We don’t have room to look at all the code in this component, so we’ll sample the simplest and most complex of the entry classes. Listing 2 shows the BulletListEntry class, which provides two constructors that delegate their work to the initListEntry in the BasicListEntry superclass. The label argument can be either a String or JComponent to maximize flexibility. If you pass in a String argument, a JLabel or JTextLabel is used depending on the relative position you want to use. If you select a RIGHT position for the content component, a JLabel has to be used and short text is recommended. Otherwise, the JTextLabel class is used for wrapping, multi-line labels and the content component will be placed BELOW.

We won’t list JTextLabel but you can find it online. It’s merely a subclass of JTextArea which sets the foreground/background colors and the font to match JLabel. It removes the default border, makes the text non-editable and sets word wrapping options to break between words and wraparound each line.

Listing 3 shows the code for the CheckListEntry class, which handles check box entries. Like the BulletListEntry class, it provides a couple of constructor variants that let you pass a String or JComponent value for the label. We create a JCheckBox instance with the state defined by the ListSelectionModel setting for the index value we use to keep track of the entry’s relative position in the bullet list. With a reference to the check box, we ourselves as an ItemListener to keep the model in sync, processing the itemStateChanged event by removing or adding to the selection interval. The RadioListEntry class works almost identically by only sets selection on a single selection ListSelectionModel.

List Panels

The JBulletList class has a number of panel subclasses that actually implement individual behavior. Some of the JBulletList behavior is inherited but we’ll look at the children first, examining the BulletListPanel and CheckListPanel, respectively. The NumberListPanel and RadioListPanel classes work almost the same way and you can find them online at www.java-pro.com. We’ll take a look at JBullerList and tie things together momentarily.

Listing 4 shows the code for the BulletListPanel class, which provides 4 constructors, the first 3 of which call the fourth. The main constructor first constructs a JLabel with the icon in place. We pad the JLabel with a space to be sure there’s room between the icon and the label text in the bullet list. The main work takes place in the for loop, where instances of the BulletListEntry class are added iteratively to the container. Notice that the width is a measure of the preferred size of the JLabel we use for the bullet and that null component lists are accounted for, just in case you want a bulleted list with only bullets and labels.

The CheckListPanel in Listing 5 is only slightly more complicated. The constructors are virtually identical, substituting check boxes for icon labels. We implement the ListSelectionListener interface and register to receive events from the SelectionModelListener. The valueChanged event handler responds to changes in the model. We walk the list of selections adding an index value to a Vector, which we can iterate through to extract an integer list that tells us which items are selected. The JBulletList superclass provides access to the list through a method called getSelectionList. For single selection radio lists, you can also call the getSelectionIndex method, which returns the first entry from the selection list. The equivalent valueChanged handling in RadioListPanel sets the selection list to a single entry list for compatibility reasons.

After setting the selectionList values, we call the fireActionEvent method from the superclass. JBulletList also implements the addActionListener and removeActionListener methods so you can register to receive action events and use the getSelectionList or getSelectionIndex method to find out which buttons are selected. Let’s take a look at the rest of our JBulletList class in Listing 6. We provide accessors for the ListSelectionModel, as well as the a toString method to make debugging easier. The rest of the class is a set of static methods that create appropriate subclasses. There are 14 create methods that let you create bullet, numbered, radio and check lists.

We haven’t talked much about the NumeredListPanel, but its worth mentioning that you can use optional prefix and suffix strings. For example, a period suffix is common and surrounding parentheses are typical as well. Naturally, you can use any variant you like.

You may have also noticed a reference to a SpaceLayout manager. This implementation is similar to the Box layout but separates arbitrary components by equal divisions. The first and last components will always be at the top and bottom or left and right, depending on the orientation you choose. This is a nice way to keep the separability evenly spaces. Strictly speaking, other layout managers may do the job but I decided I could use this in other applications, so it was worth the small amount of effort involved. Layour managers are easy to develop and I’ve written about them in the past. All my layout manager implementations use the base AbstractLayout class to do most of the default work for me. You can find both these classes online.

Summary

Figures 2 and 3 show the BulletListPanel and CheckListPanel displays with the same example entries. You can see the same output by running the JBulletListTest class and navigate the tabs to see the other options. Notices that for the Bullet and Number lists, all the content components are enabled but in the Radio and Check lists, only active entries are enabled. The label associated with the list entry is never disabled. In this example, the text field, combo box and example label text are all part of a content panel which is subject to the enable/disable behavior.

Figure 2: BulletListPanel at work.

Figure 2: BulletListPanel at work.

Figure 3: CheckListPanel at work.

Figure 3: CheckListPanel at work.

The JBulletList and supporting classes provide a nice mechanism for handling radio and check box groupings as well as nested selection structures. The bullet and number lists are convenient for presenting useful information associated with other components, such as explanations or a representation of sequence, as you might find in a wizard. The use of the ListSelectionModel is a useful convenience. It allows us to use a ListSelectionListener if we want to respond to changes based on which entries are active. You can also respond to action events and lookup the current state with either the getSelectionList or getSelectionIndex method. If you put these classes to work in your projects, I think you’ll find them surprisingly useful.

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

public abstract class BasicListEntry extends JPanel
  implements SwingConstants
{
  protected int position, width;
  protected JComponent bullet, label, content;
  protected JPanel westPanel, eastPanel;

  public BasicListEntry() {}

  public BasicListEntry(
    JComponent bullet, JComponent label, JComponent content,
    int position, int width)
  {	
    initListEntry(bullet, label, content, position, width);
  }
  
  protected static JComponent northPanel(JComponent component)
  {
    JPanel panel = new JPanel(new BorderLayout());
    panel.add(BorderLayout.NORTH, component);
    return panel;
  }
	
  protected void initListEntry(
    JComponent bullet, JComponent label, JComponent content,
    int position, int width)
  {	
    this.bullet = bullet;
    this.label = label;
    this.content = content;
    this.position = position;
    this.width = width;
  
    setLayout(new BorderLayout());
    add(BorderLayout.WEST, westPanel =
      new JPanel(new BorderLayout()));
    add(BorderLayout.CENTER, eastPanel =
      new JPanel(new BorderLayout(4, 4)));
    
    bullet.setPreferredSize(new Dimension(width, 
      bullet.getPreferredSize().height));

    setBulletComponent(bullet);
    setLabelComponent(label);
    setContentComponent(content);
  }

  public void setComponentEnabled(boolean state)
  {
    setComponentEnabled(content, state);
  }
  
  public void setComponentEnabled(Component component, boolean state)
  {
    if (component == null) return;
    component.setEnabled(state);
    if (component instanceof Container)
    {
      Container container = (Container)component;
      int count = container.getComponentCount();
      for (int i = 0; i < count; i++)
      {
        setComponentEnabled(container.getComponent(i), state);
      }
    }
  }

  public void setBulletComponent(JComponent bullet)
  {
    bullet.setPreferredSize(new Dimension(
      width, bullet.getPreferredSize().height));
    westPanel.add(BorderLayout.NORTH, bullet);
    revalidate();
  }

  public void setLabelComponent(JComponent label)
  {
    eastPanel.add(position == JBulletList.CONTENT_BELOW ?
      BorderLayout.NORTH : BorderLayout.WEST, label);
    revalidate();
  }

  public void setContentComponent(JComponent content)
  {
    if (content == null) return;
    eastPanel.add(BorderLayout.CENTER, content);
    revalidate();
  }
}

Listing 2

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

public class BulletListEntry extends BasicListEntry
{
  public BulletListEntry(ImageIcon icon,
    String label, JComponent component,
    int position, int width)
  {
    this(icon,
      position == JBulletList.CONTENT_RIGHT ?
        northPanel(new JLabel(label)) :
        (JComponent)new JTextLabel(label),
      component, position, width);
  }
  
  public BulletListEntry(ImageIcon icon,
    JComponent label, JComponent component,
    int position, int width)
  {
    initListEntry(new JLabel(" ", icon, JLabel.RIGHT),
      label, component, position, width);
  }
}

Listing 3

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

public class CheckListEntry extends BasicListEntry
  implements ItemListener
{
  protected int index;
  protected ListSelectionModel model;
  protected JCheckBox checkBox;

  public CheckListEntry(int index, ListSelectionModel model,
    String label, JComponent component,
    int position, int width)
  {
    this(index, model,
      position == JBulletList.CONTENT_RIGHT ?
        northPanel(new JLabel(label)) :
        (JComponent)new JTextLabel(label),
      component, position, width);
  }
  
  public CheckListEntry(int index, ListSelectionModel model,
    JComponent label, JComponent component,
    int position, int width)
  {
    this.index = index;
    this.model = model;
    checkBox = new JCheckBox("", model.isSelectedIndex(index));
    initListEntry(checkBox, label, component, position, width);
    checkBox.addItemListener(this);
    setComponentEnabled(false);
  }

  public void itemStateChanged(ItemEvent event)
  {
    boolean state = event.getStateChange() == ItemEvent.SELECTED;
    setComponentEnabled(state);
    if (state)
    {
      model.addSelectionInterval(index, index);
    }
    else
    {
      model.removeSelectionInterval(index, index);
    }
  }
}

Listing 4

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

public class BulletListPanel extends JBulletList
{
  public BulletListPanel(String[] labels)
  {
    this(new ImageIcon("icons/BulletIcon.GIF"),
      labels, null, CONTENT_BELOW);
  }
  
  public BulletListPanel(ImageIcon icon, String[] labels)
  {
    this(icon, labels, null, CONTENT_BELOW);
  }
  
  public BulletListPanel(String[] labels, JComponent[] components)
  {
    this(new ImageIcon("icons/BulletIcon.GIF"),
      labels, components, CONTENT_BELOW);
  }
  
  public BulletListPanel(ImageIcon icon,
    String[] labels, JComponent[] components,
    int position)
  {
    JLabel label = new JLabel(" ", icon, JLabel.RIGHT);
    for (int i = 0; i < labels.length; i++)
    {
      add(new BulletListEntry(icon, labels[i], 
        components == null ? null : components[i],
        position, label.getMinimumSize().width));
    }
  }
}

Listing 5

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

public class CheckListPanel extends JBulletList
  implements ListSelectionListener
{
  public CheckListPanel(String[] labels)
  {
    this(labels, null, CONTENT_BELOW);
  }
  
  public CheckListPanel(String[] labels,
    JComponent[] components)
  {
    this(labels, components, CONTENT_BELOW);
  }
  
  public CheckListPanel(String[] labels,
    JComponent[] components, int position)
  {
    selectionModel.setSelectionMode(
      ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);

    JCheckBox checkBox = new JCheckBox("");
    for (int i = 0; i < labels.length; i++)
    {
      add(new CheckListEntry(i, selectionModel, labels[i],
        components == null ? null : components[i],
        position, checkBox.getMinimumSize().width));
    }
    selectionModel.addListSelectionListener(this);
  }

  public void valueChanged(ListSelectionEvent event)
  {
    if (event.getValueIsAdjusting()) return;
    int min = selectionModel.getMinSelectionIndex();
    int max = selectionModel.getMaxSelectionIndex();
    Vector vector = new Vector();
    for (int i = min; i <= max; i++)
    {
      if (selectionModel.isSelectedIndex(i))
        vector.addElement(new Integer(i));
    }
    int count = vector.size();
    selectionList = new int[count];
    for (int i = 0; i < count; i++)
    {
      selectionList[i] =
        ((Integer)vector.elementAt(i)).intValue();
    }
    fireActionEvent("CheckList");
  }
}

Listing 6

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

public abstract class JBulletList extends JPanel
{
  public static final int CONTENT_RIGHT = 1;
  public static final int CONTENT_BELOW = 2;

  protected int[] selectionList;
  protected ListSelectionModel selectionModel =
    new DefaultListSelectionModel();
  
  public JBulletList()
  {
    setLayout(new SpaceLayout());
  }
  
  public ListSelectionModel getSelectionModel()
  {
    return selectionModel;
  }

  public void setSelectionModel(ListSelectionModel selectionModel)
  {
    this.selectionModel = selectionModel;
  }

  public BasicListEntry[] getListEntries()
  {
    int count = getComponentCount();
    BasicListEntry[] listEntries =
      new BasicListEntry[count];
    for (int i = 0; i < count; i++)
    {
      listEntries[i] = (BasicListEntry)getComponent(i);
    }
    return listEntries;
  }
  
  public int getSelectionIndex()
  {
    if (selectionList == null ||
      selectionList.length == 0)
      return -1;
    return selectionList[0];
  }
  
  public int[] getSelectionList()
  {
    return selectionList;
  }

  public String toString()
  {
    if (selectionList == null) return "JBulletList={}";
    StringBuffer buffer = new StringBuffer("JBulletList={");
    for (int i = 0; i < selectionList.length; i++)
    {
      if (i > 0) buffer.append(",");
      buffer.append("" + selectionList[i]);
    }
    return buffer.toString() + "}";
  }

  protected Vector listeners = new Vector();
  
  public void addActionListener(ActionListener listener)
  {
    listeners.add(listener);
  }

  public void removeActionListener(ActionListener listener)
  {
    listeners.remove(listener);
  }
  
  protected void fireActionEvent(String command)
  {
    Vector list = (Vector)listeners.clone();
    ActionEvent event = new ActionEvent(
      this, ActionEvent.ACTION_PERFORMED, command);
    for (int i = 0; i < list.size(); i++)
    {
      ActionListener listener = (ActionListener)list.get(i);
      listener.actionPerformed(event);
    }
  }

  public static JBulletList createBulletList(
    String[] labels)
  {
    return new BulletListPanel(labels);
  }

  public static JBulletList createBulletList(
    String[] labels, JComponent[] components)
  {
    return new BulletListPanel(labels, components);
  }

  public static JBulletList createBulletList(
    ImageIcon icon, String[] labels)
  {
    return new BulletListPanel(icon, labels);
  }

  public static JBulletList createBulletList(
    ImageIcon icon, String[] labels, JComponent[] components,
    int position)
  {
    return new BulletListPanel(icon, labels, components, position);
  }

  public static JBulletList createNumberList(
    String[] labels, JComponent[] components)
  {
    return new NumberListPanel(labels, components);
  }

  public static JBulletList createNumberList(
    String[] labels)
  {
    return new NumberListPanel(labels);
  }

  public static JBulletList createNumberList(
    String[] labels, JComponent[] components, int position)
  {
    return new NumberListPanel(labels, components, position);
  }

  public static JBulletList createNumberList(
    String[] labels, JComponent[] components,
    int position, String prefix, String suffix)
  {
    return new NumberListPanel(labels, components,
      position, prefix, suffix);
  }

  public static CheckListPanel createCheckList(
    String[] labels)
  {
    return new CheckListPanel(labels);
  }

  public static CheckListPanel createCheckList(
    String[] labels, JComponent[] components)
  {
    return new CheckListPanel(labels, components);
  }
  
  public static CheckListPanel createCheckList(
    String[] labels, JComponent[] components, int position)
  {
    return new CheckListPanel(labels, components, position);
  }

  public static RadioListPanel createRadioList(
    String[] labels)
  {
    return new RadioListPanel(labels);
  }

  public static RadioListPanel createRadioList(
    String[] labels, JComponent[] components)
  {
    return new RadioListPanel(labels, components);
  }

  public static RadioListPanel createRadioList(
    String[] labels, JComponent[] components, int position)
  {
    return new RadioListPanel(labels, components, position);
  }
}