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.net.URL;
27  import java.text.Format;
28  import java.text.ParseException;
29  import java.util.Enumeration;
30  import java.util.Iterator;
31  
32  import javax.swing.Icon;
33  import javax.swing.ImageIcon;
34  import javax.swing.JOptionPane;
35  import javax.swing.JTextArea;
36  import javax.swing.LookAndFeel;
37  import javax.swing.Timer;
38  import javax.swing.event.DocumentEvent;
39  import javax.swing.event.DocumentListener;
40  import javax.swing.text.Document;
41  import javax.swing.text.JTextComponent;
42  
43  import net.wotonomy.foundation.NSArray;
44  import net.wotonomy.foundation.NSSelector;
45  import net.wotonomy.foundation.internal.ValueConverter;
46  import net.wotonomy.foundation.internal.WotonomyException;
47  import net.wotonomy.ui.EOAssociation;
48  import net.wotonomy.ui.EODisplayGroup;
49  
50  /***
51  * TimedTextAssociation works like TextAssociation,
52  * but instead of using a delayed event to update the
53  * model, it uses a timer so that the model is only
54  * updated if the user pauses typing for some short interval.
55  * This is useful when the update and/or re-read of the model
56  * is a costly operation.
57  * Bindings are:
58  * <ul>
59  * <li>value: a property convertable to/from a string</li>
60  * <li>editable: a boolean property that determines whether
61  * the user can edit the text in the field</li>
62  * <li>enabled: a boolean property that determines whether
63  * the user can select the text in the field</li>
64  * <li>label: a boolean property that determines whether
65  * field should appear as a read-only, selectable label</li>
66  * <li>icon: a property that returns a Swing icon, for use
67  * with JLabels and other components with setIcon() methods.
68  * If bound to a static string, the string will be used to
69  * load an image resource from the selected object's class.</li>
70  * </ul>
71  *
72  * @author michael@mpowers.net
73  * @author $Author: cgruber $
74  * @version $Revision: 904 $
75  */
76  public class TimedTextAssociation extends EOAssociation
77  	implements FocusListener, ActionListener, DocumentListener
78  {
79      //TODO: need to refactor this so that it can subclass text association.
80      //This implementation is basically a branch from the v1.20 TextAssociation.
81      
82      static final NSArray aspects =
83          new NSArray( new Object[] {
84              ValueAspect, EnabledAspect, EditableAspect, LabelAspect, IconAspect
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", "enabled", "editable"
96          } );
97  
98  	private final static NSSelector getText =
99  		new NSSelector( "getText" );
100 	private final static NSSelector setText =
101 		new NSSelector( "setText",
102 		new Class[] { String.class } );
103 	private final static NSSelector getDocument =
104 		new NSSelector( "getDocument" );
105 	private final static NSSelector setIcon =
106 		new NSSelector( "setIcon",
107 		new Class[] { Icon.class } );
108 	private final static NSSelector addActionListener =
109 		new NSSelector( "addActionListener",
110 		new Class[] { ActionListener.class } );
111 	private final static NSSelector removeActionListener =
112 		new NSSelector( "removeActionListener",
113 		new Class[] { ActionListener.class } );
114 	private final static NSSelector addFocusListener =
115 		new NSSelector( "addFocusListener",
116 		new Class[] { FocusListener.class } );
117 	private final static NSSelector removeFocusListener =
118 		new NSSelector( "removeFocusListener",
119 		new Class[] { FocusListener.class } );
120 
121 	// null handling
122 	protected boolean wasNull;
123 	protected static final String EMPTY_STRING = "";
124 
125 	// dirty handling
126 	protected boolean needsUpdate;
127 	protected boolean hasDocument;
128     protected boolean isListening;
129 
130 	// formatting
131 	protected Format format;
132 
133     // cache the value aspect
134     private EODisplayGroup valueDisplayGroup;
135     private String valueKey;
136 
137     // coalescing document events
138     protected boolean autoUpdating;
139     protected int interval = 400; // adjust as needed
140     protected Timer keyTimer;
141 
142     // NOTE: a new key timer is created for each use and
143     // is disposed when the timer is stopped.
144     // Swing's Timer class is kept in a static list of timers
145     // and each retains a strong reference to their listeners.
146     // This caused a memory leak as associations typically
147     // refer to their controlled component which is referred
148     // to by its parents and so on until no application window
149     // will ever get garbage collected.  yikes.
150 
151     /***
152     * Constructor specifying the object to be controlled by this
153     * association.  Does not establish connection.
154     */
155     public TimedTextAssociation ( Object anObject )
156     {
157         super( anObject );
158 		wasNull = false;
159 		needsUpdate = false;
160 		hasDocument = false;
161         isListening = true;
162         valueDisplayGroup = null;
163         valueKey = null;
164 
165         autoUpdating = true;
166         keyTimer = null;
167     }
168 
169     /***
170     * Returns a List of aspect signatures whose contents
171     * correspond with the aspects list.  Each element is
172     * a string whose characters represent a capability of
173     * the corresponding aspect. <ul>
174     * <li>"A" attribute: the aspect can be bound to
175     * an attribute.</li>
176     * <li>"1" to-one: the aspect can be bound to a
177     * property that returns a single object.</li>
178     * <li>"M" to-one: the aspect can be bound to a
179     * property that returns multiple objects.</li>
180     * </ul>
181     * An empty signature "" means that the aspect can
182     * bind without needing a key.
183     * This implementation returns "A1M" for each
184     * element in the aspects array.
185     */
186     public static NSArray aspectSignatures ()
187     {
188         return aspectSignatures;
189     }
190 
191     /***
192     * Returns a List that describes the aspects supported
193     * by this class.  Each element in the list is the string
194     * name of the aspect.  This implementation returns an
195     * empty list.
196     */
197     public static NSArray aspects ()
198     {
199         return aspects;
200     }
201 
202     /***
203     * Returns a List of EOAssociation subclasses that,
204     * for the objects that are usable for this association,
205     * are less suitable than this association.
206     */
207     public static NSArray associationClassesSuperseded ()
208     {
209         return new NSArray();
210     }
211 
212     /***
213     * Returns whether this class can control the specified
214     * object.
215     */
216     public static boolean isUsableWithObject ( Object anObject )
217     {
218         return setText.implementedByObject( anObject );
219     }
220 
221     /***
222     * Returns a List of properties of the controlled object
223     * that are controlled by this class.  For example,
224     * "stringValue", or "selected".
225     */
226     public static NSArray objectKeysTaken ()
227     {
228         return objectKeysTaken;
229     }
230 
231     /***
232     * Returns the aspect that is considered primary
233     * or default.  This is typically "value" or somesuch.
234     */
235     public static String primaryAspect ()
236     {
237         return ValueAspect;
238     }
239 
240     /***
241     * Returns whether this association can bind to the
242     * specified display group on the specified key for
243     * the specified aspect.
244     */
245     public boolean canBindAspect (
246         String anAspect, EODisplayGroup aDisplayGroup, String aKey)
247     {
248         return ( aspects.containsObject( anAspect ) );
249     }
250 
251     /***
252     * Binds the specified aspect of this association to the
253     * specified key on the specified display group.
254     */
255     public void bindAspect (
256         String anAspect, EODisplayGroup aDisplayGroup, String aKey )
257 	{
258 		if ( ValueAspect.equals( anAspect ) )
259 		{
260 			valueDisplayGroup = aDisplayGroup;
261 			valueKey = aKey;
262 		}
263 		super.bindAspect( anAspect, aDisplayGroup, aKey );
264 	}
265 
266     /***
267     * Establishes a connection between this association
268     * and the controlled object.  This implementation
269 	* attempts to add this class as an ActionListener
270 	* and as a FocusListener to the specified object.
271     */
272     public void establishConnection ()
273     {
274 		Object component = object();
275 		try
276 		{
277 			if ( addActionListener.implementedByObject( component ) )
278 			{
279 				addActionListener.invoke( component, this );
280 			}
281 			if ( addFocusListener.implementedByObject( component ) )
282 			{
283 				addFocusListener.invoke( component, this );
284 			}
285             hasDocument = false;
286 			if ( getDocument.implementedByObject( component ) )
287 			{
288 				Object document = getDocument.invoke( component );
289                 if ( document instanceof Document )
290                 {
291                     ((Document)document).addDocumentListener( this );
292                     hasDocument = true;
293                 }
294 			}
295 		}
296 		catch ( Exception exc )
297 		{
298 			throw new WotonomyException(
299 				"Error while establishing connection", exc );
300 		}
301 
302         super.establishConnection();
303 
304 		// forces update from bindings
305 		subjectChanged();
306     }
307 
308     /***
309     * Breaks the connection between this association and
310     * its object.  Override to stop listening for events
311     * from the object.
312     */
313     public void breakConnection ()
314     {
315 		Object component = object();
316 		try
317 		{
318 			if ( removeActionListener.implementedByObject( component ) )
319 			{
320 				removeActionListener.invoke( component, this );
321 			}
322 			if ( removeFocusListener.implementedByObject( component ) )
323 			{
324 				removeFocusListener.invoke( component, this );
325 			}
326 			if ( getDocument.implementedByObject( component ) )
327 			{
328 				Object document = getDocument.invoke( component );
329                 if ( document instanceof Document )
330                 {
331                     ((Document)document).removeDocumentListener( this );
332                 }
333 			}
334 		}
335 		catch ( Exception exc )
336 		{
337 			throw new WotonomyException(
338 				"Error while breaking connection", exc );
339 		}
340         super.breakConnection();
341     }
342 
343     /***
344     * Called when either the selection or the contents
345     * of an associated display group have changed.
346     */
347     public void subjectChanged ()
348     {
349 		Object component = object();
350 		EODisplayGroup displayGroup;
351 		String key;
352 		Object value;
353 
354 		// value aspect
355 		displayGroup = valueDisplayGroup;
356 		if ( displayGroup != null )
357 		{
358             if ( component instanceof Component )
359             {
360                 ((Component)component).setEnabled( 
361                     displayGroup.enabledToSetSelectedObjectValueForKey( valueKey ) );
362             }
363             
364 			key = valueKey;
365 
366                         if ( displayGroup.selectedObjects().size() > 1 )
367                         {
368                             // if there're more than one object selected, set
369                             // the value to blank for all of them.
370                             Object previousValue;
371 
372                             Iterator indexIterator = displayGroup.selectionIndexes().
373                                                       iterator();
374 
375                             // get value for the first selected object.
376                             int initialIndex = ( (Integer)indexIterator.next() ).intValue();
377                             previousValue = displayGroup.valueForObjectAtIndex(
378                                                       initialIndex, key );
379                             value = null;
380 
381                             // go through the rest of the selected objects, compare each
382                             // value with the previous one. continue comparing if two
383                             // values are equal, break the while loop if they're different.
384                             // the final value will be the common value of all selected objects
385                             // if there is one, or be blank if there is not.
386                             while ( indexIterator.hasNext() )
387                             {
388                                 int index = ( (Integer)indexIterator.next() ).intValue();
389                                 Object currentValue = displayGroup.valueForObjectAtIndex(
390                                                       index, key );
391                                 if ( currentValue != null && !currentValue.equals( previousValue ) )
392                                 {
393                                     value = null;
394                                     break;
395                                 }
396                                 else
397                                 {
398                                     // currentValue is the same as the previous one
399                                     value = currentValue;
400                                 }
401 
402                             } // end while
403 
404                         } else {
405 
406                             // if there's only one object selected.
407                             value = displayGroup.selectedObjectValueForKey( key );
408                        } // end checking the size of selected objects in displayGroup
409 
410                         // convert value to string
411                         if ( value == null )
412                         {
413                                 wasNull = true;
414                                 value = EMPTY_STRING;
415                         }
416                         else
417                         {
418                                 wasNull = false;
419                                 if ( format() != null )
420                                 {
421                                         try
422                                         {
423                                                 value = format().format( value );
424                                         }
425                                         catch ( IllegalArgumentException exc )
426                                         {
427                                                 value = value.toString();
428                                         }
429                                 }
430                         }
431 
432 
433 			try
434 			{
435                             if ( ! value.toString().equals( getText.invoke( component ) ) )
436                             {
437                                 // No need to listen for any events that might get fired
438                                 // while setting the text since we are the one setting it.
439                                 boolean wasListening = isListening;
440                                 isListening = false;
441 
442                                 // setText is an expensive operation
443                                 setText.invoke( component, value.toString() );
444 
445                                 isListening = wasListening;
446                                 needsUpdate = false;
447                             }
448 			}
449 			catch ( Exception exc )
450 			{
451 				throw new WotonomyException(
452 					"Error while updating component connection", exc );
453 			}
454 		}
455 
456 		// icon aspect
457 		displayGroup = displayGroupForAspect( IconAspect );
458 		key = displayGroupKeyForAspect( IconAspect );
459 		if ( key != null )
460 		{
461 			if ( displayGroup != null )
462 			{
463 				value =
464 					displayGroup.selectedObjectValueForKey( key );
465 			}
466 			else
467 			{
468                             // treat bound key without display group
469                             // as a resource to be loaded from the selected class.
470                             value = null;
471                             Object o = displayGroup.selectedObject();
472                             if ( o != null )
473                             {
474                                 URL url = o.getClass().getResource( key );
475                                 if ( url != null )
476                                 {
477                                     value = new ImageIcon( url );
478                                 }
479                             }
480 			}
481 
482 			try
483 			{
484                 setIcon.invoke( component, value );
485 			}
486 			catch ( Exception exc )
487 			{
488 				throw new WotonomyException(
489 					"Error while updating component connection", exc );
490 			}
491 		}
492 
493 		// enabled aspect
494 		displayGroup = displayGroupForAspect( EnabledAspect );
495 		key = displayGroupKeyForAspect( EnabledAspect );
496 		if ( ( key != null )
497 		&& ( component instanceof Component ) )
498 		{
499 			if ( displayGroup != null )
500 			{
501 				value =
502 					displayGroup.selectedObjectValueForKey( key );
503 			}
504 			else
505 			{
506 				// treat bound key without display group as a value
507 				value = key;
508 			}
509             Boolean converted = null;
510             if ( value != null ) 
511             {
512                 converted = (Boolean)
513                     ValueConverter.convertObjectToClass(
514                         value, Boolean.class );
515             }
516             if ( converted == null ) converted = Boolean.FALSE;
517             if ( ((Component)component).isEnabled() != converted.booleanValue() )
518             {
519                 ((Component)component).setEnabled( converted.booleanValue() );
520             }
521 		}
522 
523 		// editable aspect
524 		displayGroup = displayGroupForAspect( EditableAspect );
525 		key = displayGroupKeyForAspect( EditableAspect );
526 		if ( ( key != null )
527 		&& ( component instanceof JTextComponent ) )
528 		{
529 			if ( displayGroup != null )
530 			{
531 				value =
532 					displayGroup.selectedObjectValueForKey( key );
533 			}
534 			else
535 			{
536 				// treat bound key without display group as a value
537 				value = key;
538 			}
539 			Boolean converted = (Boolean)
540 				ValueConverter.convertObjectToClass(
541 					value, Boolean.class );
542 
543 			if ( converted != null )
544 			{
545 				if ( converted.booleanValue() != ((JTextComponent)component).isEditable() )
546 				{
547 					((JTextComponent)component).setEditable( converted.booleanValue() );
548 				}
549 			}
550 		}
551 
552 		// label aspect
553 		displayGroup = displayGroupForAspect( LabelAspect );
554 		key = displayGroupKeyForAspect( LabelAspect );
555 
556 		if ( ( key != null )
557 		&& ( component instanceof JTextComponent ) )
558 		{
559 			if ( displayGroup != null )
560 			{
561 				value =
562 					displayGroup.selectedObjectValueForKey( key );
563 			}
564 			else
565 			{
566 				// treat bound key without display group as a value
567 				value = key;
568 			}
569 			Boolean converted = (Boolean)
570 				ValueConverter.convertObjectToClass(
571 					value, Boolean.class );
572 
573 			if ( converted != null )
574 			{
575 				if ( converted.booleanValue() )
576 				{
577                                         if ( component instanceof JTextComponent )
578                                         {
579                                             if ( component instanceof JTextArea )
580                                             {
581                                                 areaToLabel( (JTextArea) component );
582                                             }
583                                             else
584                                             {
585                                                 fieldToLabel( (JTextComponent) component );
586                                             }
587                                         }
588 				}
589 				else
590 				{
591                                         if ( component instanceof JTextComponent )
592                                         {
593                                             if ( component instanceof JTextArea )
594                                             {
595                                                 labelToArea( (JTextArea) component );
596                                             }
597                                             else
598                                             {
599                                                 labelToField( (JTextComponent ) component );
600                                             }
601                                         }
602                                 }
603 			}
604 		}
605 
606     }
607 
608     private void fieldToLabel( JTextComponent aTextField )
609     {
610         // turn on wrapping and disable editing and highlighting
611 
612         aTextField.setEditable(false);
613         aTextField.setOpaque(false);
614 
615         // Set the border, colors and font to that of a label
616 
617         LookAndFeel.installBorder(aTextField, "Label.border");
618 
619         LookAndFeel.installColorsAndFont(aTextField,
620             "Label.background",
621             "Label.foreground",
622             "Label.font");
623     }
624 
625     private void labelToField( JTextComponent aTextField )
626     {
627         // turn on wrapping and disable editing and highlighting
628 
629         aTextField.setEditable(true);
630         aTextField.setOpaque(true);
631 
632         // Set the border, colors and font to that of a label
633 
634         LookAndFeel.installBorder(aTextField, "TextField.border");
635 
636         LookAndFeel.installColorsAndFont(aTextField,
637             "TextField.background",
638             "TextField.foreground",
639             "TextField.font");
640     }
641 
642 	private void areaToLabel( JTextArea aTextArea )
643 	{
644             // turn on wrapping and disable editing and highlighting
645 
646             aTextArea.setLineWrap(true);
647             aTextArea.setWrapStyleWord(true);
648             aTextArea.setEditable(false);
649 
650             // Set the text area's border, colors and font to
651             // that of a label
652 
653             LookAndFeel.installBorder(aTextArea, "Label.border");
654 
655             LookAndFeel.installColorsAndFont(aTextArea,
656                 "Label.background",
657                 "Label.foreground",
658                 "Label.font");
659 
660 	}
661 
662 	private void labelToArea( JTextArea aTextArea )
663 	{
664             // turn on wrapping and disable editing and highlighting
665 
666             aTextArea.setEditable(true);
667 
668             // Set the border, colors and font to that of a label
669 
670             LookAndFeel.installBorder(aTextArea, "TextArea.border");
671 
672             LookAndFeel.installColorsAndFont(aTextArea,
673                 "TextArea.background",
674                 "TextArea.foreground",
675                 "TextArea.font");
676 	}
677 
678 
679     /***
680     * Forces this association to cause the object to
681     * stop editing and validate the user's input.
682     * @return false if there were problems validating,
683     * or true to continue.
684     */
685     public boolean endEditing ()
686     {
687         if ( keyTimer != null )
688         {
689             keyTimer.stop();
690             keyTimer.removeActionListener( this );
691             keyTimer = null;
692         }
693 		return writeValueToDisplayGroup();
694     }
695 
696 	/***
697 	* Writes the value currently in the component
698 	* to the selected object in the display group
699 	* bound to the value aspect.
700     * @return false if there were problems validating,
701     * or true to continue.
702 	*/
703 	protected boolean writeValueToDisplayGroup()
704 	{
705             boolean returnValue = true;
706             if ( hasDocument && !needsUpdate ) return true;
707 
708             EODisplayGroup displayGroup = valueDisplayGroup;
709             if ( displayGroup != null )
710             {
711                 String key = valueKey;
712                 Object component = object();
713                 Object value = null;
714                 try
715                 {
716                         //if ( getText.implementedByObject( component ) )
717                         //{
718                                 value = getText.invoke( component );
719                         //}
720                 }
721                 catch ( Exception exc )
722                 {
723                         throw new WotonomyException(
724                                 "Error updating display group", exc );
725                 }
726 
727                 if ( ( wasNull ) && ( EMPTY_STRING.equals( value ) ) )
728                 {
729                         value = null;
730                 }
731                 else
732                 if ( format() != null )
733                 {
734                     try
735                     {
736                         value = format().parseObject( value.toString() );
737                     }
738                     catch ( ParseException exc )
739                     {
740                         String message = exc.getMessage();
741                         //"That format was not recognized.";
742                         if ( displayGroup.associationFailedToValidateValue(
743                                 this, value.toString(), key, exc, message ) )
744                         {
745                             boolean wasListening = isListening;
746                             isListening = false;
747                             JOptionPane.showMessageDialog(
748                                 (Component)component, message );
749                             isListening = wasListening;
750                         }
751                         needsUpdate = false;
752                         return false;
753                     }
754                 }
755 
756                 needsUpdate = false;
757 
758                 // only update if the value is different from the one in the display group
759                 Object existingValue = displayGroup.selectedObjectValueForKey( key );
760                 if ( displayGroup.selectedObjects().size() == 1 )
761                 {
762                     if ( existingValue == value ) return true;
763                     if ( ( existingValue != null ) && ( existingValue.equals( value ) ) ) return true;
764                     if ( ( value != null ) && ( value.equals( existingValue ) ) ) return true;
765                 }
766 
767                 // we might lose focus if display group displays a validation message
768                 boolean wasListening = isListening;
769                 isListening = false;
770 
771                 Iterator selectedIterator = displayGroup.selectionIndexes().iterator();
772                 while ( selectedIterator.hasNext() )
773                 {
774                     int index = ( (Integer)selectedIterator.next() ).intValue();
775 
776                     if ( displayGroup.setValueForObjectAtIndex( value, index, key ) )
777                     {
778                         isListening = wasListening;
779                         needsUpdate = false;
780                     }
781                     else
782                     {
783                         isListening = wasListening;
784                         needsUpdate = false;
785                         returnValue = false;
786                     }
787                 }
788 
789             }
790             return returnValue;
791 	}
792 
793 	/***
794 	* Sets the Format that is used to convert values from the display
795 	* group to and from text that is displayed in the component.
796     * Having a formatter disables auto-updating.
797 	*/
798 	public void setFormat( Format aFormat )
799 	{
800 		format = aFormat;
801 	}
802 
803 	/***
804 	* Gets the Format that is used to convert values from the display
805 	* group to and from text that is displayed in the component.
806 	*/
807 	public Format format()
808 	{
809 		return format;
810 	}
811 
812     // interface ActionListener
813 
814 	/***
815 	* Updates object on action performed.
816 	*/
817 	public void actionPerformed( ActionEvent evt )
818 	{
819         if ( keyTimer != null )
820         {
821             keyTimer.stop();
822             keyTimer.removeActionListener( this );
823             keyTimer = null;
824         }
825         if ( ! isListening ) return;
826         if ( needsUpdate )
827         {
828             writeValueToDisplayGroup();
829         }
830 	}
831 
832     // interface FocusListener
833 
834 	/***
835 	* Notifies of beginning of edit.
836 	*/
837     public void focusGained(FocusEvent evt)
838     {
839         if ( ! isListening ) return;
840 		Object o;
841 		EODisplayGroup displayGroup;
842         Enumeration e = aspects().objectEnumerator();
843 		while ( e.hasMoreElements() )
844 		{
845 			displayGroup =
846 				displayGroupForAspect( e.nextElement().toString() );
847 			if ( displayGroup != null )
848 			{
849 				displayGroup.associationDidBeginEditing( this );
850 			}
851 		}
852     }
853 
854 	/***
855 	* Updates object on focus lost and notifies of end of edit.
856 	*/
857     public void focusLost(FocusEvent evt)
858     {
859         if ( ! isListening ) return;
860 		if ( endEditing() )
861 		{
862 			Object o;
863 			EODisplayGroup displayGroup;
864 			Enumeration e = aspects().objectEnumerator();
865 			while ( e.hasMoreElements() )
866 			{
867 				displayGroup =
868 					displayGroupForAspect( e.nextElement().toString() );
869 				if ( displayGroup != null )
870 				{
871 					displayGroup.associationDidEndEditing( this );
872 				}
873 			}
874 		}
875 		else
876 		{
877 			// probably should notify of a validation error here,
878 		}
879     }
880 
881     /***
882     * Returns whether the data model is updated for every change
883     * in the controlled component.  If false, the data is only
884     * updated on focus lost or the enter key.  Default is true.
885     */
886     public boolean isAutoUpdating()
887     {
888         if ( format() != null ) return false;
889         return autoUpdating;
890     }
891 
892     /***
893     * Sets whether the data model is updated for every change
894     * in the controlled component.
895     */
896     public void setAutoUpdating( boolean isAutoUpdating )
897     {
898         autoUpdating = isAutoUpdating;
899     }
900 
901     /***
902     * Triggers the key timer to start.
903     */
904     protected void queueUpdate()
905     {
906         if ( isAutoUpdating() )
907         {
908             if ( keyTimer == null )
909             {
910                 keyTimer = new Timer( interval, this );
911             }
912             keyTimer.restart();
913         }
914     }
915 
916    // interface DocumentListener
917 
918    public void insertUpdate(DocumentEvent e)
919    {
920        if ( ! isListening ) return;
921 	   needsUpdate = true;
922        queueUpdate();
923    }
924 
925    public void removeUpdate(DocumentEvent e)
926    {
927        if ( ! isListening ) return;
928 	   needsUpdate = true;
929        queueUpdate();
930    }
931 
932    public void changedUpdate(DocumentEvent e)
933    {
934        if ( ! isListening ) return;
935        needsUpdate = true;
936        queueUpdate();
937    }
938 
939 }
940 
941 /*
942  * $Log$
943  * Revision 1.2  2006/02/18 23:19:05  cgruber
944  * Update imports and maven dependencies.
945  *
946  * Revision 1.1  2006/02/16 13:22:22  cgruber
947  * Check in all sources in eclipse-friendly maven-enabled packages.
948  *
949  * Revision 1.3  2004/01/28 18:34:57  mpowers
950  * Better handling for enabling.
951  * Now respecting enabledToSetSelectedObjectValueForKey from display group.
952  *
953  * Revision 1.2  2003/08/06 23:07:52  chochos
954  * general code cleanup (mostly, removing unused imports)
955  *
956  * Revision 1.1  2001/12/20 18:57:24  mpowers
957  * (Re-)Contributing TimedTextAssociation.  Just like TA, except uses timers.
958  *
959  * Revision 1.20  2001/10/26 19:58:06  mpowers
960  * Better handling for non-string types.  We were testing with equals with the
961  * new value against the existing value in the component.  Now we convert
962  * the new value to a string before comparing.  Fixes case for properties
963  * of non-String types, like StringBuffer.
964  *
965  * Revision 1.19  2001/09/30 21:57:14  mpowers
966  * Timers were not getting cleaned up if breakConnection was called
967  * before the timer got a chance to fire.
968  *
969  * Revision 1.18  2001/08/22 15:42:26  mpowers
970  * Added support for JTextComponent label-izing.
971  *
972  * Revision 1.17  2001/07/30 16:32:55  mpowers
973  * Implemented support for bulk-editing.  Detail associations will now
974  * apply changes to all selected objects.
975  *
976  * Revision 1.16  2001/07/17 19:53:37  mpowers
977  * Made some private fields protected for benefit of subclassers.
978  *
979  * Revision 1.15  2001/06/30 14:59:36  mpowers
980  * LabelAspect now sets the text field's opaque setting.
981  *
982  * Revision 1.14  2001/06/29 14:54:08  mpowers
983  * Another fix for timers - timers were definitely causing a memory leak.
984  *
985  * Revision 1.13  2001/06/26 21:37:19  mpowers
986  * Fixed a null pointer in the new key timer scheme.
987  *
988  * Revision 1.12  2001/06/25 14:46:03  mpowers
989  * Fixed a memory leak involving the use of timers.
990  *
991  * Revision 1.11  2001/06/01 19:14:59  mpowers
992  * Text association's enabled aspect is now more discriminating.
993  *
994  * Revision 1.10  2001/05/18 21:07:24  mpowers
995  * Changed the way we handle failure to update object value.
996  *
997  * Revision 1.9  2001/03/13 21:39:58  mpowers
998  * Improved validation handling.
999  *
1000  * Revision 1.8  2001/03/12 12:49:10  mpowers
1001  * Improved validation handling.
1002  * Having a formatter disables auto-updating.
1003  *
1004  * Revision 1.7  2001/03/09 22:08:13  mpowers
1005  * Now handling any objects that have a valid Document.
1006  * No longer checking enabled before updating the enabled state.
1007  *
1008  * Revision 1.6  2001/03/07 19:57:32  mpowers
1009  * Fixed paste error in IconAspect.
1010  *
1011  * Revision 1.4  2001/02/17 16:52:05  mpowers
1012  * Changes in imports to support building with jdk1.1 collections.
1013  *
1014  * Revision 1.3  2001/01/31 19:12:33  mpowers
1015  * Implemented auto-updating in TextComponent.
1016  *
1017  * Revision 1.2  2001/01/10 15:53:58  mpowers
1018  * Preventing a null pointer exception if getText were to return null,
1019  * which doesn't happen for JTextFields but might happen for other objects.
1020  *
1021  * Revision 1.1.1.1  2000/12/21 15:49:08  mpowers
1022  * Contributing wotonomy.
1023  *
1024  * Revision 1.13  2000/12/20 16:25:41  michael
1025  * Added log to all files.
1026  *
1027  *
1028  */
1029