/* * Copyright (c) 2000, 2017, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License version 2 only, as * published by the Free Software Foundation. Oracle designates this * particular file as subject to the "Classpath" exception as provided * by Oracle in the LICENSE file that accompanied this code. * * This code 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 General Public License * version 2 for more details (a copy is included in the LICENSE file that * accompanied this code). * * You should have received a copy of the GNU General Public License version * 2 along with this work; if not, write to the Free Software Foundation, * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. * * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA * or visit www.oracle.com if you need additional information or have any * questions. */ package javax.swing; import java.awt.*; import java.awt.event.*; import java.awt.im.InputContext; import java.beans.BeanProperty; import java.beans.JavaBean; import java.io.*; import java.text.*; import java.util.*; import javax.swing.event.*; import javax.swing.plaf.UIResource; import javax.swing.text.*; /** * JFormattedTextField extends JTextField adding * support for formatting arbitrary values, as well as retrieving a particular * object once the user has edited the text. The following illustrates * configuring a JFormattedTextField to edit dates: *
 *   JFormattedTextField ftf = new JFormattedTextField();
 *   ftf.setValue(new Date());
 * 
*

* Once a JFormattedTextField has been created, you can * listen for editing changes by way of adding * a PropertyChangeListener and listening for * PropertyChangeEvents with the property name value. *

* JFormattedTextField allows * configuring what action should be taken when focus is lost. The possible * configurations are: * * * * * * * * * * * * *
Possible JFormattedTextField configurations and their descriptions *
Value * Description *
JFormattedTextField.REVERT * Revert the display to match that of {@code getValue}, possibly losing * the current edit. *
JFormattedTextField.COMMIT * Commits the current value. If the value being edited isn't considered * a legal value by the {@code AbstractFormatter} that is, a * {@code ParseException} is thrown, then the value will not change, and * then edited value will persist. *
JFormattedTextField.COMMIT_OR_REVERT * Similar to {@code COMMIT}, but if the value isn't legal, behave like * {@code REVERT}. *
JFormattedTextField.PERSIST * Do nothing, don't obtain a new {@code AbstractFormatter}, and don't * update the value. *
* * The default is JFormattedTextField.COMMIT_OR_REVERT, * refer to {@link #setFocusLostBehavior} for more information on this. *

* JFormattedTextField allows the focus to leave, even if * the currently edited value is invalid. To lock the focus down while the * JFormattedTextField is an invalid edit state * you can attach an InputVerifier. The following code snippet * shows a potential implementation of such an InputVerifier: *

 * public class FormattedTextFieldVerifier extends InputVerifier {
 *     public boolean verify(JComponent input) {
 *         if (input instanceof JFormattedTextField) {
 *             JFormattedTextField ftf = (JFormattedTextField)input;
 *             AbstractFormatter formatter = ftf.getFormatter();
 *             if (formatter != null) {
 *                 String text = ftf.getText();
 *                 try {
 *                      formatter.stringToValue(text);
 *                      return true;
 *                  } catch (ParseException pe) {
 *                      return false;
 *                  }
 *              }
 *          }
 *          return true;
 *      }
 *      public boolean shouldYieldFocus(JComponent input) {
 *          return verify(input);
 *      }
 *  }
 * 
*

* Alternatively, you could invoke commitEdit, which would also * commit the value. *

* JFormattedTextField does not do the formatting it self, * rather formatting is done through an instance of * JFormattedTextField.AbstractFormatter which is obtained from * an instance of JFormattedTextField.AbstractFormatterFactory. * Instances of JFormattedTextField.AbstractFormatter are * notified when they become active by way of the * install method, at which point the * JFormattedTextField.AbstractFormatter can install whatever * it needs to, typically a DocumentFilter. Similarly when * JFormattedTextField no longer * needs the AbstractFormatter, it will invoke * uninstall. *

* JFormattedTextField typically * queries the AbstractFormatterFactory for an * AbstractFormat when it gains or loses focus. Although this * can change based on the focus lost policy. If the focus lost * policy is JFormattedTextField.PERSIST * and the JFormattedTextField has been edited, the * AbstractFormatterFactory will not be queried until the * value has been committed. Similarly if the focus lost policy is * JFormattedTextField.COMMIT and an exception * is thrown from stringToValue, the * AbstractFormatterFactory will not be queried when focus is * lost or gained. *

* JFormattedTextField.AbstractFormatter * is also responsible for determining when values are committed to * the JFormattedTextField. Some * JFormattedTextField.AbstractFormatters will make new values * available on every edit, and others will never commit the value. You can * force the current value to be obtained * from the current JFormattedTextField.AbstractFormatter * by way of invoking commitEdit. commitEdit will * be invoked whenever return is pressed in the * JFormattedTextField. *

* If an AbstractFormatterFactory has not been explicitly * set, one will be set based on the Class of the value type after * setValue has been invoked (assuming value is non-null). * For example, in the following code an appropriate * AbstractFormatterFactory and AbstractFormatter * will be created to handle formatting of numbers: *

 *   JFormattedTextField tf = new JFormattedTextField();
 *   tf.setValue(100);
 * 
*

* Warning: As the AbstractFormatter will * typically install a DocumentFilter on the * Document, and a NavigationFilter on the * JFormattedTextField you should not install your own. If you do, * you are likely to see odd behavior in that the editing policy of the * AbstractFormatter will not be enforced. *

* Warning: Swing is not thread safe. For more * information see Swing's Threading * Policy. *

* Warning: * Serialized objects of this class will not be compatible with * future Swing releases. The current serialization support is * appropriate for short term storage or RMI between applications running * the same version of Swing. As of 1.4, support for long term storage * of all JavaBeans™ * has been added to the java.beans package. * Please see {@link java.beans.XMLEncoder}. * * @since 1.4 */ @JavaBean @SuppressWarnings("serial") // Same-version serialization only public class JFormattedTextField extends JTextField { private static final String uiClassID = "FormattedTextFieldUI"; private static final Action[] defaultActions = { new CommitAction(), new CancelAction() }; /** * Constant identifying that when focus is lost, * commitEdit should be invoked. If in committing the * new value a ParseException is thrown, the invalid * value will remain. * * @see #setFocusLostBehavior */ public static final int COMMIT = 0; /** * Constant identifying that when focus is lost, * commitEdit should be invoked. If in committing the new * value a ParseException is thrown, the value will be * reverted. * * @see #setFocusLostBehavior */ public static final int COMMIT_OR_REVERT = 1; /** * Constant identifying that when focus is lost, editing value should * be reverted to current value set on the * JFormattedTextField. * * @see #setFocusLostBehavior */ public static final int REVERT = 2; /** * Constant identifying that when focus is lost, the edited value * should be left. * * @see #setFocusLostBehavior */ public static final int PERSIST = 3; /** * Factory used to obtain an instance of AbstractFormatter. */ private AbstractFormatterFactory factory; /** * Object responsible for formatting the current value. */ private AbstractFormatter format; /** * Last valid value. */ private Object value; /** * True while the value being edited is valid. */ private boolean editValid; /** * Behavior when focus is lost. */ private int focusLostBehavior; /** * Indicates the current value has been edited. */ private boolean edited; /** * Used to set the dirty state. */ private DocumentListener documentListener; /** * Masked used to set the AbstractFormatterFactory. */ private Object mask; /** * ActionMap that the TextFormatter Actions are added to. */ private ActionMap textFormatterActionMap; /** * Indicates the input method composed text is in the document */ private boolean composedTextExists = false; /** * A handler for FOCUS_LOST event */ private FocusLostHandler focusLostHandler; /** * Creates a JFormattedTextField with no * AbstractFormatterFactory. Use setMask or * setFormatterFactory to configure the * JFormattedTextField to edit a particular type of * value. */ public JFormattedTextField() { super(); enableEvents(AWTEvent.FOCUS_EVENT_MASK); setFocusLostBehavior(COMMIT_OR_REVERT); } /** * Creates a JFormattedTextField with the specified value. This will * create an AbstractFormatterFactory based on the * type of value. * * @param value Initial value for the JFormattedTextField */ public JFormattedTextField(Object value) { this(); setValue(value); } /** * Creates a JFormattedTextField. format is * wrapped in an appropriate AbstractFormatter which is * then wrapped in an AbstractFormatterFactory. * * @param format Format used to look up an AbstractFormatter */ public JFormattedTextField(java.text.Format format) { this(); setFormatterFactory(getDefaultFormatterFactory(format)); } /** * Creates a JFormattedTextField with the specified * AbstractFormatter. The AbstractFormatter * is placed in an AbstractFormatterFactory. * * @param formatter AbstractFormatter to use for formatting. */ public JFormattedTextField(AbstractFormatter formatter) { this(new DefaultFormatterFactory(formatter)); } /** * Creates a JFormattedTextField with the specified * AbstractFormatterFactory. * * @param factory AbstractFormatterFactory used for formatting. */ public JFormattedTextField(AbstractFormatterFactory factory) { this(); setFormatterFactory(factory); } /** * Creates a JFormattedTextField with the specified * AbstractFormatterFactory and initial value. * * @param factory AbstractFormatterFactory used for * formatting. * @param currentValue Initial value to use */ public JFormattedTextField(AbstractFormatterFactory factory, Object currentValue) { this(currentValue); setFormatterFactory(factory); } /** * Sets the behavior when focus is lost. This will be one of * JFormattedTextField.COMMIT_OR_REVERT, * JFormattedTextField.REVERT, * JFormattedTextField.COMMIT or * JFormattedTextField.PERSIST * Note that some AbstractFormatters may push changes as * they occur, so that the value of this will have no effect. *

