1. /*
  2. * @(#)DefaultFormatter.java 1.13 04/05/05
  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.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.13 05/05/04
  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. *
  252. * @return NavigationFilter to restrict navigation
  253. */
  254. protected NavigationFilter getNavigationFilter() {
  255. if (navigationFilter == null) {
  256. navigationFilter = new DefaultNavigationFilter();
  257. }
  258. return navigationFilter;
  259. }
  260. /**
  261. * Creates a copy of the DefaultFormatter.
  262. *
  263. * @return copy of the DefaultFormatter
  264. */
  265. public Object clone() throws CloneNotSupportedException {
  266. DefaultFormatter formatter = (DefaultFormatter)super.clone();
  267. formatter.navigationFilter = null;
  268. formatter.documentFilter = null;
  269. formatter.replaceHolder = null;
  270. return formatter;
  271. }
  272. /**
  273. * Positions the cursor at the initial location.
  274. */
  275. void positionCursorAtInitialLocation() {
  276. JFormattedTextField ftf = getFormattedTextField();
  277. if (ftf != null) {
  278. ftf.setCaretPosition(getInitialVisualPosition());
  279. }
  280. }
  281. /**
  282. * Returns the initial location to position the cursor at. This forwards
  283. * the call to <code>getNextNavigatableChar</code>.
  284. */
  285. int getInitialVisualPosition() {
  286. return getNextNavigatableChar(0, 1);
  287. }
  288. /**
  289. * Subclasses should override this if they want cursor navigation
  290. * to skip certain characters. A return value of false indicates
  291. * the character at <code>offset</code> should be skipped when
  292. * navigating throught the field.
  293. */
  294. boolean isNavigatable(int offset) {
  295. return true;
  296. }
  297. /**
  298. * Returns true if the text in <code>text</code> can be inserted. This
  299. * does not mean the text will ultimately be inserted, it is used if
  300. * text can trivially reject certain characters.
  301. */
  302. boolean isLegalInsertText(String text) {
  303. return true;
  304. }
  305. /**
  306. * Returns the next editable character starting at offset incrementing
  307. * the offset by <code>direction</code>.
  308. */
  309. private int getNextNavigatableChar(int offset, int direction) {
  310. int max = getFormattedTextField().getDocument().getLength();
  311. while (offset >= 0 && offset < max) {
  312. if (isNavigatable(offset)) {
  313. return offset;
  314. }
  315. offset += direction;
  316. }
  317. return offset;
  318. }
  319. /**
  320. * A convenience methods to return the result of deleting
  321. * <code>deleteLength</code> characters at <code>offset</code>
  322. * and inserting <code>replaceString</code> at <code>offset</code>
  323. * in the current text field.
  324. */
  325. String getReplaceString(int offset, int deleteLength,
  326. String replaceString) {
  327. String string = getFormattedTextField().getText();
  328. String result;
  329. result = string.substring(0, offset);
  330. if (replaceString != null) {
  331. result += replaceString;
  332. }
  333. if (offset + deleteLength < string.length()) {
  334. result += string.substring(offset + deleteLength);
  335. }
  336. return result;
  337. }
  338. /*
  339. * Returns true if the operation described by <code>rh</code> will
  340. * result in a legal edit. This may set the <code>value</code>
  341. * field of <code>rh</code>.
  342. */
  343. boolean isValidEdit(ReplaceHolder rh) {
  344. if (!getAllowsInvalid()) {
  345. String newString = getReplaceString(rh.offset, rh.length, rh.text);
  346. try {
  347. rh.value = stringToValue(newString);
  348. return true;
  349. } catch (ParseException pe) {
  350. return false;
  351. }
  352. }
  353. return true;
  354. }
  355. /**
  356. * Invokes <code>commitEdit</code> on the JFormattedTextField.
  357. */
  358. void commitEdit() throws ParseException {
  359. JFormattedTextField ftf = getFormattedTextField();
  360. if (ftf != null) {
  361. ftf.commitEdit();
  362. }
  363. }
  364. /**
  365. * Pushes the value to the JFormattedTextField if the current value
  366. * is valid and invokes <code>setEditValid</code> based on the
  367. * validity of the value.
  368. */
  369. void updateValue() {
  370. updateValue(null);
  371. }
  372. /**
  373. * Pushes the <code>value</code> to the editor if we are to
  374. * commit on edits. If <code>value</code> is null, the current value
  375. * will be obtained from the text component.
  376. */
  377. void updateValue(Object value) {
  378. try {
  379. if (value == null) {
  380. String string = getFormattedTextField().getText();
  381. value = stringToValue(string);
  382. }
  383. if (getCommitsOnValidEdit()) {
  384. commitEdit();
  385. }
  386. setEditValid(true);
  387. } catch (ParseException pe) {
  388. setEditValid(false);
  389. }
  390. }
  391. /**
  392. * Returns the next cursor position from offset by incrementing
  393. * <code>direction</code>. This uses
  394. * <code>getNextNavigatableChar</code>
  395. * as well as constraining the location to the max position.
  396. */
  397. int getNextCursorPosition(int offset, int direction) {
  398. int newOffset = getNextNavigatableChar(offset, direction);
  399. int max = getFormattedTextField().getDocument().getLength();
  400. if (!getAllowsInvalid()) {
  401. if (direction == -1 && offset == newOffset) {
  402. // Case where hit backspace and only characters before
  403. // offset are fixed.
  404. newOffset = getNextNavigatableChar(newOffset, 1);
  405. if (newOffset >= max) {
  406. newOffset = offset;
  407. }
  408. }
  409. else if (direction == 1 && newOffset >= max) {
  410. // Don't go beyond last editable character.
  411. newOffset = getNextNavigatableChar(max - 1, -1);
  412. if (newOffset < max) {
  413. newOffset++;
  414. }
  415. }
  416. }
  417. return newOffset;
  418. }
  419. /**
  420. * Resets the cursor by using getNextCursorPosition.
  421. */
  422. void repositionCursor(int offset, int direction) {
  423. getFormattedTextField().getCaret().setDot(getNextCursorPosition
  424. (offset, direction));
  425. }
  426. /**
  427. * Finds the next navigatable character.
  428. */
  429. int getNextVisualPositionFrom(JTextComponent text, int pos,
  430. Position.Bias bias, int direction,
  431. Position.Bias[] biasRet)
  432. throws BadLocationException {
  433. int value = text.getUI().getNextVisualPositionFrom(text, pos, bias,
  434. direction, biasRet);
  435. if (value == -1) {
  436. return -1;
  437. }
  438. if (!getAllowsInvalid() && (direction == SwingConstants.EAST ||
  439. direction == SwingConstants.WEST)) {
  440. int last = -1;
  441. while (!isNavigatable(value) && value != last) {
  442. last = value;
  443. value = text.getUI().getNextVisualPositionFrom(
  444. text, value, bias, direction,biasRet);
  445. }
  446. int max = getFormattedTextField().getDocument().getLength();
  447. if (last == value || value == max) {
  448. if (value == 0) {
  449. biasRet[0] = Position.Bias.Forward;
  450. value = getInitialVisualPosition();
  451. }
  452. if (value >= max && max > 0) {
  453. // Pending: should not assume forward!
  454. biasRet[0] = Position.Bias.Forward;
  455. value = getNextNavigatableChar(max - 1, -1) + 1;
  456. }
  457. }
  458. }
  459. return value;
  460. }
  461. /**
  462. * Returns true if the edit described by <code>rh</code> will result
  463. * in a legal value.
  464. */
  465. boolean canReplace(ReplaceHolder rh) {
  466. return isValidEdit(rh);
  467. }
  468. /**
  469. * DocumentFilter method, funnels into <code>replace</code>.
  470. */
  471. void replace(DocumentFilter.FilterBypass fb, int offset,
  472. int length, String text,
  473. AttributeSet attrs) throws BadLocationException {
  474. ReplaceHolder rh = getReplaceHolder(fb, offset, length, text, attrs);
  475. replace(rh);
  476. }
  477. /**
  478. * If the edit described by <code>rh</code> is legal, this will
  479. * return true, commit the edit (if necessary) and update the cursor
  480. * position. This forwards to <code>canReplace</code> and
  481. * <code>isLegalInsertText</code> as necessary to determine if
  482. * the edit is in fact legal.
  483. * <p>
  484. * All of the DocumentFilter methods funnel into here, you should
  485. * generally only have to override this.
  486. */
  487. boolean replace(ReplaceHolder rh) throws BadLocationException {
  488. boolean valid = true;
  489. int direction = 1;
  490. if (rh.length > 0 && (rh.text == null || rh.text.length() == 0) &&
  491. (getFormattedTextField().getSelectionStart() != rh.offset ||
  492. rh.length > 1)) {
  493. direction = -1;
  494. }
  495. if (getOverwriteMode() && rh.text != null) {
  496. rh.length = Math.min(Math.max(rh.length, rh.text.length()),
  497. rh.fb.getDocument().getLength() - rh.offset);
  498. }
  499. if ((rh.text != null && !isLegalInsertText(rh.text)) ||
  500. !canReplace(rh) ||
  501. (rh.length == 0 && (rh.text == null || rh.text.length() == 0))) {
  502. valid = false;
  503. }
  504. if (valid) {
  505. int cursor = rh.cursorPosition;
  506. rh.fb.replace(rh.offset, rh.length, rh.text, rh.attrs);
  507. if (cursor == -1) {
  508. cursor = rh.offset;
  509. if (direction == 1 && rh.text != null) {
  510. cursor = rh.offset + rh.text.length();
  511. }
  512. }
  513. updateValue(rh.value);
  514. repositionCursor(cursor, direction);
  515. return true;
  516. }
  517. else {
  518. invalidEdit();
  519. }
  520. return false;
  521. }
  522. /**
  523. * NavigationFilter method, subclasses that wish finer control should
  524. * override this.
  525. */
  526. void setDot(NavigationFilter.FilterBypass fb, int dot, Position.Bias bias){
  527. fb.setDot(dot, bias);
  528. }
  529. /**
  530. * NavigationFilter method, subclasses that wish finer control should
  531. * override this.
  532. */
  533. void moveDot(NavigationFilter.FilterBypass fb, int dot,
  534. Position.Bias bias) {
  535. fb.moveDot(dot, bias);
  536. }
  537. /**
  538. * Returns the ReplaceHolder to track the replace of the specified
  539. * text.
  540. */
  541. ReplaceHolder getReplaceHolder(DocumentFilter.FilterBypass fb, int offset,
  542. int length, String text,
  543. AttributeSet attrs) {
  544. if (replaceHolder == null) {
  545. replaceHolder = new ReplaceHolder();
  546. }
  547. replaceHolder.reset(fb, offset, length, text, attrs);
  548. return replaceHolder;
  549. }
  550. /**
  551. * ReplaceHolder is used to track where insert/remove/replace is
  552. * going to happen.
  553. */
  554. static class ReplaceHolder {
  555. /** The FilterBypass that was passed to the DocumentFilter method. */
  556. DocumentFilter.FilterBypass fb;
  557. /** Offset where the remove/insert is going to occur. */
  558. int offset;
  559. /** Length of text to remove. */
  560. int length;
  561. /** The text to insert, may be null. */
  562. String text;
  563. /** AttributeSet to attach to text, may be null. */
  564. AttributeSet attrs;
  565. /** The resulting value, this may never be set. */
  566. Object value;
  567. /** Position the cursor should be adjusted from. If this is -1
  568. * the cursor position will be adjusted based on the direction of
  569. * the replace (-1: offset, 1: offset + text.length()), otherwise
  570. * the cursor position is adusted from this position.
  571. */
  572. int cursorPosition;
  573. void reset(DocumentFilter.FilterBypass fb, int offset, int length,
  574. String text, AttributeSet attrs) {
  575. this.fb = fb;
  576. this.offset = offset;
  577. this.length = length;
  578. this.text = text;
  579. this.attrs = attrs;
  580. this.value = null;
  581. cursorPosition = -1;
  582. }
  583. }
  584. /**
  585. * NavigationFilter implementation that calls back to methods with
  586. * same name in DefaultFormatter.
  587. */
  588. private class DefaultNavigationFilter extends NavigationFilter
  589. implements Serializable {
  590. public void setDot(FilterBypass fb, int dot, Position.Bias bias) {
  591. JTextComponent tc = DefaultFormatter.this.getFormattedTextField();
  592. if (tc.composedTextExists()) {
  593. // bypass the filter
  594. fb.setDot(dot, bias);
  595. } else {
  596. DefaultFormatter.this.setDot(fb, dot, bias);
  597. }
  598. }
  599. public void moveDot(FilterBypass fb, int dot, Position.Bias bias) {
  600. JTextComponent tc = DefaultFormatter.this.getFormattedTextField();
  601. if (tc.composedTextExists()) {
  602. // bypass the filter
  603. fb.moveDot(dot, bias);
  604. } else {
  605. DefaultFormatter.this.moveDot(fb, dot, bias);
  606. }
  607. }
  608. public int getNextVisualPositionFrom(JTextComponent text, int pos,
  609. Position.Bias bias,
  610. int direction,
  611. Position.Bias[] biasRet)
  612. throws BadLocationException {
  613. if (text.composedTextExists()) {
  614. // forward the call to the UI directly
  615. return text.getUI().getNextVisualPositionFrom(
  616. text, pos, bias, direction, biasRet);
  617. } else {
  618. return DefaultFormatter.this.getNextVisualPositionFrom(
  619. text, pos, bias, direction, biasRet);
  620. }
  621. }
  622. }
  623. /**
  624. * DocumentFilter implementation that calls back to the replace
  625. * method of DefaultFormatter.
  626. */
  627. private class DefaultDocumentFilter extends DocumentFilter implements
  628. Serializable {
  629. public void remove(FilterBypass fb, int offset, int length) throws
  630. BadLocationException {
  631. JTextComponent tc = DefaultFormatter.this.getFormattedTextField();
  632. if (tc.composedTextExists()) {
  633. // bypass the filter
  634. fb.remove(offset, length);
  635. } else {
  636. DefaultFormatter.this.replace(fb, offset, length, null, null);
  637. }
  638. }
  639. public void insertString(FilterBypass fb, int offset,
  640. String string, AttributeSet attr) throws
  641. BadLocationException {
  642. JTextComponent tc = DefaultFormatter.this.getFormattedTextField();
  643. if (tc.composedTextExists() ||
  644. Utilities.isComposedTextAttributeDefined(attr)) {
  645. // bypass the filter
  646. fb.insertString(offset, string, attr);
  647. } else {
  648. DefaultFormatter.this.replace(fb, offset, 0, string, attr);
  649. }
  650. }
  651. public void replace(FilterBypass fb, int offset, int length,
  652. String text, AttributeSet attr) throws
  653. BadLocationException {
  654. JTextComponent tc = DefaultFormatter.this.getFormattedTextField();
  655. if (tc.composedTextExists() ||
  656. Utilities.isComposedTextAttributeDefined(attr)) {
  657. // bypass the filter
  658. fb.replace(offset, length, text, attr);
  659. } else {
  660. DefaultFormatter.this.replace(fb, offset, length, text, attr);
  661. }
  662. }
  663. }
  664. }