ORIGINAL DRAFT

This month’s installment is a pretty ambitious entry. Broadly based on the Microsoft ListView control, our widget supports multiple list views with custom rendering and customizable, multiple state icons. It’s worth noting that this is not the Microsoft ListView control, which is much more mature, but there is much to learn from the basic design and implementation of a similar control. JListView is a powerful solution to many UI problems and a great component to have in your growing bag of tricks.

Figure 1 shows a screen shot of the JListView test harness. The buttons along the toolbar are not part of the JListView component. They allow you to change views on the fly so that you can see the results. The file icons were generated using an excellent third party Java/native library called JConfig (not to be confused with an earlier JConfigure widget from this column), which I encourage you to look into at www.tolstoy.com. You’ll need it to run JListViewTest but none of the other classes depend on it.

Figure 1: JListView in Large Icon View.

Figure 1: JListView in Large Icon View.

What JListView can do is considerable. Any collection of ListViewItem objects can be displayed in any of four views: a Large Icon view, a Vertical list view, a Horizontal list view and a Table view. Each entry has an associated state, which can be displayed with a customizable icon, or not at all if you prefer. The default state handling uses a dual state check box. Each ListViewItem can have any number of additional data elements associated with it. Like the Windows ListView, these elements are displayed only in the Table view, but in our implementation, they can be of any object type.

Data Modeling

To maximize flexibility, we’ll be using a few interfaces. For each of these interfaces, we provide a default implementation. The code for JListView spans 10 classes and 4 interfaces, most of them relatively simple. Because the work is well divided, as it should be in a good object oriented design, the individual classes are fairly straightforward.

Since the main thrust of our control is displaying list items, we start with the ListViewItem interface in Listing 1. Each item has an associated state, which is an integer enumeration starting at zero. We’ll be associating an icon with each of the states and allowing the user to move to the next state when they click on the icon. Our code also needs to support accessors for a small and a large icon. We define a small icon to be 16 x16 pixels and a large icon to be 32 x 32 pixels, though you can use other sizes if you want to. Finally, each ListViewItem has a label, which is displayed with the large or small icon, and an optional set of attributes. With suitable accessor methods to set and get these values, we are ready for a concrete implementation.

Listing 2 shows the DefaultListViewItem class. Our constructor expects the minimum, a label along with a large and small Icon. Our accessor methods allow us to set and get each of the values, but the class is otherwise uncomplicated. The only thing to keep in mind is that we don’t do anything in the way of checking for the setAttribute and getAttribute methods, making the assumption that the array was initially set by the setAttributes method.

A single list item is of little use, so we implement a ListViewModel to handle collections. Listing 3 shows the ListViewModel interface, which extends the Swing TableModel interface because we want to leverage JTable to make the table view functional. We add a pair of methods to access a ListViewItem directly, allowing us to add an item and to get one based on its index value.

Listing 4 shows our DefaultListViewModel class, which implements both the TableModel and the ListViewModel methods. Our model provides a set of headings, which you’ll want to replace to handle your own data requirements. We handle the getValueAt method with a special case to return the state or the whole ListViewItem object for the first two columns, respectively. The state is used to determine the state icon to display and the item is returned whole so the renderer can have access to appropriate icons when drawing the item label. Any other index value returns the attribute item at appropriate offsets in the array, displayed only by the table view.

Notice that we don’t implement the setValueAt method because we want entries to be handled as ListViewItem elements. We also support adding and removing a TableModelListener, though we never fire off a change event. If you decide you want to make the model more dynamic, the infrastructure for doing it is there. For the DefaultListViewModel, this is less important, given that more specific needs will almost always lead you to implementing your own model. By leveraging the TableModel, this process should be relatively familiar.

The state icon allows the user to click on a 16 x 16 region to change the state of the item, cycling through any number of possible states. Code for the StateIconList interface is provided in Listing 5. As you can see, we only need to be able to add or get an Icon and determine the size of the Icon list. Our concrete class is called CheckStateIconList, which you can look at in Listing 6. Our implementation defines a couple of useful contants for the two states and adds the icons for a check box with and without a check mark. If you decide to create your own StateIconLists, you can add any number of icons to cycle through.

Display Elements

By using a renderer to display our lists, we can cut back on a lot of overhead and provide increased flexibility, allowing developers to modify the look and feel with minimal effort. The ListViewRenderer interface in Listing 7 requires a single method called getListViewRendererComponent. The four arguments are a reference to the JListView object, the value to be rendered, and two boolean values indicating whether we have the focus and/or the item is selected. In our case, only the ListViewItem label is displayed as a selection, although some would argue that the whole row should be selected in the Table view. I chose to stay consistent with expectations on the Windows platform because of its widespread use.

Listing 8 shows the DefaultListViewRenderer class, which is complicated by the fact that it implements both the ListViewRenderer and the Swing TableCellRenderer interfaces. Furthermore, we have to deal with compound elements in different positions and of different dimensions, based on context. The Large icon view, for example, has a different drawing area and relative placement for the icon and label.

If you take a close look at the code in Listing 8, you’ll see that the constructor sets up several JLabel components as part of a BorderLayout. We can set some of these to invisible with the setVisible method and control what gets displayed between large and small icons and label placement. It’s important to set the opaque flag to true if you want to avoid painting problems.

To determine what gets drawn, we watch for three conditions. We are being passed a ListViewItem, in which case we draw the label and associated Icon in the proper context. Or, we are being passed an Integer, in which case we make sure there is an allocated StateList and use it to determine which state Icon to draw. If neither of these two conditions exist, we draw the object as a string by calling the toString method. The only other thing we have to act on is the foreground and background coloring, which is depended on the two boolean values for hasFocus and isSelected.

