/* * 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 java.awt.event.ActionEvent; import java.io.*; import java.text.*; import java.text.AttributedCharacterIterator.Attribute; import java.util.*; import javax.swing.*; /** * InternationalFormatter extends DefaultFormatter, * using an instance of java.text.Format to handle the * conversion to a String, and the conversion from a String. *

* If getAllowsInvalid() is false, this will ask the * Format to format the current text on every edit. *

* You can specify a minimum and maximum value by way of the * setMinimum and setMaximum methods. In order * for this to work the values returned from stringToValue must be * comparable to the min/max values by way of the Comparable * interface. *

* Be careful how you configure the Format and the * InternationalFormatter, as it is possible to create a * situation where certain values can not be input. Consider the date * format 'M/d/yy', an InternationalFormatter that is always * valid (setAllowsInvalid(false)), is in overwrite mode * (setOverwriteMode(true)) and the date 7/1/99. In this * case the user will not be able to enter a two digit month or day of * month. To avoid this, the format should be 'MM/dd/yy'. *

* If InternationalFormatter is configured to only allow valid * values (setAllowsInvalid(false)), every valid edit will result * in the text of the JFormattedTextField being completely reset * from the Format. * The cursor position will also be adjusted as literal characters are * added/removed from the resulting String. *

* InternationalFormatter's behavior of * stringToValue is slightly different than that of * DefaultTextFormatter, it does the following: *

    *
  1. parseObject is invoked on the Format * specified by setFormat *
  2. If a Class has been set for the values (setValueClass), * supers implementation is invoked to convert the value returned * from parseObject to the appropriate class. *
  3. If a ParseException has not been thrown, and the value * is outside the min/max a ParseException is thrown. *
  4. The value is returned. *
* InternationalFormatter implements stringToValue * in this manner so that you can specify an alternate Class than * Format may return. *

* 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 java.text.Format * @see java.lang.Comparable * * @since 1.4 */ @SuppressWarnings("serial") // Same-version serialization only public class InternationalFormatter extends DefaultFormatter { /** * Used by getFields. */ private static final Format.Field[] EMPTY_FIELD_ARRAY =new Format.Field[0]; /** * Object used to handle the conversion. */ private Format format; /** * Can be used to impose a maximum value. */ private Comparable max; /** * Can be used to impose a minimum value. */ private Comparable min; /** * InternationalFormatter's behavior is dicatated by a * AttributedCharacterIterator that is obtained from * the Format. On every edit, assuming * allows invalid is false, the Format instance is invoked * with formatToCharacterIterator. A BitSet is * also kept upto date with the non-literal characters, that is * for every index in the AttributedCharacterIterator an * entry in the bit set is updated based on the return value from * isLiteral(Map). isLiteral(int) then uses * this cached information. *

* If allowsInvalid is false, every edit results in resetting the complete * text of the JTextComponent. *

