Reworked so that the field *always* displays a valid value.

Previously invalid values were allowed because the user could be on their
way to typing valid value. The field installed itself as a FocusListener
and checked the value when focus was lost. This wasn't ideal, as a user
could enter what they thought was a valid value and then click a button and
the action of them clicking the button would instantly change the displayed
value with no chance for them to see that it changed from they had last
entered.

Now it does clever highlighting to let you edit the field while always
keeping the value between the minimum and the maximum.


git-svn-id: https://samskivert.googlecode.com/svn/trunk@1583 6335cc39-0255-0410-8fd6-9bcaacd3b74c
This commit is contained in:
ray
2005-01-29 21:23:53 +00:00
parent fbe4de5323
commit e6220d7a87
@@ -3,51 +3,210 @@
package com.samskivert.swing;
import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;
import java.awt.EventQueue;
import java.text.NumberFormat;
import java.text.ParseException;
import javax.swing.JTextField;
import javax.swing.text.AbstractDocument;
import javax.swing.text.AttributeSet;
import javax.swing.text.BadLocationException;
import javax.swing.text.DocumentFilter;
import com.samskivert.Log;
import com.samskivert.swing.util.SwingUtil;
/**
* A text field that will only take digit entries within the optional
* specified range.
* A text field that will only allow editing of integer values within
* the specified range.
*/
public class IntField extends JTextField
implements SwingUtil.DocumentValidator, SwingUtil.DocumentTransformer,
FocusListener
{
/**
* Create an IntField with the range 0 - Integer.MAX_VALUE, with
* 0 as the initial value.
*/
public IntField ()
{
this(Integer.MAX_VALUE);
}
/**
* Create an IntField with the specified maximum.
*/
public IntField (int maxValue)
{
this(0, maxValue);
}
/**
* Create an IntField with the specified minimum and maximum, with
* the minimum value initially displayed.
*/
public IntField (int minValue, int maxValue)
{
this(minValue, minValue, maxValue);
}
/**
* Create an IntField with the specified initial, minimum, and
* maximum values.
*/
public IntField (int initial, int minValue, int maxValue)
{
if (minValue < 0) {
throw new IllegalArgumentException("minValue must be at least 0");
}
if (initial > maxValue || initial < minValue) {
throw new IllegalArgumentException("initial value not between " +
"min and max");
}
_minValue = minValue;
_maxValue = maxValue;
setHorizontalAlignment(JTextField.RIGHT);
setColumns(5);
// register ourselves as the validator
SwingUtil.setDocumentHelpers(this, this, this);
addFocusListener(this);
}
// create a document filter which enforces the restrictions on
// what text may be entered. This is similar to the filter
// that is configured in SwingUtil.setDocumentHelpers, only the
// remove operation is modified to do the sneaky highlighting we do.
final AbstractDocument doc = (AbstractDocument) getDocument();
doc.setDocumentFilter(new DocumentFilter() {
public void remove (FilterBypass fb, int offset, int length)
throws BadLocationException
{
String current = doc.getText(0, doc.getLength());
String potential = current.substring(0, offset) +
current.substring(offset + length);
// if the bit to be removed is still a valid value, go ahead
if (unformatSafe(potential) >= _minValue) {
fb.remove(offset, length);
transform(fb);
return;
}
public IntField (int initial, int minValue, int maxValue)
{
this(minValue, maxValue);
// otherwise, we are going to highlight the bit instead
// of removing it
int selStart = getSelectionStart();
if (selStart > 0) {
// see if the next highlighted character is a digit
char replace = current.charAt(selStart - 1);
if (Character.isDigit(replace)) {
// and can be zeroed out
String zeroed = current.substring(0, selStart - 1) +
"0" + current.substring(selStart);
// if so, change it to a zero
if (unformatSafe(zeroed) >= _minValue) {
fb.replace(selStart - 1, 1, "0", null);
} else {
// otherwise, re-set the entire field to
// contain the minimum value, and highlight it
String min = format(_minValue);
fb.replace(0, current.length(), min, null);
setSelectionStart(0);
setSelectionEnd(min.length());
return;
}
}
}
// grow the selection to encompass one more character
setSelectionStart(selStart - 1);
}
public void insertString (FilterBypass fb, int offset,
String s, AttributeSet attr)
throws BadLocationException
{
fb.insertString(offset, s, attr);
transform(fb);
}
public void replace (FilterBypass fb, int offset, int length,
String text, AttributeSet attrs)
throws BadLocationException
{
fb.replace(offset, length, text, attrs);
transform(fb);
}
protected void transform (FilterBypass fb)
{
try {
String text = doc.getText(0, doc.getLength());
String xform = transform(text);
if (!text.equals(xform)) {
fb.replace(0, text.length(), xform, null);
}
} catch (BadLocationException ble) {
}
}
/**
* Ensure that the specified text is formatted and within
* the bounds.
*/
protected String transform (String text)
{
// if we're at least the minvalue, everything's ok
int val = unformatSafe(text);
if (val >= _minValue) {
return format(Math.min(_maxValue, val));
}
// otherwise, see if we can append zeros and make a valid value,
// remembering how many digits we added
int newVal = val;
int digits = 0;
if (newVal > 0) {
while (newVal * 10 <= _maxValue) {
newVal *= 10;
digits++;
if (newVal >= _minValue) {
break;
}
}
}
// if that didn't work, just set it to the min value and
// highlight all the digits
if (newVal < _minValue) {
newVal = _minValue;
digits = String.valueOf(newVal).length();
}
// return the new value, but post an event to immediately
// highlight the digits that the user did not enter, so
// that they can type over them
final String newText = format(newVal);
final int fdigits = digits;
EventQueue.invokeLater(new Runnable() {
public void run () {
String text = getText();
if (text.equals(newText)) {
int len = text.length();
setSelectionEnd(len);
int digits = fdigits;
for (int ii = len - 1; ii >= 0; ii--) {
if (Character.isDigit(text.charAt(ii))) {
--digits;
if (digits == 0) {
setSelectionStart(ii);
break;
}
}
}
}
}
});
return newText;
}
});
// and set the initial value
setValue(initial);
}
@@ -56,16 +215,7 @@ public class IntField extends JTextField
*/
public int getValue ()
{
String s = getText();
if (!"".equals(s)) {
try {
return _formatter.parse(s).intValue();
} catch (ParseException pe) {
Log.warning("This shouldn't happen.");
}
}
return 0;
return unformatSafe(getText());
}
/**
@@ -73,7 +223,7 @@ public class IntField extends JTextField
*/
public void setValue (int value)
{
setText(_formatter.format(value));
setText(format(value));
}
/**
@@ -81,6 +231,9 @@ public class IntField extends JTextField
*/
public void setMinValue (int minValue)
{
if (minValue < 0) {
throw new IllegalArgumentException("minValue must be at least 0");
}
_minValue = minValue;
validateText();
}
@@ -94,68 +247,49 @@ public class IntField extends JTextField
validateText();
}
// from interface SwingUtil.DocumentValidator
public boolean isValid (String text)
{
if ("".equals(text)) {
return true;
}
try {
int value = _formatter.parse(text).intValue();
return (value <= _maxValue);
} catch (ParseException nfe) {
return false;
}
}
// from interface SwingUtil.DocumentTransformer
public String transform (String text)
{
try {
return _formatter.format(_formatter.parse(text));
} catch (ParseException nfe) {
return text;
}
}
// documentation inherited from interface
public void focusGained (FocusEvent e)
{
// nada
}
// documentation inherited from interface
public void focusLost (FocusEvent e)
{
validateText();
}
/**
* Ensure that the value we're displaying is between the minimum and
* the maximum.
*/
protected void validateText ()
{
String text = getText();
int val;
setText(getText());
}
/**
* Format the specified monetary value into a string.
* This just puts commas in.
*/
public static String format (int value)
{
return _formatter.format(value);
}
/**
* Parse numbers, with commas being ok.
*/
public static int unformat (String text)
throws ParseException
{
return _formatter.parse(text).intValue();
}
/**
* Parse numbers, don't throw exceptions.
*/
public static int unformatSafe (String text)
{
try {
val = _formatter.parse(text).intValue();
return unformat(text);
} catch (ParseException pe) {
val = 0;
}
// bound it in
String validated = _formatter.format(
Math.min(_maxValue, Math.max(_minValue, val)));
if (!text.equals(validated)) {
// only do it if it changes the text- otherwise
// we booch the focus when this is called from focusLost
setText(validated);
return 0;
}
}
/** min/max */
protected int _minValue, _maxValue;
protected NumberFormat _formatter = NumberFormat.getIntegerInstance();
/** Formats and parses numbers with commas in them. */
protected static NumberFormat _formatter =
NumberFormat.getIntegerInstance();
}