- /*
 - * @(#)InternationalFormatter.java 1.14 03/01/23
 - *
 - * Copyright 2003 Sun Microsystems, Inc. All rights reserved.
 - * SUN PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
 - */
 - package javax.swing.text;
 - import java.awt.event.ActionEvent;
 - import java.io.*;
 - import java.text.*;
 - import java.util.*;
 - import javax.swing.*;
 - import javax.swing.text.*;
 - /**
 - * <code>InternationalFormatter</code> extends <code>DefaultFormatter</code>,
 - * using an instance of <code>java.text.Format</code> to handle the
 - * conversion to a String, and the conversion from a String.
 - * <p>
 - * If <code>getAllowsInvalid()</code> is false, this will ask the
 - * <code>Format</code> to format the current text on every edit.
 - * <p>
 - * You can specify a minimum and maximum value by way of the
 - * <code>setMinimum</code> and <code>setMaximum</code> methods. In order
 - * for this to work the values returned from <code>stringToValue</code> must be
 - * comparable to the min/max values by way of the <code>Comparable</code>
 - * interface.
 - * <p>
 - * Be careful how you configure the <code>Format</code> and the
 - * <code>InternationalFormatter</code>, as it is possible to create a
 - * situation where certain values can not be input. Consider the date
 - * format 'M/d/yy', an <code>InternationalFormatter</code> that is always
 - * valid (<code>setAllowsInvalid(false)</code>), is in overwrite mode
 - * (<code>setOverwriteMode(true)</code>) and the date 7/1/99. In this
 - * case the user will not be able to enter a two digit month or day of
 - * month. To avoid this, the format should be 'MM/dd/yy'.
 - * <p>
 - * If <code>InternationalFormatter</code> is configured to only allow valid
 - * values (<code>setAllowsInvalid(false)</code>), every valid edit will result
 - * in the text of the <code>JFormattedTextField</code> being completely reset
 - * from the <code>Format</code>.
 - * The cursor position will also be adjusted as literal characters are
 - * added/removed from the resulting String.
 - * <p>
 - * <code>InternationalFormatter</code>'s behavior of
 - * <code>stringToValue</code> is slightly different than that of
 - * <code>DefaultTextFormatter</code>, it does the following:
 - * <ol>
 - * <li><code>parseObject</code> is invoked on the <code>Format</code>
 - * specified by <code>setFormat</code>
 - * <li>If a Class has been set for the values (<code>setValueClass</code>),
 - * supers implementation is invoked to convert the value returned
 - * from <code>parseObject</code> to the appropriate class.
 - * <li>If a <code>ParseException</code> has not been thrown, and the value
 - * is outside the min/max a <code>ParseException</code> is thrown.
 - * <li>The value is returned.
 - * </ol>
 - * <code>InternationalFormatter</code> implements <code>stringToValue</code>
 - * in this manner so that you can specify an alternate Class than
 - * <code>Format</code> may return.
 - * <p>
 - * <strong>Warning:</strong>
 - * Serialized objects of this class will not be compatible with
 - * future Swing releases. The current serialization support is
 - * appropriate for short term storage or RMI between applications running
 - * the same version of Swing. As of 1.4, support for long term storage
 - * of all JavaBeans<sup><font size="-2">TM</font></sup>
 - * has been added to the <code>java.beans</code> package.
 - * Please see {@link java.beans.XMLEncoder}.
 - *
 - * @see java.text.Format
 - * @see java.lang.Comparable
 - *
 - * @version 1.7 04/09/01
 - * @since 1.4
 - */
 - public class InternationalFormatter extends DefaultFormatter {
 - /**
 - * Used by <code>getFields</code>.
 - */
 - private static final Format.Field[] EMPTY_FIELD_ARRAY =new Format.Field[0];
 - /**
 - * Object used to handle the conversion.
 - */
 - private Format format;
 - /**
 - * Can be used to impose a maximum value.
 - */
 - private Comparable max;
 - /**
 - * Can be used to impose a minimum value.
 - */
 - private Comparable min;
 - /**
 - * <code>InternationalFormatter</code>'s behavior is dicatated by a
 - * <code>AttributedCharacterIterator</code> that is obtained from
 - * the <code>Format</code>. On every edit, assuming
 - * allows invalid is false, the <code>Format</code> instance is invoked
 - * with <code>formatToCharacterIterator</code>. A <code>BitSet</code> is
 - * also kept upto date with the non-literal characters, that is
 - * for every index in the <code>AttributedCharacterIterator</code> an
 - * entry in the bit set is updated based on the return value from
 - * <code>isLiteral(Map)</code>. <code>isLiteral(int)</code> then uses
 - * this cached information.
 - * <p>
 - * If allowsInvalid is false, every edit results in resetting the complete
 - * text of the JTextComponent.
 - * <p>
 - * InternationalFormatterFilter can also provide two actions suitable for
 - * incrementing and decrementing. To enable this a subclass must
 - * override <code>getSupportsIncrement</code> to return true, and
 - * override <code>adjustValue</code> to handle the changing of the
 - * value. If you want to support changing the value outside of
 - * the valid FieldPositions, you will need to override
 - * <code>canIncrement</code>.
 - */
 - /**
 - * A bit is set for every index identified in the
 - * AttributedCharacterIterator that is not considered decoration.
 - * This should only be used if validMask is true.
 - */
 - private transient BitSet literalMask;
 - /**
 - * Used to iterate over characters.
 - */
 - private transient AttributedCharacterIterator iterator;
 - /**
 - * True if the Format was able to convert the value to a String and
 - * back.
 - */
 - private transient boolean validMask;
 - /**
 - * Current value being displayed.
 - */
 - private transient String string;
 - /**
 - * If true, DocumentFilter methods are unconditionally allowed,
 - * and no checking is done on their values. This is used when
 - * incrementing/decrementing via the actions.
 - */
 - private transient boolean ignoreDocumentMutate;
 - /**
 - * Creates an <code>InternationalFormatter</code> with no
 - * <code>Format</code> specified.
 - */
 - public InternationalFormatter() {
 - setOverwriteMode(false);
 - }
 - /**
 - * Creates an <code>InternationalFormatter</code> with the specified
 - * <code>Format</code> instance.
 - *
 - * @param format Format instance used for converting from/to Strings
 - */
 - public InternationalFormatter(Format format) {
 - this();
 - setFormat(format);
 - }
 - /**
 - * Sets the format that dictates the legal values that can be edited
 - * and displayed.
 - *
 - * @param format <code>Format</code> instance used for converting
 - * from/to Strings
 - */
 - public void setFormat(Format format) {
 - this.format = format;
 - }
 - /**
 - * Returns the format that dictates the legal values that can be edited
 - * and displayed.
 - *
 - * @return Format instance used for converting from/to Strings
 - */
 - public Format getFormat() {
 - return format;
 - }
 - /**
 - * Sets the minimum permissible value. If the <code>valueClass</code> has
 - * not been specified, and <code>minimum</code> is non null, the
 - * <code>valueClass</code> will be set to that of the class of
 - * <code>minimum</code>.
 - *
 - * @param minimum Minimum legal value that can be input
 - * @see #setValueClass
 - */
 - public void setMinimum(Comparable minimum) {
 - if (getValueClass() == null && minimum != null) {
 - setValueClass(minimum.getClass());
 - }
 - min = minimum;
 - }
 - /**
 - * Returns the minimum permissible value.
 - *
 - * @return Minimum legal value that can be input
 - */
 - public Comparable getMinimum() {
 - return min;
 - }
 - /**
 - * Sets the maximum permissible value. If the <code>valueClass</code> has
 - * not been specified, and <code>max</code> is non null, the
 - * <code>valueClass</code> will be set to that of the class of
 - * <code>max</code>.
 - *
 - * @param max Maximum legal value that can be input
 - * @see #setValueClass
 - */
 - public void setMaximum(Comparable max) {
 - if (getValueClass() == null && max != null) {
 - setValueClass(max.getClass());
 - }
 - this.max = max;
 - }
 - /**
 - * Returns the maximum permissible value.
 - *
 - * @return Maximum legal value that can be input
 - */
 - public Comparable getMaximum() {
 - return max;
 - }
 - /**
 - * Installs the <code>DefaultFormatter</code> onto a particular
 - * <code>JFormattedTextField</code>.
 - * This will invoke <code>valueToString</code> to convert the
 - * current value from the <code>JFormattedTextField</code> to
 - * a String. This will then install the <code>Action</code>s from
 - * <code>getActions</code>, the <code>DocumentFilter</code>
 - * returned from <code>getDocumentFilter</code> and the
 - * <code>NavigationFilter</code> returned from
 - * <code>getNavigationFilter</code> onto the
 - * <code>JFormattedTextField</code>.
 - * <p>
 - * Subclasses will typically only need to override this if they
 - * wish to install additional listeners on the
 - * <code>JFormattedTextField</code>.
 - * <p>
 - * If there is a <code>ParseException</code> in converting the
 - * current value to a String, this will set the text to an empty
 - * String, and mark the <code>JFormattedTextField</code> as being
 - * in an invalid state.
 - * <p>
 - * While this is a public method, this is typically only useful
 - * for subclassers of <code>JFormattedTextField</code>.
 - * <code>JFormattedTextField</code> will invoke this method at
 - * the appropriate times when the value changes, or its internal
 - * state changes.
 - *
 - * @param ftf JFormattedTextField to format for, may be null indicating
 - * uninstall from current JFormattedTextField.
 - */
 - public void install(JFormattedTextField ftf) {
 - super.install(ftf);
 - updateMaskIfNecessary();
 - // invoked again as the mask should now be valid.
 - positionCursorAtInitialLocation();
 - }
 - /**
 - * Returns a String representation of the Object <code>value</code>.
 - * This invokes <code>format</code> on the current <code>Format</code>.
 - *
 - * @throws ParseException if there is an error in the conversion
 - * @param value Value to convert
 - * @return String representation of value
 - */
 - public String valueToString(Object value) throws ParseException {
 - if (value == null) {
 - return "";
 - }
 - Format f = getFormat();
 - if (f == null) {
 - return value.toString();
 - }
 - return f.format(value);
 - }
 - /**
 - * Returns the <code>Object</code> representation of the
 - * <code>String</code> <code>text</code>.
 - *
 - * @param text <code>String</code> to convert
 - * @return <code>Object</code> representation of text
 - * @throws ParseException if there is an error in the conversion
 - */
 - public Object stringToValue(String text) throws ParseException {
 - Object value = stringToValue(text, getFormat());
 - // Convert to the value class if the Value returned from the
 - // Format does not match.
 - if (value != null && getValueClass() != null &&
 - !getValueClass().isInstance(value)) {
 - value = super.stringToValue(value.toString());
 - }
 - try {
 - if (!isValidValue(value, true)) {
 - throw new ParseException("Value not within min/max range", 0);
 - }
 - } catch (ClassCastException cce) {
 - throw new ParseException("Class cast exception comparing values: "
 - + cce, 0);
 - }
 - return value;
 - }
 - /**
 - * Returns the <code>Format.Field</code> constants associated with
 - * the text at <code>offset</code>. If <code>offset</code> is not
 - * a valid location into the current text, this will return an
 - * empty array.
 - *
 - * @param offset offset into text to be examined
 - * @return Format.Field constants associated with the text at the
 - * given position.
 - */
 - public Format.Field[] getFields(int offset) {
 - if (getAllowsInvalid()) {
 - // This will work if the currently edited value is valid.
 - updateMask();
 - }
 - Map attrs = getAttributes(offset);
 - if (attrs != null && attrs.size() > 0) {
 - ArrayList al = new ArrayList();
 - al.addAll(attrs.keySet());
 - return (Format.Field[])al.toArray(EMPTY_FIELD_ARRAY);
 - }
 - return EMPTY_FIELD_ARRAY;
 - }
 - /**
 - * Creates a copy of the DefaultFormatter.
 - *
 - * @return copy of the DefaultFormatter
 - */
 - public Object clone() throws CloneNotSupportedException {
 - InternationalFormatter formatter = (InternationalFormatter)super.
 - clone();
 - formatter.literalMask = null;
 - formatter.iterator = null;
 - formatter.validMask = false;
 - formatter.string = null;
 - return formatter;
 - }
 - /**
 - * If <code>getSupportsIncrement</code> returns true, this returns
 - * two Actions suitable for incrementing/decrementing the value.
 - */
 - protected Action[] getActions() {
 - if (getSupportsIncrement()) {
 - return new Action[] { new IncrementAction("increment", 1),
 - new IncrementAction("decrement", -1) };
 - }
 - return null;
 - }
 - /**
 - * Invokes <code>parseObject</code> on <code>f</code>, returning
 - * its value.
 - */
 - Object stringToValue(String text, Format f) throws ParseException {
 - if (f == null) {
 - return text;
 - }
 - return f.parseObject(text);
 - }
 - /**
 - * Returns true if <code>value</code> is between the min/max.
 - *
 - * @param wantsCCE If false, and a ClassCastException is thrown in
 - * comparing the values, the exception is consumed and
 - * false is returned.
 - */
 - boolean isValidValue(Object value, boolean wantsCCE) {
 - Comparable min = getMinimum();
 - try {
 - if (min != null && min.compareTo(value) > 0) {
 - return false;
 - }
 - } catch (ClassCastException cce) {
 - if (wantsCCE) {
 - throw cce;
 - }
 - return false;
 - }
 - Comparable max = getMaximum();
 - try {
 - if (max != null && max.compareTo(value) < 0) {
 - return false;
 - }
 - } catch (ClassCastException cce) {
 - if (wantsCCE) {
 - throw cce;
 - }
 - return false;
 - }
 - return true;
 - }
 - /**
 - * Returns a Set of the attribute identifiers at <code>index</code>.
 - */
 - Map getAttributes(int index) {
 - if (isValidMask()) {
 - AttributedCharacterIterator iterator = getIterator();
 - if (index >= 0 && index <= iterator.getEndIndex()) {
 - iterator.setIndex(index);
 - return iterator.getAttributes();
 - }
 - }
 - return null;
 - }
 - /**
 - * Returns the start of the first run that contains the attribute
 - * <code>id</code>. This will return <code>-1</code> if the attribute
 - * can not be found.
 - */
 - int getAttributeStart(AttributedCharacterIterator.Attribute id) {
 - if (isValidMask()) {
 - AttributedCharacterIterator iterator = getIterator();
 - iterator.first();
 - while (iterator.current() != CharacterIterator.DONE) {
 - if (iterator.getAttribute(id) != null) {
 - return iterator.getIndex();
 - }
 - iterator.next();
 - }
 - }
 - return -1;
 - }
 - /**
 - * Returns the <code>AttributedCharacterIterator</code> used to
 - * format the last value.
 - */
 - AttributedCharacterIterator getIterator() {
 - return iterator;
 - }
 - /**
 - * Updates the AttributedCharacterIterator and bitset, if necessary.
 - */
 - void updateMaskIfNecessary() {
 - if (!getAllowsInvalid() && (getFormat() != null)) {
 - if (!isValidMask()) {
 - updateMask();
 - }
 - else {
 - String newString = getFormattedTextField().getText();
 - if (!newString.equals(string)) {
 - updateMask();
 - }
 - }
 - }
 - }
 - /**
 - * Updates the AttributedCharacterIterator by invoking
 - * <code>formatToCharacterIterator</code> on the <code>Format</code>.
 - * If this is successful,
 - * <code>updateMask(AttributedCharacterIterator)</code>
 - * is then invoked to update the internal bitmask.
 - */
 - void updateMask() {
 - if (getFormat() != null) {
 - Document doc = getFormattedTextField().getDocument();
 - validMask = false;
 - if (doc != null) {
 - try {
 - string = doc.getText(0, doc.getLength());
 - } catch (BadLocationException ble) {
 - string = null;
 - }
 - if (string != null) {
 - try {
 - Object value = stringToValue(string);
 - AttributedCharacterIterator iterator = getFormat().
 - formatToCharacterIterator(value);
 - updateMask(iterator);
 - }
 - catch (ParseException pe) {}
 - catch (IllegalArgumentException iae) {}
 - catch (NullPointerException npe) {}
 - }
 - }
 - }
 - }
 - /**
 - * Returns the number of literal characters before <code>index</code>.
 - */
 - int getLiteralCountTo(int index) {
 - int lCount = 0;
 - for (int counter = 0; counter < index; counter++) {
 - if (isLiteral(counter)) {
 - lCount++;
 - }
 - }
 - return lCount;
 - }
 - /**
 - * Returns true if the character at index is a literal, that is
 - * not editable.
 - */
 - boolean isLiteral(int index) {
 - if (isValidMask() && index < string.length()) {
 - return literalMask.get(index);
 - }
 - return false;
 - }
 - /**
 - * Returns the literal character at index.
 - */
 - char getLiteral(int index) {
 - if (isValidMask() && string != null && index < string.length()) {
 - return string.charAt(index);
 - }
 - return (char)0;
 - }
 - /**
 - * Returns true if the character at offset is navigatable too. This
 - * is implemented in terms of <code>isLiteral</code>, subclasses
 - * may wish to provide different behavior.
 - */
 - boolean isNavigatable(int offset) {
 - return !isLiteral(offset);
 - }
 - /**
 - * Overriden to update the mask after invoking supers implementation.
 - */
 - void updateValue(Object value) {
 - super.updateValue(value);
 - updateMaskIfNecessary();
 - }
 - /**
 - * Overriden to unconditionally allow the replace if
 - * ignoreDocumentMutate is true.
 - */
 - void replace(DocumentFilter.FilterBypass fb, int offset,
 - int length, String text,
 - AttributeSet attrs) throws BadLocationException {
 - if (ignoreDocumentMutate) {
 - fb.replace(offset, length, text, attrs);
 - return;
 - }
 - super.replace(fb, offset, length, text, attrs);
 - }
 - /**
 - * Returns the index of the next non-literal character starting at
 - * index. If index is not a literal, it will be returned.
 - *
 - * @param direction Amount to increment looking for non-literal
 - */
 - private int getNextNonliteralIndex(int index, int direction) {
 - int max = getFormattedTextField().getDocument().getLength();
 - while (index >= 0 && index < max) {
 - if (isLiteral(index)) {
 - index += direction;
 - }
 - else {
 - return index;
 - }
 - }
 - return (direction == -1) ? 0 : max;
 - }
 - /**
 - * Overriden in an attempt to honor the literals.
 - * <p>
 - * If we do
 - * not allow invalid values and are in overwrite mode, this does the
 - * following for each character in the replacement range:
 - * <ol>
 - * <li>If the character is a literal, add it to the string to replace
 - * with. If there is text to insert and it doesn't match the
 - * literal, then insert the literal in the the middle of the insert
 - * text. This allows you to either paste in literals or not and
 - * get the same behavior.
 - * <li>If there is no text to insert, replace it with ' '.
 - * </ol>
 - * If not in overwrite mode, and there is text to insert it is
 - * inserted at the next non literal index going forward. If there
 - * is only text to remove, it is removed from the next non literal
 - * index going backward.
 - */
 - boolean canReplace(ReplaceHolder rh) {
 - if (!getAllowsInvalid()) {
 - String text = rh.text;
 - int tl = (text != null) ? text.length() : 0;
 - if (tl == 0 && rh.length == 1 && getFormattedTextField().
 - getSelectionStart() != rh.offset) {
 - // Backspace, adjust to actually delete next non-literal.
 - rh.offset = getNextNonliteralIndex(rh.offset, -1);
 - }
 - if (getOverwriteMode()) {
 - StringBuffer replace = null;
 - for (int counter = 0, textIndex = 0,
 - max = Math.max(tl, rh.length); counter < max;
 - counter++) {
 - if (isLiteral(rh.offset + counter)) {
 - if (replace != null) {
 - replace.append(getLiteral(rh.offset +
 - counter));
 - }
 - if (textIndex < tl && text.charAt(textIndex) ==
 - getLiteral(rh.offset + counter)) {
 - textIndex++;
 - }
 - else if (textIndex == 0) {
 - rh.offset++;
 - rh.length--;
 - counter--;
 - max--;
 - }
 - else if (replace == null) {
 - replace = new StringBuffer(max);
 - replace.append(text.substring(0, textIndex));
 - replace.append(getLiteral(rh.offset +
 - counter));
 - }
 - }
 - else if (textIndex < tl) {
 - if (replace != null) {
 - replace.append(text.charAt(textIndex));
 - }
 - textIndex++;
 - }
 - else {
 - // Nothing to replace it with, assume ' '
 - if (replace == null) {
 - replace = new StringBuffer(max);
 - if (textIndex > 0) {
 - replace.append(text.substring(0, textIndex));
 - }
 - }
 - if (replace != null) {
 - replace.append(' ');
 - }
 - }
 - }
 - if (replace != null) {
 - rh.text = replace.toString();
 - }
 - }
 - else if (tl > 0) {
 - // insert (or insert and remove)
 - rh.offset = getNextNonliteralIndex(rh.offset, 1);
 - }
 - else {
 - // remove only
 - rh.offset = getNextNonliteralIndex(rh.offset, -1);
 - }
 - ((ExtendedReplaceHolder)rh).endOffset = rh.offset;
 - ((ExtendedReplaceHolder)rh).endTextLength = (rh.text != null) ?
 - rh.text.length() : 0;
 - }
 - else {
 - ((ExtendedReplaceHolder)rh).endOffset = rh.offset;
 - ((ExtendedReplaceHolder)rh).endTextLength = (rh.text != null) ?
 - rh.text.length() : 0;
 - }
 - boolean can = super.canReplace(rh);
 - if (can && !getAllowsInvalid()) {
 - ((ExtendedReplaceHolder)rh).resetFromValue(this);
 - }
 - return can;
 - }
 - /**
 - * When in !allowsInvalid mode the text is reset on every edit, thus
 - * supers implementation will position the cursor at the wrong position.
 - * As such, this invokes supers implementation and then invokes
 - * <code>repositionCursor</code> to correctly reset the cursor.
 - */
 - boolean replace(ReplaceHolder rh) throws BadLocationException {
 - int start = -1;
 - int direction = 1;
 - int literalCount = -1;
 - if (rh.length > 0 && (rh.text == null || rh.text.length() == 0) &&
 - (getFormattedTextField().getSelectionStart() != rh.offset ||
 - rh.length > 1)) {
 - direction = -1;
 - }
 - if (!getAllowsInvalid()) {
 - if ((rh.text == null || rh.text.length() == 0) && rh.length > 0) {
 - // remove
 - start = getFormattedTextField().getSelectionStart();
 - }
 - else {
 - start = rh.offset;
 - }
 - literalCount = getLiteralCountTo(start);
 - }
 - if (super.replace(rh)) {
 - if (start != -1) {
 - int end = ((ExtendedReplaceHolder)rh).endOffset;
 - end += ((ExtendedReplaceHolder)rh).endTextLength;
 - repositionCursor(literalCount, end, direction);
 - }
 - else {
 - start = ((ExtendedReplaceHolder)rh).endOffset;
 - if (direction == 1) {
 - start += ((ExtendedReplaceHolder)rh).endTextLength;
 - }
 - repositionCursor(start, direction);
 - }
 - return true;
 - }
 - return false;
 - }
 - /**
 - * Repositions the cursor. <code>startLiteralCount</code> gives
 - * the number of literals to the start of the deleted range, end
 - * gives the ending location to adjust from, direction gives
 - * the direction relative to <code>end</code> to position the
 - * cursor from.
 - */
 - private void repositionCursor(int startLiteralCount, int end,
 - int direction) {
 - int endLiteralCount = getLiteralCountTo(end);
 - if (endLiteralCount != end) {
 - end -= startLiteralCount;
 - for (int counter = 0; counter < end; counter++) {
 - if (isLiteral(counter)) {
 - end++;
 - }
 - }
 - }
 - repositionCursor(end, 1 /*direction*/);
 - }
 - /**
 - * Returns the character from the mask that has been buffered
 - * at <code>index</code>.
 - */
 - char getBufferedChar(int index) {
 - if (isValidMask()) {
 - if (string != null && index < string.length()) {
 - return string.charAt(index);
 - }
 - }
 - return (char)0;
 - }
 - /**
 - * Returns true if the current mask is valid.
 - */
 - boolean isValidMask() {
 - return validMask;
 - }
 - /**
 - * Returns true if <code>attributes</code> is null or empty.
 - */
 - boolean isLiteral(Map attributes) {
 - return ((attributes == null) || attributes.size() == 0);
 - }
 - /**
 - * Updates the interal bitset from <code>iterator</code>. This will
 - * set <code>validMask</code> to true if <code>iterator</code> is
 - * non-null.
 - */
 - private void updateMask(AttributedCharacterIterator iterator) {
 - if (iterator != null) {
 - validMask = true;
 - this.iterator = iterator;
 - // Update the literal mask
 - if (literalMask == null) {
 - literalMask = new BitSet();
 - }
 - else {
 - for (int counter = literalMask.length() - 1; counter >= 0;
 - counter--) {
 - literalMask.clear(counter);
 - }
 - }
 - iterator.first();
 - while (iterator.current() != CharacterIterator.DONE) {
 - Map attributes = iterator.getAttributes();
 - boolean set = isLiteral(attributes);
 - int start = iterator.getIndex();
 - int end = iterator.getRunLimit();
 - while (start < end) {
 - if (set) {
 - literalMask.set(start);
 - }
 - else {
 - literalMask.clear(start);
 - }
 - start++;
 - }
 - iterator.setIndex(start);
 - }
 - }
 - }
 - /**
 - * Returns true if <code>field</code> is non-null.
 - * Subclasses that wish to allow incrementing to happen outside of
 - * the known fields will need to override this.
 - */
 - boolean canIncrement(Object field, int cursorPosition) {
 - return (field != null);
 - }
 - /**
 - * Selects the fields identified by <code>attributes</code>.
 - */
 - void selectField(Object f, int count) {
 - AttributedCharacterIterator iterator = getIterator();
 - if (iterator != null &&
 - (f instanceof AttributedCharacterIterator.Attribute)) {
 - AttributedCharacterIterator.Attribute field =
 - (AttributedCharacterIterator.Attribute)f;
 - iterator.first();
 - while (iterator.current() != CharacterIterator.DONE) {
 - while (iterator.getAttribute(field) == null &&
 - iterator.next() != CharacterIterator.DONE);
 - if (iterator.current() != CharacterIterator.DONE) {
 - int limit = iterator.getRunLimit(field);
 - if (--count <= 0) {
 - getFormattedTextField().select(iterator.getIndex(),
 - limit);
 - break;
 - }
 - iterator.setIndex(limit);
 - iterator.next();
 - }
 - }
 - }
 - }
 - /**
 - * Returns the field that will be adjusted by adjustValue.
 - */
 - Object getAdjustField(int start, Map attributes) {
 - return null;
 - }
 - /**
 - * Returns the number of occurences of <code>f</code> before
 - * the location <code>start</code> in the current
 - * <code>AttributedCharacterIterator</code>.
 - */
 - private int getFieldTypeCountTo(Object f, int start) {
 - AttributedCharacterIterator iterator = getIterator();
 - int count = 0;
 - if (iterator != null &&
 - (f instanceof AttributedCharacterIterator.Attribute)) {
 - AttributedCharacterIterator.Attribute field =
 - (AttributedCharacterIterator.Attribute)f;
 - int index = 0;
 - iterator.first();
 - while (iterator.getIndex() < start) {
 - while (iterator.getAttribute(field) == null &&
 - iterator.next() != CharacterIterator.DONE);
 - if (iterator.current() != CharacterIterator.DONE) {
 - iterator.setIndex(iterator.getRunLimit(field));
 - iterator.next();
 - count++;
 - }
 - else {
 - break;
 - }
 - }
 - }
 - return count;
 - }
 - /**
 - * Subclasses supporting incrementing must override this to handle
 - * the actual incrementing. <code>value</code> is the current value,
 - * <code>attributes</code> gives the field the cursor is in (may be
 - * null depending upon <code>canIncrement</code>) and
 - * <code>direction</code> is the amount to increment by.
 - */
 - Object adjustValue(Object value, Map attributes, Object field,
 - int direction) throws
 - BadLocationException, ParseException {
 - return null;
 - }
 - /**
 - * Returns false, indicating InternationalFormatter does not allow
 - * incrementing of the value. Subclasses that wish to support
 - * incrementing/decrementing the value should override this and
 - * return true. Subclasses should also override
 - * <code>adjustValue</code>.
 - */
 - boolean getSupportsIncrement() {
 - return false;
 - }
 - /**
 - * Resets the value of the JFormattedTextField to be
 - * <code>value</code>.
 - */
 - void resetValue(Object value) throws BadLocationException, ParseException {
 - Document doc = getFormattedTextField().getDocument();
 - String string = valueToString(value);
 - try {
 - ignoreDocumentMutate = true;
 - doc.remove(0, doc.getLength());
 - doc.insertString(0, string, null);
 - } finally {
 - ignoreDocumentMutate = false;
 - }
 - updateValue(value);
 - }
 - /**
 - * Subclassed to update the internal representation of the mask after
 - * the default read operation has completed.
 - */
 - private void readObject(ObjectInputStream s)
 - throws IOException, ClassNotFoundException {
 - s.defaultReadObject();
 - updateMaskIfNecessary();
 - }
 - /**
 - * Overriden to return an instance of <code>ExtendedReplaceHolder</code>.
 - */
 - ReplaceHolder getReplaceHolder(DocumentFilter.FilterBypass fb, int offset,
 - int length, String text,
 - AttributeSet attrs) {
 - if (replaceHolder == null) {
 - replaceHolder = new ExtendedReplaceHolder();
 - }
 - return super.getReplaceHolder(fb, offset, length, text, attrs);
 - }
 - /**
 - * As InternationalFormatter replaces the complete text on every edit,
 - * ExtendedReplaceHolder keeps track of the offset and length passed
 - * into canReplace.
 - */
 - static class ExtendedReplaceHolder extends ReplaceHolder {
 - /** Offset of the insert/remove. This may differ from offset in
 - * that if !allowsInvalid the text is replaced on every edit. */
 - int endOffset;
 - /** Length of the text. This may differ from text.length in
 - * that if !allowsInvalid the text is replaced on every edit. */
 - int endTextLength;
 - /**
 - * Resets the region to delete to be the complete document and
 - * the text from invoking valueToString on the current value.
 - */
 - void resetFromValue(InternationalFormatter formatter) {
 - // Need to reset the complete string as Format's result can
 - // be completely different.
 - offset = 0;
 - try {
 - text = formatter.valueToString(value);
 - } catch (ParseException pe) {
 - // Should never happen, otherwise canReplace would have
 - // returned value.
 - text = "";
 - }
 - length = fb.getDocument().getLength();
 - }
 - }
 - /**
 - * IncrementAction is used to increment the value by a certain amount.
 - * It calls into <code>adjustValue</code> to handle the actual
 - * incrementing of the value.
 - */
 - private class IncrementAction extends AbstractAction {
 - private int direction;
 - IncrementAction(String name, int direction) {
 - super(name);
 - this.direction = direction;
 - }
 - public void actionPerformed(ActionEvent ae) {
 - if (getAllowsInvalid()) {
 - // This will work if the currently edited value is valid.
 - updateMask();
 - }
 - boolean validEdit = false;
 - if (isValidMask()) {
 - int start = getFormattedTextField().getSelectionStart();
 - if (start != -1) {
 - AttributedCharacterIterator iterator = getIterator();
 - iterator.setIndex(start);
 - Map attributes = iterator.getAttributes();
 - Object field = getAdjustField(start, attributes);
 - if (canIncrement(field, start)) {
 - try {
 - Object value = stringToValue(
 - getFormattedTextField().getText());
 - int fieldTypeCount = getFieldTypeCountTo(
 - field, start);
 - value = adjustValue(value, attributes,
 - field, direction);
 - if (value != null && isValidValue(value, false)) {
 - resetValue(value);
 - updateMask();
 - if (isValidMask()) {
 - selectField(field, fieldTypeCount);
 - }
 - validEdit = true;
 - }
 - }
 - catch (ParseException pe) { }
 - catch (BadLocationException ble) { }
 - }
 - }
 - }
 - if (!validEdit) {
 - invalidEdit();
 - }
 - }
 - }
 - }