View Javadoc

1   /*
2   Wotonomy: OpenStep design patterns for pure Java applications.
3   Copyright (C) 2000 Intersect Software Corporation
4   
5   This library is free software; you can redistribute it and/or
6   modify it under the terms of the GNU Lesser General Public
7   License as published by the Free Software Foundation; either
8   version 2.1 of the License, or (at your option) any later version.
9   
10  This library is distributed in the hope that it will be useful,
11  but WITHOUT ANY WARRANTY; without even the implied warranty of
12  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13  Lesser General Public License for more details.
14  
15  You should have received a copy of the GNU Lesser General Public
16  License along with this library; if not, see http://www.gnu.org
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         // prepopulate titles
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         // titles aspect
249         displayGroup = displayGroupForAspect( TitlesAspect );
250         if ( displayGroup != null )
251         {
252             ComboBoxModel model = component().getModel();
253             // if first time, or if backing group has changed
254 //          if ( ( ! ( model instanceof ComboBoxAssociationModel ) )
255 //          || ( displayGroup.contentsChanged() ) )
256 //          {
257                 key = displayGroupKeyForAspect( TitlesAspect );
258                 populateTitles( displayGroup, key );
259 //          }
260         }
261 
262         // value aspect
263         displayGroup = displayGroupForAspect( ValueAspect );
264         if ( displayGroup != null )
265         {
266             key = displayGroupKeyForAspect( ValueAspect );
267             component.setEnabled( 
268                     displayGroup.enabledToSetSelectedObjectValueForKey( key ) );
269             //Object value = displayGroup.selectedObjectValueForKey( key );
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                 // get value for the first selected object.
282                 int initialIndex = ( (Integer)indexIterator.next() ).intValue();
283                 previousValue = displayGroup.valueForObjectAtIndex(
284                                           initialIndex, key );
285                 value = null;
286 
287                 // go through the rest of the selected objects, compare each
288                 // value with the previous one. continue comparing if two
289                 // values are equal, break the while loop if they're different.
290                 // the final value will be the common value of all selected objects
291                 // if there is one, or be blank if there is not.
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                         // currentValue is the same as the previous one
306                         value = currentValue;
307                     }
308 
309                 } // end while
310 
311             } else {
312                 // if there's only one object selected.
313                 value = displayGroup.selectedObjectValueForKey( key );
314             } // end checking the size of selected objects in displayGroup
315 
316 
317             // objects aspect
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 // values aspect not bound
355         {
356             // use objects group if specified
357             EODisplayGroup sourceGroup =
358                 displayGroupForAspect( ObjectsAspect );
359             if ( sourceGroup == null )
360             {
361                 // fall back on titles group
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                     // the combo box model decides what to do with this value
375                     component.setSelectedItem( null );
376                 }
377             }
378         }
379 
380         // enabled aspect
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         // selected title aspect
440         displayGroup = displayGroupForAspect( ValueAspect );
441         if ( displayGroup != null )
442         {
443             key = displayGroupKeyForAspect( ValueAspect );
444             Object value = null;
445 
446             // selected object aspect, if any
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 // selected index is -1
461                     {
462                         // the combo box is probably editable,
463                         // so there is no corresponding object.
464                         value = null;
465                     }
466                 }
467                 catch ( NullPointerException npe )
468                 {
469                     // catches NPE on line 436 of JComboBox.java:
470                     // this is a common developer error
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 // just use the selected item
477             {
478                 value = component.getSelectedItem();
479             }
480 
481             boolean returnValue = true;
482             if ( displayGroup.selectedObjects().size() == 1 )
483             {   // displayGroup has only one object
484                 // only set value if changed
485                 Object existingValue = displayGroup.selectedObjectValueForKey( key );
486                 if ( value == existingValue ) return true;
487                 if ( ( existingValue != null ) && ( existingValue.equals( value ) ) ) return true;
488 
489                 // value has changed: update the value.
490                 return displayGroup.setSelectedObjectValue( value, key );
491             }
492             else if ( displayGroup.selectedObjects().size() > 1 )
493             {
494                 // displayGroup has more than one object
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             } // end checking size of displayGroup
508 
509         }
510         else // values aspect not bound
511         {
512             // use objects group if specified
513             EODisplayGroup sourceGroup =
514                 displayGroupForAspect( ObjectsAspect );
515             if ( sourceGroup == null )
516             {
517                 // fall back on titles group
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     // interface ActionListener
540 
541     /***
542     * Updates object on action performed.
543     */
544     public void actionPerformed( ActionEvent evt )
545     {
546         writeValueToDisplayGroup();
547     }
548 
549     // interface FocusListener
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     // convenience
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         { //System.out.println( "setSelectedItem: " + anItem );
632             selectedItem = anItem;
633 
634             // must do this to notify an editable combo,
635             //   otherwise the wrong value appears.
636             fireContentsChanged( this, -1, -1 );
637         }
638 
639         public Object getSelectedItem()
640         { //System.out.println( "getSelectedItem: " + selectedItem );
641             return selectedItem;
642         }
643     }
644 }
645 
646 /*
647  * $Log$
648  * Revision 1.2  2006/02/18 23:19:05  cgruber
649  * Update imports and maven dependencies.
650  *
651  * Revision 1.1  2006/02/16 13:22:22  cgruber
652  * Check in all sources in eclipse-friendly maven-enabled packages.
653  *
654  * Revision 1.13  2004/01/28 18:34:57  mpowers
655  * Better handling for enabling.
656  * Now respecting enabledToSetSelectedObjectValueForKey from display group.
657  *
658  * Revision 1.12  2003/08/06 23:07:52  chochos
659  * general code cleanup (mostly, removing unused imports)
660  *
661  * Revision 1.11  2001/07/30 16:32:55  mpowers
662  * Implemented support for bulk-editing.  Detail associations will now
663  * apply changes to all selected objects.
664  *
665  * Revision 1.10  2001/07/23 20:17:56  mpowers
666  * Now works as an overview association if the values aspect is not bound.
667  *
668  * Revision 1.9  2001/06/30 14:57:29  mpowers
669  * Removed a println.
670  *
671  * Revision 1.8  2001/06/29 22:28:19  mpowers
672  * Tabs to spaces.
673  *
674  * Revision 1.7  2001/06/29 22:17:31  mpowers
675  * Now updating the component on establishConnection.
676  *
677  * Revision 1.6  2001/05/14 15:24:49  mpowers
678  * Only updating if change was made.  Feels like I had fixed this here before.
679  *
680  * Revision 1.5  2001/04/09 21:41:08  mpowers
681  * Fixed a bug I thought that I had fixed before.
682  *
683  * Revision 1.4  2001/03/01 20:37:17  mpowers
684  * Updated docs to emphasize that titles aspect is optional.
685  *
686  * Revision 1.3  2001/02/17 16:52:05  mpowers
687  * Changes in imports to support building with jdk1.1 collections.
688  *
689  * Revision 1.2  2001/01/10 17:01:08  mpowers
690  * Caught a common developer error.
691  *
692  * Revision 1.1.1.1  2000/12/21 15:48:43  mpowers
693  * Contributing wotonomy.
694  *
695  * Revision 1.8  2000/12/20 16:25:40  michael
696  * Added log to all files.
697  *
698  *
699  */
700