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 
35 /**
36  * <code>DefaultFormatter</code> formats arbitrary objects. Formatting is done
37  * by invoking the <code>toString</code> method. In order to convert the
38  * value back to a String, your class must provide a constructor that
39  * takes a String argument. If no single argument constructor that takes a
40  * String is found, the returned value will be the String passed into
41  * <code>stringToValue</code>.
42  * <p>
43  * Instances of <code>DefaultFormatter</code> can not be used in multiple
44  * instances of <code>JFormattedTextField</code>. To obtain a copy of
45  * an already configured <code>DefaultFormatter</code>, use the
46  * <code>clone</code> method.
47  * <p>
48  * <strong>Warning:</strong>
49  * Serialized objects of this class will not be compatible with
50  * future Swing releases. The current serialization support is
51  * appropriate for short term storage or RMI between applications running
52  * the same version of Swing.  As of 1.4, support for long term storage
53  * of all JavaBeans&trade;
54  * has been added to the <code>java.beans</code> package.
55  * Please see {@link java.beans.XMLEncoder}.
56  *
57  * @see javax.swing.JFormattedTextField.AbstractFormatter
58  *
59  * @since 1.4
60  */
61 @SuppressWarnings("serial") // Same-version serialization only
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      */
DefaultFormatter()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      */
install(JFormattedTextField ftf)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      */
setCommitsOnValidEdit(boolean commit)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      */
getCommitsOnValidEdit()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      */
setOverwriteMode(boolean overwriteMode)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      */
getOverwriteMode()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      */
setAllowsInvalid(boolean allowsInvalid)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      */
getAllowsInvalid()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      */
setValueClass(Class<?> valueClass)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      */
getValueClass()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      */
stringToValue(String string)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      */
valueToString(Object value)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      */
getDocumentFilter()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      */
getNavigationFilter()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      */
clone()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      */
positionCursorAtInitialLocation()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      */
getInitialVisualPosition()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      */
isNavigatable(int offset)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      */
isLegalInsertText(String text)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      */
getNextNavigatableChar(int offset, int direction)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      */
getReplaceString(int offset, int deleteLength, String replaceString)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      */
isValidEdit(ReplaceHolder rh)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      */
commitEdit()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      */
updateValue()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      */
updateValue(Object value)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      */
getNextCursorPosition(int offset, int direction)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      */
repositionCursor(int offset, int direction)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      */
getNextVisualPositionFrom(JTextComponent text, int pos, Position.Bias bias, int direction, Position.Bias[] biasRet)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      */
canReplace(ReplaceHolder rh)544     boolean canReplace(ReplaceHolder rh) {
545         return isValidEdit(rh);
546     }
547 
548     /**
549      * DocumentFilter method, funnels into <code>replace</code>.
550      */
replace(DocumentFilter.FilterBypass fb, int offset, int length, String text, AttributeSet attrs)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      */
replace(ReplaceHolder rh)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      */
setDot(NavigationFilter.FilterBypass fb, int dot, Position.Bias bias)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      */
moveDot(NavigationFilter.FilterBypass fb, int dot, Position.Bias bias)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      */
getReplaceHolder(DocumentFilter.FilterBypass fb, int offset, int length, String text, AttributeSet attrs)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 
reset(DocumentFilter.FilterBypass fb, int offset, int length, String text, AttributeSet attrs)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 {
setDot(FilterBypass fb, int dot, Position.Bias bias)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 
moveDot(FilterBypass fb, int dot, Position.Bias bias)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 
getNextVisualPositionFrom(JTextComponent text, int pos, Position.Bias bias, int direction, Position.Bias[] biasRet)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 {
remove(FilterBypass fb, int offset, int length)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 
insertString(FilterBypass fb, int offset, String string, AttributeSet attr)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 
replace(FilterBypass fb, int offset, int length, String text, AttributeSet attr)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 }
767