Listing 9 shows a very simple FocusBorder class, which we use in our renderer to draw the dotted line around a focussed rectangle. It’s easy enough to do this kind of thing inline but there are many uses for a class like this and the lure of reusability is strong. We capitalize on the Swing BasicGraphicsUtils class, which provides several static methods for performing useful drawing functions.

Multiple Views

Listing 10 shows the ListViewLargeIconPanel class, which you can see demonstrated in Figure 1. The setup is trivial, keeping an internal reference to the JListView object is important, and adding a mouse listener which is implemented by the class directly. The unitSize instance variable holds the dimensions of an individual cell, which you may decide to change if you want to affect the look and feel. We use a short utility method to render each cell. The render method gets the renderer and sets the size before forcing it to layout the rendering component. This is necessary because we’re using a compound renderer, which may change layout betwen calls.

The paintComponent does much of the work, iterating through the list of ListViewItem elements in the model and rendering each in appropriate cells. We provide our own implementations of getMinimumSize and getPreferredSize so that we can drop this into a JScrollPane. A more complete implementation would implement the Scrollable interface.

Our implementation of the MouseListener watches for mousePressed events and uses two utility method to determine what cell it’s in and whether the state icon is being clicked. The first method, insideIndex returns the element index from the mouse coordinates. Based on that, we change the current selection to reflect the mouse click and request the focus. Before repainting, however, we check to see if the state icon was clicked and step the state up to the next available value using the StateIconList interface, wrapping back to zero if we surpass the size of the collection.

Figure 2: JListView in Horizontal List View.

Figure 2: JListView in Horizontal List View.

Listing 11 shows the ListViewHorizontalPanel class, which is very similar to the previous class. Figure 2 shows the ListViewHorizontalPanel in action. The objective is to lay out the elements horizontally first, from left to right, moving down to the next row when we exceed the display area for a given line. The class implements the same helper methods to handle mouse events and painting, using a slightly different algorithm to lay out the individual components and detect mouse positions.

Figure 3 shows what the ListViewVerticalPanel class looks like at work. Listing 12 shows the source code. Again, the code is almost identical to the ListViewHorizontalPanel, and differs primarily in the position calculations required for mouse and drawing coordinates.

Listing 13 throws in a simple abstraction for JScrollPane handling, since most of our views require it, it seemed easier to create a panel, exposing a suitable constructor. This is fairly reusable but primarily implemented to make is easier to work with the view panels.

Figure 3: JListView in Vertical List View.

Figure 3: JListView in Vertical List View.

Figure 4 shows the ListViewTablePanel class from Listing 14. This is obviously a Swing JTable with some custom rendering to get the appropriate look and feel. We set the header rendering sizes to smaller values, for example, and turn the grid lines off for esthetic reasons. We also register mouse and keyboard listeners in the constructor while setting all this up, because we need to handle those events directly. The first column is constrained so that it cannot be resized and is removed if there is no StateIconList defined.

The code responds to mouse clicks by checking whether we are in the visible state area, changing state as necessary. The keyReleased event is used to allow toggling states with the space bar.

Figure 4: JListView in Table View.

Figure 4: JListView in Table View.

Listing 15, the JListView class, is the entry point for all of this. Primarily, it’s a container for the various views allowing us to switch between views. A set of constants identify the 4 views. The ICON view is the default view. Most of the instance variables hold various renderers, models, state lists and views. You’ll notice that we wrap the Large Icon, Horizontal and Vertical panes within our ScrollPanel. The JTable instance is handled differently because we have to control the headers more closely.

The setViewType method does most of the work, switching the visible view and repainting it as necessary. We call the revalidate method because we have no way of knowing whether the layout requirements have changed since the last call. Focus events are handled with a repaint call and keyboard events act directly on the ListSelectionModel we use to identify the current selection.

You may have noticed a number of repaint events in the code, which makes it somewhat less than optimal. If you need true speed or plan to deal with long lists, you’ll have to rework this code. When list elements are drawn, for example, it would be much more efficient to determine which previous selection was enabled, redraw that selection, and then draw only the newly selected cell. The same mechanism is applicable to focus events. Unfortunately, I didn’t have time to implement these optimizations before publication date.

Listing 16 shows the JListViewTest class, which requires the JConfig library from www.tolstoy.com to operate properly. You can cut out all this code and simplify the harness to call addItem for any item you want to show. This merely seemed like a good demonstration of something people are used to seeing this kind of component used for. It’s worth noting, however, that you can also use this widget to present the user with check box lists, a desktop (with the Large Icon view), or to display database entries from a suitable model, to list just a few examples.

Summary

The JListView widget is extremely powerful and you are likely to find many uses for it. It is well designed and very useful, but it does have some shortcomings. JListView was not optimized for large lists and will require some rework for each of the views before this is possible. As well, the selection interface is not fully implemented, so you can only select one item at a time. Still, the foundation is solid and the possibilities are open in front of you. I hope that JListView can serve you well.

Listing 1

import javax.swing.*;

public interface ListViewItem
{
  public int getState();
  public void setState(int state);
  public Icon getSmallIcon();
  public void setSmallIcon(Icon icon);
  public Icon getLargeIcon();
  public void setLargeIcon(Icon icon);
  public String getLabel();
  public void setLabel(String label);
  public Object[] getAttributes();
  public void setAttributes(Object[] attributes);
  public Object getAttribute(int index);
  public void setAttribute(int index, Object attribute);
}

Listing 2

import javax.swing.*;

public class DefaultListViewItem implements ListViewItem
{
  protected int state = 0;
  protected Icon smallIcon, largeIcon;
  protected String label;
  protected Object[] attributes;

  protected DefaultListViewItem(String label,
    Icon smallIcon, Icon largeIcon)
  {
    setLabel(label);
    setSmallIcon(smallIcon);
    setLargeIcon(largeIcon);
  }

