/*
* Copyright 2011 Uwe Krueger.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* A text field representing a number.
*/
package com.mandelsoft.swing;
import com.mandelsoft.util.BigDecimalFormat;
import java.awt.GridBagLayout;
import java.math.BigDecimal;
import java.text.DateFormat;
import java.text.FieldPosition;
import java.text.Format;
import java.text.NumberFormat;
import java.text.ParseException;
import java.text.ParsePosition;
import java.util.Date;
import java.util.Locale;
import javax.swing.InputVerifier;
import javax.swing.JComponent;
import javax.swing.JFormattedTextField;
import javax.swing.JFormattedTextField.AbstractFormatterFactory;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JTextField;
import javax.swing.SwingUtilities;
import javax.swing.text.DateFormatter;
import javax.swing.text.DefaultFormatter;
import javax.swing.text.DefaultFormatterFactory;
import javax.swing.text.InternationalFormatter;
import javax.swing.text.NumberFormatter;
/**
* FormatterFactoryDemo.java requires no other files.
*/
public class FormattedField extends JFormattedTextField {
static boolean debug=false;
///////////////////////////////////////////////////////////////////////
// own formal objects to control input
///////////////////////////////////////////////////////////////////////
/*
* unfortunately there is no general way to adjust the format behaviour
* because the JFormattedTextField class uses dedicated formmatter
* wrapperes for the different formats accepting ONLY those formats.
* Try to fake this by calling setFormat instead of using constructor
*/
private class FieldFormat extends Format {
private Format fmt;
private boolean trim;
public FieldFormat(Format fmt)
{
this.fmt=fmt;
}
public FieldFormat setTrim(boolean b)
{
this.trim=b;
return this;
}
@Override
public StringBuffer format(Object value, StringBuffer toAppendTo,
FieldPosition pos)
{
if (debug) {
System.out.println("format "+value);
}
return fmt.format(value, toAppendTo, pos);
}
@Override
public Object parseObject(String source, ParsePosition parsePosition)
{
if (debug) {
System.out.println("parse '"+parsePosition+" '"+source+"'");
}
try {
int tmp=parsePosition.getIndex();
if (trim) {
skipBlanks(source, parsePosition);
}
Object n=fmt.parseObject(source, parsePosition);
if (n==null) {
if (debug) {
System.out.println("basic fail");
}
}
else {
if (debug) {
System.out.println(n+" up to "+parsePosition);
}
if (parsePosition.getErrorIndex()<0) {
if (trim) {
skipBlanks(source, parsePosition);
}
if (parsePosition.getIndex()<source.length()) {
parsePosition.setErrorIndex(parsePosition.getIndex());
parsePosition.setIndex(tmp);
if (debug) {
System.out.println("fail length"+parsePosition);
}
return null;
}
}
}
return n;
}
catch (RuntimeException e) {
System.err.println(e);
throw e;
}
}
private void skipBlanks(String source, ParsePosition p)
{
int i=p.getIndex();
int l=source.length();
while (i<l&&Character.isWhitespace(source.charAt(i))) {
i++;
}
p.setIndex(i);
}
}
///////////////////////////////////////////////////////////////////////
// Input verification
///////////////////////////////////////////////////////////////////////
private static class FieldVerifier extends InputVerifier {
@Override
public boolean verify(JComponent input)
{
FormattedField f=(FormattedField)input;
Object fs;
if (!f.isEnforceCorrectValue()) {
return true;
}
try {
fs=f.getFormatter().stringToValue(f.getText());
return fs!=null;
}
catch (ParseException ex) {
return false;
}
}
@Override
public boolean shouldYieldFocus(JComponent input)
{
FormattedField f=(FormattedField)input;
if (verify(input)) {
return true;
}
if (f.isShowValueWarning()) {
String message=f.getValueHint();
if (message!=null) {
message="\n"+message;
}
else {
message="";
}
JOptionPane.showMessageDialog(null, //no owner frame
"Invalid field value, please try again."+
message, //text to display
"Invalid Value", //title
JOptionPane.WARNING_MESSAGE);
}
//Reinstall the input verifier.
//input.setInputVerifier(this);
return false;
}
}
static FieldVerifier verifier=new FieldVerifier();
///////////////////////////////////////////////////////////////////////
// Number Field
///////////////////////////////////////////////////////////////////////
private DefaultFormatter fmt;
private String valueHint;
private boolean enforceCorrectValue;
private boolean showValueWarning;
public FormattedField()
{ super();
setInputVerifier(verifier);
}
public FormattedField(Object v)
{ this();
setValue(v);
}
public FormattedField(Class<?> vclass, Format format)
{ this();
setFormatterFactory(getDefaultFormatterFactory(format,vclass));
}
public FormattedField(Class<? extends Number> vclass)
{
this(vclass, createDefaultNumberFormat());
}
static NumberFormat createDefaultNumberFormat()
{
//NumberFormat fmt=NumberFormat.getNumberInstance(Locale.UK);
NumberFormat fmt=new NumberFormat() {
NumberFormat fmt=NumberFormat.getNumberInstance(Locale.UK);
@Override
public StringBuffer format(double number, StringBuffer toAppendTo,
FieldPosition pos)
{
toAppendTo.append(Double.toString(number));
return toAppendTo;
}
@Override
public StringBuffer format(long number, StringBuffer toAppendTo,
FieldPosition pos)
{
return fmt.format(number, toAppendTo, pos);
}
@Override
public Number parse(String source, ParsePosition parsePosition)
{
return fmt.parse(source, parsePosition);
}
};
fmt.setGroupingUsed(false);
return fmt;
}
private AbstractFormatterFactory getDefaultFormatterFactory(Object type)
{
return getDefaultFormatterFactory(type,null);
}
private AbstractFormatterFactory getDefaultFormatterFactory(
Object type, Class<?> gvalueclass)
{
InternationalFormatter ifmt;
DefaultFormatter orig=null;
Class<?> valueclass=null;
// first check for dedicated formatters
if (type instanceof DateFormat) {
valueclass=Date.class;
fmt=ifmt=new DateFormatter();
ifmt.setFormat(new FieldFormat((Format)type).setTrim(true));
}
else if (type instanceof NumberFormat) {
fmt=ifmt=new NumberFormatter();
ifmt.setFormat(new FieldFormat((Format)type).setTrim(true));
}
else if (type instanceof Format) {
fmt=ifmt=new InternationalFormatter();
ifmt.setFormat(new FieldFormat((Format)type));
}
// then check for dedicated value types
else if (type instanceof Date) {
fmt=ifmt=new DateFormatter();
ifmt.setFormat(new FieldFormat(DateFormat.getDateInstance()).setTrim(true));
valueclass=gvalueclass;
}
else if (type instanceof Number) {
Format f;
if (type instanceof BigDecimal) {
f=new BigDecimalFormat();
}
else {
NumberFormat nf=createDefaultNumberFormat();
if ((type instanceof Double)||(type instanceof Float)) {
// no adjust
}
else {
nf.setParseIntegerOnly(true);
}
f=nf;
}
fmt=ifmt=new NumberFormatter();
ifmt.setFormat(new FieldFormat(f).setTrim(true));
valueclass=type.getClass();
}
// check for given class
else if (isOfType(gvalueclass,Number.class)) {
Format f;
if (gvalueclass==BigDecimal.class) {
f=new BigDecimalFormat();
}
else {
NumberFormat nf=createDefaultNumberFormat();
if (isOfType(gvalueclass, Double.class)||
isOfType(gvalueclass, Float.class)) {
// no adjust
}
else {
nf.setParseIntegerOnly(true);
}
f=nf;
}
fmt=ifmt=new NumberFormatter();
ifmt.setFormat(new FieldFormat(f).setTrim(true));
valueclass=gvalueclass;
}
else if (isOfType(gvalueclass,Date.class)) {
fmt=ifmt=new DateFormatter();
ifmt.setFormat(new FieldFormat((Format)type));
valueclass=gvalueclass;
}
// use default
else {
fmt=new DefaultFormatter();
valueclass=type.getClass();
}
if (valueclass!=null) {
if (gvalueclass!=null) {
if (!valueclass.isAssignableFrom(gvalueclass))
throw new IllegalArgumentException("given "+gvalueclass+
"does not match derived "+valueclass);
}
fmt.setValueClass(valueclass);
if (orig!=null) {
orig.setValueClass(valueclass);
}
}
if (orig==null) {
orig=fmt;
}
return new DefaultFormatterFactory(orig, orig, fmt);
}
private boolean isOfType(Class<?> v, Class<?>m)
{ return v!=null && m.isAssignableFrom(v);
}
private InternationalFormatter getInternationalFormatter()
{
if (fmt instanceof InternationalFormatter) {
return (InternationalFormatter)fmt;
}
throw new UnsupportedOperationException("min/max not supported");
}
public void setMinimum(Comparable min)
{
Comparable old=getMinimum();
if (min!=null) {
getInternationalFormatter().setMinimum(
(Comparable)Utils.convertValueToValueClass(min, getValueClass()));
}
else {
getInternationalFormatter().setMinimum(null);
}
firePropertyChange("minimum", old, min);
}
public void setMaximum(Comparable max)
{
Comparable old=getMaximum();
if (max!=null) {
getInternationalFormatter().setMaximum(
(Comparable)Utils.convertValueToValueClass(max, getValueClass()));
}
else {
getInternationalFormatter().setMaximum(null);
}
firePropertyChange("maximum", old, max);
}
public Comparable getMinimum()
{
return getInternationalFormatter().getMinimum();
}
public Comparable getMaximum()
{
return getInternationalFormatter().getMaximum();
}
@Override
public void setValue(Object v)
{
// copied from base class
if (v!=null&&getFormatterFactory()==null) {
setFormatterFactory(getDefaultFormatterFactory(v));
}
// keep consistent value type
if (v!=null) {
v=Utils.convertValueToValueClass(v, getValueClass());
}
super.setValue(v);
}
public String getValueHint()
{
return valueHint;
}
public void setValueHint(String hint)
{
this.valueHint=hint;
}
public boolean isEnforceCorrectValue()
{
return enforceCorrectValue;
}
public void setEnforceCorrectValue(boolean enforceCorrectValue)
{
this.enforceCorrectValue=enforceCorrectValue;
}
public boolean isShowValueWarning()
{
return showValueWarning;
}
public void setShowValueWarning(boolean showValueWarning)
{
this.showValueWarning=showValueWarning;
}
public Class<?> getValueClass()
{
return fmt.getValueClass();
}
///////////////////////////////////////////////////////////////////////
// test
///////////////////////////////////////////////////////////////////////
public static void main(String[] args)
{
//Schedule a job for the event dispatch thread:
//creating and showing this application's GUI.
SwingUtilities.invokeLater(new Runnable() {
public void run()
{
JFrame frame=new TestFrame();
frame.setDefaultCloseOperation(
JFrame.EXIT_ON_CLOSE);
frame.setVisible(true);
}
});
}
static class TestFrame extends JFrame {
TestFrame()
{
setLayout(new GridBagLayout());
JLabel l0=new JLabel("string");
FormattedField ff0=new FormattedField("test");
System.out.println(ff0.getValueClass());
ff0.setColumns(10);
add(l0,new GBC(0,0));
add(ff0,new GBC(1,0));
JLabel l1=new JLabel("double>=10");
FormattedField ff1=new FormattedField(10.0);
System.out.println(ff1.getValueClass());
ff1.setColumns(10);
ff1.setMinimum(10);
ff1.setValueHint("Value must at leat be 10.");
ff1.setHorizontalAlignment(JTextField.TRAILING);
add(l1,new GBC(0,1));
add(ff1,new GBC(1,1));
JLabel l2=new JLabel("int<=10");
FormattedField ff2=new FormattedField(10);
System.out.println(ff2.getValueClass());
ff2.setColumns(10);
ff2.setMaximum(10);
ff2.setValueHint("Value may not be greater than 10.");
ff2.setHorizontalAlignment(JTextField.LEADING);
ff2.setEnforceCorrectValue(true);
add(l2,new GBC(0,2));
add(ff2,new GBC(1,2));
JLabel l3=new JLabel("bigdec");
FormattedField ff3=new FormattedField(BigDecimal.TEN);
System.out.println(ff3.getValueClass());
ff3.setColumns(10);
ff3.setMaximum(10);
ff3.setValueHint("Value may not be greater than 10.");
ff3.setHorizontalAlignment(JTextField.LEADING);
ff3.setEnforceCorrectValue(true);
add(l3,new GBC(0,3));
add(ff3,new GBC(1,3));
pack();
}
}
}