/*
This file is part of the software framework JStatCom,
a library for building applications for data based analysis.
Copyright (C) 2000-2005 Markus Kraetzig
This library is free software; you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as
published by the Free Software Foundation; either version 2.1 of the
License, or (at your option) any later version.
This library 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
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
USA
*/
package com.jstatcom.component;
import java.text.NumberFormat;
import com.jstatcom.model.JSCConstants;
import com.jstatcom.model.JSCTypeDef;
import com.jstatcom.model.JSCTypes;
import com.jstatcom.model.NumberRange;
import com.jstatcom.model.NumberRangeTypes;
import com.jstatcom.model.Scope;
import com.jstatcom.model.Symbol;
import com.jstatcom.model.SymbolEvent;
import com.jstatcom.model.SymbolListener;
import com.jstatcom.model.SymbolTable;
import com.jstatcom.parser.NumberRangeParser;
import com.jstatcom.util.UMath;
import com.jstatcom.util.UString;
/**
* This component allows for number input that is automatically checked
* according to a NumberRange object. The range can be specified
* by constants or via symbol names for the upper and lower bounds. By default
* the number is stored in a symbol of type NUMBER, but this can
* be changed to INT. If the underlying symbol is of the wrong
* type, then an exception is thrown. The type of the bounds must be the same as
* the type for the number.
*
* Usage Note:
* If the number format type is set to NumberFormatTypes.DEFAULT,
* then the precision set for this selector defines, how displayed numbers are
* rounded. Any additional precision is lost, after a number was selected. If,
* for example, only int numbers should be possible, the
* precision can be set to 0, which is also the default. The resulting number is
* then stored as a double that is equal to an int.
*
* If the number format type is set to NumberFormatTypes.SCIENTIFIC,
* then the precision is used only for the display. No rounding is performed and
* a precision of 0 does not mean that values are equal to an int.
*
* This class fires a "number" property change, if a new number was set.
*
* @author Markus Kraetzig
* @see com.jstatcom.model.NumberRange
*/
public final class NumSelector extends IValTextField implements SymbolListener {
// To check, whether focusLost happened due to error message,
// in that case, do not update symbol during error display.
private double currentNumber = 0;
private int precision = 0;
private NumberFormat numberFormat = NumberFormatTypes.DEFAULT.getInstance();
private NumberFormatTypes numberFormatType = NumberFormatTypes.DEFAULT;
private boolean intType = false;
private String symbolName = null;
private String symbolNameLBound = null;
private String symbolNameUBound = null;
private NumberRange numberRange = null;
private final NumberRange defaultRange = new NumberRange(
Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY,
NumberRangeTypes.OPEN);
private NumberRangeTypes pendingRangeType = NumberRangeTypes.OPEN;
private double pendingLowerBound = Double.NEGATIVE_INFINITY;
private double pendingUpperBound = Double.POSITIVE_INFINITY;
private Scope symbolScope = Scope.GLOBAL;
private Symbol symbol = null;
/**
* Default constructor initializes range to (-Infinity,+Infinity) and sets
* the displayed value to 0.
*/
public NumSelector() {
super();
setNumberRange(defaultRange);
setSize(50, 20);
// setHorizontalAlignment(JLabel.RIGHT);
}
/**
* Updates display when symbol changed. This method does nothing if the
* symbol is cleared.
*
* @param evt
* the symbol event
*/
public void valueChanged(SymbolEvent evt) {
symbol = evt.getSource();
if (symbol.isEmpty())
return;
if (intType)
setNumber(symbol.getJSCInt().doubleVal());
else
setNumber(symbol.getJSCNumber().doubleVal());
}
/**
* Overrides addNotify to retrieve data objects as soon as
* the component hierarchy is established.
*
*/
public void addNotify() {
super.addNotify();
setDataForSymbolName();
}
/**
* Calls super method and sets range, if at least one range bound is
* specified by a symbol name. This ensures that the correct data for the
* upper and lower bounds are retrieved, when this component is edited.
*
* @throws IllegalArgumentException
* if overall range is not in a legal state according to the
* symbols for the bounds
*/
protected void focusGained() {
super.focusGained();
if (symbol != null)
symbol.removeSymbolListener(this);
double lowerBound = pendingLowerBound;
if (symbolNameLBound != null)
lowerBound = getDataForSymbol(symbolNameLBound, false);
double upperBound = pendingUpperBound;
if (symbolNameUBound != null)
upperBound = getDataForSymbol(symbolNameUBound, true);
if (symbolNameLBound != null || symbolNameUBound != null)
setNumberRange(new NumberRange(lowerBound, upperBound,
pendingRangeType));
}
/**
* Calls super method and sets range to default, if at least one range bound
* is specified by a symbol name. Otherwise there could be update conflicts
* while the underlying data changes, for example if the data changes before
* the range could be adjusted. If an absolute range is specified, this is
* not touched.
*
* This method also stores the data if it was specified by a symbol name.
*
*/
protected void focusLost() {
super.focusLost();
if (symbol != null)
symbol.addSymbolListener(this);
// To prevent input checking while not edited.
if (symbolNameLBound != null || symbolNameUBound != null)
numberRange = defaultRange;
}
/**
* Gets the number stored in the respective symboltable with the symbol
* identifier symbol. A default is returned if no symbol can
* be retrieved.
*
* @param symbol
* the symbol identifier for table lookup
* @param isUpper
* true for upper bound, false for
* lower bound
* @return the number for the symbol name
* @throws IllegalArgumentException
* if (sName == null)
*/
private double getDataForSymbol(String sName, boolean isUpper) {
if (sName != null) {
SymbolTable sTable = symbolScope.getSymbolTable(this);
Symbol newSymbol = null;
if (sTable != null && sName != null) {
// Get the symbol or create an empty one of the given type
if (intType) {
newSymbol = sTable.get(new JSCTypeDef(sName, JSCTypes.INT));
if (newSymbol.isEmpty()) {
if (isUpper)
return Integer.MAX_VALUE;
return Integer.MIN_VALUE;
}
return newSymbol.getJSCInt().doubleVal();
}
newSymbol = sTable.get(new JSCTypeDef(sName, JSCTypes.NUMBER));
if (newSymbol.isEmpty()) {
if (isUpper)
return Double.MAX_VALUE;
return -Double.MAX_VALUE;
}
return newSymbol.getJSCNumber().doubleVal();
}
}
if (isUpper) {
if (intType)
return Integer.MAX_VALUE;
return Double.MAX_VALUE;
}
if (intType)
return Integer.MIN_VALUE;
return -Double.MAX_VALUE;
}
/**
* Gets the number stored in the respective symboltable for the symbol
* identifier set in symbolName.
*
* @return the number for the symbol name
* @throws IllegalArgumentException
* if (symbolName == null)
*/
private void setDataForSymbolName() {
SymbolTable sTable = null;
if (symbolName != null)
sTable = symbolScope.getSymbolTable(this);
Symbol newSymbol = null;
if (sTable != null && symbolName != null) {
// Get the symbol or create an empty one of the given type
if (intType)
newSymbol = sTable
.get(new JSCTypeDef(symbolName, JSCTypes.INT));
else
newSymbol = sTable.get(new JSCTypeDef(symbolName,
JSCTypes.NUMBER));
}
// special cases
if (newSymbol == symbol)
return;
if (symbol != null)
symbol.removeSymbolListener(this);
symbol = newSymbol;
// overwrite symbol if empty or just set text of null
if (symbol == null || symbol.isEmpty()) {
setNumber(currentNumber);
} else { // just set the number in this text field
if (intType)
setNumber(symbol.getJSCInt().doubleVal());
else
setNumber(symbol.getJSCNumber().doubleVal());
}
// symbolListener was added in setNumber for clarity
}
/**
* Gets the currently displayed number as an int.
*
* @return current number
*/
public int getIntNumber() {
return new Double(currentNumber).intValue();
}
/**
* Gets the currently displayed number as double.
*
* @return the selected number as double
*/
public double getNumber() {
return currentNumber;
}
/**
* Gets the current number format type.
*
* @return the format type in use
*/
public com.jstatcom.component.NumberFormatTypes getNumberFormatType() {
return numberFormatType;
}
/**
* Gets the range that the input is checked against.
*
* @return current number range
*/
public NumberRange getNumberRange() {
return numberRange;
}
/**
* Gets the precision for the displayed numbers.
*
* @return number count right to the decimal point
*/
public int getPrecision() {
return precision;
}
/**
* Gets the current number range displayed as a string, for example
* [1,100] or ["lowerBound_p", 20].
*
* @return string representation of number range
*/
public String getRangeExpr() {
StringBuffer ret = new StringBuffer();
ret.append(numberRange.rangeType.leftBracket);
if (symbolNameLBound != null && symbolNameLBound.length() > 0)
ret.append("\"" + symbolNameLBound + "\"");
else
ret.append(numberRange.lowerBound);
ret.append(", ");
if (symbolNameUBound != null && symbolNameUBound.length() > 0)
ret.append("\"" + symbolNameUBound + "\"");
else
ret.append(numberRange.upperBound);
ret.append(numberRange.rangeType.rightBracket);
return ret.toString();
}
/**
* Gets the symbol identifier for the data object of the selected number.
*
* @return the symbol name
*/
public String getSymbolName() {
return symbolName;
}
/**
* Error handling routine if validation failed. Displays error message and
* sets back number value to the last valid one..
*
* @param msg
* an error message
*/
public void handleError(String msg) {
super.handleError(msg + "\nThe previous value {" + currentNumber
+ "} is restored.");
setNumber(currentNumber);
}
/**
* Sets the currently displayed number with checking whether it conforms to
* the specified range. This method fires a "number" property change with
* the old value and the changed value, if a new number was set.
*
* @param val
* the number to be displayed
* @throws IllegalArgumentException
* if val is not contained in range
*/
public void setNumber(double val) {
String msg;
if ((msg = numberRange.encloses(val)) != null)
throw new IllegalArgumentException(msg);
double oldValue = currentNumber;
currentNumber = val;
setTextCarefully(UString.formatNumber(val, numberFormat, precision));
// Store data for symbol.
if (symbol != null) {
symbol.removeSymbolListener(this);
if (intType)
symbol.getJSCInt().setVal((int) currentNumber);
else
symbol.getJSCNumber().setVal(currentNumber);
symbol.addSymbolListener(this);
}
// fire property change
if (oldValue != currentNumber)
firePropertyChange("number", new Double(oldValue), new Double(
currentNumber));
}
/**
* Sets the current number format type.
*
* @param newNumberFormatType
* format type to use
*/
public void setNumberFormatType(
com.jstatcom.component.NumberFormatTypes newNumberFormatType) {
numberFormatType = newNumberFormatType;
numberFormat = newNumberFormatType.getInstance();
}
/**
* Sets the range that the number input is checked against. The currently
* displayed number is automatically validated against the new range. If the
* current number is not valid, it automatically set to a legal value within
* newNumberRange. This also updates the symbol.
*
* @param newNumberRange
* the number range for validation, if null then a
* range of (-Infinity,+Infinity) is set.
*/
public void setNumberRange(NumberRange newNumberRange) {
if (newNumberRange == null)
newNumberRange = new NumberRange();
// default constructor creates(-inf,inf)
numberRange = newNumberRange;
if (numberRange.encloses(currentNumber) == null) {
setNumber(currentNumber); // number is valid
return;
}
setNumber(numberRange.defaultNumber());
}
/**
* Sets number of digits right to decimal point.
*
* @param newPrecision
* new number precision
* @throws IllegalArgumentException
* if newPrecision < 0
*/
public void setPrecision(int newPrecision) {
if (newPrecision < 0)
throw new IllegalArgumentException("Negative precision argument.");
this.precision = newPrecision;
}
/**
* Sets the range to be checked against by specifying a string of the form
* ["(", "["] lowerBound "," upperBound + ["]", ")"], where
* lowerbound/upperBound are either a number or a symbolname,
* for example
* [lowerBound_p, upperBound_p] or (0, upperBound_x] or [1,10].
*
* If the range is specified without symbol names, it is set immediately. If
* the range depends on symbols to be loaded from a symbol table, this is
* postponed to the next FOCUS_GAINED event. In the meantime,
* the default non-validating (-Infinity,Infinity) range is
* used.
*
* @param expr
* the expression to be parsed
* @throws IllegalArgumentException
* if expr == null or if it cannot be parsed to a
* valid range
*/
public void setRangeExpr(String expr) {
if (expr == null || expr.length() == 0)
throw new IllegalArgumentException("Range expression is empty.");
Object[] rangeComponents;
try {
rangeComponents = new NumberRangeParser().parseString(expr);
} catch (Throwable ex) {
throw new IllegalArgumentException(
"NumberRange construction from \"" + expr + "\" failed.\n"
+ ex);
}
double lowerBound = 0, upperBound = 0;
String lowerName = null, upperName = null;
// Reads the range components from the parser run that may contain
// symbol names.
if (rangeComponents[0] instanceof Double) {
lowerBound = ((Double) rangeComponents[0]).doubleValue();
symbolNameLBound = null;
} else
lowerName = rangeComponents[0].toString();
if (rangeComponents[1] instanceof Double) {
upperBound = ((Double) rangeComponents[1]).doubleValue();
symbolNameUBound = null;
} else
upperName = rangeComponents[1].toString();
NumberRangeTypes rangeType = (NumberRangeTypes) rangeComponents[2];
if (lowerName == null && upperName == null) { // no symbol names, set
// range and return
setNumberRange(new NumberRange(lowerBound, upperBound, rangeType));
return;
}
// If symbol names are set, the changes take only effect,
// when editing is performed, therefore pending variables.
setNumberRange(defaultRange);
pendingRangeType = rangeType;
symbolNameLBound = lowerName;
symbolNameUBound = upperName;
if (symbolNameLBound == null)
pendingLowerBound = lowerBound;
if (symbolNameUBound == null)
pendingUpperBound = upperBound;
}
/**
* Sets the symbol name to retrieve and store data from.
*
* @param newSymbolName
* the symbol name
* @throws IllegalArgumentException
* if (JSCConstants.isValidName(newSymbolName) != null)
*/
public void setSymbolName(String newSymbolName) {
JSCConstants.checkNameThrowEx(newSymbolName);
symbolName = newSymbolName;
setDataForSymbolName();
}
/**
* Validates input against the specified range whenever necessary. This
* method is invoked by the superclass and does not need to be called
* manually.
*
* @return null if ok, an error message otherwise
*/
public String validateInput() {
String inputString = getText().trim();
Double number = UString.parseToNumber(inputString);
if (number == null)
return "Input \"" + inputString + "\" is not a valid number.";
double doubleVal = number.doubleValue();
String error = null;
if (numberFormatType == NumberFormatTypes.DEFAULT)
error = numberRange.encloses(doubleVal, precision, numberFormat);
else
error = numberRange.encloses(doubleVal);
if (error != null)
return error;
if (numberFormatType == NumberFormatTypes.DEFAULT)
doubleVal = UMath.round(doubleVal, precision);
setNumber(doubleVal);
return null;
}
/**
* Gets the symbol scope.
*
* @return scope
*/
public Scope getSymbolScope() {
return symbolScope;
}
/**
* Sets the symbol scope.
*
* @param scope
* @throws IllegalArgumentException
* if (scope == null)
*/
public void setSymbolScope(Scope scope) {
if (scope == null)
throw new IllegalArgumentException("Argument was null.");
symbolScope = scope;
setDataForSymbolName();
}
/**
* Gets whether the underlying symbol is stored as JSCTypes.INT
* or as JSCTypes.NUMBER.
*
* @return true if JSCTypes.INT
*/
public boolean isIntType() {
return intType;
}
/**
* Sets whether the underlying symbol is stored as JSCTypes.INT
* or as JSCTypes.NUMBER. The default is
* JSCTypes.NUMBER. It will only take effect after a new
* symbol name or scope has been set.
*
* @param b
* if true then JSCTypes.INT if
* false then JSCTypes.NUMBER
*
*/
public void setIntType(boolean b) {
intType = b;
}
}
Ads help maintain this website.