You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
471 lines
16 KiB
471 lines
16 KiB
/*
|
|
* 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.lang.reflect.*;
|
|
import java.text.*;
|
|
import java.util.*;
|
|
import sun.reflect.misc.ReflectUtil;
|
|
import sun.swing.SwingUtilities2;
|
|
|
|
/**
|
|
* <code>NumberFormatter</code> subclasses <code>InternationalFormatter</code>
|
|
* adding special behavior for numbers. Among the specializations are
|
|
* (these are only used if the <code>NumberFormatter</code> does not display
|
|
* invalid numbers, for example, <code>setAllowsInvalid(false)</code>):
|
|
* <ul>
|
|
* <li>Pressing +/- (- is determined from the
|
|
* <code>DecimalFormatSymbols</code> associated with the
|
|
* <code>DecimalFormat</code>) in any field but the exponent
|
|
* field will attempt to change the sign of the number to
|
|
* positive/negative.
|
|
* <li>Pressing +/- (- is determined from the
|
|
* <code>DecimalFormatSymbols</code> associated with the
|
|
* <code>DecimalFormat</code>) in the exponent field will
|
|
* attempt to change the sign of the exponent to positive/negative.
|
|
* </ul>
|
|
* <p>
|
|
* If you are displaying scientific numbers, you may wish to turn on
|
|
* overwrite mode, <code>setOverwriteMode(true)</code>. For example:
|
|
* <pre>
|
|
* DecimalFormat decimalFormat = new DecimalFormat("0.000E0");
|
|
* NumberFormatter textFormatter = new NumberFormatter(decimalFormat);
|
|
* textFormatter.setOverwriteMode(true);
|
|
* textFormatter.setAllowsInvalid(false);
|
|
* </pre>
|
|
* <p>
|
|
* If you are going to allow the user to enter decimal
|
|
* values, you should either force the DecimalFormat to contain at least
|
|
* one decimal (<code>#.0###</code>), or allow the value to be invalid
|
|
* <code>setAllowsInvalid(true)</code>. Otherwise users may not be able to
|
|
* input decimal values.
|
|
* <p>
|
|
* <code>NumberFormatter</code> provides slightly different behavior to
|
|
* <code>stringToValue</code> than that of its superclass. If you have
|
|
* specified a Class for values, {@link #setValueClass}, that is one of
|
|
* of <code>Integer</code>, <code>Long</code>, <code>Float</code>,
|
|
* <code>Double</code>, <code>Byte</code> or <code>Short</code> and
|
|
* the Format's <code>parseObject</code> returns an instance of
|
|
* <code>Number</code>, the corresponding instance of the value class
|
|
* will be created using the constructor appropriate for the primitive
|
|
* type the value class represents. For example:
|
|
* <code>setValueClass(Integer.class)</code> will cause the resulting
|
|
* value to be created via
|
|
* <code>new Integer(((Number)formatter.parseObject(string)).intValue())</code>.
|
|
* This is typically useful if you
|
|
* wish to set a min/max value as the various <code>Number</code>
|
|
* implementations are generally not comparable to each other. This is also
|
|
* useful if for some reason you need a specific <code>Number</code>
|
|
* implementation for your values.
|
|
* <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}.
|
|
*
|
|
* @since 1.4
|
|
*/
|
|
public class NumberFormatter extends InternationalFormatter {
|
|
/** The special characters from the Format instance. */
|
|
private String specialChars;
|
|
|
|
/**
|
|
* Creates a <code>NumberFormatter</code> with the a default
|
|
* <code>NumberFormat</code> instance obtained from
|
|
* <code>NumberFormat.getNumberInstance()</code>.
|
|
*/
|
|
public NumberFormatter() {
|
|
this(NumberFormat.getNumberInstance());
|
|
}
|
|
|
|
/**
|
|
* Creates a NumberFormatter with the specified Format instance.
|
|
*
|
|
* @param format Format used to dictate legal values
|
|
*/
|
|
public NumberFormatter(NumberFormat format) {
|
|
super(format);
|
|
setFormat(format);
|
|
setAllowsInvalid(true);
|
|
setCommitsOnValidEdit(false);
|
|
setOverwriteMode(false);
|
|
}
|
|
|
|
/**
|
|
* Sets the format that dictates the legal values that can be edited
|
|
* and displayed.
|
|
* <p>
|
|
* If you have used the nullary constructor the value of this property
|
|
* will be determined for the current locale by way of the
|
|
* <code>NumberFormat.getNumberInstance()</code> method.
|
|
*
|
|
* @param format NumberFormat instance used to dictate legal values
|
|
*/
|
|
public void setFormat(Format format) {
|
|
super.setFormat(format);
|
|
|
|
DecimalFormatSymbols dfs = getDecimalFormatSymbols();
|
|
|
|
if (dfs != null) {
|
|
StringBuilder sb = new StringBuilder();
|
|
|
|
sb.append(dfs.getCurrencySymbol());
|
|
sb.append(dfs.getDecimalSeparator());
|
|
sb.append(dfs.getGroupingSeparator());
|
|
sb.append(dfs.getInfinity());
|
|
sb.append(dfs.getInternationalCurrencySymbol());
|
|
sb.append(dfs.getMinusSign());
|
|
sb.append(dfs.getMonetaryDecimalSeparator());
|
|
sb.append(dfs.getNaN());
|
|
sb.append(dfs.getPercent());
|
|
sb.append('+');
|
|
specialChars = sb.toString();
|
|
}
|
|
else {
|
|
specialChars = "";
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Invokes <code>parseObject</code> on <code>f</code>, returning
|
|
* its value.
|
|
*/
|
|
Object stringToValue(String text, Format f) throws ParseException {
|
|
if (f == null) {
|
|
return text;
|
|
}
|
|
Object value = f.parseObject(text);
|
|
|
|
return convertValueToValueClass(value, getValueClass());
|
|
}
|
|
|
|
/**
|
|
* Converts the passed in value to the passed in class. This only
|
|
* works if <code>valueClass</code> is one of <code>Integer</code>,
|
|
* <code>Long</code>, <code>Float</code>, <code>Double</code>,
|
|
* <code>Byte</code> or <code>Short</code> and <code>value</code>
|
|
* is an instanceof <code>Number</code>.
|
|
*/
|
|
private Object convertValueToValueClass(Object value, Class valueClass) {
|
|
if (valueClass != null && (value instanceof Number)) {
|
|
Number numberValue = (Number)value;
|
|
if (valueClass == Integer.class) {
|
|
return Integer.valueOf(numberValue.intValue());
|
|
}
|
|
else if (valueClass == Long.class) {
|
|
return Long.valueOf(numberValue.longValue());
|
|
}
|
|
else if (valueClass == Float.class) {
|
|
return Float.valueOf(numberValue.floatValue());
|
|
}
|
|
else if (valueClass == Double.class) {
|
|
return Double.valueOf(numberValue.doubleValue());
|
|
}
|
|
else if (valueClass == Byte.class) {
|
|
return Byte.valueOf(numberValue.byteValue());
|
|
}
|
|
else if (valueClass == Short.class) {
|
|
return Short.valueOf(numberValue.shortValue());
|
|
}
|
|
}
|
|
return value;
|
|
}
|
|
|
|
/**
|
|
* Returns the character that is used to toggle to positive values.
|
|
*/
|
|
private char getPositiveSign() {
|
|
return '+';
|
|
}
|
|
|
|
/**
|
|
* Returns the character that is used to toggle to negative values.
|
|
*/
|
|
private char getMinusSign() {
|
|
DecimalFormatSymbols dfs = getDecimalFormatSymbols();
|
|
|
|
if (dfs != null) {
|
|
return dfs.getMinusSign();
|
|
}
|
|
return '-';
|
|
}
|
|
|
|
/**
|
|
* Returns the character that is used to toggle to negative values.
|
|
*/
|
|
private char getDecimalSeparator() {
|
|
DecimalFormatSymbols dfs = getDecimalFormatSymbols();
|
|
|
|
if (dfs != null) {
|
|
return dfs.getDecimalSeparator();
|
|
}
|
|
return '.';
|
|
}
|
|
|
|
/**
|
|
* Returns the DecimalFormatSymbols from the Format instance.
|
|
*/
|
|
private DecimalFormatSymbols getDecimalFormatSymbols() {
|
|
Format f = getFormat();
|
|
|
|
if (f instanceof DecimalFormat) {
|
|
return ((DecimalFormat)f).getDecimalFormatSymbols();
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Subclassed to return false if <code>text</code> contains in an invalid
|
|
* character to insert, that is, it is not a digit
|
|
* (<code>Character.isDigit()</code>) and
|
|
* not one of the characters defined by the DecimalFormatSymbols.
|
|
*/
|
|
boolean isLegalInsertText(String text) {
|
|
if (getAllowsInvalid()) {
|
|
return true;
|
|
}
|
|
for (int counter = text.length() - 1; counter >= 0; counter--) {
|
|
char aChar = text.charAt(counter);
|
|
|
|
if (!Character.isDigit(aChar) &&
|
|
specialChars.indexOf(aChar) == -1){
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Subclassed to treat the decimal separator, grouping separator,
|
|
* exponent symbol, percent, permille, currency and sign as literals.
|
|
*/
|
|
boolean isLiteral(Map attrs) {
|
|
if (!super.isLiteral(attrs)) {
|
|
if (attrs == null) {
|
|
return false;
|
|
}
|
|
int size = attrs.size();
|
|
|
|
if (attrs.get(NumberFormat.Field.GROUPING_SEPARATOR) != null) {
|
|
size--;
|
|
if (attrs.get(NumberFormat.Field.INTEGER) != null) {
|
|
size--;
|
|
}
|
|
}
|
|
if (attrs.get(NumberFormat.Field.EXPONENT_SYMBOL) != null) {
|
|
size--;
|
|
}
|
|
if (attrs.get(NumberFormat.Field.PERCENT) != null) {
|
|
size--;
|
|
}
|
|
if (attrs.get(NumberFormat.Field.PERMILLE) != null) {
|
|
size--;
|
|
}
|
|
if (attrs.get(NumberFormat.Field.CURRENCY) != null) {
|
|
size--;
|
|
}
|
|
if (attrs.get(NumberFormat.Field.SIGN) != null) {
|
|
size--;
|
|
}
|
|
return size == 0;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Subclassed to make the decimal separator navigable, as well
|
|
* as making the character between the integer field and the next
|
|
* field navigable.
|
|
*/
|
|
boolean isNavigatable(int index) {
|
|
if (!super.isNavigatable(index)) {
|
|
// Don't skip the decimal, it causes wierd behavior
|
|
return getBufferedChar(index) == getDecimalSeparator();
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Returns the first <code>NumberFormat.Field</code> starting
|
|
* <code>index</code> incrementing by <code>direction</code>.
|
|
*/
|
|
private NumberFormat.Field getFieldFrom(int index, int direction) {
|
|
if (isValidMask()) {
|
|
int max = getFormattedTextField().getDocument().getLength();
|
|
AttributedCharacterIterator iterator = getIterator();
|
|
|
|
if (index >= max) {
|
|
index += direction;
|
|
}
|
|
while (index >= 0 && index < max) {
|
|
iterator.setIndex(index);
|
|
|
|
Map attrs = iterator.getAttributes();
|
|
|
|
if (attrs != null && attrs.size() > 0) {
|
|
for (Object key : attrs.keySet()) {
|
|
if (key instanceof NumberFormat.Field) {
|
|
return (NumberFormat.Field)key;
|
|
}
|
|
}
|
|
}
|
|
index += direction;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Overriden to toggle the value if the positive/minus sign
|
|
* is inserted.
|
|
*/
|
|
void replace(DocumentFilter.FilterBypass fb, int offset, int length,
|
|
String string, AttributeSet attr) throws BadLocationException {
|
|
if (!getAllowsInvalid() && length == 0 && string != null &&
|
|
string.length() == 1 &&
|
|
toggleSignIfNecessary(fb, offset, string.charAt(0))) {
|
|
return;
|
|
}
|
|
super.replace(fb, offset, length, string, attr);
|
|
}
|
|
|
|
/**
|
|
* Will change the sign of the integer or exponent field if
|
|
* <code>aChar</code> is the positive or minus sign. Returns
|
|
* true if a sign change was attempted.
|
|
*/
|
|
private boolean toggleSignIfNecessary(DocumentFilter.FilterBypass fb,
|
|
int offset, char aChar) throws
|
|
BadLocationException {
|
|
if (aChar == getMinusSign() || aChar == getPositiveSign()) {
|
|
NumberFormat.Field field = getFieldFrom(offset, -1);
|
|
Object newValue;
|
|
|
|
try {
|
|
if (field == null ||
|
|
(field != NumberFormat.Field.EXPONENT &&
|
|
field != NumberFormat.Field.EXPONENT_SYMBOL &&
|
|
field != NumberFormat.Field.EXPONENT_SIGN)) {
|
|
newValue = toggleSign((aChar == getPositiveSign()));
|
|
}
|
|
else {
|
|
// exponent
|
|
newValue = toggleExponentSign(offset, aChar);
|
|
}
|
|
if (newValue != null && isValidValue(newValue, false)) {
|
|
int lc = getLiteralCountTo(offset);
|
|
String string = valueToString(newValue);
|
|
|
|
fb.remove(0, fb.getDocument().getLength());
|
|
fb.insertString(0, string, null);
|
|
updateValue(newValue);
|
|
repositionCursor(getLiteralCountTo(offset) -
|
|
lc + offset, 1);
|
|
return true;
|
|
}
|
|
} catch (ParseException pe) {
|
|
invalidEdit();
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Invoked to toggle the sign. For this to work the value class
|
|
* must have a single arg constructor that takes a String.
|
|
*/
|
|
private Object toggleSign(boolean positive) throws ParseException {
|
|
Object value = stringToValue(getFormattedTextField().getText());
|
|
|
|
if (value != null) {
|
|
// toString isn't localized, so that using +/- should work
|
|
// correctly.
|
|
String string = value.toString();
|
|
|
|
if (string != null && string.length() > 0) {
|
|
if (positive) {
|
|
if (string.charAt(0) == '-') {
|
|
string = string.substring(1);
|
|
}
|
|
}
|
|
else {
|
|
if (string.charAt(0) == '+') {
|
|
string = string.substring(1);
|
|
}
|
|
if (string.length() > 0 && string.charAt(0) != '-') {
|
|
string = "-" + string;
|
|
}
|
|
}
|
|
if (string != null) {
|
|
Class<?> valueClass = getValueClass();
|
|
|
|
if (valueClass == null) {
|
|
valueClass = value.getClass();
|
|
}
|
|
try {
|
|
ReflectUtil.checkPackageAccess(valueClass);
|
|
SwingUtilities2.checkAccess(valueClass.getModifiers());
|
|
Constructor cons = valueClass.getConstructor(
|
|
new Class[] { String.class });
|
|
if (cons != null) {
|
|
SwingUtilities2.checkAccess(cons.getModifiers());
|
|
return cons.newInstance(new Object[]{string});
|
|
}
|
|
} catch (Throwable ex) { }
|
|
}
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Invoked to toggle the sign of the exponent (for scientific
|
|
* numbers).
|
|
*/
|
|
private Object toggleExponentSign(int offset, char aChar) throws
|
|
BadLocationException, ParseException {
|
|
String string = getFormattedTextField().getText();
|
|
int replaceLength = 0;
|
|
int loc = getAttributeStart(NumberFormat.Field.EXPONENT_SIGN);
|
|
|
|
if (loc >= 0) {
|
|
replaceLength = 1;
|
|
offset = loc;
|
|
}
|
|
if (aChar == getPositiveSign()) {
|
|
string = getReplaceString(offset, replaceLength, null);
|
|
}
|
|
else {
|
|
string = getReplaceString(offset, replaceLength,
|
|
new String(new char[] { aChar }));
|
|
}
|
|
return stringToValue(string);
|
|
}
|
|
}
|