View Javadoc

1   /*
2   Wotonomy: OpenStep design patterns for pure Java applications.
3   Copyright (C) 2000 Intersect Software Corporation
4   
5   This library is free software; you can redistribute it and/or
6   modify it under the terms of the GNU Lesser General Public
7   License as published by the Free Software Foundation; either
8   version 2.1 of the License, or (at your option) any later version.
9   
10  This library is distributed in the hope that it will be useful,
11  but WITHOUT ANY WARRANTY; without even the implied warranty of
12  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13  Lesser General Public License for more details.
14  
15  You should have received a copy of the GNU Lesser General Public
16  License along with this library; if not, see http://www.gnu.org
17  */
18  
19  package net.wotonomy.ui.swing;
20  
21  import java.awt.EventQueue;
22  import java.awt.Graphics;
23  import java.awt.Rectangle;
24  import java.awt.Toolkit;
25  import java.awt.datatransfer.Clipboard;
26  import java.awt.datatransfer.StringSelection;
27  import java.awt.event.ActionEvent;
28  import java.awt.event.ActionListener;
29  import java.awt.event.FocusEvent;
30  import java.awt.event.FocusListener;
31  import java.awt.event.KeyEvent;
32  import java.awt.event.MouseAdapter;
33  import java.awt.event.MouseEvent;
34  import java.util.Enumeration;
35  
36  import javax.swing.CellEditor;
37  import javax.swing.JComponent;
38  import javax.swing.JTable;
39  import javax.swing.KeyStroke;
40  import javax.swing.event.ListSelectionEvent;
41  import javax.swing.event.ListSelectionListener;
42  import javax.swing.table.AbstractTableModel;
43  import javax.swing.table.JTableHeader;
44  import javax.swing.table.TableColumn;
45  import javax.swing.table.TableModel;
46  
47  import net.wotonomy.control.EOSortOrdering;
48  import net.wotonomy.foundation.NSArray;
49  import net.wotonomy.foundation.NSMutableArray;
50  import net.wotonomy.foundation.internal.WotonomyException;
51  import net.wotonomy.ui.EOAssociation;
52  import net.wotonomy.ui.EODisplayGroup;
53  
54  /***
55  * TableAssociation binds one or more TableColumnAssociations
56  * to a display group.  You should not instantiate this class
57  * directly; use TableColumnAssociation.setTable() instead.
58  * Note that TableAssociation inserts itself as the controlled
59  * JTable's TableModel.
60  *
61  * Bindings are:
62  * <ul>
63  * <li>source: a property convertable to a string for
64  * display in the cells of the table column</li>
65  * <li>enabled: a property convertable to a string for
66  * display in the cells of the table column.
67  * Note that you can bind this aspect to a key equal to
68  * "true" or "false" and leave the display group null.</li>
69  * </ul>
70  *
71  * @author michael@mpowers.net
72  * @author $Author: cgruber $
73  * @version $Revision: 904 $
74  */
75  public class TableAssociation extends EOAssociation
76  	implements ActionListener, ListSelectionListener, FocusListener
77  {
78      static final NSArray aspects =
79          new NSArray( new Object[] {
80              SourceAspect, EnabledAspect
81          } );
82      static final NSArray aspectSignatures =
83          new NSArray( new Object[] {
84              AttributeToOneAspectSignature
85          } );
86      static final NSArray objectKeysTaken =
87          new NSArray( new Object[] {
88              "tableModel", "tableHeader"
89          } );
90  
91      // key command to copy contents to clipboard
92      static public final String COPY = "COPY";
93  
94  	EODisplayGroup source;
95      EODisplayGroup sortTarget; // used by TreeColumnAssociation
96  	NSMutableArray columns;
97      JTableHeader tableHeader;
98  
99      boolean pleaseIgnore;
100     boolean selectionPaintedImmediately;
101     boolean selectionTracking;
102 
103     /***
104     * Constructor specifying the object to be controlled by this
105     * association.  Throws an exception if the object is not
106 	* a TableColumn.  setTable() must be called before
107 	* establishing the connection.
108     */
109     public TableAssociation ( Object anObject )
110     {
111         super( anObject );
112 		source = null;
113 		columns = new NSMutableArray();
114 		JTable aTable = ((JTable)anObject);
115         aTable.addFocusListener( this );
116 		aTable.setModel(
117 			new TableAssociationModel( this ) );
118 			// set up keyboard events for cut-copy: Ctrl-C, Ctrl-X
119 
120         // why did sun make this harder to use? 
121         //aTable.getInputMap( JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT ).put(
122         //   KeyStroke.getKeyStroke( KeyEvent.VK_C, java.awt.Event.CTRL_MASK ), COPY);
123         //aTable.getActionMap().put(COPY, this);
124         
125         aTable.registerKeyboardAction( this, COPY,
126 			KeyStroke.getKeyStroke( KeyEvent.VK_C, 
127             Toolkit.getDefaultToolkit().getMenuShortcutKeyMask() ),
128 			JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT );
129 		aTable.registerKeyboardAction( this, COPY,
130 			KeyStroke.getKeyStroke( KeyEvent.VK_X, 
131             Toolkit.getDefaultToolkit().getMenuShortcutKeyMask() ),
132 			JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT );
133         tableHeader = new SortedTableHeader();
134         tableHeader.setColumnModel( aTable.getColumnModel() );
135         aTable.setTableHeader( tableHeader );
136         tableHeader.addMouseListener( new TableHeaderListener() );
137         pleaseIgnore = false;
138         selectionPaintedImmediately = false;
139         selectionTracking = false;
140     }
141 
142     /***
143     * Returns a List of aspect signatures whose contents
144     * correspond with the aspects list.  Each element is
145     * a string whose characters represent a capability of
146     * the corresponding aspect. <ul>
147     * <li>"A" attribute: the aspect can be bound to
148     * an attribute.</li>
149     * <li>"1" to-one: the aspect can be bound to a
150     * property that returns a single object.</li>
151     * <li>"M" to-one: the aspect can be bound to a
152     * property that returns multiple objects.</li>
153     * </ul>
154     * An empty signature "" means that the aspect can
155     * bind without needing a key.
156     * This implementation returns "A1M" for each
157     * element in the aspects array.
158     */
159     public static NSArray aspectSignatures ()
160     {
161         return aspectSignatures;
162     }
163 
164     /***
165     * Returns a List that describes the aspects supported
166     * by this class.  Each element in the list is the string
167     * name of the aspect.  This implementation returns an
168     * empty list.
169     */
170     public static NSArray aspects ()
171     {
172         return aspects;
173     }
174 
175     /***
176     * Returns a List of EOAssociation subclasses that,
177     * for the objects that are usable for this association,
178     * are less suitable than this association.
179     */
180     public static NSArray associationClassesSuperseded ()
181     {
182         return new NSArray();
183     }
184 
185     /***
186     * Returns whether this class can control the specified
187     * object.
188     */
189     public static boolean isUsableWithObject ( Object anObject )
190     {
191         return ( anObject instanceof JTable );
192     }
193 
194     /***
195     * Returns a List of properties of the controlled object
196     * that are controlled by this class.  For example,
197     * "stringValue", or "selected".
198     */
199     public static NSArray objectKeysTaken ()
200     {
201         return objectKeysTaken;
202     }
203 
204     /***
205     * Returns the aspect that is considered primary
206     * or default.  This is typically "value" or somesuch.
207     */
208     public static String primaryAspect ()
209     {
210         return ValueAspect;
211     }
212 
213     /***
214     * Returns whether this association can bind to the
215     * specified display group on the specified key for
216     * the specified aspect.
217     */
218     public boolean canBindAspect (
219         String anAspect, EODisplayGroup aDisplayGroup, String aKey)
220     {
221         return ( aspects.containsObject( anAspect ) );
222     }
223 
224     /***
225     * Binds the specified aspect of this association to the
226     * specified key on the specified display group.
227     */
228     public void bindAspect (
229         String anAspect, EODisplayGroup aDisplayGroup, String aKey )
230 	{
231 		if ( SourceAspect.equals( anAspect ) )
232 		{
233 			source = aDisplayGroup;
234 		}
235 		super.bindAspect( anAspect, aDisplayGroup, aKey );
236 	}
237 
238     /***
239     * Establishes a connection between this association
240     * and the controlled object.  Subclasses should begin
241     * listening for events from their controlled object here.
242     */
243     public void establishConnection ()
244     {
245         if ( source == null )
246         {
247             throw new WotonomyException( "No display group: " + 
248                 "please ensure that the TableColumnAssociation " + 
249                 "has a display group bound to the ValueAspect" );
250         }
251         super.establishConnection();
252         selectFromDisplayGroup();
253         addAsListener();
254     }
255 
256     /***
257     * Breaks the connection between this association and
258     * its object.  Override to stop listening for events
259     * from the object.
260     */
261     public void breakConnection ()
262     {
263         removeAsListener();
264         super.breakConnection();
265     }
266 
267     protected void addAsListener()
268     {
269         component().getSelectionModel()
270 			.addListSelectionListener( this );
271     }
272 
273     protected void removeAsListener()
274     {
275         component().getSelectionModel()
276 			.removeListSelectionListener( this );
277     }
278 
279     /***
280     * Forces this association to cause the object to
281     * stop editing and validate the user's input.
282     * @return false if there were problems validating,
283     * or true to continue.
284     */
285     public boolean endEditing ()
286     {
287         // stop any cell editing
288         CellEditor editor = component().getCellEditor();
289         if ( editor != null )
290         {
291             return editor.stopCellEditing();
292         }
293         return true;
294     }
295 
296     /***
297     * Called when either the selection or the contents
298     * of an associated display group have changed.
299     */
300     public void subjectChanged ()
301     {
302 		if ( source != null )
303 		{
304 			if ( source.contentsChanged() )
305 			{
306                 removeAsListener();
307 				((TableAssociationModel)component().getModel()).
308 					fireTableDataChanged();
309 				selectFromDisplayGroup();
310                 addAsListener();
311 
312                 // if we caused this change, do nothing
313                 if ( pleaseIgnore )
314                 {
315                     pleaseIgnore = false;
316                 }
317                 else // otherwise, update the sort indicator
318                 {
319                     tableHeader.repaint();
320 
321                     // cancel any cell editing
322                     CellEditor editor = component().getCellEditor();
323                     if ( editor != null )
324                     {
325                         editor.cancelCellEditing();
326                     }
327                 }
328   			}
329             else
330 			if ( source.selectionChanged() )
331 			{
332                 removeAsListener();
333 				selectFromDisplayGroup();
334         		addAsListener();
335 			}
336 		}
337 
338     }
339 
340 	private void selectFromDisplayGroup()
341 	{
342 		JTable component = component();
343 
344 		int index;
345 		component.getSelectionModel().clearSelection();
346 		Enumeration e =
347 			source.selectionIndexes().objectEnumerator();
348 
349 		while ( e.hasMoreElements() )
350 		{   // add selections one-by-one to support non-contiguous
351 			index = ((Number)e.nextElement()).intValue();
352 			component.getSelectionModel().addSelectionInterval(
353 				index, index ); // adds one row
354 		}
355 	}
356 
357 	// interface ListSelectionListener
358 
359     public void valueChanged(ListSelectionEvent e)
360     {
361         if ( source != null ) 
362         {
363             if ( selectionTracking || !e.getValueIsAdjusting() )
364             {
365                 int[] selectedIndices = component().getSelectedRows();
366                 final NSMutableArray indexList = new NSMutableArray();
367                 for ( int i = 0; i < selectedIndices.length; i++ )
368                 {
369                     indexList.addObject( new Integer( selectedIndices[i] ) );
370                 }
371                 
372                 // invoke later so the component is repainted before
373                 //   any potentially lengthy second-order effects happen:
374                 //   this improves user-perceived responsiveness of big apps
375                 Runnable select = new Runnable()
376                 {
377                     public void run()
378                     {
379                         pleaseIgnore = true;
380                         source.setSelectionIndexes( indexList );
381                     }
382                 };
383                 if ( selectionPaintedImmediately )
384                 {
385                     EventQueue.invokeLater( select );
386                 }
387                 else
388                 {
389                     select.run();
390                 }
391             }
392         }
393     }
394 
395     /***
396     * Determines whether the selection should be painted
397     * immediately after the user clicks and therefore
398     * before the children display group is updated.
399     * When the children group is bound to many associations
400     * or is bound to a master-detail association, updating
401     * the display group can take a perceptibly long time.
402     * This property defaults to false.
403     * @see #setSelectionPaintedImmediately
404     */
405     public boolean isSelectionPaintedImmediately()
406     {
407         return selectionPaintedImmediately;
408     }
409 
410     /***
411     * Sets whether the selection should be painted immediately.
412     * Setting this property to true will let the table paint
413     * first before the display group is updated.
414     */
415     public void setSelectionPaintedImmediately( boolean isImmediate )
416     {
417         selectionPaintedImmediately = isImmediate;
418     }
419 
420     /***
421     * Determines whether the selection is actively tracking
422     * the selection as the user moves the mouse.
423     * If true, selection will not be updated while the
424     * list selection event returns true for isValueAdjusting().
425     * This property defaults to false.
426     * @see #setSelectionTracking
427     */
428     public boolean isSelectionTracking()
429     {
430         return selectionTracking;
431     }
432 
433     /***
434     * Sets whether the selection is actively tracking
435     * the selection as the user moves the mouse.
436     * Setting this property to true will update the display
437     * group with each change to the selection.
438     * This means that any tree selection listers will
439     * also be notified before the display group is updated
440     * and will have to invokeLater if they want to see the
441     * updated display group.
442     */
443     public void setSelectionTracking( boolean isTracking )
444     {
445         selectionTracking = isTracking;
446     }
447 
448     // convenience
449     private JTable component()
450     {
451         return (JTable) object();
452     }
453 
454 	/***
455 	* Copies the contents of the table to the clipboard as a tab-delimited string.
456 	*/
457     public void copyToClipboard()
458     {
459         Toolkit toolkit = Toolkit.getDefaultToolkit();
460         Clipboard clipboard = toolkit.getSystemClipboard();
461         StringSelection selection =
462             new StringSelection( getTabDelimitedString() );
463         clipboard.setContents( selection, selection );
464     }
465 
466 	/***
467 	* Converts the contents of the table to a tab-delimited string.
468 	* @return A String containing the text contents of the table.
469 	*/
470 	public String getTabDelimitedString()
471     {
472         StringBuffer result = new StringBuffer(64);
473 
474         TableModel model = component().getModel();
475         int cols = model.getColumnCount();
476         int rows = model.getRowCount();
477 
478         Object o = null;
479         for ( int y = 0; y < rows; y++ )
480         {
481             for ( int x = 0; x < cols; x++ )
482             {
483                 o = model.getValueAt( y, x );
484                 if ( o == null ) o = "";
485                 result.append( o );
486                 result.append( '\t' );
487             }
488             result.append( '\n' );
489         }
490 
491         return result.toString();
492     }
493 
494     // interface ActionEventListener
495 
496     public void actionPerformed(ActionEvent evt)
497     {
498         String cmd = evt.getActionCommand();
499 
500         if ( COPY.equals( cmd ) )
501         {
502             copyToClipboard();
503             return;
504         }
505     }
506 
507     /***
508     * Used to render the little triangle over the sorted column(s).
509     */
510     class SortedTableHeader extends JTableHeader
511     {
512         public void paint(Graphics g)
513         {
514             super.paint( g );
515             Rectangle r;
516             TableColumnAssociation association;
517             int size = columns.size();
518             NSArray orderings;
519             if ( sortTarget != null ) 
520             {
521                 orderings = sortTarget.sortOrderings();
522             }
523             else
524             {
525                 orderings = source.sortOrderings();
526             }
527             for ( int i = 0; i < size; i++ )
528             {
529                 r = getHeaderRect( component().convertColumnIndexToView( i ) );
530                 association = (TableColumnAssociation) columns.objectAtIndex( i );
531                 association.drawSortIndicator( r, g, orderings );
532             }
533         }
534     }
535 
536     /***
537     * Used to listen for clicks on the table header.
538     */
539     class TableHeaderListener extends MouseAdapter
540     {
541         public void mouseClicked( MouseEvent evt )
542         {
543             EODisplayGroup displayGroup = sortTarget;
544             if ( displayGroup == null ) displayGroup = source;
545             
546             if ( evt.getClickCount() > 0 )
547             {
548                 int columnClicked = tableHeader.columnAtPoint( evt.getPoint() );
549                 if ( columnClicked != -1 )
550                 {
551                     columnClicked = component().convertColumnIndexToModel( columnClicked );
552                     TableColumnAssociation association = (TableColumnAssociation)
553                         columns.objectAtIndex( columnClicked );
554                     if ( association.isSortable() )
555                     {
556                         NSMutableArray newOrder =
557                             new NSMutableArray( displayGroup.sortOrderings() );
558 
559                         // click once = asc, twice = desc, thrice = clear
560                         EOSortOrdering sortOrdering;
561                         int index = association.getIndexOfMatchingOrdering( newOrder );
562                         if ( index == -1 ) sortOrdering = null;
563                         else if ( index == 1 ) sortOrdering = association.getSortOrdering( false );
564                         else sortOrdering = association.getSortOrdering( true );
565     
566                         pleaseIgnore = true;
567                         tableHeader.repaint();
568 
569                         // stop any cell editing
570                         CellEditor editor = component().getCellEditor();
571                         if ( editor != null )
572                         {
573                             editor.stopCellEditing();
574                         }
575 
576                         // remove existing key
577                         if ( index != 0 )
578                         {
579                             newOrder.removeObjectAtIndex( Math.abs( index ) - 1 );
580                         }
581                         
582                         // put new key at front of list
583                         if ( sortOrdering != null )
584                         {
585                             newOrder.insertObjectAtIndex( sortOrdering, 0 );
586                         }
587                         
588                         displayGroup.setSortOrderings( newOrder );
589                         displayGroup.updateDisplayedObjects();
590                     }
591                 }
592             }
593         }
594     }
595 
596 	/***
597 	* Notifies of beginning of edit.
598 	*/
599     public void focusGained(FocusEvent evt)
600     {
601 		Object o;
602 		EODisplayGroup displayGroup;
603         Enumeration e = aspects().objectEnumerator();
604 		while ( e.hasMoreElements() )
605 		{
606 			displayGroup =
607 				displayGroupForAspect( e.nextElement().toString() );
608 			if ( displayGroup != null )
609 			{
610 				displayGroup.associationDidBeginEditing( this );
611 			}
612 		}
613     }
614 
615 	/***
616 	* Updates object on focus lost and notifies of end of edit.
617 	*/
618     public void focusLost(FocusEvent evt)
619     {
620         if ( ! component().isEditing() )
621         {
622             Object o;
623             EODisplayGroup displayGroup;
624             Enumeration e = aspects().objectEnumerator();
625             while ( e.hasMoreElements() )
626             {
627                 displayGroup =
628                     displayGroupForAspect( e.nextElement().toString() );
629                 if ( displayGroup != null )
630                 {
631                     displayGroup.associationDidEndEditing( this );
632                 }
633             }
634         }
635     }
636 
637 	/***
638 	* Used as the model for the controlled table.
639 	* Package access so TableColumnAssociation can recognize
640 	* it and use the addColumnAssociation() method.
641 	* Extends AbstractTableModel just so we get event
642 	* broadcasting for free.
643 	*/
644 	static class TableAssociationModel extends AbstractTableModel
645 	{
646 		private TableAssociation parent;
647 
648 		private TableAssociationModel( TableAssociation aParent )
649 		{
650 			parent = aParent;
651 		}
652         
653         public TableAssociation getAssociation()
654         {
655             return parent;
656         }
657 
658 		/***
659 		* Adds the column to the list of ColumnAssociations,
660 		* and adds the corresponding column to the table
661 		* at the next available index, setting the value of
662 		* the column's model index accordingly.
663         * Establishes connection if no columns are currently 
664         * associated.
665 		*/
666 		public void addColumnAssociation(
667 			TableColumnAssociation aColumnAssociation )
668 		{
669             // establish connection if necessary
670             if ( parent.columns.size() == 0 )
671             {
672                 parent.establishConnection();
673             }
674             
675 			int newIndex = parent.columns.count();
676 			parent.columns.add( aColumnAssociation );
677 
678 			// add column to table
679 			TableColumn column = (TableColumn) aColumnAssociation.object();
680 			column.setModelIndex( newIndex );
681 			parent.component().addColumn( column );
682             
683 		}
684 
685 		/***
686 		* Removes the column from the list of ColumnAssociations,
687 		* and removes the corresponding column from the table.
688         * Breaks connection if no more columns are associated.
689 		*/
690 		public void removeColumnAssociation(
691 			TableColumnAssociation aColumnAssociation )
692 		{
693 			int index = parent.columns.indexOfIdenticalObject( aColumnAssociation );
694             if ( index == NSArray.NotFound ) return;
695             
696 			parent.columns.removeObjectAtIndex( index );
697 
698 			// remove column from table
699 			TableColumn column = (TableColumn) aColumnAssociation.object();
700 			parent.component().removeColumn( column );
701             
702             // break connection if necessary
703             if ( parent.columns.size() == 0 )
704             {
705                 parent.breakConnection();
706             }
707 		}
708 
709 		public int getRowCount()
710 		{
711 			if ( parent.source == null ) return 0;
712 			return parent.source.displayedObjects().count();
713 		}
714 
715 		public int getColumnCount()
716 		{
717 			return parent.columns.count();
718 		}
719 
720 		/***
721 		* Attempts to retrieve the header value from the specified column, 
722         * or returns " " if the value is null.
723 		*/
724 		public String getColumnName(int columnIndex)
725 		{
726             TableColumnAssociation association = (TableColumnAssociation)
727                 parent.columns.objectAtIndex( columnIndex );
728             Object value = ((TableColumn)association.object()).getHeaderValue();
729             if ( value != null ) return value.toString();
730             return " ";
731 		}
732 
733 		/***
734 		* Returns the class of the first item in the
735 		* display group bound to the column.
736 		*/
737 		public Class getColumnClass(int columnIndex)
738 		{
739             Object value;
740             int count = getRowCount();
741             for( int i = 0; i < count; i++ )
742             { //First row in column is null find one that is not.
743                 value = ((TableColumnAssociation)parent.columns.
744                     objectAtIndex( columnIndex ) ).valueAtIndex( i );
745                 if ( value != null ) return value.getClass();
746             }
747             return Object.class;
748 		}
749 
750         /***
751         * Calls the column association's isEditableAtRow method.
752         */
753 		public boolean isCellEditable(int rowIndex,
754 									  int columnIndex)
755 		{
756 			return
757 				((TableColumnAssociation)parent.columns.objectAtIndex(
758 					columnIndex ) ).isEditableAtRow( rowIndex );
759 		}
760 
761         /***
762         * Calls the column association's valueAtIndex method.
763         */
764 		public Object getValueAt(int rowIndex,
765 								 int columnIndex)
766 		{
767 			return
768 				((TableColumnAssociation)parent.columns.objectAtIndex(
769 					columnIndex ) ).valueAtIndex( rowIndex );
770 		}
771 
772         /***
773         * Calls the column association's setValueAtIndex method.
774         */
775 		public void setValueAt(Object aValue,
776 							   int rowIndex,
777 							   int columnIndex)
778 		{
779             Object existingValue = getValueAt( rowIndex, columnIndex );
780             
781             // don't update display group if value is the same as before
782             if ( aValue == existingValue ) return; // handles null case
783             if ( existingValue != null ) // both are not null
784             {
785                 Object newValue = aValue;
786                 
787                 // if different types
788                 if ( newValue.getClass() != existingValue.getClass() )
789                 {
790                     // convert to string since most editors do anyway
791                     newValue = newValue.toString();
792                     existingValue = existingValue.toString();
793                 }
794                 if ( newValue.equals( existingValue ) )
795                 {
796                     // same value - do nothing
797                     return;
798                 }
799             }
800 
801             ((TableColumnAssociation)parent.columns.objectAtIndex(
802                 columnIndex ) ).setValueAtIndex( aValue, rowIndex );
803 		}
804 
805 	}
806 
807 }
808 
809 /*
810  * $Log$
811  * Revision 1.2  2006/02/18 23:19:05  cgruber
812  * Update imports and maven dependencies.
813  *
814  * Revision 1.1  2006/02/16 13:22:22  cgruber
815  * Check in all sources in eclipse-friendly maven-enabled packages.
816  *
817  * Revision 1.31  2003/08/06 23:07:52  chochos
818  * general code cleanup (mostly, removing unused imports)
819  *
820  * Revision 1.30  2002/11/16 16:33:31  mpowers
821  * Now using platform-specific accelerator key for shortcuts.
822  *
823  * Revision 1.29  2002/05/24 14:41:29  mpowers
824  * Throwing an exception for clarity.
825  *
826  * Revision 1.28  2002/04/10 21:20:04  mpowers
827  * Better handling for tree nodes when working with editing contexts.
828  * Better handling for invalidation.  No longer broadcasting events
829  * when nodes have not been "registered" in the tree.
830  *
831  * Revision 1.27  2002/03/05 23:18:28  mpowers
832  * Added documentation.
833  * Added isSelectionPaintedImmediate and isSelectionTracking attributes
834  * to TableAssociation.
835  * Added getTableAssociation to TableColumnAssociation.
836  *
837  * Revision 1.25  2002/03/04 22:49:53  mpowers
838  * Now working with TreeColumnAssociation to delegate sorting behavior.
839  *
840  * Revision 1.23  2002/03/04 03:58:17  mpowers
841  * Refined table header click behavior.
842  *
843  * Revision 1.22  2002/03/01 23:41:39  mpowers
844  * Added facility to get table association from model.
845  *
846  * Revision 1.21  2002/03/01 20:41:22  mpowers
847  * Cleaned up unused code.
848  *
849  * Revision 1.20  2002/03/01 15:42:00  mpowers
850  * Table column headers now always show their sort indicator.
851  * A third table-column click clears the sort for that column.
852  *
853  * Revision 1.19  2002/02/28 23:01:39  mpowers
854  * TableColumnAssociations add and remove themselves from the TableAssociation
855  * when their connection is established and broken respectively.
856  * TableAssociations now break connection if they have no column associations.
857  *
858  * Revision 1.18  2002/02/28 22:45:27  mpowers
859  * Now registers as editing association when table gets focus, so that
860  * endEditing can appropriate commit any table cell editors.
861  *
862  * Revision 1.17  2001/10/26 20:01:50  mpowers
863  * We're again cancelling instead of stopping editing on subjectChanged.
864  * I believe that the introduction of NSRunLoop has allowed us to return
865  * to the correct behavior.
866  *
867  * Revision 1.16  2001/09/14 13:40:26  mpowers
868  * User-initiated selection changes are now handled on the next event loop
869  * so that the component repaints the new selection before any potentially
870  * lengthy logic is triggered by the selection change.
871  *
872  * Revision 1.15  2001/07/25 15:03:01  mpowers
873  * getColumnName now checks for a column header value before defaulting " ".
874  *
875  * Revision 1.14  2001/06/05 19:09:25  mpowers
876  * Fixed broken build.
877  *
878  * Revision 1.13  2001/06/05 19:08:12  mpowers
879  * Better determination of column class.  Previously, if the first object
880  * was null, Object.class was used.  Now we get the first non-null object.
881  *
882  * Revision 1.12  2001/05/02 17:39:01  mpowers
883  * Now selects from display group after initial population.
884  *
885  * Revision 1.11  2001/03/29 23:05:22  mpowers
886  * Fixes for editing table cells.
887  *
888  * Revision 1.10  2001/03/09 22:09:22  mpowers
889  * Now better handling jdk1.1 for rendering the column header.
890  *
891  * Revision 1.9  2001/02/27 02:10:38  mpowers
892  * No longer updating values to the display group if the value
893  * has not changed.
894  *
895  * Revision 1.8  2001/02/17 16:52:05  mpowers
896  * Changes in imports to support building with jdk1.1 collections.
897  *
898  * Revision 1.7  2001/02/16 17:47:17  mpowers
899  * Now cancelling any cell editing on subjectChanged().
900  *
901  * Revision 1.6  2001/01/12 19:11:56  mpowers
902  * Fixed table column click sorting.
903  *
904  * Revision 1.5  2001/01/12 17:20:30  mpowers
905  * Moved EOSortOrdering creation to ColumnAssociation.
906  *
907  * Revision 1.4  2001/01/11 21:55:57  mpowers
908  * Implemented sort indicator for table column headers.
909  *
910  * Revision 1.3  2001/01/11 20:34:26  mpowers
911  * Implemented EOSortOrdering and added support in framework.
912  * Added header-click to sort table columns.
913  *
914  * Revision 1.2  2001/01/08 20:40:51  mpowers
915  * JTables in 1.3 clear their selection when the data model changes,
916  * which sends a list selection event.  We now reset the selection again
917  * after the data changes so we're compatible across 1.2 and 1.3.
918  *
919  * Revision 1.1.1.1  2000/12/21 15:49:00  mpowers
920  * Contributing wotonomy.
921  *
922  * Revision 1.7  2000/12/20 16:25:41  michael
923  * Added log to all files.
924  *
925  *
926  */
927