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 > 0 and 287 * <= {@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 > {@link #getRangeStart()} and 296 * <= {@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 > 0 and 322 * <= {@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 > 0 and 330 * <= {@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 > 0 and 350 * <= {@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 > 0 and 378 * <= {@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 > 0 and 393 * <= {@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 }