* InternationalFormatterFilter can also provide two actions suitable for * incrementing and decrementing. To enable this a subclass must * override getSupportsIncrement to return true, and * override adjustValue to handle the changing of the * value. If you want to support changing the value outside of * the valid FieldPositions, you will need to override * canIncrement. */ /** * A bit is set for every index identified in the * AttributedCharacterIterator that is not considered decoration. * This should only be used if validMask is true. */ private transient BitSet literalMask; /** * Used to iterate over characters. */ private transient AttributedCharacterIterator iterator; /** * True if the Format was able to convert the value to a String and * back. */ private transient boolean validMask; /** * Current value being displayed. */ private transient String string; /** * If true, DocumentFilter methods are unconditionally allowed, * and no checking is done on their values. This is used when * incrementing/decrementing via the actions. */ private transient boolean ignoreDocumentMutate; /** * Creates an InternationalFormatter with no * Format specified. */ public InternationalFormatter() { setOverwriteMode(false); } /** * Creates an InternationalFormatter with the specified * Format instance. * * @param format Format instance used for converting from/to Strings */ public InternationalFormatter(Format format) { this(); setFormat(format); } /** * Sets the format that dictates the legal values that can be edited * and displayed. * * @param format Format instance used for converting * from/to Strings */ public void setFormat(Format format) { this.format = format; } /** * Returns the format that dictates the legal values that can be edited * and displayed. * * @return Format instance used for converting from/to Strings */ public Format getFormat() { return format; } /** * Sets the minimum permissible value. If the valueClass has * not been specified, and minimum is non null, the * valueClass will be set to that of the class of * minimum. * * @param minimum Minimum legal value that can be input * @see #setValueClass */ public void setMinimum(Comparable minimum) { if (getValueClass() == null && minimum != null) { setValueClass(minimum.getClass()); } min = minimum; } /** * Returns the minimum permissible value. * * @return Minimum legal value that can be input */ public Comparable getMinimum() { return min; } /** * Sets the maximum permissible value. If the valueClass has * not been specified, and max is non null, the * valueClass will be set to that of the class of * max. * * @param max Maximum legal value that can be input * @see #setValueClass */ public void setMaximum(Comparable max) { if (getValueClass() == null && max != null) { setValueClass(max.getClass()); } this.max = max; } /** * Returns the maximum permissible value. * * @return Maximum legal value that can be input */ public Comparable getMaximum() { return max; } /** * 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); updateMaskIfNecessary(); // invoked again as the mask should now be valid. positionCursorAtInitialLocation(); } /** * Returns a String representation of the Object value. * This invokes format on the current Format. * * @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 ""; } Format f = getFormat(); if (f == null) { return value.toString(); } return f.format(value); } /** * Returns the Object representation of the * String text. * * @param text String to convert * @return Object representation of text * @throws ParseException if there is an error in the conversion */ public Object stringToValue(String text) throws ParseException { Object value = stringToValue(text, getFormat()); // Convert to the value class if the Value returned from the // Format does not match. if (value != null && getValueClass() != null && !getValueClass().isInstance(value)) { value = super.stringToValue(value.toString()); } try { if (!isValidValue(value, true)) { throw new ParseException("Value not within min/max range", 0); } } catch (ClassCastException cce) { throw new ParseException("Class cast exception comparing values: " + cce, 0); } return value; } /** * Returns the Format.Field constants associated with * the text at offset. If offset is not * a valid location into the current text, this will return an * empty array. * * @param offset offset into text to be examined * @return Format.Field constants associated with the text at the * given position. */ public Format.Field[] getFields(int offset) { if (getAllowsInvalid()) { // This will work if the currently edited value is valid. updateMask(); } Map attrs = getAttributes(offset); if (attrs != null && attrs.size() > 0) { ArrayList al = new ArrayList(); al.addAll(attrs.keySet()); return al.toArray(EMPTY_FIELD_ARRAY); } return EMPTY_FIELD_ARRAY; } /** * Creates a copy of the DefaultFormatter. * * @return copy of the DefaultFormatter */ public Object clone() throws CloneNotSupportedException { InternationalFormatter formatter = (InternationalFormatter)super. clone(); formatter.literalMask = null; formatter.iterator = null; formatter.validMask = false; formatter.string = null; return formatter; } /** * If getSupportsIncrement returns true, this returns * two Actions suitable for incrementing/decrementing the value. */ protected Action[] getActions() { if (getSupportsIncrement()) { return new Action[] { new IncrementAction("increment", 1), new IncrementAction("decrement", -1) }; } return null; } /** * Invokes parseObject on f, returning * its value. */ Object stringToValue(String text, Format f) throws ParseException { if (f == null) { return text; } return f.parseObject(text); } /** * Returns true if value is between the min/max. * * @param wantsCCE If false, and a ClassCastException is thrown in * comparing the values, the exception is consumed and * false is returned. */ boolean isValidValue(Object value, boolean wantsCCE) { @SuppressWarnings("unchecked") Comparable min = (Comparable)getMinimum(); try { if (min != null && min.compareTo(value) > 0) { return false; } } catch (ClassCastException cce) { if (wantsCCE) { throw cce; } return false; } @SuppressWarnings("unchecked") Comparable max = (Comparable)getMaximum(); try { if (max != null && max.compareTo(value) < 0) { return false; } } catch (ClassCastException cce) { if (wantsCCE) { throw cce; } return false; } return true; } /** * Returns a Set of the attribute identifiers at index. */ Map getAttributes(int index) { if (isValidMask()) { AttributedCharacterIterator iterator = getIterator(); if (index >= 0 && index <= iterator.getEndIndex()) { iterator.setIndex(index); return iterator.getAttributes(); } } return null; } /** * Returns the start of the first run that contains the attribute * id. This will return -1 if the attribute * can not be found. */ int getAttributeStart(AttributedCharacterIterator.Attribute id) { if (isValidMask()) { AttributedCharacterIterator iterator = getIterator(); iterator.first(); while (iterator.current() != CharacterIterator.DONE) { if (iterator.getAttribute(id) != null) { return iterator.getIndex(); } iterator.next(); } } return -1; } /** * Returns the AttributedCharacterIterator used to * format the last value. */ AttributedCharacterIterator getIterator() { return iterator; } /** * Updates the AttributedCharacterIterator and bitset, if necessary. */ void updateMaskIfNecessary() { if (!getAllowsInvalid() && (getFormat() != null)) { if (!isValidMask()) { updateMask(); } else { String newString = getFormattedTextField().getText(); if (!newString.equals(string)) { updateMask(); } } } } /** * Updates the AttributedCharacterIterator by invoking * formatToCharacterIterator on the Format. * If this is successful, * updateMask(AttributedCharacterIterator) * is then invoked to update the internal bitmask. */ void updateMask() { if (getFormat() != null) { Document doc = getFormattedTextField().getDocument(); validMask = false; if (doc != null) { try { string = doc.getText(0, doc.getLength()); } catch (BadLocationException ble) { string = null; } if (string != null) { try { Object value = stringToValue(string); AttributedCharacterIterator iterator = getFormat(). formatToCharacterIterator(value); updateMask(iterator); } catch (ParseException pe) {} catch (IllegalArgumentException iae) {} catch (NullPointerException npe) {} } } } } /** * Returns the number of literal characters before index. */ int getLiteralCountTo(int index) { int lCount = 0; for (int counter = 0; counter < index; counter++) { if (isLiteral(counter)) { lCount++; } } return lCount; } /** * Returns true if the character at index is a literal, that is * not editable. */ boolean isLiteral(int index) { if (isValidMask() && index < string.length()) { return literalMask.get(index); } return false; } /** * Returns the literal character at index. */ char getLiteral(int index) { if (isValidMask() && string != null && index < string.length()) { return string.charAt(index); } return (char)0; } /** * Returns true if the character at offset is navigable too. This * is implemented in terms of isLiteral, subclasses * may wish to provide different behavior. */ boolean isNavigatable(int offset) { return !isLiteral(offset); } /** * Overriden to update the mask after invoking supers implementation. */ void updateValue(Object value) { super.updateValue(value); updateMaskIfNecessary(); } /** * Overriden to unconditionally allow the replace if * ignoreDocumentMutate is true. */ void replace(DocumentFilter.FilterBypass fb, int offset, int length, String text, AttributeSet attrs) throws BadLocationException { if (ignoreDocumentMutate) { fb.replace(offset, length, text, attrs); return; } super.replace(fb, offset, length, text, attrs); } /** * Returns the index of the next non-literal character starting at * index. If index is not a literal, it will be returned. * * @param direction Amount to increment looking for non-literal */ private int getNextNonliteralIndex(int index, int direction) { int max = getFormattedTextField().getDocument().getLength(); while (index >= 0 && index < max) { if (isLiteral(index)) { index += direction; } else { return index; } } return (direction == -1) ? 0 : max; } /** * Overriden in an attempt to honor the literals. *

If we do not allow invalid values and are in overwrite mode, this * {@code rh.length} is corrected as to preserve trailing literals. * If not in overwrite mode, and there is text to insert it is * inserted at the next non literal index going forward. If there * is only text to remove, it is removed from the next non literal * index going backward. */ boolean canReplace(ReplaceHolder rh) { if (!getAllowsInvalid()) { String text = rh.text; int tl = (text != null) ? text.length() : 0; JTextComponent c = getFormattedTextField(); if (tl == 0 && rh.length == 1 && c.getSelectionStart() != rh.offset) { // Backspace, adjust to actually delete next non-literal. rh.offset = getNextNonliteralIndex(rh.offset, -1); } else if (getOverwriteMode()) { int pos = rh.offset; int textPos = pos; boolean overflown = false; for (int i = 0; i < rh.length; i++) { while (isLiteral(pos)) pos++; if (pos >= string.length()) { pos = textPos; overflown = true; break; } textPos = ++pos; } if (overflown || c.getSelectedText() == null) { rh.length = pos - rh.offset; } } else if (tl > 0) { // insert (or insert and remove) rh.offset = getNextNonliteralIndex(rh.offset, 1); } else { // remove only rh.offset = getNextNonliteralIndex(rh.offset, -1); } ((ExtendedReplaceHolder)rh).endOffset = rh.offset; ((ExtendedReplaceHolder)rh).endTextLength = (rh.text != null) ? rh.text.length() : 0; } else { ((ExtendedReplaceHolder)rh).endOffset = rh.offset; ((ExtendedReplaceHolder)rh).endTextLength = (rh.text != null) ? rh.text.length() : 0; } boolean can = super.canReplace(rh); if (can && !getAllowsInvalid()) { ((ExtendedReplaceHolder)rh).resetFromValue(this); } return can; } /** * When in !allowsInvalid mode the text is reset on every edit, thus * supers implementation will position the cursor at the wrong position. * As such, this invokes supers implementation and then invokes * repositionCursor to correctly reset the cursor. */ boolean replace(ReplaceHolder rh) throws BadLocationException { int start = -1; int direction = 1; int literalCount = -1; if (rh.length > 0 && (rh.text == null || rh.text.length() == 0) && (getFormattedTextField().getSelectionStart() != rh.offset || rh.length > 1)) { direction = -1; } if (!getAllowsInvalid()) { if ((rh.text == null || rh.text.length() == 0) && rh.length > 0) { // remove start = getFormattedTextField().getSelectionStart(); } else { start = rh.offset; } literalCount = getLiteralCountTo(start); } if (super.replace(rh)) { if (start != -1) { int end = ((ExtendedReplaceHolder)rh).endOffset; end += ((ExtendedReplaceHolder)rh).endTextLength; repositionCursor(literalCount, end, direction); } else { start = ((ExtendedReplaceHolder)rh).endOffset; if (direction == 1) { start += ((ExtendedReplaceHolder)rh).endTextLength; } repositionCursor(start, direction); } return true; } return false; } /** * Repositions the cursor. startLiteralCount gives * the number of literals to the start of the deleted range, end * gives the ending location to adjust from, direction gives * the direction relative to end to position the * cursor from. */ private void repositionCursor(int startLiteralCount, int end, int direction) { int endLiteralCount = getLiteralCountTo(end); if (endLiteralCount != end) { end -= startLiteralCount; for (int counter = 0; counter < end; counter++) { if (isLiteral(counter)) { end++; } } } repositionCursor(end, 1 /*direction*/); } /** * Returns the character from the mask that has been buffered * at index. */ char getBufferedChar(int index) { if (isValidMask()) { if (string != null && index < string.length()) { return string.charAt(index); } } return (char)0; } /** * Returns true if the current mask is valid. */ boolean isValidMask() { return validMask; } /** * Returns true if attributes is null or empty. */ boolean isLiteral(Map attributes) { return ((attributes == null) || attributes.size() == 0); } /** * Updates the interal bitset from iterator. This will * set validMask to true if iterator is * non-null. */ private void updateMask(AttributedCharacterIterator iterator) { if (iterator != null) { validMask = true; this.iterator = iterator; // Update the literal mask if (literalMask == null) { literalMask = new BitSet(); } else { for (int counter = literalMask.length() - 1; counter >= 0; counter--) { literalMask.clear(counter); } } iterator.first(); while (iterator.current() != CharacterIterator.DONE) { Map attributes = iterator.getAttributes(); boolean set = isLiteral(attributes); int start = iterator.getIndex(); int end = iterator.getRunLimit(); while (start < end) { if (set) { literalMask.set(start); } else { literalMask.clear(start); } start++; } iterator.setIndex(start); } } } /** * Returns true if field is non-null. * Subclasses that wish to allow incrementing to happen outside of * the known fields will need to override this. */ boolean canIncrement(Object field, int cursorPosition) { return (field != null); } /** * Selects the fields identified by attributes. */ void selectField(Object f, int count) { AttributedCharacterIterator iterator = getIterator(); if (iterator != null && (f instanceof AttributedCharacterIterator.Attribute)) { AttributedCharacterIterator.Attribute field = (AttributedCharacterIterator.Attribute)f; iterator.first(); while (iterator.current() != CharacterIterator.DONE) { while (iterator.getAttribute(field) == null && iterator.next() != CharacterIterator.DONE); if (iterator.current() != CharacterIterator.DONE) { int limit = iterator.getRunLimit(field); if (--count <= 0) { getFormattedTextField().select(iterator.getIndex(), limit); break; } iterator.setIndex(limit); iterator.next(); } } } } /** * Returns the field that will be adjusted by adjustValue. */ Object getAdjustField(int start, Map attributes) { return null; } /** * Returns the number of occurrences of f before * the location start in the current * AttributedCharacterIterator. */ private int getFieldTypeCountTo(Object f, int start) { AttributedCharacterIterator iterator = getIterator(); int count = 0; if (iterator != null && (f instanceof AttributedCharacterIterator.Attribute)) { AttributedCharacterIterator.Attribute field = (AttributedCharacterIterator.Attribute)f; iterator.first(); while (iterator.getIndex() < start) { while (iterator.getAttribute(field) == null && iterator.next() != CharacterIterator.DONE); if (iterator.current() != CharacterIterator.DONE) { iterator.setIndex(iterator.getRunLimit(field)); iterator.next(); count++; } else { break; } } } return count; } /** * Subclasses supporting incrementing must override this to handle * the actual incrementing. value is the current value, * attributes gives the field the cursor is in (may be * null depending upon canIncrement) and * direction is the amount to increment by. */ Object adjustValue(Object value, Map attributes, Object field, int direction) throws BadLocationException, ParseException { return null; } /** * Returns false, indicating InternationalFormatter does not allow * incrementing of the value. Subclasses that wish to support * incrementing/decrementing the value should override this and * return true. Subclasses should also override * adjustValue. */ boolean getSupportsIncrement() { return false; } /** * Resets the value of the JFormattedTextField to be * value. */ void resetValue(Object value) throws BadLocationException, ParseException { Document doc = getFormattedTextField().getDocument(); String string = valueToString(value); try { ignoreDocumentMutate = true; doc.remove(0, doc.getLength()); doc.insertString(0, string, null); } finally { ignoreDocumentMutate = false; } updateValue(value); } /** * Subclassed to update the internal representation of the mask after * the default read operation has completed. */ private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException { s.defaultReadObject(); updateMaskIfNecessary(); } /** * Overriden to return an instance of ExtendedReplaceHolder. */ ReplaceHolder getReplaceHolder(DocumentFilter.FilterBypass fb, int offset, int length, String text, AttributeSet attrs) { if (replaceHolder == null) { replaceHolder = new ExtendedReplaceHolder(); } return super.getReplaceHolder(fb, offset, length, text, attrs); } /** * As InternationalFormatter replaces the complete text on every edit, * ExtendedReplaceHolder keeps track of the offset and length passed * into canReplace. */ static class ExtendedReplaceHolder extends ReplaceHolder { /** Offset of the insert/remove. This may differ from offset in * that if !allowsInvalid the text is replaced on every edit. */ int endOffset; /** Length of the text. This may differ from text.length in * that if !allowsInvalid the text is replaced on every edit. */ int endTextLength; /** * Resets the region to delete to be the complete document and * the text from invoking valueToString on the current value. */ void resetFromValue(InternationalFormatter formatter) { // Need to reset the complete string as Format's result can // be completely different. offset = 0; try { text = formatter.valueToString(value); } catch (ParseException pe) { // Should never happen, otherwise canReplace would have // returned value. text = ""; } length = fb.getDocument().getLength(); } } /** * IncrementAction is used to increment the value by a certain amount. * It calls into adjustValue to handle the actual * incrementing of the value. */ private class IncrementAction extends AbstractAction { private int direction; IncrementAction(String name, int direction) { super(name); this.direction = direction; } public void actionPerformed(ActionEvent ae) { if (getFormattedTextField().isEditable()) { if (getAllowsInvalid()) { // This will work if the currently edited value is valid. updateMask(); } boolean validEdit = false; if (isValidMask()) { int start = getFormattedTextField().getSelectionStart(); if (start != -1) { AttributedCharacterIterator iterator = getIterator(); iterator.setIndex(start); Map attributes = iterator.getAttributes(); Object field = getAdjustField(start, attributes); if (canIncrement(field, start)) { try { Object value = stringToValue( getFormattedTextField().getText()); int fieldTypeCount = getFieldTypeCountTo( field, start); value = adjustValue(value, attributes, field, direction); if (value != null && isValidValue(value, false)) { resetValue(value); updateMask(); if (isValidMask()) { selectField(field, fieldTypeCount); } validEdit = true; } } catch (ParseException pe) { } catch (BadLocationException ble) { } } } } if (!validEdit) { invalidEdit(); } } } } }