1. /*
  2. * @(#)NumberFormatter.java 1.10 03/12/19
  3. *
  4. * Copyright 2004 Sun Microsystems, Inc. All rights reserved.
  5. * SUN PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
  6. */
  7. package javax.swing.text;
  8. import java.lang.reflect.*;
  9. import java.text.*;
  10. import java.util.*;
  11. import javax.swing.text.*;
  12. /**
  13. * <code>NumberFormatter</code> subclasses <code>InternationalFormatter</code>
  14. * adding special behavior for numbers. Among the specializations are
  15. * (these are only used if the <code>NumberFormatter</code> does not display
  16. * invalid nubers, eg <code>setAllowsInvalid(false)</code>):
  17. * <ul>
  18. * <li>Pressing +/- (- is determined from the
  19. * <code>DecimalFormatSymbols</code> associated with the
  20. * <code>DecimalFormat</code>) in any field but the exponent
  21. * field will attempt to change the sign of the number to
  22. * positive/negative.
  23. * <li>Pressing +/- (- is determined from the
  24. * <code>DecimalFormatSymbols</code> associated with the
  25. * <code>DecimalFormat</code>) in the exponent field will
  26. * attemp to change the sign of the exponent to positive/negative.
  27. * </ul>
  28. * <p>
  29. * If you are displaying scientific numbers, you may wish to turn on
  30. * overwrite mode, <code>setOverwriteMode(true)</code>. For example:
  31. * <pre>
  32. * DecimalFormat decimalFormat = new DecimalFormat("0.000E0");
  33. * NumberFormatter textFormatter = new NumberFormatter(decimalFormat);
  34. * textFormatter.setOverwriteMode(true);
  35. * textFormatter.setAllowsInvalid(false);
  36. * </pre>
  37. * <p>
  38. * If you are going to allow the user to enter decimal
  39. * values, you should either force the DecimalFormat to contain at least
  40. * one decimal (<code>#.0###</code>), or allow the value to be invalid
  41. * <code>setAllowsInvalid(true)</code>. Otherwise users may not be able to
  42. * input decimal values.
  43. * <p>
  44. * <code>NumberFormatter</code> provides slightly different behavior to
  45. * <code>stringToValue</code> than that of its superclass. If you have
  46. * specified a Class for values, {@link #setValueClass}, that is one of
  47. * of <code>Integer</code>, <code>Long</code>, <code>Float</code>,
  48. * <code>Double</code>, <code>Byte</code> or <code>Short</code> and
  49. * the Format's <code>parseObject</code> returns an instance of
  50. * <code>Number</code>, the corresponding instance of the value class
  51. * will be created using the constructor appropriate for the primitive
  52. * type the value class represents. For example:
  53. * <code>setValueClass(Integer.class)</code> will cause the resulting
  54. * value to be created via
  55. * <code>new Integer(((Number)formatter.parseObject(string)).intValue())</code>.
  56. * This is typically useful if you
  57. * wish to set a min/max value as the various <code>Number</code>
  58. * implementations are generally not comparable to each other. This is also
  59. * useful if for some reason you need a specific <code>Number</code>
  60. * implementation for your values.
  61. * <p>
  62. * <strong>Warning:</strong>
  63. * Serialized objects of this class will not be compatible with
  64. * future Swing releases. The current serialization support is
  65. * appropriate for short term storage or RMI between applications running
  66. * the same version of Swing. As of 1.4, support for long term storage
  67. * of all JavaBeans<sup><font size="-2">TM</font></sup>
  68. * has been added to the <code>java.beans</code> package.
  69. * Please see {@link java.beans.XMLEncoder}.
  70. *
  71. * @version 1.4 03/05/01
  72. * @since 1.4
  73. */
  74. public class NumberFormatter extends InternationalFormatter {
  75. /** The special characters from the Format instance. */
  76. private String specialChars;
  77. /**
  78. * Creates a <code>NumberFormatter</code> with the a default
  79. * <code>NumberFormat</code> instance obtained from
  80. * <code>NumberFormat.getNumberInstance()</code>.
  81. */
  82. public NumberFormatter() {
  83. this(NumberFormat.getNumberInstance());
  84. }
  85. /**
  86. * Creates a NumberFormatter with the specified Format instance.
  87. *
  88. * @param format Format used to dictate legal values
  89. */
  90. public NumberFormatter(NumberFormat format) {
  91. super(format);
  92. setFormat(format);
  93. setAllowsInvalid(true);
  94. setCommitsOnValidEdit(false);
  95. setOverwriteMode(false);
  96. }
  97. /**
  98. * Sets the format that dictates the legal values that can be edited
  99. * and displayed.
  100. * <p>
  101. * If you have used the nullary constructor the value of this property
  102. * will be determined for the current locale by way of the
  103. * <code>NumberFormat.getNumberInstance()</code> method.
  104. *
  105. * @param format NumberFormat instance used to dictate legal values
  106. */
  107. public void setFormat(Format format) {
  108. super.setFormat(format);
  109. DecimalFormatSymbols dfs = getDecimalFormatSymbols();
  110. if (dfs != null) {
  111. StringBuffer sb = new StringBuffer();
  112. sb.append(dfs.getCurrencySymbol());
  113. sb.append(dfs.getDecimalSeparator());
  114. sb.append(dfs.getGroupingSeparator());
  115. sb.append(dfs.getInfinity());
  116. sb.append(dfs.getInternationalCurrencySymbol());
  117. sb.append(dfs.getMinusSign());
  118. sb.append(dfs.getMonetaryDecimalSeparator());
  119. sb.append(dfs.getNaN());
  120. sb.append(dfs.getPercent());
  121. sb.append('+');
  122. specialChars = sb.toString();
  123. }
  124. else {
  125. specialChars = "";
  126. }
  127. }
  128. /**
  129. * Invokes <code>parseObject</code> on <code>f</code>, returning
  130. * its value.
  131. */
  132. Object stringToValue(String text, Format f) throws ParseException {
  133. if (f == null) {
  134. return text;
  135. }
  136. Object value = f.parseObject(text);
  137. return convertValueToValueClass(value, getValueClass());
  138. }
  139. /**
  140. * Converts the passed in value to the passed in class. This only
  141. * works if <code>valueClass</code> is one of <code>Integer</code>,
  142. * <code>Long</code>, <code>Float</code>, <code>Double</code>,
  143. * <code>Byte</code> or <code>Short</code> and <code>value</code>
  144. * is an instanceof <code>Number</code>.
  145. */
  146. private Object convertValueToValueClass(Object value, Class valueClass) {
  147. if (valueClass != null && (value instanceof Number)) {
  148. if (valueClass == Integer.class) {
  149. return new Integer(((Number)value).intValue());
  150. }
  151. else if (valueClass == Long.class) {
  152. return new Long(((Number)value).longValue());
  153. }
  154. else if (valueClass == Float.class) {
  155. return new Float(((Number)value).floatValue());
  156. }
  157. else if (valueClass == Double.class) {
  158. return new Double(((Number)value).doubleValue());
  159. }
  160. else if (valueClass == Byte.class) {
  161. return new Byte(((Number)value).byteValue());
  162. }
  163. else if (valueClass == Short.class) {
  164. return new Short(((Number)value).shortValue());
  165. }
  166. }
  167. return value;
  168. }
  169. /**
  170. * Returns the character that is used to toggle to positive values.
  171. */
  172. private char getPositiveSign() {
  173. return '+';
  174. }
  175. /**
  176. * Returns the character that is used to toggle to negative values.
  177. */
  178. private char getMinusSign() {
  179. DecimalFormatSymbols dfs = getDecimalFormatSymbols();
  180. if (dfs != null) {
  181. return dfs.getMinusSign();
  182. }
  183. return '-';
  184. }
  185. /**
  186. * Returns the character that is used to toggle to negative values.
  187. */
  188. private char getDecimalSeparator() {
  189. DecimalFormatSymbols dfs = getDecimalFormatSymbols();
  190. if (dfs != null) {
  191. return dfs.getDecimalSeparator();
  192. }
  193. return '.';
  194. }
  195. /**
  196. * Returns the DecimalFormatSymbols from the Format instance.
  197. */
  198. private DecimalFormatSymbols getDecimalFormatSymbols() {
  199. Format f = getFormat();
  200. if (f instanceof DecimalFormat) {
  201. return ((DecimalFormat)f).getDecimalFormatSymbols();
  202. }
  203. return null;
  204. }
  205. /**
  206. */
  207. private boolean isValidInsertionCharacter(char aChar) {
  208. return (Character.isDigit(aChar) || specialChars.indexOf(aChar) != -1);
  209. }
  210. /**
  211. * Subclassed to return false if <code>text</code> contains in an invalid
  212. * character to insert, that is, it is not a digit
  213. * (<code>Character.isDigit()</code>) and
  214. * not one of the characters defined by the DecimalFormatSymbols.
  215. */
  216. boolean isLegalInsertText(String text) {
  217. if (getAllowsInvalid()) {
  218. return true;
  219. }
  220. for (int counter = text.length() - 1; counter >= 0; counter--) {
  221. char aChar = text.charAt(counter);
  222. if (!Character.isDigit(aChar) &&
  223. specialChars.indexOf(aChar) == -1){
  224. return false;
  225. }
  226. }
  227. return true;
  228. }
  229. /**
  230. * Subclassed to treat the decimal separator, grouping separator,
  231. * exponent symbol, percent, permille, currency and sign as literals.
  232. */
  233. boolean isLiteral(Map attrs) {
  234. if (!super.isLiteral(attrs)) {
  235. if (attrs == null) {
  236. return false;
  237. }
  238. int size = attrs.size();
  239. if (attrs.get(NumberFormat.Field.GROUPING_SEPARATOR) != null) {
  240. size--;
  241. if (attrs.get(NumberFormat.Field.INTEGER) != null) {
  242. size--;
  243. }
  244. }
  245. if (attrs.get(NumberFormat.Field.EXPONENT_SYMBOL) != null) {
  246. size--;
  247. }
  248. if (attrs.get(NumberFormat.Field.PERCENT) != null) {
  249. size--;
  250. }
  251. if (attrs.get(NumberFormat.Field.PERMILLE) != null) {
  252. size--;
  253. }
  254. if (attrs.get(NumberFormat.Field.CURRENCY) != null) {
  255. size--;
  256. }
  257. if (attrs.get(NumberFormat.Field.SIGN) != null) {
  258. size--;
  259. }
  260. if (size == 0) {
  261. return true;
  262. }
  263. return false;
  264. }
  265. return true;
  266. }
  267. /**
  268. * Subclassed to make the decimal separator navigatable, as well
  269. * as making the character between the integer field and the next
  270. * field navigatable.
  271. */
  272. boolean isNavigatable(int index) {
  273. if (!super.isNavigatable(index)) {
  274. // Don't skip the decimal, it causes wierd behavior
  275. if (getBufferedChar(index) == getDecimalSeparator()) {
  276. return true;
  277. }
  278. return false;
  279. }
  280. return true;
  281. }
  282. /**
  283. * Returns the first <code>NumberFormat.Field</code> starting
  284. * <code>index</code> incrementing by <code>direction</code>.
  285. */
  286. private NumberFormat.Field getFieldFrom(int index, int direction) {
  287. if (isValidMask()) {
  288. int max = getFormattedTextField().getDocument().getLength();
  289. AttributedCharacterIterator iterator = getIterator();
  290. if (index >= max) {
  291. index += direction;
  292. }
  293. while (index >= 0 && index < max) {
  294. iterator.setIndex(index);
  295. Map attrs = iterator.getAttributes();
  296. if (attrs != null && attrs.size() > 0) {
  297. Iterator keys = attrs.keySet().iterator();
  298. while (keys.hasNext()) {
  299. Object key = keys.next();
  300. if (key instanceof NumberFormat.Field) {
  301. return (NumberFormat.Field)key;
  302. }
  303. }
  304. }
  305. index += direction;
  306. }
  307. }
  308. return null;
  309. }
  310. /**
  311. * Overriden to toggle the value if the positive/minus sign
  312. * is inserted.
  313. */
  314. void replace(DocumentFilter.FilterBypass fb, int offset, int length,
  315. String string, AttributeSet attr) throws BadLocationException {
  316. if (!getAllowsInvalid() && length == 0 && string != null &&
  317. string.length() == 1 &&
  318. toggleSignIfNecessary(fb, offset, string.charAt(0))) {
  319. return;
  320. }
  321. super.replace(fb, offset, length, string, attr);
  322. }
  323. /**
  324. * Will change the sign of the integer or exponent field if
  325. * <code>aChar</code> is the positive or minus sign. Returns
  326. * true if a sign change was attempted.
  327. */
  328. private boolean toggleSignIfNecessary(DocumentFilter.FilterBypass fb,
  329. int offset, char aChar) throws
  330. BadLocationException {
  331. if (aChar == getMinusSign() || aChar == getPositiveSign()) {
  332. NumberFormat.Field field = getFieldFrom(offset, -1);
  333. Object newValue;
  334. try {
  335. if (field == null ||
  336. (field != NumberFormat.Field.EXPONENT &&
  337. field != NumberFormat.Field.EXPONENT_SYMBOL &&
  338. field != NumberFormat.Field.EXPONENT_SIGN)) {
  339. newValue = toggleSign((aChar == getPositiveSign()));
  340. }
  341. else {
  342. // exponent
  343. newValue = toggleExponentSign(offset, aChar);
  344. }
  345. if (newValue != null && isValidValue(newValue, false)) {
  346. int lc = getLiteralCountTo(offset);
  347. String string = valueToString(newValue);
  348. fb.remove(0, fb.getDocument().getLength());
  349. fb.insertString(0, string, null);
  350. updateValue(newValue);
  351. repositionCursor(getLiteralCountTo(offset) -
  352. lc + offset, 1);
  353. return true;
  354. }
  355. } catch (ParseException pe) {
  356. invalidEdit();
  357. }
  358. }
  359. return false;
  360. }
  361. /**
  362. * Returns true if the range offset to length identifies the only
  363. * integer field.
  364. */
  365. private boolean isOnlyIntegerField(int offset, int length) {
  366. if (isValidMask()) {
  367. int start = getAttributeStart(NumberFormat.Field.INTEGER);
  368. if (start != -1) {
  369. AttributedCharacterIterator iterator = getIterator();
  370. iterator.setIndex(start);
  371. if (offset > start || iterator.getRunLimit(
  372. NumberFormat.Field.INTEGER) > (offset + length)) {
  373. return false;
  374. }
  375. return true;
  376. }
  377. }
  378. return false;
  379. }
  380. /**
  381. * Invoked to toggle the sign. For this to work the value class
  382. * must have a single arg constructor that takes a String.
  383. */
  384. private Object toggleSign(boolean positive) throws ParseException {
  385. Object value = stringToValue(getFormattedTextField().getText());
  386. if (value != null) {
  387. // toString isn't localized, so that using +/- should work
  388. // correctly.
  389. String string = value.toString();
  390. if (string != null && string.length() > 0) {
  391. if (positive) {
  392. if (string.charAt(0) == '-') {
  393. string = string.substring(1);
  394. }
  395. }
  396. else {
  397. if (string.charAt(0) == '+') {
  398. string = string.substring(1);
  399. }
  400. if (string.length() > 0 && string.charAt(0) != '-') {
  401. string = "-" + string;
  402. }
  403. }
  404. if (string != null) {
  405. Class valueClass = getValueClass();
  406. if (valueClass == null) {
  407. valueClass = value.getClass();
  408. }
  409. try {
  410. Constructor cons = valueClass.getConstructor(
  411. new Class[] { String.class });
  412. if (cons != null) {
  413. return cons.newInstance(new Object[]{string});
  414. }
  415. } catch (Throwable ex) { }
  416. }
  417. }
  418. }
  419. return null;
  420. }
  421. /**
  422. * Invoked to toggle the sign of the exponent (for scientific
  423. * numbers).
  424. */
  425. private Object toggleExponentSign(int offset, char aChar) throws
  426. BadLocationException, ParseException {
  427. String string = getFormattedTextField().getText();
  428. int replaceLength = 0;
  429. int loc = getAttributeStart(NumberFormat.Field.EXPONENT_SIGN);
  430. if (loc >= 0) {
  431. replaceLength = 1;
  432. offset = loc;
  433. }
  434. if (aChar == getPositiveSign()) {
  435. string = getReplaceString(offset, replaceLength, null);
  436. }
  437. else {
  438. string = getReplaceString(offset, replaceLength,
  439. new String(new char[] { aChar }));
  440. }
  441. return stringToValue(string);
  442. }
  443. }