diff options
| author | Benjamin Culkin <scorpress@gmail.com> | 2024-05-19 17:56:33 -0400 |
|---|---|---|
| committer | Benjamin Culkin <scorpress@gmail.com> | 2024-05-19 17:56:33 -0400 |
| commit | aedc34d55462a75e329bbf342251ff6504cd117e (patch) | |
| tree | bcc8f1f2352582717b484df302aeea6696b8f000 /projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/TableColumnAssociation.java | |
Initial import from SVN
Diffstat (limited to 'projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/TableColumnAssociation.java')
| -rw-r--r-- | projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/TableColumnAssociation.java | 708 |
1 files changed, 708 insertions, 0 deletions
diff --git a/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/TableColumnAssociation.java b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/TableColumnAssociation.java new file mode 100644 index 0000000..e1c32f3 --- /dev/null +++ b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/TableColumnAssociation.java @@ -0,0 +1,708 @@ +/* +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: +* <ul> +* <li>value: a property convertable to a string for +* display in the cells of the table column</li> +* <li>editable: a property convertable to a boolean +* that determines the editability of the corresponding +* cells in the column.</li> +* </ul> + +* 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. <ul> + * <li>"A" attribute: the aspect can be bound to + * an attribute.</li> + * <li>"1" to-one: the aspect can be bound to a + * property that returns a single object.</li> + * <li>"M" to-one: the aspect can be bound to a + * property that returns multiple objects.</li> + * </ul> + * 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. + * + * + */ + |