  public int getState()
  {
    return state;
  }
  
  public void setState(int state)
  {
    this.state = state;
  }
  
  public Icon getSmallIcon()
  {
    return smallIcon;
  }
  
  public void setSmallIcon(Icon smallIcon)
  {
    this.smallIcon = smallIcon;
  }
  
  public Icon getLargeIcon()
  {
    return largeIcon;
  }
  
  public void setLargeIcon(Icon largeIcon)
  {
    this.largeIcon = largeIcon;
  }
  
  public String getLabel()
  {
    return label;
  }
  
  public void setLabel(String label)
  {
    this.label = label;
  }
  
  public Object[] getAttributes()
  {
    return attributes;
  }
  
  public void setAttributes(Object[] attributes)
  {
    this.attributes = attributes;
  }
  
  public Object getAttribute(int index)
  {
    return attributes[index];
  }
  
  public void setAttribute(int index, Object attribute)
  {
    attributes[index] = attribute;
  }
	
  public String toString()
  {
    return "label=" + label + ", state=" + state;
  }
}

Listing 3

import javax.swing.table.*;

public interface ListViewModel extends TableModel
{
  public void addItem(ListViewItem item);
  public ListViewItem getItem(int index);
}

Listing 4

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

public class DefaultListViewModel
  implements ListViewModel, TableModel
{
  protected List items = new ArrayList();
  protected Vector listeners = new Vector();
  
  String[] headings = {"", " Name",
    " Size", " Type", " Modified"};

  public void addItem(ListViewItem item)
  {
    items.add(item);
  }
  
  public ListViewItem getItem(int index)
  {
    return (ListViewItem)items.get(index);
  }
  
  public String getColumnName(int col)
  {
    return headings[col];
  }
  
  public Class getColumnClass(int col)
  {
    if (col == 0) return ListViewItem.class;
    return String.class;
  }
  
  public int getRowCount()
  {
    return items.size();
  }
  
  public int getColumnCount()
  {
    return headings.length;
  }
  
  public boolean isCellEditable(int row, int col)
  {
    return false;
  }
  
  public void setValueAt(Object value, int x, int y)
  {
    // Intentionally not implemented...
  }
  
  public Object getValueAt(int row, int col)
  {
    ListViewItem item = (ListViewItem)items.get(row);
    if (col == 0) return new Integer(item.getState());
    if (col == 1) return item;
    return item.getAttribute(col - 2);
  }
  
  public void addTableModelListener(TableModelListener listener)
  {
    listeners.addElement(listener);
  }

  public void removeTableModelListener(TableModelListener listener)
  {
    listeners.removeElement(listener);
  }
}

Listing 5

import javax.swing.*;

public interface StateIconList
{
  public int size();
  public void addIcon(Icon icon);
  public Icon getIcon(int index);
}
</PRE>
<PRE><B>Listing 6</B>

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

public class CheckStateIconList implements StateIconList
{
  public static final int UNCHECKED = 0;
  public static final int CHECKED = 1;
  
  protected Vector list = new Vector();
  
  public CheckStateIconList()
  {
    addIcon(new ImageIcon(&quot;Uncheck16x16.gif&quot;));
    addIcon(new ImageIcon(&quot;Check16x16.gif&quot;));
  }
  
  public int size()
  {
    return list.size();
  }
  
  public void addIcon(Icon icon)
  {
    list.addElement(icon);
  }

  public Icon getIcon(int index)
  {
    return (Icon)list.elementAt(index);
  }
}

Listing 7

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

public interface ListViewRenderer
{
  public Component getListViewRendererComponent(
    JListView list, Object value,
    boolean hasFocus, boolean isSelected);
}

Listing 8

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

