/*
* Copyright (c) 2000, 2010, Oracle and/or its affiliates. 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. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle 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 Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.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.text.AttributedCharacterIterator.Attribute;
import java.util.*;
import javax.swing.*;
/**
* InternationalFormatter
extends DefaultFormatter
,
* using an instance of java.text.Format
to handle the
* conversion to a String, and the conversion from a String.
*
* If getAllowsInvalid()
is false, this will ask the
* Format
to format the current text on every edit.
*
* You can specify a minimum and maximum value by way of the
* setMinimum
and setMaximum
methods. In order
* for this to work the values returned from stringToValue
must be
* comparable to the min/max values by way of the Comparable
* interface.
*
* Be careful how you configure the Format
and the
* InternationalFormatter
, as it is possible to create a
* situation where certain values can not be input. Consider the date
* format 'M/d/yy', an InternationalFormatter
that is always
* valid (setAllowsInvalid(false)
), is in overwrite mode
* (setOverwriteMode(true)
) 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'.
*
* If InternationalFormatter
is configured to only allow valid
* values (setAllowsInvalid(false)
), every valid edit will result
* in the text of the JFormattedTextField
being completely reset
* from the Format
.
* The cursor position will also be adjusted as literal characters are
* added/removed from the resulting String.
*
* InternationalFormatter
's behavior of
* stringToValue
is slightly different than that of
* DefaultTextFormatter
, it does the following:
*
parseObject
is invoked on the Format
* specified by setFormat
* setValueClass
),
* supers implementation is invoked to convert the value returned
* from parseObject
to the appropriate class.
* ParseException
has not been thrown, and the value
* is outside the min/max a ParseException
is thrown.
* InternationalFormatter
implements stringToValue
* in this manner so that you can specify an alternate Class than
* Format
may return.
*
* Warning:
* 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 JavaBeansTM
* has been added to the java.beans
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 getFields
.
*/
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;
/**
* InternationalFormatter
's behavior is dicatated by a
* AttributedCharacterIterator
that is obtained from
* the Format
. On every edit, assuming
* allows invalid is false, the Format
instance is invoked
* with formatToCharacterIterator
. A BitSet
is
* also kept upto date with the non-literal characters, that is
* for every index in the AttributedCharacterIterator
an
* entry in the bit set is updated based on the return value from
* isLiteral(Map)
. isLiteral(int)
then uses
* this cached information.
*
* If allowsInvalid is false, every edit results in resetting the complete * text of the JTextComponent. *
* InternationalFormatterFilter can also provide two actions suitable for
* incrementing and decrementing. To enable this a subclass must
* override getSupportsIncrement
to return true, and
* override adjustValue
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
* canIncrement
.
*/
/**
* 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 InternationalFormatter
with no
* Format
specified.
*/
public InternationalFormatter() {
setOverwriteMode(false);
}
/**
* Creates an InternationalFormatter
with the specified
* Format
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 Format
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 valueClass
has
* not been specified, and minimum
is non null, the
* valueClass
will be set to that of the class of
* minimum
.
*
* @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 valueClass
has
* not been specified, and max
is non null, the
* valueClass
will be set to that of the class of
* max
.
*
* @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 DefaultFormatter
onto a particular
* JFormattedTextField
.
* This will invoke valueToString
to convert the
* current value from the JFormattedTextField
to
* a String. This will then install the Action
s from
* getActions
, the DocumentFilter
* returned from getDocumentFilter
and the
* NavigationFilter
returned from
* getNavigationFilter
onto the
* JFormattedTextField
.
*
* Subclasses will typically only need to override this if they
* wish to install additional listeners on the
* JFormattedTextField
.
*
* If there is a ParseException
in converting the
* current value to a String, this will set the text to an empty
* String, and mark the JFormattedTextField
as being
* in an invalid state.
*
* While this is a public method, this is typically only useful
* for subclassers of 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
* JFormattedTextField
.
* JFormattedTextField
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 value
.
* This invokes format
on the current Format
.
*
* @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 Object
representation of the
* String
text
.
*
* @param text String
to convert
* @return Object
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 Format.Field
constants associated with
* the text at offset
. If offset
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();
}
MapgetSupportsIncrement
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 parseObject
on f
, returning
* its value.
*/
Object stringToValue(String text, Format f) throws ParseException {
if (f == null) {
return text;
}
return f.parseObject(text);
}
/**
* Returns true if value
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 index
.
*/
Mapid
. This will return -1
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 AttributedCharacterIterator
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
* formatToCharacterIterator
on the Format
.
* If this is successful,
* updateMask(AttributedCharacterIterator)
* 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 index
.
*/
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 isLiteral
, 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.
* repositionCursor
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. startLiteralCount
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 end
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 index
.
*/
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 attributes
is null or empty.
*/
boolean isLiteral(Map attributes) {
return ((attributes == null) || attributes.size() == 0);
}
/**
* Updates the interal bitset from iterator
. This will
* set validMask
to true if iterator
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 field
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 attributes
.
*/
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 f
before
* the location start
in the current
* AttributedCharacterIterator
.
*/
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. value
is the current value,
* attributes
gives the field the cursor is in (may be
* null depending upon canIncrement
) and
* direction
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
* adjustValue
.
*/
boolean getSupportsIncrement() {
return false;
}
/**
* Resets the value of the JFormattedTextField to be
* value
.
*/
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 ExtendedReplaceHolder
.
*/
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 adjustValue
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();
}
}
}
}
}