/*
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:
*
* - value: a property convertable to a string for
* display in the cells of the table column
* - editable: a property convertable to a boolean
* that determines the editability of the corresponding
* cells in the column.
*
* 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.
* - "A" attribute: the aspect can be bound to
* an attribute.
* - "1" to-one: the aspect can be bound to a
* property that returns a single object.
* - "M" to-one: the aspect can be bound to a
* property that returns multiple objects.
*
* 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.
*
*
*/