/* * Copyright (c) 2000, 2014, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License version 2 only, as * published by the Free Software Foundation. Oracle designates this * particular file as subject to the "Classpath" exception as provided * by Oracle in the LICENSE file that accompanied this code. * * This code is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License * version 2 for more details (a copy is included in the LICENSE file that * accompanied this code). * * You should have received a copy of the GNU General Public License version * 2 along with this work; if not, write to the Free Software Foundation, * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. * * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA * or visit www.oracle.com if you need additional information or have any * questions. */ package javax.swing.text; import sun.reflect.misc.ReflectUtil; import sun.swing.SwingUtilities2; import java.io.Serializable; import java.lang.reflect.*; import java.text.ParseException; import javax.swing.*; import javax.swing.text.*; /** * DefaultFormatter formats arbitrary objects. Formatting is done * by invoking the toString method. In order to convert the * value back to a String, your class must provide a constructor that * takes a String argument. If no single argument constructor that takes a * String is found, the returned value will be the String passed into * stringToValue. *

* Instances of DefaultFormatter can not be used in multiple * instances of JFormattedTextField. To obtain a copy of * an already configured DefaultFormatter, use the * clone method. *

* Warning: * Serialized objects of this class will not be compatible with * future Swing releases. The current serialization support is * appropriate for short term storage or RMI between applications running * the same version of Swing. As of 1.4, support for long term storage * of all JavaBeans™ * has been added to the java.beans package. * Please see {@link java.beans.XMLEncoder}. * * @see javax.swing.JFormattedTextField.AbstractFormatter * * @since 1.4 */ @SuppressWarnings("serial") // Same-version serialization only public class DefaultFormatter extends JFormattedTextField.AbstractFormatter implements Cloneable, Serializable { /** Indicates if the value being edited must match the mask. */ private boolean allowsInvalid; /** If true, editing mode is in overwrite (or strikethough). */ private boolean overwriteMode; /** If true, any time a valid edit happens commitEdit is invoked. */ private boolean commitOnEdit; /** Class used to create new instances. */ private Class valueClass; /** NavigationFilter that forwards calls back to DefaultFormatter. */ private NavigationFilter navigationFilter; /** DocumentFilter that forwards calls back to DefaultFormatter. */ private DocumentFilter documentFilter; /** Used during replace to track the region to replace. */ transient ReplaceHolder replaceHolder; /** * Creates a DefaultFormatter. */ public DefaultFormatter() { overwriteMode = true; allowsInvalid = true; } /** * Installs the DefaultFormatter onto a particular * JFormattedTextField. * This will invoke valueToString to convert the * current value from the JFormattedTextField to * a String. This will then install the Actions from * getActions, the DocumentFilter * returned from getDocumentFilter and the * NavigationFilter returned from * getNavigationFilter onto the * JFormattedTextField. *

* Subclasses will typically only need to override this if they * wish to install additional listeners on the * JFormattedTextField. *

* If there is a ParseException in converting the * current value to a String, this will set the text to an empty * String, and mark the JFormattedTextField as being * in an invalid state. *

