--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/jdk/src/share/classes/javax/swing/text/InternationalFormatter.java Sat Dec 01 00:00:00 2007 +0000
@@ -0,0 +1,1103 @@
+/*
+ * Copyright 2000-2006 Sun Microsystems, Inc. All Rights Reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Sun designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Sun in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa Clara,
+ * CA 95054 USA or visit www.sun.com if you need additional information or
+ * have any questions.
+ */
+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
+ *
+ * @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 (getFormattedTextField().isEditable()) {
+ 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();
+ }
+ }
+ }
+ }
+}