1 /*
   2  * Copyright (c) 2014, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 package javafx.scene.control;
  26 
  27 import com.sun.javafx.scene.control.FormatterAccessor;
  28 import javafx.beans.NamedArg;
  29 import javafx.beans.property.ObjectProperty;
  30 import javafx.beans.property.ObjectPropertyBase;
  31 import javafx.util.StringConverter;
  32 
  33 import java.util.function.Consumer;
  34 import java.util.function.UnaryOperator;
  35 
  36 /**
  37  * A Formatter describes a format of a {@code TextInputControl} text by using two distinct mechanisms:
  38  * <ul>
  39  *     <li>A filter ({@link #getFilter()}) that can intercept and modify user input. This helps to keep the text
  40  *     in the desired format. A default text supplier can be used to provide the intial text.</li>
  41  *     <li>A value converter ({@link #getValueConverter()}) and value ({@link #valueProperty()})
  42  *     can be used to provide special format that represents a value of type {@code V}.
  43  *     If the control is editable and the text is changed by the user, the value is then updated to correspond to the text.
  44  * </ul>
  45  * <p>
  46  * It's possible to have a formatter with just filter or value converter. If value converter is not provided however, setting a value will
  47  * result in an {@code IllegalStateException} and the value is always null.
  48  * <p>
  49  * Since {@code Formatter} contains a value which represents the state of the {@code TextInputControl} to which it is currently assigned, a single
  50  * {@code Formatter} instance can be used only in one {@code TextInputControl} at a time.
  51  *
  52  * @param <V> The type of the value
  53  * @since JavaFX 8u40
  54  */
  55 public class TextFormatter<V> {
  56     private final StringConverter<V> valueConverter;
  57     private final UnaryOperator<Change> filter;
  58 
  59     private Consumer<TextFormatter<?>> textUpdater;
  60 
  61     /**
  62      * This string converter converts the text to the same String value. This might be useful for cases where you
  63      * want to manipulate with the text through the value or you need to provide a default text value.
  64      */
  65     public static final StringConverter<String> IDENTITY_STRING_CONVERTER = new StringConverter<String>() {
  66         @Override
  67         public String toString(String object) {
  68             return object == null ? "" : object;
  69         }
  70         @Override
  71         public String fromString(String string) {
  72             return string;
  73         }
  74     };
  75 
  76 
  77     /**
  78      * Creates a new Formatter with the provided filter.
  79      * @param filter The filter to use in this formatter or null
  80      */
  81     public TextFormatter(@NamedArg("filter") UnaryOperator<Change> filter) {
  82         this(null, null, filter);
  83     }
  84 
  85     /**
  86      * Creates a new Formatter with the provided filter, value converter and default value.
  87      * @param valueConverter The value converter to use in this formatter or null.
  88      * @param defaultValue the default value.
  89      * @param filter The filter to use in this formatter or null
  90      */
  91     public TextFormatter(@NamedArg("valueConverter") StringConverter<V> valueConverter,
  92                          @NamedArg("defaultValue") V defaultValue, @NamedArg("filter") UnaryOperator<Change> filter) {
  93         this.filter = filter;
  94         this.valueConverter = valueConverter;
  95         setValue(defaultValue);
  96     }
  97 
  98     /**
  99      * Creates a new Formatter with the provided value converter and default value.
 100      * @param valueConverter The value converter to use in this formatter. This must not be null.
 101      * @param defaultValue the default value
 102      */
 103     public TextFormatter(@NamedArg("valueConverter") StringConverter<V> valueConverter, @NamedArg("defaultValue") V defaultValue) {
 104         this(valueConverter, defaultValue, null);
 105     }
 106 
 107     /**
 108      * Creates a new Formatter with the provided value converter. The default value will be null.
 109      * @param valueConverter The value converter to use in this formatter. This must not be null.
 110      */
 111     public TextFormatter(@NamedArg("valueConverter") StringConverter<V> valueConverter) {
 112         this(valueConverter, null, null);
 113     }
 114 
 115 
 116     /**
 117      * The converter between the values and text.
 118      * It maintains a "binding" between the {@link javafx.scene.control.TextInputControl#textProperty()} }
 119      * and {@link #valueProperty()} }. The value is updated when the control loses it's focus or it is commited (TextField only).
 120      * Setting the value will update the text of the control, usin the provided converter.
 121      *
 122      * If it's impossible to convert text to value, an exception should be thrown.
 123      * @return StringConverter for values or null if none provided
 124      * @see javafx.scene.control.TextField#commitValue()
 125      * @see javafx.scene.control.TextField#cancelEdit()
 126      */
 127     public final StringConverter<V> getValueConverter() {
 128         return valueConverter;
 129     }
 130 
 131     /**
 132      * Filter allows user to intercept and modify any change done to the text content.
 133      * <p>
 134      * The filter itself is an {@code UnaryOperator} that accepts {@link javafx.scene.control.TextFormatter.Change} object.
 135      * It should return a {@link javafx.scene.control.TextFormatter.Change} object that contains the actual (filtered)
 136      * change. Returning null rejects the change.
 137      * @return the filter for this formatter or null if there is none
 138      */
 139     public final UnaryOperator<Change> getFilter() {
 140         return filter;
 141     }
 142 
 143     /**
 144      * The current value for this formatter. When the formatter is set on a {@code TextInputControl} and has a
 145      * {@code valueConverter}, the value is set by the control, when the text is commited.
 146      */
 147     private final ObjectProperty<V> value = new ObjectPropertyBase<V>() {
 148 
 149         @Override
 150         public Object getBean() {
 151             return TextFormatter.this;
 152         }
 153 
 154         @Override
 155         public String getName() {
 156             return "value";
 157         }
 158 
 159         @Override
 160         protected void invalidated() {
 161             if (valueConverter == null && get() != null) {
 162                 if (isBound()) {
 163                     unbind();
 164                 }
 165                 throw new IllegalStateException("Value changes are not supported when valueConverter is not set");
 166             }
 167             updateText();
 168         }
 169     };
 170 
 171     public final ObjectProperty<V> valueProperty() {
 172         return value;
 173     }
 174     public final void setValue(V value) {
 175         if (valueConverter == null && value != null) {
 176             throw new IllegalStateException("Value changes are not supported when valueConverter is not set");
 177         }
 178         this.value.set(value);
 179     }
 180     public final V getValue() {
 181         return value.get();
 182     }
 183 
 184     private void updateText() {
 185         if (textUpdater != null) {
 186             textUpdater.accept(this);
 187         }
 188     }
 189 
 190     void bindToControl(Consumer<TextFormatter<?>> updater) {
 191         if (textUpdater != null) {
 192             throw new IllegalStateException("Formatter is already used in other control");
 193         }
 194         this.textUpdater = updater;
 195     }
 196 
 197     void unbindFromControl() {
 198         this.textUpdater = null;
 199     }
 200 
 201     void updateValue(String text) {
 202         if (!value.isBound()) {
 203             try {
 204                 V v = valueConverter.fromString(text);
 205                 setValue(v);
 206             } catch (Exception e) {
 207                 updateText(); // Set the text with the latest value
 208             }
 209         }
 210     }
 211 
 212     /**
 213      * Contains the state representing a change in the content or selection for a
 214      * TextInputControl. This object is passed to any registered
 215      * {@code formatter} on the TextInputControl whenever the text
 216      * for the TextInputControl is modified.
 217      * <p>
 218      *     This class contains state and convenience methods for determining what
 219      *     change occurred on the control. It also has a reference to the
 220      *     TextInputControl itself so that the developer may query any other
 221      *     state on the control. Note that you should never modify the state
 222      *     of the control directly from within the formatter handler.
 223      * </p>
 224      * <p>
 225      *     The Change of the text is described by <b>range</b> ({@link #getRangeStart()}, {@link #getRangeEnd()}) and
 226      *     text ({@link #getText()}. There are 3 cases that can occur:
 227      *     <ul>
 228      *         <li><b>Some text was deleted:</b> In this case, {@code text} is empty and {@code range} denotes the {@code range} of deleted text.
 229      *         E.g. In text "Lorem ipsum dolor sit amet", removal of the second word would result in {@code range} being (6,11) and
 230      *         an empty {@code text}. Similarly, if you want to delete some different or additional text, just set the {@code range}.
 231      *         If you want to remove first word instead of the second, just call {@code setRange(0,5)}</li>
 232      *         <li><b>Some text was added:</b> Now the {@code range} is empty (means nothing was deleted), but it's value is still important.
 233      *         Both the start and end of the {@code range} point to the index wheret the new text was added. E.g. adding "ipsum " to "Lorem dolor sit amet"
 234      *         would result in a change with {@code range} of (6,6) and {@code text} containing the String "ipsum ".</li>
 235      *         <li><b>Some text was replaced:</b> The combination of the 2 cases above. Both {@code text} and {@code range} are not empty. The text in {@code range} is deleted
 236      *         and replaced by {@code text} in the Change. The new text is added instead of the old text, which is at the beginning of the {@code range}.
 237      *         E.g. when some text is being deleted, you can simply replace it by some placeholder text just by setting a new text
 238      *         ({@code setText("new text")})</li>
 239      *     </ul>
 240      * <p>
 241      *     The Change is mutable, but not observable. It should be used
 242      *     only for the life of a single change. It is intended that the
 243      *     Change will be modified from within the formatter.
 244      * </p>
 245      * @since JavaFX 8u40
 246      */
 247     public static final class Change implements Cloneable {
 248         private final FormatterAccessor accessor;
 249         private Control control;
 250         int start;
 251         int end;
 252         String text;
 253 
 254         int anchor;
 255         int caret;
 256 
 257         Change(Control control, FormatterAccessor accessor,  int anchor, int caret) {
 258             this(control, accessor, caret, caret, "", anchor, caret);
 259         }
 260 
 261         Change(Control control, FormatterAccessor accessor, int start, int end, String text) {
 262             this(control, accessor, start, end, text, start + text.length(), start + text.length());
 263         }
 264 
 265         // Restrict construction to TextInputControl only. Because we are the
 266         // only ones who can create this, we don't bother doing a check here
 267         // to make sure the arguments are within reason (they will be).
 268         Change(Control control, FormatterAccessor accessor, int start, int end, String text, int anchor, int caret) {
 269             this.control = control;
 270             this.accessor = accessor;
 271             this.start = start;
 272             this.end = end;
 273             this.text = text;
 274             this.anchor = anchor;
 275             this.caret = caret;
 276         }
 277 
 278         /**
 279          * Gets the control associated with this change.
 280          * @return The control associated with this change. This will never be null.
 281          */
 282         public final Control getControl() { return control; }
 283 
 284         /**
 285          * Gets the start index into the {@link TextInputControl#getText()}
 286          * for the modification. This will always be a value &gt; 0 and
 287          * &lt;= {@link TextInputControl#getLength()}.
 288          *
 289          * @return The start index
 290          */
 291         public final int getRangeStart() { return start; }
 292 
 293         /**
 294          * Gets the end index into the {@link TextInputControl#getText()}
 295          * for the modification. This will always be a value &gt; {@link #getRangeStart()} and
 296          * &lt;= {@link TextInputControl#getLength()}.
 297          *
 298          * @return The end index
 299          */
 300         public final int getRangeEnd() { return end; }
 301 
 302         /**
 303          * A method assigning both the start and end values
 304          * together, in such a way as to ensure they are valid with respect to
 305          * each other. The start must be less than or equal to the end.
 306          *
 307          * @param start The new start value. Must be a valid start value
 308          * @param end The new end value. Must be a valid end value
 309          */
 310         public final void setRange(int start, int end) {
 311             int length = accessor.getTextLength();
 312             if (start < 0 || start > length || end < 0 || end > length) {
 313                 throw new IndexOutOfBoundsException();
 314             }
 315             this.start = start;
 316             this.end = end;
 317         }
 318 
 319 
 320         /**
 321          * Gets the new caret position. This value will always be &gt; 0 and
 322          * &lt;= {@link #getControlNewText()}{@code}.getLength()}
 323          *
 324          * @return The new caret position
 325          */
 326         public final int getCaretPosition() { return caret; }
 327 
 328         /**
 329          * Gets the new anchor. This value will always be &gt; 0 and
 330          * &lt;= {@link #getControlNewText()}{@code}.getLength()}
 331          *
 332          * @return The new anchor position
 333          */
 334         public final int getAnchor() { return anchor; }
 335 
 336         /**
 337          * Gets the current caret position of the control.
 338          * @return The previous caret position
 339          */
 340         public final int getControlCaretPosition() { return accessor.getCaret();}
 341 
 342         /**
 343          * Gets the current anchor position of the control.
 344          * @return The previous anchor
 345          */
 346         public final int getControlAnchor() { return accessor.getAnchor(); }
 347 
 348         /**
 349          * Sets the selection. The anchor and caret position values must be &gt; 0 and
 350          * &lt;= {@link #getControlNewText()}{@code}.getLength()}. Note that there
 351          * is an order dependence here, in that the positions should be
 352          * specified after the new text has been specified.
 353          *
 354          * @param newAnchor The new anchor position
 355          * @param newCaretPosition The new caret position
 356          */
 357         public final void selectRange(int newAnchor, int newCaretPosition) {
 358             if (newAnchor < 0 || newAnchor > accessor.getTextLength() - (end - start) + text.length()
 359                     || newCaretPosition < 0 || newCaretPosition > accessor.getTextLength() - (end - start) + text.length()) {
 360                 throw new IndexOutOfBoundsException();
 361             }
 362             anchor = newAnchor;
 363             caret = newCaretPosition;
 364         }
 365 
 366         /**
 367          * Gets the selection of this change. Note that the selection range refers to {@link #getControlNewText()}, not
 368          * the current control text.
 369          * @return The selected range of this change.
 370          */
 371         public final IndexRange getSelection() {
 372             return IndexRange.normalize(anchor, caret);
 373         }
 374 
 375 
 376         /**
 377          * Sets the anchor. The anchor value must be &gt; 0 and
 378          * &lt;= {@link #getControlNewText()}{@code}.getLength()}. Note that there
 379          * is an order dependence here, in that the position should be
 380          * specified after the new text has been specified.
 381          *
 382          * @param newAnchor The new anchor position
 383          */
 384         public final void setAnchor(int newAnchor) {
 385             if (newAnchor < 0 || newAnchor > accessor.getTextLength() - (end - start) + text.length()) {
 386                 throw new IndexOutOfBoundsException();
 387             }
 388             anchor = newAnchor;
 389         }
 390 
 391         /**
 392          * Sets the caret position. The caret position value must be &gt; 0 and
 393          * &lt;= {@link #getControlNewText()}{@code}.getLength()}. Note that there
 394          * is an order dependence here, in that the position should be
 395          * specified after the new text has been specified.
 396          *
 397          * @param newCaretPosition The new caret position
 398          */
 399         public final void setCaretPosition(int newCaretPosition) {
 400             if (newCaretPosition < 0 || newCaretPosition > accessor.getTextLength() - (end - start) + text.length()) {
 401                 throw new IndexOutOfBoundsException();
 402             }
 403             caret = newCaretPosition;
 404         }
 405 
 406         /**
 407          * Gets the text used in this change. For example, this may be new
 408          * text being added, or text which is replacing all the control's text
 409          * within the range of start and end. Typically it is an empty string
 410          * only for cases where the range is being deleted.
 411          *
 412          * @return The text involved in this change. This will never be null.
 413          */
 414         public final String getText() { return text; }
 415 
 416         /**
 417          * Sets the text to use in this change. This is used to replace the
 418          * range from start to end, if such a range exists, or to insert text
 419          * at the position represented by start == end.
 420          *
 421          * @param value The text. This cannot be null.
 422          */
 423         public final void setText(String value) {
 424             if (value == null) throw new NullPointerException();
 425             text = value;
 426         }
 427 
 428         /**
 429          * This is the full text that control has before the change. To get the text
 430          * after this change, use {@link #getControlNewText()}.
 431          * @return the previous text of control
 432          */
 433         public final String getControlText() {
 434             return accessor.getText(0, accessor.getTextLength());
 435         }
 436 
 437         /**
 438          * Gets the complete new text which will be used on the control after
 439          * this change. Note that some controls (such as TextField) may do further
 440          * filtering after the change is made (such as stripping out newlines)
 441          * such that you cannot assume that the newText will be exactly the same
 442          * as what is finally set as the content on the control, however it is
 443          * correct to assume that this is the case for the purpose of computing
 444          * the new caret position and new anchor position (as those values supplied
 445          * will be modified as necessary after the control has stripped any
 446          * additional characters that the control might strip).
 447          *
 448          * @return The controls proposed new text at the time of this call, according
 449          *         to the state set for start, end, and text properties on this Change object.
 450          */
 451         public final String getControlNewText() {
 452             return accessor.getText(0, start) + text + accessor.getText(end, accessor.getTextLength());
 453         }
 454 
 455         /**
 456          * Gets whether this change was in response to text being added. Note that
 457          * after the Change object is modified by the formatter (by one
 458          * of the setters) the return value of this method is not altered. It answers
 459          * as to whether this change was fired as a result of text being added,
 460          * not whether text will end up being added in the end.
 461          *
 462          * @return true if text was being added
 463          */
 464         public final boolean isAdded() { return !text.isEmpty(); }
 465 
 466         /**
 467          * Gets whether this change was in response to text being deleted. Note that
 468          * after the Change object is modified by the formatter (by one
 469          * of the setters) the return value of this method is not altered. It answers
 470          * as to whether this change was fired as a result of text being deleted,
 471          * not whether text will end up being deleted in the end.
 472          *
 473          * @return true if text was being deleted
 474          */
 475         public final boolean isDeleted() { return start != end; }
 476 
 477         /**
 478          * Gets whether this change was in response to text being replaced. Note that
 479          * after the Change object is modified by the formatter (by one
 480          * of the setters) the return value of this method is not altered. It answers
 481          * as to whether this change was fired as a result of text being replaced,
 482          * not whether text will end up being replaced in the end.
 483          *
 484          * @return true if text was being replaced
 485          */
 486         public final boolean isReplaced() {
 487             return isAdded() && isDeleted();
 488         }
 489 
 490         /**
 491          * The content change is any of add, delete or replace changes. Basically it's a shortcut for
 492          * {@code c.isAdded() || c.isDeleted() };
 493          * @return true if the content changed
 494          */
 495         public final boolean isContentChange() {
 496             return isAdded() || isDeleted();
 497         }
 498 
 499         @Override
 500         public String toString() {
 501             StringBuilder builder = new StringBuilder("TextInputControl.Change [");
 502             if (isReplaced()) {
 503                 builder.append(" replaced \"").append(accessor.getText(start, end)).append("\" with \"").append(text).
 504                         append("\" at (").append(start).append(", ").append(end).append(")");
 505             } else if (isDeleted()) {
 506                 builder.append(" deleted \"").append(accessor.getText(start, end)).
 507                         append("\" at (").append(start).append(", ").append(end).append(")");
 508             } else if (isAdded()) {
 509                 builder.append(" added \"").append(text).append("\" at ").append(start);
 510             }
 511             if (isAdded() || isDeleted()) {
 512                 builder.append("; ");
 513             } else {
 514                 builder.append(" ");
 515             }
 516             builder.append("new selection (anchor, caret): [").append(anchor).append(", ").append(caret).append("]");
 517             builder.append(" ]");
 518             return builder.toString();
 519         }
 520 
 521         @Override
 522         public Change clone() {
 523             try {
 524                 return (Change) super.clone();
 525             } catch (CloneNotSupportedException e) {
 526                 // Cannot happen
 527                 throw new RuntimeException(e);
 528             }
 529         }
 530     }
 531 
 532 }