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