Formatted text field API (RT-14000, RT-30881)

Martin Sladecek martin.sladecek at oracle.com
Wed Jul 2 07:49:40 UTC 2014


There's a new proposal here: https://javafx-jira.kenai.com/browse/RT-14000

I merged FormattedTextField and content filter to a single Formatter 
class, that's now part of the TextInputControl.

-Martin

On 06/11/2014 10:50 AM, Martin Sladecek wrote:
> Hello,
> I would like to start some discussion about formatted text field API. 
> The related JIRA issues are RT-14000 (formatted text field) and 
> RT-30881 (content filter).
>
> The RT-30881 defines a content filter for all text input controls (in 
> TextInputControl class), like TextField and TextArea. This was 
> originally implemented by Richard Bair and briefly discussed here: 
> http://mail.openjdk.java.net/pipermail/openjfx-dev/2012-December/004687.html. 
> I've tried to build formatted text field on top of that (and made some 
> changes to the API) since content filtering is something most 
> formatted text fields will use.
>
> First, the TextInputControl additions:
>
> * contentFilter property of type ObjectProperty<ContentFilter>
>
> So let's look at the content filter and content change (both nested 
> classes of TextInputControl):
>
>   /**
>      * Content Filter specifies the filter to be used with {@link 
> TextInputControl#contentFilterProperty()}.
>      * It allow user to intercept and modify any change done to the 
> text content.
>      * To avoid content that's not valid for the filter, it's possible 
> to assign a default value supplier for the filter.
>      * <p>
>      * The filter itself is an {@code UnaryOperator} that accepts 
> {@link javafx.scene.control.TextInputControl.ContentChange} object.
>      * It should return a {@link 
> javafx.scene.control.TextInputControl.ContentChange} object that 
> contains the actual (filtered)
>      * change. Returning null rejects the change.
>      * <p>
>      * If default value supplier is provided, it is used when the 
> {@code ContentFilter} is assigned to a {@code TextInputControl}
>      * and it's current text is invalid. It is expected that the 
> provided default value is accepted by the filtering operator.
>      */
>     public static class ContentFilter {
>
>         /**
>          * Creates a new filter with the providing filtering operator.
>          * @param filter the filtering operator
>          *
>          * @throws java.lang.NullPointerException if filter is null
>          */
>         public ContentFilter(UnaryOperator<ContentChange> filter) {}
>
>         /**
>          * Creates a new filter with the providing filtering operator 
> and default value supplier.
>          * @param filter the filtering operator
>          * @param defaultValue the default value or null
>          *
>          * @throws java.lang.NullPointerException if filter is null
>          */
>         public ContentFilter(UnaryOperator<ContentChange> filter, 
> Supplier<String> defaultValue) {}
>
>         /**
>          * The filtering operator of this filter.
>          * @return the operator
>          */
>         public UnaryOperator<ContentChange> getFilter() {}
>
>         /**
>          * The default value provider of this filter
>          * @return the default value provider or null
>          */
>         public Supplier<String> getDefaultValue() {}
>
>         /**
>          * Chains this filter with a filtering operator. The other 
> filtering operator is used only if this
>          * filter's operator rejects the operation. The default value 
> of the new {@code ContentFilter} is the same
>          * as of this filter.
>          * @param other the filtering operator to chain
>          * @return a new ContentFilter as described above
>          */
>         public ContentFilter orApply(UnaryOperator<ContentChange> 
> other) {}
>
>         /**
>          * Chains this filter with a filtering operator of another 
> {@code ContentFilter}. The other filtering operator is used only if this
>          * filter's operator rejects the operation. The default value 
> of the new {@code ContentFilter} is the same
>          * as of this filter.
>          * @param other the filter to chain
>          * @return a new ContentFilter as described above
>          */
>         public ContentFilter orApply(ContentFilter other) {}
>
>     }
>
> /**
>      * Contains the state representing a change in the content for a
>      * TextInputControl. This object is passed to any registered
>      * {@code contentFilter} on the TextInputControl whenever the text
>      * for the TextInputControl is modified.
>      * <p>
>      *     This class contains state and convenience methods for 
> determining what
>      *     change occurred on the control. It also has a reference to the
>      *     TextInputControl itself so that the developer may query any 
> other
>      *     state on the control. Note that you should never modify the 
> state
>      *     of the control directly from within the contentFilter handler.
>      * </p>
>      * <p>
>      *     The ContentChange is mutable, but not observable. It should 
> be used
>      *     only for the life of a single change. It is intended that the
>      *     ContentChange will be modified from within the contentFilter.
>      * </p>
>      */
>     public static final class ContentChange implements Cloneable{
>
>         /**
>          * Gets the control associated with this change.
>          * @return The control associated with this change. This will 
> never be null.
>          */
>         public final TextInputControl getControl() {}
>
>         /**
>          * Gets the start index into the {@link 
> javafx.scene.control.TextInputControl#getText()}
>          * for the modification. This will always be a value > 0 and
>          * <= {@link 
> javafx.scene.control.TextInputControl#getLength()}.
>          *
>          * @return The start index
>          */
>         public final int getStart() {}
>
>         /**
>          * Sets the start index for the range to be modified. This 
> value must
>          * always be a value > 0 and <= {@link 
> javafx.scene.control.TextInputControl#getLength()}
>          * <p>
>          * <b>Note</b> that setting start before the current end will 
> also change end to the same value.
>          *
>          * @param value the new start index
>          */
>         public final void setStart(int value) {}
>
>         /**
>          * Gets the end index into the {@link 
> javafx.scene.control.TextInputControl#getText()}
>          * for the modification. This will always be a value > 
> {@link #getStart()} and
>          * <= {@link 
> javafx.scene.control.TextInputControl#getLength()}.
>          *
>          * @return The end index
>          */
>         public final int getEnd() {}
>
>         /**
>          * Sets the end index for the range to be modified. This value 
> must
>          * always be a value > {@link #getStart()} and <= {@link 
> javafx.scene.control.TextInputControl#getLength()}.
>          * Note that there is an order dependency between modifying 
> the start
>          * and end values. They must always be modified in order such 
> that they
>          * do not violate their constraints.
>          *
>          * @param value The new end index
>          */
>         public final void setEnd(int value) {}
>
>         /**
>          * A convenience method returning an IndexRange representing the
>          * start and end values.
>          *
>          * @return a non-null IndexRange representing the range from 
> start to end.
>          */
>         public final IndexRange getRange() {}
>
>         /**
>          * A convenience method assigning both the start and end values
>          * together, in such a way as to ensure they are valid with 
> respect to
>          * each other. One way to use this method is to set the range 
> like this:
>          * {@code
>          *     change.setRange(IndexRange.normalize(newStart, newEnd));
>          * }
>          *
>          * @param value The new range. Cannot be null.
>          */
>         public final void setRange(IndexRange value) {}
>
>         /**
>          * A convenience method assigning both the start and end values
>          * together, in such a way as to ensure they are valid with 
> respect to
>          * each other. The start must be less than or equal to the end.
>          *
>          * @param start The new start value. Must be a valid start value
>          * @param end The new end value. Must be a valid end value
>          */
>         public final void setRange(int start, int end) {}
>
>         /**
>          * Gets the new anchor. This value will always be > 0 and
>          * <= {@link #getProposedControlText()}{@code}.getLength()}
>          *
>          * @return The new anchor position
>          */
>         public final int getNewAnchor() {}
>
>         /**
>          * Sets the new anchor. This value must be > 0 and
>          * <= {@link 
> #getProposedControlText()}{@code}.getLength()}. Note that there
>          * is an order dependence here, in that the anchor should be
>          * specified after the new text has been specified.
>          *
>          * @param value The new anchor position
>          */
>         public final void setNewAnchor(int value) {}
>
>         /**
>          * Gets the new caret position. This value will always be > 
> 0 and
>          * <= {@link #getProposedControlText()}{@code}.getLength()}
>          *
>          * @return The new caret position
>          */
>         public final int getNewCaretPosition() {}
>
>         /**
>          * Sets the new caret position. This value must be > 0 and
>          * <= {@link 
> #getProposedControlText()}{@code}.getLength()}. Note that there
>          * is an order dependence here, in that the caret position 
> should be
>          * specified after the new text has been specified.
>          *
>          * @param value The new caret position
>          */
>         public final void setNewCaretPosition(int value) {}
>
>         /**
>          * Gets the text used in this change. For example, this may be 
> new
>          * text being added, or text which is replacing all the 
> control's text
>          * within the range of start and end. Typically it is an empty 
> string
>          * only for cases where the range is being deleted.
>          *
>          * @return The text involved in this change. This will never 
> be null.
>          */
>         public final String getText() {}
>
>         /**
>          * Sets the text to use in this change. This is used to 
> replace the
>          * range from start to end, if such a range exists, or to 
> insert text
>          * at the position represented by start == end.
>          *
>          * @param value The text. This cannot be null.
>          */
>         public final void setText(String value) {}
>
>         /**
>          * Gets the complete new text which will be used on the 
> control after
>          * this change. Note that some controls (such as TextField) 
> may do further
>          * filtering after the change is made (such as stripping out 
> newlines)
>          * such that you cannot assume that the newText will be 
> exactly the same
>          * as what is finally set as the content on the control, 
> however it is
>          * correct to assume that this is the case for the purpose of 
> computing
>          * the new caret position and new anchor position (as those 
> values supplied
>          * will be modified as necessary after the control has 
> stripped any
>          * additional characters that the control might strip).
>          *
>          * @return The controls proposed new text at the time of this 
> call, according
>          *         to the state set for start, end, and text 
> properties on this ContentChange object.
>          */
>         public final String getProposedControlText() {}
>
>         /**
>          * Gets whether this change was in response to text being 
> added. Note that
>          * after the ContentChange object is modified by the 
> contentFilter (by one
>          * of the setters) the return value of this method is not 
> altered. It answers
>          * as to whether this change was fired as a result of text 
> being added,
>          * not whether text will end up being added in the end.
>          *
>          * <p>
>          * Text may have been added either cause it was {@link 
> #isInserted()},
>          * {@link #isAppended()}, or {@link #isPrepended()}.
>          * </p>
>          *
>          * @return true if text was being added
>          */
>         public final boolean isAdded() {}
>
>         /**
>          * Gets whether this change was in response to text being 
> deleted. Note that
>          * after the ContentChange object is modified by the 
> contentFilter (by one
>          * of the setters) the return value of this method is not 
> altered. It answers
>          * as to whether this change was fired as a result of text 
> being deleted,
>          * not whether text will end up being deleted in the end.
>          *
>          * @return true if text was being deleted
>          */
>         public final boolean isDeleted() {}
>
>         /**
>          * Gets whether this change was in response to text being 
> added, and in
>          * particular, inserted into the midst of the control text. If 
> this is
>          * true, then {@link #isAdded()} will return true.
>          *
>          * @return true if text was being inserted
>          */
>         public final boolean isInserted() {}
>
>         /**
>          * Gets whether this change was in response to text being 
> added, and in
>          * particular, appended to the end of the control text. If 
> this is
>          * true, then {@link #isAdded()} will return true.
>          *
>          * @return true if text was being appended
>          */
>         public final boolean isAppended() {}
>
>         /**
>          * Gets whether this change was in response to text being 
> added, and in
>          * particular, prepended at the start of the control text. If 
> this is
>          * true, then {@link #isAdded()} will return true.
>          *
>          * @return true if text was being prepended
>          */
>         public final boolean isPrepended() {}
>
>         /**
>          * Gets whether this change was in response to text being 
> replaced. Note that
>          * after the ContentChange object is modified by the 
> contentFilter (by one
>          * of the setters) the return value of this method is not 
> altered. It answers
>          * as to whether this change was fired as a result of text 
> being replaced,
>          * not whether text will end up being replaced in the end.
>          *
>          * @return true if text was being replaced
>          */
>         public final boolean isReplaced() {}
>
>         @Override
>         public ContentChange clone() {}
> }
>
>
>
> The new FormattedTextField class relies on the content filtering and 
> adds a concept of values convertible to/from a text:
>
> **
>  * FormattedTextField is a special kind of TextField that handles 
> conversion between a value of type {@code T} and
>  * a text of this TextField.
>  * <p>
>  * There are two types of converters:
>  * <ul>
>  *     <li>{@link #valueConverterProperty()} represents the default 
> converter between the text and values</li>
>  *     <li>{@link #editConverterProperty()} is a special converter 
> that takes precedence when the textfield is being edited.
>  *     This allows to have a edit-friendly format and a nice display 
> format for the value</li>
>  * </ul>
>  *
>  * <p>
>  * When editing the text field, the value is not updated until either 
> {@code ENTER} key is pressed or focus is lost.
>  * If the conversion fail, the last known valid value is used instead.
>  *
>  * @param <T> the value type
>  */
> public final class FormattedTextField<T> extends TextField{
>
>     /**
>      * Creates a formatted text field with the specified converter and 
> a null value.
>      * @param valueConverter the value converter
>      */
>     public FormattedTextField(StringConverter<T> valueConverter) {}
>
>     /**
>      * Creates a formatted text field with the specified converter and 
> a value
>      * @param valueConverter the value converter
>      * @param value the initial value
>      */
>     public FormattedTextField(StringConverter<T> valueConverter, T 
> value) {}
>
>     /**
>      * This represents the current value of the formatted text field. 
> If a {@link #valueConverterProperty()} is provided,
>      * and the text field is not being edited, the value is a 
> representation of the text in the text field.
>      */
>     public final ReadOnlyObjectProperty<T> valueProperty() {}
>     public final void setValue(T value) {}
>     public final T getValue() {}
>
>     /**
>      * The default converter between the values and text.
>      * Note that changing the converter might lead to a change of 
> value, but only if the text can be converted by the new
>      * converter. Otherwise, the current value is converted to a text, 
> which is set to the field.
>      * @see #editConverterProperty()
>      */
>     public final ObjectProperty<StringConverter<T>> 
> valueConverterProperty() {}
>     public final void setValueConverter(StringConverter<T> converter) {}
>     public final StringConverter<T> getValueConverter() {}
>
>     /**
>      * Converter between values and text when the field is being edited.
>      * @see #valueConverterProperty()
>      */
>     public final ObjectProperty<StringConverter<T>> 
> editConverterProperty() {}
>     public final void setEditConverter(StringConverter<T> converter) {}
>     public final StringConverter<T> getEditConverter() {}
>
> }
>
> You can find the whole patch here: 
> https://javafx-jira.kenai.com/secure/attachment/44678/rt-30881_14000_proposal.patch
> There are some examples for content filtering in RT-30881. I'll attach 
> some formatted text field samples soon.
>
> Thanks,
> -Martin
>



More information about the openjfx-dev mailing list