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&trade;
  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 }