public class DefaultListViewRenderer extends JLabel
  implements ListViewRenderer, TableCellRenderer
{
  protected JListView listView;
  protected Color selectionColor = new Color(0, 0, 196);
  protected Border focusBorder = new FocusBorder();
  protected Border normalBorder;
  protected JLabel largeIcon, smallIcon, label;
  
  public DefaultListViewRenderer(JListView listView)
  {
    this.listView = listView;
    setOpaque(true);
    setLayout(new BorderLayout());
    add(BorderLayout.NORTH, largeIcon = new JLabel());
    largeIcon.setPreferredSize(new Dimension(36, 36));
    largeIcon.setVerticalAlignment(CENTER);
    largeIcon.setHorizontalAlignment(CENTER);
    add(BorderLayout.WEST, smallIcon = new JLabel());
    smallIcon.setPreferredSize(new Dimension(18, 18));
    smallIcon.setVerticalAlignment(CENTER);
    smallIcon.setHorizontalAlignment(CENTER);
    add(BorderLayout.CENTER, label = new JLabel());
    label.setOpaque(true);
    label.setBorder(normalBorder);
  }

  public Component getTableCellRendererComponent(
    JTable table, Object value, boolean isSelected,
    boolean hasFocus, int row, int column)
  {
    if (value instanceof ListViewItem)
    {
      ListViewItem item = (ListViewItem)value;
      smallIcon.setIcon(item.getSmallIcon());
      largeIcon.setVisible(false);
      smallIcon.setVisible(true);
      
      label.setText(item.getLabel());
      label.setHorizontalAlignment(LEFT);
      label.setVerticalAlignment(CENTER);
      if (normalBorder == null)
        normalBorder = new LineBorder(table.getBackground());
      label.setBackground(isSelected ?
        selectionColor : table.getBackground());
      label.setForeground(isSelected ?
        Color.white : table.getForeground());
      label.setBorder(hasFocus &amp;&amp; isSelected ?
        focusBorder : normalBorder);
    }
    else if (listView.states != null &amp;&amp; value instanceof Integer)
    {
      Integer integer = (Integer)value;
      int state = integer.intValue();
      Icon stateIcon = listView.states.getIcon(state);
      
      largeIcon.setVisible(false);
      smallIcon.setVisible(false);
      label.setText(null);
      label.setIcon(stateIcon);
      label.setHorizontalAlignment(RIGHT);
      label.setVerticalAlignment(CENTER);
      label.setBackground(table.getBackground());
      label.setForeground(table.getForeground());
      label.setBorder(normalBorder);
    }
    else
    {
      label.setText(value.toString());
      largeIcon.setVisible(false);
      smallIcon.setVisible(false);
      label.setBackground(table.getBackground());
      label.setForeground(table.getForeground());
      label.setBorder(normalBorder);
    }
    setBackground(table.getBackground());
    
    return this;
  }

  public Component getListViewRendererComponent(
    JListView list, Object value,
    boolean hasFocus, boolean isSelected)
  {
    boolean isLarge = list.getViewType() == JListView.ICON;
    if (value instanceof ListViewItem)
    {
      ListViewItem item = (ListViewItem)value;
      if (isLarge)
      {
        largeIcon.setIcon(item.getLargeIcon());
      }
      else
      {
        smallIcon.setIcon(item.getSmallIcon());
      }
      largeIcon.setVisible(isLarge);
      smallIcon.setVisible(!isLarge);
      
      label.setText(item.getLabel());
      label.setHorizontalAlignment(isLarge ? CENTER : LEFT);
      label.setVerticalAlignment(isLarge ? TOP : CENTER);
    }
    else
    {
      label.setText(value.toString());
      largeIcon.setVisible(false);
      smallIcon.setVisible(false);
    }
    
    if (normalBorder == null)
      normalBorder = new LineBorder(list.getBackground());
    label.setBackground(isSelected ?
      selectionColor : list.getBackground());
    label.setForeground(isSelected ?
      Color.white : list.getForeground());
    label.setBorder(hasFocus &amp;&amp; isSelected ?
      focusBorder : normalBorder);
    setBackground(list.getBackground());
    return this;
  }
}

Listing 9

import java.awt.*;
import javax.swing.border.*;
import javax.swing.plaf.basic.*;

public class FocusBorder implements Border
{
  public Insets getBorderInsets(Component component)
  {
    return new Insets(1, 1, 1, 1);
  }
  
  public boolean isBorderOpaque()
  {
    return false;
  }
  
  public void paintBorder(Component comp, Graphics g,
    int x, int y, int w, int h)
  {
    g.setColor(Color.white);
    BasicGraphicsUtils.drawDashedRect(g, x, y, w, h);
  }
}

Listing 10

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

public class ListViewLargeIconPanel extends JPanel
  implements MouseListener, SwingConstants
{
  protected JListView listView;
  protected CellRendererPane pane;
  protected Dimension unitSize = new Dimension(64, 64);
  
  public ListViewLargeIconPanel(JListView listView)
  {
    this.listView = listView;
    setBackground(Color.white);
    setLayout(new BorderLayout());
    add(BorderLayout.CENTER, pane = new CellRendererPane());
    addMouseListener(this);
  }
  
  private void render(Graphics g, Object value,
    boolean hasFocus, boolean isSelected,
    int x, int y, int w, int h)
  {
    Component render = listView.getRenderer().
      getListViewRendererComponent(
        listView, value, hasFocus, isSelected);
    render.setSize(w, h);
    render.doLayout();
    pane.paintComponent(g, render, this, x, y, w, h);
  }
  
  public void paintComponent(Graphics g)
  {
    g.setColor(getBackground());
    g.fillRect(0, 0, getSize().width, getSize().height);
    int width = getSize().width;
    int height = getSize().height;
    int y = 0, x = 0;
    int count = listView.getModel().getRowCount();
    for (int i = 0; i &lt; count; i++)
    {
      if (x + unitSize.width &gt;= width)
      {
        x = 0;
        y += unitSize.height;
      }
      render(g, listView.getModel().getItem(i), listView.hasFocus(),
        listView.getSelector().isSelectedIndex(i), x, y,
        unitSize.width, unitSize.height);
      
      if (listView.states != null)
      {
        listView.check.setIcon(listView.states.getIcon(
          listView.getModel().getItem(i).getState()));
        pane.paintComponent(g, listView.check, this, x, y, 18, 18);
      }
      x += unitSize.width;
    }
  }

  public Dimension getMinimumSize()
  {
    return getPreferredSize();
  }

  public Dimension getPreferredSize()
  {
    int count = listView.getModel().getRowCount();
    int width = listView.getSize().width - 25;
    int height = unitSize.height * (int)(count /
      Math.max(1, width / unitSize.width) + 1);
    return new Dimension(width, height);
  }
  
  public void mouseClicked(MouseEvent event) {}
  public void mouseReleased(MouseEvent event) {}
  public void mouseEntered(MouseEvent event) {}
  public void mouseExited(MouseEvent event) {}
  public void mousePressed(MouseEvent event)
  {
    int index = insideIndex(event.getX(), event.getY());
    if (index &lt; 0) return;
    listView.getSelector().setSelectionInterval(index, index);
    
    index = insideCheckIndex(event.getX(), event.getY());
    if (listView.states != null &amp;&amp; index &gt;= 0)
    {
      int state = listView.getModel().getItem(index).getState() + 1;
      if (state &gt;= listView.states.size()) state = 0;
      listView.getModel().getItem(index).setState(state);
    }
    listView.requestFocus();
    repaint();
    listView.fireActionEvent(&quot;Mouse&quot;);
  }
  
  public int insideIndex(int posX, int posY)
  {
    int width = getSize().width;
    int height = getSize().height;
    int y = 0;
    int x = 0;
    int index = 0;
    int count = listView.getModel().getRowCount();
    for (int i = 0; i &lt; count; i++)
    {
      if (x + unitSize.width &gt;= width)
      {
        x = 0;
        y += unitSize.height;
      }
      if (posX &gt; x &amp;&amp; posX &lt; x + unitSize.width &amp;&amp;
          posY &gt; y &amp;&amp; posY &lt; y + unitSize.height)
            return index;
      index++;
      x += unitSize.width;
    }
    return -1;
  }

  public int insideCheckIndex(int posX, int posY)
  {
    int width = getSize().width;
    int height = getSize().height;
    int y = 0;
    int x = 0;
    int index = 0;
    int count = listView.getModel().getRowCount();
    for (int i = 0; i &lt; count; i++)
    {
      if (x + unitSize.width &gt;= width)
      {
        x = 0;
        y += unitSize.height;
      }
      if (posX &gt; x &amp;&amp; posX &lt; x + 18 &amp;&amp;
          posY &gt; y &amp;&amp; posY &lt; y + 18)
            return index;
      index++;
      x += unitSize.width;
    }
    return -1;
  }
}

