- /*
- * @(#)InternationalFormatter.java 1.17 04/05/12
- *
- * Copyright 2004 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();
- }
- }
- }
- }