summaryrefslogtreecommitdiff
path: root/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/TableAssociation.java
diff options
context:
space:
mode:
Diffstat (limited to 'projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/TableAssociation.java')
-rw-r--r--projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/TableAssociation.java927
1 files changed, 927 insertions, 0 deletions
diff --git a/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/TableAssociation.java b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/TableAssociation.java
new file mode 100644
index 0000000..bc02f7d
--- /dev/null
+++ b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/TableAssociation.java
@@ -0,0 +1,927 @@
+/*
+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.EventQueue;
+import java.awt.Graphics;
+import java.awt.Rectangle;
+import java.awt.Toolkit;
+import java.awt.datatransfer.Clipboard;
+import java.awt.datatransfer.StringSelection;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.event.FocusEvent;
+import java.awt.event.FocusListener;
+import java.awt.event.KeyEvent;
+import java.awt.event.MouseAdapter;
+import java.awt.event.MouseEvent;
+import java.util.Enumeration;
+
+import javax.swing.CellEditor;
+import javax.swing.JComponent;
+import javax.swing.JTable;
+import javax.swing.KeyStroke;
+import javax.swing.event.ListSelectionEvent;
+import javax.swing.event.ListSelectionListener;
+import javax.swing.table.AbstractTableModel;
+import javax.swing.table.JTableHeader;
+import javax.swing.table.TableColumn;
+import javax.swing.table.TableModel;
+
+import net.wotonomy.control.EOSortOrdering;
+import net.wotonomy.foundation.NSArray;
+import net.wotonomy.foundation.NSMutableArray;
+import net.wotonomy.foundation.internal.WotonomyException;
+import net.wotonomy.ui.EOAssociation;
+import net.wotonomy.ui.EODisplayGroup;
+
+/**
+* TableAssociation binds one or more TableColumnAssociations
+* to a display group. You should not instantiate this class
+* directly; use TableColumnAssociation.setTable() instead.
+* Note that TableAssociation inserts itself as the controlled
+* JTable's TableModel.
+*
+* Bindings are:
+* <ul>
+* <li>source: a property convertable to a string for
+* display in the cells of the table column</li>
+* <li>enabled: a property convertable to a string for
+* display in the cells of the table column.
+* Note that you can bind this aspect to a key equal to
+* "true" or "false" and leave the display group null.</li>
+* </ul>
+*
+* @author michael@mpowers.net
+* @author $Author: cgruber $
+* @version $Revision: 904 $
+*/
+public class TableAssociation extends EOAssociation
+ implements ActionListener, ListSelectionListener, FocusListener
+{
+ static final NSArray aspects =
+ new NSArray( new Object[] {
+ SourceAspect, EnabledAspect
+ } );
+ static final NSArray aspectSignatures =
+ new NSArray( new Object[] {
+ AttributeToOneAspectSignature
+ } );
+ static final NSArray objectKeysTaken =
+ new NSArray( new Object[] {
+ "tableModel", "tableHeader"
+ } );
+
+ // key command to copy contents to clipboard
+ static public final String COPY = "COPY";
+
+ EODisplayGroup source;
+ EODisplayGroup sortTarget; // used by TreeColumnAssociation
+ NSMutableArray columns;
+ JTableHeader tableHeader;
+
+ boolean pleaseIgnore;
+ boolean selectionPaintedImmediately;
+ boolean selectionTracking;
+
+ /**
+ * 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 TableAssociation ( Object anObject )
+ {
+ super( anObject );
+ source = null;
+ columns = new NSMutableArray();
+ JTable aTable = ((JTable)anObject);
+ aTable.addFocusListener( this );
+ aTable.setModel(
+ new TableAssociationModel( this ) );
+ // set up keyboard events for cut-copy: Ctrl-C, Ctrl-X
+
+ // why did sun make this harder to use?
+ //aTable.getInputMap( JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT ).put(
+ // KeyStroke.getKeyStroke( KeyEvent.VK_C, java.awt.Event.CTRL_MASK ), COPY);
+ //aTable.getActionMap().put(COPY, this);
+
+ aTable.registerKeyboardAction( this, COPY,
+ KeyStroke.getKeyStroke( KeyEvent.VK_C,
+ Toolkit.getDefaultToolkit().getMenuShortcutKeyMask() ),
+ JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT );
+ aTable.registerKeyboardAction( this, COPY,
+ KeyStroke.getKeyStroke( KeyEvent.VK_X,
+ Toolkit.getDefaultToolkit().getMenuShortcutKeyMask() ),
+ JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT );
+ tableHeader = new SortedTableHeader();
+ tableHeader.setColumnModel( aTable.getColumnModel() );
+ aTable.setTableHeader( tableHeader );
+ tableHeader.addMouseListener( new TableHeaderListener() );
+ pleaseIgnore = false;
+ selectionPaintedImmediately = false;
+ selectionTracking = false;
+ }
+
+ /**
+ * 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 JTable );
+ }
+
+ /**
+ * 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 ( SourceAspect.equals( anAspect ) )
+ {
+ source = aDisplayGroup;
+ }
+ 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 ()
+ {
+ if ( source == null )
+ {
+ throw new WotonomyException( "No display group: " +
+ "please ensure that the TableColumnAssociation " +
+ "has a display group bound to the ValueAspect" );
+ }
+ super.establishConnection();
+ selectFromDisplayGroup();
+ addAsListener();
+ }
+
+ /**
+ * Breaks the connection between this association and
+ * its object. Override to stop listening for events
+ * from the object.
+ */
+ public void breakConnection ()
+ {
+ removeAsListener();
+ super.breakConnection();
+ }
+
+ protected void addAsListener()
+ {
+ component().getSelectionModel()
+ .addListSelectionListener( this );
+ }
+
+ protected void removeAsListener()
+ {
+ component().getSelectionModel()
+ .removeListSelectionListener( this );
+ }
+
+ /**
+ * Forces this association to cause the object to
+ * stop editing and validate the user's input.
+ * @return false if there were problems validating,
+ * or true to continue.
+ */
+ public boolean endEditing ()
+ {
+ // stop any cell editing
+ CellEditor editor = component().getCellEditor();
+ if ( editor != null )
+ {
+ return editor.stopCellEditing();
+ }
+ return true;
+ }
+
+ /**
+ * Called when either the selection or the contents
+ * of an associated display group have changed.
+ */
+ public void subjectChanged ()
+ {
+ if ( source != null )
+ {
+ if ( source.contentsChanged() )
+ {
+ removeAsListener();
+ ((TableAssociationModel)component().getModel()).
+ fireTableDataChanged();
+ selectFromDisplayGroup();
+ addAsListener();
+
+ // if we caused this change, do nothing
+ if ( pleaseIgnore )
+ {
+ pleaseIgnore = false;
+ }
+ else // otherwise, update the sort indicator
+ {
+ tableHeader.repaint();
+
+ // cancel any cell editing
+ CellEditor editor = component().getCellEditor();
+ if ( editor != null )
+ {
+ editor.cancelCellEditing();
+ }
+ }
+ }
+ else
+ if ( source.selectionChanged() )
+ {
+ removeAsListener();
+ selectFromDisplayGroup();
+ addAsListener();
+ }
+ }
+
+ }
+
+ private void selectFromDisplayGroup()
+ {
+ JTable component = component();
+
+ int index;
+ component.getSelectionModel().clearSelection();
+ Enumeration e =
+ source.selectionIndexes().objectEnumerator();
+
+ while ( e.hasMoreElements() )
+ { // add selections one-by-one to support non-contiguous
+ index = ((Number)e.nextElement()).intValue();
+ component.getSelectionModel().addSelectionInterval(
+ index, index ); // adds one row
+ }
+ }
+
+ // interface ListSelectionListener
+
+ public void valueChanged(ListSelectionEvent e)
+ {
+ if ( source != null )
+ {
+ if ( selectionTracking || !e.getValueIsAdjusting() )
+ {
+ int[] selectedIndices = component().getSelectedRows();
+ final NSMutableArray indexList = new NSMutableArray();
+ for ( int i = 0; i < selectedIndices.length; i++ )
+ {
+ indexList.addObject( new Integer( selectedIndices[i] ) );
+ }
+
+ // invoke later so the component is repainted before
+ // any potentially lengthy second-order effects happen:
+ // this improves user-perceived responsiveness of big apps
+ Runnable select = new Runnable()
+ {
+ public void run()
+ {
+ pleaseIgnore = true;
+ source.setSelectionIndexes( indexList );
+ }
+ };
+ if ( selectionPaintedImmediately )
+ {
+ EventQueue.invokeLater( select );
+ }
+ else
+ {
+ select.run();
+ }
+ }
+ }
+ }
+
+ /**
+ * Determines whether the selection should be painted
+ * immediately after the user clicks and therefore
+ * before the children display group is updated.
+ * When the children group is bound to many associations
+ * or is bound to a master-detail association, updating
+ * the display group can take a perceptibly long time.
+ * This property defaults to false.
+ * @see #setSelectionPaintedImmediately
+ */
+ public boolean isSelectionPaintedImmediately()
+ {
+ return selectionPaintedImmediately;
+ }
+
+ /**
+ * Sets whether the selection should be painted immediately.
+ * Setting this property to true will let the table paint
+ * first before the display group is updated.
+ */
+ public void setSelectionPaintedImmediately( boolean isImmediate )
+ {
+ selectionPaintedImmediately = isImmediate;
+ }
+
+ /**
+ * Determines whether the selection is actively tracking
+ * the selection as the user moves the mouse.
+ * If true, selection will not be updated while the
+ * list selection event returns true for isValueAdjusting().
+ * This property defaults to false.
+ * @see #setSelectionTracking
+ */
+ public boolean isSelectionTracking()
+ {
+ return selectionTracking;
+ }
+
+ /**
+ * Sets whether the selection is actively tracking
+ * the selection as the user moves the mouse.
+ * Setting this property to true will update the display
+ * group with each change to the selection.
+ * This means that any tree selection listers will
+ * also be notified before the display group is updated
+ * and will have to invokeLater if they want to see the
+ * updated display group.
+ */
+ public void setSelectionTracking( boolean isTracking )
+ {
+ selectionTracking = isTracking;
+ }
+
+ // convenience
+ private JTable component()
+ {
+ return (JTable) object();
+ }
+
+ /**
+ * Copies the contents of the table to the clipboard as a tab-delimited string.
+ */
+ public void copyToClipboard()
+ {
+ Toolkit toolkit = Toolkit.getDefaultToolkit();
+ Clipboard clipboard = toolkit.getSystemClipboard();
+ StringSelection selection =
+ new StringSelection( getTabDelimitedString() );
+ clipboard.setContents( selection, selection );
+ }
+
+ /**
+ * Converts the contents of the table to a tab-delimited string.
+ * @return A String containing the text contents of the table.
+ */
+ public String getTabDelimitedString()
+ {
+ StringBuffer result = new StringBuffer(64);
+
+ TableModel model = component().getModel();
+ int cols = model.getColumnCount();
+ int rows = model.getRowCount();
+
+ Object o = null;
+ for ( int y = 0; y < rows; y++ )
+ {
+ for ( int x = 0; x < cols; x++ )
+ {
+ o = model.getValueAt( y, x );
+ if ( o == null ) o = "";
+ result.append( o );
+ result.append( '\t' );
+ }
+ result.append( '\n' );
+ }
+
+ return result.toString();
+ }
+
+ // interface ActionEventListener
+
+ public void actionPerformed(ActionEvent evt)
+ {
+ String cmd = evt.getActionCommand();
+
+ if ( COPY.equals( cmd ) )
+ {
+ copyToClipboard();
+ return;
+ }
+ }
+
+ /**
+ * Used to render the little triangle over the sorted column(s).
+ */
+ class SortedTableHeader extends JTableHeader
+ {
+ public void paint(Graphics g)
+ {
+ super.paint( g );
+ Rectangle r;
+ TableColumnAssociation association;
+ int size = columns.size();
+ NSArray orderings;
+ if ( sortTarget != null )
+ {
+ orderings = sortTarget.sortOrderings();
+ }
+ else
+ {
+ orderings = source.sortOrderings();
+ }
+ for ( int i = 0; i < size; i++ )
+ {
+ r = getHeaderRect( component().convertColumnIndexToView( i ) );
+ association = (TableColumnAssociation) columns.objectAtIndex( i );
+ association.drawSortIndicator( r, g, orderings );
+ }
+ }
+ }
+
+ /**
+ * Used to listen for clicks on the table header.
+ */
+ class TableHeaderListener extends MouseAdapter
+ {
+ public void mouseClicked( MouseEvent evt )
+ {
+ EODisplayGroup displayGroup = sortTarget;
+ if ( displayGroup == null ) displayGroup = source;
+
+ if ( evt.getClickCount() > 0 )
+ {
+ int columnClicked = tableHeader.columnAtPoint( evt.getPoint() );
+ if ( columnClicked != -1 )
+ {
+ columnClicked = component().convertColumnIndexToModel( columnClicked );
+ TableColumnAssociation association = (TableColumnAssociation)
+ columns.objectAtIndex( columnClicked );
+ if ( association.isSortable() )
+ {
+ NSMutableArray newOrder =
+ new NSMutableArray( displayGroup.sortOrderings() );
+
+ // click once = asc, twice = desc, thrice = clear
+ EOSortOrdering sortOrdering;
+ int index = association.getIndexOfMatchingOrdering( newOrder );
+ if ( index == -1 ) sortOrdering = null;
+ else if ( index == 1 ) sortOrdering = association.getSortOrdering( false );
+ else sortOrdering = association.getSortOrdering( true );
+
+ pleaseIgnore = true;
+ tableHeader.repaint();
+
+ // stop any cell editing
+ CellEditor editor = component().getCellEditor();
+ if ( editor != null )
+ {
+ editor.stopCellEditing();
+ }
+
+ // remove existing key
+ if ( index != 0 )
+ {
+ newOrder.removeObjectAtIndex( Math.abs( index ) - 1 );
+ }
+
+ // put new key at front of list
+ if ( sortOrdering != null )
+ {
+ newOrder.insertObjectAtIndex( sortOrdering, 0 );
+ }
+
+ displayGroup.setSortOrderings( newOrder );
+ displayGroup.updateDisplayedObjects();
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Notifies of beginning of edit.
+ */
+ public void focusGained(FocusEvent evt)
+ {
+ Object o;
+ EODisplayGroup displayGroup;
+ Enumeration e = aspects().objectEnumerator();
+ while ( e.hasMoreElements() )
+ {
+ displayGroup =
+ displayGroupForAspect( e.nextElement().toString() );
+ if ( displayGroup != null )
+ {
+ displayGroup.associationDidBeginEditing( this );
+ }
+ }
+ }
+
+ /**
+ * Updates object on focus lost and notifies of end of edit.
+ */
+ public void focusLost(FocusEvent evt)
+ {
+ if ( ! component().isEditing() )
+ {
+ Object o;
+ EODisplayGroup displayGroup;
+ Enumeration e = aspects().objectEnumerator();
+ while ( e.hasMoreElements() )
+ {
+ displayGroup =
+ displayGroupForAspect( e.nextElement().toString() );
+ if ( displayGroup != null )
+ {
+ displayGroup.associationDidEndEditing( this );
+ }
+ }
+ }
+ }
+
+ /**
+ * Used as the model for the controlled table.
+ * Package access so TableColumnAssociation can recognize
+ * it and use the addColumnAssociation() method.
+ * Extends AbstractTableModel just so we get event
+ * broadcasting for free.
+ */
+ static class TableAssociationModel extends AbstractTableModel
+ {
+ private TableAssociation parent;
+
+ private TableAssociationModel( TableAssociation aParent )
+ {
+ parent = aParent;
+ }
+
+ public TableAssociation getAssociation()
+ {
+ return parent;
+ }
+
+ /**
+ * Adds the column to the list of ColumnAssociations,
+ * and adds the corresponding column to the table
+ * at the next available index, setting the value of
+ * the column's model index accordingly.
+ * Establishes connection if no columns are currently
+ * associated.
+ */
+ public void addColumnAssociation(
+ TableColumnAssociation aColumnAssociation )
+ {
+ // establish connection if necessary
+ if ( parent.columns.size() == 0 )
+ {
+ parent.establishConnection();
+ }
+
+ int newIndex = parent.columns.count();
+ parent.columns.add( aColumnAssociation );
+
+ // add column to table
+ TableColumn column = (TableColumn) aColumnAssociation.object();
+ column.setModelIndex( newIndex );
+ parent.component().addColumn( column );
+
+ }
+
+ /**
+ * Removes the column from the list of ColumnAssociations,
+ * and removes the corresponding column from the table.
+ * Breaks connection if no more columns are associated.
+ */
+ public void removeColumnAssociation(
+ TableColumnAssociation aColumnAssociation )
+ {
+ int index = parent.columns.indexOfIdenticalObject( aColumnAssociation );
+ if ( index == NSArray.NotFound ) return;
+
+ parent.columns.removeObjectAtIndex( index );
+
+ // remove column from table
+ TableColumn column = (TableColumn) aColumnAssociation.object();
+ parent.component().removeColumn( column );
+
+ // break connection if necessary
+ if ( parent.columns.size() == 0 )
+ {
+ parent.breakConnection();
+ }
+ }
+
+ public int getRowCount()
+ {
+ if ( parent.source == null ) return 0;
+ return parent.source.displayedObjects().count();
+ }
+
+ public int getColumnCount()
+ {
+ return parent.columns.count();
+ }
+
+ /**
+ * Attempts to retrieve the header value from the specified column,
+ * or returns " " if the value is null.
+ */
+ public String getColumnName(int columnIndex)
+ {
+ TableColumnAssociation association = (TableColumnAssociation)
+ parent.columns.objectAtIndex( columnIndex );
+ Object value = ((TableColumn)association.object()).getHeaderValue();
+ if ( value != null ) return value.toString();
+ return " ";
+ }
+
+ /**
+ * Returns the class of the first item in the
+ * display group bound to the column.
+ */
+ public Class getColumnClass(int columnIndex)
+ {
+ Object value;
+ int count = getRowCount();
+ for( int i = 0; i < count; i++ )
+ { //First row in column is null find one that is not.
+ value = ((TableColumnAssociation)parent.columns.
+ objectAtIndex( columnIndex ) ).valueAtIndex( i );
+ if ( value != null ) return value.getClass();
+ }
+ return Object.class;
+ }
+
+ /**
+ * Calls the column association's isEditableAtRow method.
+ */
+ public boolean isCellEditable(int rowIndex,
+ int columnIndex)
+ {
+ return
+ ((TableColumnAssociation)parent.columns.objectAtIndex(
+ columnIndex ) ).isEditableAtRow( rowIndex );
+ }
+
+ /**
+ * Calls the column association's valueAtIndex method.
+ */
+ public Object getValueAt(int rowIndex,
+ int columnIndex)
+ {
+ return
+ ((TableColumnAssociation)parent.columns.objectAtIndex(
+ columnIndex ) ).valueAtIndex( rowIndex );
+ }
+
+ /**
+ * Calls the column association's setValueAtIndex method.
+ */
+ public void setValueAt(Object aValue,
+ int rowIndex,
+ int columnIndex)
+ {
+ Object existingValue = getValueAt( rowIndex, columnIndex );
+
+ // don't update display group if value is the same as before
+ if ( aValue == existingValue ) return; // handles null case
+ if ( existingValue != null ) // both are not null
+ {
+ Object newValue = aValue;
+
+ // if different types
+ if ( newValue.getClass() != existingValue.getClass() )
+ {
+ // convert to string since most editors do anyway
+ newValue = newValue.toString();
+ existingValue = existingValue.toString();
+ }
+ if ( newValue.equals( existingValue ) )
+ {
+ // same value - do nothing
+ return;
+ }
+ }
+
+ ((TableColumnAssociation)parent.columns.objectAtIndex(
+ columnIndex ) ).setValueAtIndex( aValue, rowIndex );
+ }
+
+ }
+
+}
+
+/*
+ * $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.31 2003/08/06 23:07:52 chochos
+ * general code cleanup (mostly, removing unused imports)
+ *
+ * Revision 1.30 2002/11/16 16:33:31 mpowers
+ * Now using platform-specific accelerator key for shortcuts.
+ *
+ * Revision 1.29 2002/05/24 14:41:29 mpowers
+ * Throwing an exception for clarity.
+ *
+ * Revision 1.28 2002/04/10 21:20:04 mpowers
+ * Better handling for tree nodes when working with editing contexts.
+ * Better handling for invalidation. No longer broadcasting events
+ * when nodes have not been "registered" in the tree.
+ *
+ * Revision 1.27 2002/03/05 23:18:28 mpowers
+ * Added documentation.
+ * Added isSelectionPaintedImmediate and isSelectionTracking attributes
+ * to TableAssociation.
+ * Added getTableAssociation to TableColumnAssociation.
+ *
+ * Revision 1.25 2002/03/04 22:49:53 mpowers
+ * Now working with TreeColumnAssociation to delegate sorting behavior.
+ *
+ * Revision 1.23 2002/03/04 03:58:17 mpowers
+ * Refined table header click behavior.
+ *
+ * Revision 1.22 2002/03/01 23:41:39 mpowers
+ * Added facility to get table association from model.
+ *
+ * Revision 1.21 2002/03/01 20:41:22 mpowers
+ * Cleaned up unused code.
+ *
+ * Revision 1.20 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.19 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.18 2002/02/28 22:45:27 mpowers
+ * Now registers as editing association when table gets focus, so that
+ * endEditing can appropriate commit any table cell editors.
+ *
+ * Revision 1.17 2001/10/26 20:01:50 mpowers
+ * We're again cancelling instead of stopping editing on subjectChanged.
+ * I believe that the introduction of NSRunLoop has allowed us to return
+ * to the correct behavior.
+ *
+ * Revision 1.16 2001/09/14 13:40:26 mpowers
+ * User-initiated selection changes are now handled on the next event loop
+ * so that the component repaints the new selection before any potentially
+ * lengthy logic is triggered by the selection change.
+ *
+ * Revision 1.15 2001/07/25 15:03:01 mpowers
+ * getColumnName now checks for a column header value before defaulting " ".
+ *
+ * Revision 1.14 2001/06/05 19:09:25 mpowers
+ * Fixed broken build.
+ *
+ * Revision 1.13 2001/06/05 19:08:12 mpowers
+ * Better determination of column class. Previously, if the first object
+ * was null, Object.class was used. Now we get the first non-null object.
+ *
+ * Revision 1.12 2001/05/02 17:39:01 mpowers
+ * Now selects from display group after initial population.
+ *
+ * Revision 1.11 2001/03/29 23:05:22 mpowers
+ * Fixes for editing table cells.
+ *
+ * Revision 1.10 2001/03/09 22:09:22 mpowers
+ * Now better handling jdk1.1 for rendering the column header.
+ *
+ * Revision 1.9 2001/02/27 02:10:38 mpowers
+ * No longer updating values to the display group if the value
+ * has not changed.
+ *
+ * Revision 1.8 2001/02/17 16:52:05 mpowers
+ * Changes in imports to support building with jdk1.1 collections.
+ *
+ * Revision 1.7 2001/02/16 17:47:17 mpowers
+ * Now cancelling any cell editing on subjectChanged().
+ *
+ * Revision 1.6 2001/01/12 19:11:56 mpowers
+ * Fixed table column click sorting.
+ *
+ * Revision 1.5 2001/01/12 17:20:30 mpowers
+ * Moved EOSortOrdering creation to ColumnAssociation.
+ *
+ * Revision 1.4 2001/01/11 21:55:57 mpowers
+ * Implemented sort indicator for table column headers.
+ *
+ * Revision 1.3 2001/01/11 20:34:26 mpowers
+ * Implemented EOSortOrdering and added support in framework.
+ * Added header-click to sort table columns.
+ *
+ * Revision 1.2 2001/01/08 20:40:51 mpowers
+ * JTables in 1.3 clear their selection when the data model changes,
+ * which sends a list selection event. We now reset the selection again
+ * after the data changes so we're compatible across 1.2 and 1.3.
+ *
+ * Revision 1.1.1.1 2000/12/21 15:49:00 mpowers
+ * Contributing wotonomy.
+ *
+ * Revision 1.7 2000/12/20 16:25:41 michael
+ * Added log to all files.
+ *
+ *
+ */
+