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(disabled) return;
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 != null) aButton.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 }
|