1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 package net.wotonomy.ui.swing;
20
21 import java.awt.event.ActionEvent;
22 import java.awt.event.ActionListener;
23 import java.awt.event.FocusEvent;
24 import java.awt.event.FocusListener;
25 import java.util.Enumeration;
26 import java.util.Iterator;
27 import java.util.List;
28
29 import javax.swing.AbstractListModel;
30 import javax.swing.ComboBoxModel;
31 import javax.swing.JComboBox;
32
33 import net.wotonomy.foundation.NSArray;
34 import net.wotonomy.foundation.internal.ValueConverter;
35 import net.wotonomy.foundation.internal.WotonomyException;
36 import net.wotonomy.ui.EOAssociation;
37 import net.wotonomy.ui.EODisplayGroup;
38
39 /***
40 * ComboBoxAssociation binds JComboBoxes to
41 * display groups. Bindings are:
42 * <ul>
43 *
44 * <li>value: optional - a property of the selected object in the
45 * display group that will be bind to the item the user
46 * selects or the text that the user enters in the field.
47 * If the value aspect is not bound, then the combo box works
48 * as an "overview" assocation and changing the selected object
49 * in the combobox will modify the selection of the display group
50 * bound to the objects or the titles display groups (in that order).</li>
51 *
52 * <li>titles: optional - a property of the objects in the bound
53 * display group that will appear in the list. If the
54 * objects aspect is not bound, this property is also
55 * used to populate the value binding. If the titles
56 * aspect itself is not bound, the items already in the
57 * combobox will be used to update the value in the
58 * selected object in the bound display group.</li>
59 *
60 * <li>objects: optional - if specified, when the user
61 * selects a title in the list, the property of the
62 * object at the corresponding index of the bound display
63 * group will be used to populate the value binding.
64 * If the objects aspect is used with an editable combo
65 * box, any value entered that does not match one of the
66 * titles in the list will produce a null value.</li>
67 *
68 * <li>enabled: optional - a boolean property of the
69 * selected object in the display group that determines whether
70 * the user can edit the field.</li>
71 *
72 * </ul>
73 *
74 * @author michael@mpowers.net
75 * @author $Author: cgruber $
76 * @version $Revision: 904 $
77 */
78 public class ComboBoxAssociation extends EOAssociation
79 implements FocusListener, ActionListener
80 {
81 static final NSArray aspects =
82 new NSArray( new Object[] {
83 TitlesAspect, ValueAspect,
84 ObjectsAspect, EnabledAspect
85 } );
86 static final NSArray aspectSignatures =
87 new NSArray( new Object[] {
88 AttributeToOneAspectSignature,
89 AttributeToOneAspectSignature,
90 AttributeToOneAspectSignature,
91 AttributeToOneAspectSignature
92 } );
93 static final NSArray objectKeysTaken =
94 new NSArray( new Object[] {
95 "text"
96 } );
97
98 private boolean wasNull;
99 private static final String EMPTY_STRING = "";
100
101 /***
102 * Constructor specifying the object to be controlled by this
103 * association. Does not establish connection.
104 */
105 public ComboBoxAssociation ( Object anObject )
106 {
107 super( anObject );
108 }
109
110 /***
111 * Returns a List of aspect signatures whose contents
112 * correspond with the aspects list. Each element is
113 * a string whose characters represent a capability of
114 * the corresponding aspect. <ul>
115 * <li>"A" attribute: the aspect can be bound to
116 * an attribute.</li>
117 * <li>"1" to-one: the aspect can be bound to a
118 * property that returns a single object.</li>
119 * <li>"M" to-one: the aspect can be bound to a
120 * property that returns multiple objects.</li>
121 * </ul>
122 * An empty signature "" means that the aspect can
123 * bind without needing a key.
124 * This implementation returns "A1M" for each
125 * element in the aspects array.
126 */
127 public static NSArray aspectSignatures ()
128 {
129 return aspectSignatures;
130 }
131
132 /***
133 * Returns a List that describes the aspects supported
134 * by this class. Each element in the list is the string
135 * name of the aspect. This implementation returns an
136 * empty list.
137 */
138 public static NSArray aspects ()
139 {
140 return aspects;
141 }
142
143 /***
144 * Returns a List of EOAssociation subclasses that,
145 * for the objects that are usable for this association,
146 * are less suitable than this association.
147 */
148 public static NSArray associationClassesSuperseded ()
149 {
150 return new NSArray();
151 }
152
153 /***
154 * Returns whether this class can control the specified
155 * object.
156 */
157 public static boolean isUsableWithObject ( Object anObject )
158 {
159 return ( anObject instanceof JComboBox );
160 }
161
162 /***
163 * Returns a List of properties of the controlled object
164 * that are controlled by this class. For example,
165 * "stringValue", or "selected".
166 */
167 public static NSArray objectKeysTaken ()
168 {
169 return objectKeysTaken;
170 }
171
172 /***
173 * Returns the aspect that is considered primary
174 * or default. This is typically "value" or somesuch.
175 */
176 public static String primaryAspect ()
177 {
178 return ValueAspect;
179 }
180
181 /***
182 * Returns whether this association can bind to the
183 * specified display group on the specified key for
184 * the specified aspect.
185 */
186 public boolean canBindAspect (
187 String anAspect, EODisplayGroup aDisplayGroup, String aKey)
188 {
189 return ( aspects.containsObject( anAspect ) );
190 }
191
192 /***
193 * Establishes a connection between this association
194 * and the controlled object. Subclasses should begin
195 * listening for events from their controlled object here.
196 */
197 public void establishConnection ()
198 {
199 super.establishConnection();
200
201
202 EODisplayGroup displayGroup =
203 displayGroupForAspect( TitlesAspect );
204 if ( displayGroup != null )
205 {
206 String key = displayGroupKeyForAspect( TitlesAspect );
207 populateTitles( displayGroup, key );
208 }
209 addAsListener();
210 subjectChanged();
211 }
212
213 protected void addAsListener()
214 {
215 component().addActionListener( this );
216 component().addFocusListener( this );
217 }
218
219 /***
220 * Breaks the connection between this association and
221 * its object. Override to stop listening for events
222 * from the object.
223 */
224 public void breakConnection ()
225 {
226 removeAsListener();
227 super.breakConnection();
228 }
229
230 protected void removeAsListener()
231 {
232 component().removeActionListener( this );
233 component().removeFocusListener( this );
234 }
235
236 /***
237 * Called when either the selection or the contents
238 * of an associated display group have changed.
239 */
240 public void subjectChanged ()
241 {
242 removeAsListener();
243
244 JComboBox component = component();
245 EODisplayGroup displayGroup;
246 String key;
247
248
249 displayGroup = displayGroupForAspect( TitlesAspect );
250 if ( displayGroup != null )
251 {
252 ComboBoxModel model = component().getModel();
253
254
255
256
257 key = displayGroupKeyForAspect( TitlesAspect );
258 populateTitles( displayGroup, key );
259
260 }
261
262
263 displayGroup = displayGroupForAspect( ValueAspect );
264 if ( displayGroup != null )
265 {
266 key = displayGroupKeyForAspect( ValueAspect );
267 component.setEnabled(
268 displayGroup.enabledToSetSelectedObjectValueForKey( key ) );
269
270 Object value;
271
272
273 if ( displayGroup.selectedObjects().size() > 1 )
274 {
275
276 Object previousValue;
277
278 Iterator indexIterator = displayGroup.selectionIndexes().
279 iterator();
280
281
282 int initialIndex = ( (Integer)indexIterator.next() ).intValue();
283 previousValue = displayGroup.valueForObjectAtIndex(
284 initialIndex, key );
285 value = null;
286
287
288
289
290
291
292 while ( indexIterator.hasNext() )
293 {
294 int index = ( (Integer)indexIterator.next() ).intValue();
295 Object currentValue = displayGroup.valueForObjectAtIndex(
296 index, key );
297
298 if ( currentValue != null && !currentValue.equals( previousValue ) )
299 {
300 value = null;
301 break;
302 }
303 else
304 {
305
306 value = currentValue;
307 }
308
309 }
310
311 } else {
312
313 value = displayGroup.selectedObjectValueForKey( key );
314 }
315
316
317
318 EODisplayGroup objectsDisplayGroup =
319 displayGroupForAspect( ObjectsAspect );
320 if ( ( objectsDisplayGroup != null ) && ( value != null ) )
321 {
322 String objectKey = displayGroupKeyForAspect( ObjectsAspect );
323 Object match;
324 int index = NSArray.NotFound;
325 int count = objectsDisplayGroup.displayedObjects().count();
326 for ( int i = 0; i < count; i++ )
327 {
328 match = objectsDisplayGroup.valueForObjectAtIndex( i, objectKey );
329 if ( value.equals( match ) )
330 {
331 index = i;
332 }
333 }
334 if ( index == NSArray.NotFound )
335 {
336 if ( component.getSelectedItem() != null )
337 {
338 component.setSelectedItem( null );
339 }
340 }
341 else
342 {
343 if ( component.getSelectedIndex() != index )
344 {
345 component.setSelectedIndex( index );
346 }
347 }
348 }
349 else
350 {
351 component.setSelectedItem( value );
352 }
353 }
354 else
355 {
356
357 EODisplayGroup sourceGroup =
358 displayGroupForAspect( ObjectsAspect );
359 if ( sourceGroup == null )
360 {
361
362 sourceGroup = displayGroupForAspect( TitlesAspect );
363 }
364
365 if ( sourceGroup != null )
366 {
367 List selection = sourceGroup.selectionIndexes();
368 if ( ( selection != null ) && ( selection.size() > 0 ) )
369 {
370 component.setSelectedIndex( ((Integer)selection.get(0)).intValue() );
371 }
372 else
373 {
374
375 component.setSelectedItem( null );
376 }
377 }
378 }
379
380
381 displayGroup = displayGroupForAspect( EnabledAspect );
382 if ( displayGroup != null )
383 {
384 key = displayGroupKeyForAspect( EnabledAspect );
385 Object value =
386 displayGroup.selectedObjectValueForKey( key );
387 Boolean converted = null;
388 if ( value != null )
389 {
390 converted = (Boolean)
391 ValueConverter.convertObjectToClass(
392 value, Boolean.class );
393 }
394 if ( converted == null ) converted = Boolean.FALSE;
395 if ( converted.booleanValue() != component.isEnabled() )
396 {
397 component.setEnabled( converted.booleanValue() );
398 }
399 }
400
401 addAsListener();
402 }
403
404 /***
405 * Called to repopulate the title list from the
406 * specified display group.
407 */
408 protected void populateTitles(
409 EODisplayGroup displayGroup, String key )
410 {
411 component().setModel(
412 new ComboBoxAssociationModel( displayGroup, key ) );
413 }
414
415 /***
416 * Forces this association to cause the object to
417 * stop editing and validate the user's input.
418 * @return false if there were problems validating,
419 * or true to continue.
420 */
421 public boolean endEditing ()
422 {
423 return writeValueToDisplayGroup();
424 }
425
426 /***
427 * Writes the value currently in the component
428 * to the selected object in the display group
429 * bound to the value aspect.
430 * @return false if there were problems validating,
431 * or true to continue.
432 */
433 protected boolean writeValueToDisplayGroup()
434 {
435 JComboBox component = component();
436 EODisplayGroup displayGroup;
437 String key;
438
439
440 displayGroup = displayGroupForAspect( ValueAspect );
441 if ( displayGroup != null )
442 {
443 key = displayGroupKeyForAspect( ValueAspect );
444 Object value = null;
445
446
447 EODisplayGroup objectsGroup =
448 displayGroupForAspect( ObjectsAspect );
449 if ( objectsGroup != null )
450 {
451 try
452 {
453 String objectKey = displayGroupKeyForAspect( ObjectsAspect );
454 int index = component.getSelectedIndex();
455 if ( index != -1 )
456 {
457 value = objectsGroup
458 .valueForObjectAtIndex( index, objectKey );
459 }
460 else
461 {
462
463
464 value = null;
465 }
466 }
467 catch ( NullPointerException npe )
468 {
469
470
471 throw new WotonomyException( "ComboBoxAssociation: " +
472 "The object in the VALUE property may not have been found in the " +
473 "objects in the TITLES group.", npe );
474 }
475 }
476 else
477 {
478 value = component.getSelectedItem();
479 }
480
481 boolean returnValue = true;
482 if ( displayGroup.selectedObjects().size() == 1 )
483 {
484
485 Object existingValue = displayGroup.selectedObjectValueForKey( key );
486 if ( value == existingValue ) return true;
487 if ( ( existingValue != null ) && ( existingValue.equals( value ) ) ) return true;
488
489
490 return displayGroup.setSelectedObjectValue( value, key );
491 }
492 else if ( displayGroup.selectedObjects().size() > 1 )
493 {
494
495 Iterator selectedIterator = displayGroup.selectionIndexes().iterator();
496 while ( selectedIterator.hasNext() )
497 {
498 int index = ( (Integer)selectedIterator.next() ).intValue();
499
500 if ( !displayGroup.setValueForObjectAtIndex( value, index, key ) )
501 {
502 returnValue = false;
503 }
504 }
505 return returnValue;
506
507 }
508
509 }
510 else
511 {
512
513 EODisplayGroup sourceGroup =
514 displayGroupForAspect( ObjectsAspect );
515 if ( sourceGroup == null )
516 {
517
518 sourceGroup = displayGroupForAspect( TitlesAspect );
519 }
520
521 if ( sourceGroup != null )
522 {
523 int index = component.getSelectedIndex();
524 if ( index != -1 )
525 {
526 sourceGroup.setSelectionIndexes( new NSArray( new Integer( index ) ) );
527 }
528 else
529 {
530 sourceGroup.setSelectedObject( null );
531 }
532 return true;
533 }
534 }
535
536 return false;
537 }
538
539
540
541 /***
542 * Updates object on action performed.
543 */
544 public void actionPerformed( ActionEvent evt )
545 {
546 writeValueToDisplayGroup();
547 }
548
549
550
551 /***
552 * Notifies of beginning of edit.
553 */
554 public void focusGained(FocusEvent evt)
555 {
556 Object o;
557 EODisplayGroup displayGroup;
558 Enumeration e = aspects().objectEnumerator();
559 while ( e.hasMoreElements() )
560 {
561 displayGroup =
562 displayGroupForAspect( e.nextElement().toString() );
563 if ( displayGroup != null )
564 {
565 displayGroup.associationDidBeginEditing( this );
566 }
567 }
568 }
569
570 /***
571 * Updates object on focus lost and notifies of end of edit.
572 */
573 public void focusLost(FocusEvent evt)
574 {
575 if ( component().isEditable() )
576 {
577 if ( endEditing() )
578 {
579 Object o;
580 EODisplayGroup displayGroup;
581 Enumeration e = aspects().objectEnumerator();
582 while ( e.hasMoreElements() )
583 {
584 displayGroup =
585 displayGroupForAspect( e.nextElement().toString() );
586 if ( displayGroup != null )
587 {
588 displayGroup.associationDidEndEditing( this );
589 }
590 }
591 }
592 }
593 }
594
595
596
597 private JComboBox component()
598 {
599 return (JComboBox) object();
600 }
601
602 /***
603 * Used as the data model for the controlled combo box.
604 */
605 private class ComboBoxAssociationModel extends AbstractListModel
606 implements ComboBoxModel
607 {
608 EODisplayGroup displayGroup;
609 String key;
610 Object selectedItem;
611
612 ComboBoxAssociationModel(
613 EODisplayGroup aDisplayGroup, String aKey )
614 {
615 displayGroup = aDisplayGroup;
616 key = aKey;
617 selectedItem = null;
618 }
619
620 public Object getElementAt(int index)
621 {
622 return displayGroup.valueForObjectAtIndex( index, key );
623 }
624
625 public int getSize()
626 {
627 return displayGroup.displayedObjects().count();
628 }
629
630 public void setSelectedItem(Object anItem)
631 {
632 selectedItem = anItem;
633
634
635
636 fireContentsChanged( this, -1, -1 );
637 }
638
639 public Object getSelectedItem()
640 {
641 return selectedItem;
642 }
643 }
644 }
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700