/*
 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.