I already have a use for a bi-directional binding. I have a UI that shows properties of a component. The component's properties can be updated via the UI, and also by other events from background processes that are running, or even just as a reaction to some other property changing. So I need the binding from the model to the control to be bi-directional. The UI view is both read/write. The properties can be numbers, rational numbers expressed as fractions ##/###, times HH:MM:SS, enum values, etc.
(My app has some complex requirements so in actual fact a simple binding probably isn't enough. But in principle it could be.) Or just think of a spreadsheet in Google docs being edited by two people on a network... I want to be able to update a cell, and I want to see when someone else updates it. Two different read/write fields connected to the same model. I just thought of something else. Can this be easily used as the field for a editable ComboBox? Scott On Wed, Jun 11, 2014 at 10:08 AM, Martin Sladecek < [email protected]> wrote: > Although I'm not sure the second one can be considered a use-case, I agree > that somebody might want uneditable field with some format (and it's up to > the developer to make it uneditable), so I'll make the value rw. > > -Martin > > On 11.6.2014 16:03, Scott Palmer wrote: > >> There are two cases for this: >> - a regular binding could be used with a read-only FormattedTextField >> for cases where you want the text representation shown in the UI and >> have it selectable and readable for copy/paste. >> - a bi-directional binding should "just work" >> >> Scott >> >> On Wed, Jun 11, 2014 at 9:59 AM, Martin Sladecek >> <[email protected]> wrote: >> >>> Binding the property would mean the text field can be edited (the field >>> is >>> still editable), but it's content cannot be committed (by either pressing >>> ENTER or focusing out of the field), so it's content is practically >>> irrelevant. >>> If you think there's a reasonable use case for this, there should be no >>> problem with having writable value. >>> >>> -Martin >>> >>> >>> On 11.6.2014 15:53, Scott Palmer wrote: >>> >>>> In FormattedTextField why is the value property returned as a >>>> read-only property? >>>> This control should allow bi-directional bindings to the value property. >>>> >>>> Scott >>>> >>>> On Wed, Jun 11, 2014 at 4:50 AM, Martin Sladecek >>>> <[email protected]> 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 >>>>> >>>>> >