Listing 11

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

public class ListViewHorizontalPanel extends JPanel
  implements MouseListener, SwingConstants
{
  protected JListView listView;
  protected CellRendererPane pane;
  protected Dimension unitSize = new Dimension(128, 18);
  
  public ListViewHorizontalPanel(JListView listView)
  {
    this.listView = listView;
    setBackground(Color.white);
    setLayout(new BorderLayout());
    add(BorderLayout.CENTER, pane = new CellRendererPane());
    addMouseListener(this);
  }
  
  private void render(Graphics g, Object value,
    boolean hasFocus, boolean isSelected,
    int x, int y, int w, int h)
  {
    Component render = listView.getRenderer().
      getListViewRendererComponent(
        listView, value, hasFocus, isSelected);
    render.setSize(w, h);
    render.doLayout();
    pane.paintComponent(g, render, this, x, y, w, h);
  }
  
  public void paintComponent(Graphics g)
  {
    g.setColor(getBackground());
    g.fillRect(0, 0, getSize().width, getSize().height);
    int width = getSize().width;
    int height = getSize().height;
    int y = 0, x = 0;
    int count = listView.getModel().getRowCount();
    for (int i = 0; i &lt; count; i++)
    {
      if (x + unitSize.width &gt;= width)
      {
        x = 0;
        y += unitSize.height;
      }
      int offset = listView.states == null ? 0 : 18;
      render(g, listView.getModel().getItem(i), listView.hasFocus(),
        listView.getSelector().isSelectedIndex(i), x + offset, y,
        unitSize.width - offset, unitSize.height);
      
      if (listView.states != null)
      {
        listView.check.setIcon(listView.states.getIcon(
          listView.getModel().getItem(i).getState()));
        pane.paintComponent(g, listView.check, this, x, y, 18, 18);
      }
      x += unitSize.width;
    }
  }

  public Dimension getMinimumSize()
  {
    return getPreferredSize();
  }

  public Dimension getPreferredSize()
  {
    int count = listView.getModel().getRowCount();
    int width = listView.getSize().width - 25;
    int height = unitSize.height * (int)(count /
      Math.max(1, width / unitSize.width) + 1);
    return new Dimension(width, height);
  }
  
  public void mouseClicked(MouseEvent event) {}
  public void mouseReleased(MouseEvent event) {}
  public void mouseEntered(MouseEvent event) {}
  public void mouseExited(MouseEvent event) {}
  public void mousePressed(MouseEvent event)
  {
    int index = insideIndex(event.getX(), event.getY());
    if (index &lt; 0) return;
    listView.getSelector().setSelectionInterval(index, index);
    
    index = insideCheckIndex(event.getX(), event.getY());
    if (listView.states != null &amp;&amp; index &gt;= 0)
    {
      int state = listView.getModel().getItem(index).getState() + 1;
      if (state &gt;= listView.states.size()) state = 0;
      listView.getModel().getItem(index).setState(state);
    }
    listView.requestFocus();
    repaint();
    listView.fireActionEvent(&quot;Mouse&quot;);
  }
  
  public int insideIndex(int posX, int posY)
  {
    int width = getSize().width;
    int height = getSize().height;
    int y = 0;
    int x = 0;
    int index = 0;
    int count = listView.getModel().getRowCount();
    for (int i = 0; i &lt; count; i++)
    {
      if (x + unitSize.width &gt;= width)
      {
        x = 0;
        y += unitSize.height;
      }
      if (posX &gt; x &amp;&amp; posX &lt; x + unitSize.width &amp;&amp;
          posY &gt; y &amp;&amp; posY &lt; y + unitSize.height)
            return index;
      index++;
      x += unitSize.width;
    }
    return -1;
  }

  public int insideCheckIndex(int posX, int posY)
  {
    int width = getSize().width;
    int height = getSize().height;
    int y = 0;
    int x = 0;
    int index = 0;
    int count = listView.getModel().getRowCount();
    for (int i = 0; i &lt; count; i++)
    {
      if (x + unitSize.width &gt;= width)
      {
        x = 0;
        y += unitSize.height;
      }
      if (posX &gt; x &amp;&amp; posX &lt; x + 18 &amp;&amp;
          posY &gt; y &amp;&amp; posY &lt; y + 18)
            return index;
      index++;
      x += unitSize.width;
    }
    return -1;
  }
}

Listing 12

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

