/*
 * @(#)RadioPanel.java    2.3.3   2 May 2006
 *
 * Copyright 2006
 * College of Computer and Information Science
 * Northeastern University
 * Boston, MA  02115
 *
 * The Java Power Tools software may be used for educational
 * purposes as long as this copyright notice is retained intact
 * at the top of all source files.
 *
 * To discuss possible commercial use of this software, 
 * contact Richard Rasala at Northeastern University, 
 * College of Computer and Information Science,
 * 617-373-2462 or rasala@ccs.neu.edu.
 *
 * The Java Power Tools software has been designed and built
 * in collaboration with Viera Proulx and Jeff Raab.
 *
 * Should this software be modified, the words "Modified from 
 * Original" must be included as a comment below this notice.
 *
 * All publication rights are retained.  This software or its 
 * documentation may not be published in any media either
 * in whole or in part without explicit permission.
 *
 * This software was created with support from Northeastern 
 * University and from NSF grant DUE-9950829.
 */

package edu.neu.ccs.gui;

import edu.neu.ccs.*;
import edu.neu.ccs.quick.*;
import edu.neu.ccs.util.*;

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

/**
 * <P><CODE>RadioPanel</CODE> implements the base panel
 * for matching strings and radio buttons and maintaining the
 * mutual exclusiveness of button selection.<P>
 *
 * @author  Richard Rasala
 * @version 2.3.3
 * @since   2.3
 */
