KickJava   Java API By Example, From Geeks To Geeks.

Java > Open Source Codes > javax > swing > text > InternationalFormatter


1 /*
2  * @(#)InternationalFormatter.java 1.17 04/05/12
3  *
4  * Copyright 2004 Sun Microsystems, Inc. All rights reserved.
5  * SUN PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
6  */

7 package javax.swing.text;
8
9 import java.awt.event.ActionEvent JavaDoc;
10 import java.io.*;
11 import java.text.*;
12 import java.util.*;
13 import javax.swing.*;
14 import javax.swing.text.*;
15
16 /**
17  * <code>InternationalFormatter</code> extends <code>DefaultFormatter</code>,
18  * using an instance of <code>java.text.Format</code> to handle the
19  * conversion to a String, and the conversion from a String.
20  * <p>
21  * If <code>getAllowsInvalid()</code> is false, this will ask the
22  * <code>Format</code> to format the current text on every edit.
23  * <p>
24  * You can specify a minimum and maximum value by way of the
25  * <code>setMinimum</code> and <code>setMaximum</code> methods. In order
26  * for this to work the values returned from <code>stringToValue</code> must be
27  * comparable to the min/max values by way of the <code>Comparable</code>
28  * interface.
29  * <p>
30  * Be careful how you configure the <code>Format</code> and the
31  * <code>InternationalFormatter</code>, as it is possible to create a
32  * situation where certain values can not be input. Consider the date
33  * format 'M/d/yy', an <code>InternationalFormatter</code> that is always
34  * valid (<code>setAllowsInvalid(false)</code>), is in overwrite mode
35  * (<code>setOverwriteMode(true)</code>) and the date 7/1/99. In this
36  * case the user will not be able to enter a two digit month or day of
37  * month. To avoid this, the format should be 'MM/dd/yy'.
38  * <p>
39  * If <code>InternationalFormatter</code> is configured to only allow valid
40  * values (<code>setAllowsInvalid(false)</code>), every valid edit will result
41  * in the text of the <code>JFormattedTextField</code> being completely reset
42  * from the <code>Format</code>.
43  * The cursor position will also be adjusted as literal characters are
44  * added/removed from the resulting String.
45  * <p>
46  * <code>InternationalFormatter</code>'s behavior of
47  * <code>stringToValue</code> is slightly different than that of
48  * <code>DefaultTextFormatter</code>, it does the following:
49  * <ol>
50  * <li><code>parseObject</code> is invoked on the <code>Format</code>
51  * specified by <code>setFormat</code>
52  * <li>If a Class has been set for the values (<code>setValueClass</code>),
53  * supers implementation is invoked to convert the value returned
54  * from <code>parseObject</code> to the appropriate class.
55  * <li>If a <code>ParseException</code> has not been thrown, and the value
56  * is outside the min/max a <code>ParseException</code> is thrown.
57  * <li>The value is returned.
58  * </ol>
59  * <code>InternationalFormatter</code> implements <code>stringToValue</code>
60  * in this manner so that you can specify an alternate Class than
61  * <code>Format</code> may return.
62  * <p>
63  * <strong>Warning:</strong>
64  * Serialized objects of this class will not be compatible with
65  * future Swing releases. The current serialization support is
66  * appropriate for short term storage or RMI between applications running
67  * the same version of Swing. As of 1.4, support for long term storage
68  * of all JavaBeans<sup><font size="-2">TM</font></sup>
69  * has been added to the <code>java.beans</code> package.
70  * Please see {@link java.beans.XMLEncoder}.
71  *
72  * @see java.text.Format
73  * @see java.lang.Comparable
74  *
75  * @version 1.7 04/09/01
76  * @since 1.4
77  */