public class ListViewVerticalPanel extends JPanel
  implements MouseListener, SwingConstants
{
  protected JListView listView;
  protected CellRendererPane pane;
  protected Dimension unitSize = new Dimension(128, 18);
  
  public ListViewVerticalPanel(JListView listView)
  {
    this.listView = listView;
    setBackground(Color.white);
    setLayout(new BorderLayout());
    add(BorderLayout.CENTER, pane = new CellRendererPane());
    addMouseListener(this);
  }
  
  private void render(Graphics g, Object value,
    boolean hasFocus, boolean isSelected,
    int x, int y, int w, int h)
  {
    Component render = listView.getRenderer().
      getListViewRendererComponent(
        listView, value, hasFocus, isSelected);
    render.setSize(w, h);
    render.doLayout();
    pane.paintComponent(g, render, this, x, y, w, h);
  }
  
  public void paintComponent(Graphics g)
  {
    g.setColor(getBackground());
    g.fillRect(0, 0, getSize().width, getSize().height);
    int width = getSize().width;
    int height = getSize().height;
    int y = 0, x = 0;
    int count = listView.getModel().getRowCount();
    for (int i = 0; i &lt; count; i++)
    {
      if (y + unitSize.height &gt;= height)
      {
        y = 0;
        x += unitSize.width;
      }
      int offset = listView.states == null ? 0 : 18;
      render(g, listView.getModel().getItem(i), listView.hasFocus(),
        listView.getSelector().isSelectedIndex(i), x + offset, y,
        unitSize.width - offset, unitSize.height);
      
      if (listView.states != null)
      {
        listView.check.setIcon(listView.states.getIcon(
          listView.getModel().getItem(i).getState()));
        pane.paintComponent(g, listView.check, this, x, y, 18, 18);
      }
      y += unitSize.height;
    }
  }

  public Dimension getMinimumSize()
  {
    return getPreferredSize();
  }

  public Dimension getPreferredSize()
  {
    int count = listView.getModel().getRowCount();
    int height = listView.getSize().height - 25;
    int width = unitSize.width * (int)(count /
      Math.max(1, height / unitSize.height) + 1);
    return new Dimension(width, height);
  }
  
  public void mouseClicked(MouseEvent event) {}
  public void mouseReleased(MouseEvent event) {}
  public void mouseEntered(MouseEvent event) {}
  public void mouseExited(MouseEvent event) {}
  public void mousePressed(MouseEvent event)
  {
    int index = insideIndex(event.getX(), event.getY());
    if (index &lt; 0) return;
    listView.getSelector().setSelectionInterval(index, index);
    
    index = insideCheckIndex(event.getX(), event.getY());
    if (listView.states != null &amp;&amp; index &gt;= 0)
    {
      int state = listView.getModel().getItem(index).getState() + 1;
      if (state &gt;= listView.states.size()) state = 0;
      listView.getModel().getItem(index).setState(state);
    }
    listView.requestFocus();
    requestFocus();
    repaint();
    listView.fireActionEvent(&quot;Mouse&quot;);
  }
  
  public int insideIndex(int posX, int posY)
  {
    int width = getSize().width;
    int height = getSize().height;
    int y = 0;
    int x = 0;
    int index = 0;
    int count = listView.getModel().getRowCount();
    for (int i = 0; i &lt; count; i++)
    {
      if (y + unitSize.height &gt;= height)
      {
        y = 0;
        x += unitSize.width;
      }
      if (posX &gt; x &amp;&amp; posX &lt; x + unitSize.width &amp;&amp;
          posY &gt; y &amp;&amp; posY &lt; y + unitSize.height)
            return index;
      index++;
      y += unitSize.height;
    }
    return -1;
  }

  public int insideCheckIndex(int posX, int posY)
  {
    int width = getSize().width;
    int height = getSize().height;
    int y = 0;
    int x = 0;
    int index = 0;
    int count = listView.getModel().getRowCount();
    for (int i = 0; i &lt; count; i++)
    {
      if (y + unitSize.height &gt;= height)
      {
        y = 0;
        x += unitSize.width;
      }
      if (posX &gt; x &amp;&amp; posX &lt; x + 18 &amp;&amp;
          posY &gt; y &amp;&amp; posY &lt; y + 18)
            return index;
      index++;
      y += unitSize.height;
    }
    return -1;
  }
}

Listing 13

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

public class ScrollPanel extends JPanel
{
  public ScrollPanel(Component component, boolean vert, boolean horz)
  {
    setLayout(new BorderLayout());
    add(BorderLayout.CENTER, new JScrollPane(component,
      vert ? JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED :
        JScrollPane.VERTICAL_SCROLLBAR_NEVER,
      horz ? JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED :
        JScrollPane.HORIZONTAL_SCROLLBAR_NEVER));
  }
}

Listing 14

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

