/*
* Ext GWT 2.2.4 - Ext for GWT
* Copyright(c) 2007-2010, Ext JS, LLC.
* licensing@extjs.com
*
* http://extjs.com/license
*/
package com.extjs.gxt.ui.client.widget.form;
import java.util.ArrayList;
import java.util.List;
import com.extjs.gxt.ui.client.GXT;
import com.extjs.gxt.ui.client.event.ClickRepeaterEvent;
import com.extjs.gxt.ui.client.event.ComponentEvent;
import com.extjs.gxt.ui.client.event.Events;
import com.extjs.gxt.ui.client.event.FieldEvent;
import com.extjs.gxt.ui.client.event.Listener;
import com.extjs.gxt.ui.client.util.ClickRepeater;
import com.extjs.gxt.ui.client.util.Format;
import com.extjs.gxt.ui.client.util.KeyNav;
import com.extjs.gxt.ui.client.util.Size;
import com.google.gwt.dom.client.NativeEvent;
import com.google.gwt.i18n.client.LocaleInfo;
import com.google.gwt.i18n.client.NumberFormat;
import com.google.gwt.i18n.client.constants.NumberConstants;
import com.google.gwt.user.client.Element;
/**
* Numeric text field that provides automatic keystroke filtering and numeric
* validation.
*
* <p>
* When the field wraps any thing other than Double, either
* {@link #setPropertyEditorType(Class)} or
* {@link #setPropertyEditor(PropertyEditor)} should be called with the
* appropriate number type.
*
* <code><pre>
* SpinnerField field = new SpinnerField();
* field.setPropertyEditorType(Integer.class);
* </pre></code>
*
* <dl>
* <dt>Inherited Events:</dt>
* <dd>Field Focus</dd>
* <dd>Field Blur</dd>
* <dd>Field Change</dd>
* <dd>Field Invalid</dd>
* <dd>Field Valid</dd>
* <dd>Field KeyPress</dd>
* <dd>Field SpecialKey</dd>
* </dl>
*/
public class SpinnerField extends TwinTriggerField<Number> {
/**
* SpinnerField messages.
*/
public class SpinnerFieldMessages extends TextFieldMessages {
private String maxText;
private String minText;
private String nanText;
private String negativeText = GXT.MESSAGES.numberField_negativeText();
/**
* Returns the max error text.
*
* @return the error text
*/
public String getMaxText() {
return maxText;
}
/**
* Returns the minimum error text.
*
* @return the minimum error text
*/
public String getMinText() {
return minText;
}
/**
* Returns the not a number error text.
*
* @return the not a number error text
*/
public String getNanText() {
return nanText;
}
/**
* Returns the negative error text.
*
* @return the error text
*/
public String getNegativeText() {
return negativeText;
}
/**
* Error text to display if the maximum value validation fails (defaults to
* "The maximum value for this field is {maxValue}").
*
* @param maxText the max error text
*/
public void setMaxText(String maxText) {
this.maxText = maxText;
}
/**
* Sets the Error text to display if the minimum value validation fails
* (defaults to "The minimum value for this field is {minValue}").
*
* @param minText min error text
*/
public void setMinText(String minText) {
this.minText = minText;
}
/**
* Sets the error text to display if the value is not a valid number. For
* example, this can happen if a valid character like '.' or '-' is left in
* the field with no number (defaults to "{value} is not a valid number").
*
* @param nanText the not a number text
*/
public void setNanText(String nanText) {
this.nanText = nanText;
}
/**
* Sets the negative error text (defaults to 'The value must be greater or
* equal to 0').
*
* @param negativeText the error text
*/
public void setNegativeText(String negativeText) {
this.negativeText = negativeText;
}
}
protected List<Character> allowed;
protected NumberConstants constants;
protected String decimalSeparator = ".";
protected KeyNav<ComponentEvent> keyNav;
private boolean allowDecimals = true;
private boolean allowNegative = true;
private String baseChars = "0123456789";
private Number increment = 1d;
private Number maxValue = Double.MAX_VALUE;
private Number minValue = Double.NEGATIVE_INFINITY;
/**
* Creates a new number field.
*/
public SpinnerField() {
messages = new SpinnerFieldMessages();
propertyEditor = new NumberPropertyEditor();
constants = LocaleInfo.getCurrentLocale().getNumberConstants();
decimalSeparator = constants.decimalSeparator();
}
/**
* Returns true of decimal values are allowed.
*
* @return the allow decimal state
*/
public boolean getAllowDecimals() {
return allowDecimals;
}
/**
* Returns true if negative values are allowed.
*
* @return the allow negative value state
*/
public boolean getAllowNegative() {
return allowNegative;
}
/**
* Returns the base characters.
*
* @return the base characters
*/
public String getBaseChars() {
return baseChars;
}
/**
* Returns the field's number format.
*
* @return the number format
*/
public NumberFormat getFormat() {
return getPropertyEditor().getFormat();
}
/**
* Sets the increment value.
*
* @return the increment
*/
public Number getIncrement() {
return increment;
}
/**
* Returns the fields max value.
*
* @return the max value
*/
public Number getMaxValue() {
return maxValue;
}
@Override
public SpinnerFieldMessages getMessages() {
return (SpinnerFieldMessages) messages;
}
/**
* Returns the field's minimum value.
*
* @return the min value
*/
public Number getMinValue() {
return minValue;
}
@Override
public NumberPropertyEditor getPropertyEditor() {
return (NumberPropertyEditor) propertyEditor;
}
/**
* Returns the number property editor number type.
*
* @see NumberPropertyEditor#setType(Class)
* @return the number type
*/
public Class<?> getPropertyEditorType() {
return getPropertyEditor().getType();
}
/**
* Sets whether decimal value are allowed (defaults to true).
*
* @param allowDecimals true to allow negative values
*/
public void setAllowDecimals(boolean allowDecimals) {
this.allowDecimals = allowDecimals;
}
/**
* Sets whether negative value are allowed.
*
* @param allowNegative true to allow negative values
*/
public void setAllowNegative(boolean allowNegative) {
this.allowNegative = allowNegative;
}
/**
* Sets the base set of characters to evaluate as valid numbers (defaults to
* '0123456789').
*
* @param baseChars the base character
*/
public void setBaseChars(String baseChars) {
assertPreRender();
this.baseChars = baseChars;
}
/**
* Sets the cell's number formatter.
*
* @param format the format
*/
public void setFormat(NumberFormat format) {
getPropertyEditor().setFormat(format);
}
/**
* Sets the increment that should be used (defaults to 1d).
*
* @param increment the increment to set.
*/
public void setIncrement(Number increment) {
this.increment = increment;
}
/**
* Sets the field's max allowable value.
*
* @param maxValue the max value
*/
public void setMaxValue(Number maxValue) {
this.maxValue = maxValue.doubleValue();
if (rendered && maxValue.doubleValue() != Double.MAX_VALUE) {
getInputEl().dom.setAttribute("aria-valuemax", "" + maxValue);
}
}
/**
* Sets the field's minimum allowed value.
*
* @param minValue the minimum value
*/
public void setMinValue(Number minValue) {
this.minValue = minValue.doubleValue();
if (rendered && maxValue.doubleValue() != Double.NEGATIVE_INFINITY) {
getInputEl().dom.setAttribute("aria-valuemin", "" + minValue);
}
}
/**
* Specifies the number type used when converting a String to a Number
* instance (defaults to Double).
*
* @param type the number type (Short, Integer, Long, Float, Double).
*/
public void setPropertyEditorType(Class<?> type) {
getPropertyEditor().setType(type);
}
@Override
protected Size adjustInputSize() {
return new Size(isHideTrigger() ? 0 : trigger.getStyleSize().width, 0);
}
protected void afterRender() {
super.afterRender();
addStyleOnOver(trigger.dom, "x-form-spinner-overup");
addStyleOnOver(twinTrigger.dom, "x-form-spinner-overdown");
}
protected void doSpin(boolean up) {
if (!readOnly) {
Number n = getValue();
double d = n == null ? 0d : getValue().doubleValue();
if (up) {
setValue(Math.max(minValue.doubleValue(), Math.min(d + increment.doubleValue(), maxValue.doubleValue())));
} else {
setValue(Math.max(
minValue.doubleValue(),
Math.min(allowNegative ? d - increment.doubleValue() : Math.max(0, d - increment.doubleValue()),
maxValue.doubleValue())));
}
}
}
@Override
protected void onKeyPress(FieldEvent fe) {
super.onKeyPress(fe);
if (fe.isSpecialKey(getKeyCode(fe.getEvent()))) {
return;
}
char key = getChar(fe.getEvent());
if (!allowed.contains(key)) {
fe.stopEvent();
}
}
@Override
protected void onRender(Element target, int index) {
super.onRender(target, index);
allowed = new ArrayList<Character>();
for (int i = 0; i < baseChars.length(); i++) {
allowed.add(baseChars.charAt(i));
}
if (allowNegative) {
allowed.add('-');
}
if (allowDecimals) {
for (int i = 0; i < decimalSeparator.length(); i++) {
allowed.add(decimalSeparator.charAt(i));
}
}
Listener<ClickRepeaterEvent> listener = new Listener<ClickRepeaterEvent>() {
public void handleEvent(ClickRepeaterEvent be) {
if (SpinnerField.this.isEnabled()) {
if (!hasFocus) {
focus();
}
if (be.getType() == Events.OnClick) {
if (be.getEl() == trigger) {
onTriggerClick(null);
} else if (be.getEl() == twinTrigger) {
onTwinTriggerClick(null);
}
} else if (be.getType() == Events.OnMouseDown) {
if (be.getEl() == trigger) {
trigger.addStyleName("x-form-spinner-clickup");
} else if (be.getEl() == twinTrigger) {
twinTrigger.addStyleName("x-form-spinner-clickdown");
}
} else if (be.getType() == Events.OnMouseUp) {
if (be.getEl() == trigger) {
trigger.removeStyleName("x-form-spinner-clickup");
} else if (be.getEl() == twinTrigger) {
twinTrigger.removeStyleName("x-form-spinner-clickdown");
}
}
}
}
};
ClickRepeater cr = new ClickRepeater(trigger);
cr.addListener(Events.OnClick, listener);
cr.addListener(Events.OnMouseDown, listener);
cr.addListener(Events.OnMouseUp, listener);
addAttachable(cr);
cr = new ClickRepeater(twinTrigger);
cr.addListener(Events.OnClick, listener);
cr.addListener(Events.OnMouseDown, listener);
cr.addListener(Events.OnMouseUp, listener);
addAttachable(cr);
addStyleName("x-spinner-field");
trigger.addStyleName("x-form-spinner-up");
twinTrigger.addStyleName("x-form-spinner-down");
setMaxValue(maxValue);
setMinValue(minValue);
getInputEl().dom.setAttribute("role", "spinbutton");
keyNav = new KeyNav<ComponentEvent>(this) {
@Override
public void onDown(ComponentEvent ce) {
doSpin(false);
}
@Override
public void onUp(ComponentEvent ce) {
doSpin(true);
}
};
}
protected void onTriggerClick(ComponentEvent ce) {
super.onTriggerClick(ce);
// only do it from the ClickRepeater, not from onBrowserEvent
if (ce == null) {
doSpin(true);
}
}
protected void onTwinTriggerClick(ComponentEvent ce) {
super.onTwinTriggerClick(ce);
// only do it from the ClickRepeater, not from onBrowserEvent
if (ce == null) {
doSpin(false);
}
}
@Override
protected boolean validateValue(String value) {
// validator should run after super rules
Validator tv = validator;
validator = null;
if (!super.validateValue(value)) {
validator = tv;
return false;
}
validator = tv;
if (value.length() < 1) { // if it's blank and textfield didn't flag it then
// its valid it's valid
return true;
}
String v = value;
Number d = null;
try {
d = getPropertyEditor().convertStringValue(v);
} catch (Exception e) {
String error = "";
if (getMessages().getNanText() == null) {
error = GXT.MESSAGES.numberField_nanText(v);
} else {
error = Format.substitute(getMessages().getNanText(), v);
}
markInvalid(error);
return false;
}
if (d.doubleValue() < minValue.doubleValue()) {
String error = "";
if (getMessages().getMinText() == null) {
error = GXT.MESSAGES.numberField_minText(minValue.doubleValue());
} else {
error = Format.substitute(getMessages().getMinText(), minValue);
}
markInvalid(error);
return false;
}
if (d.doubleValue() > maxValue.doubleValue()) {
String error = "";
if (getMessages().getMaxText() == null) {
error = GXT.MESSAGES.numberField_maxText(maxValue.doubleValue());
} else {
error = Format.substitute(getMessages().getMaxText(), maxValue);
}
markInvalid(error);
return false;
}
if (!allowNegative && d.doubleValue() < 0) {
markInvalid(getMessages().getNegativeText());
return false;
}
if (validator != null) {
String msg = validator.validate(this, value);
if (msg != null) {
markInvalid(msg);
return false;
}
}
if (GXT.isAriaEnabled()) {
getInputEl().dom.setAttribute("aria-valuenow", "" + value);
}
return true;
}
// needed due to GWT 2.1 changes
private native char getChar(NativeEvent e) /*-{
return e.which || e.charCode || e.keyCode || 0;
}-*/;
// needed due to GWT 2.1 changes
private native int getKeyCode(NativeEvent e) /*-{
return e.keyCode || 0;
}-*/;
}