/*
* Copyright (c) 2000, 2013, Oracle and/or its affiliates. All rights reserved.
* ORACLE 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.text.
AttributedCharacterIterator.
Attribute;
import java.util.*;
import javax.swing.*;
/**
* <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™
* 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<
Attribute,
Object>
attrs =
getAttributes(
offset);
if (
attrs != null &&
attrs.
size() > 0) {
ArrayList<
Attribute>
al = new
ArrayList<
Attribute>();
al.
addAll(
attrs.
keySet());
return
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<
Attribute,
Object>
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 navigable 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
* {@code rh.length} is corrected as to preserve trailing literals.
* 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;
JTextComponent c =
getFormattedTextField();
if (
tl == 0 &&
rh.
length == 1 &&
c.
getSelectionStart() !=
rh.
offset) {
// Backspace, adjust to actually delete next non-literal.
rh.
offset =
getNextNonliteralIndex(
rh.
offset, -1);
} else if (
getOverwriteMode()) {
int
pos =
rh.
offset;
int
textPos =
pos;
boolean
overflown = false;
for (int
i = 0;
i <
rh.
length;
i++) {
while (
isLiteral(
pos))
pos++;
if (
pos >=
string.
length()) {
pos =
textPos;
overflown = true;
break;
}
textPos = ++
pos;
}
if (
overflown ||
c.
getSelectedText() == null) {
rh.
length =
pos -
rh.
offset;
}
}
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 occurrences 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;
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();
}
}
}
}
}