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