JChoice.java
001 /*
002  *  Copyright (c) 1995-2012, The University of Sheffield. See the file
003  *  COPYRIGHT.txt in the software or at http://gate.ac.uk/gate/COPYRIGHT.txt
004  *
005  *  This file is part of GATE (see http://gate.ac.uk/), and is free
006  *  software, licenced under the GNU Library General Public License,
007  *  Version 2, June 1991 (in the distribution as file licence.html,
008  *  and also available at http://gate.ac.uk/gate/licence.html).
009  *
010  *  Valentin Tablan, 13 Sep 2007
011  *
012  *  $Id: JChoice.java 17874 2014-04-18 11:19:47Z markagreenwood $
013  */
014 package gate.swing;
015 
016 import java.awt.BorderLayout;
017 import java.awt.Component;
018 import java.awt.Dimension;
019 import java.awt.FlowLayout;
020 import java.awt.Insets;
021 import java.awt.ItemSelectable;
022 import java.awt.Point;
023 import java.awt.event.ActionEvent;
024 import java.awt.event.ActionListener;
025 import java.awt.event.ItemEvent;
026 import java.awt.event.ItemListener;
027 import java.util.EventListener;
028 import java.util.HashMap;
029 import java.util.Map;
030 
031 import javax.swing.AbstractButton;
032 import javax.swing.Box;
033 import javax.swing.ComboBoxModel;
034 import javax.swing.DefaultComboBoxModel;
035 import javax.swing.JButton;
036 import javax.swing.JComboBox;
037 import javax.swing.JFrame;
038 import javax.swing.JPanel;
039 import javax.swing.JToggleButton;
040 import javax.swing.event.ListDataListener;
041 
042 /**
043  * A GUI component intended to allow quick selection from a set of
044  * options. When the number of choices is small (i.e less or equal to
045  {@link #maximumFastChoices}) then the options are represented as a
046  * set of buttons in a flow layout. If more options are available, a
047  * simple {@link JComboBox} is used instead.
048  */
049 @SuppressWarnings("serial")
050 public class JChoice<E> extends JPanel implements ItemSelectable {
051 
052   @Override
053   public Object[] getSelectedObjects() {
054     return new Object[]{getSelectedItem()};
055   }
056 
057   /**
058    * The default value for the {@link #maximumWidth} parameter.
059    */
060   public static final int DEFAULT_MAX_WIDTH = 500;
061 
062   /**
063    * The default value for the {@link #maximumFastChoices} parameter.
064    */
065   public static final int DEFAULT_MAX_FAST_CHOICES = 10;
066 
067   
068   /**
069    * The maximum number of options for which the flow of buttons is used
070    * instead of a combobox. By default this value is
071    {@link #DEFAULT_MAX_FAST_CHOICES}
072    */
073   private int maximumFastChoices;
074 
075 
076   /**
077    * Margin used for choice buttons. 
078    */
079   private Insets defaultButtonMargin;
080   
081   /**
082    * The maximum width allowed for this component. This value is only
083    * used when the component appears as a flow of buttons. By default
084    * this value is {@link #DEFAULT_MAX_WIDTH}. This is used to force the flow 
085    * layout do a multi-line layout, as by default it prefers to lay all 
086    * components in a single very wide line.
087    */
088   private int maximumWidth;
089 
090   /**
091    * The layout used by this container.
092    */
093   private FlowLayout layout;
094 
095   /**
096    * The combobox used for a large number of choices. 
097    */
098   private JComboBox<E> combo;
099   
100   /**
101    * Internal item listener for both the combo and the buttons, used to keep
102    * the two in sync. 
103    */
104   private ItemListener sharedItemListener; 
105   
106   /**
107    * The data model used for choices and selection.
108    */
109   private ComboBoxModel<E> model;
110   
111   /**
112    * Keeps a mapping between the button and the corresponding option from the
113    * model.
114    */
115   private Map<AbstractButton, Object> buttonToValueMap;
116   
117   
118   /**
119    * Creates a FastChoice with a default empty data model.
120    */
121   public JChoice() {
122     this(new DefaultComboBoxModel<E>());
123   }
124   
125   /**
126    * A map from wrapped action listeners to listener
127    */
128   private Map<EventListener, ListenerWrapper> listenersMap;
129   
130   /**
131    * Creates a FastChoice with the given data model.
132    */
133   public JChoice(ComboBoxModel<E> model) {
134     layout = new FlowLayout();
135     layout.setHgap(0);
136     layout.setVgap(0);
137     layout.setAlignment(FlowLayout.LEFT);
138     setLayout(layout);
139     this.model = model;
140     //by default nothing is selected
141     setSelectedItem(null);
142     initLocalData();
143     buildGui();
144   }
145 
146   /**
147    * Creates a FastChoice with a default data model populated from the provided
148    * array of objects.
149    */
150   public JChoice(E[] items) {
151     this(new DefaultComboBoxModel<E>(items));
152   }
153   
154   
155   /**
156    * Initialises some local values.
157    */
158   private void initLocalData(){
159     maximumFastChoices = DEFAULT_MAX_FAST_CHOICES;
160     maximumWidth = DEFAULT_MAX_WIDTH;
161     listenersMap = new HashMap<EventListener, ListenerWrapper>();
162     combo = new JComboBox<E>(model);
163     buttonToValueMap = new HashMap<AbstractButton, Object>();
164     sharedItemListener = new ItemListener(){
165       /**
166        * Flag used to disable event handling while generating events. Used as an
167        * exit mechanism from event handling loops. 
168        */
169       private boolean disabled = false;
170       
171       @Override
172       public void itemStateChanged(ItemEvent e) {
173         if(disabledreturn;
174         if(e.getSource() == combo){
175           //event from the combo
176           //disable event handling, to avoid unwanted cycles
177           disabled = true;
178           if(e.getStateChange() == ItemEvent.SELECTED){
179             //update the state of all buttons
180             for(AbstractButton aBtn : buttonToValueMap.keySet()){
181               Object aValue = buttonToValueMap.get(aBtn);
182               if(aValue.equals(e.getItem())){
183                 //this is the selected button
184                 if(!aBtn.isSelected()){
185                   aBtn.setSelected(true);
186                   aBtn.requestFocusInWindow();
187                 }
188               }else{
189                 //this is a button that should not be selected
190                 if(aBtn.isSelected()) aBtn.setSelected(false);
191               }
192             }
193           }else if(e.getStateChange() == ItemEvent.DESELECTED){
194             //deselections due to other value being selected are handled
195             //above.
196             //here we only need to handle the case when the selection was
197             //removed, but not replaced (i.e. setSelectedItem(null)
198             for(AbstractButton aBtn : buttonToValueMap.keySet()){
199               Object aValue = buttonToValueMap.get(aBtn);
200               if(aValue.equals(e.getItem())){
201                 //this is the de-selected button
202                 if(aBtn.isSelected()) aBtn.setSelected(false);
203               }
204             }
205           }
206           //re-enable event handling
207           disabled = false;
208         }else if(e.getSource() instanceof AbstractButton){
209           //event from the buttons
210           if(buttonToValueMap.containsKey(e.getSource())){
211             Object value = buttonToValueMap.get(e.getSource());
212             if(e.getStateChange() == ItemEvent.SELECTED){
213               model.setSelectedItem(value);
214             }else if(e.getStateChange() == ItemEvent.DESELECTED){
215               model.setSelectedItem(null);
216             }
217           }          
218         }
219       }      
220     };
221     combo.addItemListener(sharedItemListener);
222   }
223   
224   public static void main(String[] args){
225     final JChoice<String> fChoice = new JChoice<String>(new String[]{
226             "Jan",
227             "Feb",
228             "Mar",
229             "Apr",
230             "May",
231             "Jun",
232             "Jul",
233             "Aug",
234             "Sep",
235             "Oct",
236             "Nov",
237             "Dec"});
238     fChoice.setMaximumFastChoices(20);
239     fChoice.addActionListener(new ActionListener(){
240       @Override
241       public void actionPerformed(ActionEvent e) {
242         System.out.println("Action (" + e.getActionCommand() ") :" + fChoice.getSelectedItem() " selected!");
243       }
244     });
245     fChoice.addItemListener(new ItemListener(){
246       @Override
247       public void itemStateChanged(ItemEvent e) {
248         System.out.println("Item " + e.getItem().toString() +
249                (e.getStateChange() == ItemEvent.SELECTED ? " selected!" :
250                " deselected!"));
251       }
252       
253     });
254     JFrame aFrame = new JFrame("Fast Chioce Test Frame");
255     aFrame.getContentPane().add(fChoice);
256     
257     Box topBox = Box.createHorizontalBox();
258     JButton aButn = new JButton("Clear");
259     aButn.addActionListener(new ActionListener(){
260       @Override
261       public void actionPerformed(ActionEvent e) {
262         System.out.println("Clearing");
263         fChoice.setSelectedItem(null);
264       }
265     });
266     topBox.add(Box.createHorizontalStrut(10));
267     topBox.add(aButn);
268     topBox.add(Box.createHorizontalStrut(10));
269     topBox.add(new JToggleButton("GAGA"));
270     
271     aFrame.add(topBox, BorderLayout.NORTH);
272     aFrame.pack();
273     aFrame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
274     aFrame.setVisible(true);
275   }
276   
277   /**
278    @param l
279    @see javax.swing.JComboBox#removeActionListener(java.awt.event.ActionListener)
280    */
281   public void removeActionListener(ActionListener l) {
282     ListenerWrapper wrapper = listenersMap.remove(l);
283     combo.removeActionListener(wrapper);
284   }
285 
286   /**
287    @param listener
288    @see javax.swing.JComboBox#removeItemListener(java.awt.event.ItemListener)
289    */
290   @Override
291   public void removeItemListener(ItemListener listener) {
292     ListenerWrapper wrapper = listenersMap.remove(listener);
293     combo.removeActionListener(wrapper);
294   }
295 
296   /**
297    @param l
298    @see javax.swing.JComboBox#addActionListener(java.awt.event.ActionListener)
299    */
300   public void addActionListener(ActionListener l) {
301     combo.addActionListener(new ListenerWrapper(l));
302   }
303 
304   /**
305    @param listener
306    @see javax.swing.JComboBox#addItemListener(java.awt.event.ItemListener)
307    */
308   @Override
309   public void addItemListener(ItemListener listener) {
310     combo.addItemListener(new ListenerWrapper(listener));
311   }
312 
313   /**
314    * (Re)constructs the UI. This can be called many times, whenever a 
315    * significant value (such as {@link #maximumFastChoices}, or the model)
316    * has changed.
317    */
318   private void buildGui(){
319     removeAll();
320     if(model != null && model.getSize() 0){
321       if(model.getSize() > maximumFastChoices){
322         //use combobox
323         add(combo);
324       }else{
325         //use buttons
326         //first clear the old buttons, if any exist
327         if(buttonToValueMap.size() 0){
328           for(AbstractButton aBtn : buttonToValueMap.keySet()){
329             aBtn.removeItemListener(sharedItemListener);
330           }
331         }
332         //now create the new buttons
333         buttonToValueMap.clear();
334         for(int i = 0; i < model.getSize(); i++){
335           Object aValue = model.getElementAt(i);
336           JToggleButton aButton = new JToggleButton(aValue.toString());
337           if(defaultButtonMargin != nullaButton.setMargin(defaultButtonMargin);
338           aButton.addItemListener(sharedItemListener);
339           buttonToValueMap.put(aButton, aValue);
340           add(aButton);
341         }
342       }
343     }
344     revalidate();
345   }
346   
347   
348   /**
349    @param l
350    @see javax.swing.ListModel#addListDataListener(javax.swing.event.ListDataListener)
351    */
352   public void addListDataListener(ListDataListener l) {
353     model.addListDataListener(l);
354   }
355 
356   /**
357    @see javax.swing.ListModel#getElementAt(int)
358    */
359   public Object getElementAt(int index) {
360     return model.getElementAt(index);
361   }
362 
363   /**
364    @see javax.swing.ComboBoxModel#getSelectedItem()
365    */
366   public Object getSelectedItem() {
367     return model.getSelectedItem();
368   }
369 
370   /**
371    @see javax.swing.ListModel#getSize()
372    */
373   public int getItemCount() {
374     return model.getSize();
375   }
376 
377   /**
378    @param l
379    @see javax.swing.ListModel#removeListDataListener(javax.swing.event.ListDataListener)
380    */
381   public void removeListDataListener(ListDataListener l) {
382     model.removeListDataListener(l);
383   }
384 
385   /**
386    @param anItem
387    @see javax.swing.ComboBoxModel#setSelectedItem(java.lang.Object)
388    */
389   public void setSelectedItem(Object anItem) {
390     model.setSelectedItem(anItem);
391   }
392 
393   /*
394    * (non-Javadoc)
395    
396    * @see javax.swing.JComponent#getPreferredSize()
397    */
398   @Override
399   public Dimension getPreferredSize() {
400     Dimension size = super.getPreferredSize();
401     if(getItemCount() <= maximumFastChoices && size.width > maximumWidth) {
402       setSize(maximumWidth, Integer.MAX_VALUE);
403       doLayout();
404       int compCnt = getComponentCount();
405       if(compCnt > 0) {
406         Component lastComp = getComponent(compCnt - 1);
407         Point compLoc = lastComp.getLocation();
408         Dimension compSize = lastComp.getSize();
409         size.width = maximumWidth;
410         size.height = compLoc.y + compSize.height + getInsets().bottom;
411       }
412     }
413     return size;
414   }
415   
416 
417   /**
418    @return the maximumFastChoices
419    */
420   public int getMaximumFastChoices() {
421     return maximumFastChoices;
422   }
423 
424   /**
425    @param maximumFastChoices the maximumFastChoices to set
426    */
427   public void setMaximumFastChoices(int maximumFastChoices) {
428     this.maximumFastChoices = maximumFastChoices;
429     buildGui();
430   }
431 
432   
433   /**
434    @return the model
435    */
436   public ComboBoxModel<E> getModel() {
437     return model;
438   }
439 
440   /**
441    @param model the model to set
442    */
443   public void setModel(ComboBoxModel<E> model) {
444     this.model = model;
445     combo.setModel(model);
446     buildGui();
447   }
448 
449   /**
450    @return the maximumWidth
451    */
452   public int getMaximumWidth() {
453     return maximumWidth;
454   }
455 
456   /**
457    @param maximumWidth the maximumWidth to set
458    */
459   public void setMaximumWidth(int maximumWidth) {
460     this.maximumWidth = maximumWidth;
461   }
462   
463   /**
464    * An action listener that changes the source of events to be this object.
465    */
466   private class ListenerWrapper implements ActionListener, ItemListener{
467     public ListenerWrapper(EventListener originalListener) {
468       this.originalListener = originalListener;
469       listenersMap.put(originalListener, this);
470     }
471 
472     @Override
473     public void itemStateChanged(ItemEvent e) {
474       //generate a new event with this as source
475       ((ItemListener)originalListener).itemStateChanged(
476               new ItemEvent(JChoice.this, e.getID(), e.getItem()
477                       e.getStateChange()));
478     }
479 
480     @Override
481     public void actionPerformed(ActionEvent e) {
482       //generate a new event
483       ((ActionListener)originalListener).actionPerformed(new ActionEvent(
484               JChoice.this, e.getID(), e.getActionCommand(), e.getWhen()
485               e.getModifiers()));
486     }
487     private EventListener originalListener;
488   }
489 
490   /**
491    @return the defaultButtonMargin
492    */
493   public Insets getDefaultButtonMargin() {
494     return defaultButtonMargin;
495   }
496 
497   /**
498    @param defaultButtonMargin the defaultButtonMargin to set
499    */
500   public void setDefaultButtonMargin(Insets defaultButtonMargin) {
501     this.defaultButtonMargin = defaultButtonMargin;
502     buildGui();
503   }
504 }