From e6220d7a872623286073744f58eef506d9b71dc9 Mon Sep 17 00:00:00 2001 From: ray Date: Sat, 29 Jan 2005 21:23:53 +0000 Subject: [PATCH] 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 --- .../java/com/samskivert/swing/IntField.java | 284 +++++++++++++----- 1 file changed, 209 insertions(+), 75 deletions(-) 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(); }