/* Wotonomy: OpenStep design patterns for pure Java applications. Copyright (C) 2000 Intersect Software Corporation This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; if not, see http://www.gnu.org */ package net.wotonomy.ui.swing; import java.awt.Component; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.FocusEvent; import java.awt.event.FocusListener; import java.lang.reflect.InvocationTargetException; import java.net.URL; import java.text.Format; import java.text.ParseException; import java.util.Enumeration; import java.util.Iterator; import javax.swing.Icon; import javax.swing.ImageIcon; import javax.swing.JOptionPane; import javax.swing.JTextArea; import javax.swing.LookAndFeel; import javax.swing.event.DocumentEvent; import javax.swing.event.DocumentListener; import javax.swing.text.AbstractDocument; import javax.swing.text.DefaultStyledDocument; import javax.swing.text.Document; import javax.swing.text.JTextComponent; import net.wotonomy.foundation.NSArray; import net.wotonomy.foundation.NSDictionary; import net.wotonomy.foundation.NSNotification; import net.wotonomy.foundation.NSNotificationCenter; import net.wotonomy.foundation.NSNotificationQueue; import net.wotonomy.foundation.NSSelector; import net.wotonomy.foundation.internal.ValueConverter; import net.wotonomy.foundation.internal.WotonomyException; import net.wotonomy.ui.EOAssociation; import net.wotonomy.ui.EODisplayGroup; /** * TextAssociation binds JTextComponents and other objects * with getText() and setText() methods to a display group. * Note that JLabels are supported with both the Text and * Icon aspects. * Bindings are: * * * @author michael@mpowers.net * @author $Author: cgruber $ * @version $Revision: 904 $ */ public class TextAssociation extends EOAssociation implements FocusListener, ActionListener, DocumentListener { static final NSArray aspects = new NSArray( new Object[] { ValueAspect, EnabledAspect, EditableAspect, VisibleAspect, LabelAspect, IconAspect } ); static final NSArray aspectSignatures = new NSArray( new Object[] { AttributeToOneAspectSignature, AttributeToOneAspectSignature, AttributeToOneAspectSignature, AttributeToOneAspectSignature, AttributeToOneAspectSignature, AttributeToOneAspectSignature } ); static final NSArray objectKeysTaken = new NSArray( new Object[] { "text", "enabled", "editable", "visible" } ); private final static NSSelector getText = new NSSelector( "getText" ); private final static NSSelector setText = new NSSelector( "setText", new Class[] { String.class } ); private final static NSSelector getDocument = new NSSelector( "getDocument" ); private final static NSSelector setIcon = new NSSelector( "setIcon", new Class[] { Icon.class } ); private final static NSSelector addActionListener = new NSSelector( "addActionListener", new Class[] { ActionListener.class } ); private final static NSSelector removeActionListener = new NSSelector( "removeActionListener", new Class[] { ActionListener.class } ); private final static NSSelector addFocusListener = new NSSelector( "addFocusListener", new Class[] { FocusListener.class } ); private final static NSSelector removeFocusListener = new NSSelector( "removeFocusListener", new Class[] { FocusListener.class } ); // null handling protected boolean wasNull; protected static final String EMPTY_STRING = ""; // dirty handling protected boolean needsUpdate; protected boolean hasDocument; protected boolean isListening; // formatting protected Format format; // on-the-fly validation protected boolean activeUpdate; // type conversion protected Class lastKnownType; // cache the value aspect private EODisplayGroup valueDisplayGroup; private String valueKey; // hacky flags needed for no activeUpdate private boolean pleaseIgnoreNextChange = false; private boolean pleaseAcceptNextChange = false; private boolean externallyChanged = true; /** * Constructor specifying the object to be controlled by this * association. Does not establish connection. */ public TextAssociation ( Object anObject ) { super( anObject ); wasNull = false; needsUpdate = false; activeUpdate = true; hasDocument = false; isListening = true; valueDisplayGroup = null; valueKey = null; format = null; lastKnownType = null; // register for idle notifications NSSelector handleNotification = new NSSelector( "handleNotification", new Class[] { NSNotification.class } ); NSNotificationCenter.defaultCenter().addObserver( this, handleNotification, null, this ); } /** * Returns a List of aspect signatures whose contents * correspond with the aspects list. Each element is * a string whose characters represent a capability of * the corresponding aspect. * An empty signature "" means that the aspect can * bind without needing a key. * This implementation returns "A1M" for each * element in the aspects array. */ public static NSArray aspectSignatures () { return aspectSignatures; } /** * Returns a List that describes the aspects supported * by this class. Each element in the list is the string * name of the aspect. This implementation returns an * empty list. */ public static NSArray aspects () { return aspects; } /** * Returns a List of EOAssociation subclasses that, * for the objects that are usable for this association, * are less suitable than this association. */ public static NSArray associationClassesSuperseded () { return new NSArray(); } /** * Returns whether this class can control the specified * object. */ public static boolean isUsableWithObject ( Object anObject ) { return setText.implementedByObject( anObject ); } /** * Returns a List of properties of the controlled object * that are controlled by this class. For example, * "stringValue", or "selected". */ public static NSArray objectKeysTaken () { return objectKeysTaken; } /** * Returns the aspect that is considered primary * or default. This is typically "value" or somesuch. */ public static String primaryAspect () { return ValueAspect; } /** * Returns whether this association can bind to the * specified display group on the specified key for * the specified aspect. */ public boolean canBindAspect ( String anAspect, EODisplayGroup aDisplayGroup, String aKey) { return ( aspects.containsObject( anAspect ) ); } /** * Binds the specified aspect of this association to the * specified key on the specified display group. */ public void bindAspect ( String anAspect, EODisplayGroup aDisplayGroup, String aKey ) { if ( ValueAspect.equals( anAspect ) ) { valueDisplayGroup = aDisplayGroup; valueKey = aKey; } super.bindAspect( anAspect, aDisplayGroup, aKey ); } /** * Establishes a connection between this association * and the controlled object. This implementation * attempts to add this class as an ActionListener * and as a FocusListener to the specified object. */ public void establishConnection () { Object component = object(); try { if ( addActionListener.implementedByObject( component ) ) { addActionListener.invoke( component, this ); } if ( addFocusListener.implementedByObject( component ) ) { addFocusListener.invoke( component, this ); } hasDocument = false; if ( getDocument.implementedByObject( component ) ) { Object document = getDocument.invoke( component ); if ( document instanceof Document ) { ((Document)document).addDocumentListener( this ); hasDocument = true; } } } catch ( Exception exc ) { throw new WotonomyException( "Error while establishing connection", exc ); } super.establishConnection(); // forces update from bindings subjectChanged(); } /** * Breaks the connection between this association and * its object. Override to stop listening for events * from the object. */ public void breakConnection () { Object component = object(); try { if ( removeActionListener.implementedByObject( component ) ) { removeActionListener.invoke( component, this ); } if ( removeFocusListener.implementedByObject( component ) ) { removeFocusListener.invoke( component, this ); } if ( getDocument.implementedByObject( component ) ) { Object document = getDocument.invoke( component ); if ( document instanceof Document ) { ((Document)document).removeDocumentListener( this ); } } } catch ( Exception exc ) { throw new WotonomyException( "Error while breaking connection", exc ); } super.breakConnection(); } public void objectWillChange( Object anObject ) { super.objectWillChange( anObject ); externallyChanged = true; } /** * Called when either the selection or the contents * of an associated display group have changed. */ public void subjectChanged() { if ( pleaseIgnoreNextChange ) { pleaseIgnoreNextChange = false; externallyChanged = false; return; } externallyChanged = true; Object component = object(); EODisplayGroup displayGroup; String key; Object value; // value aspect displayGroup = valueDisplayGroup; if ( displayGroup != null ) { if ( component instanceof Component ) { ((Component)component).setEnabled( displayGroup.enabledToSetSelectedObjectValueForKey( valueKey ) ); } // if activeUpdate or we are not the editing association if ( activeUpdate || displayGroup.editingAssociation() != this || pleaseAcceptNextChange ) { pleaseAcceptNextChange = false; key = valueKey; if ( displayGroup.selectedObjects().size() > 1 ) { // if there're more than one object selected, set // the value to blank for all of them. Object previousValue; Iterator indexIterator = displayGroup.selectionIndexes(). iterator(); // get value for the first selected object. int initialIndex = ( (Integer)indexIterator.next() ).intValue(); previousValue = displayGroup.valueForObjectAtIndex( initialIndex, key ); value = previousValue; // go through the rest of the selected objects, compare each // value with the previous one. continue comparing if two // values are equal, break the while loop if they're different. // the final value will be the common value of all selected objects // if there is one, or be blank if there is not. while ( indexIterator.hasNext() ) { int index = ( (Integer)indexIterator.next() ).intValue(); Object currentValue = displayGroup.valueForObjectAtIndex( index, key ); if ( currentValue != null && previousValue != null && !currentValue.toString().equals( previousValue.toString() ) ) { value = null; break; } } // end while } else { // if there's only one object selected. value = displayGroup.selectedObjectValueForKey( key ); } // end checking the size of selected objects in displayGroup // null handling if ( value == null ) { wasNull = true; value = EMPTY_STRING; lastKnownType = null; } else { wasNull = false; lastKnownType = value.getClass(); if ( format() != null ) { try { value = format().format( value ); } catch ( IllegalArgumentException exc ) { value = value.toString(); } } } try { if ( needToReadValueFromDisplayGroup( value.toString(), getText ) ) { // No need to listen for any events that might get fired // while setting the text since we are the one setting it. boolean wasListening = isListening; isListening = false; // setText is an expensive operation setText.invoke( component, value.toString() ); isListening = wasListening; needsUpdate = false; } } catch ( Exception exc ) { throw new WotonomyException( "Error while updating component connection", exc ); } } } // icon aspect displayGroup = displayGroupForAspect( IconAspect ); key = displayGroupKeyForAspect( IconAspect ); if ( key != null ) { if ( displayGroup != null ) { value = displayGroup.selectedObjectValueForKey( key ); } else { // treat bound key without display group // as a resource to be loaded from the selected class. value = null; Object o = displayGroup.selectedObject(); if ( o != null ) { URL url = o.getClass().getResource( key ); if ( url != null ) { value = new ImageIcon( url ); } } } try { setIcon.invoke( component, value ); } catch ( Exception exc ) { throw new WotonomyException( "Error while updating component connection", exc ); } } // enabled aspect displayGroup = displayGroupForAspect( EnabledAspect ); key = displayGroupKeyForAspect( EnabledAspect ); if ( ( key != null ) && ( component instanceof Component ) ) { if ( displayGroup != null ) { value = displayGroup.selectedObjectValueForKey( key ); } else { // treat bound key without display group as a value value = key; } Boolean converted = null; if ( value != null ) { converted = (Boolean) ValueConverter.convertObjectToClass( value, Boolean.class ); } if ( converted == null ) converted = Boolean.FALSE; if ( ((Component)component).isEnabled() != converted.booleanValue() ) { ((Component)component).setEnabled( converted.booleanValue() ); } } // editable aspect displayGroup = displayGroupForAspect( EditableAspect ); key = displayGroupKeyForAspect( EditableAspect ); if ( ( key != null ) && ( component instanceof JTextComponent ) ) { if ( displayGroup != null ) { value = displayGroup.selectedObjectValueForKey( key ); } else { // treat bound key without display group as a value value = key; } Boolean converted = (Boolean) ValueConverter.convertObjectToClass( value, Boolean.class ); if ( converted != null ) { if ( converted.booleanValue() != ((JTextComponent)component).isEditable() ) { ((JTextComponent)component).setEditable( converted.booleanValue() ); } } } // visible aspect displayGroup = displayGroupForAspect( VisibleAspect ); key = displayGroupKeyForAspect( VisibleAspect ); if ( ( key != null ) && ( component instanceof Component ) ) { if ( displayGroup != null ) { value = displayGroup.selectedObjectValueForKey( key ); } else { // treat bound key without display group as a value value = key; } Boolean converted = (Boolean) ValueConverter.convertObjectToClass( value, Boolean.class ); if ( converted != null ) { if ( converted.booleanValue() != ((Component)component).isVisible() ) { ((Component)component).setVisible( converted.booleanValue() ); } } } // label aspect displayGroup = displayGroupForAspect( LabelAspect ); key = displayGroupKeyForAspect( LabelAspect ); if ( ( key != null ) && ( component instanceof JTextComponent ) ) { if ( displayGroup != null ) { value = displayGroup.selectedObjectValueForKey( key ); } else { // treat bound key without display group as a value value = key; } Boolean converted = (Boolean) ValueConverter.convertObjectToClass( value, Boolean.class ); if ( converted != null ) { if ( converted.booleanValue() ) { if ( component instanceof JTextComponent ) { if ( component instanceof JTextArea ) { areaToLabel( (JTextArea) component ); } else { fieldToLabel( (JTextComponent) component ); } } } else { if ( component instanceof JTextComponent ) { if ( component instanceof JTextArea ) { labelToArea( (JTextArea) component ); } else { labelToField( (JTextComponent ) component ); } } } } } } private void fieldToLabel( JTextComponent aTextField ) { // turn on wrapping and disable editing and highlighting aTextField.setEditable(false); aTextField.setOpaque(false); // Set the border, colors and font to that of a label //LookAndFeel.installBorder(aTextField, "Label.border"); aTextField.setBorder( null ); LookAndFeel.installColorsAndFont(aTextField, "Label.background", "Label.foreground", "Label.font"); } private void labelToField( JTextComponent aTextField ) { // turn on wrapping and disable editing and highlighting aTextField.setEditable(true); aTextField.setOpaque(true); // Set the border, colors and font to that of a label LookAndFeel.installBorder(aTextField, "TextField.border"); LookAndFeel.installColorsAndFont(aTextField, "TextField.background", "TextField.foreground", "TextField.font"); } private void areaToLabel( JTextArea aTextArea ) { // turn on wrapping and disable editing and highlighting aTextArea.setLineWrap(true); aTextArea.setWrapStyleWord(true); aTextArea.setEditable(false); // Set the text area's border, colors and font to // that of a label //LookAndFeel.installBorder(aTextArea, "Label.border"); aTextArea.setBorder( null ); LookAndFeel.installColorsAndFont(aTextArea, "Label.background", "Label.foreground", "Label.font"); } private void labelToArea( JTextArea aTextArea ) { // turn on wrapping and disable editing and highlighting aTextArea.setEditable(true); // Set the border, colors and font to that of a label LookAndFeel.installBorder(aTextArea, "TextArea.border"); LookAndFeel.installColorsAndFont(aTextArea, "TextArea.background", "TextArea.foreground", "TextArea.font"); } /** * Forces this association to cause the object to * stop editing and validate the user's input. * @return false if there were problems validating, * or true to continue. */ public boolean endEditing() { pleaseAcceptNextChange = true; pleaseIgnoreNextChange = false; return writeValueToDisplayGroup(); } /** * Writes the value currently in the component * to the selected object in the display group * bound to the value aspect. * @return false if there were problems validating, * or true to continue. */ protected boolean writeValueToDisplayGroup() { boolean returnValue = true; if ( hasDocument && !needsUpdate ) return true; EODisplayGroup displayGroup = valueDisplayGroup; if ( displayGroup != null ) { String key = valueKey; Object component = object(); Object value = null; try { //if ( getText.implementedByObject( component ) ) //{ value = getText.invoke( component ); //} } catch ( Exception exc ) { throw new WotonomyException( "Error updating display group", exc ); } if ( ( wasNull ) && ( EMPTY_STRING.equals( value ) ) ) { value = null; } else if ( format() != null ) { try { value = format().parseObject( value.toString() ); } catch ( ParseException exc ) { String message = exc.getMessage(); //"That format was not recognized."; if ( displayGroup.associationFailedToValidateValue( this, value.toString(), key, exc, message ) ) { boolean wasListening = isListening; isListening = false; JOptionPane.showMessageDialog( (Component)component, message ); isListening = wasListening; } needsUpdate = false; return false; } } if ( ( lastKnownType != null ) && ( value != null ) ) { // convert back to last known type, if necessary/possible Class type = value.getClass(); if ( ( type != null ) && ( type != lastKnownType ) ) { Object converted = ValueConverter.convertObjectToClass( value, lastKnownType ); if ( converted != null ) { value = converted; } // else: not possible, ignore } } needsUpdate = false; // only update if the value is different from the one in the display group if ( ! needToWriteValueToDisplayGroup( value, displayGroup ) ) return true; // we might lose focus if display group displays a validation message boolean wasListening = isListening; isListening = false; Iterator selectedIterator = displayGroup.selectionIndexes().iterator(); while ( selectedIterator.hasNext() ) { int index = ( (Integer)selectedIterator.next() ).intValue(); if ( displayGroup.setValueForObjectAtIndex( value, index, key ) ) { needsUpdate = false; } else { needsUpdate = false; returnValue = false; } } isListening = wasListening; } return returnValue; } /** * Called to determine whether the display group needs to be * updated. This implementation reads the value from the display * group and only returns true if the specified value is different. * This is done as an optimization since writes are more expensive * than reads. Override to customize this behavior. */ protected boolean needToWriteValueToDisplayGroup( Object aValue, EODisplayGroup aDisplayGroup ) { Object existingValue = aDisplayGroup.selectedObjectValueForKey( valueKey ); if ( aDisplayGroup.selectedObjects().size() == 1 ) { if ( existingValue == aValue ) return false; if ( ( existingValue != null ) && ( existingValue.equals( aValue ) ) ) return false; if ( ( aValue != null ) && ( aValue.equals( existingValue ) ) ) return false; } return true; } /** * Called to determine whether the controlled component needs to be * updated. This implementation reads the value from the selector * and only returns true if the specified value is different. * This is done as an optimization since updating the component * can be an expensive operation. Override to customize this behavior. */ protected boolean needToReadValueFromDisplayGroup( Object aValue, NSSelector aSelector ) throws IllegalAccessException, InvocationTargetException, NoSuchMethodException { return !aValue.toString().equals( aSelector.invoke( object() ) ); } /** * Sets the Format that is used to convert values from the display * group to and from text that is displayed in the component. */ public void setFormat( Format aFormat ) { format = aFormat; } /** * Gets the Format that is used to convert values from the display * group to and from text that is displayed in the component. */ public Format format() { return format; } /** * Returns whether the text association is configured to actively * update the model in response to changes in the component. */ public boolean isActiveUpdate() { return activeUpdate; } /** * Sets whether the text association should actively * update the model in response to changes in the component. * Default is true. False indicates that the model will be updated * only when the component loses focus or fires an action event. */ public void setActiveUpdate( boolean isActiveUpdate ) { activeUpdate = isActiveUpdate; } // interface ActionListener /** * Updates object on action performed. */ public void actionPerformed( ActionEvent evt ) { if ( ! isListening ) return; if ( needsUpdate ) { pleaseAcceptNextChange = true; // needed if activeUpdate = false writeValueToDisplayGroup(); } } // interface FocusListener /** * Notifies of beginning of edit. */ public void focusGained(FocusEvent evt) { if ( ! isListening ) return; pleaseAcceptNextChange = true; externallyChanged = true; Object o; EODisplayGroup displayGroup; Enumeration e = aspects().objectEnumerator(); while ( e.hasMoreElements() ) { displayGroup = displayGroupForAspect( e.nextElement().toString() ); if ( displayGroup != null ) { displayGroup.associationDidBeginEditing( this ); } } } /** * Updates object on focus lost and notifies of end of edit. */ public void focusLost(FocusEvent evt) { if ( ! isListening ) return; if ( endEditing() ) { Object o; EODisplayGroup displayGroup; Enumeration e = aspects().objectEnumerator(); while ( e.hasMoreElements() ) { displayGroup = displayGroupForAspect( e.nextElement().toString() ); if ( displayGroup != null ) { displayGroup.associationDidEndEditing( this ); } } } else { // probably should notify of a validation error here, } } /** * Queues a notification to PostWhenIdle. */ protected void queueUpdate(DocumentEvent e) { if ( e.getDocument() instanceof DefaultStyledDocument ) { if ( e instanceof AbstractDocument.DefaultDocumentEvent ) { int docLength = e.getDocument().getLength(); if ( ( e.getType().equals( DocumentEvent.EventType.CHANGE ) ) ) { if ( e.getOffset() == 0 && e.getLength() == docLength ) { // ignore document events for the whole document // since default styled document broadcasts these // using invokeLater, and we've already received // notification about the actual style change. // see: DefaultStyledDocument.ChangeUpdateRunnable return; } } } } NSNotificationQueue.defaultQueue().enqueueNotification( new NSNotification( "TextAssociation.DocumentChanged", this, new NSDictionary( new Object[] { "event" }, new Object[] { e } ) ), NSNotificationQueue.PostWhenIdle ); } /** * Handles idle notification. */ public void handleNotification( NSNotification aNotification ) { if ( activeUpdate ) { writeValueToDisplayGroup(); } } // interface DocumentListener public void insertUpdate(DocumentEvent e) { if ( ! isListening ) return; needsUpdate = true; queueUpdate( e ); } public void removeUpdate(DocumentEvent e) { if ( ! isListening ) return; needsUpdate = true; queueUpdate( e ); } public void changedUpdate(DocumentEvent e) { if ( ! isListening ) return; needsUpdate = true; queueUpdate( e ); } } /* * $Log$ * Revision 1.2 2006/02/18 23:19:05 cgruber * Update imports and maven dependencies. * * Revision 1.1 2006/02/16 13:22:22 cgruber * Check in all sources in eclipse-friendly maven-enabled packages. * * Revision 1.41 2004/02/05 02:18:18 mpowers * Now setting border to null to new Aqua LAF behaves. * * Revision 1.40 2004/01/28 22:47:56 mpowers * un-activeUpdate was brokne. * * Revision 1.39 2004/01/28 18:34:57 mpowers * Better handling for enabling. * Now respecting enabledToSetSelectedObjectValueForKey from display group. * * Revision 1.38 2003/08/06 23:07:52 chochos * general code cleanup (mostly, removing unused imports) * * Revision 1.37 2003/02/06 16:21:34 mpowers * Fix for activeUpdate: no longer bothering with editing context's changes. * * Revision 1.36 2002/10/24 18:19:24 mpowers * Bug fix - thanks to dwang. * * Revision 1.35 2002/08/02 19:19:30 mpowers * Added control points for when to read or write from the display group. * Added flags needed to fix problems with non-activeUpdate and commit key. * * Revision 1.33 2002/03/08 23:18:01 mpowers * Added visible aspect. * * Revision 1.32 2002/03/06 16:13:53 mpowers * Yet another fix for style document changes: swing's DefaultStyledDocument * using an invoke later to launch a final StyleChanged event, which occurs * after the TextAssociation has reestablished itself as a document listener, * causing the item to be marked dirty. We're now handling this case. * * Revision 1.31 2002/03/04 22:10:37 mpowers * Supressing active update only marks dirty when contents have changed. * * Revision 1.30 2002/02/23 16:19:12 mpowers * Now only marking an editing context as dirty if it's not already dirty. * * Revision 1.29 2002/02/19 18:38:29 mpowers * Minor optimization: activeUpdate now checked in handleNotification. * * Revision 1.28 2002/02/19 16:36:47 mpowers * Better support for active update: objects are now marked as changed * even though the model itself is not updated -- this allows editing * context itself to be marked as having changes to be saved. * * Revision 1.27 2002/01/23 19:50:11 mpowers * Fix for a null pointer when value is null and last known type is not. * (from dwang) * * Revision 1.26 2002/01/14 19:37:22 mpowers * Fix for NPE when value is null and auto update is false. * * Revision 1.25 2001/12/10 03:16:11 mpowers * Fixed bug with isListening when no items are in display group. * * Revision 1.24 2001/11/16 19:14:51 mpowers * Brought back the idea of configuring whether updates occur on each change. * * Revision 1.23 2001/11/08 20:06:06 mpowers * Now performing type-conversion as a convenience. * * Revision 1.22 2001/11/04 18:24:20 mpowers * Better handling for non-string values when bulk-editing. * * Revision 1.21 2001/11/01 15:53:34 mpowers * Now that NSNotificationQueue correctly implements PostWhenIdle, we can * finally discard our use of Swing's Timer in favor of using the queue * to coalesce document changed events. * * Revision 1.20 2001/10/26 19:58:06 mpowers * Better handling for non-string types. We were testing with equals with the * new value against the existing value in the component. Now we convert * the new value to a string before comparing. Fixes case for properties * of non-String types, like StringBuffer. * * Revision 1.19 2001/09/30 21:57:14 mpowers * Timers were not getting cleaned up if breakConnection was called * before the timer got a chance to fire. * * Revision 1.18 2001/08/22 15:42:26 mpowers * Added support for JTextComponent label-izing. * * Revision 1.17 2001/07/30 16:32:55 mpowers * Implemented support for bulk-editing. Detail associations will now * apply changes to all selected objects. * * Revision 1.16 2001/07/17 19:53:37 mpowers * Made some private fields protected for benefit of subclassers. * * Revision 1.15 2001/06/30 14:59:36 mpowers * LabelAspect now sets the text field's opaque setting. * * Revision 1.14 2001/06/29 14:54:08 mpowers * Another fix for timers - timers were definitely causing a memory leak. * * Revision 1.13 2001/06/26 21:37:19 mpowers * Fixed a null pointer in the new key timer scheme. * * Revision 1.12 2001/06/25 14:46:03 mpowers * Fixed a memory leak involving the use of timers. * * Revision 1.11 2001/06/01 19:14:59 mpowers * Text association's enabled aspect is now more discriminating. * * Revision 1.10 2001/05/18 21:07:24 mpowers * Changed the way we handle failure to update object value. * * Revision 1.9 2001/03/13 21:39:58 mpowers * Improved validation handling. * * Revision 1.8 2001/03/12 12:49:10 mpowers * Improved validation handling. * Having a formatter disables auto-updating. * * Revision 1.7 2001/03/09 22:08:13 mpowers * Now handling any objects that have a valid Document. * No longer checking enabled before updating the enabled state. * * Revision 1.6 2001/03/07 19:57:32 mpowers * Fixed paste error in IconAspect. * * Revision 1.4 2001/02/17 16:52:05 mpowers * Changes in imports to support building with jdk1.1 collections. * * Revision 1.3 2001/01/31 19:12:33 mpowers * Implemented auto-updating in TextComponent. * * Revision 1.2 2001/01/10 15:53:58 mpowers * Preventing a null pointer exception if getText were to return null, * which doesn't happen for JTextFields but might happen for other objects. * * Revision 1.1.1.1 2000/12/21 15:49:08 mpowers * Contributing wotonomy. * * Revision 1.13 2000/12/20 16:25:41 michael * Added log to all files. * * */