/* Wotonomy: OpenStep design patterns for pure Java applications. Copyright (C) 2000 Intersect Software Corporation This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; if not, see http://www.gnu.org */ package net.wotonomy.ui.swing; import java.awt.Color; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.Polygon; import java.awt.Rectangle; import java.awt.RenderingHints; import java.util.Iterator; import java.util.List; import javax.swing.JTable; import javax.swing.table.TableColumn; import net.wotonomy.control.EOSortOrdering; import net.wotonomy.foundation.NSArray; import net.wotonomy.foundation.internal.ValueConverter; import net.wotonomy.foundation.internal.WotonomyException; import net.wotonomy.ui.EOAssociation; import net.wotonomy.ui.EODisplayGroup; import net.wotonomy.ui.swing.TableAssociation.TableAssociationModel; /** * TableColumnAssociation binds a column of a JTable * to a property of the elements of a display group. * Bindings are: * * Because TableColumns do not have a handle to their * containing JTable, setTable() must be called before * calling establishConnection(). This will add the * controlled TableColumn to the specified JTable. * * Columns appear in the table in the order in which * setTable is called on the corresponding association. * The original table model index is ignored. * * Column names appear in the table based on the value * of TableColumn.getHeaderValue(). * * @author michael@mpowers.net * @author $Author: cgruber $ * @version $Revision: 904 $ */ public class TableColumnAssociation extends EOAssociation { static final NSArray aspects = new NSArray( new Object[] { ValueAspect, EditableAspect } ); static final NSArray aspectSignatures = new NSArray( new Object[] { AttributeToOneAspectSignature } ); static final NSArray objectKeysTaken = new NSArray( new Object[] { "table" } ); static Color[] sortIndicatorColorList; EODisplayGroup valueDisplayGroup, editableDisplayGroup; String valueKey, editableKey; boolean sortable; boolean sortCaseSensitive; JTable table; /** * Constructor specifying the object to be controlled by this * association. Throws an exception if the object is not * a TableColumn. setTable() must be called before * establishing the connection. */ public TableColumnAssociation ( Object anObject ) { super( anObject ); valueDisplayGroup = null; valueKey = null; editableDisplayGroup = null; editableKey = null; sortable = true; sortCaseSensitive = true; table = null; } /** * Sets the table to be used for this column association. * If no TableAssociation exists for the specified table, * one will be created automatically. The controlled * table column will be added to the table. Note that * the table column's model index is ignored: table columns * appear in the table in the order in which setTable is * called on their corresponding associations. */ public void setTable( JTable aTable ) { table = aTable; if ( table == null ) return; // creates table association if not existing getTableAssociation(); } /** * Returns the table association for this table column, * or null if no table has been set. This method will * create the association if none exists for the table. */ public TableAssociation getTableAssociation() { if ( table == null ) return null; TableAssociation result; if ( ! ( table.getModel() instanceof TableAssociationModel ) ) { result = new TableAssociation( table ); result.bindAspect( SourceAspect, displayGroupForAspect( ValueAspect ), "" ); } else { result = ((TableAssociationModel)table.getModel()).getAssociation(); } return result; } /** * Returns a List of aspect signatures whose contents * correspond with the aspects list. Each element is * a string whose characters represent a capability of * the corresponding aspect. * An empty signature "" means that the aspect can * bind without needing a key. * This implementation returns "A1M" for each * element in the aspects array. */ public static NSArray aspectSignatures () { return aspectSignatures; } /** * Returns a List that describes the aspects supported * by this class. Each element in the list is the string * name of the aspect. This implementation returns an * empty list. */ public static NSArray aspects () { return aspects; } /** * Returns a List of EOAssociation subclasses that, * for the objects that are usable for this association, * are less suitable than this association. */ public static NSArray associationClassesSuperseded () { return new NSArray(); } /** * Returns whether this class can control the specified * object. */ public static boolean isUsableWithObject ( Object anObject ) { return ( anObject instanceof TableColumn ); } /** * Returns a List of properties of the controlled object * that are controlled by this class. For example, * "stringValue", or "selected". */ public static NSArray objectKeysTaken () { return objectKeysTaken; } /** * Returns the aspect that is considered primary * or default. This is typically "value" or somesuch. */ public static String primaryAspect () { return ValueAspect; } /** * Returns whether this association can bind to the * specified display group on the specified key for * the specified aspect. */ public boolean canBindAspect ( String anAspect, EODisplayGroup aDisplayGroup, String aKey) { return ( aspects.containsObject( anAspect ) ); } /** * Binds the specified aspect of this association to the * specified key on the specified display group. */ public void bindAspect ( String anAspect, EODisplayGroup aDisplayGroup, String aKey ) { if ( ValueAspect.equals( anAspect ) ) { valueDisplayGroup = aDisplayGroup; valueKey = aKey; } if ( EditableAspect.equals( anAspect ) ) { editableDisplayGroup = aDisplayGroup; editableKey = aKey; } super.bindAspect( anAspect, aDisplayGroup, aKey ); } /** * Establishes a connection between this association * and the controlled object. Subclasses should begin * listening for events from their controlled object here. */ public void establishConnection () { addAsListener(); if ( table == null ) throw new WotonomyException( "A table must be specified by calling setTable()" ); // add association to model TableAssociationModel model = (TableAssociationModel) table.getModel(); model.addColumnAssociation( this ); super.establishConnection(); } /** * Breaks the connection between this association and * its object. Override to stop listening for events * from the object. */ public void breakConnection () { removeAsListener(); if ( table == null ) throw new WotonomyException( "TableColumnAssociation's table may not be null" ); // remove association from model TableAssociationModel model = (TableAssociationModel) table.getModel(); model.removeColumnAssociation( this ); super.breakConnection(); } protected void addAsListener() { } protected void removeAsListener() { } /** * Returns the value to be displayed at the specified index. * This method is called by the TableAssocation to populate * the table model. * This implementation simply retrieves the value from the * display group bound to the value aspect. */ public Object valueAtIndex( int aRowIndex ) { if ( valueDisplayGroup != null ) { return valueDisplayGroup.valueForObjectAtIndex( aRowIndex, valueKey ); } return null; } /** * Sets a value for the specified index. This method is * called by the TableAssocation after a cell has been * edited. * This implementation simply sets the value in the * display group bound to the value aspect. */ public void setValueAtIndex( Object aValue, int aRowIndex ) { if ( valueDisplayGroup != null ) { valueDisplayGroup.setValueForObjectAtIndex( aValue, aRowIndex, valueKey ); } } /** * Returns whether this column should be sorted when the * user clicks on the column header. Defaults to true. */ public boolean isSortable() { return sortable; } /** * Sets whether this column should be sorted when the * user clicks on the column header. */ public void setSortable( boolean isSortable ) { sortable = isSortable; } /** * Returns whether this column should be sorted * in a case sensitive manner. Defaults to true. */ public boolean isSortCaseSensitive() { return sortCaseSensitive; } /** * Sets whether this column should be sorted when * in a case sensitive manner. * If false, the column contents should be string values. */ public void setSortCaseSensitive( boolean isCaseSensitive ) { sortCaseSensitive = isCaseSensitive; } /** * Called by the TableAssociation to determine whether * the value at the specified row is editable. * This is determined by the binding of the Editable aspect, * looking at the value of the corresponding index in that * display group. Note: because the display group may * not have the same number if items, the selected index is * used if the editable display group is not the same as the * the value display group. */ public boolean isEditableAtRow( int aRowIndex ) { if ( editableKey == null ) return false; Object value = null; if ( editableDisplayGroup != null ) { // if using the same group for both, return the value for the index if ( editableDisplayGroup.equals( valueDisplayGroup ) ) { value = editableDisplayGroup.valueForObjectAtIndex( aRowIndex, editableKey ); } else // using an external display group to determine editability { // ignore index and use the selected object value from display group value = editableDisplayGroup.selectedObjectValueForKey( editableKey ); } } else { // treat bound key without display group as a value value = editableKey; } if ( value == null ) return false; // null defaults to false Boolean result = (Boolean) ValueConverter.convertObjectToClass( value, Boolean.class ); if ( result == null ) return true; // non-null defaults to true return result.booleanValue(); } // convenience private TableColumn component() { return (TableColumn) object(); } /** * Called by TableAssociation to get a EOSortOrdering suitable * for the information in this column. * This implementation returns a EOSortOrdering with the key * equal to the value aspect's key and the appropriate selector * for the specified ascending value and the case sensitivity * of this column. * Override to customize the sort for your column. */ public EOSortOrdering getSortOrdering( boolean isAscending ) { if ( isAscending ) { if ( isSortCaseSensitive() ) { return new EOSortOrdering( valueKey, EOSortOrdering.CompareAscending ) ; } else { return new EOSortOrdering( valueKey, EOSortOrdering.CompareCaseInsensitiveAscending ) ; } } else { if ( isSortCaseSensitive() ) { return new EOSortOrdering( valueKey, EOSortOrdering.CompareDescending ) ; } else { return new EOSortOrdering( valueKey, EOSortOrdering.CompareCaseInsensitiveDescending ) ; } } } /** * Returns the one-based index of this assocation's sort ordering * in the specified list of orderings. If the sign of the returned * value is negative, the ordering is descending. If the return * value is zero, no matching ordering was found. */ protected int getIndexOfMatchingOrdering( List orderings ) { // find index of matching ordering int index = 0; EOSortOrdering ordering = null; Iterator i = orderings.iterator(); while ( i.hasNext() ) { index++; ordering = (EOSortOrdering) i.next(); if ( ordering.key().equals( valueKey ) ) { // determine ascending or descending if ( getSortOrdering( true ).equals( ordering ) ) { return index; } else if ( getSortOrdering( false ).equals( ordering ) ) { return -index; } } } return 0; } /** * Called by TableAssociation to draw some indicator in the * specified rectangle using the specified graphics to indicate * the specified sort state. The rectangle corresponds to the * bounds of the column header. * This implementation draws a small transparent gray triangle at * the right edge of the bounding rectangle. * Override to do something different or to do nothing at all. */ protected void drawSortIndicator( Rectangle aBoundingRectangle, Graphics aGraphicsContext, List orderings ) { int index = getIndexOfMatchingOrdering( orderings ); if ( index == 0 ) return; boolean isAscending = ( index > 0 ); index = Math.abs( index ); // turn on anti-aliasing if ( aGraphicsContext instanceof Graphics2D ) { ((Graphics2D)aGraphicsContext).setRenderingHint( RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON ); } Rectangle r = new Rectangle( aBoundingRectangle ); // resize to a right-justified square, sides equal to height r.setBounds( r.x + r.width - r.height, r.y, r.height, r.height ); // resize to about a third smaller int portion = r.height / 3; r.grow( -portion, -portion ); // transparencies cause java2d printing to rasterize, // resulting in excessive memory usage and print time. // aGraphicsContext.setColor( new Color( 0, 0, 0, 255 / (index*2) ) ); aGraphicsContext.setColor( getSortIndicatorColor( index ) ); Polygon triangle; if ( !isAscending ) { triangle = new Polygon( new int[] { r.x, r.x+r.width/2, r.x+r.width }, new int[] { r.y, r.y+r.height, r.y }, 3 ); } else { triangle = new Polygon( new int[] { r.x, r.x+r.width/2, r.x+r.width }, new int[] { r.y+r.height, r.y, r.y+r.height }, 3 ); } aGraphicsContext.fillPolygon( triangle ); } /** * Returns a color to be used by the sort indicator based on the index * of the sorting column. The goal of this method is to make the color * appear lighter and lighter, the "less" primary the sort order for this * column is. This can be acheives simply though a "transparent" color, * however, during printing of the corresponding table, java print * kicks into "raster" based printing when printing a component with * a transparent color instead of "vector" based printing. Raster * based printing can take up to 20-30 times longer to print than * vector printing and consume several times the amount of memory. * Raster-based printing should be avoided at all costs if the a component * is to be printed (as of Java 1.3.1). * @param index The "sort" index of the associated table column. The higher * the index, the lighter the color will be. An index of 0 will * return null. * @return The color to use when rendering the sort indicator. */ protected static Color getSortIndicatorColor( int index ) { if ( index == 0 ) return null; // Create the color list if not already created. if ( sortIndicatorColorList == null ) { // Default size to 13 elements, it would be extremely rare that a // user sorts more than 12 columns at a time (although possible). // (Index 0 is not used.) sortIndicatorColorList = new Color[ 13 ]; } // Get the color out of the color list. Use the index directly as // an index into an ordered list. If the color has already been // created for that index, then return it, otherwise create the color. if ( ( index < sortIndicatorColorList.length ) && ( sortIndicatorColorList[ index ] != null ) ) { return sortIndicatorColorList[ index ]; } // The following logic performs the same affect as the above // transparent color, without actually using a transparent color. // Start with the table header's background color and derive a color // that is "darker" than that color. Any color this logic creates will // be between those two colors. Color lightColor = java.awt.SystemColor.control; Color darkColor = lightColor.darker().darker(); // Make the light color (the upper bound) a little darker, so that even // the lightest triangle will still be slightly visible. lightColor = new Color( Math.max( ( int )( lightColor.getRed() * 0.9), 0 ), Math.max( ( int )( lightColor.getGreen() * 0.9), 0 ), Math.max( ( int )( lightColor.getBlue() * 0.9), 0) ); // Subtract the light color from the dark color. This is the range // between the two colors. Color difference = new Color( lightColor.getRed() - darkColor.getRed(), lightColor.getGreen() - darkColor.getGreen(), lightColor.getBlue() - darkColor.getBlue() ); // If the index is 1, user the dark color as is. Otherwise scale the // color closer and closer to the lighter color as the index gets // biggger and bigger. if ( index > 1 ) { float factor = ( float )Math.pow( 0.5, ( index - 1 ) ); darkColor = new Color( Math.max( lightColor.getRed() - ( int )( difference.getRed() * factor ), 0 ), Math.max( lightColor.getGreen() - ( int )( difference.getGreen() * factor ), 0 ), Math.max( lightColor.getBlue() - ( int )( difference.getBlue() * factor ), 0 ) ); } // Cache the created color in the color list for this index. if ( index >= sortIndicatorColorList.length ) { // The color list is too small, create a new larger list with // some padding for even larger indicies. Color[] oldList = sortIndicatorColorList; sortIndicatorColorList = new Color[ index + 5 ]; System.arraycopy( oldList, 0, sortIndicatorColorList, 0, oldList.length ); } sortIndicatorColorList[ index ] = darkColor; return darkColor; } } /* * $Log$ * Revision 1.2 2006/02/18 23:19:05 cgruber * Update imports and maven dependencies. * * Revision 1.1 2006/02/16 13:22:22 cgruber * Check in all sources in eclipse-friendly maven-enabled packages. * * Revision 1.16 2003/08/06 23:07:52 chochos * general code cleanup (mostly, removing unused imports) * * Revision 1.15 2002/08/22 15:42:49 mpowers * No longer using transparency to render sort indicator (see comments). * * Revision 1.14 2002/04/12 21:05:57 mpowers * Now distinguishing changes in titles group even better. * * Revision 1.13 2002/03/05 23:18:28 mpowers * Added documentation. * Added isSelectionPaintedImmediate and isSelectionTracking attributes * to TableAssociation. * Added getTableAssociation to TableColumnAssociation. * * Revision 1.12 2002/03/04 22:11:43 mpowers * Darkened the sort indicator to better differentiate the first sort. * * Revision 1.11 2002/03/04 03:58:17 mpowers * Refined table header click behavior. * * Revision 1.10 2002/03/01 15:42:00 mpowers * Table column headers now always show their sort indicator. * A third table-column click clears the sort for that column. * * Revision 1.9 2002/02/28 23:01:39 mpowers * TableColumnAssociations add and remove themselves from the TableAssociation * when their connection is established and broken respectively. * TableAssociations now break connection if they have no column associations. * * Revision 1.8 2001/06/05 16:03:56 mpowers * Flipped the triangle to be consistent with Aqua. * * Revision 1.7 2001/03/09 22:09:22 mpowers * Now better handling jdk1.1 for rendering the column header. * * Revision 1.6 2001/02/17 16:52:05 mpowers * Changes in imports to support building with jdk1.1 collections. * * Revision 1.5 2001/01/12 19:11:56 mpowers * Fixed table column click sorting. * * Revision 1.4 2001/01/12 17:20:30 mpowers * Moved EOSortOrdering creation to ColumnAssociation. * * Revision 1.3 2001/01/11 21:55:57 mpowers * Implemented sort indicator for table column headers. * * Revision 1.2 2001/01/11 20:34:26 mpowers * Implemented EOSortOrdering and added support in framework. * Added header-click to sort table columns. * * Revision 1.1.1.1 2000/12/21 15:49:03 mpowers * Contributing wotonomy. * * Revision 1.5 2000/12/20 16:25:41 michael * Added log to all files. * * */