public class ListViewTablePanel extends JPanel
  implements SwingConstants, MouseListener, KeyListener
{
  protected JListView listView;
  protected JTable table;
  
  public ListViewTablePanel(JListView listView)
  {
    this.listView = listView;
    setBackground(Color.white);
    setLayout(new BorderLayout());
    table = new JTable(listView.getModel());
    table.addKeyListener(this);
    table.addMouseListener(this);
    JScrollPane scroll = new JScrollPane(table);
    add(BorderLayout.CENTER, scroll);
    table.setShowGrid(false);
    table.setRowMargin(0);
    table.setDefaultRenderer(Integer.class,
      new DefaultListViewRenderer(listView));
    table.setDefaultRenderer(ListViewItem.class,
      new DefaultListViewRenderer(listView));
    table.setDefaultRenderer(String.class,
      new DefaultListViewRenderer(listView));
    table.setRowHeight(18);
    table.getTableHeader().setPreferredSize(new Dimension(18, 18));
    for (int i = 0; i &lt; table.getColumnCount(); i++)
    {
      TableCellRenderer headerRenderer = 
        table.getColumnModel().getColumn(i).getHeaderRenderer();
      ((JLabel)headerRenderer).setHorizontalAlignment(LEFT);
    }
    int width = listView.states == null ? 0 : 18;
    table.getColumnModel().getColumn(0).setResizable(false);
    table.getColumnModel().getColumn(0).setMinWidth(width);
    table.getColumnModel().getColumn(0).setMaxWidth(width);
    table.getColumnModel().getColumn(0).setWidth(width);
  }

  public void mouseClicked(MouseEvent event) {}
  public void mouseReleased(MouseEvent event) {}
  public void mousePressed(MouseEvent event)
  {
    if (listView.states == null) return;
    Point point = event.getPoint();
    int col = table.columnAtPoint(point);
    if (col == 0)
    {
      int  index = table.rowAtPoint(point);
      int state = listView.getModel().getItem(index).getState() + 1;
      if (state &gt;= listView.states.size()) state = 0;
      listView.getModel().getItem(index).setState(state);
      repaint();
    }
    int row = table.columnAtPoint(point);
    listView.getSelector().setSelectionInterval(row, row);
    listView.fireActionEvent(&quot;Mouse&quot;);
  }
  public void mouseEntered(MouseEvent event) {}
  public void mouseExited(MouseEvent event) {}

  public void keyTyped(KeyEvent event) {}
  public void keyPressed(KeyEvent event) {}
  public void keyReleased(KeyEvent event)
  {
    if (listView.states == null) return;
    int code = event.getKeyCode();
    if (code == KeyEvent.VK_SPACE)
    {
      int index = table.getSelectedRow();
      int state = listView.getModel().getItem(index).getState() + 1;
      if (state &gt;= listView.states.size()) state = 0;
      listView.getModel().getItem(index).setState(state);
      repaint();
      listView.fireActionEvent(&quot;Mouse&quot;);
    }
  }
}

Listing 15

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

public class JListView extends JPanel
  implements FocusListener, KeyListener, SwingConstants
{
  public static final int ICON = 1;		// Large iconic view
  public static final int HORZ = 2;		// Horizontal first
  public static final int VERT = 3;		// Vertical first
  public static final int TABLE = 4;

  protected int viewType = ICON;
  protected ListViewModel model =
    new DefaultListViewModel();
  protected ListViewRenderer renderer =
    new DefaultListViewRenderer(this);
  protected ListSelectionModel selector =
    new DefaultListSelectionModel();
  protected StateIconList states =
    new CheckStateIconList();
  protected JLabel check = new JLabel();
  
  protected ScrollPanel iconPanel = new ScrollPanel(
    new ListViewLargeIconPanel(this), true, false);
  protected ScrollPanel horzPanel = new ScrollPanel(
    new ListViewHorizontalPanel(this), true, false);
  protected ScrollPanel vertPanel = new ScrollPanel(
    new ListViewVerticalPanel(this), false, true);
  protected ListViewTablePanel tablePanel =
    new ListViewTablePanel(this);
  protected JPanel panel;
  protected Vector listeners = new Vector();
  
  public JListView()
  {
    setBackground(Color.white);
    setLayout(new BorderLayout());

    selector.addSelectionInterval(0, 0);
    addFocusListener(this);
    addKeyListener(this);
    
    setToolTipText(null);	// de-Activate tooltips
    setViewType(viewType);	// Initial setup
  }
  
  public void addItem(ListViewItem item)
  {
    model.addItem(item);
  }
  
  public int getViewType()
  {
    return viewType;
  }
  
  public void setViewType(int viewType)
  {
    this.viewType = viewType;
    if (viewType == ICON)
    {
      if (panel != null) remove(panel);
      panel = iconPanel;
      add(BorderLayout.CENTER, iconPanel);
      revalidate();
    }
    if (viewType == HORZ)
    {
      if (panel != null) remove(panel);
      panel = horzPanel;
      add(BorderLayout.CENTER, horzPanel);
      revalidate();
    }
    if (viewType == VERT)
    {
      if (panel != null) remove(panel);
      panel = vertPanel;
      add(BorderLayout.CENTER, vertPanel);
      revalidate();
    }
    if (viewType == TABLE)
    {
      if (panel != null) remove(panel);
      panel = tablePanel;
      add(BorderLayout.CENTER, tablePanel);
      revalidate();
    }
  }

  public ListViewModel getModel()
  {
    return model;
  }

  public ListViewRenderer getRenderer()
  {
    return renderer;
  }

  public ListSelectionModel getSelector()
  {
    return selector;
  }

  public boolean isFocusTraversable()
  {
    return true;
  }
  
  public void focusGained(FocusEvent event)
  {
    repaint();
  }

  public void focusLost(FocusEvent event)
  {
    repaint();
  }

  public void keyTyped(KeyEvent event) {}
  public void keyReleased(KeyEvent event) {}
  public void keyPressed(KeyEvent event)
  {
    int code = event.getKeyCode();
    if (code == KeyEvent.VK_SPACE)
    {
      int index = selector.getLeadSelectionIndex();
      int state = model.getItem(index).getState() + 1;
      if (state &gt;= states.size()) state = 0;
      model.getItem(index).setState(state);
      repaint();
    }
    if (code == KeyEvent.VK_HOME)
    {
      int index = 0;
      selector.setSelectionInterval(index, index);
      repaint();
      fireActionEvent(&quot;KeyStroke&quot;);
    }
    if (code == KeyEvent.VK_END)
    {
      int index = model.getRowCount() - 1;
      selector.setSelectionInterval(index, index);
      repaint();
      fireActionEvent(&quot;KeyStroke&quot;);
    }
    if (code == KeyEvent.VK_LEFT ||
        code == KeyEvent.VK_UP)
    {
      int index = selector.getLeadSelectionIndex();
      index--;
      if (index &lt; 0) index = 0;
      selector.setSelectionInterval(index, index);
      repaint();
      fireActionEvent(&quot;KeyStroke&quot;);
    }
    if (code == KeyEvent.VK_RIGHT ||
        code == KeyEvent.VK_DOWN)
    {
      int index = selector.getLeadSelectionIndex();
      index++;
      if (index &gt;= model.getRowCount())
        index = model.getRowCount() - 1;
      selector.setSelectionInterval(index, index);
      repaint();
      fireActionEvent(&quot;KeyStroke&quot;);
    }
  }
  
  public void addActionListener(ActionListener listener)
  {
    listeners.addElement(listener);
  }

  public void removeActionListener(ActionListener listener)
  {
    listeners.removeElement(listener);
  }

  public void fireActionEvent(String command)
  {
    ActionListener listener;
    Vector list = (Vector)listeners.clone();
    ActionEvent event = new ActionEvent(this,
      ActionEvent.ACTION_PERFORMED, command);
    for (int i = 0; i &lt; list.size(); i++)
    {
      listener = ((ActionListener)list.elementAt(i));
      listener.actionPerformed(event);
    }
  }

  public int getSelectedIndex()
  {
    return selector.getLeadSelectionIndex();
  }
	
  public ListViewItem getSelectedItem()
  {
    return model.getItem(selector.getLeadSelectionIndex());
  }

  public void setSelectedIndex(int index)
  {
    selector.setSelectionInterval(index, index);
    repaint();
  }
}

