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