/*
* org.openmicroscopy.shoola.util.ui.NumericalTextField
*
*------------------------------------------------------------------------------
* Copyright (C) 2006-2015 University of Dundee. All rights reserved.
*
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
* This program 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 for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*
*------------------------------------------------------------------------------
*/
package org.openmicroscopy.shoola.util.ui;
import java.awt.Color;
import java.awt.Toolkit;
import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import javax.swing.JTextField;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.text.AttributeSet;
import javax.swing.text.PlainDocument;
import org.openmicroscopy.shoola.util.CommonsLangUtils;
/**
* A text field containing only numerical value.
*
* @author Jean-Marie Burel
* <a href="mailto:j.burel@dundee.ac.uk">j.burel@dundee.ac.uk</a>
* @author Donald MacDonald
* <a href="mailto:donald@lifesci.dundee.ac.uk">donald@lifesci.dundee.ac.uk</a>
* @version 3.0
* @since 3.0-Beta3
*/
public class NumericalTextField
extends JTextField
implements DocumentListener, FocusListener
{
/** Bounds property indicating that the text has been updated. */
public static final String TEXT_UPDATED_PROPERTY = "textUpdated";
/** Block typing out-of-range values */
public static final int VALIDATION_MODE_BLOCK = 0;
/** Correct out-of-range values to minimum or maximum */
public static final int VALIDATION_MODE_CORRECT = 1;
/** Accepted value if integer. */
private static final String NUMERIC = "0123456789";
/** Accepted value if double or float. */
private static final String FLOAT = NUMERIC+".";
/** The color used for the foreground when the user is editing the value. */
private Color editedColor;
/** The default foreground color. */
private Color defaultForeground;
/** Helper reference to the document. */
private NumericalPlainDocument document;
/** The default Text. */
private String originalText;
/** The type of number to handle: integer, double or float. */
private Class<?> numberType;
/** Flag indicating if negative values are accepted. */
private boolean negativeAccepted;
/** The accepted characters. */
private String accepted;
/** Flag indicating if a warning should be shown if the
* valid value range is exceeded */
private boolean showWarning = false;
/**
* Checks if the value is correct.
*
* @return See above.
*/
private String checkValue()
{
String str = getText();
String result = str;
try {
if (Integer.class.equals(numberType)) {
int min = (int) getMinimum();
int max = (int) getMaximum();
if (CommonsLangUtils.isBlank(str)) {
result = "" + min;
}
int val = Integer.parseInt(str);
if (val < min)
result = "" + min;
if (val > max)
result = "" + max;
} else if (Double.class.equals(numberType)) {
Double min = getMinimum();
Double max = getMaximum();
if (CommonsLangUtils.isBlank(str)) {
return ""+min;
}
double val = Double.parseDouble(str);
if (val < min)
result = "" + min;
if (val > max)
result = "" + max;
} else if (Long.class.equals(numberType)) {
Long min = new Long((long) getMinimum());
Long max = new Long((long) getMaximum());
if (CommonsLangUtils.isBlank(str)) {
result = ""+min;
}
long val = Long.parseLong(str);
if (val < min)
result = "" + min;
if (val > max)
result = "" + max;
} else if (Float.class.equals(numberType)) {
Float min = new Float(getMinimum());
Float max = new Float(getMaximum());
if (CommonsLangUtils.isBlank(str)) {
result = ""+min;
}
float val = Float.parseFloat(str);
if (val < min)
result = "" + min;
if (val > max)
result = "" + max;
}
} catch(NumberFormatException nfe) {
String msg = "The value you entered is not a valid number";
PopupHint hint = new PopupHint(this, msg, 8000);
hint.show();
return "";
}
if(!result.equals(str) && showWarning) {
String msg = "<html>The value you entered is outside of the allowed range,<br>therefore it is reset to the minimal/maximal allowed value.</hmtl>";
PopupHint hint = new PopupHint(this, msg, 8000);
hint.show();
}
return result;
}
/**
* Updates the <code>foreground</code> color depending on the text entered.
*/
private void updateForeGround()
{
String text = getText();
if (editedColor != null) {
if (originalText != null) {
if (originalText.equals(text)) setForeground(defaultForeground);
else setForeground(editedColor);
}
}
if (originalText == null) {
originalText = text;
defaultForeground = getForeground();
}
firePropertyChange(TEXT_UPDATED_PROPERTY, Boolean.valueOf(false),
Boolean.valueOf(true));
}
/**
* Creates a default instance with {@link Double#MIN_VALUE} as min value
* and {@link Double#MAX_VALUE} as max value.
*/
public NumericalTextField()
{
this(0.0, Integer.MAX_VALUE);
}
/**
* Creates a new instance.
*
* @param min The minimum value of the text field.
* @param max The maximum value of the text field.
*/
public NumericalTextField(double min, double max)
{
this(min, max, Integer.class);
}
/**
* Creates a new instance.
*
* @param min The minimum value of the text field.
* @param max The maximum value of the text field.
* @param type The number type.
*/
public NumericalTextField(double min, double max, Class<?> type)
{
this(min, max, type, VALIDATION_MODE_CORRECT);
}
/**
* Creates a new instance.
*
* @param min The minimum value of the text field.
* @param max The maximum value of the text field.
* @param type The number type.
* @param validationMode See {@link #VALIDATION_MODE_BLOCK}, {@link #VALIDATION_MODE_CORRECT}
*/
public NumericalTextField(double min, double max, Class<?> type, int validationMode)
{
boolean blockInput = validationMode == VALIDATION_MODE_BLOCK;
document = new NumericalPlainDocument(min, max, blockInput);
setHorizontalAlignment(JTextField.RIGHT);
setDocument(document);
originalText = null;
editedColor = null;
document.addDocumentListener(this);
addFocusListener(this);
addKeyListener(new KeyAdapter() {
/**
* Checks if the text is valid.
* @see KeyListener#keyPressed(KeyEvent)
*/
public void keyPressed(KeyEvent e)
{
if (e.getKeyCode() == KeyEvent.VK_ENTER) {
String s = getText();
String v = checkValue();
if (v != null && !v.equals(s)) {
setText(v);
}
}
}
});
numberType = type;
if (Integer.class.equals(type) || Long.class.equals(type))
accepted = NUMERIC;
else
accepted = FLOAT;
setNegativeAccepted(min < 0);
}
/**
* Sets to <code>true</code> if negative values are accepted,
* to <code>false</code> otherwise.
*
* @param negativeAccepted The value to set.
*/
public void setNegativeAccepted(boolean negativeAccepted)
{
this.negativeAccepted = negativeAccepted;
if (negativeAccepted) {
accepted += "-";
double min = document.getMinimum();
if (min >= 0) {
if (numberType == null || Integer.class.equals(numberType))
min = Integer.MIN_VALUE;
else if (Long.class.equals(numberType))
min = Long.MIN_VALUE;
else if (Float.class.equals(numberType))
min = Float.MIN_VALUE;
else min = Double.MIN_VALUE;
document.setMinimum(min);
}
}
}
/**
* Returns <code>true</code> if negative values are accepted,
* <code>false</code> otherwise.
*
* @return See above.
*/
public boolean isNegativeAccepted() { return negativeAccepted; }
/**
* Sets the type of number to handle.
*
* @param numberType The value to set.
*/
public void setNumberType(Class<?> numberType)
{
if (numberType == null)
numberType = Integer.class;
this.numberType = numberType;
if (numberType.equals(Integer.class) ||
numberType.equals(Long.class)) accepted = NUMERIC;
else accepted = FLOAT;
setNegativeAccepted(negativeAccepted);
if (numberType.equals(Double.class)) {
setMinimum(0.0);
setMaximum(Double.MAX_VALUE);
} else if (numberType.equals(Float.class)) {
setMinimum(0.0);
setMaximum(Float.MAX_VALUE);
} else if (numberType.equals(Long.class)) {
setMinimum(0.0);
setMaximum(Long.MAX_VALUE);
}
}
/**
* Sets the minimum value.
*
* @param min The value to set.
*/
public void setMinimum(double min)
{
document.setMinimum(min);
if (min < 0) setNegativeAccepted(true);
}
/**
* Sets the maximum value.
*
* @param max The value to set.
*/
public void setMaximum(double max) { document.setMaximum(max); }
/**
* Returns the maximum value.
*
* @return See above.
*/
public double getMaximum() { return document.getMaximum(); }
/**
* Returns the minimum value.
*
* @return See above.
*/
public double getMinimum() { return document.getMinimum(); }
/**
* Sets the edited color.
*
* @param editedColor The value to set.
*/
public void setEditedColor(Color editedColor)
{
this.editedColor = editedColor;
}
/**
* Returns the value as a number. Make sure the minimum value is
* returned if the value entered is not correct.
*
* @return See above.
*/
public Number getValueAsNumber()
{
String str = getText();
if (CommonsLangUtils.isBlank(str)) {
return null;
}
str = checkValue();
if (Integer.class.equals(numberType))
return Integer.parseInt(str);
else if (Double.class.equals(numberType))
return Double.parseDouble(str);
else if (Float.class.equals(numberType))
return Float.parseFloat(str);
else if (Long.class.equals(numberType))
return Long.parseLong(str);
return null;
}
/**
* If set to <code>true</code> a warning hint will be shown
* in the case a value outside the given min/max range is entered
* @param showWarning See above
*/
public void setShowWarning(boolean showWarning) {
this.showWarning = showWarning;
}
/**
* Updates the <code>foreground</code> color depending on the text entered.
* @see DocumentListener#insertUpdate(DocumentEvent)
*/
public void insertUpdate(DocumentEvent e) { updateForeGround(); }
/**
* Updates the <code>foreground</code> color depending on the text entered.
* @see DocumentListener#removeUpdate(DocumentEvent)
*/
public void removeUpdate(DocumentEvent e) { updateForeGround(); }
/**
* Adds a <code>0</code> if the value of the field ends up with a
* <code>.</code>
* @see FocusListener#focusLost(FocusEvent)
*/
public void focusLost(FocusEvent e)
{
String s = getText();
if (s != null && s.endsWith(".")) {
s += "0";
setText(s);
}
String v = checkValue();
if (v != null && !v.equals(s)) {
setText(v);
}
}
/**
* Required by the {@link DocumentListener} I/F but no-operation
* implementation in our case.
* @see DocumentListener#changedUpdate(DocumentEvent)
*/
public void changedUpdate(DocumentEvent e) {}
/**
* Required by the {@link FocusListener} I/F but no-op implementation
* in our case.
* @see FocusListener#focusGained(FocusEvent)
*/
public void focusGained(FocusEvent e) {}
/**
* Inner class to make sure that we can only enter numerical value.
*/
class NumericalPlainDocument
extends PlainDocument
{
/** The minimum value of the text field. */
private double min;
/** The maximum value of the text field. */
private double max;
/** Flag to indicate if out-of-range input is prohibited */
private boolean blockOutOfRangeInput;
/**
* Returns <code>true</code> if the passed string is in the
* [min, max] range if a range is specified, <code>false</code>
* otherwise.
*
* @param str The string to handle.
* @return See above
*/
private boolean isInRange(String str)
{
try {
if (Integer.class.equals(numberType)) {
int val = Integer.parseInt(str);
int mx = (int) max;
int mi = (int) min;
return (val >= mi && val <= mx);
} else if (Double.class.equals(numberType)) {
double val = Double.parseDouble(str);
return (val >= min && val <= max);
} else if (Long.class.equals(numberType)) {
long val = Long.parseLong(str);
long mx = (long) max;
long mi = (long) min;
return (val >= mi && val <= mx);
} else if (Float.class.equals(numberType)) {
float val = Float.parseFloat(str);
return (val >= min && val <= max);
}
} catch(NumberFormatException nfe) {}
return false;
}
/**
* Creates a new instance.
*
* @param min
* The minimum value.
* @param max
* The maximum value.
* @param blockOutOfRangeInput
* If set the user won't be able to type out-of-range values
*/
NumericalPlainDocument(double min, double max, boolean blockOutOfRangeInput)
{
this.min = min;
this.max = max;
this.blockOutOfRangeInput = blockOutOfRangeInput;
}
/**
* Sets the minimum value.
*
* @param min The value to set.
*/
void setMinimum(double min) { this.min = min; }
/**
* Sets the maximum value.
*
* @param max The value to set.
*/
void setMaximum(double max) { this.max = max; }
/**
* Returns the minimum value.
*
* @return See above.
*/
double getMinimum() { return min; }
/**
* Returns the maximum value.
*
* @return See above.
*/
double getMaximum() { return max; }
/**
* Overridden to make sure that the value inserted is a numerical
* value in the defined range.
* @see PlainDocument#insertString(int, String, AttributeSet)
*/
public void insertString(int offset, String str, AttributeSet a)
{
try {
if (str == null) return;
for (int i = 0; i < str.length(); i++) {
if (accepted.indexOf(String.valueOf(str.charAt(i))) == -1)
return;
}
if (accepted.equals(FLOAT) ||
(accepted.equals(FLOAT+"-") && negativeAccepted)) {
if (str.indexOf(".") != -1) {
if (getText(0, getLength()).indexOf(".") != -1)
return;
}
}
if (negativeAccepted && str.indexOf("-") != -1) {
if (str.indexOf("-") != 0 || offset != 0)
return;
}
if (str.equals(".") && accepted.equals(FLOAT)) {
super.insertString(offset, str, a);
} else if (str.equals("-") && negativeAccepted) {
super.insertString(offset, str, a);
} else {
String s = this.getText(0, this.getLength());
s += str;
if (!blockOutOfRangeInput || isInRange(s))
super.insertString(offset, str, a);
}
} catch (Exception e) {
Toolkit.getDefaultToolkit().beep();
}
}
}
}