1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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
92 static public final String COPY = "COPY";
93
94 EODisplayGroup source;
95 EODisplayGroup sortTarget;
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
119
120
121
122
123
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
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
313 if ( pleaseIgnore )
314 {
315 pleaseIgnore = false;
316 }
317 else
318 {
319 tableHeader.repaint();
320
321
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 {
351 index = ((Number)e.nextElement()).intValue();
352 component.getSelectionModel().addSelectionInterval(
353 index, index );
354 }
355 }
356
357
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
373
374
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
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
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
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
570 CellEditor editor = component().getCellEditor();
571 if ( editor != null )
572 {
573 editor.stopCellEditing();
574 }
575
576
577 if ( index != 0 )
578 {
579 newOrder.removeObjectAtIndex( Math.abs( index ) - 1 );
580 }
581
582
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
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
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
699 TableColumn column = (TableColumn) aColumnAssociation.object();
700 parent.component().removeColumn( column );
701
702
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 {
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
782 if ( aValue == existingValue ) return;
783 if ( existingValue != null )
784 {
785 Object newValue = aValue;
786
787
788 if ( newValue.getClass() != existingValue.getClass() )
789 {
790
791 newValue = newValue.toString();
792 existingValue = existingValue.toString();
793 }
794 if ( newValue.equals( existingValue ) )
795 {
796
797 return;
798 }
799 }
800
801 ((TableColumnAssociation)parent.columns.objectAtIndex(
802 columnIndex ) ).setValueAtIndex( aValue, rowIndex );
803 }
804
805 }
806
807 }
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927