/*
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.
*
*
*/