/*
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:
*
* - value: a property convertable to/from a string
* - editable: a boolean property that determines whether
* the user can edit the text in the field
* - enabled: a boolean property that determines whether
* the user can select the text in the field
* - visible: a boolean property that determines whether
* the field is visible
* - label: a boolean property that determines whether
* field should appear as a read-only, selectable label
* - icon: a property that returns a Swing icon, for use
* with JLabels and other components with setIcon() methods.
* If bound to a static string, the string will be used to
* load an image resource from the selected object's class.
*
*
* @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.
* - "A" attribute: the aspect can be bound to
* an attribute.
* - "1" to-one: the aspect can be bound to a
* property that returns a single object.
* - "M" to-one: the aspect can be bound to a
* property that returns multiple objects.
*
* 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.
*
*
*/