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}