1. /*
  2. * @(#)DefaultFormatter.java 1.10 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.io.Serializable;
  9. import java.lang.reflect.*;
  10. import java.text.ParseException;
  11. import javax.swing.*;
  12. import javax.swing.text.*;
  13. /**
  14. * <code>DefaultFormatter</code> formats aribtrary objects. Formatting is done
  15. * by invoking the <code>toString</code> method. In order to convert the
  16. * value back to a String, your class must provide a constructor that
  17. * takes a String argument. If no single argument constructor that takes a
  18. * String is found, the returned value will be the String passed into
  19. * <code>stringToValue</code>.
  20. * <p>
  21. * Instances of <code>DefaultFormatter</code> can not be used in multiple
  22. * instances of <code>JFormattedTextField</code>. To obtain a copy of
  23. * an already configured <code>DefaultFormatter</code>, use the
  24. * <code>clone</code> method.
  25. * <p>
  26. * <strong>Warning:</strong>
  27. * Serialized objects of this class will not be compatible with
  28. * future Swing releases. The current serialization support is
  29. * appropriate for short term storage or RMI between applications running
  30. * the same version of Swing. As of 1.4, support for long term storage
  31. * of all JavaBeans<sup><font size="-2">TM</font></sup>
  32. * has been added to the <code>java.beans</code> package.
  33. * Please see {@link java.beans.XMLEncoder}.
  34. *
  35. * @see javax.swing.JFormattedTextField.AbstractFormatter
  36. *
  37. * @version 1.10 01/23/03
  38. * @since 1.4
  39. */
  40. public class DefaultFormatter extends JFormattedTextField.AbstractFormatter
  41. implements Cloneable, Serializable {
  42. /** Indicates if the value being edited must match the mask. */
  43. private boolean allowsInvalid;
  44. /** If true, editing mode is in overwrite (or strikethough). */
  45. private boolean overwriteMode;
  46. /** If true, any time a valid edit happens commitEdit is invoked. */
  47. private boolean commitOnEdit;
  48. /** Class used to create new instances. */
  49. private Class valueClass;
  50. /** NavigationFilter that forwards calls back to DefaultFormatter. */
  51. private NavigationFilter navigationFilter;
  52. /** DocumentFilter that forwards calls back to DefaultFormatter. */
  53. private DocumentFilter documentFilter;
  54. /** Used during replace to track the region to replace. */
  55. transient ReplaceHolder replaceHolder;
  56. /**
  57. * Creates a DefaultFormatter.
  58. */
  59. public DefaultFormatter() {
  60. overwriteMode = true;
  61. allowsInvalid = true;
  62. }
  63. /**
  64. * Installs the <code>DefaultFormatter</code> onto a particular
  65. * <code>JFormattedTextField</code>.
  66. * This will invoke <code>valueToString</code> to convert the
  67. * current value from the <code>JFormattedTextField</code> to
  68. * a String. This will then install the <code>Action</code>s from
  69. * <code>getActions</code>, the <code>DocumentFilter</code>
  70. * returned from <code>getDocumentFilter</code> and the
  71. * <code>NavigationFilter</code> returned from
  72. * <code>getNavigationFilter</code> onto the
  73. * <code>JFormattedTextField</code>.
  74. * <p>
  75. * Subclasses will typically only need to override this if they
  76. * wish to install additional listeners on the
  77. * <code>JFormattedTextField</code>.
  78. * <p>
  79. * If there is a <code>ParseException</code> in converting the
  80. * current value to a String, this will set the text to an empty
  81. * String, and mark the <code>JFormattedTextField</code> as being
  82. * in an invalid state.
  83. * <p>
  84. * While this is a public method, this is typically only useful
  85. * for subclassers of <code>JFormattedTextField</code>.
  86. * <code>JFormattedTextField</code> will invoke this method at
  87. * the appropriate times when the value changes, or its internal
  88. * state changes.
  89. *
  90. * @param ftf JFormattedTextField to format for, may be null indicating
  91. * uninstall from current JFormattedTextField.
  92. */
  93. public void install(JFormattedTextField ftf) {
  94. super.install(ftf);
  95. positionCursorAtInitialLocation();
  96. }
  97. /**
  98. * Sets when edits are published back to the
  99. * <code>JFormattedTextField</code>. If true, <code>commitEdit</code>
  100. * is invoked after every valid edit (any time the text is edited). On
  101. * the other hand, if this is false than the <code>DefaultFormatter</code>
  102. * does not publish edits back to the <code>JFormattedTextField</code>.
  103. * As such, the only time the value of the <code>JFormattedTextField</code>
  104. * will change is when <code>commitEdit</code> is invoked on
  105. * <code>JFormattedTextField</code>, typically when enter is pressed
  106. * or focus leaves the <code>JFormattedTextField</code>.
  107. *
  108. * @param commit Used to indicate when edits are commited back to the
  109. * JTextComponent
  110. */
  111. public void setCommitsOnValidEdit(boolean commit) {
  112. commitOnEdit = commit;
  113. }
  114. /**
  115. * Returns when edits are published back to the
  116. * <code>JFormattedTextField</code>.
  117. *
  118. * @return true if edits are commited after evey valid edit
  119. */
  120. public boolean getCommitsOnValidEdit() {
  121. return commitOnEdit;
  122. }
  123. /**
  124. * Configures the behavior when inserting characters. If
  125. * <code>overwriteMode</code> is true (the default), new characters
  126. * overwrite existing characters in the model.
  127. *
  128. * @param overwriteMode Indicates if overwrite or overstrike mode is used
  129. */
  130. public void setOverwriteMode(boolean overwriteMode) {
  131. this.overwriteMode = overwriteMode;
  132. }
  133. /**
  134. * Returns the behavior when inserting characters.
  135. *
  136. * @return true if newly inserted characters overwrite existing characters
  137. */
  138. public boolean getOverwriteMode() {
  139. return overwriteMode;
  140. }
  141. /**
  142. * Sets whether or not the value being edited is allowed to be invalid
  143. * for a length of time (that is, <code>stringToValue</code> throws
  144. * a <code>ParseException</code>).
  145. * It is often convenient to allow the user to temporarily input an
  146. * invalid value.
  147. *
  148. * @param allowsInvalid Used to indicate if the edited value must always
  149. * be valid
  150. */
  151. public void setAllowsInvalid(boolean allowsInvalid) {
  152. this.allowsInvalid = allowsInvalid;
  153. }
  154. /**
  155. * Returns whether or not the value being edited is allowed to be invalid
  156. * for a length of time.
  157. *
  158. * @return false if the edited value must always be valid
  159. */
  160. public boolean getAllowsInvalid() {
  161. return allowsInvalid;
  162. }
  163. /**
  164. * Sets that class that is used to create new Objects. If the
  165. * passed in class does not have a single argument constructor that
  166. * takes a String, String values will be used.
  167. *
  168. * @param valueClass Class used to construct return value from
  169. * stringToValue
  170. */
  171. public void setValueClass(Class valueClass) {
  172. this.valueClass = valueClass;
  173. }
  174. /**
  175. * Returns that class that is used to create new Objects.
  176. *
  177. * @return Class used to constuct return value from stringToValue
  178. */
  179. public Class getValueClass() {
  180. return valueClass;
  181. }
  182. /**
  183. * Converts the passed in String into an instance of
  184. * <code>getValueClass</code> by way of the constructor that
  185. * takes a String argument. If <code>getValueClass</code>
  186. * returns null, the Class of the current value in the
  187. * <code>JFormattedTextField</code> will be used. If this is null, a
  188. * String will be returned. If the constructor thows an exception, a
  189. * <code>ParseException</code> will be thrown. If there is no single
  190. * argument String constructor, <code>string</code> will be returned.
  191. *
  192. * @throws ParseException if there is an error in the conversion
  193. * @param string String to convert
  194. * @return Object representation of text
  195. */
  196. public Object stringToValue(String string) throws ParseException {
  197. Class vc = getValueClass();
  198. JFormattedTextField ftf = getFormattedTextField();
  199. if (vc == null && ftf != null) {
  200. Object value = ftf.getValue();
  201. if (value != null) {
  202. vc = value.getClass();
  203. }
  204. }
  205. if (vc != null) {
  206. Constructor cons;
  207. try {
  208. cons = vc.getConstructor(new Class[] { String.class });
  209. } catch (NoSuchMethodException nsme) {
  210. cons = null;
  211. }
  212. if (cons != null) {
  213. try {
  214. return cons.newInstance(new Object[] { string });
  215. } catch (Throwable ex) {
  216. throw new ParseException("Error creating instance", 0);
  217. }
  218. }
  219. }
  220. return string;
  221. }
  222. /**
  223. * Converts the passed in Object into a String by way of the
  224. * <code>toString</code> method.
  225. *
  226. * @throws ParseException if there is an error in the conversion
  227. * @param value Value to convert
  228. * @return String representation of value
  229. */
  230. public String valueToString(Object value) throws ParseException {
  231. if (value == null) {
  232. return "";
  233. }
  234. return value.toString();
  235. }
  236. /**
  237. * Returns the <code>DocumentFilter</code> used to restrict the characters
  238. * that can be input into the <code>JFormattedTextField</code>.
  239. *
  240. * @return DocumentFilter to restrict edits
  241. */
  242. protected DocumentFilter getDocumentFilter() {
  243. if (documentFilter == null) {
  244. documentFilter = new DefaultDocumentFilter();
  245. }
  246. return documentFilter;
  247. }
  248. /**
  249. * Returns the <code>NavigationFilter</code> used to restrict where the
  250. * cursor can be placed.
  251. * characters that can be input into the <code>JFormattedTextField</code>.
  252. *
  253. * @return NavigationFilter to restrict navigation
  254. */
  255. protected NavigationFilter getNavigationFilter() {
  256. if (navigationFilter == null) {
  257. navigationFilter = new DefaultNavigationFilter();
  258. }
  259. return navigationFilter;
  260. }
  261. /**
  262. * Creates a copy of the DefaultFormatter.
  263. *
  264. * @return copy of the DefaultFormatter
  265. */
  266. public Object clone() throws CloneNotSupportedException {
  267. DefaultFormatter formatter = (DefaultFormatter)super.clone();
  268. formatter.navigationFilter = null;
  269. formatter.documentFilter = null;
  270. formatter.replaceHolder = null;
  271. return formatter;
  272. }
  273. /**
  274. * Positions the cursor at the initial location.
  275. */
  276. void positionCursorAtInitialLocation() {
  277. JFormattedTextField ftf = getFormattedTextField();
  278. if (ftf != null) {
  279. ftf.setCaretPosition(getInitialVisualPosition());
  280. }
  281. }
  282. /**
  283. * Returns the initial location to position the cursor at. This forwards
  284. * the call to <code>getNextNavigatableChar</code>.
  285. */
  286. int getInitialVisualPosition() {
  287. return getNextNavigatableChar(0, 1);
  288. }
  289. /**
  290. * Subclasses should override this if they want cursor navigation
  291. * to skip certain characters. A return value of false indicates
  292. * the character at <code>offset</code> should be skipped when
  293. * navigating throught the field.
  294. */
  295. boolean isNavigatable(int offset) {
  296. return true;
  297. }
  298. /**
  299. * Returns true if the text in <code>text</code> can be inserted. This
  300. * does not mean the text will ultimately be inserted, it is used if
  301. * text can trivially reject certain characters.
  302. */
  303. boolean isLegalInsertText(String text) {
  304. return true;
  305. }
  306. /**
  307. * Returns the next editable character starting at offset incrementing
  308. * the offset by <code>direction</code>.
  309. */
  310. private int getNextNavigatableChar(int offset, int direction) {
  311. int max = getFormattedTextField().getDocument().getLength();
  312. while (offset >= 0 && offset < max) {
  313. if (isNavigatable(offset)) {
  314. return offset;
  315. }
  316. offset += direction;
  317. }
  318. return offset;
  319. }
  320. /**
  321. * A convenience methods to return the result of deleting
  322. * <code>deleteLength</code> characters at <code>offset</code>
  323. * and inserting <code>replaceString</code> at <code>offset</code>
  324. * in the current text field.
  325. */
  326. String getReplaceString(int offset, int deleteLength,
  327. String replaceString) {
  328. String string = getFormattedTextField().getText();
  329. String result;
  330. result = string.substring(0, offset);
  331. if (replaceString != null) {
  332. result += replaceString;
  333. }
  334. if (offset + deleteLength < string.length()) {
  335. result += string.substring(offset + deleteLength);
  336. }
  337. return result;
  338. }
  339. /*
  340. * Returns true if the operation described by <code>rh</code> will
  341. * result in a legal edit. This may set the <code>value</code>
  342. * field of <code>rh</code>.
  343. */
  344. boolean isValidEdit(ReplaceHolder rh) {
  345. if (!getAllowsInvalid()) {
  346. String newString = getReplaceString(rh.offset, rh.length, rh.text);
  347. try {
  348. rh.value = stringToValue(newString);
  349. return true;
  350. } catch (ParseException pe) {
  351. return false;
  352. }
  353. }
  354. return true;
  355. }
  356. /**
  357. * Invokes <code>commitEdit</code> on the JFormattedTextField.
  358. */
  359. void commitEdit() throws ParseException {
  360. JFormattedTextField ftf = getFormattedTextField();
  361. if (ftf != null) {
  362. ftf.commitEdit();
  363. }
  364. }
  365. /**
  366. * Pushes the value to the JFormattedTextField if the current value
  367. * is valid and invokes <code>setEditValid</code> based on the
  368. * validity of the value.
  369. */
  370. void updateValue() {
  371. updateValue(null);
  372. }
  373. /**
  374. * Pushes the <code>value</code> to the editor if we are to
  375. * commit on edits. If <code>value</code> is null, the current value
  376. * will be obtained from the text component.
  377. */
  378. void updateValue(Object value) {
  379. try {
  380. if (value == null) {
  381. String string = getFormattedTextField().getText();
  382. value = stringToValue(string);
  383. }
  384. if (getCommitsOnValidEdit()) {
  385. commitEdit();
  386. }
  387. setEditValid(true);
  388. } catch (ParseException pe) {
  389. setEditValid(false);
  390. }
  391. }
  392. /**
  393. * Returns the next cursor position from offset by incrementing
  394. * <code>direction</code>. This uses
  395. * <code>getNextNavigatableChar</code>
  396. * as well as constraining the location to the max position.
  397. */
  398. int getNextCursorPosition(int offset, int direction) {
  399. int newOffset = getNextNavigatableChar(offset, direction);
  400. int max = getFormattedTextField().getDocument().getLength();
  401. if (!getAllowsInvalid()) {
  402. if (direction == -1 && offset == newOffset) {
  403. // Case where hit backspace and only characters before
  404. // offset are fixed.
  405. newOffset = getNextNavigatableChar(newOffset, 1);
  406. if (newOffset >= max) {
  407. newOffset = offset;
  408. }
  409. }
  410. else if (direction == 1 && newOffset >= max) {
  411. // Don't go beyond last editable character.
  412. newOffset = getNextNavigatableChar(max - 1, -1);
  413. if (newOffset < max) {
  414. newOffset++;
  415. }
  416. }
  417. }
  418. return newOffset;
  419. }
  420. /**
  421. * Resets the cursor by using getNextCursorPosition.
  422. */
  423. void repositionCursor(int offset, int direction) {
  424. getFormattedTextField().getCaret().setDot(getNextCursorPosition
  425. (offset, direction));
  426. }
  427. /**
  428. * Finds the next navigatable character.
  429. */
  430. int getNextVisualPositionFrom(JTextComponent text, int pos,
  431. Position.Bias bias, int direction,
  432. Position.Bias[] biasRet)
  433. throws BadLocationException {
  434. int value = text.getUI().getNextVisualPositionFrom(text, pos, bias,
  435. direction, biasRet);
  436. if (value == -1) {
  437. return -1;
  438. }
  439. if (!getAllowsInvalid() && (direction == SwingConstants.EAST ||
  440. direction == SwingConstants.WEST)) {
  441. int last = -1;
  442. while (!isNavigatable(value) && value != last) {
  443. last = value;
  444. value = text.getUI().getNextVisualPositionFrom(
  445. text, value, bias, direction,biasRet);
  446. }
  447. int max = getFormattedTextField().getDocument().getLength();
  448. if (last == value || value == max) {
  449. if (value == 0) {
  450. biasRet[0] = Position.Bias.Forward;
  451. value = getInitialVisualPosition();
  452. }
  453. if (value >= max && max > 0) {
  454. // Pending: should not assume forward!
  455. biasRet[0] = Position.Bias.Forward;
  456. value = getNextNavigatableChar(max - 1, -1) + 1;
  457. }
  458. }
  459. }
  460. return value;
  461. }
  462. /**
  463. * Returns true if the edit described by <code>rh</code> will result
  464. * in a legal value.
  465. */
  466. boolean canReplace(ReplaceHolder rh) {
  467. return isValidEdit(rh);
  468. }
  469. /**
  470. * DocumentFilter method, funnels into <code>replace</code>.
  471. */
  472. void replace(DocumentFilter.FilterBypass fb, int offset,
  473. int length, String text,
  474. AttributeSet attrs) throws BadLocationException {
  475. ReplaceHolder rh = getReplaceHolder(fb, offset, length, text, attrs);
  476. replace(rh);
  477. }
  478. /**
  479. * If the edit described by <code>rh</code> is legal, this will
  480. * return true, commit the edit (if necessary) and update the cursor
  481. * position. This forwards to <code>canReplace</code> and
  482. * <code>isLegalInsertText</code> as necessary to determine if
  483. * the edit is in fact legal.
  484. * <p>
  485. * All of the DocumentFilter methods funnel into here, you should
  486. * generally only have to override this.
  487. */
  488. boolean replace(ReplaceHolder rh) throws BadLocationException {
  489. boolean valid = true;
  490. int direction = 1;
  491. if (rh.length > 0 && (rh.text == null || rh.text.length() == 0) &&
  492. (getFormattedTextField().getSelectionStart() != rh.offset ||
  493. rh.length > 1)) {
  494. direction = -1;
  495. }
  496. if (getOverwriteMode() && rh.text != null) {
  497. rh.length = Math.min(Math.max(rh.length, rh.text.length()),
  498. rh.fb.getDocument().getLength() - rh.offset);
  499. }
  500. if ((rh.text != null && !isLegalInsertText(rh.text)) ||
  501. !canReplace(rh) ||
  502. (rh.length == 0 && (rh.text == null || rh.text.length() == 0))) {
  503. valid = false;
  504. }
  505. if (valid) {
  506. int cursor = rh.cursorPosition;
  507. rh.fb.replace(rh.offset, rh.length, rh.text, rh.attrs);
  508. if (cursor == -1) {
  509. cursor = rh.offset;
  510. if (direction == 1 && rh.text != null) {
  511. cursor = rh.offset + rh.text.length();
  512. }
  513. }
  514. updateValue(rh.value);
  515. repositionCursor(cursor, direction);
  516. return true;
  517. }
  518. else {
  519. invalidEdit();
  520. }
  521. return false;
  522. }
  523. /**
  524. * NavigationFilter method, subclasses that wish finer control should
  525. * override this.
  526. */
  527. void setDot(NavigationFilter.FilterBypass fb, int dot, Position.Bias bias){
  528. fb.setDot(dot, bias);
  529. }
  530. /**
  531. * NavigationFilter method, subclasses that wish finer control should
  532. * override this.
  533. */
  534. void moveDot(NavigationFilter.FilterBypass fb, int dot,
  535. Position.Bias bias) {
  536. fb.moveDot(dot, bias);
  537. }
  538. /**
  539. * Returns the ReplaceHolder to track the replace of the specified
  540. * text.
  541. */
  542. ReplaceHolder getReplaceHolder(DocumentFilter.FilterBypass fb, int offset,
  543. int length, String text,
  544. AttributeSet attrs) {
  545. if (replaceHolder == null) {
  546. replaceHolder = new ReplaceHolder();
  547. }
  548. replaceHolder.reset(fb, offset, length, text, attrs);
  549. return replaceHolder;
  550. }
  551. /**
  552. * ReplaceHolder is used to track where insert/remove/replace is
  553. * going to happen.
  554. */
  555. static class ReplaceHolder {
  556. /** The FilterBypass that was passed to the DocumentFilter method. */
  557. DocumentFilter.FilterBypass fb;
  558. /** Offset where the remove/insert is going to occur. */
  559. int offset;
  560. /** Length of text to remove. */
  561. int length;
  562. /** The text to insert, may be null. */
  563. String text;
  564. /** AttributeSet to attach to text, may be null. */
  565. AttributeSet attrs;
  566. /** The resulting value, this may never be set. */
  567. Object value;
  568. /** Position the cursor should be adjusted from. If this is -1
  569. * the cursor position will be adjusted based on the direction of
  570. * the replace (-1: offset, 1: offset + text.length()), otherwise
  571. * the cursor position is adusted from this position.
  572. */
  573. int cursorPosition;
  574. void reset(DocumentFilter.FilterBypass fb, int offset, int length,
  575. String text, AttributeSet attrs) {
  576. this.fb = fb;
  577. this.offset = offset;
  578. this.length = length;
  579. this.text = text;
  580. this.attrs = attrs;
  581. this.value = null;
  582. cursorPosition = -1;
  583. }
  584. }
  585. /**
  586. * NavigationFilter implementation that calls back to methods with
  587. * same name in DefaultFormatter.
  588. */
  589. private class DefaultNavigationFilter extends NavigationFilter
  590. implements Serializable {
  591. public void setDot(FilterBypass fb, int dot, Position.Bias bias) {
  592. JTextComponent tc = DefaultFormatter.this.getFormattedTextField();
  593. if (tc.composedTextExists()) {
  594. // bypass the filter
  595. fb.setDot(dot, bias);
  596. } else {
  597. DefaultFormatter.this.setDot(fb, dot, bias);
  598. }
  599. }
  600. public void moveDot(FilterBypass fb, int dot, Position.Bias bias) {
  601. JTextComponent tc = DefaultFormatter.this.getFormattedTextField();
  602. if (tc.composedTextExists()) {
  603. // bypass the filter
  604. fb.moveDot(dot, bias);
  605. } else {
  606. DefaultFormatter.this.moveDot(fb, dot, bias);
  607. }
  608. }
  609. public int getNextVisualPositionFrom(JTextComponent text, int pos,
  610. Position.Bias bias,
  611. int direction,
  612. Position.Bias[] biasRet)
  613. throws BadLocationException {
  614. if (text.composedTextExists()) {
  615. // forward the call to the UI directly
  616. return text.getUI().getNextVisualPositionFrom(
  617. text, pos, bias, direction, biasRet);
  618. } else {
  619. return DefaultFormatter.this.getNextVisualPositionFrom(
  620. text, pos, bias, direction, biasRet);
  621. }
  622. }
  623. }
  624. /**
  625. * DocumentFilter implementation that calls back to the replace
  626. * method of DefaultFormatter.
  627. */
  628. private class DefaultDocumentFilter extends DocumentFilter implements
  629. Serializable {
  630. public void remove(FilterBypass fb, int offset, int length) throws
  631. BadLocationException {
  632. JTextComponent tc = DefaultFormatter.this.getFormattedTextField();
  633. if (tc.composedTextExists()) {
  634. // bypass the filter
  635. fb.remove(offset, length);
  636. } else {
  637. DefaultFormatter.this.replace(fb, offset, length, null, null);
  638. }
  639. }
  640. public void insertString(FilterBypass fb, int offset,
  641. String string, AttributeSet attr) throws
  642. BadLocationException {
  643. JTextComponent tc = DefaultFormatter.this.getFormattedTextField();
  644. if (tc.composedTextExists() ||
  645. Utilities.isComposedTextAttributeDefined(attr)) {
  646. // bypass the filter
  647. fb.insertString(offset, string, attr);
  648. } else {
  649. DefaultFormatter.this.replace(fb, offset, 0, string, attr);
  650. }
  651. }
  652. public void replace(FilterBypass fb, int offset, int length,
  653. String text, AttributeSet attr) throws
  654. BadLocationException {
  655. JTextComponent tc = DefaultFormatter.this.getFormattedTextField();
  656. if (tc.composedTextExists() ||
  657. Utilities.isComposedTextAttributeDefined(attr)) {
  658. // bypass the filter
  659. fb.replace(offset, length, text, attr);
  660. } else {
  661. DefaultFormatter.this.replace(fb, offset, length, text, attr);
  662. }
  663. }
  664. }
  665. }