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