/* * $Id$ * * Copyright 2008 Sun Microsystems, Inc., 4150 Network Circle, * Santa Clara, California 95054, U.S.A. All rights reserved. * * 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., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA */ package org.jdesktop.swingx.table; import java.awt.Color; import java.awt.Component; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.text.NumberFormat; import java.text.ParseException; import javax.swing.DefaultCellEditor; import javax.swing.InputMap; import javax.swing.InputVerifier; import javax.swing.JComponent; import javax.swing.JFormattedTextField; import javax.swing.JTable; import javax.swing.JTextField; import javax.swing.KeyStroke; import javax.swing.border.LineBorder; import javax.swing.text.NumberFormatter; import org.jdesktop.swingx.text.StrictNumberFormatter; import org.jdesktop.swingx.text.NumberFormatExt; /** * * Issue #393-swingx: localized NumberEditor. Added feature to use StrictNumberFormatter. * * @author Noel Grandin * @author Jeanette Winzenburg */ public class NumberEditorExt extends DefaultCellEditor { private static Class<?>[] argTypes = new Class[]{String.class}; java.lang.reflect.Constructor<?> constructor; private boolean useStrictFormatter; /** * Instantiates an editor with default NumberFormat and default NumberFormatter. */ public NumberEditorExt() { this(null); } /** * Instantiates an editor with the given NumberFormat and default NumberFormatter. * * @param format the NumberFormat to use for conversion, may be null to indicate * usage of default NumberFormat. */ public NumberEditorExt(NumberFormat format) { this(format, false); } /** * Instantiates an editor with default NumberFormat and NumberFormatter depending * on useStrictFormatter. * * @param useStrictFormatter if true, uses a StrictNumberFormatter, else uses * default NumberFormatter */ public NumberEditorExt(boolean useStrictFormatter) { this(null, useStrictFormatter); } /** * Instantiates an editor with the given NumberFormat and NumberFormatter depending on * useStrictFormatter. * * @param format the NumberFormat to use for conversion, may be null to indicate * usage of default NumberFormat * @param useStrictFormatter if true, uses a StrictNumberFormatter, else uses * default NumberFormatter */ public NumberEditorExt(NumberFormat format, boolean useStrictFormatter) { super(useStrictFormatter ? createFormattedTextFieldX(format) : createFormattedTextField(format)); this.useStrictFormatter = useStrictFormatter; final JFormattedTextField textField = getComponent(); textField.setName("Table.editor"); textField.setHorizontalAlignment(JTextField.RIGHT); // remove action listener added in DefaultCellEditor textField.removeActionListener(delegate); // replace the delegate created in DefaultCellEditor delegate = new EditorDelegate() { @Override public void setValue(Object value) { getComponent().setValue(value); } @Override public Object getCellEditorValue() { try { getComponent().commitEdit(); return getComponent().getValue(); } catch (ParseException ex) { return null; } } }; textField.addActionListener(delegate); } @Override public boolean stopCellEditing() { if (!isValid()) return false; return super.stopCellEditing(); } /** * Returns a boolean indicating whether the current text is valid for * instantiating the expected Number type. * * @return true if text is valid, false otherwise. */ protected boolean isValid() { if (!getComponent().isEditValid()) return false; try { if (!hasStrictFormatter()) getNumber(); return true; } catch (Exception ex) { } return false; } /** * Returns the editor value as number. May fail for a variety of reasons, * as it forces parsing of the current text as well as reflective construction * of the target type. * * @return the editor value or null * @throws Exception if creation of the expected type fails in some way. */ protected Number getNumber() throws Exception { Number number = (Number) super.getCellEditorValue(); if (number==null) return null; return hasStrictFormatter() ? number : (Number) constructor.newInstance(new Object[]{number.toString()}); } /** * @return */ protected boolean hasStrictFormatter() { return useStrictFormatter; } /** Override and set the border back to normal in case there was an error previously */ @Override public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column) { ((JComponent)getComponent()).setBorder(new LineBorder(Color.black)); try { final Class<?> type = table.getColumnClass(column); if (hasStrictFormatter()) { // delegate to formatter which decides at parsing time // then either handles or throws ((NumberFormatter) getComponent().getFormatter()).setValueClass(type); } else { // Assume that the Number object we are dealing with has a constructor which takes // a single string parameter. if (!Number.class.isAssignableFrom(type)) { throw new IllegalStateException("NumberEditor can only handle subclasses of java.lang.Number"); } constructor = type.getConstructor(argTypes); } // JW: in strict mode this may fail in setting the value in the formatter return super.getTableCellEditorComponent(table, value, isSelected, row, column); } catch (Exception ex) { // PENDING JW: super generic editor swallows all failures and returns null // should we do so as well? throw new IllegalStateException("value/type not compatible with Number", ex); } } /** * {@inheritDoc} <p> * * Overridden to instantiate a Number of the expected type. Note that this * may throw a IllegalStateException if invoked without querying * for a valid value with stopCellEditing. This should not happen during * normal usage. * * @throws IllegalStateException if current value invalid * */ @Override public Number getCellEditorValue() throws IllegalStateException { try { return getNumber(); } catch (Exception ex) { throw new IllegalStateException("Number conversion not possible from " + "current string " + getComponent().getText()); } } /** * {@inheritDoc} <p> * * Convenience override with type cast. */ @Override public JFormattedTextField getComponent() { return (JFormattedTextField) super.getComponent(); } /** * Creates and returns a JFormattedTextField configured with SwingX extended * NumberFormat and StrictNumberFormatter. This method is called if * the constructor parameter useStrictFormatter is true. * * Use a static method so that we can do some stuff before calling the * superclass. */ private static JFormattedTextField createFormattedTextFieldX( NumberFormat format) { StrictNumberFormatter formatter = new StrictNumberFormatter( new NumberFormatExt(format)); final JFormattedTextField textField = new JFormattedTextField( formatter); /* * FIXME: I am sure there is a better way to do this, but I don't know * what it is. JTable sets up a binding for the ESCAPE key, but * JFormattedTextField overrides that binding with it's own. Remove the * JFormattedTextField binding. */ InputMap map = textField.getInputMap(); map.put(KeyStroke.getKeyStroke("ESCAPE"), "none"); // while (map != null) { // map.remove(KeyStroke.getKeyStroke("pressed ESCAPE")); // map = map.getParent(); // } /* * Set an input verifier to prevent the cell losing focus when the value * is invalid */ textField.setInputVerifier(new InputVerifier() { @Override public boolean verify(JComponent input) { JFormattedTextField ftf = (JFormattedTextField) input; return ftf.isEditValid(); } }); /* * The formatted text field will not call stopCellEditing() until the * value is valid. So do the red border thing here. */ textField.addPropertyChangeListener("editValid", new PropertyChangeListener() { @Override public void propertyChange(PropertyChangeEvent evt) { if (evt.getNewValue() == Boolean.TRUE) { ((JFormattedTextField) evt.getSource()) .setBorder(new LineBorder(Color.black)); } else { ((JFormattedTextField) evt.getSource()) .setBorder(new LineBorder(Color.red)); } } }); return textField; } /** * Creates and returns a JFormattedTextField configured with defaults. This * method is called if the contructor useStrictFormatter is false.<p> * * Use a static method so that we can do some stuff before calling the * superclass. */ private static JFormattedTextField createFormattedTextField( NumberFormat formatter) { final JFormattedTextField textField = new JFormattedTextField( new NumberFormatExt(formatter)); /* * FIXME: I am sure there is a better way to do this, but I don't know * what it is. JTable sets up a binding for the ESCAPE key, but * JFormattedTextField overrides that binding with it's own. Remove the * JFormattedTextField binding. */ InputMap map = textField.getInputMap(); map.put(KeyStroke.getKeyStroke("ESCAPE"), "none"); // while (map != null) { // map.remove(KeyStroke.getKeyStroke("pressed ESCAPE")); // map = map.getParent(); // } /* * Set an input verifier to prevent the cell losing focus when the value * is invalid */ textField.setInputVerifier(new InputVerifier() { @Override public boolean verify(JComponent input) { JFormattedTextField ftf = (JFormattedTextField) input; return ftf.isEditValid(); } }); /* * The formatted text field will not call stopCellEditing() until the * value is valid. So do the red border thing here. */ textField.addPropertyChangeListener("editValid", new PropertyChangeListener() { @Override public void propertyChange(PropertyChangeEvent evt) { if (evt.getNewValue() == Boolean.TRUE) { ((JFormattedTextField) evt.getSource()) .setBorder(new LineBorder(Color.black)); } else { ((JFormattedTextField) evt.getSource()) .setBorder(new LineBorder(Color.red)); } } }); return textField; } }