001/*
002 * $Id: JXRadioGroup.java 4158 2012-02-03 18:29:40Z kschaefe $
003 *
004 * Copyright 2006 Sun Microsystems, Inc., 4150 Network Circle,
005 * Santa Clara, California 95054, U.S.A. All rights reserved.
006 *
007 * This library is free software; you can redistribute it and/or
008 * modify it under the terms of the GNU Lesser General Public
009 * License as published by the Free Software Foundation; either
010 * version 2.1 of the License, or (at your option) any later version.
011 *
012 * This library is distributed in the hope that it will be useful,
013 * but WITHOUT ANY WARRANTY; without even the implied warranty of
014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
015 * Lesser General Public License for more details.
016 *
017 * You should have received a copy of the GNU Lesser General Public
018 * License along with this library; if not, write to the Free Software
019 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
020 */
021package org.jdesktop.swingx;
022
023import java.awt.Component;
024import java.awt.event.ActionEvent;
025import java.awt.event.ActionListener;
026import java.awt.event.ItemEvent;
027import java.awt.event.ItemListener;
028import java.util.ArrayList;
029import java.util.Enumeration;
030import java.util.List;
031
032import javax.swing.AbstractButton;
033import javax.swing.BoxLayout;
034import javax.swing.ButtonGroup;
035import javax.swing.ButtonModel;
036import javax.swing.ComboBoxModel;
037import javax.swing.JComboBox;
038import javax.swing.JComponent;
039import javax.swing.JPanel;
040import javax.swing.JRadioButton;
041import javax.swing.event.EventListenerList;
042
043import org.jdesktop.beans.JavaBean;
044
045/**
046 * <p>
047 * {@code JXRadioGroup} is a group of radio buttons that functions as a unit. It
048 * is similar in concept to a {@link JComboBox} in functionality, but can offer
049 * a better presentation for a small number of choices. {@code JXRadioGroup}
050 * should be used in preference to {@code JComboBox} when the number of choices
051 * is small (less than six) or the choices are verbose.
052 * </p>
053 * <p>
054 * Notes:
055 * <ol>
056 * <li>Enabling and disabling the JXRadioGroup will enable/disable all of the
057 * child buttons inside the JXRadioGroup.</li>
058 * <li>
059 * If the generic type parameter of JXRadioGroup is a subclass of
060 * {@link AbstractButton}, then the buttons will be added "as is" to the
061 * container. If the generic type is anything else, buttons will be created as
062 * {@link JRadioButton} objects, and the button text will be set by calling
063 * toString() on the value object.</li>
064 * <li>
065 * Alternatively, if you want to configure the buttons individually, construct
066 * the JXRadioGroup normally, and then call {@link #getChildButton(int)} or
067 * {@link #getChildButton(Object)} and configure the buttons.</li>
068 * </ol>
069 * </p>
070 * <p>
071 * TODO back with a model (possibly reuse of extend {@link ComboBoxModel}
072 * </p>
073 * 
074 * @author Amy Fowler
075 * @author Noel Grandin
076 * @version 1.0
077 */
078@JavaBean
079public class JXRadioGroup<T> extends JPanel {
080
081    private static final long serialVersionUID = 3257285842266567986L;
082
083    private ButtonGroup buttonGroup;
084
085    private final List<T> values = new ArrayList<T>();
086
087    private ActionSelectionListener actionHandler;
088
089    /**
090     * Create a default JXRadioGroup with a default layout axis of {@link BoxLayout#X_AXIS}.
091     */
092    public JXRadioGroup() {
093        setLayout(new BoxLayout(this, BoxLayout.X_AXIS));
094        buttonGroup = new ButtonGroup();
095    }
096    
097    /**
098     * Create a default JXRadioGroup with a default layout axis of {@link BoxLayout#X_AXIS}.
099     * 
100     * @param radioValues the list of values used to create the group.
101     */
102    public JXRadioGroup(T[] radioValues) {
103        this();
104        for (int i = 0; i < radioValues.length; i++) {
105            add(radioValues[i]);
106        }
107    }
108    
109    /**
110     * Convenience factory method.
111     * Reduces code clutter when dealing with generics.
112     * 
113     * @param radioValues the list of values used to create the group.
114     */
115    public static <T> JXRadioGroup<T> create(T[] radioValues)
116    {
117        return new JXRadioGroup<T>(radioValues);
118    }
119
120    /**
121     * Set the layout axis of the radio group.
122     * 
123     * @param axis values from {@link BoxLayout}.
124     */
125    public void setLayoutAxis(int axis)
126    {
127        setLayout(new BoxLayout(this, axis));
128    }
129
130    /**
131     * Sets the values backing this group. This replaces the current set of
132     * values with the new set.
133     * 
134     * @param radioValues
135     *            the new backing values for this group
136     */
137    public void setValues(T[] radioValues) {
138        clearAll();
139        for (int i = 0; i < radioValues.length; i++) {
140            add(radioValues[i]);
141        }
142    }
143
144    private void clearAll() {
145        values.clear();
146        buttonGroup = new ButtonGroup();
147        // remove all the child components
148        removeAll();
149    }
150
151    /**
152     * You can use this method to manually add your own AbstractButton objects, provided you declared
153     * the class as <code>JXRadioGroup&lt;JRadioButton&gt;</code>.
154     */
155    public void add(T radioValue) {
156        if (values.contains(radioValue))
157        {
158            throw new IllegalArgumentException("cannot add the same value twice " + radioValue);
159        }
160        if (radioValue instanceof AbstractButton) {
161            values.add(radioValue);
162            addButton((AbstractButton) radioValue);
163        } else {
164            values.add(radioValue);
165            // Note: the "quote + object" trick here allows null values
166            addButton(new JRadioButton(""+radioValue));
167        }
168    }
169
170    private void addButton(AbstractButton button) {
171        buttonGroup.add(button);
172        super.add(button);
173        if (actionHandler == null) {
174            actionHandler = new ActionSelectionListener();
175        }
176        button.addActionListener(actionHandler);
177        button.addItemListener(actionHandler);
178    }
179
180    private class ActionSelectionListener implements ActionListener, ItemListener
181    {
182        @Override
183        public void actionPerformed(ActionEvent e) {
184            fireActionEvent(e);
185        }
186
187        @Override
188        public void itemStateChanged(ItemEvent e) {
189            fireActionEvent(null);
190        }
191    }
192
193    /**
194     * Gets the currently selected button.
195     * 
196     * @return the currently selected button
197     * @see #getSelectedValue()
198     */
199    public AbstractButton getSelectedButton() {
200        final ButtonModel selectedModel = buttonGroup.getSelection();
201        final AbstractButton children[] = getButtonComponents();
202        for (int i = 0; i < children.length; i++) {
203            AbstractButton button = children[i];
204            if (button.getModel() == selectedModel) {
205                return button;
206            }
207        }
208        return null;
209    }
210
211    private AbstractButton[] getButtonComponents() {
212        final Component[] children = getComponents();
213        final List<AbstractButton> buttons = new ArrayList<AbstractButton>();
214        for (int i = 0; i < children.length; i++) {
215            if (children[i] instanceof AbstractButton) {
216                buttons.add((AbstractButton) children[i]);
217            }
218        }
219        return buttons.toArray(new AbstractButton[buttons.size()]);
220    }
221
222    private int getSelectedIndex() {
223        final ButtonModel selectedModel = buttonGroup.getSelection();
224        final Component children[] = getButtonComponents();
225        for (int i = 0; i < children.length; i++) {
226            AbstractButton button = (AbstractButton) children[i];
227            if (button.getModel() == selectedModel) {
228                return i;
229            }
230        }
231        return -1;
232    }
233
234    /**
235     * The currently selected value.
236     * 
237     * @return the current value
238     */
239    public T getSelectedValue() {
240        final int index = getSelectedIndex();
241        return (index < 0 || index >= values.size()) ? null : values.get(index);
242    }
243
244    /**
245     * Selects the supplied value.
246     * 
247     * @param value
248     *            the value to select
249     */
250    public void setSelectedValue(T value) {
251        final int index = values.indexOf(value);
252        AbstractButton button = getButtonComponents()[index];
253        button.setSelected(true);
254    }
255    
256    /**
257     * Retrieve the child button by index.
258     */
259    public AbstractButton getChildButton(int index) {
260        return getButtonComponents()[index];
261    }
262
263    /**
264     * Retrieve the child button that represents this value.
265     */
266    public AbstractButton getChildButton(T value) {
267        final int index = values.indexOf(value);
268        return getButtonComponents()[index];
269    }
270
271    /**
272     * Get the number of child buttons.
273     */
274    public int getChildButtonCount() {
275        return getButtonComponents().length;
276    }
277    
278    /** 
279     * Adds an <code>ActionListener</code>. 
280     * <p>
281     * The <code>ActionListener</code> will receive an <code>ActionEvent</code>
282     * when a selection has been made.
283     *
284     * @param l  the <code>ActionListener</code> that is to be notified
285     * @see #setSelectedValue(Object)
286     */
287    public void addActionListener(ActionListener l) {
288        listenerList.add(ActionListener.class, l);
289    }
290
291    /**
292     * Removes an <code>ActionListener</code>.
293     * 
294     * @param l
295     *            the <code>ActionListener</code> to remove
296     */
297    public void removeActionListener(ActionListener l) {
298        listenerList.remove(ActionListener.class, l);
299    }
300
301    /**
302     * Returns an array of all the <code>ActionListener</code>s added
303     * to this JRadioGroup with addActionListener().
304     *
305     * @return all of the <code>ActionListener</code>s added or an empty
306     *         array if no listeners have been added
307     */
308    public ActionListener[] getActionListeners() {
309        return listenerList.getListeners(ActionListener.class);
310    }
311
312    /**
313     * Notifies all listeners that have registered interest for notification on
314     * this event type.
315     * 
316     * @param e
317     *            the event to pass to the listeners
318     * @see EventListenerList
319     */
320    protected void fireActionEvent(ActionEvent e) {
321        for (ActionListener l : getActionListeners()) {
322            l.actionPerformed(e);
323        }
324    }
325
326    /**
327     * Enable/disable all of the child buttons
328     * 
329     * @see JComponent#setEnabled(boolean)
330     */
331    @Override
332    public void setEnabled(boolean enabled) {
333        super.setEnabled(enabled);
334        for (Enumeration<AbstractButton> en = buttonGroup.getElements(); en.hasMoreElements();) {
335            final AbstractButton button = en.nextElement();
336            /* We don't want to enable a button where the action does not
337             * permit it. */
338            if (enabled && button.getAction() != null
339                    && !button.getAction().isEnabled()) {
340                // do nothing
341            } else {
342                button.setEnabled(enabled);
343            }
344        }
345    }
346
347}