blob: d4f87907d37a6a4d6ad92d4f3b236d84c133cb13 [file] [log] [blame]
J. Duke319a3b92007-12-01 00:00:00 +00001/*
2 * Copyright 2000-2006 Sun Microsystems, Inc. All Rights Reserved.
3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4 *
5 * This code is free software; you can redistribute it and/or modify it
6 * under the terms of the GNU General Public License version 2 only, as
7 * published by the Free Software Foundation. Sun designates this
8 * particular file as subject to the "Classpath" exception as provided
9 * by Sun in the LICENSE file that accompanied this code.
10 *
11 * This code is distributed in the hope that it will be useful, but WITHOUT
12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
14 * version 2 for more details (a copy is included in the LICENSE file that
15 * accompanied this code).
16 *
17 * You should have received a copy of the GNU General Public License version
18 * 2 along with this work; if not, write to the Free Software Foundation,
19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
20 *
21 * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa Clara,
22 * CA 95054 USA or visit www.sun.com if you need additional information or
23 * have any questions.
24 */
25package javax.swing.text;
26
27import java.awt.event.ActionEvent;
28import java.io.*;
29import java.text.*;
30import java.util.*;
31import javax.swing.*;
32import javax.swing.text.*;
33
34/**
35 * <code>InternationalFormatter</code> extends <code>DefaultFormatter</code>,
36 * using an instance of <code>java.text.Format</code> to handle the
37 * conversion to a String, and the conversion from a String.
38 * <p>
39 * If <code>getAllowsInvalid()</code> is false, this will ask the
40 * <code>Format</code> to format the current text on every edit.
41 * <p>
42 * You can specify a minimum and maximum value by way of the
43 * <code>setMinimum</code> and <code>setMaximum</code> methods. In order
44 * for this to work the values returned from <code>stringToValue</code> must be
45 * comparable to the min/max values by way of the <code>Comparable</code>
46 * interface.
47 * <p>
48 * Be careful how you configure the <code>Format</code> and the
49 * <code>InternationalFormatter</code>, as it is possible to create a
50 * situation where certain values can not be input. Consider the date
51 * format 'M/d/yy', an <code>InternationalFormatter</code> that is always
52 * valid (<code>setAllowsInvalid(false)</code>), is in overwrite mode
53 * (<code>setOverwriteMode(true)</code>) and the date 7/1/99. In this
54 * case the user will not be able to enter a two digit month or day of
55 * month. To avoid this, the format should be 'MM/dd/yy'.
56 * <p>
57 * If <code>InternationalFormatter</code> is configured to only allow valid
58 * values (<code>setAllowsInvalid(false)</code>), every valid edit will result
59 * in the text of the <code>JFormattedTextField</code> being completely reset
60 * from the <code>Format</code>.
61 * The cursor position will also be adjusted as literal characters are
62 * added/removed from the resulting String.
63 * <p>
64 * <code>InternationalFormatter</code>'s behavior of
65 * <code>stringToValue</code> is slightly different than that of
66 * <code>DefaultTextFormatter</code>, it does the following:
67 * <ol>
68 * <li><code>parseObject</code> is invoked on the <code>Format</code>
69 * specified by <code>setFormat</code>
70 * <li>If a Class has been set for the values (<code>setValueClass</code>),
71 * supers implementation is invoked to convert the value returned
72 * from <code>parseObject</code> to the appropriate class.
73 * <li>If a <code>ParseException</code> has not been thrown, and the value
74 * is outside the min/max a <code>ParseException</code> is thrown.
75 * <li>The value is returned.
76 * </ol>
77 * <code>InternationalFormatter</code> implements <code>stringToValue</code>
78 * in this manner so that you can specify an alternate Class than
79 * <code>Format</code> may return.
80 * <p>
81 * <strong>Warning:</strong>
82 * Serialized objects of this class will not be compatible with
83 * future Swing releases. The current serialization support is
84 * appropriate for short term storage or RMI between applications running
85 * the same version of Swing. As of 1.4, support for long term storage
86 * of all JavaBeans<sup><font size="-2">TM</font></sup>
87 * has been added to the <code>java.beans</code> package.
88 * Please see {@link java.beans.XMLEncoder}.
89 *
90 * @see java.text.Format
91 * @see java.lang.Comparable
92 *
93 * @since 1.4
94 */
95public class InternationalFormatter extends DefaultFormatter {
96 /**
97 * Used by <code>getFields</code>.
98 */
99 private static final Format.Field[] EMPTY_FIELD_ARRAY =new Format.Field[0];
100
101 /**
102 * Object used to handle the conversion.
103 */
104 private Format format;
105 /**
106 * Can be used to impose a maximum value.
107 */
108 private Comparable max;
109 /**
110 * Can be used to impose a minimum value.
111 */
112 private Comparable min;
113
114 /**
115 * <code>InternationalFormatter</code>'s behavior is dicatated by a
116 * <code>AttributedCharacterIterator</code> that is obtained from
117 * the <code>Format</code>. On every edit, assuming
118 * allows invalid is false, the <code>Format</code> instance is invoked
119 * with <code>formatToCharacterIterator</code>. A <code>BitSet</code> is
120 * also kept upto date with the non-literal characters, that is
121 * for every index in the <code>AttributedCharacterIterator</code> an
122 * entry in the bit set is updated based on the return value from
123 * <code>isLiteral(Map)</code>. <code>isLiteral(int)</code> then uses
124 * this cached information.
125 * <p>
126 * If allowsInvalid is false, every edit results in resetting the complete
127 * text of the JTextComponent.
128 * <p>
129 * InternationalFormatterFilter can also provide two actions suitable for
130 * incrementing and decrementing. To enable this a subclass must
131 * override <code>getSupportsIncrement</code> to return true, and
132 * override <code>adjustValue</code> to handle the changing of the
133 * value. If you want to support changing the value outside of
134 * the valid FieldPositions, you will need to override
135 * <code>canIncrement</code>.
136 */
137 /**
138 * A bit is set for every index identified in the
139 * AttributedCharacterIterator that is not considered decoration.
140 * This should only be used if validMask is true.
141 */
142 private transient BitSet literalMask;
143 /**
144 * Used to iterate over characters.
145 */
146 private transient AttributedCharacterIterator iterator;
147 /**
148 * True if the Format was able to convert the value to a String and
149 * back.
150 */
151 private transient boolean validMask;
152 /**
153 * Current value being displayed.
154 */
155 private transient String string;
156 /**
157 * If true, DocumentFilter methods are unconditionally allowed,
158 * and no checking is done on their values. This is used when
159 * incrementing/decrementing via the actions.
160 */
161 private transient boolean ignoreDocumentMutate;
162
163
164 /**
165 * Creates an <code>InternationalFormatter</code> with no
166 * <code>Format</code> specified.
167 */
168 public InternationalFormatter() {
169 setOverwriteMode(false);
170 }
171
172 /**
173 * Creates an <code>InternationalFormatter</code> with the specified
174 * <code>Format</code> instance.
175 *
176 * @param format Format instance used for converting from/to Strings
177 */
178 public InternationalFormatter(Format format) {
179 this();
180 setFormat(format);
181 }
182
183 /**
184 * Sets the format that dictates the legal values that can be edited
185 * and displayed.
186 *
187 * @param format <code>Format</code> instance used for converting
188 * from/to Strings
189 */
190 public void setFormat(Format format) {
191 this.format = format;
192 }
193
194 /**
195 * Returns the format that dictates the legal values that can be edited
196 * and displayed.
197 *
198 * @return Format instance used for converting from/to Strings
199 */
200 public Format getFormat() {
201 return format;
202 }
203
204 /**
205 * Sets the minimum permissible value. If the <code>valueClass</code> has
206 * not been specified, and <code>minimum</code> is non null, the
207 * <code>valueClass</code> will be set to that of the class of
208 * <code>minimum</code>.
209 *
210 * @param minimum Minimum legal value that can be input
211 * @see #setValueClass
212 */
213 public void setMinimum(Comparable minimum) {
214 if (getValueClass() == null && minimum != null) {
215 setValueClass(minimum.getClass());
216 }
217 min = minimum;
218 }
219
220 /**
221 * Returns the minimum permissible value.
222 *
223 * @return Minimum legal value that can be input
224 */
225 public Comparable getMinimum() {
226 return min;
227 }
228
229 /**
230 * Sets the maximum permissible value. If the <code>valueClass</code> has
231 * not been specified, and <code>max</code> is non null, the
232 * <code>valueClass</code> will be set to that of the class of
233 * <code>max</code>.
234 *
235 * @param max Maximum legal value that can be input
236 * @see #setValueClass
237 */
238 public void setMaximum(Comparable max) {
239 if (getValueClass() == null && max != null) {
240 setValueClass(max.getClass());
241 }
242 this.max = max;
243 }
244
245 /**
246 * Returns the maximum permissible value.
247 *
248 * @return Maximum legal value that can be input
249 */
250 public Comparable getMaximum() {
251 return max;
252 }
253
254 /**
255 * Installs the <code>DefaultFormatter</code> onto a particular
256 * <code>JFormattedTextField</code>.
257 * This will invoke <code>valueToString</code> to convert the
258 * current value from the <code>JFormattedTextField</code> to
259 * a String. This will then install the <code>Action</code>s from
260 * <code>getActions</code>, the <code>DocumentFilter</code>
261 * returned from <code>getDocumentFilter</code> and the
262 * <code>NavigationFilter</code> returned from
263 * <code>getNavigationFilter</code> onto the
264 * <code>JFormattedTextField</code>.
265 * <p>
266 * Subclasses will typically only need to override this if they
267 * wish to install additional listeners on the
268 * <code>JFormattedTextField</code>.
269 * <p>
270 * If there is a <code>ParseException</code> in converting the
271 * current value to a String, this will set the text to an empty
272 * String, and mark the <code>JFormattedTextField</code> as being
273 * in an invalid state.
274 * <p>
275 * While this is a public method, this is typically only useful
276 * for subclassers of <code>JFormattedTextField</code>.
277 * <code>JFormattedTextField</code> will invoke this method at
278 * the appropriate times when the value changes, or its internal
279 * state changes.
280 *
281 * @param ftf JFormattedTextField to format for, may be null indicating
282 * uninstall from current JFormattedTextField.
283 */
284 public void install(JFormattedTextField ftf) {
285 super.install(ftf);
286 updateMaskIfNecessary();
287 // invoked again as the mask should now be valid.
288 positionCursorAtInitialLocation();
289 }
290
291 /**
292 * Returns a String representation of the Object <code>value</code>.
293 * This invokes <code>format</code> on the current <code>Format</code>.
294 *
295 * @throws ParseException if there is an error in the conversion
296 * @param value Value to convert
297 * @return String representation of value
298 */
299 public String valueToString(Object value) throws ParseException {
300 if (value == null) {
301 return "";
302 }
303 Format f = getFormat();
304
305 if (f == null) {
306 return value.toString();
307 }
308 return f.format(value);
309 }
310
311 /**
312 * Returns the <code>Object</code> representation of the
313 * <code>String</code> <code>text</code>.
314 *
315 * @param text <code>String</code> to convert
316 * @return <code>Object</code> representation of text
317 * @throws ParseException if there is an error in the conversion
318 */
319 public Object stringToValue(String text) throws ParseException {
320 Object value = stringToValue(text, getFormat());
321
322 // Convert to the value class if the Value returned from the
323 // Format does not match.
324 if (value != null && getValueClass() != null &&
325 !getValueClass().isInstance(value)) {
326 value = super.stringToValue(value.toString());
327 }
328 try {
329 if (!isValidValue(value, true)) {
330 throw new ParseException("Value not within min/max range", 0);
331 }
332 } catch (ClassCastException cce) {
333 throw new ParseException("Class cast exception comparing values: "
334 + cce, 0);
335 }
336 return value;
337 }
338
339 /**
340 * Returns the <code>Format.Field</code> constants associated with
341 * the text at <code>offset</code>. If <code>offset</code> is not
342 * a valid location into the current text, this will return an
343 * empty array.
344 *
345 * @param offset offset into text to be examined
346 * @return Format.Field constants associated with the text at the
347 * given position.
348 */
349 public Format.Field[] getFields(int offset) {
350 if (getAllowsInvalid()) {
351 // This will work if the currently edited value is valid.
352 updateMask();
353 }
354
355 Map attrs = getAttributes(offset);
356
357 if (attrs != null && attrs.size() > 0) {
358 ArrayList al = new ArrayList();
359
360 al.addAll(attrs.keySet());
361 return (Format.Field[])al.toArray(EMPTY_FIELD_ARRAY);
362 }
363 return EMPTY_FIELD_ARRAY;
364 }
365
366 /**
367 * Creates a copy of the DefaultFormatter.
368 *
369 * @return copy of the DefaultFormatter
370 */
371 public Object clone() throws CloneNotSupportedException {
372 InternationalFormatter formatter = (InternationalFormatter)super.
373 clone();
374
375 formatter.literalMask = null;
376 formatter.iterator = null;
377 formatter.validMask = false;
378 formatter.string = null;
379 return formatter;
380 }
381
382 /**
383 * If <code>getSupportsIncrement</code> returns true, this returns
384 * two Actions suitable for incrementing/decrementing the value.
385 */
386 protected Action[] getActions() {
387 if (getSupportsIncrement()) {
388 return new Action[] { new IncrementAction("increment", 1),
389 new IncrementAction("decrement", -1) };
390 }
391 return null;
392 }
393
394 /**
395 * Invokes <code>parseObject</code> on <code>f</code>, returning
396 * its value.
397 */
398 Object stringToValue(String text, Format f) throws ParseException {
399 if (f == null) {
400 return text;
401 }
402 return f.parseObject(text);
403 }
404
405 /**
406 * Returns true if <code>value</code> is between the min/max.
407 *
408 * @param wantsCCE If false, and a ClassCastException is thrown in
409 * comparing the values, the exception is consumed and
410 * false is returned.
411 */
412 boolean isValidValue(Object value, boolean wantsCCE) {
413 Comparable min = getMinimum();
414
415 try {
416 if (min != null && min.compareTo(value) > 0) {
417 return false;
418 }
419 } catch (ClassCastException cce) {
420 if (wantsCCE) {
421 throw cce;
422 }
423 return false;
424 }
425
426 Comparable max = getMaximum();
427 try {
428 if (max != null && max.compareTo(value) < 0) {
429 return false;
430 }
431 } catch (ClassCastException cce) {
432 if (wantsCCE) {
433 throw cce;
434 }
435 return false;
436 }
437 return true;
438 }
439
440 /**
441 * Returns a Set of the attribute identifiers at <code>index</code>.
442 */
443 Map getAttributes(int index) {
444 if (isValidMask()) {
445 AttributedCharacterIterator iterator = getIterator();
446
447 if (index >= 0 && index <= iterator.getEndIndex()) {
448 iterator.setIndex(index);
449 return iterator.getAttributes();
450 }
451 }
452 return null;
453 }
454
455
456 /**
457 * Returns the start of the first run that contains the attribute
458 * <code>id</code>. This will return <code>-1</code> if the attribute
459 * can not be found.
460 */
461 int getAttributeStart(AttributedCharacterIterator.Attribute id) {
462 if (isValidMask()) {
463 AttributedCharacterIterator iterator = getIterator();
464
465 iterator.first();
466 while (iterator.current() != CharacterIterator.DONE) {
467 if (iterator.getAttribute(id) != null) {
468 return iterator.getIndex();
469 }
470 iterator.next();
471 }
472 }
473 return -1;
474 }
475
476 /**
477 * Returns the <code>AttributedCharacterIterator</code> used to
478 * format the last value.
479 */
480 AttributedCharacterIterator getIterator() {
481 return iterator;
482 }
483
484 /**
485 * Updates the AttributedCharacterIterator and bitset, if necessary.
486 */
487 void updateMaskIfNecessary() {
488 if (!getAllowsInvalid() && (getFormat() != null)) {
489 if (!isValidMask()) {
490 updateMask();
491 }
492 else {
493 String newString = getFormattedTextField().getText();
494
495 if (!newString.equals(string)) {
496 updateMask();
497 }
498 }
499 }
500 }
501
502 /**
503 * Updates the AttributedCharacterIterator by invoking
504 * <code>formatToCharacterIterator</code> on the <code>Format</code>.
505 * If this is successful,
506 * <code>updateMask(AttributedCharacterIterator)</code>
507 * is then invoked to update the internal bitmask.
508 */
509 void updateMask() {
510 if (getFormat() != null) {
511 Document doc = getFormattedTextField().getDocument();
512
513 validMask = false;
514 if (doc != null) {
515 try {
516 string = doc.getText(0, doc.getLength());
517 } catch (BadLocationException ble) {
518 string = null;
519 }
520 if (string != null) {
521 try {
522 Object value = stringToValue(string);
523 AttributedCharacterIterator iterator = getFormat().
524 formatToCharacterIterator(value);
525
526 updateMask(iterator);
527 }
528 catch (ParseException pe) {}
529 catch (IllegalArgumentException iae) {}
530 catch (NullPointerException npe) {}
531 }
532 }
533 }
534 }
535
536 /**
537 * Returns the number of literal characters before <code>index</code>.
538 */
539 int getLiteralCountTo(int index) {
540 int lCount = 0;
541
542 for (int counter = 0; counter < index; counter++) {
543 if (isLiteral(counter)) {
544 lCount++;
545 }
546 }
547 return lCount;
548 }
549
550 /**
551 * Returns true if the character at index is a literal, that is
552 * not editable.
553 */
554 boolean isLiteral(int index) {
555 if (isValidMask() && index < string.length()) {
556 return literalMask.get(index);
557 }
558 return false;
559 }
560
561 /**
562 * Returns the literal character at index.
563 */
564 char getLiteral(int index) {
565 if (isValidMask() && string != null && index < string.length()) {
566 return string.charAt(index);
567 }
568 return (char)0;
569 }
570
571 /**
572 * Returns true if the character at offset is navigatable too. This
573 * is implemented in terms of <code>isLiteral</code>, subclasses
574 * may wish to provide different behavior.
575 */
576 boolean isNavigatable(int offset) {
577 return !isLiteral(offset);
578 }
579
580 /**
581 * Overriden to update the mask after invoking supers implementation.
582 */
583 void updateValue(Object value) {
584 super.updateValue(value);
585 updateMaskIfNecessary();
586 }
587
588 /**
589 * Overriden to unconditionally allow the replace if
590 * ignoreDocumentMutate is true.
591 */
592 void replace(DocumentFilter.FilterBypass fb, int offset,
593 int length, String text,
594 AttributeSet attrs) throws BadLocationException {
595 if (ignoreDocumentMutate) {
596 fb.replace(offset, length, text, attrs);
597 return;
598 }
599 super.replace(fb, offset, length, text, attrs);
600 }
601
602 /**
603 * Returns the index of the next non-literal character starting at
604 * index. If index is not a literal, it will be returned.
605 *
606 * @param direction Amount to increment looking for non-literal
607 */
608 private int getNextNonliteralIndex(int index, int direction) {
609 int max = getFormattedTextField().getDocument().getLength();
610
611 while (index >= 0 && index < max) {
612 if (isLiteral(index)) {
613 index += direction;
614 }
615 else {
616 return index;
617 }
618 }
619 return (direction == -1) ? 0 : max;
620 }
621
622 /**
623 * Overriden in an attempt to honor the literals.
624 * <p>
625 * If we do
626 * not allow invalid values and are in overwrite mode, this does the
627 * following for each character in the replacement range:
628 * <ol>
629 * <li>If the character is a literal, add it to the string to replace
630 * with. If there is text to insert and it doesn't match the
631 * literal, then insert the literal in the the middle of the insert
632 * text. This allows you to either paste in literals or not and
633 * get the same behavior.
634 * <li>If there is no text to insert, replace it with ' '.
635 * </ol>
636 * If not in overwrite mode, and there is text to insert it is
637 * inserted at the next non literal index going forward. If there
638 * is only text to remove, it is removed from the next non literal
639 * index going backward.
640 */
641 boolean canReplace(ReplaceHolder rh) {
642 if (!getAllowsInvalid()) {
643 String text = rh.text;
644 int tl = (text != null) ? text.length() : 0;
645
646 if (tl == 0 && rh.length == 1 && getFormattedTextField().
647 getSelectionStart() != rh.offset) {
648 // Backspace, adjust to actually delete next non-literal.
649 rh.offset = getNextNonliteralIndex(rh.offset, -1);
650 }
651 if (getOverwriteMode()) {
652 StringBuffer replace = null;
653
654 for (int counter = 0, textIndex = 0,
655 max = Math.max(tl, rh.length); counter < max;
656 counter++) {
657 if (isLiteral(rh.offset + counter)) {
658 if (replace != null) {
659 replace.append(getLiteral(rh.offset +
660 counter));
661 }
662 if (textIndex < tl && text.charAt(textIndex) ==
663 getLiteral(rh.offset + counter)) {
664 textIndex++;
665 }
666 else if (textIndex == 0) {
667 rh.offset++;
668 rh.length--;
669 counter--;
670 max--;
671 }
672 else if (replace == null) {
673 replace = new StringBuffer(max);
674 replace.append(text.substring(0, textIndex));
675 replace.append(getLiteral(rh.offset +
676 counter));
677 }
678 }
679 else if (textIndex < tl) {
680 if (replace != null) {
681 replace.append(text.charAt(textIndex));
682 }
683 textIndex++;
684 }
685 else {
686 // Nothing to replace it with, assume ' '
687 if (replace == null) {
688 replace = new StringBuffer(max);
689 if (textIndex > 0) {
690 replace.append(text.substring(0, textIndex));
691 }
692 }
693 if (replace != null) {
694 replace.append(' ');
695 }
696 }
697 }
698 if (replace != null) {
699 rh.text = replace.toString();
700 }
701 }
702 else if (tl > 0) {
703 // insert (or insert and remove)
704 rh.offset = getNextNonliteralIndex(rh.offset, 1);
705 }
706 else {
707 // remove only
708 rh.offset = getNextNonliteralIndex(rh.offset, -1);
709 }
710 ((ExtendedReplaceHolder)rh).endOffset = rh.offset;
711 ((ExtendedReplaceHolder)rh).endTextLength = (rh.text != null) ?
712 rh.text.length() : 0;
713 }
714 else {
715 ((ExtendedReplaceHolder)rh).endOffset = rh.offset;
716 ((ExtendedReplaceHolder)rh).endTextLength = (rh.text != null) ?
717 rh.text.length() : 0;
718 }
719 boolean can = super.canReplace(rh);
720 if (can && !getAllowsInvalid()) {
721 ((ExtendedReplaceHolder)rh).resetFromValue(this);
722 }
723 return can;
724 }
725
726 /**
727 * When in !allowsInvalid mode the text is reset on every edit, thus
728 * supers implementation will position the cursor at the wrong position.
729 * As such, this invokes supers implementation and then invokes
730 * <code>repositionCursor</code> to correctly reset the cursor.
731 */
732 boolean replace(ReplaceHolder rh) throws BadLocationException {
733 int start = -1;
734 int direction = 1;
735 int literalCount = -1;
736
737 if (rh.length > 0 && (rh.text == null || rh.text.length() == 0) &&
738 (getFormattedTextField().getSelectionStart() != rh.offset ||
739 rh.length > 1)) {
740 direction = -1;
741 }
742 if (!getAllowsInvalid()) {
743 if ((rh.text == null || rh.text.length() == 0) && rh.length > 0) {
744 // remove
745 start = getFormattedTextField().getSelectionStart();
746 }
747 else {
748 start = rh.offset;
749 }
750 literalCount = getLiteralCountTo(start);
751 }
752 if (super.replace(rh)) {
753 if (start != -1) {
754 int end = ((ExtendedReplaceHolder)rh).endOffset;
755
756 end += ((ExtendedReplaceHolder)rh).endTextLength;
757 repositionCursor(literalCount, end, direction);
758 }
759 else {
760 start = ((ExtendedReplaceHolder)rh).endOffset;
761 if (direction == 1) {
762 start += ((ExtendedReplaceHolder)rh).endTextLength;
763 }
764 repositionCursor(start, direction);
765 }
766 return true;
767 }
768 return false;
769 }
770
771 /**
772 * Repositions the cursor. <code>startLiteralCount</code> gives
773 * the number of literals to the start of the deleted range, end
774 * gives the ending location to adjust from, direction gives
775 * the direction relative to <code>end</code> to position the
776 * cursor from.
777 */
778 private void repositionCursor(int startLiteralCount, int end,
779 int direction) {
780 int endLiteralCount = getLiteralCountTo(end);
781
782 if (endLiteralCount != end) {
783 end -= startLiteralCount;
784 for (int counter = 0; counter < end; counter++) {
785 if (isLiteral(counter)) {
786 end++;
787 }
788 }
789 }
790 repositionCursor(end, 1 /*direction*/);
791 }
792
793 /**
794 * Returns the character from the mask that has been buffered
795 * at <code>index</code>.
796 */
797 char getBufferedChar(int index) {
798 if (isValidMask()) {
799 if (string != null && index < string.length()) {
800 return string.charAt(index);
801 }
802 }
803 return (char)0;
804 }
805
806 /**
807 * Returns true if the current mask is valid.
808 */
809 boolean isValidMask() {
810 return validMask;
811 }
812
813 /**
814 * Returns true if <code>attributes</code> is null or empty.
815 */
816 boolean isLiteral(Map attributes) {
817 return ((attributes == null) || attributes.size() == 0);
818 }
819
820 /**
821 * Updates the interal bitset from <code>iterator</code>. This will
822 * set <code>validMask</code> to true if <code>iterator</code> is
823 * non-null.
824 */
825 private void updateMask(AttributedCharacterIterator iterator) {
826 if (iterator != null) {
827 validMask = true;
828 this.iterator = iterator;
829
830 // Update the literal mask
831 if (literalMask == null) {
832 literalMask = new BitSet();
833 }
834 else {
835 for (int counter = literalMask.length() - 1; counter >= 0;
836 counter--) {
837 literalMask.clear(counter);
838 }
839 }
840
841 iterator.first();
842 while (iterator.current() != CharacterIterator.DONE) {
843 Map attributes = iterator.getAttributes();
844 boolean set = isLiteral(attributes);
845 int start = iterator.getIndex();
846 int end = iterator.getRunLimit();
847
848 while (start < end) {
849 if (set) {
850 literalMask.set(start);
851 }
852 else {
853 literalMask.clear(start);
854 }
855 start++;
856 }
857 iterator.setIndex(start);
858 }
859 }
860 }
861
862 /**
863 * Returns true if <code>field</code> is non-null.
864 * Subclasses that wish to allow incrementing to happen outside of
865 * the known fields will need to override this.
866 */
867 boolean canIncrement(Object field, int cursorPosition) {
868 return (field != null);
869 }
870
871 /**
872 * Selects the fields identified by <code>attributes</code>.
873 */
874 void selectField(Object f, int count) {
875 AttributedCharacterIterator iterator = getIterator();
876
877 if (iterator != null &&
878 (f instanceof AttributedCharacterIterator.Attribute)) {
879 AttributedCharacterIterator.Attribute field =
880 (AttributedCharacterIterator.Attribute)f;
881
882 iterator.first();
883 while (iterator.current() != CharacterIterator.DONE) {
884 while (iterator.getAttribute(field) == null &&
885 iterator.next() != CharacterIterator.DONE);
886 if (iterator.current() != CharacterIterator.DONE) {
887 int limit = iterator.getRunLimit(field);
888
889 if (--count <= 0) {
890 getFormattedTextField().select(iterator.getIndex(),
891 limit);
892 break;
893 }
894 iterator.setIndex(limit);
895 iterator.next();
896 }
897 }
898 }
899 }
900
901 /**
902 * Returns the field that will be adjusted by adjustValue.
903 */
904 Object getAdjustField(int start, Map attributes) {
905 return null;
906 }
907
908 /**
909 * Returns the number of occurences of <code>f</code> before
910 * the location <code>start</code> in the current
911 * <code>AttributedCharacterIterator</code>.
912 */
913 private int getFieldTypeCountTo(Object f, int start) {
914 AttributedCharacterIterator iterator = getIterator();
915 int count = 0;
916
917 if (iterator != null &&
918 (f instanceof AttributedCharacterIterator.Attribute)) {
919 AttributedCharacterIterator.Attribute field =
920 (AttributedCharacterIterator.Attribute)f;
921 int index = 0;
922
923 iterator.first();
924 while (iterator.getIndex() < start) {
925 while (iterator.getAttribute(field) == null &&
926 iterator.next() != CharacterIterator.DONE);
927 if (iterator.current() != CharacterIterator.DONE) {
928 iterator.setIndex(iterator.getRunLimit(field));
929 iterator.next();
930 count++;
931 }
932 else {
933 break;
934 }
935 }
936 }
937 return count;
938 }
939
940 /**
941 * Subclasses supporting incrementing must override this to handle
942 * the actual incrementing. <code>value</code> is the current value,
943 * <code>attributes</code> gives the field the cursor is in (may be
944 * null depending upon <code>canIncrement</code>) and
945 * <code>direction</code> is the amount to increment by.
946 */
947 Object adjustValue(Object value, Map attributes, Object field,
948 int direction) throws
949 BadLocationException, ParseException {
950 return null;
951 }
952
953 /**
954 * Returns false, indicating InternationalFormatter does not allow
955 * incrementing of the value. Subclasses that wish to support
956 * incrementing/decrementing the value should override this and
957 * return true. Subclasses should also override
958 * <code>adjustValue</code>.
959 */
960 boolean getSupportsIncrement() {
961 return false;
962 }
963
964 /**
965 * Resets the value of the JFormattedTextField to be
966 * <code>value</code>.
967 */
968 void resetValue(Object value) throws BadLocationException, ParseException {
969 Document doc = getFormattedTextField().getDocument();
970 String string = valueToString(value);
971
972 try {
973 ignoreDocumentMutate = true;
974 doc.remove(0, doc.getLength());
975 doc.insertString(0, string, null);
976 } finally {
977 ignoreDocumentMutate = false;
978 }
979 updateValue(value);
980 }
981
982 /**
983 * Subclassed to update the internal representation of the mask after
984 * the default read operation has completed.
985 */
986 private void readObject(ObjectInputStream s)
987 throws IOException, ClassNotFoundException {
988 s.defaultReadObject();
989 updateMaskIfNecessary();
990 }
991
992
993 /**
994 * Overriden to return an instance of <code>ExtendedReplaceHolder</code>.
995 */
996 ReplaceHolder getReplaceHolder(DocumentFilter.FilterBypass fb, int offset,
997 int length, String text,
998 AttributeSet attrs) {
999 if (replaceHolder == null) {
1000 replaceHolder = new ExtendedReplaceHolder();
1001 }
1002 return super.getReplaceHolder(fb, offset, length, text, attrs);
1003 }
1004
1005
1006 /**
1007 * As InternationalFormatter replaces the complete text on every edit,
1008 * ExtendedReplaceHolder keeps track of the offset and length passed
1009 * into canReplace.
1010 */
1011 static class ExtendedReplaceHolder extends ReplaceHolder {
1012 /** Offset of the insert/remove. This may differ from offset in
1013 * that if !allowsInvalid the text is replaced on every edit. */
1014 int endOffset;
1015 /** Length of the text. This may differ from text.length in
1016 * that if !allowsInvalid the text is replaced on every edit. */
1017 int endTextLength;
1018
1019 /**
1020 * Resets the region to delete to be the complete document and
1021 * the text from invoking valueToString on the current value.
1022 */
1023 void resetFromValue(InternationalFormatter formatter) {
1024 // Need to reset the complete string as Format's result can
1025 // be completely different.
1026 offset = 0;
1027 try {
1028 text = formatter.valueToString(value);
1029 } catch (ParseException pe) {
1030 // Should never happen, otherwise canReplace would have
1031 // returned value.
1032 text = "";
1033 }
1034 length = fb.getDocument().getLength();
1035 }
1036 }
1037
1038
1039 /**
1040 * IncrementAction is used to increment the value by a certain amount.
1041 * It calls into <code>adjustValue</code> to handle the actual
1042 * incrementing of the value.
1043 */
1044 private class IncrementAction extends AbstractAction {
1045 private int direction;
1046
1047 IncrementAction(String name, int direction) {
1048 super(name);
1049 this.direction = direction;
1050 }
1051
1052 public void actionPerformed(ActionEvent ae) {
1053
1054 if (getFormattedTextField().isEditable()) {
1055 if (getAllowsInvalid()) {
1056 // This will work if the currently edited value is valid.
1057 updateMask();
1058 }
1059
1060 boolean validEdit = false;
1061
1062 if (isValidMask()) {
1063 int start = getFormattedTextField().getSelectionStart();
1064
1065 if (start != -1) {
1066 AttributedCharacterIterator iterator = getIterator();
1067
1068 iterator.setIndex(start);
1069
1070 Map attributes = iterator.getAttributes();
1071 Object field = getAdjustField(start, attributes);
1072
1073 if (canIncrement(field, start)) {
1074 try {
1075 Object value = stringToValue(
1076 getFormattedTextField().getText());
1077 int fieldTypeCount = getFieldTypeCountTo(
1078 field, start);
1079
1080 value = adjustValue(value, attributes,
1081 field, direction);
1082 if (value != null && isValidValue(value, false)) {
1083 resetValue(value);
1084 updateMask();
1085
1086 if (isValidMask()) {
1087 selectField(field, fieldTypeCount);
1088 }
1089 validEdit = true;
1090 }
1091 }
1092 catch (ParseException pe) { }
1093 catch (BadLocationException ble) { }
1094 }
1095 }
1096 }
1097 if (!validEdit) {
1098 invalidEdit();
1099 }
1100 }
1101 }
1102 }
1103}