1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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
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
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
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
389 if ( editableDisplayGroup.equals( valueDisplayGroup ) )
390 {
391 value =
392 editableDisplayGroup.valueForObjectAtIndex( aRowIndex, editableKey );
393 }
394 else
395 {
396
397 value =
398 editableDisplayGroup.selectedObjectValueForKey( editableKey );
399 }
400 }
401 else
402 {
403
404 value = editableKey;
405 }
406 if ( value == null ) return false;
407 Boolean result = (Boolean)
408 ValueConverter.convertObjectToClass( value, Boolean.class );
409 if ( result == null ) return true;
410 return result.booleanValue();
411 }
412
413
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
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
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
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
526 r.setBounds( r.x + r.width - r.height, r.y, r.height, r.height );
527
528
529 int portion = r.height / 3;
530 r.grow( -portion, -portion );
531
532
533
534
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
575 if ( sortIndicatorColorList == null )
576 {
577
578
579
580 sortIndicatorColorList = new Color[ 13 ];
581 }
582
583
584
585
586 if ( ( index < sortIndicatorColorList.length ) &&
587 ( sortIndicatorColorList[ index ] != null ) )
588 {
589 return sortIndicatorColorList[ index ];
590 }
591
592
593
594
595
596
597 Color lightColor = java.awt.SystemColor.control;
598 Color darkColor = lightColor.darker().darker();
599
600
601
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
608
609 Color difference = new Color( lightColor.getRed() - darkColor.getRed(),
610 lightColor.getGreen() - darkColor.getGreen(),
611 lightColor.getBlue() - darkColor.getBlue() );
612
613
614
615
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
626 if ( index >= sortIndicatorColorList.length )
627 {
628
629
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
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708