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.Component;
22  import java.awt.event.ActionEvent;
23  import java.awt.event.ActionListener;
24  import java.awt.event.FocusEvent;
25  import java.awt.event.FocusListener;
26  import java.lang.reflect.InvocationTargetException;
27  import java.net.URL;
28  import java.text.Format;
29  import java.text.ParseException;
30  import java.util.Enumeration;
31  import java.util.Iterator;
32  
33  import javax.swing.Icon;
34  import javax.swing.ImageIcon;
35  import javax.swing.JOptionPane;
36  import javax.swing.JTextArea;
37  import javax.swing.LookAndFeel;
38  import javax.swing.event.DocumentEvent;
39  import javax.swing.event.DocumentListener;
40  import javax.swing.text.AbstractDocument;
41  import javax.swing.text.DefaultStyledDocument;
42  import javax.swing.text.Document;
43  import javax.swing.text.JTextComponent;
44  
45  import net.wotonomy.foundation.NSArray;
46  import net.wotonomy.foundation.NSDictionary;
47  import net.wotonomy.foundation.NSNotification;
48  import net.wotonomy.foundation.NSNotificationCenter;
49  import net.wotonomy.foundation.NSNotificationQueue;
50  import net.wotonomy.foundation.NSSelector;
51  import net.wotonomy.foundation.internal.ValueConverter;
52  import net.wotonomy.foundation.internal.WotonomyException;
53  import net.wotonomy.ui.EOAssociation;
54  import net.wotonomy.ui.EODisplayGroup;
55  
56  /***
57  * TextAssociation binds JTextComponents and other objects
58  * with getText() and setText() methods to a display group.
59  * Note that JLabels are supported with both the Text and
60  * Icon aspects.
61  * Bindings are:
62  * <ul>
63  * <li>value: a property convertable to/from a string</li>
64  * <li>editable: a boolean property that determines whether
65  * the user can edit the text in the field</li>
66  * <li>enabled: a boolean property that determines whether
67  * the user can select the text in the field</li>
68  * <li>visible: a boolean property that determines whether
69  * the field is visible</li>
70  * <li>label: a boolean property that determines whether
71  * field should appear as a read-only, selectable label</li>
72  * <li>icon: a property that returns a Swing icon, for use
73  * with JLabels and other components with setIcon() methods.
74  * If bound to a static string, the string will be used to
75  * load an image resource from the selected object's class.</li>
76  * </ul>
77  *
78  * @author michael@mpowers.net
79  * @author $Author: cgruber $
80  * @version $Revision: 904 $
81  */
82  public class TextAssociation extends EOAssociation
83      implements FocusListener, ActionListener, DocumentListener
84  {
85      static final NSArray aspects =
86          new NSArray( new Object[] {
87              ValueAspect, EnabledAspect, EditableAspect, VisibleAspect, LabelAspect, IconAspect
88          } );
89      static final NSArray aspectSignatures =
90          new NSArray( new Object[] {
91              AttributeToOneAspectSignature,
92              AttributeToOneAspectSignature,
93              AttributeToOneAspectSignature,
94              AttributeToOneAspectSignature,
95              AttributeToOneAspectSignature,
96              AttributeToOneAspectSignature
97          } );
98      static final NSArray objectKeysTaken =
99          new NSArray( new Object[] {
100             "text", "enabled", "editable", "visible"
101         } );
102 
103     private final static NSSelector getText =
104         new NSSelector( "getText" );
105     private final static NSSelector setText =
106         new NSSelector( "setText",
107         new Class[] { String.class } );
108     private final static NSSelector getDocument =
109         new NSSelector( "getDocument" );
110     private final static NSSelector setIcon =
111         new NSSelector( "setIcon",
112         new Class[] { Icon.class } );
113     private final static NSSelector addActionListener =
114         new NSSelector( "addActionListener",
115         new Class[] { ActionListener.class } );
116     private final static NSSelector removeActionListener =
117         new NSSelector( "removeActionListener",
118         new Class[] { ActionListener.class } );
119     private final static NSSelector addFocusListener =
120         new NSSelector( "addFocusListener",
121         new Class[] { FocusListener.class } );
122     private final static NSSelector removeFocusListener =
123         new NSSelector( "removeFocusListener",
124         new Class[] { FocusListener.class } );
125 
126     // null handling
127     protected boolean wasNull;
128     protected static final String EMPTY_STRING = "";
129 
130     // dirty handling
131     protected boolean needsUpdate;
132     protected boolean hasDocument;
133     protected boolean isListening;
134 
135     // formatting
136     protected Format format;
137 
138     // on-the-fly validation
139     protected boolean activeUpdate;
140 
141     // type conversion
142     protected Class lastKnownType;
143 
144     // cache the value aspect
145     private EODisplayGroup valueDisplayGroup;
146     private String valueKey;
147 
148     // hacky flags needed for no activeUpdate
149     private boolean pleaseIgnoreNextChange = false;
150     private boolean pleaseAcceptNextChange = false;
151     private boolean externallyChanged = true;
152 
153 
154     /***
155     * Constructor specifying the object to be controlled by this
156     * association.  Does not establish connection.
157     */
158     public TextAssociation ( Object anObject )
159     {
160         super( anObject );
161         wasNull = false;
162         needsUpdate = false;
163         activeUpdate = true;
164         hasDocument = false;
165         isListening = true;
166         valueDisplayGroup = null;
167         valueKey = null;
168         format = null;
169         lastKnownType = null;
170 
171         // register for idle notifications
172         NSSelector handleNotification =
173             new NSSelector( "handleNotification",
174                 new Class[] { NSNotification.class } );
175         NSNotificationCenter.defaultCenter().addObserver(
176             this, handleNotification, null, this );
177     }
178 
179     /***
180     * Returns a List of aspect signatures whose contents
181     * correspond with the aspects list.  Each element is
182     * a string whose characters represent a capability of
183     * the corresponding aspect. <ul>
184     * <li>"A" attribute: the aspect can be bound to
185     * an attribute.</li>
186     * <li>"1" to-one: the aspect can be bound to a
187     * property that returns a single object.</li>
188     * <li>"M" to-one: the aspect can be bound to a
189     * property that returns multiple objects.</li>
190     * </ul>
191     * An empty signature "" means that the aspect can
192     * bind without needing a key.
193     * This implementation returns "A1M" for each
194     * element in the aspects array.
195     */
196     public static NSArray aspectSignatures ()
197     {
198         return aspectSignatures;
199     }
200 
201     /***
202     * Returns a List that describes the aspects supported
203     * by this class.  Each element in the list is the string
204     * name of the aspect.  This implementation returns an
205     * empty list.
206     */
207     public static NSArray aspects ()
208     {
209         return aspects;
210     }
211 
212     /***
213     * Returns a List of EOAssociation subclasses that,
214     * for the objects that are usable for this association,
215     * are less suitable than this association.
216     */
217     public static NSArray associationClassesSuperseded ()
218     {
219         return new NSArray();
220     }
221 
222     /***
223     * Returns whether this class can control the specified
224     * object.
225     */
226     public static boolean isUsableWithObject ( Object anObject )
227     {
228         return setText.implementedByObject( anObject );
229     }
230 
231     /***
232     * Returns a List of properties of the controlled object
233     * that are controlled by this class.  For example,
234     * "stringValue", or "selected".
235     */
236     public static NSArray objectKeysTaken ()
237     {
238         return objectKeysTaken;
239     }
240 
241     /***
242     * Returns the aspect that is considered primary
243     * or default.  This is typically "value" or somesuch.
244     */
245     public static String primaryAspect ()
246     {
247         return ValueAspect;
248     }
249 
250     /***
251     * Returns whether this association can bind to the
252     * specified display group on the specified key for
253     * the specified aspect.
254     */
255     public boolean canBindAspect (
256         String anAspect, EODisplayGroup aDisplayGroup, String aKey)
257     {
258         return ( aspects.containsObject( anAspect ) );
259     }
260 
261     /***
262     * Binds the specified aspect of this association to the
263     * specified key on the specified display group.
264     */
265     public void bindAspect (
266         String anAspect, EODisplayGroup aDisplayGroup, String aKey )
267     {
268         if ( ValueAspect.equals( anAspect ) )
269         {
270             valueDisplayGroup = aDisplayGroup;
271             valueKey = aKey;
272         }
273         super.bindAspect( anAspect, aDisplayGroup, aKey );
274     }
275 
276     /***
277     * Establishes a connection between this association
278     * and the controlled object.  This implementation
279     * attempts to add this class as an ActionListener
280     * and as a FocusListener to the specified object.
281     */
282     public void establishConnection ()
283     {
284         Object component = object();
285         try
286         {
287             if ( addActionListener.implementedByObject( component ) )
288             {
289                 addActionListener.invoke( component, this );
290             }
291             if ( addFocusListener.implementedByObject( component ) )
292             {
293                 addFocusListener.invoke( component, this );
294             }
295             hasDocument = false;
296             if ( getDocument.implementedByObject( component ) )
297             {
298                 Object document = getDocument.invoke( component );
299                 if ( document instanceof Document )
300                 {
301                     ((Document)document).addDocumentListener( this );
302                     hasDocument = true;
303                 }
304             }
305         }
306         catch ( Exception exc )
307         {
308             throw new WotonomyException(
309                 "Error while establishing connection", exc );
310         }
311 
312         super.establishConnection();
313 
314         // forces update from bindings
315         subjectChanged();
316     }
317 
318     /***
319     * Breaks the connection between this association and
320     * its object.  Override to stop listening for events
321     * from the object.
322     */
323     public void breakConnection ()
324     {
325         Object component = object();
326         try
327         {
328             if ( removeActionListener.implementedByObject( component ) )
329             {
330                 removeActionListener.invoke( component, this );
331             }
332             if ( removeFocusListener.implementedByObject( component ) )
333             {
334                 removeFocusListener.invoke( component, this );
335             }
336             if ( getDocument.implementedByObject( component ) )
337             {
338                 Object document = getDocument.invoke( component );
339                 if ( document instanceof Document )
340                 {
341                     ((Document)document).removeDocumentListener( this );
342                 }
343             }
344         }
345         catch ( Exception exc )
346         {
347             throw new WotonomyException(
348                 "Error while breaking connection", exc );
349         }
350         super.breakConnection();
351     }
352 
353     public void objectWillChange( Object anObject )
354     {
355         super.objectWillChange( anObject );
356         externallyChanged = true;
357     }
358 
359     /***
360     * Called when either the selection or the contents
361     * of an associated display group have changed.
362     */
363     public void subjectChanged()
364     {
365         if ( pleaseIgnoreNextChange )
366         {
367           pleaseIgnoreNextChange = false;
368           externallyChanged = false;
369           return;
370         }
371 
372         externallyChanged = true;
373 
374         Object component = object();
375         EODisplayGroup displayGroup;
376         String key;
377         Object value;
378 
379         // value aspect
380         displayGroup = valueDisplayGroup;
381         if ( displayGroup != null )
382         {
383             if ( component instanceof Component )
384             {
385                 ((Component)component).setEnabled( 
386                     displayGroup.enabledToSetSelectedObjectValueForKey( valueKey ) );
387             }
388             
389             // if activeUpdate or we are not the editing association
390             if ( activeUpdate || displayGroup.editingAssociation() != this || pleaseAcceptNextChange )
391             {
392                 pleaseAcceptNextChange = false;
393                 key = valueKey;
394 
395                 if ( displayGroup.selectedObjects().size() > 1 )
396                 {
397                     // if there're more than one object selected, set
398                     // the value to blank for all of them.
399                     Object previousValue;
400 
401                     Iterator indexIterator = displayGroup.selectionIndexes().
402                                               iterator();
403 
404                     // get value for the first selected object.
405                     int initialIndex = ( (Integer)indexIterator.next() ).intValue();
406                     previousValue = displayGroup.valueForObjectAtIndex(
407                                               initialIndex, key );
408                     value = previousValue;
409 
410                     // go through the rest of the selected objects, compare each
411                     // value with the previous one. continue comparing if two
412                     // values are equal, break the while loop if they're different.
413                     // the final value will be the common value of all selected objects
414                     // if there is one, or be blank if there is not.
415                     while ( indexIterator.hasNext() )
416                     {
417                         int index = ( (Integer)indexIterator.next() ).intValue();
418                         Object currentValue = displayGroup.valueForObjectAtIndex(
419                                               index, key );
420                         if ( currentValue != null && previousValue != null
421                             && !currentValue.toString().equals( previousValue.toString() ) )                                 {
422                             value = null;
423                             break;
424                         }
425 
426                     } // end while
427 
428                 } else {
429 
430                     // if there's only one object selected.
431                     value = displayGroup.selectedObjectValueForKey( key );
432                } // end checking the size of selected objects in displayGroup
433 
434                 // null handling
435                 if ( value == null )
436                 {
437                         wasNull = true;
438                         value = EMPTY_STRING;
439                         lastKnownType = null;
440                 }
441                 else
442                 {
443                         wasNull = false;
444                         lastKnownType = value.getClass();
445                         if ( format() != null )
446                         {
447                                 try
448                                 {
449                                         value = format().format( value );
450                                 }
451                                 catch ( IllegalArgumentException exc )
452                                 {
453                                         value = value.toString();
454                                 }
455                         }
456                 }
457 
458 
459                 try
460                 {
461                     if ( needToReadValueFromDisplayGroup( value.toString(), getText ) )
462                     {
463                         // No need to listen for any events that might get fired
464                         // while setting the text since we are the one setting it.
465                         boolean wasListening = isListening;
466                         isListening = false;
467 
468                         // setText is an expensive operation
469                         setText.invoke( component, value.toString() );
470 
471                         isListening = wasListening;
472                         needsUpdate = false;
473                     }
474                 }
475                 catch ( Exception exc )
476                 {
477                     throw new WotonomyException(
478                         "Error while updating component connection", exc );
479                 }
480             }
481         }
482 
483         // icon aspect
484         displayGroup = displayGroupForAspect( IconAspect );
485         key = displayGroupKeyForAspect( IconAspect );
486         if ( key != null )
487         {
488             if ( displayGroup != null )
489             {
490                 value =
491                     displayGroup.selectedObjectValueForKey( key );
492             }
493             else
494             {
495                             // treat bound key without display group
496                             // as a resource to be loaded from the selected class.
497                             value = null;
498                             Object o = displayGroup.selectedObject();
499                             if ( o != null )
500                             {
501                                 URL url = o.getClass().getResource( key );
502                                 if ( url != null )
503                                 {
504                                     value = new ImageIcon( url );
505                                 }
506                             }
507             }
508 
509             try
510             {
511                 setIcon.invoke( component, value );
512             }
513             catch ( Exception exc )
514             {
515                 throw new WotonomyException(
516                     "Error while updating component connection", exc );
517             }
518         }
519 
520         // enabled aspect
521         displayGroup = displayGroupForAspect( EnabledAspect );
522         key = displayGroupKeyForAspect( EnabledAspect );
523         if ( ( key != null )
524         && ( component instanceof Component ) )
525         {
526             if ( displayGroup != null )
527             {
528                 value =
529                     displayGroup.selectedObjectValueForKey( key );
530             }
531             else
532             {
533                 // treat bound key without display group as a value
534                 value = key;
535             }
536             Boolean converted = null;
537             if ( value != null ) 
538             {
539                 converted = (Boolean)
540                     ValueConverter.convertObjectToClass(
541                         value, Boolean.class );
542             }
543             if ( converted == null ) converted = Boolean.FALSE;
544             if ( ((Component)component).isEnabled() != converted.booleanValue() )
545             {
546                 ((Component)component).setEnabled( converted.booleanValue() );
547             }
548         }
549 
550         // editable aspect
551         displayGroup = displayGroupForAspect( EditableAspect );
552         key = displayGroupKeyForAspect( EditableAspect );
553         if ( ( key != null )
554         && ( component instanceof JTextComponent ) )
555         {
556             if ( displayGroup != null )
557             {
558                 value =
559                     displayGroup.selectedObjectValueForKey( key );
560             }
561             else
562             {
563                 // treat bound key without display group as a value
564                 value = key;
565             }
566             Boolean converted = (Boolean)
567                 ValueConverter.convertObjectToClass(
568                     value, Boolean.class );
569 
570             if ( converted != null )
571             {
572                 if ( converted.booleanValue() != ((JTextComponent)component).isEditable() )
573                 {
574                     ((JTextComponent)component).setEditable( converted.booleanValue() );
575                 }
576             }
577         }
578 
579         // visible aspect
580         displayGroup = displayGroupForAspect( VisibleAspect );
581         key = displayGroupKeyForAspect( VisibleAspect );
582         if ( ( key != null )
583         && ( component instanceof Component ) )
584         {
585             if ( displayGroup != null )
586             {
587                 value =
588                     displayGroup.selectedObjectValueForKey( key );
589             }
590             else
591             {
592                 // treat bound key without display group as a value
593                 value = key;
594             }
595             Boolean converted = (Boolean)
596                 ValueConverter.convertObjectToClass(
597                     value, Boolean.class );
598 
599             if ( converted != null )
600             {
601                 if ( converted.booleanValue() != ((Component)component).isVisible() )
602                 {
603                     ((Component)component).setVisible( converted.booleanValue() );
604                 }
605             }
606         }
607 
608         // label aspect
609         displayGroup = displayGroupForAspect( LabelAspect );
610         key = displayGroupKeyForAspect( LabelAspect );
611 
612         if ( ( key != null )
613         && ( component instanceof JTextComponent ) )
614         {
615             if ( displayGroup != null )
616             {
617                 value =
618                     displayGroup.selectedObjectValueForKey( key );
619             }
620             else
621             {
622                 // treat bound key without display group as a value
623                 value = key;
624             }
625             Boolean converted = (Boolean)
626                 ValueConverter.convertObjectToClass(
627                     value, Boolean.class );
628 
629             if ( converted != null )
630             {
631                 if ( converted.booleanValue() )
632                 {
633                                         if ( component instanceof JTextComponent )
634                                         {
635                                             if ( component instanceof JTextArea )
636                                             {
637                                                 areaToLabel( (JTextArea) component );
638                                             }
639                                             else
640                                             {
641                                                 fieldToLabel( (JTextComponent) component );
642                                             }
643                                         }
644                 }
645                 else
646                 {
647                                         if ( component instanceof JTextComponent )
648                                         {
649                                             if ( component instanceof JTextArea )
650                                             {
651                                                 labelToArea( (JTextArea) component );
652                                             }
653                                             else
654                                             {
655                                                 labelToField( (JTextComponent ) component );
656                                             }
657                                         }
658                                 }
659             }
660         }
661     }
662     
663     private void fieldToLabel( JTextComponent aTextField )
664     {
665         // turn on wrapping and disable editing and highlighting
666 
667         aTextField.setEditable(false);
668         aTextField.setOpaque(false);
669 
670         // Set the border, colors and font to that of a label
671 
672         //LookAndFeel.installBorder(aTextField, "Label.border");
673         aTextField.setBorder( null );
674 
675         LookAndFeel.installColorsAndFont(aTextField,
676             "Label.background",
677             "Label.foreground",
678             "Label.font");
679     }
680 
681     private void labelToField( JTextComponent aTextField )
682     {
683         // turn on wrapping and disable editing and highlighting
684 
685         aTextField.setEditable(true);
686         aTextField.setOpaque(true);
687 
688         // Set the border, colors and font to that of a label
689 
690         LookAndFeel.installBorder(aTextField, "TextField.border");
691 
692         LookAndFeel.installColorsAndFont(aTextField,
693             "TextField.background",
694             "TextField.foreground",
695             "TextField.font");
696     }
697 
698     private void areaToLabel( JTextArea aTextArea )
699     {
700             // turn on wrapping and disable editing and highlighting
701 
702             aTextArea.setLineWrap(true);
703             aTextArea.setWrapStyleWord(true);
704             aTextArea.setEditable(false);
705 
706             // Set the text area's border, colors and font to
707             // that of a label
708 
709             //LookAndFeel.installBorder(aTextArea, "Label.border");
710             aTextArea.setBorder( null );
711 
712             LookAndFeel.installColorsAndFont(aTextArea,
713                 "Label.background",
714                 "Label.foreground",
715                 "Label.font");
716 
717     }
718 
719     private void labelToArea( JTextArea aTextArea )
720     {
721             // turn on wrapping and disable editing and highlighting
722 
723             aTextArea.setEditable(true);
724 
725             // Set the border, colors and font to that of a label
726 
727             LookAndFeel.installBorder(aTextArea, "TextArea.border");
728 
729             LookAndFeel.installColorsAndFont(aTextArea,
730                 "TextArea.background",
731                 "TextArea.foreground",
732                 "TextArea.font");
733     }
734 
735 
736     /***
737     * Forces this association to cause the object to
738     * stop editing and validate the user's input.
739     * @return false if there were problems validating,
740     * or true to continue.
741     */
742     public boolean endEditing()
743     {
744         pleaseAcceptNextChange = true;
745         pleaseIgnoreNextChange = false;
746         return writeValueToDisplayGroup();
747     }
748 
749     /***
750     * Writes the value currently in the component
751     * to the selected object in the display group
752     * bound to the value aspect.
753     * @return false if there were problems validating,
754     * or true to continue.
755     */
756     protected boolean writeValueToDisplayGroup()
757     {
758             boolean returnValue = true;
759             if ( hasDocument && !needsUpdate ) return true;
760 
761             EODisplayGroup displayGroup = valueDisplayGroup;
762             if ( displayGroup != null )
763             {
764                 String key = valueKey;
765                 Object component = object();
766                 Object value = null;
767                 try
768                 {
769                         //if ( getText.implementedByObject( component ) )
770                         //{
771                                 value = getText.invoke( component );
772                         //}
773                 }
774                 catch ( Exception exc )
775                 {
776                         throw new WotonomyException(
777                                 "Error updating display group", exc );
778                 }
779 
780                 if ( ( wasNull ) && ( EMPTY_STRING.equals( value ) ) )
781                 {
782                         value = null;
783                 }
784                 else
785                 if ( format() != null )
786                 {
787                     try
788                     {
789                         value = format().parseObject( value.toString() );
790                     }
791                     catch ( ParseException exc )
792                     {
793                         String message = exc.getMessage();
794                         //"That format was not recognized.";
795                         if ( displayGroup.associationFailedToValidateValue(
796                                 this, value.toString(), key, exc, message ) )
797                         {
798                             boolean wasListening = isListening;
799                             isListening = false;
800                             JOptionPane.showMessageDialog(
801                                 (Component)component, message );
802                             isListening = wasListening;
803                         }
804                         needsUpdate = false;
805                         return false;
806                     }
807                 }
808 
809                 if ( ( lastKnownType != null ) && ( value != null ) )
810                 {
811                     // convert back to last known type, if necessary/possible
812                     Class type = value.getClass();
813                     if ( ( type != null ) && ( type != lastKnownType ) )
814                     {
815                         Object converted =
816                             ValueConverter.convertObjectToClass(
817                                 value, lastKnownType );
818                         if ( converted != null )
819                         {
820                             value = converted;
821                         }
822                         // else: not possible, ignore
823                     }
824                 }
825 
826                 needsUpdate = false;
827 
828                 // only update if the value is different from the one in the display group
829                 if ( ! needToWriteValueToDisplayGroup( value, displayGroup ) ) return true;
830 
831                 // we might lose focus if display group displays a validation message
832                 boolean wasListening = isListening;
833                 isListening = false;
834 
835                 Iterator selectedIterator = displayGroup.selectionIndexes().iterator();
836                 while ( selectedIterator.hasNext() )
837                 {
838                     int index = ( (Integer)selectedIterator.next() ).intValue();
839 
840                     if ( displayGroup.setValueForObjectAtIndex( value, index, key ) )
841                     {
842                         needsUpdate = false;
843                     }
844                     else
845                     {
846                         needsUpdate = false;
847                         returnValue = false;
848                     }
849                 }
850                 isListening = wasListening;
851 
852             }
853             return returnValue;
854     }
855 
856     /***
857     * Called to determine whether the display group needs to be
858     * updated.  This implementation reads the value from the display
859     * group and only returns true if the specified value is different.
860     * This is done as an optimization since writes are more expensive
861     * than reads.  Override to customize this behavior.
862     */
863     protected boolean needToWriteValueToDisplayGroup(
864         Object aValue, EODisplayGroup aDisplayGroup )
865     {
866         Object existingValue = aDisplayGroup.selectedObjectValueForKey( valueKey );
867         if ( aDisplayGroup.selectedObjects().size() == 1 )
868         {
869             if ( existingValue == aValue ) return false;
870             if ( ( existingValue != null ) && ( existingValue.equals( aValue ) ) ) return false;
871             if ( ( aValue != null ) && ( aValue.equals( existingValue ) ) ) return false;
872         }
873         return true;
874     }
875 
876     /***
877     * Called to determine whether the controlled component needs to be
878     * updated.  This implementation reads the value from the selector
879     * and only returns true if the specified value is different.
880     * This is done as an optimization since updating the component
881     * can be an expensive operation.  Override to customize this behavior.
882     */
883     protected boolean needToReadValueFromDisplayGroup(
884         Object aValue, NSSelector aSelector )
885     throws IllegalAccessException, InvocationTargetException, NoSuchMethodException
886     {
887         return !aValue.toString().equals( aSelector.invoke( object() ) );
888     }
889 
890     /***
891     * Sets the Format that is used to convert values from the display
892     * group to and from text that is displayed in the component.
893     */
894     public void setFormat( Format aFormat )
895     {
896         format = aFormat;
897     }
898 
899     /***
900     * Gets the Format that is used to convert values from the display
901     * group to and from text that is displayed in the component.
902     */
903     public Format format()
904     {
905         return format;
906     }
907 
908     /***
909     * Returns whether the text association is configured to actively
910     * update the model in response to changes in the component.
911     */
912     public boolean isActiveUpdate()
913     {
914         return activeUpdate;
915     }
916 
917     /***
918     * Sets whether the text association should actively
919     * update the model in response to changes in the component.
920     * Default is true. False indicates that the model will be updated
921     * only when the component loses focus or fires an action event.
922     */
923     public void setActiveUpdate( boolean isActiveUpdate )
924     {
925         activeUpdate = isActiveUpdate;
926     }
927 
928     // interface ActionListener
929 
930     /***
931     * Updates object on action performed.
932     */
933     public void actionPerformed( ActionEvent evt )
934     {
935         if ( ! isListening ) return;
936         if ( needsUpdate )
937         {
938             pleaseAcceptNextChange = true; // needed if activeUpdate = false
939             writeValueToDisplayGroup();
940         }
941     }
942 
943     // interface FocusListener
944 
945     /***
946     * Notifies of beginning of edit.
947     */
948     public void focusGained(FocusEvent evt)
949     {
950         if ( ! isListening ) return;
951 
952         pleaseAcceptNextChange = true;
953         externallyChanged = true;
954 
955         Object o;
956         EODisplayGroup displayGroup;
957         Enumeration e = aspects().objectEnumerator();
958         while ( e.hasMoreElements() )
959         {
960             displayGroup =
961                 displayGroupForAspect( e.nextElement().toString() );
962             if ( displayGroup != null )
963             {
964                 displayGroup.associationDidBeginEditing( this );
965             }
966         }
967     }
968 
969     /***
970     * Updates object on focus lost and notifies of end of edit.
971     */
972     public void focusLost(FocusEvent evt)
973     {
974         if ( ! isListening ) return;
975         if ( endEditing() )
976         {
977             Object o;
978             EODisplayGroup displayGroup;
979             Enumeration e = aspects().objectEnumerator();
980             while ( e.hasMoreElements() )
981             {
982                 displayGroup =
983                     displayGroupForAspect( e.nextElement().toString() );
984                 if ( displayGroup != null )
985                 {
986                     displayGroup.associationDidEndEditing( this );
987                 }
988             }
989         }
990         else
991         {
992             // probably should notify of a validation error here,
993         }
994     }
995 
996     /***
997     * Queues a notification to PostWhenIdle.
998     */
999     protected void queueUpdate(DocumentEvent e)
1000     {
1001         if ( e.getDocument() instanceof DefaultStyledDocument )
1002         {
1003             if ( e instanceof AbstractDocument.DefaultDocumentEvent )
1004             {
1005                 int docLength = e.getDocument().getLength();
1006 
1007                 if ( ( e.getType().equals( DocumentEvent.EventType.CHANGE ) ) )
1008                 {
1009                     if ( e.getOffset() == 0 && e.getLength() == docLength )
1010                     {
1011                         // ignore document events for the whole document
1012                         //  since default styled document broadcasts these
1013                         //  using invokeLater, and we've already received
1014                         //  notification about the actual style change.
1015                         // see: DefaultStyledDocument.ChangeUpdateRunnable
1016                         return;
1017                     }
1018                 }
1019             }
1020         }
1021 
1022         NSNotificationQueue.defaultQueue().enqueueNotification(
1023             new NSNotification( "TextAssociation.DocumentChanged", this,
1024             new NSDictionary( new Object[] { "event" }, new Object[] { e } ) ),
1025             NSNotificationQueue.PostWhenIdle );
1026     }
1027 
1028     /***
1029     * Handles idle notification.
1030     */
1031     public void handleNotification( NSNotification aNotification )
1032     {
1033         if ( activeUpdate )
1034         {
1035             writeValueToDisplayGroup();
1036         }
1037     }
1038 
1039    // interface DocumentListener
1040 
1041    public void insertUpdate(DocumentEvent e)
1042    {
1043        if ( ! isListening ) return;
1044        needsUpdate = true;
1045        queueUpdate( e );
1046    }
1047 
1048    public void removeUpdate(DocumentEvent e)
1049    {
1050        if ( ! isListening ) return;
1051        needsUpdate = true;
1052        queueUpdate( e );
1053    }
1054 
1055    public void changedUpdate(DocumentEvent e)
1056    {
1057        if ( ! isListening ) return;
1058        needsUpdate = true;
1059        queueUpdate( e );
1060    }
1061 
1062 }
1063 
1064 /*
1065  * $Log$
1066  * Revision 1.2  2006/02/18 23:19:05  cgruber
1067  * Update imports and maven dependencies.
1068  *
1069  * Revision 1.1  2006/02/16 13:22:22  cgruber
1070  * Check in all sources in eclipse-friendly maven-enabled packages.
1071  *
1072  * Revision 1.41  2004/02/05 02:18:18  mpowers
1073  * Now setting border to null to new Aqua LAF behaves.
1074  *
1075  * Revision 1.40  2004/01/28 22:47:56  mpowers
1076  * un-activeUpdate was brokne.
1077  *
1078  * Revision 1.39  2004/01/28 18:34:57  mpowers
1079  * Better handling for enabling.
1080  * Now respecting enabledToSetSelectedObjectValueForKey from display group.
1081  *
1082  * Revision 1.38  2003/08/06 23:07:52  chochos
1083  * general code cleanup (mostly, removing unused imports)
1084  *
1085  * Revision 1.37  2003/02/06 16:21:34  mpowers
1086  * Fix for activeUpdate: no longer bothering with editing context's changes.
1087  *
1088  * Revision 1.36  2002/10/24 18:19:24  mpowers
1089  * Bug fix - thanks to dwang.
1090  *
1091  * Revision 1.35  2002/08/02 19:19:30  mpowers
1092  * Added control points for when to read or write from the display group.
1093  * Added flags needed to fix problems with non-activeUpdate and commit key.
1094  *
1095  * Revision 1.33  2002/03/08 23:18:01  mpowers
1096  * Added visible aspect.
1097  *
1098  * Revision 1.32  2002/03/06 16:13:53  mpowers
1099  * Yet another fix for style document changes: swing's DefaultStyledDocument
1100  * using an invoke later to launch a final StyleChanged event, which occurs
1101  * after the TextAssociation has reestablished itself as a document listener,
1102  * causing the item to be marked dirty.  We're now handling this case.
1103  *
1104  * Revision 1.31  2002/03/04 22:10:37  mpowers
1105  * Supressing active update only marks dirty when contents have changed.
1106  *
1107  * Revision 1.30  2002/02/23 16:19:12  mpowers
1108  * Now only marking an editing context as dirty if it's not already dirty.
1109  *
1110  * Revision 1.29  2002/02/19 18:38:29  mpowers
1111  * Minor optimization: activeUpdate now checked in handleNotification.
1112  *
1113  * Revision 1.28  2002/02/19 16:36:47  mpowers
1114  * Better support for active update: objects are now marked as changed
1115  * even though the model itself is not updated -- this allows editing
1116  * context itself to be marked as having changes to be saved.
1117  *
1118  * Revision 1.27  2002/01/23 19:50:11  mpowers
1119  * Fix for a null pointer when value is null and last known type is not.
1120  * (from dwang)
1121  *
1122  * Revision 1.26  2002/01/14 19:37:22  mpowers
1123  * Fix for NPE when value is null and auto update is false.
1124  *
1125  * Revision 1.25  2001/12/10 03:16:11  mpowers
1126  * Fixed bug with isListening when no items are in display group.
1127  *
1128  * Revision 1.24  2001/11/16 19:14:51  mpowers
1129  * Brought back the idea of configuring whether updates occur on each change.
1130  *
1131  * Revision 1.23  2001/11/08 20:06:06  mpowers
1132  * Now performing type-conversion as a convenience.
1133  *
1134  * Revision 1.22  2001/11/04 18:24:20  mpowers
1135  * Better handling for non-string values when bulk-editing.
1136  *
1137  * Revision 1.21  2001/11/01 15:53:34  mpowers
1138  * Now that NSNotificationQueue correctly implements PostWhenIdle, we can
1139  * finally discard our use of Swing's Timer in favor of using the queue
1140  * to coalesce document changed events.
1141  *
1142  * Revision 1.20  2001/10/26 19:58:06  mpowers
1143  * Better handling for non-string types.  We were testing with equals with the
1144  * new value against the existing value in the component.  Now we convert
1145  * the new value to a string before comparing.  Fixes case for properties
1146  * of non-String types, like StringBuffer.
1147  *
1148  * Revision 1.19  2001/09/30 21:57:14  mpowers
1149  * Timers were not getting cleaned up if breakConnection was called
1150  * before the timer got a chance to fire.
1151  *
1152  * Revision 1.18  2001/08/22 15:42:26  mpowers
1153  * Added support for JTextComponent label-izing.
1154  *
1155  * Revision 1.17  2001/07/30 16:32:55  mpowers
1156  * Implemented support for bulk-editing.  Detail associations will now
1157  * apply changes to all selected objects.
1158  *
1159  * Revision 1.16  2001/07/17 19:53:37  mpowers
1160  * Made some private fields protected for benefit of subclassers.
1161  *
1162  * Revision 1.15  2001/06/30 14:59:36  mpowers
1163  * LabelAspect now sets the text field's opaque setting.
1164  *
1165  * Revision 1.14  2001/06/29 14:54:08  mpowers
1166  * Another fix for timers - timers were definitely causing a memory leak.
1167  *
1168  * Revision 1.13  2001/06/26 21:37:19  mpowers
1169  * Fixed a null pointer in the new key timer scheme.
1170  *
1171  * Revision 1.12  2001/06/25 14:46:03  mpowers
1172  * Fixed a memory leak involving the use of timers.
1173  *
1174  * Revision 1.11  2001/06/01 19:14:59  mpowers
1175  * Text association's enabled aspect is now more discriminating.
1176  *
1177  * Revision 1.10  2001/05/18 21:07:24  mpowers
1178  * Changed the way we handle failure to update object value.
1179  *
1180  * Revision 1.9  2001/03/13 21:39:58  mpowers
1181  * Improved validation handling.
1182  *
1183  * Revision 1.8  2001/03/12 12:49:10  mpowers
1184  * Improved validation handling.
1185  * Having a formatter disables auto-updating.
1186  *
1187  * Revision 1.7  2001/03/09 22:08:13  mpowers
1188  * Now handling any objects that have a valid Document.
1189  * No longer checking enabled before updating the enabled state.
1190  *
1191  * Revision 1.6  2001/03/07 19:57:32  mpowers
1192  * Fixed paste error in IconAspect.
1193  *
1194  * Revision 1.4  2001/02/17 16:52:05  mpowers
1195  * Changes in imports to support building with jdk1.1 collections.
1196  *
1197  * Revision 1.3  2001/01/31 19:12:33  mpowers
1198  * Implemented auto-updating in TextComponent.
1199  *
1200  * Revision 1.2  2001/01/10 15:53:58  mpowers
1201  * Preventing a null pointer exception if getText were to return null,
1202  * which doesn't happen for JTextFields but might happen for other objects.
1203  *
1204  * Revision 1.1.1.1  2000/12/21 15:49:08  mpowers
1205  * Contributing wotonomy.
1206  *
1207  * Revision 1.13  2000/12/20 16:25:41  michael
1208  * Added log to all files.
1209  *
1210  *
1211  */
1212