001/* 002 * $Id: NumberEditorExt.java 3927 2011-02-22 16:34:11Z kleopatra $ 003 * 004 * Copyright 2008 Sun Microsystems, Inc., 4150 Network Circle, 005 * Santa Clara, California 95054, U.S.A. All rights reserved. 006 * 007 * This library is free software; you can redistribute it and/or 008 * modify it under the terms of the GNU Lesser General Public 009 * License as published by the Free Software Foundation; either 010 * version 2.1 of the License, or (at your option) any later version. 011 * 012 * This library is distributed in the hope that it will be useful, 013 * but WITHOUT ANY WARRANTY; without even the implied warranty of 014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 015 * Lesser General Public License for more details. 016 * 017 * You should have received a copy of the GNU Lesser General Public 018 * License along with this library; if not, write to the Free Software 019 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA 020 */ 021package org.jdesktop.swingx.table; 022 023import java.awt.Color; 024import java.awt.Component; 025import java.beans.PropertyChangeEvent; 026import java.beans.PropertyChangeListener; 027import java.text.NumberFormat; 028import java.text.ParseException; 029 030import javax.swing.DefaultCellEditor; 031import javax.swing.InputMap; 032import javax.swing.InputVerifier; 033import javax.swing.JComponent; 034import javax.swing.JFormattedTextField; 035import javax.swing.JTable; 036import javax.swing.JTextField; 037import javax.swing.KeyStroke; 038import javax.swing.border.LineBorder; 039import javax.swing.text.NumberFormatter; 040 041import org.jdesktop.swingx.text.StrictNumberFormatter; 042import org.jdesktop.swingx.text.NumberFormatExt; 043 044 045/** 046 * 047 * Issue #393-swingx: localized NumberEditor. Added feature to use StrictNumberFormatter. 048 * 049 * @author Noel Grandin 050 * @author Jeanette Winzenburg 051 */ 052public class NumberEditorExt extends DefaultCellEditor { 053 054 private static Class<?>[] argTypes = new Class[]{String.class}; 055 java.lang.reflect.Constructor<?> constructor; 056 private boolean useStrictFormatter; 057 058 /** 059 * Instantiates an editor with default NumberFormat and default NumberFormatter. 060 */ 061 public NumberEditorExt() { 062 this(null); 063 } 064 065 /** 066 * Instantiates an editor with the given NumberFormat and default NumberFormatter. 067 * 068 * @param format the NumberFormat to use for conversion, may be null to indicate 069 * usage of default NumberFormat. 070 */ 071 public NumberEditorExt(NumberFormat format) { 072 this(format, false); 073 } 074 075 /** 076 * Instantiates an editor with default NumberFormat and NumberFormatter depending 077 * on useStrictFormatter. 078 * 079 * @param useStrictFormatter if true, uses a StrictNumberFormatter, else uses 080 * default NumberFormatter 081 */ 082 public NumberEditorExt(boolean useStrictFormatter) { 083 this(null, useStrictFormatter); 084 } 085 086 /** 087 * Instantiates an editor with the given NumberFormat and NumberFormatter depending on 088 * useStrictFormatter. 089 * 090 * @param format the NumberFormat to use for conversion, may be null to indicate 091 * usage of default NumberFormat 092 * @param useStrictFormatter if true, uses a StrictNumberFormatter, else uses 093 * default NumberFormatter 094 */ 095 public NumberEditorExt(NumberFormat format, boolean useStrictFormatter) { 096 097 super(useStrictFormatter ? createFormattedTextFieldX(format) : createFormattedTextField(format)); 098 this.useStrictFormatter = useStrictFormatter; 099 final JFormattedTextField textField = getComponent(); 100 101 textField.setName("Table.editor"); 102 textField.setHorizontalAlignment(JTextField.RIGHT); 103 104 // remove action listener added in DefaultCellEditor 105 textField.removeActionListener(delegate); 106 // replace the delegate created in DefaultCellEditor 107 delegate = new EditorDelegate() { 108 @Override 109 public void setValue(Object value) { 110 getComponent().setValue(value); 111 } 112 113 @Override 114 public Object getCellEditorValue() { 115 try { 116 getComponent().commitEdit(); 117 return getComponent().getValue(); 118 } catch (ParseException ex) { 119 return null; 120 } 121 } 122 }; 123 textField.addActionListener(delegate); 124 } 125 126 @Override 127 public boolean stopCellEditing() { 128 if (!isValid()) return false; 129 return super.stopCellEditing(); 130 } 131 132 /** 133 * Returns a boolean indicating whether the current text is valid for 134 * instantiating the expected Number type. 135 * 136 * @return true if text is valid, false otherwise. 137 */ 138 protected boolean isValid() { 139 if (!getComponent().isEditValid()) return false; 140 try { 141 if (!hasStrictFormatter()) 142 getNumber(); 143 return true; 144 } catch (Exception ex) { 145 146 } 147 return false; 148 } 149 150 /** 151 * Returns the editor value as number. May fail for a variety of reasons, 152 * as it forces parsing of the current text as well as reflective construction 153 * of the target type. 154 * 155 * @return the editor value or null 156 * @throws Exception if creation of the expected type fails in some way. 157 */ 158 protected Number getNumber() throws Exception { 159 Number number = (Number) super.getCellEditorValue(); 160 if (number==null) return null; 161 return hasStrictFormatter() ? number : 162 (Number) constructor.newInstance(new Object[]{number.toString()}); 163 } 164 165 /** 166 * @return 167 */ 168 protected boolean hasStrictFormatter() { 169 return useStrictFormatter; 170 } 171 172 /** Override and set the border back to normal in case there was an error previously */ 173 @Override 174 public Component getTableCellEditorComponent(JTable table, Object value, 175 boolean isSelected, 176 int row, int column) { 177 ((JComponent)getComponent()).setBorder(new LineBorder(Color.black)); 178 try { 179 final Class<?> type = table.getColumnClass(column); 180 if (hasStrictFormatter()) { 181 // delegate to formatter which decides at parsing time 182 // then either handles or throws 183 ((NumberFormatter) getComponent().getFormatter()).setValueClass(type); 184 } else { 185 // Assume that the Number object we are dealing with has a constructor which takes 186 // a single string parameter. 187 if (!Number.class.isAssignableFrom(type)) { 188 throw new IllegalStateException("NumberEditor can only handle subclasses of java.lang.Number"); 189 } 190 constructor = type.getConstructor(argTypes); 191 } 192 // JW: in strict mode this may fail in setting the value in the formatter 193 return super.getTableCellEditorComponent(table, value, isSelected, row, column); 194 } catch (Exception ex) { 195 // PENDING JW: super generic editor swallows all failures and returns null 196 // should we do so as well? 197 throw new IllegalStateException("value/type not compatible with Number", ex); 198 } 199 } 200 201 /** 202 * {@inheritDoc} <p> 203 * 204 * Overridden to instantiate a Number of the expected type. Note that this 205 * may throw a IllegalStateException if invoked without querying 206 * for a valid value with stopCellEditing. This should not happen during 207 * normal usage. 208 * 209 * @throws IllegalStateException if current value invalid 210 * 211 */ 212 @Override 213 public Number getCellEditorValue() throws IllegalStateException { 214 try { 215 return getNumber(); 216 } catch (Exception ex) { 217 throw new IllegalStateException("Number conversion not possible from " + 218 "current string " + getComponent().getText()); 219 } 220 } 221 222 223 /** 224 * {@inheritDoc} <p> 225 * 226 * Convenience override with type cast. 227 */ 228 @Override 229 public JFormattedTextField getComponent() { 230 return (JFormattedTextField) super.getComponent(); 231 } 232 233 /** 234 * Creates and returns a JFormattedTextField configured with SwingX extended 235 * NumberFormat and StrictNumberFormatter. This method is called if 236 * the constructor parameter useStrictFormatter is true. 237 * 238 * Use a static method so that we can do some stuff before calling the 239 * superclass. 240 */ 241 private static JFormattedTextField createFormattedTextFieldX( 242 NumberFormat format) { 243 StrictNumberFormatter formatter = new StrictNumberFormatter( 244 new NumberFormatExt(format)); 245 final JFormattedTextField textField = new JFormattedTextField( 246 formatter); 247 /* 248 * FIXME: I am sure there is a better way to do this, but I don't know 249 * what it is. JTable sets up a binding for the ESCAPE key, but 250 * JFormattedTextField overrides that binding with it's own. Remove the 251 * JFormattedTextField binding. 252 */ 253 InputMap map = textField.getInputMap(); 254 map.put(KeyStroke.getKeyStroke("ESCAPE"), "none"); 255// while (map != null) { 256// map.remove(KeyStroke.getKeyStroke("pressed ESCAPE")); 257// map = map.getParent(); 258// } 259 /* 260 * Set an input verifier to prevent the cell losing focus when the value 261 * is invalid 262 */ 263 textField.setInputVerifier(new InputVerifier() { 264 @Override 265 public boolean verify(JComponent input) { 266 JFormattedTextField ftf = (JFormattedTextField) input; 267 return ftf.isEditValid(); 268 } 269 }); 270 /* 271 * The formatted text field will not call stopCellEditing() until the 272 * value is valid. So do the red border thing here. 273 */ 274 textField.addPropertyChangeListener("editValid", 275 new PropertyChangeListener() { 276 @Override 277 public void propertyChange(PropertyChangeEvent evt) { 278 if (evt.getNewValue() == Boolean.TRUE) { 279 ((JFormattedTextField) evt.getSource()) 280 .setBorder(new LineBorder(Color.black)); 281 } else { 282 ((JFormattedTextField) evt.getSource()) 283 .setBorder(new LineBorder(Color.red)); 284 } 285 } 286 }); 287 return textField; 288 } 289 290 291 /** 292 * Creates and returns a JFormattedTextField configured with defaults. This 293 * method is called if the contructor useStrictFormatter is false.<p> 294 * 295 * Use a static method so that we can do some stuff before calling the 296 * superclass. 297 */ 298 private static JFormattedTextField createFormattedTextField( 299 NumberFormat formatter) { 300 final JFormattedTextField textField = new JFormattedTextField( 301 new NumberFormatExt(formatter)); 302 /* 303 * FIXME: I am sure there is a better way to do this, but I don't know 304 * what it is. JTable sets up a binding for the ESCAPE key, but 305 * JFormattedTextField overrides that binding with it's own. Remove the 306 * JFormattedTextField binding. 307 */ 308 InputMap map = textField.getInputMap(); 309 map.put(KeyStroke.getKeyStroke("ESCAPE"), "none"); 310// while (map != null) { 311// map.remove(KeyStroke.getKeyStroke("pressed ESCAPE")); 312// map = map.getParent(); 313// } 314 /* 315 * Set an input verifier to prevent the cell losing focus when the value 316 * is invalid 317 */ 318 textField.setInputVerifier(new InputVerifier() { 319 @Override 320 public boolean verify(JComponent input) { 321 JFormattedTextField ftf = (JFormattedTextField) input; 322 return ftf.isEditValid(); 323 } 324 }); 325 /* 326 * The formatted text field will not call stopCellEditing() until the 327 * value is valid. So do the red border thing here. 328 */ 329 textField.addPropertyChangeListener("editValid", 330 new PropertyChangeListener() { 331 @Override 332 public void propertyChange(PropertyChangeEvent evt) { 333 if (evt.getNewValue() == Boolean.TRUE) { 334 ((JFormattedTextField) evt.getSource()) 335 .setBorder(new LineBorder(Color.black)); 336 } else { 337 ((JFormattedTextField) evt.getSource()) 338 .setBorder(new LineBorder(Color.red)); 339 } 340 } 341 }); 342 return textField; 343 } 344}