View Javadoc

1   /*
2   Wotonomy: OpenStep design patterns for pure Java applications.
3   Copyright (C) 2000 Intersect Software Corporation
4   
5   This library is free software; you can redistribute it and/or
6   modify it under the terms of the GNU Lesser General Public
7   License as published by the Free Software Foundation; either
8   version 2.1 of the License, or (at your option) any later version.
9   
10  This library is distributed in the hope that it will be useful,
11  but WITHOUT ANY WARRANTY; without even the implied warranty of
12  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13  Lesser General Public License for more details.
14  
15  You should have received a copy of the GNU Lesser General Public
16  License along with this library; if not, see http://www.gnu.org
17  */
18  
19  package net.wotonomy.ui.swing;
20  
21  import java.awt.Component;
22  import java.awt.event.ActionEvent;
23  import java.awt.event.ActionListener;
24  import java.awt.event.FocusEvent;
25  import java.awt.event.FocusListener;
26  import java.util.Calendar;
27  import java.util.Date;
28  import java.util.Enumeration;
29  import java.util.Iterator;
30  
31  import net.wotonomy.foundation.NSArray;
32  import net.wotonomy.foundation.NSSelector;
33  import net.wotonomy.foundation.internal.ValueConverter;
34  import net.wotonomy.foundation.internal.WotonomyException;
35  import net.wotonomy.ui.EOAssociation;
36  import net.wotonomy.ui.EODisplayGroup;
37  
38  /***
39  * DateAssociation binds any component that has a set and get Date methods and
40  * fire actions events when the date has been changed.  Bindings are:
41  * <ul>
42  * <li>value: a property convertable to/from a date</li>
43  * <li>editable: a boolean property that determines whether
44  * the user can edit the date in the component</li>
45  * <li>enabled: a boolean property that determines whether
46  * the component is enabled or disabled</li>
47  * </ul>
48  *
49  * @author rob@yahoo.com
50  * @version $Revision: 904 $
51  */
52  public class DateAssociation extends EOAssociation
53                               implements ActionListener, FocusListener
54  {
55      static final NSArray aspects =
56          new NSArray( new Object[] {
57              ValueAspect, EnabledAspect, EditableAspect
58          } );
59      static final NSArray aspectSignatures =
60          new NSArray( new Object[] {
61              AttributeToOneAspectSignature,
62  			AttributeToOneAspectSignature,
63  			AttributeToOneAspectSignature
64          } );
65      static final NSArray objectKeysTaken =
66          new NSArray( new Object[] {
67              "date", "enabled", "editable"
68          } );
69  
70  	private final static NSSelector getDate =
71  		new NSSelector( "getDate" );
72  	private final static NSSelector setDate =
73  		new NSSelector( "setDate",
74  		new Class[] { Date.class } );
75  	private final static NSSelector addActionListener =
76  		new NSSelector( "addActionListener",
77  		new Class[] { ActionListener.class } );
78  	private final static NSSelector removeActionListener =
79  		new NSSelector( "removeActionListener",
80  		new Class[] { ActionListener.class } );
81  	private final static NSSelector addFocusListener =
82  		new NSSelector( "addFocusListener",
83  		new Class[] { FocusListener.class } );
84  	private final static NSSelector removeFocusListener =
85  		new NSSelector( "removeFocusListener",
86  		new Class[] { FocusListener.class } );
87  	private final static NSSelector setEditable =
88  		new NSSelector( "setEditable",
89  		new Class[] { boolean.class } );
90  
91  	// dirty handling
92  	private boolean needsUpdate;
93      private Date nullValue; // placeholder for null value flag
94  
95      /***
96      * Constructor specifying the object to be controlled by this
97      * association.  Does not establish connection.
98      */
99      public DateAssociation ( Object anObject )
100     {
101         super( anObject );
102 		needsUpdate = false;
103         nullValue = null;
104     }
105 
106     /***
107     * Returns a List of aspect signatures whose contents
108     * correspond with the aspects list.  Each element is
109     * a string whose characters represent a capability of
110     * the corresponding aspect. <ul>
111     * <li>"A" attribute: the aspect can be bound to
112     * an attribute.</li>
113     * <li>"1" to-one: the aspect can be bound to a
114     * property that returns a single object.</li>
115     * <li>"M" to-one: the aspect can be bound to a
116     * property that returns multiple objects.</li>
117     * </ul>
118     * An empty signature "" means that the aspect can
119     * bind without needing a key.
120     * This implementation returns "A1M" for each
121     * element in the aspects array.
122     */
123     public static NSArray aspectSignatures ()
124     {
125         return aspectSignatures;
126     }
127 
128     /***
129     * Returns a List that describes the aspects supported
130     * by this class.  Each element in the list is the string
131     * name of the aspect.  This implementation returns an
132     * empty list.
133     */
134     public static NSArray aspects ()
135     {
136         return aspects;
137     }
138 
139     /***
140     * Returns a List of EOAssociation subclasses that,
141     * for the objects that are usable for this association,
142     * are less suitable than this association.
143     */
144     public static NSArray associationClassesSuperseded ()
145     {
146         return new NSArray();
147     }
148 
149     /***
150     * Returns whether this class can control the specified
151     * object.
152     */
153     public static boolean isUsableWithObject ( Object anObject )
154     {
155         return setDate.implementedByObject( anObject );
156     }
157 
158     /***
159     * Returns a List of properties of the controlled object
160     * that are controlled by this class.  For example,
161     * "stringValue", or "selected".
162     */
163     public static NSArray objectKeysTaken ()
164     {
165         return objectKeysTaken;
166     }
167 
168     /***
169     * Returns the aspect that is considered primary
170     * or default.  This is typically "value" or somesuch.
171     */
172     public static String primaryAspect ()
173     {
174         return ValueAspect;
175     }
176 
177     /***
178     * Returns whether this association can bind to the
179     * specified display group on the specified key for
180     * the specified aspect.
181     */
182     public boolean canBindAspect (
183         String anAspect, EODisplayGroup aDisplayGroup, String aKey)
184     {
185         return ( aspects.containsObject( anAspect ) );
186     }
187 
188     /***
189     * Establishes a connection between this association
190     * and the controlled object.  This implementation
191 	* attempts to add this class as an ActionListener
192 	* and Focus Listener to the specified object.
193     */
194     public void establishConnection ()
195     {
196 		Object component = object();
197 		try
198 		{
199 			if ( addActionListener.implementedByObject( component ) )
200 			{
201 				addActionListener.invoke( component, this );
202 			}
203 			if ( addFocusListener.implementedByObject( component ) )
204 			{
205 				addFocusListener.invoke( component, this );
206 			}
207 		}
208 		catch ( Exception exc )
209 		{
210 			throw new WotonomyException(
211 				"Error while establishing connection", exc );
212 		}
213 
214         super.establishConnection();
215 
216 		// forces update from bindings
217 		subjectChanged();
218     }
219 
220     /***
221     * Breaks the connection between this association and
222     * its object.  Override to stop listening for events
223     * from the object.
224     */
225     public void breakConnection ()
226     {
227 		Object component = object();
228 		try
229 		{
230 			if ( removeActionListener.implementedByObject( component ) )
231 			{
232 				removeActionListener.invoke( component, this );
233 			}
234 			if ( removeFocusListener.implementedByObject( component ) )
235 			{
236 				removeFocusListener.invoke( component, this );
237 			}
238 		}
239 		catch ( Exception exc )
240 		{
241 			throw new WotonomyException(
242 				"Error while breaking connection", exc );
243 		}
244         super.breakConnection();
245     }
246 
247     /***
248     * Called when either the selection or the contents
249     * of an associated display group have changed.
250     */
251     public void subjectChanged ()
252     {
253 		Object component = object();
254 		EODisplayGroup displayGroup;
255 		String key;
256 		Object value;
257 
258 		// value aspect
259 		displayGroup = displayGroupForAspect( ValueAspect );
260 		if ( displayGroup != null )
261 		{
262                     key = displayGroupKeyForAspect( ValueAspect );
263                     if ( component instanceof Component )
264                     {
265                         ((Component)component).setEnabled( 
266                                 displayGroup.enabledToSetSelectedObjectValueForKey( key ) );
267                     }
268                     if ( displayGroup.selectedObjects().size() > 1 )
269                     {
270                         // if there're more than one object selected, set
271                         // the value to blank for all of them.
272                         Object previousValue;
273 
274                         Iterator indexIterator = displayGroup.selectionIndexes().
275                                                   iterator();
276 
277                         // get value for the first selected object.
278                         int initialIndex = ( (Integer)indexIterator.next() ).intValue();
279                         previousValue = displayGroup.valueForObjectAtIndex(
280                                                   initialIndex, key );
281                         value = null;
282 
283                         // go through the rest of the selected objects, compare each
284                         // value with the previous one. continue comparing if two
285                         // values are equal, break the while loop if they're different.
286                         // the final value will be the common value of all selected objects
287                         // if there is one, or be blank if there is not.
288                         while ( indexIterator.hasNext() )
289                         {
290                             int index = ( (Integer)indexIterator.next() ).intValue();
291                             Object currentValue = displayGroup.valueForObjectAtIndex(
292                                                   index, key );
293                             if ( currentValue != null && !currentValue.equals( previousValue ) )
294                             {
295                                 value = null;
296                                 break;
297                             }
298                             else
299                             {
300                                 // currentValue is the same as the previous one
301                                 value = currentValue;
302                             }
303 
304                         } // end while
305 
306                     } else {
307 
308                         value = displayGroup.selectedObjectValueForKey( key );
309                     } // end checking size of displayGroup
310 
311                     // convert value to date
312                     try
313                     {
314                         Date dateValue = null;
315                         // (Date) ValueConverter.convertObjectToClass( value, Date.class );
316 
317                         if ( value instanceof Date )
318                         {
319                             dateValue = (Date) value;
320                         }
321 
322                         if ( ( dateValue == null ) && ( value instanceof Calendar ) )
323                         {
324                             dateValue = ( ( Calendar )value ).getTime();
325                         }
326 
327                         if ( dateValue == null )
328                         {
329                             // current time (placeholder)
330                             nullValue = new Date();
331                             dateValue = nullValue;
332                         }
333                         else
334                         {
335                             nullValue = null;
336                         }
337 
338                         if ( !dateValue.equals( getDate.invoke( component ) ) )
339                         {	// No need to update if there is no change.
340                             setDate.invoke( component, dateValue );
341                             needsUpdate = false;
342                         }
343 
344                     }
345                     catch ( Exception exc )
346                     {
347                             throw new WotonomyException(
348                                     "Error while updating component connection", exc );
349                     }
350 		}
351 
352 		// enabled aspect
353 		displayGroup = displayGroupForAspect( EnabledAspect );
354 		key = displayGroupKeyForAspect( EnabledAspect );
355 		if ( ( ( displayGroup != null ) || ( key != null ) )
356 		&& ( component instanceof Component ) )
357 		{
358 			if ( displayGroup != null )
359 			{
360 				value =
361 					displayGroup.selectedObjectValueForKey( key );
362 			}
363 			else
364 			{
365 				// treat bound key without display group as a value
366 				value = key;
367 			}
368             Boolean converted = null;
369             if ( value != null ) 
370             {
371                 converted = (Boolean)
372                     ValueConverter.convertObjectToClass(
373                         value, Boolean.class );
374             }
375             if ( converted == null ) converted = Boolean.FALSE;
376             if ( converted.booleanValue() != ((Component)component).isEnabled() )
377             {
378                 ((Component)component).setEnabled( converted.booleanValue() );
379             }
380 		}
381 
382 		// editable aspect
383 		displayGroup = displayGroupForAspect( EditableAspect );
384 		key = displayGroupKeyForAspect( EditableAspect );
385 		if ( ( ( displayGroup != null ) || ( key != null ) )
386 		&& ( setEditable.implementedByObject( component ) ) )
387 		{
388             try
389             {
390                 if ( displayGroup != null )
391                 {
392                     value =
393                         displayGroup.selectedObjectValueForKey( key );
394                 }
395                 else
396                 {
397                     // treat bound key without display group as a value
398                     value = key;
399                 }
400                 Boolean converted = (Boolean)
401                     ValueConverter.convertObjectToClass(
402                         value, Boolean.class );
403 
404                 if ( converted != null )
405                 {
406                     setEditable.invoke( component, converted );
407                 }
408             }
409 			catch ( Exception exc )
410 			{
411 				throw new WotonomyException(
412 					"Error while updating component connection (editable aspect)", exc );
413 			}
414 		}
415     }
416 
417     /***
418     * Forces this association to cause the object to
419     * stop editing and validate the user's input.
420     * @return false if there were problems validating,
421     * or true to continue.
422     */
423     public boolean endEditing ()
424     {
425 		return writeValueToDisplayGroup();
426     }
427 
428 	/***
429 	* Writes the value currently in the component
430 	* to the selected object in the display group
431 	* bound to the value aspect.
432         * @return false if there were problems validating,
433         * or true to continue.
434 	*/
435 	protected boolean writeValueToDisplayGroup()
436 	{
437             if ( !needsUpdate ) return true;
438 
439             EODisplayGroup displayGroup =
440                     displayGroupForAspect( ValueAspect );
441             if ( displayGroup != null )
442             {
443                 String key = displayGroupKeyForAspect( ValueAspect );
444                                 Object component = object();
445                 Object value = null;
446                 try
447                 {
448                     if ( getDate.implementedByObject( component ) )
449                     {
450                             value = getDate.invoke( component );
451                     }
452                     if ( nullValue != null )
453                     {
454                         if ( nullValue.equals( value ) )
455                         {
456                             value = null;
457                         }
458                     }
459                 }
460                 catch ( Exception exc )
461                 {
462                         throw new WotonomyException(
463                                 "Error updating display group", exc );
464                 }
465 
466                 needsUpdate = false;
467 
468                 boolean returnValue = true;
469                 Iterator selectedIterator = displayGroup.selectionIndexes().iterator();
470                 while ( selectedIterator.hasNext() )
471                 {
472                     int index = ( (Integer)selectedIterator.next() ).intValue();
473 
474                     if ( !displayGroup.setValueForObjectAtIndex( value, index, key ) )
475                     {
476                         returnValue = false;
477                     }
478                 }
479                 return returnValue;
480 
481             }
482             return false;
483 	}
484 
485     // interface ActionListener
486 
487 	/***
488 	* Updates object on action performed.
489 	*/
490 	public void actionPerformed( ActionEvent evt )
491 	{
492         needsUpdate = true;
493 		writeValueToDisplayGroup();  // TODO: Should we do this here or on focus lost?
494 	}
495 
496     // interface FocusListener
497 
498 	/***
499 	* Notifies of beginning of edit.
500 	*/
501     public void focusGained(FocusEvent evt)
502     {
503 		Object o;
504 		EODisplayGroup displayGroup;
505         Enumeration e = aspects().objectEnumerator();
506 		while ( e.hasMoreElements() )
507 		{
508 			displayGroup =
509 				displayGroupForAspect( e.nextElement().toString() );
510 			if ( displayGroup != null )
511 			{
512 				displayGroup.associationDidBeginEditing( this );
513 			}
514 		}
515     }
516 
517 	/***
518 	* Updates object on focus lost and notifies of end of edit.
519 	*/
520     public void focusLost(FocusEvent evt)
521     {
522 		if ( endEditing() )
523 		{
524 			Object o;
525 			EODisplayGroup displayGroup;
526 			Enumeration e = aspects().objectEnumerator();
527 			while ( e.hasMoreElements() )
528 			{
529 				displayGroup =
530 					displayGroupForAspect( e.nextElement().toString() );
531 				if ( displayGroup != null )
532 				{
533 					displayGroup.associationDidEndEditing( this );
534 				}
535 			}
536 		}
537 		else
538 		{
539 			// probably should notify of a validation error here,
540 			// but how to also handle actionPerformed without copying code?
541 /*
542 			Object value = null;
543 			try
544 			{
545 				if ( getText.implementedByObject( object() ) )
546 				{
547 					value = getText.invoke( object() );
548 				}
549 			}
550 			catch ( Exception exc )
551 			{
552 				throw new WotonomyException(
553 					"Error updating display group", exc );
554 			}
555 
556 			EODisplayGroup displayGroup =
557 					displayGroupForAspect( ValueAspect );
558 			String key = displayGroupKeyForAspect( ValueAspect );
559 			if ( displayGroup != null )
560 			{
561 				if ( displayGroup.associationFailedToValidateValue(
562 					this, (String) value, key, object(),
563 					"That format was not recognized." ) )
564 				{
565 					new net.wotonomy.ui.swing.util.StackTraceInspector();
566 				}
567 				if ( object() instanceof Component )
568 				{
569 					((Component)object()).requestFocus();
570 				}
571 			}
572 */
573 		}
574     }
575 }
576 
577 /*
578  * $Log$
579  * Revision 1.2  2006/02/18 23:19:05  cgruber
580  * Update imports and maven dependencies.
581  *
582  * Revision 1.1  2006/02/16 13:22:22  cgruber
583  * Check in all sources in eclipse-friendly maven-enabled packages.
584  *
585  * Revision 1.7  2004/01/28 18:34:57  mpowers
586  * Better handling for enabling.
587  * Now respecting enabledToSetSelectedObjectValueForKey from display group.
588  *
589  * Revision 1.6  2003/08/06 23:07:52  chochos
590  * general code cleanup (mostly, removing unused imports)
591  *
592  * Revision 1.5  2001/07/30 16:32:55  mpowers
593  * Implemented support for bulk-editing.  Detail associations will now
594  * apply changes to all selected objects.
595  *
596  * Revision 1.4  2001/02/17 17:23:49  mpowers
597  * More changes to support compiling with jdk1.1 collections.
598  *
599  * Revision 1.3  2001/02/17 16:52:05  mpowers
600  * Changes in imports to support building with jdk1.1 collections.
601  *
602  * Revision 1.2  2001/01/17 16:25:26  mpowers
603  * Now catching null values from data object.
604  *
605  * Revision 1.1  2001/01/10 22:26:32  mpowers
606  * Contributing DateAssociation.
607  *
608  * Revision 1.1  2001/01/10 21:30:27  rglista
609  * Initial checkin
610  *
611  *
612  */
613