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.Component;
22 import java.awt.event.ActionEvent;
23 import java.awt.event.ActionListener;
24 import java.awt.event.FocusEvent;
25 import java.awt.event.FocusListener;
26 import java.net.URL;
27 import java.text.Format;
28 import java.text.ParseException;
29 import java.util.Enumeration;
30 import java.util.Iterator;
31
32 import javax.swing.Icon;
33 import javax.swing.ImageIcon;
34 import javax.swing.JOptionPane;
35 import javax.swing.JTextArea;
36 import javax.swing.LookAndFeel;
37 import javax.swing.Timer;
38 import javax.swing.event.DocumentEvent;
39 import javax.swing.event.DocumentListener;
40 import javax.swing.text.Document;
41 import javax.swing.text.JTextComponent;
42
43 import net.wotonomy.foundation.NSArray;
44 import net.wotonomy.foundation.NSSelector;
45 import net.wotonomy.foundation.internal.ValueConverter;
46 import net.wotonomy.foundation.internal.WotonomyException;
47 import net.wotonomy.ui.EOAssociation;
48 import net.wotonomy.ui.EODisplayGroup;
49
50 /***
51 * TimedTextAssociation works like TextAssociation,
52 * but instead of using a delayed event to update the
53 * model, it uses a timer so that the model is only
54 * updated if the user pauses typing for some short interval.
55 * This is useful when the update and/or re-read of the model
56 * is a costly operation.
57 * Bindings are:
58 * <ul>
59 * <li>value: a property convertable to/from a string</li>
60 * <li>editable: a boolean property that determines whether
61 * the user can edit the text in the field</li>
62 * <li>enabled: a boolean property that determines whether
63 * the user can select the text in the field</li>
64 * <li>label: a boolean property that determines whether
65 * field should appear as a read-only, selectable label</li>
66 * <li>icon: a property that returns a Swing icon, for use
67 * with JLabels and other components with setIcon() methods.
68 * If bound to a static string, the string will be used to
69 * load an image resource from the selected object's class.</li>
70 * </ul>
71 *
72 * @author michael@mpowers.net
73 * @author $Author: cgruber $
74 * @version $Revision: 904 $
75 */
76 public class TimedTextAssociation extends EOAssociation
77 implements FocusListener, ActionListener, DocumentListener
78 {
79
80
81
82 static final NSArray aspects =
83 new NSArray( new Object[] {
84 ValueAspect, EnabledAspect, EditableAspect, LabelAspect, IconAspect
85 } );
86 static final NSArray aspectSignatures =
87 new NSArray( new Object[] {
88 AttributeToOneAspectSignature,
89 AttributeToOneAspectSignature,
90 AttributeToOneAspectSignature,
91 AttributeToOneAspectSignature
92 } );
93 static final NSArray objectKeysTaken =
94 new NSArray( new Object[] {
95 "text", "enabled", "editable"
96 } );
97
98 private final static NSSelector getText =
99 new NSSelector( "getText" );
100 private final static NSSelector setText =
101 new NSSelector( "setText",
102 new Class[] { String.class } );
103 private final static NSSelector getDocument =
104 new NSSelector( "getDocument" );
105 private final static NSSelector setIcon =
106 new NSSelector( "setIcon",
107 new Class[] { Icon.class } );
108 private final static NSSelector addActionListener =
109 new NSSelector( "addActionListener",
110 new Class[] { ActionListener.class } );
111 private final static NSSelector removeActionListener =
112 new NSSelector( "removeActionListener",
113 new Class[] { ActionListener.class } );
114 private final static NSSelector addFocusListener =
115 new NSSelector( "addFocusListener",
116 new Class[] { FocusListener.class } );
117 private final static NSSelector removeFocusListener =
118 new NSSelector( "removeFocusListener",
119 new Class[] { FocusListener.class } );
120
121
122 protected boolean wasNull;
123 protected static final String EMPTY_STRING = "";
124
125
126 protected boolean needsUpdate;
127 protected boolean hasDocument;
128 protected boolean isListening;
129
130
131 protected Format format;
132
133
134 private EODisplayGroup valueDisplayGroup;
135 private String valueKey;
136
137
138 protected boolean autoUpdating;
139 protected int interval = 400;
140 protected Timer keyTimer;
141
142
143
144
145
146
147
148
149
150
151 /***
152 * Constructor specifying the object to be controlled by this
153 * association. Does not establish connection.
154 */
155 public TimedTextAssociation ( Object anObject )
156 {
157 super( anObject );
158 wasNull = false;
159 needsUpdate = false;
160 hasDocument = false;
161 isListening = true;
162 valueDisplayGroup = null;
163 valueKey = null;
164
165 autoUpdating = true;
166 keyTimer = null;
167 }
168
169 /***
170 * Returns a List of aspect signatures whose contents
171 * correspond with the aspects list. Each element is
172 * a string whose characters represent a capability of
173 * the corresponding aspect. <ul>
174 * <li>"A" attribute: the aspect can be bound to
175 * an attribute.</li>
176 * <li>"1" to-one: the aspect can be bound to a
177 * property that returns a single object.</li>
178 * <li>"M" to-one: the aspect can be bound to a
179 * property that returns multiple objects.</li>
180 * </ul>
181 * An empty signature "" means that the aspect can
182 * bind without needing a key.
183 * This implementation returns "A1M" for each
184 * element in the aspects array.
185 */
186 public static NSArray aspectSignatures ()
187 {
188 return aspectSignatures;
189 }
190
191 /***
192 * Returns a List that describes the aspects supported
193 * by this class. Each element in the list is the string
194 * name of the aspect. This implementation returns an
195 * empty list.
196 */
197 public static NSArray aspects ()
198 {
199 return aspects;
200 }
201
202 /***
203 * Returns a List of EOAssociation subclasses that,
204 * for the objects that are usable for this association,
205 * are less suitable than this association.
206 */
207 public static NSArray associationClassesSuperseded ()
208 {
209 return new NSArray();
210 }
211
212 /***
213 * Returns whether this class can control the specified
214 * object.
215 */
216 public static boolean isUsableWithObject ( Object anObject )
217 {
218 return setText.implementedByObject( anObject );
219 }
220
221 /***
222 * Returns a List of properties of the controlled object
223 * that are controlled by this class. For example,
224 * "stringValue", or "selected".
225 */
226 public static NSArray objectKeysTaken ()
227 {
228 return objectKeysTaken;
229 }
230
231 /***
232 * Returns the aspect that is considered primary
233 * or default. This is typically "value" or somesuch.
234 */
235 public static String primaryAspect ()
236 {
237 return ValueAspect;
238 }
239
240 /***
241 * Returns whether this association can bind to the
242 * specified display group on the specified key for
243 * the specified aspect.
244 */
245 public boolean canBindAspect (
246 String anAspect, EODisplayGroup aDisplayGroup, String aKey)
247 {
248 return ( aspects.containsObject( anAspect ) );
249 }
250
251 /***
252 * Binds the specified aspect of this association to the
253 * specified key on the specified display group.
254 */
255 public void bindAspect (
256 String anAspect, EODisplayGroup aDisplayGroup, String aKey )
257 {
258 if ( ValueAspect.equals( anAspect ) )
259 {
260 valueDisplayGroup = aDisplayGroup;
261 valueKey = aKey;
262 }
263 super.bindAspect( anAspect, aDisplayGroup, aKey );
264 }
265
266 /***
267 * Establishes a connection between this association
268 * and the controlled object. This implementation
269 * attempts to add this class as an ActionListener
270 * and as a FocusListener to the specified object.
271 */
272 public void establishConnection ()
273 {
274 Object component = object();
275 try
276 {
277 if ( addActionListener.implementedByObject( component ) )
278 {
279 addActionListener.invoke( component, this );
280 }
281 if ( addFocusListener.implementedByObject( component ) )
282 {
283 addFocusListener.invoke( component, this );
284 }
285 hasDocument = false;
286 if ( getDocument.implementedByObject( component ) )
287 {
288 Object document = getDocument.invoke( component );
289 if ( document instanceof Document )
290 {
291 ((Document)document).addDocumentListener( this );
292 hasDocument = true;
293 }
294 }
295 }
296 catch ( Exception exc )
297 {
298 throw new WotonomyException(
299 "Error while establishing connection", exc );
300 }
301
302 super.establishConnection();
303
304
305 subjectChanged();
306 }
307
308 /***
309 * Breaks the connection between this association and
310 * its object. Override to stop listening for events
311 * from the object.
312 */
313 public void breakConnection ()
314 {
315 Object component = object();
316 try
317 {
318 if ( removeActionListener.implementedByObject( component ) )
319 {
320 removeActionListener.invoke( component, this );
321 }
322 if ( removeFocusListener.implementedByObject( component ) )
323 {
324 removeFocusListener.invoke( component, this );
325 }
326 if ( getDocument.implementedByObject( component ) )
327 {
328 Object document = getDocument.invoke( component );
329 if ( document instanceof Document )
330 {
331 ((Document)document).removeDocumentListener( this );
332 }
333 }
334 }
335 catch ( Exception exc )
336 {
337 throw new WotonomyException(
338 "Error while breaking connection", exc );
339 }
340 super.breakConnection();
341 }
342
343 /***
344 * Called when either the selection or the contents
345 * of an associated display group have changed.
346 */
347 public void subjectChanged ()
348 {
349 Object component = object();
350 EODisplayGroup displayGroup;
351 String key;
352 Object value;
353
354
355 displayGroup = valueDisplayGroup;
356 if ( displayGroup != null )
357 {
358 if ( component instanceof Component )
359 {
360 ((Component)component).setEnabled(
361 displayGroup.enabledToSetSelectedObjectValueForKey( valueKey ) );
362 }
363
364 key = valueKey;
365
366 if ( displayGroup.selectedObjects().size() > 1 )
367 {
368
369
370 Object previousValue;
371
372 Iterator indexIterator = displayGroup.selectionIndexes().
373 iterator();
374
375
376 int initialIndex = ( (Integer)indexIterator.next() ).intValue();
377 previousValue = displayGroup.valueForObjectAtIndex(
378 initialIndex, key );
379 value = null;
380
381
382
383
384
385
386 while ( indexIterator.hasNext() )
387 {
388 int index = ( (Integer)indexIterator.next() ).intValue();
389 Object currentValue = displayGroup.valueForObjectAtIndex(
390 index, key );
391 if ( currentValue != null && !currentValue.equals( previousValue ) )
392 {
393 value = null;
394 break;
395 }
396 else
397 {
398
399 value = currentValue;
400 }
401
402 }
403
404 } else {
405
406
407 value = displayGroup.selectedObjectValueForKey( key );
408 }
409
410
411 if ( value == null )
412 {
413 wasNull = true;
414 value = EMPTY_STRING;
415 }
416 else
417 {
418 wasNull = false;
419 if ( format() != null )
420 {
421 try
422 {
423 value = format().format( value );
424 }
425 catch ( IllegalArgumentException exc )
426 {
427 value = value.toString();
428 }
429 }
430 }
431
432
433 try
434 {
435 if ( ! value.toString().equals( getText.invoke( component ) ) )
436 {
437
438
439 boolean wasListening = isListening;
440 isListening = false;
441
442
443 setText.invoke( component, value.toString() );
444
445 isListening = wasListening;
446 needsUpdate = false;
447 }
448 }
449 catch ( Exception exc )
450 {
451 throw new WotonomyException(
452 "Error while updating component connection", exc );
453 }
454 }
455
456
457 displayGroup = displayGroupForAspect( IconAspect );
458 key = displayGroupKeyForAspect( IconAspect );
459 if ( key != null )
460 {
461 if ( displayGroup != null )
462 {
463 value =
464 displayGroup.selectedObjectValueForKey( key );
465 }
466 else
467 {
468
469
470 value = null;
471 Object o = displayGroup.selectedObject();
472 if ( o != null )
473 {
474 URL url = o.getClass().getResource( key );
475 if ( url != null )
476 {
477 value = new ImageIcon( url );
478 }
479 }
480 }
481
482 try
483 {
484 setIcon.invoke( component, value );
485 }
486 catch ( Exception exc )
487 {
488 throw new WotonomyException(
489 "Error while updating component connection", exc );
490 }
491 }
492
493
494 displayGroup = displayGroupForAspect( EnabledAspect );
495 key = displayGroupKeyForAspect( EnabledAspect );
496 if ( ( key != null )
497 && ( component instanceof Component ) )
498 {
499 if ( displayGroup != null )
500 {
501 value =
502 displayGroup.selectedObjectValueForKey( key );
503 }
504 else
505 {
506
507 value = key;
508 }
509 Boolean converted = null;
510 if ( value != null )
511 {
512 converted = (Boolean)
513 ValueConverter.convertObjectToClass(
514 value, Boolean.class );
515 }
516 if ( converted == null ) converted = Boolean.FALSE;
517 if ( ((Component)component).isEnabled() != converted.booleanValue() )
518 {
519 ((Component)component).setEnabled( converted.booleanValue() );
520 }
521 }
522
523
524 displayGroup = displayGroupForAspect( EditableAspect );
525 key = displayGroupKeyForAspect( EditableAspect );
526 if ( ( key != null )
527 && ( component instanceof JTextComponent ) )
528 {
529 if ( displayGroup != null )
530 {
531 value =
532 displayGroup.selectedObjectValueForKey( key );
533 }
534 else
535 {
536
537 value = key;
538 }
539 Boolean converted = (Boolean)
540 ValueConverter.convertObjectToClass(
541 value, Boolean.class );
542
543 if ( converted != null )
544 {
545 if ( converted.booleanValue() != ((JTextComponent)component).isEditable() )
546 {
547 ((JTextComponent)component).setEditable( converted.booleanValue() );
548 }
549 }
550 }
551
552
553 displayGroup = displayGroupForAspect( LabelAspect );
554 key = displayGroupKeyForAspect( LabelAspect );
555
556 if ( ( key != null )
557 && ( component instanceof JTextComponent ) )
558 {
559 if ( displayGroup != null )
560 {
561 value =
562 displayGroup.selectedObjectValueForKey( key );
563 }
564 else
565 {
566
567 value = key;
568 }
569 Boolean converted = (Boolean)
570 ValueConverter.convertObjectToClass(
571 value, Boolean.class );
572
573 if ( converted != null )
574 {
575 if ( converted.booleanValue() )
576 {
577 if ( component instanceof JTextComponent )
578 {
579 if ( component instanceof JTextArea )
580 {
581 areaToLabel( (JTextArea) component );
582 }
583 else
584 {
585 fieldToLabel( (JTextComponent) component );
586 }
587 }
588 }
589 else
590 {
591 if ( component instanceof JTextComponent )
592 {
593 if ( component instanceof JTextArea )
594 {
595 labelToArea( (JTextArea) component );
596 }
597 else
598 {
599 labelToField( (JTextComponent ) component );
600 }
601 }
602 }
603 }
604 }
605
606 }
607
608 private void fieldToLabel( JTextComponent aTextField )
609 {
610
611
612 aTextField.setEditable(false);
613 aTextField.setOpaque(false);
614
615
616
617 LookAndFeel.installBorder(aTextField, "Label.border");
618
619 LookAndFeel.installColorsAndFont(aTextField,
620 "Label.background",
621 "Label.foreground",
622 "Label.font");
623 }
624
625 private void labelToField( JTextComponent aTextField )
626 {
627
628
629 aTextField.setEditable(true);
630 aTextField.setOpaque(true);
631
632
633
634 LookAndFeel.installBorder(aTextField, "TextField.border");
635
636 LookAndFeel.installColorsAndFont(aTextField,
637 "TextField.background",
638 "TextField.foreground",
639 "TextField.font");
640 }
641
642 private void areaToLabel( JTextArea aTextArea )
643 {
644
645
646 aTextArea.setLineWrap(true);
647 aTextArea.setWrapStyleWord(true);
648 aTextArea.setEditable(false);
649
650
651
652
653 LookAndFeel.installBorder(aTextArea, "Label.border");
654
655 LookAndFeel.installColorsAndFont(aTextArea,
656 "Label.background",
657 "Label.foreground",
658 "Label.font");
659
660 }
661
662 private void labelToArea( JTextArea aTextArea )
663 {
664
665
666 aTextArea.setEditable(true);
667
668
669
670 LookAndFeel.installBorder(aTextArea, "TextArea.border");
671
672 LookAndFeel.installColorsAndFont(aTextArea,
673 "TextArea.background",
674 "TextArea.foreground",
675 "TextArea.font");
676 }
677
678
679 /***
680 * Forces this association to cause the object to
681 * stop editing and validate the user's input.
682 * @return false if there were problems validating,
683 * or true to continue.
684 */
685 public boolean endEditing ()
686 {
687 if ( keyTimer != null )
688 {
689 keyTimer.stop();
690 keyTimer.removeActionListener( this );
691 keyTimer = null;
692 }
693 return writeValueToDisplayGroup();
694 }
695
696 /***
697 * Writes the value currently in the component
698 * to the selected object in the display group
699 * bound to the value aspect.
700 * @return false if there were problems validating,
701 * or true to continue.
702 */
703 protected boolean writeValueToDisplayGroup()
704 {
705 boolean returnValue = true;
706 if ( hasDocument && !needsUpdate ) return true;
707
708 EODisplayGroup displayGroup = valueDisplayGroup;
709 if ( displayGroup != null )
710 {
711 String key = valueKey;
712 Object component = object();
713 Object value = null;
714 try
715 {
716
717
718 value = getText.invoke( component );
719
720 }
721 catch ( Exception exc )
722 {
723 throw new WotonomyException(
724 "Error updating display group", exc );
725 }
726
727 if ( ( wasNull ) && ( EMPTY_STRING.equals( value ) ) )
728 {
729 value = null;
730 }
731 else
732 if ( format() != null )
733 {
734 try
735 {
736 value = format().parseObject( value.toString() );
737 }
738 catch ( ParseException exc )
739 {
740 String message = exc.getMessage();
741
742 if ( displayGroup.associationFailedToValidateValue(
743 this, value.toString(), key, exc, message ) )
744 {
745 boolean wasListening = isListening;
746 isListening = false;
747 JOptionPane.showMessageDialog(
748 (Component)component, message );
749 isListening = wasListening;
750 }
751 needsUpdate = false;
752 return false;
753 }
754 }
755
756 needsUpdate = false;
757
758
759 Object existingValue = displayGroup.selectedObjectValueForKey( key );
760 if ( displayGroup.selectedObjects().size() == 1 )
761 {
762 if ( existingValue == value ) return true;
763 if ( ( existingValue != null ) && ( existingValue.equals( value ) ) ) return true;
764 if ( ( value != null ) && ( value.equals( existingValue ) ) ) return true;
765 }
766
767
768 boolean wasListening = isListening;
769 isListening = false;
770
771 Iterator selectedIterator = displayGroup.selectionIndexes().iterator();
772 while ( selectedIterator.hasNext() )
773 {
774 int index = ( (Integer)selectedIterator.next() ).intValue();
775
776 if ( displayGroup.setValueForObjectAtIndex( value, index, key ) )
777 {
778 isListening = wasListening;
779 needsUpdate = false;
780 }
781 else
782 {
783 isListening = wasListening;
784 needsUpdate = false;
785 returnValue = false;
786 }
787 }
788
789 }
790 return returnValue;
791 }
792
793 /***
794 * Sets the Format that is used to convert values from the display
795 * group to and from text that is displayed in the component.
796 * Having a formatter disables auto-updating.
797 */
798 public void setFormat( Format aFormat )
799 {
800 format = aFormat;
801 }
802
803 /***
804 * Gets the Format that is used to convert values from the display
805 * group to and from text that is displayed in the component.
806 */
807 public Format format()
808 {
809 return format;
810 }
811
812
813
814 /***
815 * Updates object on action performed.
816 */
817 public void actionPerformed( ActionEvent evt )
818 {
819 if ( keyTimer != null )
820 {
821 keyTimer.stop();
822 keyTimer.removeActionListener( this );
823 keyTimer = null;
824 }
825 if ( ! isListening ) return;
826 if ( needsUpdate )
827 {
828 writeValueToDisplayGroup();
829 }
830 }
831
832
833
834 /***
835 * Notifies of beginning of edit.
836 */
837 public void focusGained(FocusEvent evt)
838 {
839 if ( ! isListening ) return;
840 Object o;
841 EODisplayGroup displayGroup;
842 Enumeration e = aspects().objectEnumerator();
843 while ( e.hasMoreElements() )
844 {
845 displayGroup =
846 displayGroupForAspect( e.nextElement().toString() );
847 if ( displayGroup != null )
848 {
849 displayGroup.associationDidBeginEditing( this );
850 }
851 }
852 }
853
854 /***
855 * Updates object on focus lost and notifies of end of edit.
856 */
857 public void focusLost(FocusEvent evt)
858 {
859 if ( ! isListening ) return;
860 if ( endEditing() )
861 {
862 Object o;
863 EODisplayGroup displayGroup;
864 Enumeration e = aspects().objectEnumerator();
865 while ( e.hasMoreElements() )
866 {
867 displayGroup =
868 displayGroupForAspect( e.nextElement().toString() );
869 if ( displayGroup != null )
870 {
871 displayGroup.associationDidEndEditing( this );
872 }
873 }
874 }
875 else
876 {
877
878 }
879 }
880
881 /***
882 * Returns whether the data model is updated for every change
883 * in the controlled component. If false, the data is only
884 * updated on focus lost or the enter key. Default is true.
885 */
886 public boolean isAutoUpdating()
887 {
888 if ( format() != null ) return false;
889 return autoUpdating;
890 }
891
892 /***
893 * Sets whether the data model is updated for every change
894 * in the controlled component.
895 */
896 public void setAutoUpdating( boolean isAutoUpdating )
897 {
898 autoUpdating = isAutoUpdating;
899 }
900
901 /***
902 * Triggers the key timer to start.
903 */
904 protected void queueUpdate()
905 {
906 if ( isAutoUpdating() )
907 {
908 if ( keyTimer == null )
909 {
910 keyTimer = new Timer( interval, this );
911 }
912 keyTimer.restart();
913 }
914 }
915
916
917
918 public void insertUpdate(DocumentEvent e)
919 {
920 if ( ! isListening ) return;
921 needsUpdate = true;
922 queueUpdate();
923 }
924
925 public void removeUpdate(DocumentEvent e)
926 {
927 if ( ! isListening ) return;
928 needsUpdate = true;
929 queueUpdate();
930 }
931
932 public void changedUpdate(DocumentEvent e)
933 {
934 if ( ! isListening ) return;
935 needsUpdate = true;
936 queueUpdate();
937 }
938
939 }
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029