diff --git a/projects/samskivert/src/java/com/samskivert/swing/IntField.java b/projects/samskivert/src/java/com/samskivert/swing/IntField.java index 70503da4..05b3cd9f 100644 --- a/projects/samskivert/src/java/com/samskivert/swing/IntField.java +++ b/projects/samskivert/src/java/com/samskivert/swing/IntField.java @@ -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(); }