Listing 16

import java.io.*;
import java.awt.*;
import java.awt.image.*;
import java.util.*;
import java.text.*;
import java.awt.event.*;
import javax.swing.*;
import COM.tolstoy.jconfig.*;

public class JListViewTest extends JFrame
  implements ActionListener
{
  static DateFormat formater = DateFormat.getDateTimeInstance(
    DateFormat.SHORT, DateFormat.MEDIUM);
  
  JButton large, small, list, table;
  JListView listView;
  
  public JListViewTest() throws IOException
  {
    getContentPane().setLayout(new BorderLayout());
    JToolBar toolbar = new JToolBar();
    toolbar.add(large = new JButton(
      new ImageIcon(&quot;LargeView16x16.gif&quot;)));
    large.setMargin(new Insets(0, 0, 0, 0));
    large.setFocusPainted(false);
    large.addActionListener(this);
    toolbar.add(small = new JButton(
      new ImageIcon(&quot;SmallView16x16.gif&quot;)));
    small.setMargin(new Insets(0, 0, 0, 0));
    small.setFocusPainted(false);
    small.addActionListener(this);
    toolbar.add(list = new JButton(
      new ImageIcon(&quot;ListView16x16.gif&quot;)));
    list.setMargin(new Insets(0, 0, 0, 0));
    list.setFocusPainted(false);
    list.addActionListener(this);
    toolbar.add(table = new JButton(
      new ImageIcon(&quot;ReportView16x16.gif&quot;)));
    table.setMargin(new Insets(0, 0, 0, 0));
    table.setFocusPainted(false);
    table.addActionListener(this);
    getContentPane().add(BorderLayout.NORTH, toolbar);
    getContentPane().add(BorderLayout.CENTER,
      listView = new JListView());
    listView.addActionListener(this);

    File path = new File(&quot;.&quot;);
    File[] list = path.listFiles();
    for (int i = 0; i &lt; list.length; i++)
    {
      if (list[i].isFile())
      {
        ListViewItem item = getItem(list[i]);
        String type = &quot;File&quot;;
        String size = &quot;&quot; + list[i].length();
        String date = formater.format(new Date(list[i].lastModified()));
        item.setAttributes(new String[] {size, type, date});
        listView.addItem(item);
      }
    }
  }
  
  public void actionPerformed(ActionEvent event)
  {
    Object source = event.getSource();
    if (source instanceof JListView)
    {
      System.out.println(listView.getSelectedItem());
      return;
    }
    if (source == large)
      listView.setViewType(JListView.ICON);
    if (source == small)
      listView.setViewType(JListView.HORZ);
    if (source == list) 
      listView.setViewType(JListView.VERT);
    if (source == table)
      listView.setViewType(JListView.TABLE);
    listView.repaint();
  }
  
  public ListViewItem getItem(File file)
    throws IOException
  {
    try
    {
      DiskObject diskObject = FileRegistry.createDiskObject(file, 0);
      IconBundle icons = diskObject.getIconBundle();
      ImageIcon smallIcon = createImageIcon(icons, IconBundle.kSmallIcon);
      ImageIcon largeIcon = createImageIcon(icons, IconBundle.kLargeIcon);
      return new DefaultListViewItem(file.getName(), smallIcon, largeIcon);
    }
    catch (DiskFileException e)
    {
      throw new IOException(e.getMessage());
    }
  }

  private ImageIcon createImageIcon(IconBundle icons, int type)
  {
    int width = icons.getIconWidth(type);
    int height = icons.getIconHeight(type);
    int[] array = new int[width * height];
    icons.getIcon(type, IconBundle.kXformNone,
      IconBundle.kAlignNone, array);
    MemoryImageSource source = new MemoryImageSource(
      width, height, array, 0, width);
    Image image = Toolkit.getDefaultToolkit().createImage(source);
    return new ImageIcon(image);
  }
  
  public static void main(String[] args)
    throws IOException
  {
    // Init JConfig
    int signature = JUtils.asciiToInt(&quot;item&quot;);
    FileRegistry.initialize(new File(&quot;.&quot;), signature);
    
    PLAF.setNativeLookAndFeel(true);
    JFrame frame = new JListViewTest();
    frame.setBounds(0, 0, 500, 300);
    frame.setVisible(true);
  }
}