summaryrefslogtreecommitdiff
path: root/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/TableColumnAssociation.java
diff options
context:
space:
mode:
authorBenjamin Culkin <scorpress@gmail.com>2024-05-19 17:56:33 -0400
committerBenjamin Culkin <scorpress@gmail.com>2024-05-19 17:56:33 -0400
commitaedc34d55462a75e329bbf342251ff6504cd117e (patch)
treebcc8f1f2352582717b484df302aeea6696b8f000 /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.java708
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.
+ *
+ *
+ */
+