This document outlines a proposal for formatted text fields, that is, a way for developers to specify the legal set of characters that can be input into a text component. This document is arranged in the following sections:
JFormattedTextFieldJFormattedTextField WorksJFormattedTextField.AbstractFormatterJFormattedTextField.AbstractFormatterFactoryDefaultFormatterAbstractFormatter implementationsjava.text.Format changes
JFormattedTextField will allow formatting of dates, numbers,
Strings and abitrary Objects. JFormattedTextField will
rely on the java.text.Format classes to handle formatting of dates
and numbers. To configure a JFormattedTextField to input dates
in the current locale specific format can be done with:
new JFormattedTextField(new Date());
If you need to display the dates in a specific format, you can
use one of the SimpleDateFormat constructors, eg:
new JFormattedTextField(new SimpleDateFormat("MM/dd/yy"));
Numbers will be handled by an instance of java.text.NumberFormat. The
following shows a couple of varitions in creating a JFormattedTextField
to edit numbers. Refer to the javadoc of NumberFormat for
details on what the constructors are doing:
new JFormattedTextField(new Number(1000));
new JFormattedTextField(new DecimalFormat("#,###"));
new JFormattedTextField(new DecimalFormat("0.###E0"));
JFormattedTextField will also support editing of strings given a mask that specifies what the legal characters are at a given character position. To create a JFormattedTextField to edit US phone numbers the following could be used:
new JFormattedTextField(new MaskFormatter("(###) ###-####"));
JFormattedTextField itself will expose a minimal amount of API in addition to that of its super class, JTextField. In its simplest terms, JFormattedTextField can be thought of as a JTextField with an additional value property (of type Object) and an object to handle the formatting (instance of AbstractFormatter).
As the value property is of type Object, it will be necessary for the developer to cast the return type based on how the JFormattedTextField has been configured. The following shows a typical scenario of using a JFormattedTextField with dates:
JFormattedTextField ftf = new JFormattedTextField();
ftf.setValue(new Date());
...
Date date = (Date)ftf.getValue();
And a typical session for editing numbers looks like:
JFormattedTextField ftf = new JFormattedTextField();
ftf.setValue(new Integer(1000));
...
int intValue = ((Number)ftf.getValue()).intValue();
Filtering the text that can be input into a text component currently requries creating a subclass of Document. This is a rather heavy operation for such a simple, and common, usage. To make this task easier, we will create a class, DocumentFilter, that can be plugged into a Document (Document is an interface, which we will not change, instead AbstractDocument will get a setter/getter for a DocumentFilter, and a property will be set for Documents that do not descend from AbstractDocument so that others can support DocumentFilter should they want to). When a Document with a DocumentFilter is messaged to remove or insert content, the Document in turn invokes the corresponding method on the DocumentFilter. It is the DocumentFilters responsibility to callback if the operation should proceed. In this manner the DocumentFilter will have total control over how the Document can be mutated. DocumentFilter is defined by:
public void remove(FilterBypass fb, int offset, int length) throws
BadLocationException;
public void insertString(FilterBypass fb, int offset, String string,
AttributeSet attr) throws BadLocationException;
If the DocumentFilter wants to mutate the Document from inside the remove or insertString methods it should either invoke supers implementation, or invoke the method on the FilterBypass. Invoking the method on super or FilterBypass provides a way to circument the filter so that the caller doesn't get stuck in stack recursion. The DocumentFilter is not limited to only invoking one method back on the FilterBypass. It can invoke any of the methods exposed by FilterBypass, and can invoke them as many times as it wishes (within the scope of one of DocumentFilters methods). FilterBypass is defined by:
public abstract Document getDocument();
public abstract void remove(int offset, int length) throws
BadLocationException;
public abstract void insertString(int offset, String string,
AttributeSet attr) throws
BadLocationException;
The following example illustrates creating a DocumentFilter that maps lower case to upper case letters:
DocumentFilter upperDF = new DocumentFilter() {
public void insertString(FilterBypass fb, int offset, String string,
AttributeSet attr) throws BadLocationException {
super.insertString(fb, offset, string.toUpperCase(), attr);
}
};
Similar to DocumentFilter, a new class, NavigationFilter, will be created that allows for filtering where the selection can be placed. NavigationFilter will also be called by the navigation actions (such as left, right, center) to determine the next position to place the selection from a given position. NavigationFilter will be a property on JTextComponent, and is defined by:
public void setDot(FilterBypass fb, int dot, Position.Bias bias);
public void moveDot(FilterBypass fb, int dot, Position.Bias bias);
public int getNextVisualPositionFrom(JTextComponent text, int pos,
Position.Bias bias, int direction,
Position.Bias[] biasRet)
throws BadLocationException;
Note that getNextVisualPositionFrom is currently
defined in View, for consistancy, the method in NavigationFilter
is named the same.
Similar to DocumentFilter, NavigationFilter is passed a FilterBypass that should be invoked to handle the actual mutation. NavigationFilter.FilterBypass is defined by:
public abstract Caret getCaret();
public abstract void setDot(int dot, Position.Bias bias);
public abstract void moveDot(int dot, Position.Bias bias);
As previously mentioned, an instance of AbstractFormatter is used to format a particular Object value. AbstractFormatter can also impose an editing policy by having a DocumentFilter and impose a navigation policy by having a NavigationFilter. AbstractFormatter is defined by:
public void install(JFormattedTextField ftf);
public void uninstall();
public abstract Object stringToValue(String text) throws ParseException;
public abstract String valueToString(Object value) throws ParseException;
protected JFormattedTextField getFormattedTextField();
protected void setEditValid(boolean valid);
protected void invalidEdit();
protected Action[] getActions();
protected DocumentFilter getDocumentFilter();
protected NavigationFilter getNavigationFilter();
Once JFormattedTextField is ready to use an AbstractFormatter it will invoke install. AbstractFormatter.install will do the following:
Subclasses will typically only override install if they need to install additional Listeners beyond the DocumentFilter and NavigationFilter, or perhaps place the caret at an initial location.
Some AbstractFormatters will allow the JFormattedTextField to contain an invalid value when editing. To allow the JFormattedTextField to provide indication of this, the AbstractFormatter should invoke setEditValid(false) when the user enters a bogus value, and then invoke setEditValid(true) when the value is valid again.
When JFormattedTextField is done with an AbstractFormatter, it will invoke uninstall. uninstall will remove the previously installed Listeners.
JFormattedTextField will delegate the creation of AbstractFormatters to an instance of AbstractFormatterFactory (a public static inner class of JFormattedTextField). This will make it trivial for developers to provide different formatters for different states of the JFormattedTextField. For example, you could provide a different AbstractFormatter if the current value was null, or a different formatter when editing vs displaying. AbstractFormatterFactory is defined by:
public abstract AbstractFormatter getFormatter(JFormattedTextField ftf);
If the developer has not supplied an AbstractFormatterFactory, one will be created based on the Class of the value.
DefaultFormatter extends JFormattedTextField.AbstractFormatter and will be the superclass for all the formatter implementations we provide. DefaultFormatter formats Objects using toString and creates the Object using the constructor that takes a String. DefaultFormatter will allow a number of various configuration options:
| Option | Description |
|---|---|
| CommitsOnValidEdit | Sets when edits are published back to the JFormattedTextField. If true, commitEdit is invoked on the JFormattedTextField after every succesful edit, otherwise commitEdit will only be invoked when return is pressed. |
| OverwriteMode | Configures the behavior when inserting characters. If overwriteMode is true (the default), new characters overwrite existing characters in the model as they are inserted. |
| AllowsInvalid | Sets whether or not the value being edited is allowed to be invalid. It is often convenient to allow the user to enter invalid values until a commit is attempted. |
The following table lists the AbstractFormatter implementations that we will provide, as well as intended use:
| AbstractFormatter | Object Type | Notes |
|---|---|---|
| DefaultFormatter | Object | valueToString uses Object.toString() and stringToValue uses the single argument constructor that takes a String. |
| MaskFormatter | Strings | Behavior is dictated by a per character mask that specifies legal values (e.g. "###-####"). |
| InternationalFormatter | Objects | Uses an instance of java.text.Format to handle valueToString and stringToValue. |
| NumberFormatter | Numbers | Uses an instance of NumberFormat to handle formatting, descends from InternationalFormatter. |
| DateFormatter | Date | Uses an instance of DateFormat to handle formatting, descends from InternationalFormatter. |
The Swing support for formatted dates and numbers makes
extensive use of the Format classes in the
java.text package. The following problems were
encountered in using the existing api:
format() repeatedly.
Format subclasses out there that we
don't know about, it's quite possible that we'll conflict with
them. Note that the constants for DateFormat and
NumberFormat already overlap, so a
NumberFormat will happily interpret
YEAR_FIELD as FRACTION_FIELD. The
problem is exacerbated by the existence of the polymorphic
Format.format(Object, StringBuffer,
FieldPosition) method.
NumberFormat for the MONTH_FIELD
(the actual implementation of this case returns 0 for both
begin index and end index).
Format subclass actually supports unless it knows
the specific subclass. This, along with the fact that not all
characters in a formatted string are part of fields, makes the
implementation of a generic method that returns all fields
impossible.
These issues have largely been addressed by adding the following
method to java.text.Format:
/**
* Formats an Object producing an AttributedCharacterIterator.
* You can use the returned AttributedCharacterIterator
* to build the resulting String, as well as to determine information
* about the resulting String.
*
* Each attribute key of the AttributedCharacterIterator will be of type
* Field. It is up to each Format implementation
* to define what the legal values are for each attribute in the
* AttributedCharacterIterator, but typically the attribute
* key is also used as the attribute value.
*
The default implementation creates an
* AttributedCharacterIterator with no attributes. Subclasses
* that support fields should override this and create an
* AttributedCharacterIterator with meaningful attributes.
*
* @exception NullPointerException if obj is null.
* @exception IllegalArgumentException when the Format cannot format the
* given object.
* @param obj The object to format
* @return AttributedCharacterIterator describing the formatted value.
* @since 1.4
*/
public AttributedCharacterIterator formatToCharacterIterator(Object obj);
And then having each Format class use a type safe
enumeration for the constants it supports.
The following classes are being proposed: