1. /*
  2. * @(#)NumberFormatter.java 1.8 03/01/23
  3. *
  4. * Copyright 2003 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 and currency 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 (size == 0) {
  258. return true;
  259. }
  260. return false;
  261. }
  262. return true;
  263. }
  264. /**
  265. * Subclassed to make the decimal separator navigatable, as well
  266. * as making the character between the integer field and the next
  267. * field navigatable.
  268. */
  269. boolean isNavigatable(int index) {
  270. if (!super.isNavigatable(index)) {
  271. // Don't skip the decimal, it causes wierd behavior
  272. if (getBufferedChar(index) == getDecimalSeparator()) {
  273. return true;
  274. }
  275. return false;
  276. }
  277. return true;
  278. }
  279. /**
  280. * Returns the first <code>NumberFormat.Field</code> starting
  281. * <code>index</code> incrementing by <code>direction</code>.
  282. */
  283. private NumberFormat.Field getFieldFrom(int index, int direction) {
  284. if (isValidMask()) {
  285. int max = getFormattedTextField().getDocument().getLength();
  286. AttributedCharacterIterator iterator = getIterator();
  287. if (index >= max) {
  288. index += direction;
  289. }
  290. while (index >= 0 && index < max) {
  291. iterator.setIndex(index);
  292. Map attrs = iterator.getAttributes();
  293. if (attrs != null && attrs.size() > 0) {
  294. Iterator keys = attrs.keySet().iterator();
  295. while (keys.hasNext()) {
  296. Object key = keys.next();
  297. if (key instanceof NumberFormat.Field) {
  298. return (NumberFormat.Field)key;
  299. }
  300. }
  301. }
  302. index += direction;
  303. }
  304. }
  305. return null;
  306. }
  307. /**
  308. * Overriden to toggle the value if the positive/minus sign
  309. * is inserted.
  310. */
  311. void replace(DocumentFilter.FilterBypass fb, int offset, int length,
  312. String string, AttributeSet attr) throws BadLocationException {
  313. if (!getAllowsInvalid() && length == 0 && string != null &&
  314. string.length() == 1 &&
  315. toggleSignIfNecessary(fb, offset, string.charAt(0))) {
  316. return;
  317. }
  318. super.replace(fb, offset, length, string, attr);
  319. }
  320. /**
  321. * Will change the sign of the integer or exponent field if
  322. * <code>aChar</code> is the positive or minus sign. Returns
  323. * true if a sign change was attempted.
  324. */
  325. private boolean toggleSignIfNecessary(DocumentFilter.FilterBypass fb,
  326. int offset, char aChar) throws
  327. BadLocationException {
  328. if (aChar == getMinusSign() || aChar == getPositiveSign()) {
  329. NumberFormat.Field field = getFieldFrom(offset, -1);
  330. Object newValue;
  331. try {
  332. if (field == null ||
  333. (field != NumberFormat.Field.EXPONENT &&
  334. field != NumberFormat.Field.EXPONENT_SYMBOL &&
  335. field != NumberFormat.Field.EXPONENT_SIGN)) {
  336. newValue = toggleSign((aChar == getPositiveSign()));
  337. }
  338. else {
  339. // exponent
  340. newValue = toggleExponentSign(offset, aChar);
  341. }
  342. if (newValue != null && isValidValue(newValue, false)) {
  343. int lc = getLiteralCountTo(offset);
  344. String string = valueToString(newValue);
  345. fb.remove(0, fb.getDocument().getLength());
  346. fb.insertString(0, string, null);
  347. updateValue(newValue);
  348. repositionCursor(getLiteralCountTo(offset) -
  349. lc + offset, 1);
  350. return true;
  351. }
  352. } catch (ParseException pe) {
  353. invalidEdit();
  354. }
  355. }
  356. return false;
  357. }
  358. /**
  359. * Returns true if the range offset to length identifies the only
  360. * integer field.
  361. */
  362. private boolean isOnlyIntegerField(int offset, int length) {
  363. if (isValidMask()) {
  364. int start = getAttributeStart(NumberFormat.Field.INTEGER);
  365. if (start != -1) {
  366. AttributedCharacterIterator iterator = getIterator();
  367. iterator.setIndex(start);
  368. if (offset > start || iterator.getRunLimit(
  369. NumberFormat.Field.INTEGER) > (offset + length)) {
  370. return false;
  371. }
  372. return true;
  373. }
  374. }
  375. return false;
  376. }
  377. /**
  378. * Invoked to toggle the sign. For this to work the value class
  379. * must have a single arg constructor that takes a String.
  380. */
  381. private Object toggleSign(boolean positive) throws ParseException {
  382. Object value = stringToValue(getFormattedTextField().getText());
  383. if (value != null) {
  384. // toString isn't localized, so that using +/- should work
  385. // correctly.
  386. String string = value.toString();
  387. if (string != null && string.length() > 0) {
  388. if (positive) {
  389. if (string.charAt(0) == '-') {
  390. string = string.substring(1);
  391. }
  392. }
  393. else {
  394. if (string.charAt(0) == '+') {
  395. string = string.substring(1);
  396. }
  397. if (string.length() > 0 && string.charAt(0) != '-') {
  398. string = "-" + string;
  399. }
  400. }
  401. if (string != null) {
  402. Class valueClass = getValueClass();
  403. if (valueClass == null) {
  404. valueClass = value.getClass();
  405. }
  406. try {
  407. Constructor cons = valueClass.getConstructor(
  408. new Class[] { String.class });
  409. if (cons != null) {
  410. return cons.newInstance(new Object[]{string});
  411. }
  412. } catch (Throwable ex) { }
  413. }
  414. }
  415. }
  416. return null;
  417. }
  418. /**
  419. * Invoked to toggle the sign of the exponent (for scientific
  420. * numbers).
  421. */
  422. private Object toggleExponentSign(int offset, char aChar) throws
  423. BadLocationException, ParseException {
  424. String string = getFormattedTextField().getText();
  425. int replaceLength = 0;
  426. int loc = getAttributeStart(NumberFormat.Field.EXPONENT_SIGN);
  427. if (loc >= 0) {
  428. replaceLength = 1;
  429. offset = loc;
  430. }
  431. if (aChar == getPositiveSign()) {
  432. string = getReplaceString(offset, replaceLength, null);
  433. }
  434. else {
  435. string = getReplaceString(offset, replaceLength,
  436. new String(new char[] { aChar }));
  437. }
  438. return stringToValue(string);
  439. }
  440. }