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