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