* This will throw an IllegalArgumentException if the object * passed in is not one of the afore mentioned values. *

* The default value of this property is * JFormattedTextField.COMMIT_OR_REVERT. * * @param behavior Identifies behavior when focus is lost * @throws IllegalArgumentException if behavior is not one of the known * values */ @BeanProperty(bound = false, enumerationValues = { "JFormattedTextField.COMMIT", "JFormattedTextField.COMMIT_OR_REVERT", "JFormattedTextField.REVERT", "JFormattedTextField.PERSIST"}, description = "Behavior when component loses focus") public void setFocusLostBehavior(int behavior) { if (behavior != COMMIT && behavior != COMMIT_OR_REVERT && behavior != PERSIST && behavior != REVERT) { throw new IllegalArgumentException("setFocusLostBehavior must be one of: JFormattedTextField.COMMIT, JFormattedTextField.COMMIT_OR_REVERT, JFormattedTextField.PERSIST or JFormattedTextField.REVERT"); } focusLostBehavior = behavior; } /** * Returns the behavior when focus is lost. This will be one of * COMMIT_OR_REVERT, * COMMIT, * REVERT or * PERSIST * Note that some AbstractFormatters may push changes as * they occur, so that the value of this will have no effect. * * @return returns behavior when focus is lost */ public int getFocusLostBehavior() { return focusLostBehavior; } /** * Sets the AbstractFormatterFactory. * AbstractFormatterFactory is * able to return an instance of AbstractFormatter that is * used to format a value for display, as well an enforcing an editing * policy. *

* If you have not explicitly set an AbstractFormatterFactory * by way of this method (or a constructor) an * AbstractFormatterFactory and consequently an * AbstractFormatter will be used based on the * Class of the value. NumberFormatter will * be used for Numbers, DateFormatter will * be used for Dates, otherwise DefaultFormatter * will be used. *

* This is a JavaBeans bound property. * * @param tf AbstractFormatterFactory used to lookup * instances of AbstractFormatter */ @BeanProperty(visualUpdate = true, description = "AbstractFormatterFactory, responsible for returning an AbstractFormatter that can format the current value.") public void setFormatterFactory(AbstractFormatterFactory tf) { AbstractFormatterFactory oldFactory = factory; factory = tf; firePropertyChange("formatterFactory", oldFactory, tf); setValue(getValue(), true, false); } /** * Returns the current AbstractFormatterFactory. * * @see #setFormatterFactory * @return AbstractFormatterFactory used to determine * AbstractFormatters */ public AbstractFormatterFactory getFormatterFactory() { return factory; } /** * Sets the current AbstractFormatter. *

* You should not normally invoke this, instead set the * AbstractFormatterFactory or set the value. * JFormattedTextField will * invoke this as the state of the JFormattedTextField * changes and requires the value to be reset. * JFormattedTextField passes in the * AbstractFormatter obtained from the * AbstractFormatterFactory. *

* This is a JavaBeans bound property. * * @see #setFormatterFactory * @param format AbstractFormatter to use for formatting */ protected void setFormatter(AbstractFormatter format) { AbstractFormatter oldFormat = this.format; if (oldFormat != null) { oldFormat.uninstall(); } setEditValid(true); this.format = format; if (format != null) { format.install(this); } setEdited(false); firePropertyChange("textFormatter", oldFormat, format); } /** * Returns the AbstractFormatter that is used to format and * parse the current value. * * @return AbstractFormatter used for formatting */ @BeanProperty(visualUpdate = true, description = "TextFormatter, responsible for formatting the current value") public AbstractFormatter getFormatter() { return format; } /** * Sets the value that will be formatted by an * AbstractFormatter obtained from the current * AbstractFormatterFactory. If no * AbstractFormatterFactory has been specified, this will * attempt to create one based on the type of value. *

* The default value of this property is null. *

* This is a JavaBeans bound property. * * @param value Current value to display */ @BeanProperty(visualUpdate = true, description = "The value to be formatted.") public void setValue(Object value) { if (value != null && getFormatterFactory() == null) { setFormatterFactory(getDefaultFormatterFactory(value)); } setValue(value, true, true); } /** * Returns the last valid value. Based on the editing policy of * the AbstractFormatter this may not return the current * value. The currently edited value can be obtained by invoking * commitEdit followed by getValue. * * @return Last valid value */ public Object getValue() { return value; } /** * Forces the current value to be taken from the * AbstractFormatter and set as the current value. * This has no effect if there is no current * AbstractFormatter installed. * * @throws ParseException if the AbstractFormatter is not able * to format the current value */ public void commitEdit() throws ParseException { AbstractFormatter format = getFormatter(); if (format != null) { setValue(format.stringToValue(getText()), false, true); } } /** * Sets the validity of the edit on the receiver. You should not normally * invoke this. This will be invoked by the * AbstractFormatter as the user edits the value. *

* Not all formatters will allow the component to get into an invalid * state, and thus this may never be invoked. *

* Based on the look and feel this may visually change the state of * the receiver. * * @param isValid boolean indicating if the currently edited value is * valid. */ @BeanProperty(visualUpdate = true, description = "True indicates the edited value is valid") private void setEditValid(boolean isValid) { if (isValid != editValid) { editValid = isValid; firePropertyChange("editValid", Boolean.valueOf(!isValid), Boolean.valueOf(isValid)); } } /** * Returns true if the current value being edited is valid. The value of * this is managed by the current AbstractFormatter, as such * there is no public setter for it. * * @return true if the current value being edited is valid. */ @BeanProperty(bound = false) public boolean isEditValid() { return editValid; } /** * Invoked when the user inputs an invalid value. This gives the * component a chance to provide feedback. The default * implementation beeps. */ protected void invalidEdit() { UIManager.getLookAndFeel().provideErrorFeedback(JFormattedTextField.this); } /** * Processes any input method events, such as * InputMethodEvent.INPUT_METHOD_TEXT_CHANGED or * InputMethodEvent.CARET_POSITION_CHANGED. * * @param e the InputMethodEvent * @see InputMethodEvent */ protected void processInputMethodEvent(InputMethodEvent e) { AttributedCharacterIterator text = e.getText(); int commitCount = e.getCommittedCharacterCount(); // Keep track of the composed text if (text != null) { int begin = text.getBeginIndex(); int end = text.getEndIndex(); composedTextExists = ((end - begin) > commitCount); } else { composedTextExists = false; } super.processInputMethodEvent(e); } /** * Processes any focus events, such as * FocusEvent.FOCUS_GAINED or * FocusEvent.FOCUS_LOST. * * @param e the FocusEvent * @see FocusEvent */ protected void processFocusEvent(FocusEvent e) { super.processFocusEvent(e); // ignore temporary focus event if (e.isTemporary()) { return; } if (isEdited() && e.getID() == FocusEvent.FOCUS_LOST) { InputContext ic = getInputContext(); if (focusLostHandler == null) { focusLostHandler = new FocusLostHandler(); } // if there is a composed text, process it first if ((ic != null) && composedTextExists) { ic.endComposition(); EventQueue.invokeLater(focusLostHandler); } else { focusLostHandler.run(); } } else if (!isEdited()) { // reformat setValue(getValue(), true, true); } } /** * FOCUS_LOST behavior implementation */ private class FocusLostHandler implements Runnable, Serializable { public void run() { int fb = JFormattedTextField.this.getFocusLostBehavior(); if (fb == JFormattedTextField.COMMIT || fb == JFormattedTextField.COMMIT_OR_REVERT) { try { JFormattedTextField.this.commitEdit(); // Give it a chance to reformat. JFormattedTextField.this.setValue( JFormattedTextField.this.getValue(), true, true); } catch (ParseException pe) { if (fb == JFormattedTextField.COMMIT_OR_REVERT) { JFormattedTextField.this.setValue( JFormattedTextField.this.getValue(), true, true); } } } else if (fb == JFormattedTextField.REVERT) { JFormattedTextField.this.setValue( JFormattedTextField.this.getValue(), true, true); } } } /** * Fetches the command list for the editor. This is * the list of commands supported by the plugged-in UI * augmented by the collection of commands that the * editor itself supports. These are useful for binding * to events, such as in a keymap. * * @return the command list */ @BeanProperty(bound = false) public Action[] getActions() { return TextAction.augmentList(super.getActions(), defaultActions); } /** * Gets the class ID for a UI. * * @return the string "FormattedTextFieldUI" * @see JComponent#getUIClassID */ @BeanProperty(bound = false) public String getUIClassID() { return uiClassID; } /** * Associates the editor with a text document. * The currently registered factory is used to build a view for * the document, which gets displayed by the editor after revalidation. * A PropertyChange event ("document") is propagated to each listener. * * @param doc the document to display/edit * @see #getDocument */ @BeanProperty(expert = true, description = "the text document model") public void setDocument(Document doc) { if (documentListener != null && getDocument() != null) { getDocument().removeDocumentListener(documentListener); } super.setDocument(doc); if (documentListener == null) { documentListener = new DocumentHandler(); } doc.addDocumentListener(documentListener); } /* * See readObject and writeObject in JComponent for more * information about serialization in Swing. * * @param s Stream to write to */ private void writeObject(ObjectOutputStream s) throws IOException { s.defaultWriteObject(); if (getUIClassID().equals(uiClassID)) { byte count = JComponent.getWriteObjCounter(this); JComponent.setWriteObjCounter(this, --count); if (count == 0 && ui != null) { ui.installUI(this); } } } /** * Resets the Actions that come from the TextFormatter to * actions. */ private void setFormatterActions(Action[] actions) { if (actions == null) { if (textFormatterActionMap != null) { textFormatterActionMap.clear(); } } else { if (textFormatterActionMap == null) { ActionMap map = getActionMap(); textFormatterActionMap = new ActionMap(); while (map != null) { ActionMap parent = map.getParent(); if (parent instanceof UIResource || parent == null) { map.setParent(textFormatterActionMap); textFormatterActionMap.setParent(parent); break; } map = parent; } } for (int counter = actions.length - 1; counter >= 0; counter--) { Object key = actions[counter].getValue(Action.NAME); if (key != null) { textFormatterActionMap.put(key, actions[counter]); } } } } /** * Does the setting of the value. If createFormat is true, * this will also obtain a new AbstractFormatter from the * current factory. The property change event will be fired if * firePC is true. */ private void setValue(Object value, boolean createFormat, boolean firePC) { Object oldValue = this.value; this.value = value; if (createFormat) { AbstractFormatterFactory factory = getFormatterFactory(); AbstractFormatter atf; if (factory != null) { atf = factory.getFormatter(this); } else { atf = null; } setFormatter(atf); } else { // Assumed to be valid setEditValid(true); } setEdited(false); if (firePC) { firePropertyChange("value", oldValue, value); } } /** * Sets the edited state of the receiver. */ private void setEdited(boolean edited) { this.edited = edited; } /** * Returns true if the receiver has been edited. */ private boolean isEdited() { return edited; } /** * Returns an AbstractFormatterFactory suitable for the passed in * Object type. */ private AbstractFormatterFactory getDefaultFormatterFactory(Object type) { if (type instanceof DateFormat) { return new DefaultFormatterFactory(new DateFormatter ((DateFormat)type)); } if (type instanceof NumberFormat) { return new DefaultFormatterFactory(new NumberFormatter( (NumberFormat)type)); } if (type instanceof Format) { return new DefaultFormatterFactory(new InternationalFormatter( (Format)type)); } if (type instanceof Date) { return new DefaultFormatterFactory(new DateFormatter()); } if (type instanceof Number) { AbstractFormatter displayFormatter = new NumberFormatter(); ((NumberFormatter)displayFormatter).setValueClass(type.getClass()); AbstractFormatter editFormatter = new NumberFormatter( new DecimalFormat("#.#")); ((NumberFormatter)editFormatter).setValueClass(type.getClass()); return new DefaultFormatterFactory(displayFormatter, displayFormatter,editFormatter); } return new DefaultFormatterFactory(new DefaultFormatter()); } /** * Instances of AbstractFormatterFactory are used by * JFormattedTextField to obtain instances of * AbstractFormatter which in turn are used to format values. * AbstractFormatterFactory can return different * AbstractFormatters based on the state of the * JFormattedTextField, perhaps returning different * AbstractFormatters when the * JFormattedTextField has focus vs when it * doesn't have focus. * @since 1.4 */ public abstract static class AbstractFormatterFactory { /** * Returns an AbstractFormatter that can handle formatting * of the passed in JFormattedTextField. * * @param tf JFormattedTextField requesting AbstractFormatter * @return AbstractFormatter to handle formatting duties, a null * return value implies the JFormattedTextField should behave * like a normal JTextField */ public abstract AbstractFormatter getFormatter(JFormattedTextField tf); } /** * Instances of AbstractFormatter are used by * JFormattedTextField to handle the conversion both * from an Object to a String, and back from a String to an Object. * AbstractFormatters can also enforce editing policies, * or navigation policies, or manipulate the * JFormattedTextField in any way it sees fit to * enforce the desired policy. *

* An AbstractFormatter can only be active in * one JFormattedTextField at a time. * JFormattedTextField invokes * install when it is ready to use it followed * by uninstall when done. Subclasses * that wish to install additional state should override * install and message super appropriately. *

* Subclasses must override the conversion methods * stringToValue and valueToString. Optionally * they can override getActions, * getNavigationFilter and getDocumentFilter * to restrict the JFormattedTextField in a particular * way. *

* Subclasses that allow the JFormattedTextField to be in * a temporarily invalid state should invoke setEditValid * at the appropriate times. * @since 1.4 */ public abstract static class AbstractFormatter implements Serializable { private JFormattedTextField ftf; /** * Installs the AbstractFormatter onto a particular * JFormattedTextField. * This will invoke valueToString to convert the * current value from the JFormattedTextField to * a String. This will then install the Actions from * getActions, the DocumentFilter * returned from getDocumentFilter and the * NavigationFilter returned from * getNavigationFilter onto the * JFormattedTextField. *

* Subclasses will typically only need to override this if they * wish to install additional listeners on the * JFormattedTextField. *

* If there is a ParseException in converting the * current value to a String, this will set the text to an empty * String, and mark the JFormattedTextField as being * in an invalid state. *

* While this is a public method, this is typically only useful * for subclassers of JFormattedTextField. * JFormattedTextField will invoke this method at * the appropriate times when the value changes, or its internal * state changes. You will only need to invoke this yourself if * you are subclassing JFormattedTextField and * installing/uninstalling AbstractFormatter at a * different time than JFormattedTextField does. * * @param ftf JFormattedTextField to format for, may be null indicating * uninstall from current JFormattedTextField. */ public void install(JFormattedTextField ftf) { if (this.ftf != null) { uninstall(); } this.ftf = ftf; if (ftf != null) { try { ftf.setText(valueToString(ftf.getValue())); } catch (ParseException pe) { ftf.setText(""); setEditValid(false); } installDocumentFilter(getDocumentFilter()); ftf.setNavigationFilter(getNavigationFilter()); ftf.setFormatterActions(getActions()); } } /** * Uninstalls any state the AbstractFormatter may have * installed on the JFormattedTextField. This resets the * DocumentFilter, NavigationFilter * and additional Actions installed on the * JFormattedTextField. */ public void uninstall() { if (this.ftf != null) { installDocumentFilter(null); this.ftf.setNavigationFilter(null); this.ftf.setFormatterActions(null); } } /** * Parses text returning an arbitrary Object. Some * formatters may return null. * * @throws ParseException if there is an error in the conversion * @param text String to convert * @return Object representation of text */ public abstract Object stringToValue(String text) throws ParseException; /** * Returns the string value to display for value. * * @throws ParseException if there is an error in the conversion * @param value Value to convert * @return String representation of value */ public abstract String valueToString(Object value) throws ParseException; /** * Returns the current JFormattedTextField the * AbstractFormatter is installed on. * * @return JFormattedTextField formatting for. */ protected JFormattedTextField getFormattedTextField() { return ftf; } /** * This should be invoked when the user types an invalid character. * This forwards the call to the current JFormattedTextField. */ protected void invalidEdit() { JFormattedTextField ftf = getFormattedTextField(); if (ftf != null) { ftf.invalidEdit(); } } /** * Invoke this to update the editValid property of the * JFormattedTextField. If you an enforce a policy * such that the JFormattedTextField is always in a * valid state, you will never need to invoke this. * * @param valid Valid state of the JFormattedTextField */ protected void setEditValid(boolean valid) { JFormattedTextField ftf = getFormattedTextField(); if (ftf != null) { ftf.setEditValid(valid); } } /** * Subclass and override if you wish to provide a custom set of * Actions. install will install these * on the JFormattedTextField's ActionMap. * * @return Array of Actions to install on JFormattedTextField */ protected Action[] getActions() { return null; } /** * Subclass and override if you wish to provide a * DocumentFilter to restrict what can be input. * install will install the returned value onto * the JFormattedTextField. * * @return DocumentFilter to restrict edits */ protected DocumentFilter getDocumentFilter() { return null; } /** * Subclass and override if you wish to provide a filter to restrict * where the user can navigate to. * install will install the returned value onto * the JFormattedTextField. * * @return NavigationFilter to restrict navigation */ protected NavigationFilter getNavigationFilter() { return null; } /** * Clones the AbstractFormatter. The returned instance * is not associated with a JFormattedTextField. * * @return Copy of the AbstractFormatter */ protected Object clone() throws CloneNotSupportedException { AbstractFormatter formatter = (AbstractFormatter)super.clone(); formatter.ftf = null; return formatter; } /** * Installs the DocumentFilter filter * onto the current JFormattedTextField. * * @param filter DocumentFilter to install on the Document. */ private void installDocumentFilter(DocumentFilter filter) { JFormattedTextField ftf = getFormattedTextField(); if (ftf != null) { Document doc = ftf.getDocument(); if (doc instanceof AbstractDocument) { ((AbstractDocument)doc).setDocumentFilter(filter); } doc.putProperty(DocumentFilter.class, null); } } } /** * Used to commit the edit. This extends JTextField.NotifyAction * so that isEnabled is true while a JFormattedTextField * has focus, and extends actionPerformed to invoke * commitEdit. */ static class CommitAction extends JTextField.NotifyAction { public void actionPerformed(ActionEvent e) { JTextComponent target = getFocusedComponent(); if (target instanceof JFormattedTextField) { // Attempt to commit the value try { ((JFormattedTextField)target).commitEdit(); } catch (ParseException pe) { ((JFormattedTextField)target).invalidEdit(); // value not committed, don't notify ActionListeners return; } } // Super behavior. super.actionPerformed(e); } public boolean isEnabled() { JTextComponent target = getFocusedComponent(); if (target instanceof JFormattedTextField) { JFormattedTextField ftf = (JFormattedTextField)target; if (!ftf.isEdited()) { return false; } return true; } return super.isEnabled(); } } /** * CancelAction will reset the value in the JFormattedTextField when * actionPerformed is invoked. It will only be * enabled if the focused component is an instance of * JFormattedTextField. */ private static class CancelAction extends TextAction { public CancelAction() { super("reset-field-edit"); } public void actionPerformed(ActionEvent e) { JTextComponent target = getFocusedComponent(); if (target instanceof JFormattedTextField) { JFormattedTextField ftf = (JFormattedTextField)target; ftf.setValue(ftf.getValue()); } } public boolean isEnabled() { JTextComponent target = getFocusedComponent(); if (target instanceof JFormattedTextField) { JFormattedTextField ftf = (JFormattedTextField)target; if (!ftf.isEdited()) { return false; } return true; } return super.isEnabled(); } } /** * Sets the dirty state as the document changes. */ private class DocumentHandler implements DocumentListener, Serializable { public void insertUpdate(DocumentEvent e) { setEdited(true); } public void removeUpdate(DocumentEvent e) { setEdited(true); } public void changedUpdate(DocumentEvent e) {} } }