You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1059 lines
35 KiB
1059 lines
35 KiB
/*
|
|
* Copyright (c) 2000, 2013, Oracle and/or its affiliates. All rights reserved.
|
|
* ORACLE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*/
|
|
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.*;
|
|
|
|
/**
|
|
* <code>InternationalFormatter</code> extends <code>DefaultFormatter</code>,
|
|
* using an instance of <code>java.text.Format</code> to handle the
|
|
* conversion to a String, and the conversion from a String.
|
|
* <p>
|
|
* If <code>getAllowsInvalid()</code> is false, this will ask the
|
|
* <code>Format</code> to format the current text on every edit.
|
|
* <p>
|
|
* You can specify a minimum and maximum value by way of the
|
|
* <code>setMinimum</code> and <code>setMaximum</code> methods. In order
|
|
* for this to work the values returned from <code>stringToValue</code> must be
|
|
* comparable to the min/max values by way of the <code>Comparable</code>
|
|
* interface.
|
|
* <p>
|
|
* Be careful how you configure the <code>Format</code> and the
|
|
* <code>InternationalFormatter</code>, as it is possible to create a
|
|
* situation where certain values can not be input. Consider the date
|
|
* format 'M/d/yy', an <code>InternationalFormatter</code> that is always
|
|
* valid (<code>setAllowsInvalid(false)</code>), is in overwrite mode
|
|
* (<code>setOverwriteMode(true)</code>) 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'.
|
|
* <p>
|
|
* If <code>InternationalFormatter</code> is configured to only allow valid
|
|
* values (<code>setAllowsInvalid(false)</code>), every valid edit will result
|
|
* in the text of the <code>JFormattedTextField</code> being completely reset
|
|
* from the <code>Format</code>.
|
|
* The cursor position will also be adjusted as literal characters are
|
|
* added/removed from the resulting String.
|
|
* <p>
|
|
* <code>InternationalFormatter</code>'s behavior of
|
|
* <code>stringToValue</code> is slightly different than that of
|
|
* <code>DefaultTextFormatter</code>, it does the following:
|
|
* <ol>
|
|
* <li><code>parseObject</code> is invoked on the <code>Format</code>
|
|
* specified by <code>setFormat</code>
|
|
* <li>If a Class has been set for the values (<code>setValueClass</code>),
|
|
* supers implementation is invoked to convert the value returned
|
|
* from <code>parseObject</code> to the appropriate class.
|
|
* <li>If a <code>ParseException</code> has not been thrown, and the value
|
|
* is outside the min/max a <code>ParseException</code> is thrown.
|
|
* <li>The value is returned.
|
|
* </ol>
|
|
* <code>InternationalFormatter</code> implements <code>stringToValue</code>
|
|
* in this manner so that you can specify an alternate Class than
|
|
* <code>Format</code> may return.
|
|
* <p>
|
|
* <strong>Warning:</strong>
|
|
* 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 <code>java.beans</code> package.
|
|
* Please see {@link java.beans.XMLEncoder}.
|
|
*
|
|
* @see java.text.Format
|
|
* @see java.lang.Comparable
|
|
*
|
|
* @since 1.4
|
|
*/
|
|
public class InternationalFormatter extends DefaultFormatter {
|
|
/**
|
|
* Used by <code>getFields</code>.
|
|
*/
|
|
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;
|
|
|
|
/**
|
|
* <code>InternationalFormatter</code>'s behavior is dicatated by a
|
|
* <code>AttributedCharacterIterator</code> that is obtained from
|
|
* the <code>Format</code>. On every edit, assuming
|
|
* allows invalid is false, the <code>Format</code> instance is invoked
|
|
* with <code>formatToCharacterIterator</code>. A <code>BitSet</code> is
|
|
* also kept upto date with the non-literal characters, that is
|
|
* for every index in the <code>AttributedCharacterIterator</code> an
|
|
* entry in the bit set is updated based on the return value from
|
|
* <code>isLiteral(Map)</code>. <code>isLiteral(int)</code> then uses
|
|
* this cached information.
|
|
* <p>
|
|
* If allowsInvalid is false, every edit results in resetting the complete
|
|
* text of the JTextComponent.
|
|
* <p>
|
|
* InternationalFormatterFilter can also provide two actions suitable for
|
|
* incrementing and decrementing. To enable this a subclass must
|
|
* override <code>getSupportsIncrement</code> to return true, and
|
|
* override <code>adjustValue</code> 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
|
|
* <code>canIncrement</code>.
|
|
*/
|
|
/**
|
|
* 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 <code>InternationalFormatter</code> with no
|
|
* <code>Format</code> specified.
|
|
*/
|
|
public InternationalFormatter() {
|
|
setOverwriteMode(false);
|
|
}
|
|
|
|
/**
|
|
* Creates an <code>InternationalFormatter</code> with the specified
|
|
* <code>Format</code> 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 <code>Format</code> 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 <code>valueClass</code> has
|
|
* not been specified, and <code>minimum</code> is non null, the
|
|
* <code>valueClass</code> will be set to that of the class of
|
|
* <code>minimum</code>.
|
|
*
|
|
* @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 <code>valueClass</code> has
|
|
* not been specified, and <code>max</code> is non null, the
|
|
* <code>valueClass</code> will be set to that of the class of
|
|
* <code>max</code>.
|
|
*
|
|
* @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 <code>DefaultFormatter</code> onto a particular
|
|
* <code>JFormattedTextField</code>.
|
|
* This will invoke <code>valueToString</code> to convert the
|
|
* current value from the <code>JFormattedTextField</code> to
|
|
* a String. This will then install the <code>Action</code>s from
|
|
* <code>getActions</code>, the <code>DocumentFilter</code>
|
|
* returned from <code>getDocumentFilter</code> and the
|
|
* <code>NavigationFilter</code> returned from
|
|
* <code>getNavigationFilter</code> onto the
|
|
* <code>JFormattedTextField</code>.
|
|
* <p>
|
|
* Subclasses will typically only need to override this if they
|
|
* wish to install additional listeners on the
|
|
* <code>JFormattedTextField</code>.
|
|
* <p>
|
|
* If there is a <code>ParseException</code> in converting the
|
|
* current value to a String, this will set the text to an empty
|
|
* String, and mark the <code>JFormattedTextField</code> as being
|
|
* in an invalid state.
|
|
* <p>
|
|
* While this is a public method, this is typically only useful
|
|
* for subclassers of <code>JFormattedTextField</code>.
|
|
* <code>JFormattedTextField</code> 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 <code>value</code>.
|
|
* This invokes <code>format</code> on the current <code>Format</code>.
|
|
*
|
|
* @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 <code>Object</code> representation of the
|
|
* <code>String</code> <code>text</code>.
|
|
*
|
|
* @param text <code>String</code> to convert
|
|
* @return <code>Object</code> 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 <code>Format.Field</code> constants associated with
|
|
* the text at <code>offset</code>. If <code>offset</code> 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<Attribute, Object> attrs = getAttributes(offset);
|
|
|
|
if (attrs != null && attrs.size() > 0) {
|
|
ArrayList<Attribute> al = new ArrayList<Attribute>();
|
|
|
|
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 <code>getSupportsIncrement</code> 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 <code>parseObject</code> on <code>f</code>, returning
|
|
* its value.
|
|
*/
|
|
Object stringToValue(String text, Format f) throws ParseException {
|
|
if (f == null) {
|
|
return text;
|
|
}
|
|
return f.parseObject(text);
|
|
}
|
|
|
|
/**
|
|
* Returns true if <code>value</code> 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) {
|
|
Comparable min = getMinimum();
|
|
|
|
try {
|
|
if (min != null && min.compareTo(value) > 0) {
|
|
return false;
|
|
}
|
|
} catch (ClassCastException cce) {
|
|
if (wantsCCE) {
|
|
throw cce;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
Comparable max = 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 <code>index</code>.
|
|
*/
|
|
Map<Attribute, Object> 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
|
|
* <code>id</code>. This will return <code>-1</code> 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 <code>AttributedCharacterIterator</code> 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
|
|
* <code>formatToCharacterIterator</code> on the <code>Format</code>.
|
|
* If this is successful,
|
|
* <code>updateMask(AttributedCharacterIterator)</code>
|
|
* 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 <code>index</code>.
|
|
*/
|
|
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 <code>isLiteral</code>, 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.
|
|
* <p>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
|
|
* <code>repositionCursor</code> 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. <code>startLiteralCount</code> 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 <code>end</code> 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 <code>index</code>.
|
|
*/
|
|
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 <code>attributes</code> is null or empty.
|
|
*/
|
|
boolean isLiteral(Map attributes) {
|
|
return ((attributes == null) || attributes.size() == 0);
|
|
}
|
|
|
|
/**
|
|
* Updates the interal bitset from <code>iterator</code>. This will
|
|
* set <code>validMask</code> to true if <code>iterator</code> 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 <code>field</code> 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 <code>attributes</code>.
|
|
*/
|
|
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 <code>f</code> before
|
|
* the location <code>start</code> in the current
|
|
* <code>AttributedCharacterIterator</code>.
|
|
*/
|
|
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. <code>value</code> is the current value,
|
|
* <code>attributes</code> gives the field the cursor is in (may be
|
|
* null depending upon <code>canIncrement</code>) and
|
|
* <code>direction</code> 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
|
|
* <code>adjustValue</code>.
|
|
*/
|
|
boolean getSupportsIncrement() {
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Resets the value of the JFormattedTextField to be
|
|
* <code>value</code>.
|
|
*/
|
|
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 <code>ExtendedReplaceHolder</code>.
|
|
*/
|
|
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 <code>adjustValue</code> 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();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|