78 public class InternationalFormatter extends DefaultFormatter JavaDoc {
79     /**
80      * Used by <code>getFields</code>.
81      */

82     private static final Format.Field[] EMPTY_FIELD_ARRAY =new Format.Field[0];
83
84     /**
85      * Object used to handle the conversion.
86      */

87     private Format format;
88     /**
89      * Can be used to impose a maximum value.
90      */

91     private Comparable JavaDoc max;
92     /**
93      * Can be used to impose a minimum value.
94      */

95     private Comparable JavaDoc min;
96
97     /**
98      * <code>InternationalFormatter</code>'s behavior is dicatated by a
99      * <code>AttributedCharacterIterator</code> that is obtained from
100      * the <code>Format</code>. On every edit, assuming
101      * allows invalid is false, the <code>Format</code> instance is invoked
102      * with <code>formatToCharacterIterator</code>. A <code>BitSet</code> is
103      * also kept upto date with the non-literal characters, that is
104      * for every index in the <code>AttributedCharacterIterator</code> an
105      * entry in the bit set is updated based on the return value from
106      * <code>isLiteral(Map)</code>. <code>isLiteral(int)</code> then uses
107      * this cached information.
108      * <p>
109      * If allowsInvalid is false, every edit results in resetting the complete
110      * text of the JTextComponent.
111      * <p>
112      * InternationalFormatterFilter can also provide two actions suitable for
113      * incrementing and decrementing. To enable this a subclass must
114      * override <code>getSupportsIncrement</code> to return true, and
115      * override <code>adjustValue</code> to handle the changing of the
116      * value. If you want to support changing the value outside of
117      * the valid FieldPositions, you will need to override
118      * <code>canIncrement</code>.
119      */

120     /**
121      * A bit is set for every index identified in the
122      * AttributedCharacterIterator that is not considered decoration.
123      * This should only be used if validMask is true.
124      */

125     private transient BitSet literalMask;
126     /**
127      * Used to iterate over characters.
128      */

129     private transient AttributedCharacterIterator iterator;
130     /**
131      * True if the Format was able to convert the value to a String and
132      * back.
133      */

134     private transient boolean validMask;
135     /**
136      * Current value being displayed.
137      */

138     private transient String JavaDoc string;
139     /**
140      * If true, DocumentFilter methods are unconditionally allowed,
141      * and no checking is done on their values. This is used when
142      * incrementing/decrementing via the actions.
143      */

144     private transient boolean ignoreDocumentMutate;
145
146
147     /**
148      * Creates an <code>InternationalFormatter</code> with no
149      * <code>Format</code> specified.
150      */

151     public InternationalFormatter() {
152         setOverwriteMode(false);
153     }
154
155     /**
156      * Creates an <code>InternationalFormatter</code> with the specified
157      * <code>Format</code> instance.
158      *
159      * @param format Format instance used for converting from/to Strings
160      */

161     public InternationalFormatter(Format format) {
162         this();
163         setFormat(format);
164     }
165
166     /**
167      * Sets the format that dictates the legal values that can be edited
168      * and displayed.
169      *
170      * @param format <code>Format</code> instance used for converting
171      * from/to Strings
172      */

173     public void setFormat(Format format) {
174         this.format = format;
175     }
176
177     /**
178      * Returns the format that dictates the legal values that can be edited
179      * and displayed.
180      *
181      * @return Format instance used for converting from/to Strings
182      */

183     public Format getFormat() {
184         return format;
185     }
186
187     /**
188      * Sets the minimum permissible value. If the <code>valueClass</code> has
189      * not been specified, and <code>minimum</code> is non null, the
190      * <code>valueClass</code> will be set to that of the class of
191      * <code>minimum</code>.
192      *
193      * @param minimum Minimum legal value that can be input
194      * @see #setValueClass
195      */

196     public void setMinimum(Comparable JavaDoc minimum) {
197         if (getValueClass() == null && minimum != null) {
198             setValueClass(minimum.getClass());
199         }
200         min = minimum;
201     }
202
203     /**
204      * Returns the minimum permissible value.
205      *
206      * @return Minimum legal value that can be input
207      */

208     public Comparable JavaDoc getMinimum() {
209         return min;
210     }
211
212     /**
213      * Sets the maximum permissible value. If the <code>valueClass</code> has
214      * not been specified, and <code>max</code> is non null, the
215      * <code>valueClass</code> will be set to that of the class of
216      * <code>max</code>.
217      *
218      * @param max Maximum legal value that can be input
219      * @see #setValueClass
220      */

221     public void setMaximum(Comparable JavaDoc max) {
222         if (getValueClass() == null && max != null) {
223             setValueClass(max.getClass());
224         }
225         this.max = max;
226     }
227
228     /**
229      * Returns the maximum permissible value.
230      *
231      * @return Maximum legal value that can be input
232      */

233     public Comparable JavaDoc getMaximum() {
234         return max;
235     }
236
237     /**
238      * Installs the <code>DefaultFormatter</code> onto a particular
239      * <code>JFormattedTextField</code>.
240      * This will invoke <code>valueToString</code> to convert the
241      * current value from the <code>JFormattedTextField</code> to
242      * a String. This will then install the <code>Action</code>s from
243      * <code>getActions</code>, the <code>DocumentFilter</code>
244      * returned from <code>getDocumentFilter</code> and the
245      * <code>NavigationFilter</code> returned from
246      * <code>getNavigationFilter</code> onto the
247      * <code>JFormattedTextField</code>.
248      * <p>
249      * Subclasses will typically only need to override this if they
250      * wish to install additional listeners on the
251      * <code>JFormattedTextField</code>.
252      * <p>
253      * If there is a <code>ParseException</code> in converting the
254      * current value to a String, this will set the text to an empty
255      * String, and mark the <code>JFormattedTextField</code> as being
256      * in an invalid state.
257      * <p>
258      * While this is a public method, this is typically only useful
259      * for subclassers of <code>JFormattedTextField</code>.
260      * <code>JFormattedTextField</code> will invoke this method at
261      * the appropriate times when the value changes, or its internal
262      * state changes.
263      *
264      * @param ftf JFormattedTextField to format for, may be null indicating
265      * uninstall from current JFormattedTextField.
266      */

267     public void install(JFormattedTextField ftf) {
268         super.install(ftf);
269         updateMaskIfNecessary();
270         // invoked again as the mask should now be valid.
271
positionCursorAtInitialLocation();
272     }
273
274     /**
275      * Returns a String representation of the Object <code>value</code>.
276      * This invokes <code>format</code> on the current <code>Format</code>.
277      *
278      * @throws ParseException if there is an error in the conversion
279      * @param value Value to convert
280      * @return String representation of value
281      */

282     public String JavaDoc valueToString(Object JavaDoc value) throws ParseException {
283         if (value == null) {
284             return "";
285         }
286         Format f = getFormat();
287
288         if (f == null) {
289             return value.toString();
290         }
291         return f.format(value);
292     }
293
294     /**
295      * Returns the <code>Object</code> representation of the
296      * <code>String</code> <code>text</code>.
297      *
298      * @param text <code>String</code> to convert
299      * @return <code>Object</code> representation of text
300      * @throws ParseException if there is an error in the conversion
301      */

302     public Object JavaDoc stringToValue(String JavaDoc text) throws ParseException {
303         Object JavaDoc value = stringToValue(text, getFormat());
304
305         // Convert to the value class if the Value returned from the
306
// Format does not match.
307
if (value != null && getValueClass() != null &&
308                              !getValueClass().isInstance(value)) {
309             value = super.stringToValue(value.toString());
310         }
311         try {
312             if (!isValidValue(value, true)) {
313                 throw new ParseException("Value not within min/max range", 0);
314             }
315         } catch (ClassCastException JavaDoc cce) {
316             throw new ParseException("Class cast exception comparing values: "
317                                      + cce, 0);
318         }
319         return value;
320     }
321
322     /**
323      * Returns the <code>Format.Field</code> constants associated with
324      * the text at <code>offset</code>. If <code>offset</code> is not
325      * a valid location into the current text, this will return an
326      * empty array.
327      *
328      * @param offset offset into text to be examined
329      * @return Format.Field constants associated with the text at the
330      * given position.
331      */

332     public Format.Field[] getFields(int offset) {
333         if (getAllowsInvalid()) {
334             // This will work if the currently edited value is valid.
335
updateMask();
336         }
337
338         Map attrs = getAttributes(offset);
339
340         if (attrs != null && attrs.size() > 0) {
341             ArrayList al = new ArrayList();
342
343             al.addAll(attrs.keySet());
344             return (Format.Field[])al.toArray(EMPTY_FIELD_ARRAY);
345         }
346         return EMPTY_FIELD_ARRAY;
347     }
348
349     /**
350      * Creates a copy of the DefaultFormatter.
351      *
352      * @return copy of the DefaultFormatter
353      */

354     public Object JavaDoc clone() throws CloneNotSupportedException JavaDoc {
355         InternationalFormatter JavaDoc formatter = (InternationalFormatter JavaDoc)super.
356                                            clone();
357
358         formatter.literalMask = null;
359         formatter.iterator = null;
360         formatter.validMask = false;
361         formatter.string = null;
362         return formatter;
363     }
364
365     /**
366      * If <code>getSupportsIncrement</code> returns true, this returns
367      * two Actions suitable for incrementing/decrementing the value.
368      */

369     protected Action[] getActions() {
370         if (getSupportsIncrement()) {
371             return new Action[] { new IncrementAction("increment", 1),
372                                   new IncrementAction("decrement", -1) };
373         }
374         return null;
375     }
376
377     /**
378      * Invokes <code>parseObject</code> on <code>f</code>, returning
379      * its value.
380      */

381     Object JavaDoc stringToValue(String JavaDoc text, Format f) throws ParseException {
382         if (f == null) {
383             return text;
384         }
385         return f.parseObject(text);
386     }
387
388     /**
389      * Returns true if <code>value</code> is between the min/max.
390      *
391      * @param wantsCCE If false, and a ClassCastException is thrown in
392      * comparing the values, the exception is consumed and
393      * false is returned.
394      */

395     boolean isValidValue(Object JavaDoc value, boolean wantsCCE) {
396         Comparable JavaDoc min = getMinimum();
397
398         try {
399             if (min != null && min.compareTo(value) > 0) {
400                 return false;
401             }
402         } catch (ClassCastException JavaDoc cce) {
403             if (wantsCCE) {
404                 throw cce;
405             }
406             return false;
407         }
408
409         Comparable JavaDoc max = getMaximum();
410         try {
411             if (max != null && max.compareTo(value) < 0) {
412                 return false;
413             }
414         } catch (ClassCastException JavaDoc cce) {
415             if (wantsCCE) {
416                 throw cce;
417             }
418             return false;
419         }
420         return true;
421     }
422
423     /**
424      * Returns a Set of the attribute identifiers at <code>index</code>.
425      */

426     Map getAttributes(int index) {
427         if (isValidMask()) {
428             AttributedCharacterIterator iterator = getIterator();
429
430             if (index >= 0 && index <= iterator.getEndIndex()) {
431                 iterator.setIndex(index);
432                 return iterator.getAttributes();
433             }
434         }
435         return null;
436     }
437
438
439     /**
440      * Returns the start of the first run that contains the attribute
441      * <code>id</code>. This will return <code>-1</code> if the attribute
442      * can not be found.
443      */

444     int getAttributeStart(AttributedCharacterIterator.Attribute id) {
445         if (isValidMask()) {
446             AttributedCharacterIterator iterator = getIterator();
447
448             iterator.first();
449             while (iterator.current() != CharacterIterator.DONE) {
450                 if (iterator.getAttribute(id) != null) {
451                     return iterator.getIndex();
452                 }
453                 iterator.next();
454             }
455         }
456         return -1;
457     }
458
459     /**
460      * Returns the <code>AttributedCharacterIterator</code> used to
461      * format the last value.
462      */

463     AttributedCharacterIterator getIterator() {
464         return iterator;
465     }
466
467     /**
468      * Updates the AttributedCharacterIterator and bitset, if necessary.
469      */

470     void updateMaskIfNecessary() {
471         if (!getAllowsInvalid() && (getFormat() != null)) {
472             if (!isValidMask()) {
473                 updateMask();
474             }
475             else {
476                 String JavaDoc newString = getFormattedTextField().getText();
477
478                 if (!newString.equals(string)) {
479                     updateMask();
480                 }
481             }
482         }
483     }
484
485     /**
486      * Updates the AttributedCharacterIterator by invoking
487      * <code>formatToCharacterIterator</code> on the <code>Format</code>.
488      * If this is successful,
489      * <code>updateMask(AttributedCharacterIterator)</code>
490      * is then invoked to update the internal bitmask.
491      */

492     void updateMask() {
493         if (getFormat() != null) {
494             Document JavaDoc doc = getFormattedTextField().getDocument();
495
496             validMask = false;
497             if (doc != null) {
498                 try {
499                     string = doc.getText(0, doc.getLength());
500                 } catch (BadLocationException JavaDoc ble) {
501                     string = null;
502                 }
503                 if (string != null) {
504                     try {
505                         Object JavaDoc value = stringToValue(string);
506                         AttributedCharacterIterator iterator = getFormat().
507                                   formatToCharacterIterator(value);
508
509                         updateMask(iterator);
510                     }
511                     catch (ParseException pe) {}
512                     catch (IllegalArgumentException JavaDoc iae) {}
513                     catch (NullPointerException JavaDoc npe) {}
514                 }
515             }
516         }
517     }
518
519     /**
520      * Returns the number of literal characters before <code>index</code>.
521      */

522     int getLiteralCountTo(int index) {
523         int lCount = 0;
524
525         for (int counter = 0; counter < index; counter++) {
526             if (isLiteral(counter)) {
527                 lCount++;
528             }
529         }
530         return lCount;
531     }
532
533     /**
534      * Returns true if the character at index is a literal, that is
535      * not editable.
536      */

537     boolean isLiteral(int index) {
538         if (isValidMask() && index < string.length()) {
539             return literalMask.get(index);
540         }
541         return false;
542     }
543
544     /**
545      * Returns the literal character at index.
546      */

547     char getLiteral(int index) {
548         if (isValidMask() && string != null && index < string.length()) {
549             return string.charAt(index);
550         }
551         return (char)0;
552     }
553
554     /**
555      * Returns true if the character at offset is navigatable too. This
556      * is implemented in terms of <code>isLiteral</code>, subclasses
557      * may wish to provide different behavior.
558      */

559     boolean isNavigatable(int offset) {
560         return !isLiteral(offset);
561     }
562
563     /**
564      * Overriden to update the mask after invoking supers implementation.
565      */

566     void updateValue(Object JavaDoc value) {
567         super.updateValue(value);
568         updateMaskIfNecessary();
569     }
570
571     /**
572      * Overriden to unconditionally allow the replace if
573      * ignoreDocumentMutate is true.
574      */

575     void replace(DocumentFilter.FilterBypass JavaDoc fb, int offset,
576                      int length, String JavaDoc text,
577                      AttributeSet JavaDoc attrs) throws BadLocationException JavaDoc {
578         if (ignoreDocumentMutate) {
579             fb.replace(offset, length, text, attrs);
580             return;
581         }
582         super.replace(fb, offset, length, text, attrs);
583     }
584
585     /**
586      * Returns the index of the next non-literal character starting at
587      * index. If index is not a literal, it will be returned.
588      *
589      * @param direction Amount to increment looking for non-literal
590      */

591     private int getNextNonliteralIndex(int index, int direction) {
592         int max = getFormattedTextField().getDocument().getLength();
593
594         while (index >= 0 && index < max) {
595             if (isLiteral(index)) {
596                 index += direction;
597             }
598             else {
599                 return index;
600             }
601         }
602         return (direction == -1) ? 0 : max;
603     }
604
605     /**
606      * Overriden in an attempt to honor the literals.
607      * <p>
608      * If we do
609      * not allow invalid values and are in overwrite mode, this does the
610      * following for each character in the replacement range:
611      * <ol>
612      * <li>If the character is a literal, add it to the string to replace
613      * with. If there is text to insert and it doesn't match the
614      * literal, then insert the literal in the the middle of the insert
615      * text. This allows you to either paste in literals or not and
616      * get the same behavior.
617      * <li>If there is no text to insert, replace it with ' '.
618      * </ol>
619      * If not in overwrite mode, and there is text to insert it is
620      * inserted at the next non literal index going forward. If there
621      * is only text to remove, it is removed from the next non literal
622      * index going backward.
623      */

624     boolean canReplace(ReplaceHolder rh) {
625         if (!getAllowsInvalid()) {
626             String JavaDoc text = rh.text;
627             int tl = (text != null) ? text.length() : 0;
628
629             if (tl == 0 && rh.length == 1 && getFormattedTextField().
630                               getSelectionStart() != rh.offset) {
631                 // Backspace, adjust to actually delete next non-literal.
632
rh.offset = getNextNonliteralIndex(rh.offset, -1);
633             }
634             if (getOverwriteMode()) {
635                 StringBuffer JavaDoc replace = null;
636
637                 for (int counter = 0, textIndex = 0,
638                          max = Math.max(tl, rh.length); counter < max;
639                          counter++) {
640                     if (isLiteral(rh.offset + counter)) {
641                         if (replace != null) {
642                             replace.append(getLiteral(rh.offset +
643                                                       counter));
644                         }
645                         if (textIndex < tl && text.charAt(textIndex) ==
646                                       getLiteral(rh.offset + counter)) {
647                             textIndex++;
648                         }
649                         else if (textIndex == 0) {
650                             rh.offset++;
651                             rh.length--;
652                             counter--;
653                             max--;
654                         }
655                         else if (replace == null) {
656                             replace = new StringBuffer JavaDoc(max);
657                             replace.append(text.substring(0, textIndex));
658                             replace.append(getLiteral(rh.offset +
659                                                       counter));
660                         }
661                     }
662                     else if (textIndex < tl) {
663                         if (replace != null) {
664                             replace.append(text.charAt(textIndex));
665                         }
666                         textIndex++;
667                     }
668                     else {
669                         // Nothing to replace it with, assume ' '
670
if (replace == null) {
671                             replace = new StringBuffer JavaDoc(max);
672                             if (textIndex > 0) {
673                                 replace.append(text.substring(0, textIndex));
674                             }
675                         }
676                         if (replace != null) {
677                             replace.append(' ');
678                         }
679                     }
680                 }
681                 if (replace != null) {
682                     rh.text = replace.toString();
683                 }
684             }
685             else if (tl > 0) {
686                 // insert (or insert and remove)
687
rh.offset = getNextNonliteralIndex(rh.offset, 1);
688             }
689             else {
690                 // remove only
691
rh.offset = getNextNonliteralIndex(rh.offset, -1);
692             }
693             ((ExtendedReplaceHolder)rh).endOffset = rh.offset;
694             ((ExtendedReplaceHolder)rh).endTextLength = (rh.text != null) ?
695                                                     rh.text.length() : 0;
696         }
697         else {
698             ((ExtendedReplaceHolder)rh).endOffset = rh.offset;
699             ((ExtendedReplaceHolder)rh).endTextLength = (rh.text != null) ?
700                                                     rh.text.length() : 0;
701         }
702         boolean can = super.canReplace(rh);
703         if (can && !getAllowsInvalid()) {
704             ((ExtendedReplaceHolder)rh).resetFromValue(this);
705         }
706         return can;
707     }
708
709     /**
710      * When in !allowsInvalid mode the text is reset on every edit, thus
711      * supers implementation will position the cursor at the wrong position.
712      * As such, this invokes supers implementation and then invokes
713      * <code>repositionCursor</code> to correctly reset the cursor.
714      */

715     boolean replace(ReplaceHolder rh) throws BadLocationException JavaDoc {
716         int start = -1;
717         int direction = 1;
718         int literalCount = -1;
719
720         if (rh.length > 0 && (rh.text == null || rh.text.length() == 0) &&
721                (getFormattedTextField().getSelectionStart() != rh.offset ||
722                    rh.length > 1)) {
723             direction = -1;
724         }
725         if (!getAllowsInvalid()) {
726             if ((rh.text == null || rh.text.length() == 0) && rh.length > 0) {
727                 // remove
728
start = getFormattedTextField().getSelectionStart();
729             }
730             else {
731                 start = rh.offset;
732             }
733             literalCount = getLiteralCountTo(start);
734         }
735         if (super.replace(rh)) {
736             if (start != -1) {
737                 int end = ((ExtendedReplaceHolder)rh).endOffset;
738
739                 end += ((ExtendedReplaceHolder)rh).endTextLength;
740                 repositionCursor(literalCount, end, direction);
741             }
742             else {
743                 start = ((ExtendedReplaceHolder)rh).endOffset;
744                 if (direction == 1) {
745                     start += ((ExtendedReplaceHolder)rh).endTextLength;
746                 }
747                 repositionCursor(start, direction);
748             }
749             return true;
750         }
751         return false;
752     }
753
754     /**
755      * Repositions the cursor. <code>startLiteralCount</code> gives
756      * the number of literals to the start of the deleted range, end
757      * gives the ending location to adjust from, direction gives
758      * the direction relative to <code>end</code> to position the
759      * cursor from.
760      */

761     private void repositionCursor(int startLiteralCount, int end,
762                                   int direction) {
763         int endLiteralCount = getLiteralCountTo(end);
764
765         if (endLiteralCount != end) {
766             end -= startLiteralCount;
767             for (int counter = 0; counter < end; counter++) {
768                 if (isLiteral(counter)) {
769                     end++;
770                 }
771             }
772         }
773         repositionCursor(end, 1 /*direction*/);
774     }
775
776     /**
777      * Returns the character from the mask that has been buffered
778      * at <code>index</code>.
779      */

780     char getBufferedChar(int index) {
781         if (isValidMask()) {
782             if (string != null && index < string.length()) {
783                 return string.charAt(index);
784             }
785         }
786         return (char)0;
787     }
788
789     /**
790      * Returns true if the current mask is valid.
791      */

792     boolean isValidMask() {
793         return validMask;
794     }
795
796     /**
797      * Returns true if <code>attributes</code> is null or empty.
798      */

799     boolean isLiteral(Map attributes) {
800         return ((attributes == null) || attributes.size() == 0);
801     }
802
803     /**
804      * Updates the interal bitset from <code>iterator</code>. This will
805      * set <code>validMask</code> to true if <code>iterator</code> is
806      * non-null.
807      */

808     private void updateMask(AttributedCharacterIterator iterator) {
809         if (iterator != null) {
810             validMask = true;
811             this.iterator = iterator;
812
813             // Update the literal mask
814
if (literalMask == null) {
815                 literalMask = new BitSet();
816             }
817             else {
818                 for (int counter = literalMask.length() - 1; counter >= 0;
819                      counter--) {
820                     literalMask.clear(counter);
821                 }
822             }
823
824             iterator.first();
825             while (iterator.current() != CharacterIterator.DONE) {
826                 Map attributes = iterator.getAttributes();
827                 boolean set = isLiteral(attributes);
828                 int start = iterator.getIndex();
829                 int end = iterator.getRunLimit();
830
831                 while (start < end) {
832                     if (set) {
833                         literalMask.set(start);
834                     }
835                     else {
836                         literalMask.clear(start);
837                     }
838                     start++;
839                 }
840                 iterator.setIndex(start);
841             }
842         }
843     }
844
845     /**
846      * Returns true if <code>field</code> is non-null.
847      * Subclasses that wish to allow incrementing to happen outside of
848      * the known fields will need to override this.
849      */

850     boolean canIncrement(Object JavaDoc field, int cursorPosition) {
851         return (field != null);
852     }
853
854     /**
855      * Selects the fields identified by <code>attributes</code>.
856      */

857     void selectField(Object JavaDoc f, int count) {
858         AttributedCharacterIterator iterator = getIterator();
859
860         if (iterator != null &&
861                         (f instanceof AttributedCharacterIterator.Attribute)) {
862             AttributedCharacterIterator.Attribute field =
863                                    (AttributedCharacterIterator.Attribute)f;
864
865             iterator.first();
866             while (iterator.current() != CharacterIterator.DONE) {
867                 while (iterator.getAttribute(field) == null &&
868                        iterator.next() != CharacterIterator.DONE);
869                 if (iterator.current() != CharacterIterator.DONE) {
870                     int limit = iterator.getRunLimit(field);
871
872                     if (--count <= 0) {
873                         getFormattedTextField().select(iterator.getIndex(),
874                                                        limit);
875                         break;
876                     }
877                     iterator.setIndex(limit);
878                     iterator.next();
879                 }
880             }
881         }
882     }
883
884     /**
885      * Returns the field that will be adjusted by adjustValue.
886      */

887     Object JavaDoc getAdjustField(int start, Map attributes) {
888         return null;
889     }
890
891     /**
892      * Returns the number of occurences of <code>f</code> before
893      * the location <code>start</code> in the current
894      * <code>AttributedCharacterIterator</code>.
895      */

896     private int getFieldTypeCountTo(Object JavaDoc f, int start) {
897         AttributedCharacterIterator iterator = getIterator();
898         int count = 0;
899
900         if (iterator != null &&
901                     (f instanceof AttributedCharacterIterator.Attribute)) {
902             AttributedCharacterIterator.Attribute field =
903                                    (AttributedCharacterIterator.Attribute)f;
904             int index = 0;
905
906             iterator.first();
907             while (iterator.getIndex() < start) {
908                 while (iterator.getAttribute(field) == null &&
909                        iterator.next() != CharacterIterator.DONE);
910                 if (iterator.current() != CharacterIterator.DONE) {
911                     iterator.setIndex(iterator.getRunLimit(field));
912                     iterator.next();
913                     count++;
914                 }
915                 else {
916                     break;
917                 }
918             }
919         }
920         return count;
921     }
922
923     /**
924      * Subclasses supporting incrementing must override this to handle
925      * the actual incrementing. <code>value</code> is the current value,
926      * <code>attributes</code> gives the field the cursor is in (may be
927      * null depending upon <code>canIncrement</code>) and
928      * <code>direction</code> is the amount to increment by.
929      */

930     Object JavaDoc adjustValue(Object JavaDoc value, Map attributes, Object JavaDoc field,
931                            int direction) throws
932                       BadLocationException JavaDoc, ParseException {
933         return null;
934     }
935
936     /**
937      * Returns false, indicating InternationalFormatter does not allow
938      * incrementing of the value. Subclasses that wish to support
939      * incrementing/decrementing the value should override this and
940      * return true. Subclasses should also override
941      * <code>adjustValue</code>.
942      */

943     boolean getSupportsIncrement() {
944         return false;
945     }
946
947     /**
948      * Resets the value of the JFormattedTextField to be
949      * <code>value</code>.
950      */

951     void resetValue(Object JavaDoc value) throws BadLocationException JavaDoc, ParseException {
952         Document JavaDoc doc = getFormattedTextField().getDocument();
953         String JavaDoc string = valueToString(value);
954
955         try {
956             ignoreDocumentMutate = true;
957             doc.remove(0, doc.getLength());
958             doc.insertString(0, string, null);
959         } finally {
960             ignoreDocumentMutate = false;
961         }
962         updateValue(value);
963     }
964
965     /**
966      * Subclassed to update the internal representation of the mask after
967      * the default read operation has completed.
968      */

969     private void readObject(ObjectInputStream s)
970         throws IOException, ClassNotFoundException JavaDoc {
971         s.defaultReadObject();
972         updateMaskIfNecessary();
973     }
974
975
976     /**
977      * Overriden to return an instance of <code>ExtendedReplaceHolder</code>.
978      */

979     ReplaceHolder getReplaceHolder(DocumentFilter.FilterBypass JavaDoc fb, int offset,
980                                    int length, String JavaDoc text,
981                                    AttributeSet JavaDoc attrs) {
982         if (replaceHolder == null) {
983             replaceHolder = new ExtendedReplaceHolder();
984         }
985         return super.getReplaceHolder(fb, offset, length, text, attrs);
986     }
987
988
989     /**
990      * As InternationalFormatter replaces the complete text on every edit,
991      * ExtendedReplaceHolder keeps track of the offset and length passed
992      * into canReplace.
993      */

994     static class ExtendedReplaceHolder extends ReplaceHolder {
995         /** Offset of the insert/remove. This may differ from offset in
996          * that if !allowsInvalid the text is replaced on every edit. */

997         int endOffset;
998         /** Length of the text. This may differ from text.length in
999          * that if !allowsInvalid the text is replaced on every edit. */

1000        int endTextLength;
1001
1002        /**
1003         * Resets the region to delete to be the complete document and
1004         * the text from invoking valueToString on the current value.
1005         */

1006        void resetFromValue(InternationalFormatter JavaDoc formatter) {
1007            // Need to reset the complete string as Format's result can
1008
// be completely different.
1009
offset = 0;
1010            try {
1011                text = formatter.valueToString(value);
1012            } catch (ParseException pe) {
1013                // Should never happen, otherwise canReplace would have
1014
// returned value.
1015
text = "";
1016            }
1017            length = fb.getDocument().getLength();
1018        }
1019    }
1020
1021
1022    /**
1023     * IncrementAction is used to increment the value by a certain amount.
1024     * It calls into <code>adjustValue</code> to handle the actual
1025     * incrementing of the value.
1026     */

1027    private class IncrementAction extends AbstractAction {
1028        private int direction;
1029
1030        IncrementAction(String JavaDoc name, int direction) {
1031            super(name);
1032            this.direction = direction;
1033        }
1034
1035        public void actionPerformed(ActionEvent JavaDoc ae) {
1036            if (getAllowsInvalid()) {
1037                // This will work if the currently edited value is valid.
1038
updateMask();
1039            }
1040
1041            boolean validEdit = false;
1042
1043            if (isValidMask()) {
1044                int start = getFormattedTextField().getSelectionStart();
1045
1046                if (start != -1) {
1047                    AttributedCharacterIterator iterator = getIterator();
1048
1049                    iterator.setIndex(start);
1050
1051                    Map attributes = iterator.getAttributes();
1052                    Object JavaDoc field = getAdjustField(start, attributes);
1053
1054                    if (canIncrement(field, start)) {
1055                        try {
1056                            Object JavaDoc value = stringToValue(
1057                                         getFormattedTextField().getText());
1058                            int fieldTypeCount = getFieldTypeCountTo(
1059                                                        field, start);
1060
1061                            value = adjustValue(value, attributes,
1062                                                field, direction);
1063                            if (value != null && isValidValue(value, false)) {
1064                                resetValue(value);
1065                                updateMask();
1066
1067                                if (isValidMask()) {
1068                                    selectField(field, fieldTypeCount);
1069                                }
1070                                validEdit = true;
1071                            }
1072                        }
1073                        catch (ParseException pe) { }
1074                        catch (BadLocationException JavaDoc ble) { }
1075                    }
1076                }
1077            }
1078            if (!validEdit) {
1079                invalidEdit();
1080            }
1081        }
1082    }
1083}
1084
Popular Tags