/* 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. * * */