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.Color;
22  import java.awt.Graphics;
23  import java.awt.Graphics2D;
24  import java.awt.Polygon;
25  import java.awt.Rectangle;
26  import java.awt.RenderingHints;
27  import java.util.Iterator;
28  import java.util.List;
29  
30  import javax.swing.JTable;
31  import javax.swing.table.TableColumn;
32  
33  import net.wotonomy.control.EOSortOrdering;
34  import net.wotonomy.foundation.NSArray;
35  import net.wotonomy.foundation.internal.ValueConverter;
36  import net.wotonomy.foundation.internal.WotonomyException;
37  import net.wotonomy.ui.EOAssociation;
38  import net.wotonomy.ui.EODisplayGroup;
39  import net.wotonomy.ui.swing.TableAssociation.TableAssociationModel;
40  
41  
42  /***
43  * TableColumnAssociation binds a column of a JTable
44  * to a property of the elements of a display group.  
45  * Bindings are: 
46  * <ul>
47  * <li>value: a property convertable to a string for
48  * display in the cells of the table column</li> 
49  * <li>editable: a property convertable to a boolean 
50  * that determines the editability of the corresponding
51  * cells in the column.</li> 
52  * </ul>
53  
54  * Because TableColumns do not have a handle to their
55  * containing JTable, setTable() must be called before
56  * calling establishConnection().  This will add the 
57  * controlled TableColumn to the specified JTable.
58  *
59  * Columns appear in the table in the order in which
60  * setTable is called on the corresponding association.
61  * The original table model index is ignored.
62  *
63  * Column names appear in the table based on the value
64  * of TableColumn.getHeaderValue().
65  *
66  * @author michael@mpowers.net
67  * @author $Author: cgruber $
68  * @version $Revision: 904 $
69  */
70  public class TableColumnAssociation extends EOAssociation
71  {
72      static final NSArray aspects = 
73          new NSArray( new Object[] {
74              ValueAspect, EditableAspect
75          } );
76      static final NSArray aspectSignatures = 
77          new NSArray( new Object[] {
78              AttributeToOneAspectSignature
79          } );
80      static final NSArray objectKeysTaken = 
81          new NSArray( new Object[] {
82              "table" 
83          } );
84  
85      static Color[] sortIndicatorColorList;
86  		
87  	EODisplayGroup valueDisplayGroup, editableDisplayGroup;
88  	String valueKey, editableKey;
89      boolean sortable;
90      boolean sortCaseSensitive;
91      JTable table;
92  	
93      /***
94      * Constructor specifying the object to be controlled by this
95      * association.  Throws an exception if the object is not
96  	* a TableColumn.  setTable() must be called before 
97  	* establishing the connection.
98      */
99      public TableColumnAssociation ( Object anObject )
100     {
101         super( anObject );
102 		valueDisplayGroup = null;
103 		valueKey = null;
104 		editableDisplayGroup = null;
105 		editableKey = null;
106         sortable = true;
107         sortCaseSensitive = true;
108         table = null;
109     }
110 	
111 	/***
112 	* Sets the table to be used for this column association.
113 	* If no TableAssociation exists for the specified table,
114 	* one will be created automatically.  The controlled
115 	* table column will be added to the table.  Note that
116 	* the table column's model index is ignored: table columns
117 	* appear in the table in the order in which setTable is
118 	* called on their corresponding associations.
119 	*/
120 	public void setTable( JTable aTable )
121 	{
122         table = aTable;
123         if ( table == null ) return;
124         
125         // creates table association if not existing
126         getTableAssociation();        
127 	}
128     
129     /***
130     * Returns the table association for this table column,
131     * or null if no table has been set.  This method will
132     * create the association if none exists for the table.
133     */
134     public TableAssociation getTableAssociation()
135     {
136         if ( table == null ) return null;
137         
138         TableAssociation result;        
139 		if ( ! ( table.getModel() instanceof TableAssociationModel ) )
140 		{
141 			result = new TableAssociation( table );
142 			result.bindAspect( 
143 				SourceAspect, displayGroupForAspect( ValueAspect ), "" );
144 		}
145         else
146         {
147             result = ((TableAssociationModel)table.getModel()).getAssociation();
148         }
149         return result;
150     }
151     
152     /***
153     * Returns a List of aspect signatures whose contents
154     * correspond with the aspects list.  Each element is 
155     * a string whose characters represent a capability of
156     * the corresponding aspect. <ul>
157     * <li>"A" attribute: the aspect can be bound to
158     * an attribute.</li>
159     * <li>"1" to-one: the aspect can be bound to a
160     * property that returns a single object.</li>
161     * <li>"M" to-one: the aspect can be bound to a
162     * property that returns multiple objects.</li>
163     * </ul> 
164     * An empty signature "" means that the aspect can
165     * bind without needing a key.
166     * This implementation returns "A1M" for each
167     * element in the aspects array.
168     */
169     public static NSArray aspectSignatures ()
170     {
171         return aspectSignatures;
172     }
173     
174     /***
175     * Returns a List that describes the aspects supported
176     * by this class.  Each element in the list is the string
177     * name of the aspect.  This implementation returns an
178     * empty list.
179     */
180     public static NSArray aspects ()
181     {
182         return aspects;
183     }
184     
185     /***
186     * Returns a List of EOAssociation subclasses that,
187     * for the objects that are usable for this association,
188     * are less suitable than this association.
189     */
190     public static NSArray associationClassesSuperseded ()
191     {
192         return new NSArray();
193     }
194     
195     /***
196     * Returns whether this class can control the specified 
197     * object. 
198     */
199     public static boolean isUsableWithObject ( Object anObject )
200     {
201         return ( anObject instanceof TableColumn );
202     }
203     
204     /***
205     * Returns a List of properties of the controlled object
206     * that are controlled by this class.  For example,
207     * "stringValue", or "selected".
208     */
209     public static NSArray objectKeysTaken ()
210     {
211         return objectKeysTaken;
212     }
213     
214     /***
215     * Returns the aspect that is considered primary
216     * or default.  This is typically "value" or somesuch.
217     */
218     public static String primaryAspect ()
219     {
220         return ValueAspect;
221     }
222         
223     /***
224     * Returns whether this association can bind to the
225     * specified display group on the specified key for
226     * the specified aspect.  
227     */
228     public boolean canBindAspect ( 
229         String anAspect, EODisplayGroup aDisplayGroup, String aKey)
230     {
231         return ( aspects.containsObject( anAspect ) );
232     }
233     
234     /***
235     * Binds the specified aspect of this association to the
236     * specified key on the specified display group.
237     */
238     public void bindAspect ( 
239         String anAspect, EODisplayGroup aDisplayGroup, String aKey )
240 	{
241 		if ( ValueAspect.equals( anAspect ) )
242 		{
243 			valueDisplayGroup = aDisplayGroup;	
244 			valueKey = aKey;
245 		}
246 		if ( EditableAspect.equals( anAspect ) )
247 		{
248 			editableDisplayGroup = aDisplayGroup;
249 			editableKey = aKey;
250 		}
251 		super.bindAspect( anAspect, aDisplayGroup, aKey );
252 	}
253 	
254     /***
255     * Establishes a connection between this association
256     * and the controlled object.  Subclasses should begin
257     * listening for events from their controlled object here.
258     */
259     public void establishConnection ()
260     {
261         addAsListener();
262         
263         if ( table == null ) throw new WotonomyException(
264             "A table must be specified by calling setTable()" );
265 
266 		// add association to model
267 		TableAssociationModel model = 
268 			(TableAssociationModel) table.getModel();
269         model.addColumnAssociation( this );
270         
271         super.establishConnection();
272     }
273     
274     /***
275     * Breaks the connection between this association and 
276     * its object.  Override to stop listening for events
277     * from the object.
278     */
279     public void breakConnection ()
280     {
281         removeAsListener();
282 
283         if ( table == null ) throw new WotonomyException(
284             "TableColumnAssociation's table may not be null" );
285 
286 		// remove association from model
287 		TableAssociationModel model = 
288 			(TableAssociationModel) table.getModel();
289 		model.removeColumnAssociation( this );
290         
291         super.breakConnection();
292     }
293 
294     protected void addAsListener()
295     {
296     }
297 
298     protected void removeAsListener()
299     {
300     }
301 	
302 	/***
303 	* Returns the value to be displayed at the specified index.
304 	* This method is called by the TableAssocation to populate
305 	* the table model.
306 	* This implementation simply retrieves the value from the
307 	* display group bound to the value aspect.
308 	*/
309 	public Object valueAtIndex( int aRowIndex )
310 	{
311 		if ( valueDisplayGroup != null )
312 		{
313 			return valueDisplayGroup.valueForObjectAtIndex( 
314 				aRowIndex, valueKey );
315 		}
316 		return null;
317 	}
318 	
319 	/***
320 	* Sets a value for the specified index.  This method is 
321 	* called by the TableAssocation after a cell has been
322 	* edited.
323 	* This implementation simply sets the value in the 
324 	* display group bound to the value aspect.
325 	*/
326 	public void setValueAtIndex( Object aValue, int aRowIndex )
327 	{
328 		if ( valueDisplayGroup != null )
329 		{
330 			valueDisplayGroup.setValueForObjectAtIndex( 
331 				aValue, aRowIndex, valueKey );
332 		}
333 	}
334     
335     /***
336     * Returns whether this column should be sorted when the
337     * user clicks on the column header.  Defaults to true.
338     */
339     public boolean isSortable()
340     {
341         return sortable;
342     }
343     
344     /***
345     * Sets whether this column should be sorted when the
346     * user clicks on the column header.
347     */
348     public void setSortable( boolean isSortable )
349     {
350         sortable = isSortable;   
351     }
352 
353     /***
354     * Returns whether this column should be sorted 
355     * in a case sensitive manner.  Defaults to true.
356     */
357     public boolean isSortCaseSensitive()
358     {
359         return sortCaseSensitive;
360     }
361     
362     /***
363     * Sets whether this column should be sorted when
364     * in a case sensitive manner.
365     * If false, the column contents should be string values.
366     */
367     public void setSortCaseSensitive( boolean isCaseSensitive )
368     {
369         sortCaseSensitive = isCaseSensitive;   
370     }
371 
372 	/***
373 	* Called by the TableAssociation to determine whether 
374 	* the value at the specified row is editable.
375 	* This is determined by the binding of the Editable aspect,
376 	* looking at the value of the corresponding index in that
377 	* display group.  Note: because the display group may
378 	* not have the same number if items, the selected index is
379 	* used if the editable display group is not the same as the
380 	* the value display group.
381 	*/
382 	public boolean isEditableAtRow( int aRowIndex )
383 	{
384 		if ( editableKey == null ) return false;
385 		Object value = null;
386 		if ( editableDisplayGroup != null )
387 		{
388 			// if using the same group for both, return the value for the index
389 			if ( editableDisplayGroup.equals( valueDisplayGroup ) )
390 			{
391 				value =
392 					editableDisplayGroup.valueForObjectAtIndex( aRowIndex, editableKey );
393 			}
394 			else // using an external display group to determine editability
395 			{
396 				// ignore index and use the selected object value from display group
397 				value =
398 					editableDisplayGroup.selectedObjectValueForKey( editableKey );
399 			}
400 		}
401 		else
402 		{
403 			// treat bound key without display group as a value
404 			value = editableKey;	
405 		}
406 		if ( value == null ) return false; // null defaults to false
407 		Boolean result = (Boolean)
408 			ValueConverter.convertObjectToClass( value, Boolean.class );
409 		if ( result == null ) return true; // non-null defaults to true
410 		return result.booleanValue();
411 	}
412 		
413     // convenience
414 
415     private TableColumn component()
416     {
417         return (TableColumn) object();
418     }
419     
420     /***
421     * Called by TableAssociation to get a EOSortOrdering suitable 
422     * for the information in this column.
423     * This implementation returns a EOSortOrdering with the key
424     * equal to the value aspect's key and the appropriate selector
425     * for the specified ascending value and the case sensitivity
426     * of this column.
427     * Override to customize the sort for your column.
428     */
429     public EOSortOrdering getSortOrdering( boolean isAscending )
430     {
431         if ( isAscending )
432         {
433             if ( isSortCaseSensitive() )
434             {
435                 return new EOSortOrdering(
436                     valueKey, 
437                     EOSortOrdering.CompareAscending ) ;
438             }
439             else
440             {
441                 return new EOSortOrdering(
442                     valueKey, 
443                     EOSortOrdering.CompareCaseInsensitiveAscending ) ;
444             }
445         }
446         else
447         {
448             if ( isSortCaseSensitive() )
449             {
450                 return new EOSortOrdering(
451                     valueKey, 
452                     EOSortOrdering.CompareDescending ) ;
453             }
454             else
455             {
456                 return new EOSortOrdering(
457                     valueKey, 
458                     EOSortOrdering.CompareCaseInsensitiveDescending ) ;
459             }
460         }
461     }
462     
463     /***
464     * Returns the one-based index of this assocation's sort ordering
465     * in the specified list of orderings.  If the sign of the returned
466     * value is negative, the ordering is descending.  If the return
467     * value is zero, no matching ordering was found.
468     */
469     protected int getIndexOfMatchingOrdering( List orderings )
470     {
471         // find index of matching ordering
472         int index = 0;
473         EOSortOrdering ordering = null;
474         Iterator i = orderings.iterator();
475         while ( i.hasNext() )
476         {
477             index++;
478             ordering = (EOSortOrdering) i.next();
479             if ( ordering.key().equals( valueKey ) )
480             {
481                 // determine ascending or descending 
482                 if ( getSortOrdering( true ).equals( ordering ) )
483                 {
484                     return index;
485                 }
486                 else
487                 if ( getSortOrdering( false ).equals( ordering ) )
488                 {
489                     return -index;
490                 }
491             }
492         }
493         return 0;
494         
495     }
496     
497     /***
498     * Called by TableAssociation to draw some indicator in the
499     * specified rectangle using the specified graphics to indicate
500     * the specified sort state.  The rectangle corresponds to the
501     * bounds of the column header.
502     * This implementation draws a small transparent gray triangle at
503     * the right edge of the bounding rectangle.
504     * Override to do something different or to do nothing at all.
505     */ 
506     protected void drawSortIndicator( Rectangle aBoundingRectangle,
507         Graphics aGraphicsContext, List orderings )
508     {
509         int index = getIndexOfMatchingOrdering( orderings );
510         if ( index == 0 ) return;
511         
512         boolean isAscending = ( index > 0 );
513         index = Math.abs( index );
514         
515         // turn on anti-aliasing
516         if ( aGraphicsContext instanceof Graphics2D )
517         {
518             ((Graphics2D)aGraphicsContext).setRenderingHint( 
519                 RenderingHints.KEY_ANTIALIASING, 
520                 RenderingHints.VALUE_ANTIALIAS_ON );
521         }
522         
523         Rectangle r = new Rectangle( aBoundingRectangle );
524         
525         // resize to a right-justified square, sides equal to height
526         r.setBounds( r.x + r.width - r.height, r.y, r.height, r.height );
527         
528         // resize to about a third smaller
529         int portion = r.height / 3;
530         r.grow( -portion, -portion );
531         
532         // transparencies cause java2d printing to rasterize,
533         //   resulting in excessive memory usage and print time.
534         // aGraphicsContext.setColor( new Color( 0, 0, 0, 255 / (index*2) ) );
535         aGraphicsContext.setColor( getSortIndicatorColor( index ) );
536         
537         Polygon triangle;
538         if ( !isAscending )
539         {
540             triangle = new Polygon( 
541                 new int[] { r.x, r.x+r.width/2, r.x+r.width },
542                 new int[] { r.y, r.y+r.height, r.y }, 3 );            
543         }
544         else
545         {
546             triangle = new Polygon( 
547                 new int[] { r.x, r.x+r.width/2, r.x+r.width },            
548                 new int[] { r.y+r.height, r.y, r.y+r.height }, 3 );
549         }                
550         aGraphicsContext.fillPolygon( triangle );
551     }
552     
553     /***
554      * Returns a color to be used by the sort indicator based on the index
555      * of the sorting column.  The goal of this method is to make the color
556      * appear lighter and lighter, the "less" primary the sort order for this
557      * column is.  This can be acheives simply though a "transparent" color,
558      * however, during printing of the corresponding table, java print
559      * kicks into "raster" based printing when printing a component with
560      * a transparent color instead of "vector" based printing.  Raster
561      * based printing can take up to 20-30 times longer to print than
562      * vector printing and consume several times the amount of memory.
563      * Raster-based printing should be avoided at all costs if the a component
564      * is to be printed (as of Java 1.3.1).
565      * @param index The "sort" index of the associated table column.  The higher
566      *        the index, the lighter the color will be.  An index of 0 will
567      *        return null.
568      * @return The color to use when rendering the sort indicator.
569      */
570     protected static Color getSortIndicatorColor( int index )
571     {
572         if ( index == 0 ) return null;
573 
574         // Create the color list if not already created.
575         if ( sortIndicatorColorList == null )
576         {
577             // Default size to 13 elements, it would be extremely rare that a
578             // user sorts more than 12 columns at a time (although possible).
579             // (Index 0 is not used.)
580             sortIndicatorColorList = new Color[ 13 ];
581         }
582 
583         // Get the color out of the color list.  Use the index directly as
584         // an index into an ordered list.  If the color has already been
585         // created for that index, then return it, otherwise create the color.
586         if ( ( index < sortIndicatorColorList.length ) &&
587              ( sortIndicatorColorList[ index ] != null ) )
588         {
589             return sortIndicatorColorList[ index ];
590         }
591 
592         // The following logic performs the same affect as the above
593         // transparent color, without actually using a transparent color.
594         // Start with the table header's background color and derive a color
595         // that is "darker" than that color.  Any color this logic creates will
596         // be between those two colors.
597         Color lightColor = java.awt.SystemColor.control;
598         Color darkColor = lightColor.darker().darker();
599 
600         // Make the light color (the upper bound) a little darker, so that even
601         // the lightest triangle will still be slightly visible.
602         lightColor = new Color(
603             Math.max( ( int )( lightColor.getRed() * 0.9), 0 ),
604             Math.max( ( int )( lightColor.getGreen() * 0.9), 0 ),
605             Math.max( ( int )( lightColor.getBlue() * 0.9), 0) );
606 
607         // Subtract the light color from the dark color.  This is the range
608         // between the two colors.
609         Color difference = new Color( lightColor.getRed() - darkColor.getRed(),
610             lightColor.getGreen() - darkColor.getGreen(),
611             lightColor.getBlue() - darkColor.getBlue() );
612 
613         // If the index is 1, user the dark color as is.  Otherwise scale the
614         // color closer and closer to the lighter color as the index gets
615         // biggger and bigger.
616         if ( index > 1 )
617         {
618             float factor = ( float )Math.pow( 0.5, ( index - 1 ) );
619             darkColor = new Color(
620                 Math.max( lightColor.getRed() - ( int )( difference.getRed() * factor ), 0 ),
621                 Math.max( lightColor.getGreen() - ( int )( difference.getGreen() * factor ), 0 ),
622                 Math.max( lightColor.getBlue() - ( int )( difference.getBlue() * factor ), 0 ) );
623         }
624 
625         // Cache the created color in the color list for this index.
626         if ( index >= sortIndicatorColorList.length )
627         {
628             // The color list is too small, create a new larger list with
629             // some padding for even larger indicies.
630             Color[] oldList = sortIndicatorColorList;
631             sortIndicatorColorList = new Color[ index + 5 ];
632             System.arraycopy( oldList, 0, sortIndicatorColorList, 0, oldList.length );
633         }
634         sortIndicatorColorList[ index ] = darkColor;
635 
636         return darkColor;
637     } 
638 }
639 
640 /*
641  * $Log$
642  * Revision 1.2  2006/02/18 23:19:05  cgruber
643  * Update imports and maven dependencies.
644  *
645  * Revision 1.1  2006/02/16 13:22:22  cgruber
646  * Check in all sources in eclipse-friendly maven-enabled packages.
647  *
648  * Revision 1.16  2003/08/06 23:07:52  chochos
649  * general code cleanup (mostly, removing unused imports)
650  *
651  * Revision 1.15  2002/08/22 15:42:49  mpowers
652  * No longer using transparency to render sort indicator (see comments).
653  *
654  * Revision 1.14  2002/04/12 21:05:57  mpowers
655  * Now distinguishing changes in titles group even better.
656  *
657  * Revision 1.13  2002/03/05 23:18:28  mpowers
658  * Added documentation.
659  * Added isSelectionPaintedImmediate and isSelectionTracking attributes
660  * to TableAssociation.
661  * Added getTableAssociation to TableColumnAssociation.
662  *
663  * Revision 1.12  2002/03/04 22:11:43  mpowers
664  * Darkened the sort indicator to better differentiate the first sort.
665  *
666  * Revision 1.11  2002/03/04 03:58:17  mpowers
667  * Refined table header click behavior.
668  *
669  * Revision 1.10  2002/03/01 15:42:00  mpowers
670  * Table column headers now always show their sort indicator.
671  * A third table-column click clears the sort for that column.
672  *
673  * Revision 1.9  2002/02/28 23:01:39  mpowers
674  * TableColumnAssociations add and remove themselves from the TableAssociation
675  * when their connection is established and broken respectively.
676  * TableAssociations now break connection if they have no column associations.
677  *
678  * Revision 1.8  2001/06/05 16:03:56  mpowers
679  * Flipped the triangle to be consistent with Aqua.
680  *
681  * Revision 1.7  2001/03/09 22:09:22  mpowers
682  * Now better handling jdk1.1 for rendering the column header.
683  *
684  * Revision 1.6  2001/02/17 16:52:05  mpowers
685  * Changes in imports to support building with jdk1.1 collections.
686  *
687  * Revision 1.5  2001/01/12 19:11:56  mpowers
688  * Fixed table column click sorting.
689  *
690  * Revision 1.4  2001/01/12 17:20:30  mpowers
691  * Moved EOSortOrdering creation to ColumnAssociation.
692  *
693  * Revision 1.3  2001/01/11 21:55:57  mpowers
694  * Implemented sort indicator for table column headers.
695  *
696  * Revision 1.2  2001/01/11 20:34:26  mpowers
697  * Implemented EOSortOrdering and added support in framework.
698  * Added header-click to sort table columns.
699  *
700  * Revision 1.1.1.1  2000/12/21 15:49:03  mpowers
701  * Contributing wotonomy.
702  *
703  * Revision 1.5  2000/12/20 16:25:41  michael
704  * Added log to all files.
705  *
706  *
707  */
708