public class RadioPanel extends JPanel 
    implements Displayable, JPTConstants
{
    /** Bound property name for the option count property. */
    public static final String OPTION_COUNT    = "option.count";
    
    
    /** Bound property name for the selected index property. */
    public static final String SELECTED_INDEX  = "selected.index";
    
    
    /** Bound property name for the selected index property. */
    public static final String SELECTED_LABEL  = "selected.label";
    
    
    /** Bound property name for the selected index property. */
    public static final String SELECTED_BUTTON = "selected.button";
    
    
    /** Bound property name for the set label text property. */
    public static final String SET_LABEL_TEXT  = "set.label.text";
    
    
    /** Bound property name for the set background property. */
    public static final String BACKGROUND = "set.background";
    
    
    /** Default initial selection for an options view. */
    protected static final int DEFAULT_SELECTION = 0;
    
    
    /** The default view state. */
    protected String defaultViewState = "";
    
    
    /**
     * The string-object map that contains pairs with a label and
     * its associated radio button.
     */
    protected StringObjectMap labelButtonMap = new StringObjectMap();
    
    
    /**
     * The hash map that contains pairs with a label and an index
     * installed as <CODE>Integer</CODE> with the position of the
     * label in the view.
     */
    protected QuickHashMap labelIndexMap = new QuickHashMap();
    
    
    /** The hash map that contains pairs with a model and its button. */
    protected QuickHashMap modelButtonMap = new QuickHashMap();
    
    
    /** Button buttonGroup to enforce mutual exclusion for selections. */
    protected ButtonGroup buttonGroup = new ButtonGroup();
    
    
    /** The default layout. */
    protected TableLayout defaultLayout = new TableLayout(1, 1, 0, 0, WEST, VERTICAL);
    
    
    /** The common actions for all buttons. */
    protected ActionSequence commonactions = new ActionSequence();
    
    
    //////////////////
    // Constructors //
    //////////////////
    
    /**
     * <P>The default constructor.</P>
     *
     * <p>Other constructors:</p>
     *
     * <ul><code>
     *   <li>RadioPanel(LayoutManager)</li>
     *   <li>RadioPanel(String[])</li>
     *   <li>RadioPanel(String[], LayoutManager)</li>
     *   <li>RadioPanel(String[], int)</li>
     *   <li>RadioPanel(String[], int, LayoutManager)</li>
     *   <li>RadioPanel(String[], ActionListener)</li>
     *   <li>RadioPanel(String[], ActionListener, LayoutManager)</li>
     *   <li>RadioPanel(String[], ActionListener, int)</li>
     *   <li>RadioPanel(String[], ActionListener, int, LayoutManager)</li>
     *   <li>RadioPanel(String[], ActionListener[])</li>
     *   <li>RadioPanel(String[], ActionListener[], LayoutManager)</li>
     *   <li>RadioPanel(String[], ActionListener[], int)</li>
     *   <li>RadioPanel(String[], ActionListener[], int, LayoutManager)</li>
     * </code></ul>
     */
    public RadioPanel()
    {
        initializeRadioPanel(null, DEFAULT_SELECTION, null);
    }
    
    
    /**
     * <P>Constructs a panel with the given layout manager.</P>
     *
     * <P>If the given layout manager is <CODE>null</CODE>, then a
     * <CODE>TableLayout</CODE> with <CODE>WEST</CODE> alignment
     * and <CODE>VERTICAL</CODE> orientation will be used.</P>
     *
     * @param layout    the layout manager for the panel
     * @see #RadioPanel()
     */
    public RadioPanel(LayoutManager layout)
    {
        initializeRadioPanel(null, DEFAULT_SELECTION, layout);
    }
    
    
    /**
     * <P>Constructs a panel containing radio buttons labeled 
     * with the given array of strings.</P>
     *
     * <P>The strings in the array of strings should be distinct
     * as duplicate strings will be ignored.</P>
     *
     * <P>If the strings array is <CODE>null</CODE> or of length
     * zero, the panel will be initialized with no buttons.</P>
     *
     * @param strings   the array of labels for the radio buttons
     * @see #RadioPanel()
     */
    public RadioPanel(String[] strings)
    {
        initializeRadioPanel(strings, DEFAULT_SELECTION, null);
    }
    
    
    /**
     * <P>Constructs a panel containing radio buttons labeled 
     * with the given array of strings;
     * the given layout manager is used to layout the buttons.</P>
     *
     * <P>The strings in the array of strings should be distinct
     * as duplicate strings will be ignored.</P>
     *
     * <P>If the strings array is <CODE>null</CODE> or of length
     * zero, the panel will be initialized with no buttons.</P>
     *
     * <P>If the given layout manager is <CODE>null</CODE>, then a
     * <CODE>TableLayout</CODE> with <CODE>WEST</CODE> alignment
     * and <CODE>VERTICAL</CODE> orientation will be used.</P>
     *
     * @param strings   the array of labels for the radio buttons
     * @param layout    the layout manager for the panel
     * @see #RadioPanel()
     * @since 2.3.3
     */
    public RadioPanel(String[] strings, LayoutManager layout)
    {
        initializeRadioPanel(strings, DEFAULT_SELECTION, layout);
    }
    
    
    /**
     * <P>Constructs a panel containing radio buttons labeled 
     * with the given array of strings;
     * the given selection is the index of the button that is
     * initially selected.</P>
     *
     * <P>The strings in the array of strings should be distinct
     * as duplicate strings will be ignored.</P>
     *
     * <P>If the initial selection index is invalid and if there
     * are buttons in the panel then the selection index will be
     * set to 0.</P>
     *
     * <P>If the strings array is <CODE>null</CODE> or of length
     * zero, the panel will be initialized with no buttons.</P>
     *
     * @param strings   the array of labels for the radio buttons
     * @param selection the index of the button selected by default
     * @see #RadioPanel()
     */
    public RadioPanel(String[] strings, int selection)
    {
        initializeRadioPanel(strings, selection, null);
    }
    
    
    /**
     * <P>Constructs a panel containing radio buttons labeled 
     * with the given array of strings;
     * the given selection is the index of the button that is
     * initially selected;
     * the given layout manager is used to layout the buttons.</P>
     *
     * <P>The strings in the array of strings should be distinct
     * as duplicate strings will be ignored.</P>
     *
     * <P>If the initial selection index is invalid and if there
     * are buttons in the panel then the selection index will be
     * set to 0.</P>
     *
     * <P>If the strings array is <CODE>null</CODE> or of length
     * zero, the panel will be initialized with no buttons.</P>
     *
     * <P>If the given layout manager is <CODE>null</CODE>, then a
     * <CODE>TableLayout</CODE> with <CODE>WEST</CODE> alignment
     * and <CODE>VERTICAL</CODE> orientation will be used.</P>
     *
     * @param strings   the array of labels for the radio buttons
     * @param selection the index of the button selected by default
     * @param layout    the layout manager for the panel
     * @see #RadioPanel()
     */
    public RadioPanel(String[] strings, int selection, LayoutManager layout)
    {
        initializeRadioPanel(strings, selection, layout);
    }
    
    
    /**
     * <P>Constructs a panel containing radio buttons labeled 
     * with the given array of strings;
     * the given action listener becomes a listener for all buttons.</P>
     *
     * <P>The strings in the array of strings should be distinct
     * as duplicate strings will be ignored.</P>
     *
     * <P>If the strings array is <CODE>null</CODE> or of length
     * zero, the panel will be initialized with no buttons.</P>
     *
     * @param strings   the array of labels for the radio buttons
     * @param action    the action listener that will listen for selection
     * @see #RadioPanel()
     */
    public RadioPanel(String[] strings, ActionListener action)
    {
        addActionListener(action);
        initializeRadioPanel(strings, DEFAULT_SELECTION, null);
    }
    
    
    /**
     * <P>Constructs a panel containing radio buttons labeled 
     * with the given array of strings;
     * the given action listener becomes a listener for all buttons;
     * the given layout manager is used to layout the buttons.</P>
     *
     * <P>The strings in the array of strings should be distinct
     * as duplicate strings will be ignored.</P>
     *
     * <P>If the strings array is <CODE>null</CODE> or of length
     * zero, the panel will be initialized with no buttons.</P>
     *
     * <P>If the given layout manager is <CODE>null</CODE>, then a
     * <CODE>TableLayout</CODE> with <CODE>WEST</CODE> alignment
     * and <CODE>VERTICAL</CODE> orientation will be used.</P>
     *
     * @param strings   the array of labels for the radio buttons
     * @param action    the action listener that will listen for selection
     * @param layout    the layout manager for the panel
     * @see #RadioPanel()
     * @since 2.3.3
     */
    public RadioPanel(String[] strings, ActionListener action, LayoutManager layout)
    {
        addActionListener(action);
        initializeRadioPanel(strings, DEFAULT_SELECTION, layout);
    }
    
    
    /**
     * <P>Constructs a panel containing radio buttons labeled 
     * with the given array of strings;
     * the given action listener becomes a listener for all buttons;
     * the given selection is the index of the button that is
     * initially selected.</P>
     *
     * <P>The strings in the array of strings should be distinct
     * as duplicate strings will be ignored.</P>
     *
     * <P>If the initial selection index is invalid and if there
     * are buttons in the panel then the selection index will be
     * set to 0.</P>
     *
     * <P>If the strings array is <CODE>null</CODE> or of length
     * zero, the panel will be initialized with no buttons.</P>
     *
     * @param strings   the array of labels for the radio buttons
     * @param action    the action listener that will listen for selection
     * @param selection the index of the button selected by default
     * @see #RadioPanel()
     */
    public RadioPanel(String[] strings, ActionListener action, int selection)
    {
        addActionListener(action);
        initializeRadioPanel(strings, selection, null);
    }
    
    
    /**
     * <P>Constructs a panel containing radio buttons labeled 
     * with the given array of strings;
     * the given action listener becomes a listener for all buttons;
     * the given selection is the index of the button that is
     * initially selected;
     * the given layout manager is used to layout the buttons.</P>
     *
     * <P>The strings in the array of strings should be distinct
     * as duplicate strings will be ignored.</P>
     *
     * <P>If the initial selection index is invalid and if there
     * are buttons in the panel then the selection index will be
     * set to 0.</P>
     *
     * <P>If the strings array is <CODE>null</CODE> or of length
     * zero, the panel will be initialized with no buttons.</P>
     *
     * <P>If the given layout manager is <CODE>null</CODE>, then a
     * <CODE>TableLayout</CODE> with <CODE>WEST</CODE> alignment
     * and <CODE>VERTICAL</CODE> orientation will be used.</P>
     *
     * @param strings   the array of labels for the radio buttons
     * @param action    the action listener that will listen for selection
     * @param selection the index of the button selected by default
     * @param layout    the layout manager for the panel
     * @see #RadioPanel()
     */
    public RadioPanel(
        String[]       strings,
        ActionListener action,
        int            selection,
        LayoutManager  layout)
    {
        addActionListener(action);
        initializeRadioPanel(strings, selection, layout);
    }
    
    
    /**
     * <P>Constructs a panel containing radio buttons labeled 
     * with the given array of strings;
     * the given action listeners become listeners for the
     * corresponding buttons.</P>
     *
     * <P>The strings in the array of strings should be distinct
     * as duplicate strings will be ignored.</P>
     *
     * <P>If the strings array is <CODE>null</CODE> or of length
     * zero, the panel will be initialized with no buttons.</P>
     *
     * @param strings   the array of labels for the radio buttons
     * @param actions   the action listeners that will listen for selection
     * @see #RadioPanel()
     */
    public RadioPanel(String[] strings, ActionListener[] actions)
    {
        initializeRadioPanel(strings, DEFAULT_SELECTION, null);
        addActionListeners(actions);
    }
    
    
    /**
     * <P>Constructs a panel containing radio buttons labeled 
     * with the given array of strings;
     * the given action listeners become listeners for the
     * corresponding buttons.</P>
     * the given layout manager is used to layout the buttons.</P>
     *
     * <P>The strings in the array of strings should be distinct
     * as duplicate strings will be ignored.</P>
     *
     * <P>If the strings array is <CODE>null</CODE> or of length
     * zero, the panel will be initialized with no buttons.</P>
     *
     * <P>If the given layout manager is <CODE>null</CODE>, then a
     * <CODE>TableLayout</CODE> with <CODE>WEST</CODE> alignment
     * and <CODE>VERTICAL</CODE> orientation will be used.</P>
     *
     * @param strings   the array of labels for the radio buttons
     * @param actions   the action listeners that will listen for selection
     * @param layout    the layout manager for the panel
     * @see #RadioPanel()
     * @since 2.3.3
     */
    public RadioPanel(String[] strings, ActionListener[] actions, LayoutManager layout)
    {
        initializeRadioPanel(strings, DEFAULT_SELECTION, layout);
        addActionListeners(actions);
    }
    
    
    /**
     * <P>Constructs a panel containing radio buttons labeled 
     * with the given array of strings;
     * the given action listeners become listeners for the
     * corresponding buttons.</P>
     * the given selection is the index of the button that is
     * initially selected.</P>
     *
     * <P>The strings in the array of strings should be distinct
     * as duplicate strings will be ignored.</P>
     *
     * <P>If the initial selection index is invalid and if there
     * are buttons in the panel then the selection index will be
     * set to 0.</P>
     *
     * <P>If the strings array is <CODE>null</CODE> or of length
     * zero, the panel will be initialized with no buttons.</P>
     *
     * @param strings   the array of labels for the radio buttons
     * @param actions   the action listeners that will listen for selection
     * @param selection the index of the button selected by default
     * @see #RadioPanel()
     */
    public RadioPanel(String[] strings, ActionListener[] actions, int selection)
    {
        initializeRadioPanel(strings, selection, null);
        addActionListeners(actions);
    }
    
    
    /**
     * <P>Constructs a panel containing radio buttons labeled 
     * with the given array of strings;
     * the given action listeners become listeners for the
     * corresponding buttons.</P>
     * the given selection is the index of the button that is
     * initially selected;
     * the given layout manager is used to layout the buttons.</P>
     *
     * <P>The strings in the array of strings should be distinct
     * as duplicate strings will be ignored.</P>
     *
     * <P>If the initial selection index is invalid and if there
     * are buttons in the panel then the selection index will be
     * set to 0.</P>
     *
     * <P>If the strings array is <CODE>null</CODE> or of length
     * zero, the panel will be initialized with no buttons.</P>
     *
     * <P>If the given layout manager is <CODE>null</CODE>, then a
     * <CODE>TableLayout</CODE> with <CODE>WEST</CODE> alignment
     * and <CODE>VERTICAL</CODE> orientation will be used.</P>
     *
     * @param strings   the array of labels for the radio buttons
     * @param actions   the action listeners that will listen for selection
     * @param selection the index of the button selected by default
     * @param layout    the layout manager for the panel
     * @see #RadioPanel()
     */
    public RadioPanel(
        String[]         strings,
        ActionListener[] actions,
        int              selection,
        LayoutManager    layout)
    {
        initializeRadioPanel(strings, selection, layout);
        addActionListeners(actions);
    }
    
    
    /**
     * Performs the common initialization code for this class.
     *
     * @param strings   the array of labels for the radio buttons
     * @param selection the index of the button selected by default
     * @param layout    the layout manager for the panel
     */
    protected final void initializeRadioPanel(
        String[]      strings,
        int           selection,
        LayoutManager layout)
    {
        super.setFont(getDefaultFont());
        
        if (layout == null)
            layout = defaultLayout;
        
        setLayout(layout);
        
        addOptions(strings);
        
        setSelectedIndex(selection);
        
        setDefaultViewState(getViewState());
    }
    
    
    ////////////////
    // Public API //
    ////////////////
    
    /**
     * Returns the index of the option with the given label
     * or -1 if the label is not in the view.
     *
     * @return the index corresponding to the label
     * @see #getIndex(JRadioButton)
     * @see #getLabel(int)
     * @see #getLabel(JRadioButton)
     * @see #getOptionButton(int)
     * @see #getOptionButton(String)
     * @see #getOptionButton(ButtonModel)
     * @see #getButtonModel(JRadioButton)
     */
    public final int getIndex(String label) {
        if (label == null)
            return -1;
        
        if (! labelButtonMap.containsString(label))
            return -1;
        
        Integer index = (Integer) labelIndexMap.get(label);
        
        return index.intValue();
    }
    
    
    /**
     * Returns the index of the option with the given button
     * or -1 if the button is not in the view.
     *
     * @return the index corresponding to the label
     * @see #getIndex(String)
     * @see #getLabel(int)
     * @see #getLabel(JRadioButton)
     * @see #getOptionButton(int)
     * @see #getOptionButton(String)
     * @see #getOptionButton(ButtonModel)
     * @see #getButtonModel(JRadioButton)
     */
    public final int getIndex(JRadioButton button) {
        return getIndex(getLabel(button));
    }
    
    
    /**
     * Returns the label of the option at the given index
     * or <CODE>null</CODE> if the index is out of bounds.
     *
     * @return the label corresponding to the index
     * @see #getIndex(String)
     * @see #getIndex(JRadioButton)
     * @see #getLabel(JRadioButton)
     * @see #getOptionButton(int)
     * @see #getOptionButton(String)
     * @see #getOptionButton(ButtonModel)
     * @see #getButtonModel(JRadioButton)
     */
    public final String getLabel(int index) {
        return labelButtonMap.getString(index);
    }
    
    
    /**
     * Returns the label of the given button
     * or <CODE>null</CODE> if the button is <CODE>null</CODE>
     * or is not in the view.
     *
     * @return the label corresponding to the index
     * @see #getIndex(String)
     * @see #getIndex(JRadioButton)
     * @see #getLabel(int)
     * @see #getOptionButton(int)
     * @see #getOptionButton(String)
     * @see #getOptionButton(ButtonModel)
     * @see #getButtonModel(JRadioButton)
     */
    public final String getLabel(JRadioButton button) {
        if (button == null)
            return null;
        
        if (! labelButtonMap.containsObject(button))
            return null;
        
        return button.getText();
    }
    
    
    /**
     * Returns the <CODE>JRadioButton</CODE> at the given index
     * or returns <CODE>null</CODE> if the index is out of bounds.
     *
     * @param index the index of the desired button
     * @see #getIndex(String)
     * @see #getIndex(JRadioButton)
     * @see #getLabel(int)
     * @see #getLabel(JRadioButton)
     * @see #getOptionButton(String)
     * @see #getOptionButton(ButtonModel)
     * @see #getButtonModel(JRadioButton)
     */
    public final JRadioButton getOptionButton(int index) {
        return (JRadioButton) labelButtonMap.getObject(index);
    }
    
    
    /**
     * Returns the <CODE>JRadioButton</CODE> corresponding to the
     * given label
     * or returns <CODE>null</CODE> if the label is <CODE>null</CODE>
     * or if the label does not correspond to a button in the view.
     *
     * @param label the label of the desired button
     * @see #getIndex(String)
     * @see #getIndex(JRadioButton)
     * @see #getLabel(int)
     * @see #getLabel(JRadioButton)
     * @see #getOptionButton(int)
     * @see #getOptionButton(ButtonModel)
     * @see #getButtonModel(JRadioButton)
     */
    public final JRadioButton getOptionButton(String label) {
        return (JRadioButton) labelButtonMap.getObject(label);
    }
    
    
    /**
     * Returns the <CODE>JRadioButton</CODE> of the button model or
     * returns <CODE>null</CODE> if the model is <CODE>null</CODE>
     * or if the model does not correspond to a button in the view.
     *
     * @param model the button model of the desired button
     * @see #getIndex(String)
     * @see #getIndex(JRadioButton)
     * @see #getLabel(int)
     * @see #getLabel(JRadioButton)
     * @see #getOptionButton(int)
     * @see #getOptionButton(String)
     * @see #getButtonModel(JRadioButton)
     */
    protected final JRadioButton getOptionButton(ButtonModel model) {
        return (JRadioButton) modelButtonMap.get(model);
    }
    
    
    /**
     * Returns the <CODE>ButtonModel</CODE> of the radio button or
     * returns <CODE>null</CODE> if the button is <CODE>null</CODE>
     * or is not in the view.
     *
     * @param button the radio button
     * @see #getIndex(String)
     * @see #getIndex(JRadioButton)
     * @see #getLabel(int)
     * @see #getLabel(JRadioButton)
     * @see #getOptionButton(int)
     * @see #getOptionButton(String)
     * @see #getOptionButton(ButtonModel)
     */
    protected final ButtonModel getButtonModel(JRadioButton button) {
        if (button == null)
            return null;
        
        if (! labelButtonMap.containsObject(button))
            return null;
        
        return button.getModel();
    }
    
    
    /**
     * <P>Selects the button at the given index.</P>
     *
     * <P>Does nothing if the index is out of bounds or is
     * equal to the current selected index.</P>
     *
     * @param index the index of the button to be selected
     * @return whether or not the selection was changed
     * @see #getSelectedIndex()
     * @see #setSelectedLabel(String)
     * @see #getSelectedLabel()
     * @see #setSelectedButton(JRadioButton)
     * @see #getSelectedButton()
     */
    public final boolean setSelectedIndex(int index) {
        if (getOptionCount() == 0)
            return false;
        
        int oldIndex = getSelectedIndex();
        
        if (index == oldIndex)
            return false;
        
        ButtonModel model = getButtonModel(getOptionButton(index));
        
        if (model == null)
            return false;
    
        buttonGroup.setSelected(model, true);
        repaint();
        
        firePropertyChange(SELECTED_INDEX, oldIndex, index);
        
        return true;
    }
    
    
    /**
     * Returns the index of the selected button,
     * or -1 if there are no buttons.
     *
     * @see #setSelectedIndex(int)
     * @see #setSelectedLabel(String)
     * @see #getSelectedLabel()
     * @see #setSelectedButton(JRadioButton)
     * @see #getSelectedButton()
     */
    public final int getSelectedIndex() {
        return getIndex(getSelectedButton());
    }
    
    
    /**
     * <P>Selects the button corresponding to the label.</P> 
     *
     * <P>This method does not change the current selection 
     * if the label corresponds to the current selection
     * or is not associated with a button.</P>
     *
     * @param  label the label of the option to be selected
     * @return whether or not the selection was changed
     * @see #setSelectedIndex(int)
     * @see #getSelectedIndex()
     * @see #getSelectedLabel()
     * @see #setSelectedButton(JRadioButton)
     * @see #getSelectedButton()
     */
    public final boolean setSelectedLabel(String label) {
        if (getOptionCount() == 0)
            return false;
        
        if (label == null)
            return false;
        
        String oldLabel = getSelectedLabel();
        
        if (label.equals(oldLabel))
            return false;
        
        ButtonModel model = getButtonModel(getOptionButton(label));
        
        if (model == null)
            return false;
        
        buttonGroup.setSelected(model, true);
        repaint();
        
        firePropertyChange(SELECTED_LABEL, oldLabel, label);
        
        return true;
    }
    
    
    /**
     * Returns the label of the currently selected button
     * or <CODE>null</CODE> if there are no buttons.
     *
     * @see #setSelectedIndex(int)
     * @see #getSelectedIndex()
     * @see #setSelectedLabel(String)
     * @see #setSelectedButton(JRadioButton)
     * @see #getSelectedButton()
     */
    public final String getSelectedLabel() {
        return getLabel(getSelectedButton());
    }
    
    
    /**
     * <P>Selects the given button.</P> 
     *
     * <P>This method does not change the current selection 
     * if the button is already the selected button
     * or is not in the view.</P>
     *
     * @param  button the button to be selected
     * @return whether or not the selection was changed
     * @see #setSelectedIndex(int)
     * @see #getSelectedIndex()
     * @see #setSelectedLabel(String)
     * @see #getSelectedLabel()
     * @see #getSelectedButton()
     */
    public final boolean setSelectedButton(JRadioButton button) {
        if (getOptionCount() == 0)
            return false;
        
        if (button == null)
            return false;
        
        JRadioButton oldButton = getSelectedButton();
        
        if (button == oldButton)
            return false;
        
        ButtonModel model = getButtonModel(button);
        
        if (model == null)
            return false;
        
        buttonGroup.setSelected(model, true);
        repaint();
        
        firePropertyChange(SELECTED_BUTTON, oldButton, button);
        
        return true;
    }
    
    
    /**
     * Returns the currently selected button
     * or <CODE>null</CODE> if there are no buttons.
     *
     * @return the currently selected button
     * @see #setSelectedIndex(int)
     * @see #getSelectedIndex()
     * @see #setSelectedLabel(String)
     * @see #getSelectedLabel()
     * @see #setSelectedButton(JRadioButton)
     */
    public final JRadioButton getSelectedButton() {
        if (getOptionCount() == 0)
            return null;
        
        return getOptionButton(buttonGroup.getSelection());
    }
    
    
    /**
     * <P>Sets the label for the button at the given index 
     * to the given label.</P>
     *
     * <P>Does nothing if:</P>
     *
     * <UL>
     *   <LI>The index is invalid.</LI>
     *   <LI>The label is <CODE>null</CODE>.</LI>
     *   <LI>The label already occurs as a button label.</LI>
     * </UL>
     *
     * @param index the index of the button 
     * @param label the new label for the button
     * @return whether or not the operation was successful
     * @see #getLabelText(int)
     */
    public final boolean setLabelText(int index, String label) {
        if ((index < 0) || (index >= getOptionCount()))
            return false;
    
        if (label == null)
            return false;
        
        // remove the label-button pair at the index
        StringObjectPair pair = labelButtonMap.remove(index);
        
        String string = pair.getString();
        JRadioButton button = (JRadioButton) pair.getObject();
        
        // check to see if the label occurs in the rest of the map
        // if so revert to the original map and exit
        if (labelButtonMap.containsString(label)) {
            labelButtonMap.addPair(index, pair);
            return false;
        }
        
        // remove the label-index pair
        labelIndexMap.remove(string);
        
        // install the replacement entries in the maps
        labelButtonMap.addPair(index, label, button);
        labelIndexMap.put(label, new Integer(index));
        
        // change the button text
        button.setText(label);
        refreshComponent();
        
        firePropertyChange(SET_LABEL_TEXT, string, label);
        
        return true;
    }
    
    
    /**
     * <P>Returns the label text for the button at the given index,
     * or <CODE>null</CODE> if the given index is invalid.<P>
     *
     * <P>Equivalent to <CODE>getLabel(index)</CODE> but retained
     * for compatibility with older code.</P>
     *
     * @param index the index of the option 
     *      whose label is to be returned
     * @see #setLabelText(int, String)
     */
    public final String getLabelText(int index) {
        return getLabel(index);
    }
    
    
    /**
     * <p>Adds a button to the end of the view with the given label.</p>
     *
     * <p>Refreshes the component.</p>
     *
     * @param label the label for the button
     * @see #addOption(String, Action)
     * @see #addOption(String, boolean)
     * @see #addOption(String, Action, boolean)
     * @see #addOptions(String[])
     * @see #addOptions(String[], Action)
     * @see #addOptions(String[], Action[])
     */
    public final void addOption(String label) {
        addOption(label, null, false);
    }
    
    
    /**
     * <p>Adds a button to the end of the view with the given label
     * and the given action.<p>
     *
     * <p>Refreshes the component.</p>
     *
     * @param label the label for the button
     * @param action the action to perform if the option is pressed
     * @see #addOption(String)
     * @see #addOption(String, boolean)
     * @see #addOption(String, Action, boolean)
     * @see #addOptions(String[])
     * @see #addOptions(String[], Action)
     * @see #addOptions(String[], Action[])
     */
    public final void addOption(String label, Action action) {
        addOption(label, action, false);
    }
    
    
    /**
     * <p>Adds a button to the end of the view with the given label
     * and the given selection state.</p>
     *
     * <p>Refreshes the component.</p>
     *
     * @param label the label for the button
     * @param selected whether or not the option should be selected
     * @see #addOption(String)
     * @see #addOption(String, Action)
     * @see #addOption(String, Action, boolean)
     * @see #addOptions(String[])
     * @see #addOptions(String[], Action)
     * @see #addOptions(String[], Action[])
     */
    public final void addOption(String label, boolean selected) {
        addOption(label, null, selected);
    }
    
    
    /**
     * <p>Adds a button to the end of the view with the given label,
     * the given action, and the given selection state.</p>
     *
     * <p>Refreshes the component.</p>
     *
     * @param label the label for the button
     * @param action the action to perform if the option is pressed
     * @param selected whether or not the option should be selected
     * @see #addOption(String)
     * @see #addOption(String, Action)
     * @see #addOption(String, boolean)
     * @see #addOptions(String[])
     * @see #addOptions(String[], Action)
     * @see #addOptions(String[], Action[])
     */
    public final void addOption(String label, Action action, boolean selected) {
        // null check on label
        if (label == null)
            return;
        
        // check if the label string is already present
        if (labelButtonMap.containsString(label))
            return;
        
        // find next index and update selection status if needed
        int index = labelButtonMap.size();
        
        if (index == 0) {
            selected = true;
            setDefaultViewState(label);
        }
        
        // create the new radio button for this option
        JRadioButton button = new JRadioButton(label);
        button.setFont(getFont());
        
        // get the button model
        ButtonModel model  = button.getModel();
        
        // store the two-way label-button pair
        labelButtonMap.addPair(label, button);
        
        // store the label-index pair
        labelIndexMap. put(label, new Integer(index));
        
        // store the model-button pair
        modelButtonMap.put(model, button);
        
        // add the action listeners
        button.addActionListener(commonactions);
        
        if (action != null)
            button.addActionListener(action);
        
        // add the button to the mutually exclusive button buttonGroup
        buttonGroup.add(button);
        buttonGroup.setSelected(model, selected);
        
        // add the button to the panel and refresh
        add(button);
        refreshComponent();
        
        // notify listeners of property change
        firePropertyChange(OPTION_COUNT, index , index + 1);
    }
    
    
    /**
     * <p>Add the options in succession to the end of the view.</p>
     *
     * <p>Refreshes the component.</p>
     *
     * @param labels the label text for the various options
     * @see #addOption(String)
     * @see #addOption(String, Action)
     * @see #addOption(String, boolean)
     * @see #addOption(String, Action, boolean)
     * @see #addOptions(String[], Action)
     * @see #addOptions(String[], Action[])
     */
    public final void addOptions(String[] labels) {
        if (labels == null)
            return;
        
        for (int i = 0; i < labels.length; i++)
            addOption(labels[i]);
    }
    
    
    /**
     * <p>Add the options in succession to the end of the view
     * with the same action for each option.</p>
     *
     * <p>Refreshes the component.</p>
     *
     * @param labels the label text for the various options
     * @param action the action to pair with each option
     * @see #addOption(String)
     * @see #addOption(String, Action)
     * @see #addOption(String, boolean)
     * @see #addOption(String, Action, boolean)
     * @see #addOptions(String[])
     * @see #addOptions(String[], Action[])
     */
    public final void addOptions(String[] labels, Action action) {
        if (labels == null)
            return;
        
        for (int i = 0; i < labels.length; i++)
            addOption(labels[i], action);
    }
    
    
    /**
     * <p>Add the options in succession to the end of the view
     * with the each option paired with a corresponding action.</p>
     *
     * <p>Refreshes the component.</p>
     *
     * @param labels the label text for the various options
     * @param actions the actions to pair with the options
     * @see #addOption(String)
     * @see #addOption(String, Action)
     * @see #addOption(String, boolean)
     * @see #addOption(String, Action, boolean)
     * @see #addOptions(String[])
     * @see #addOptions(String[], Action)
     */
    public final void addOptions(String[] labels, Action[] actions) {
        if (labels == null)
            return;
        
        int actionslength = (actions != null) ? actions.length : 0;
        
        // add the various options
        for (int i = 0; i < labels.length; i++)
            if (i < actionslength)
                addOption(labels[i], actions[i]);
            else
                addOption(labels[i]);
    }
    
    
    /**
     * Returns the number of options for this view.
     */
    public final int getOptionCount() {
        return labelButtonMap.size();
    }
    
    
    /**
     * Returns an array containing the radio buttons for this view.
     *
     * @see #getOptionButton(int)
     */
    public final JRadioButton[] getOptionButtons() {
        // avoid errors during super class initialization
        if (labelButtonMap == null)
            return new JRadioButton[0];
        
        Object[] objects = labelButtonMap.getObjects();
        
        int length = objects.length;
        
        JRadioButton[] buttons = new JRadioButton[length];
        
        for (int i = 0; i < length; i++)
            buttons[i] = (JRadioButton) objects[i];
        
        return buttons;
    }
    
    
    /**
     * <P>Adds the given action listener to all buttons.</P>
     *
     * <P>Does nothing if the action listener is <CODE>null</CODE>.</P>
     *
     * @param listener the action listener to add to all buttons
     */
    public final void addActionListener(ActionListener listener) {
        if (listener == null)
            return;
        
        commonactions.add(listener);
    }
    
    
    /**
     * <P>Adds the given action listener to the button at the given
     * index.</P>
     *
     * <P>Does nothing if the index is out of bounds
     * or the action listener is <CODE>null</CODE>.</P>
     *
     * @param index the index of the button
     * @param listener the action listener to add to the button
     */
    public final void addActionListener(int index, ActionListener listener) {
        if (listener == null)
            return;
        
        if ((index < 0) || (index >= getOptionCount()))
            return;
        
        getOptionButton(index).addActionListener(listener);
    }
    
    
    /**
     * <P>Adds the given action listeners to the corresponding buttons.</P>
     *
     * <P>Does nothing if the action listeners array is <CODE>null</CODE>.</P>
     *
     * @param listeners the action listeners to add to the corresponding buttons
     */
    public final void addActionListeners(ActionListener[] listeners) {
        if (listeners == null)
            return;
        
        int length = Math.min(getOptionCount(), listeners.length);
        
        for (int i = 0; i < length; i++)
            addActionListener(i, listeners[i]);
    }
    
    
    /**
     * <P>Removes the given action listener from all buttons.</P>
     *
     * <P>Does nothing if the action listener is <CODE>null</CODE>.</P>
     *
     * @param listener the action to remove from all buttons
     */
    public final void removeActionListener(ActionListener listener) {
        if (listener == null)
            return;
        
        commonactions.remove(listener);
    }
    
    
    /**
     * <P>Removes the given action listener from the button at the
     * given index.</P>
     *
     * <P>Does nothing if the index is out of bounds
     * or the action listener is <CODE>null</CODE>.</P>
     *
     * @param index the index of the button
     * @param listener the listener to remove from the button
     */
    public final void removeActionListener(int index, ActionListener listener) {
        if (listener == null)
            return;
        
        if ((index < 0) || (index >= getOptionCount()))
            return;
        
        getOptionButton(index).removeActionListener(listener);
    }
    
    
    /**
     * <P>Removes the given action listeners from the corresponding buttons.</P>
     *
     * <P>Does nothing if the action listeners array is <CODE>null</CODE>.</P>
     *
     * @param listeners the action listeners to remove from the corresponding buttons
     */
    public final void removeActionListeners(ActionListener[] listeners) {
        if (listeners == null)
            return;
        
        int length = Math.min(getOptionCount(), listeners.length);
        
        for (int i = 0; i < length; i++)
            removeActionListener(i, listeners[i]);
    }
    
    
    /**
     * <p>Sets the font for this component.</p>
     *
     * <p>If the given font is <code>null</code> then it is set to
     * <code>getDefaultFont()</code>.</p>
     *
     * <p>Calls the super <code>setFont</code> method and
     * then sets the font for each radio button.</p>
     *
     * <p>Refreshes the component.</p>
     *
     * <p>Fires property change FONT.</p>
     *
     * @param font the font to set for this view
     * @since 2.3.3
     */
    public void setFont(Font font) {
        // avoid errors during super class initialization
        if (labelButtonMap == null) {
            super.setFont(font);
            return;
        }
        
        if (font == null)
            font = getDefaultFont();
    
        super.setFont(font);
        
        JRadioButton[] buttons = getOptionButtons();
        
        int length = buttons.length;
        
        if (length > 0) {
            for (int i = 0; i < length; i++)
                buttons[i].setFont(font);
        }
        
        refreshComponent();
        
        // notify listeners of property change
        firePropertyChange(FONT, null, null);
    }
    
    
    /**
     * Returns the default font for a JRadioButton.
     *
     * @since 2.3.3
     */
    public static Font getDefaultFont() {
        return new JRadioButton(" ").getFont();
    }
    
    
    /**
     * <p>Sets the background for this component and its buttons.</p>
     *
     * <p>If the given color is <code>null</code> then sets the given
     * color to <code>getDefaultBackground()</code>.</p>
     *
     * <p>Calls the super <code>setBackground</code> method and
     * then sets the background for each radio button.</p>
     *
     * <p>Fires property change BACKGROUND.</p>
     *
     * @param color the background to set for this component
     * @since 2.3.3
     */
    public void setBackground(Color color) {
        // avoid errors during super class initialization
        if (labelButtonMap == null) {
            super.setBackground(color);
            return;
        }
        
        if (color == null)
            color = getDefaultBackground();
    
        super.setBackground(color);
        
        JRadioButton[] buttons = getOptionButtons();
        
        int length = buttons.length;
        
        if (length > 0) {
            for (int i = 0; i < length; i++)
                buttons[i].setBackground(color);
        }
        
        repaint();
        
        // notify listeners of property change
        firePropertyChange(BACKGROUND, null, null);
    }
    
    
    /**
     * Returns the default background for a JRadioButton.
     *
     * @since 2.3.3
     */
    public static Color getDefaultBackground() {
        return new JRadioButton(" ").getBackground();
    }
    
    /////////////////
    // Displayable //
    /////////////////

    /**
     * <P>Sets the currently selected button to the button whose string
     * is the given state.</P>
     *
     * <P>If the given state does not correspond to a button, then does
     * nothing.</P>
     *
     * <P>This method may be overridden in a derived class if the class
     * does not consider the view state of the view to be equivalent to
     * the button label of the selected button.</P>
     *
     * @param state the string to select the button 
     * @see #getViewState()
     * @see Displayable
     */
    public void setViewState(String state) {
        String oldState = getSelectedLabel();
        
        if (setSelectedLabel(state))
            firePropertyChange(VIEW_STATE, oldState, state);
    }
    
    
    /**
     * <P>Returns the label associated with the currently selected button
     * or <CODE>null</CODE> if the panel is empty.</P>
     *
     * <P>This method may be overridden in a derived class if the class
     * does not consider the view state of the view to be equivalent to
     * the button label of the selected button.</P>
     *
     * @see #setViewState(String)
     * @see Displayable
     */
    public String getViewState() {
        return getSelectedLabel();
    }
    
    
    /**
     * <P>Sets the string corresponding to the default button selection.</P>
     *
     * <P>If the given string does not correspond to a button at the time
     * the method <CODE>reset()</CODE> is called, then <CODE>reset()</CODE>
     * will do nothing.  In other words, the given state is not checked for
     * validity at the time this method is called but only when the default
     * view state is used to reset the view state.</P>
     *
     * <P>This method may be overridden in a derived class if the class
     * does not consider the view state of the view to be equivalent to
     * the button label of the selected button.</P>
     *
     * @param state the proposed default view state
     * @see #reset()
     * @see Displayable
     */
    public void setDefaultViewState(String state) {
        if (state == null)
            return;
        
        String oldState = getDefaultViewState();
        
        if (state.equals(oldState))
            return;
        
        defaultViewState = state;  

        // notify listeners of property change
        firePropertyChange(DEFAULT_VIEW_STATE, oldState, state);
    }
    
    
    /**
     * Returns the current default view state string.
     *
     * <P>This method may be overridden in a derived class if the class
     * does not consider the view state of the view to be equivalent to
     * the button label of the selected button.</P>
     *
     * @return the current default view state string
     * @see #setDefaultViewState(String)
     * @see #reset()
     */
    public String getDefaultViewState() {
        return defaultViewState;
    }
    
    
    /**
     * <P>Sets the currently selected button to the one corresponding
     * to the current default view state string.</P>
     *
     * <P>If the current default view state string does not currently
     * correspond to a button, then does nothing.</P>
     *
     * <P>Equivalent to <CODE>setViewState(getDefaultViewState())</CODE>.</P>
     *
     * @see #setDefaultViewState(String)
     * @see Displayable
     */
    public final void reset() {
        setViewState(getDefaultViewState());
    }
    
    
    /**
     * Enable or disable the panel and its individual radio buttons.
     *
     * @param isEnabled whether to enable or disable
     */
    public final void setEnabled(boolean isEnabled) {
        int length = getOptionCount();
        
        for (int i = 0; i < length; i++)
            getOptionButton(i).setEnabled(isEnabled);
        
        super.setEnabled(isEnabled);
    }
    
    
    /** Refreshes the component by repacking the parent window. */
    public void refreshComponent() {
        Refresh.packParentWindow(this);
    }
    
}