* While this is a public method, this is typically only useful * for subclassers of JFormattedTextField. * JFormattedTextField will invoke this method at * the appropriate times when the value changes, or its internal * state changes. * * @param ftf JFormattedTextField to format for, may be null indicating * uninstall from current JFormattedTextField. */ public void install(JFormattedTextField ftf) { super.install(ftf); positionCursorAtInitialLocation(); } /** * Sets when edits are published back to the * JFormattedTextField. If true, commitEdit * is invoked after every valid edit (any time the text is edited). On * the other hand, if this is false than the DefaultFormatter * does not publish edits back to the JFormattedTextField. * As such, the only time the value of the JFormattedTextField * will change is when commitEdit is invoked on * JFormattedTextField, typically when enter is pressed * or focus leaves the JFormattedTextField. * * @param commit Used to indicate when edits are committed back to the * JTextComponent */ public void setCommitsOnValidEdit(boolean commit) { commitOnEdit = commit; } /** * Returns when edits are published back to the * JFormattedTextField. * * @return true if edits are committed after every valid edit */ public boolean getCommitsOnValidEdit() { return commitOnEdit; } /** * Configures the behavior when inserting characters. If * overwriteMode is true (the default), new characters * overwrite existing characters in the model. * * @param overwriteMode Indicates if overwrite or overstrike mode is used */ public void setOverwriteMode(boolean overwriteMode) { this.overwriteMode = overwriteMode; } /** * Returns the behavior when inserting characters. * * @return true if newly inserted characters overwrite existing characters */ public boolean getOverwriteMode() { return overwriteMode; } /** * Sets whether or not the value being edited is allowed to be invalid * for a length of time (that is, stringToValue throws * a ParseException). * It is often convenient to allow the user to temporarily input an * invalid value. * * @param allowsInvalid Used to indicate if the edited value must always * be valid */ public void setAllowsInvalid(boolean allowsInvalid) { this.allowsInvalid = allowsInvalid; } /** * Returns whether or not the value being edited is allowed to be invalid * for a length of time. * * @return false if the edited value must always be valid */ public boolean getAllowsInvalid() { return allowsInvalid; } /** * Sets that class that is used to create new Objects. If the * passed in class does not have a single argument constructor that * takes a String, String values will be used. * * @param valueClass Class used to construct return value from * stringToValue */ public void setValueClass(Class valueClass) { this.valueClass = valueClass; } /** * Returns that class that is used to create new Objects. * * @return Class used to construct return value from stringToValue */ public Class getValueClass() { return valueClass; } /** * Converts the passed in String into an instance of * getValueClass by way of the constructor that * takes a String argument. If getValueClass * returns null, the Class of the current value in the * JFormattedTextField will be used. If this is null, a * String will be returned. If the constructor throws an exception, a * ParseException will be thrown. If there is no single * argument String constructor, string will be returned. * * @throws ParseException if there is an error in the conversion * @param string String to convert * @return Object representation of text */ public Object stringToValue(String string) throws ParseException { Class vc = getValueClass(); JFormattedTextField ftf = getFormattedTextField(); if (vc == null && ftf != null) { Object value = ftf.getValue(); if (value != null) { vc = value.getClass(); } } if (vc != null) { Constructor cons; try { ReflectUtil.checkPackageAccess(vc); SwingUtilities2.checkAccess(vc.getModifiers()); cons = vc.getConstructor(new Class[]{String.class}); } catch (NoSuchMethodException nsme) { cons = null; } if (cons != null) { try { SwingUtilities2.checkAccess(cons.getModifiers()); return cons.newInstance(new Object[] { string }); } catch (Throwable ex) { throw new ParseException("Error creating instance", 0); } } } return string; } /** * Converts the passed in Object into a String by way of the * toString method. * * @throws ParseException if there is an error in the conversion * @param value Value to convert * @return String representation of value */ public String valueToString(Object value) throws ParseException { if (value == null) { return ""; } return value.toString(); } /** * Returns the DocumentFilter used to restrict the characters * that can be input into the JFormattedTextField. * * @return DocumentFilter to restrict edits */ protected DocumentFilter getDocumentFilter() { if (documentFilter == null) { documentFilter = new DefaultDocumentFilter(); } return documentFilter; } /** * Returns the NavigationFilter used to restrict where the * cursor can be placed. * * @return NavigationFilter to restrict navigation */ protected NavigationFilter getNavigationFilter() { if (navigationFilter == null) { navigationFilter = new DefaultNavigationFilter(); } return navigationFilter; } /** * Creates a copy of the DefaultFormatter. * * @return copy of the DefaultFormatter */ public Object clone() throws CloneNotSupportedException { DefaultFormatter formatter = (DefaultFormatter)super.clone(); formatter.navigationFilter = null; formatter.documentFilter = null; formatter.replaceHolder = null; return formatter; } /** * Positions the cursor at the initial location. */ void positionCursorAtInitialLocation() { JFormattedTextField ftf = getFormattedTextField(); if (ftf != null) { ftf.setCaretPosition(getInitialVisualPosition()); } } /** * Returns the initial location to position the cursor at. This forwards * the call to getNextNavigatableChar. */ int getInitialVisualPosition() { return getNextNavigatableChar(0, 1); } /** * Subclasses should override this if they want cursor navigation * to skip certain characters. A return value of false indicates * the character at offset should be skipped when * navigating throught the field. */ boolean isNavigatable(int offset) { return true; } /** * Returns true if the text in text can be inserted. This * does not mean the text will ultimately be inserted, it is used if * text can trivially reject certain characters. */ boolean isLegalInsertText(String text) { return true; } /** * Returns the next editable character starting at offset incrementing * the offset by direction. */ private int getNextNavigatableChar(int offset, int direction) { int max = getFormattedTextField().getDocument().getLength(); while (offset >= 0 && offset < max) { if (isNavigatable(offset)) { return offset; } offset += direction; } return offset; } /** * A convenience methods to return the result of deleting * deleteLength characters at offset * and inserting replaceString at offset * in the current text field. */ String getReplaceString(int offset, int deleteLength, String replaceString) { String string = getFormattedTextField().getText(); String result; result = string.substring(0, offset); if (replaceString != null) { result += replaceString; } if (offset + deleteLength < string.length()) { result += string.substring(offset + deleteLength); } return result; } /* * Returns true if the operation described by rh will * result in a legal edit. This may set the value * field of rh. */ boolean isValidEdit(ReplaceHolder rh) { if (!getAllowsInvalid()) { String newString = getReplaceString(rh.offset, rh.length, rh.text); try { rh.value = stringToValue(newString); return true; } catch (ParseException pe) { return false; } } return true; } /** * Invokes commitEdit on the JFormattedTextField. */ void commitEdit() throws ParseException { JFormattedTextField ftf = getFormattedTextField(); if (ftf != null) { ftf.commitEdit(); } } /** * Pushes the value to the JFormattedTextField if the current value * is valid and invokes setEditValid based on the * validity of the value. */ void updateValue() { updateValue(null); } /** * Pushes the value to the editor if we are to * commit on edits. If value is null, the current value * will be obtained from the text component. */ void updateValue(Object value) { try { if (value == null) { String string = getFormattedTextField().getText(); value = stringToValue(string); } if (getCommitsOnValidEdit()) { commitEdit(); } setEditValid(true); } catch (ParseException pe) { setEditValid(false); } } /** * Returns the next cursor position from offset by incrementing * direction. This uses * getNextNavigatableChar * as well as constraining the location to the max position. */ int getNextCursorPosition(int offset, int direction) { int newOffset = getNextNavigatableChar(offset, direction); int max = getFormattedTextField().getDocument().getLength(); if (!getAllowsInvalid()) { if (direction == -1 && offset == newOffset) { // Case where hit backspace and only characters before // offset are fixed. newOffset = getNextNavigatableChar(newOffset, 1); if (newOffset >= max) { newOffset = offset; } } else if (direction == 1 && newOffset >= max) { // Don't go beyond last editable character. newOffset = getNextNavigatableChar(max - 1, -1); if (newOffset < max) { newOffset++; } } } return newOffset; } /** * Resets the cursor by using getNextCursorPosition. */ void repositionCursor(int offset, int direction) { getFormattedTextField().getCaret().setDot(getNextCursorPosition (offset, direction)); } /** * Finds the next navigable character. */ int getNextVisualPositionFrom(JTextComponent text, int pos, Position.Bias bias, int direction, Position.Bias[] biasRet) throws BadLocationException { int value = text.getUI().getNextVisualPositionFrom(text, pos, bias, direction, biasRet); if (value == -1) { return -1; } if (!getAllowsInvalid() && (direction == SwingConstants.EAST || direction == SwingConstants.WEST)) { int last = -1; while (!isNavigatable(value) && value != last) { last = value; value = text.getUI().getNextVisualPositionFrom( text, value, bias, direction,biasRet); } int max = getFormattedTextField().getDocument().getLength(); if (last == value || value == max) { if (value == 0) { biasRet[0] = Position.Bias.Forward; value = getInitialVisualPosition(); } if (value >= max && max > 0) { // Pending: should not assume forward! biasRet[0] = Position.Bias.Forward; value = getNextNavigatableChar(max - 1, -1) + 1; } } } return value; } /** * Returns true if the edit described by rh will result * in a legal value. */ boolean canReplace(ReplaceHolder rh) { return isValidEdit(rh); } /** * DocumentFilter method, funnels into replace. */ void replace(DocumentFilter.FilterBypass fb, int offset, int length, String text, AttributeSet attrs) throws BadLocationException { ReplaceHolder rh = getReplaceHolder(fb, offset, length, text, attrs); replace(rh); } /** * If the edit described by rh is legal, this will * return true, commit the edit (if necessary) and update the cursor * position. This forwards to canReplace and * isLegalInsertText as necessary to determine if * the edit is in fact legal. *

* All of the DocumentFilter methods funnel into here, you should * generally only have to override this. */ boolean replace(ReplaceHolder rh) throws BadLocationException { boolean valid = true; int direction = 1; if (rh.length > 0 && (rh.text == null || rh.text.length() == 0) && (getFormattedTextField().getSelectionStart() != rh.offset || rh.length > 1)) { direction = -1; } if (getOverwriteMode() && rh.text != null && getFormattedTextField().getSelectedText() == null) { rh.length = Math.min(Math.max(rh.length, rh.text.length()), rh.fb.getDocument().getLength() - rh.offset); } if ((rh.text != null && !isLegalInsertText(rh.text)) || !canReplace(rh) || (rh.length == 0 && (rh.text == null || rh.text.length() == 0))) { valid = false; } if (valid) { int cursor = rh.cursorPosition; rh.fb.replace(rh.offset, rh.length, rh.text, rh.attrs); if (cursor == -1) { cursor = rh.offset; if (direction == 1 && rh.text != null) { cursor = rh.offset + rh.text.length(); } } updateValue(rh.value); repositionCursor(cursor, direction); return true; } else { invalidEdit(); } return false; } /** * NavigationFilter method, subclasses that wish finer control should * override this. */ void setDot(NavigationFilter.FilterBypass fb, int dot, Position.Bias bias){ fb.setDot(dot, bias); } /** * NavigationFilter method, subclasses that wish finer control should * override this. */ void moveDot(NavigationFilter.FilterBypass fb, int dot, Position.Bias bias) { fb.moveDot(dot, bias); } /** * Returns the ReplaceHolder to track the replace of the specified * text. */ ReplaceHolder getReplaceHolder(DocumentFilter.FilterBypass fb, int offset, int length, String text, AttributeSet attrs) { if (replaceHolder == null) { replaceHolder = new ReplaceHolder(); } replaceHolder.reset(fb, offset, length, text, attrs); return replaceHolder; } /** * ReplaceHolder is used to track where insert/remove/replace is * going to happen. */ static class ReplaceHolder { /** The FilterBypass that was passed to the DocumentFilter method. */ DocumentFilter.FilterBypass fb; /** Offset where the remove/insert is going to occur. */ int offset; /** Length of text to remove. */ int length; /** The text to insert, may be null. */ String text; /** AttributeSet to attach to text, may be null. */ AttributeSet attrs; /** The resulting value, this may never be set. */ Object value; /** Position the cursor should be adjusted from. If this is -1 * the cursor position will be adjusted based on the direction of * the replace (-1: offset, 1: offset + text.length()), otherwise * the cursor position is adusted from this position. */ int cursorPosition; void reset(DocumentFilter.FilterBypass fb, int offset, int length, String text, AttributeSet attrs) { this.fb = fb; this.offset = offset; this.length = length; this.text = text; this.attrs = attrs; this.value = null; cursorPosition = -1; } } /** * NavigationFilter implementation that calls back to methods with * same name in DefaultFormatter. */ private class DefaultNavigationFilter extends NavigationFilter implements Serializable { public void setDot(FilterBypass fb, int dot, Position.Bias bias) { JTextComponent tc = DefaultFormatter.this.getFormattedTextField(); if (tc.composedTextExists()) { // bypass the filter fb.setDot(dot, bias); } else { DefaultFormatter.this.setDot(fb, dot, bias); } } public void moveDot(FilterBypass fb, int dot, Position.Bias bias) { JTextComponent tc = DefaultFormatter.this.getFormattedTextField(); if (tc.composedTextExists()) { // bypass the filter fb.moveDot(dot, bias); } else { DefaultFormatter.this.moveDot(fb, dot, bias); } } public int getNextVisualPositionFrom(JTextComponent text, int pos, Position.Bias bias, int direction, Position.Bias[] biasRet) throws BadLocationException { if (text.composedTextExists()) { // forward the call to the UI directly return text.getUI().getNextVisualPositionFrom( text, pos, bias, direction, biasRet); } else { return DefaultFormatter.this.getNextVisualPositionFrom( text, pos, bias, direction, biasRet); } } } /** * DocumentFilter implementation that calls back to the replace * method of DefaultFormatter. */ private class DefaultDocumentFilter extends DocumentFilter implements Serializable { public void remove(FilterBypass fb, int offset, int length) throws BadLocationException { JTextComponent tc = DefaultFormatter.this.getFormattedTextField(); if (tc.composedTextExists()) { // bypass the filter fb.remove(offset, length); } else { DefaultFormatter.this.replace(fb, offset, length, null, null); } } public void insertString(FilterBypass fb, int offset, String string, AttributeSet attr) throws BadLocationException { JTextComponent tc = DefaultFormatter.this.getFormattedTextField(); if (tc.composedTextExists() || Utilities.isComposedTextAttributeDefined(attr)) { // bypass the filter fb.insertString(offset, string, attr); } else { DefaultFormatter.this.replace(fb, offset, 0, string, attr); } } public void replace(FilterBypass fb, int offset, int length, String text, AttributeSet attr) throws BadLocationException { JTextComponent tc = DefaultFormatter.this.getFormattedTextField(); if (tc.composedTextExists() || Utilities.isComposedTextAttributeDefined(attr)) { // bypass the filter fb.replace(offset, length, text, attr); } else { DefaultFormatter.this.replace(fb, offset, length, text, attr); } } } }