
package uk.co.wingpath.gui;

import java.util.*;
import javax.swing.*;
import javax.swing.event.*;
import java.awt.*;
import java.awt.event.*;
import uk.co.wingpath.event.*;
import uk.co.wingpath.event.Event;
import uk.co.wingpath.util.*;


/**
* This class implements {@code WComponent} using a scrollable {@code JList}.
* It allows the user to select one item from a list of alternatives.
* <p>The class is a drop-in alternative to a {@code WComboBox}.
*/
public class WListBox<T>
    extends WAbstractComponent<T>
{
    private final JList listBox;
    private final T [] values;
    private T value;
    private int lastIndex;
    private int alignment = SwingConstants.LEFT;

    /**
    * Constructs a <code>WListBox</code>.
    * @param label text for the associated label, or <code>null</code> if
    * there is no associated label.
    * @param vals the values corresponding to each selection. These
    * are the possible values that may be returned by <code>getValue</code>.
    * The first value will be the initially selected value.
    * A {@code null} value may be used to indicate a non-selectable item.
    * @param items the items to be displayed for each selection.
    * Each item may be a String, an Icon, or a JSeparator.
    */
    public WListBox (String label, final T [] vals, Object [] items)
    {
        Event.checkIsEventDispatchThread ();
        if (items.length != vals.length)
        {
            throw new IllegalArgumentException (
                "vals & items have different sizes");
        }
        listBox = new JList (items);
        listBox.setSelectionMode (ListSelectionModel.SINGLE_SELECTION);
        JScrollPane scrollPane = new JScrollPane (listBox);
        initialize (scrollPane, label);
        values = vals.clone ();
        value = values.length == 0 ? null : values [0];
        setMaximumRowCount (5);

        lastIndex = 0;

        listBox.addListSelectionListener (new ListSelectionListener ()
            {
                public void valueChanged (ListSelectionEvent e)
                {
                    if (e.getValueIsAdjusting ())
                        return;
                    int index = listBox.getSelectedIndex ();
                    T v = index < 0 ? null : values [index];
                    if (v == null && index >= 0)
                    {
                        // Item is non-selectable (separator or heading).
                        // Search backwards or forwards for a selectable item.
                        // If the user is stepping through the items using
                        // the keyboard, we have to search for a selectable
                        // in the direction that the user is stepping.
                        int i = index;
                        if (i <= lastIndex)
                        {
                            while (i >= 0 && values [i] == null)
                                i--;
                            if (i < 0)
                                i = index;
                        }

                        while (i < values.length && values [i] == null)
                            i++;
                        if (i < values.length)
                        {
                            // Found a selectable item.
                            listBox.setSelectedIndex (i);
                            index = i;
                            v = values [index];
                        }
                    }
                    lastIndex = index;
                    if (!(v == null ? value == null : v.equals (value)))
                    {
                        value = v;
                        fireValueChanged (false);
                    }
                }
            });

        final ListCellRenderer renderer = listBox.getCellRenderer ();
        listBox.setCellRenderer (
            new ListCellRenderer ()
            {
                public Component getListCellRendererComponent (JList list,
                    Object value, int index, boolean isSelected,
                    boolean cellHasFocus)
                {
                    if (value instanceof JSeparator)
                    {
                        return (JSeparator) value;
                    }
                    Component component =
                        renderer.getListCellRendererComponent (
                            list, value, index, isSelected, cellHasFocus);
                    if (component instanceof JLabel)
                        ((JLabel) component).setHorizontalAlignment (alignment);
                    return component;
                }
            });
    }

    /**
    * Sets the maximum number of rows to be displayed.
    * If the number of items is greater than the maximum number of rows,
    * a scrollbar is used.
    * Scrolling may be inhibited by passing 0 for the number of rows.
    * @param rows the maximum number of rows, or 0 to inhibit scrolling.
    */
    public void setMaximumRowCount (int rows)
    {
        if (rows == 0)
            rows = values.length;
        listBox.setVisibleRowCount (rows);
    }

    /**
     * Sets the horizontal alignment of the selection text.
     * Valid parameter values are:
     * <ul>
     * <li><code>SwingConstants.LEFT</code>
     * <li><code>SwingConstants.CENTER</code>
     * <li><code>SwingConstants.RIGHT</code>
     * <li><code>SwingConstants.LEADING</code>
     * <li><code>SwingConstants.TRAILING</code>
     * </ul>
     * @param alignment the required alignment
     */
    @Override
    public void setAlignment (int alignment)
    {
        Event.checkIsEventDispatchThread ();
        this.alignment = alignment;
    }

    /**
    * Registers text to be displayed as tooltips for individual selections.
    * @param tooltips tooltip text array, in same order as the values that were
    * supplied to the <code>WListBox</code> constructor.
    */
    @Override
    public void setToolTipText (String [] tooltips)
    {
        Event.checkIsEventDispatchThread ();
        Gui.setToolTipText (listBox, tooltips);
    }

    /**
    * Gets the value of the component. This will be null if no item is
    * selected.
    * @return the value of the component.
    */
    public T getValue ()
    {
        return value;
    }

    /**
    * Sets the value of the component. If the value is null, no item will
    * be selected.
    * <p>This method does NOT fire a value event.
    * @param value the new value for the component.
    */
    public void setValue (T value)
    {
        Event.checkIsEventDispatchThread ();

        if (value == null)
        {
            // The order of the next two statements is important - we don't
            // want to fire a ValueEvent.
            this.value = null;
            listBox.clearSelection ();
            return;
        }

        for (int i = 0 ; i < values.length ; i++)
        {
            if (value.equals (values [i]))
            {
                // The order of the next two statements is important - we don't
                // want to fire a ValueEvent.
                this.value = value;
                listBox.setSelectedIndex (i);
                return;
            }
        }

        throw new IllegalArgumentException ("Unrecognized value: " + value);
    }
}

