1 /* 2 * Copyright (c) 2011, 2017, 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 26 package javafx.scene.control; 27 28 import com.sun.javafx.scene.control.FormatterAccessor; 29 import javafx.beans.DefaultProperty; 30 import javafx.beans.InvalidationListener; 31 import javafx.beans.Observable; 32 import javafx.beans.binding.IntegerBinding; 33 import javafx.beans.binding.StringBinding; 34 import javafx.beans.property.BooleanProperty; 35 import javafx.beans.property.ObjectProperty; 36 import javafx.beans.property.ObjectPropertyBase; 37 import javafx.beans.property.ReadOnlyBooleanProperty; 38 import javafx.beans.property.ReadOnlyBooleanWrapper; 39 import javafx.beans.property.ReadOnlyIntegerProperty; 40 import javafx.beans.property.ReadOnlyIntegerWrapper; 41 import javafx.beans.property.ReadOnlyObjectProperty; 42 import javafx.beans.property.ReadOnlyObjectWrapper; 43 import javafx.beans.property.ReadOnlyStringProperty; 44 import javafx.beans.property.ReadOnlyStringWrapper; 45 import javafx.beans.property.SimpleBooleanProperty; 46 import javafx.beans.property.SimpleStringProperty; 47 import javafx.beans.property.StringProperty; 48 import javafx.beans.value.ChangeListener; 49 import javafx.beans.value.ObservableStringValue; 50 import javafx.beans.value.ObservableValue; 51 import javafx.beans.value.WritableValue; 52 import javafx.css.CssMetaData; 53 import javafx.css.FontCssMetaData; 54 import javafx.css.PseudoClass; 55 import javafx.css.StyleOrigin; 56 import javafx.css.Styleable; 57 import javafx.css.StyleableObjectProperty; 58 import javafx.css.StyleableProperty; 59 import javafx.scene.AccessibleAction; 60 import javafx.scene.AccessibleAttribute; 61 import javafx.scene.input.Clipboard; 62 import javafx.scene.input.ClipboardContent; 63 import javafx.scene.text.Font; 64 65 import java.text.BreakIterator; 66 import java.util.ArrayList; 67 import java.util.Collections; 68 import java.util.Date; 69 import java.util.List; 70 71 import com.sun.javafx.util.Utils; 72 import com.sun.javafx.binding.ExpressionHelper; 73 import com.sun.javafx.scene.NodeHelper; 74 import javafx.util.StringConverter; 75 76 /** 77 * Abstract base class for text input controls. 78 * @since JavaFX 2.0 79 */ 80 @DefaultProperty("text") 81 public abstract class TextInputControl extends Control { 82 /** 83 * Interface representing a text input's content. Since it is an ObservableStringValue, 84 * you can also bind to, or observe the content. 85 * @since JavaFX 2.0 86 */ 87 protected interface Content extends ObservableStringValue { 88 /** 89 * Retrieves a subset of the content. 90 * 91 * @param start the start 92 * @param end the end 93 * @return a subset of the content 94 */ 95 public String get(int start, int end); 96 97 /** 98 * Inserts a sequence of characters into the content. 99 * 100 * @param index the index 101 * @param text the text string 102 * @param notifyListeners the notify listener flag 103 * @since JavaFX 2.1 104 */ 105 public void insert(int index, String text, boolean notifyListeners); 106 107 /** 108 * Removes a sequence of characters from the content. 109 * 110 * @param start the start 111 * @param end the end 112 * @param notifyListeners the notify listener flag 113 * @since JavaFX 2.1 114 */ 115 public void delete(int start, int end, boolean notifyListeners); 116 117 /** 118 * Returns the number of characters represented by the content. 119 * @return the number of characters 120 */ 121 public int length(); 122 } 123 124 /*************************************************************************** 125 * * 126 * Constructors * 127 * * 128 **************************************************************************/ 129 130 /** 131 * Creates a new TextInputControl. The content is an immutable property and 132 * must be specified (as non-null) at the time of construction. 133 * 134 * @param content a non-null implementation of Content. 135 */ 136 protected TextInputControl(final Content content) { 137 this.content = content; 138 139 // Add a listener so that whenever the Content is changed, we notify 140 // listeners of the text property that it is invalid. 141 content.addListener(observable -> { 142 if (content.length() > 0) { 143 text.textIsNull = false; 144 } 145 text.controlContentHasChanged(); 146 }); 147 148 // Bind the length to be based on the length of the text property 149 length.bind(new IntegerBinding() { 150 { bind(text); } 151 @Override protected int computeValue() { 152 String txt = text.get(); 153 return txt == null ? 0 : txt.length(); 154 } 155 }); 156 157 // Bind the selected text to be based on the selection and text properties 158 selectedText.bind(new StringBinding() { 159 { bind(selection, text); } 160 @Override protected String computeValue() { 161 String txt = text.get(); 162 IndexRange sel = selection.get(); 163 if (txt == null || sel == null) return ""; 164 165 int start = sel.getStart(); 166 int end = sel.getEnd(); 167 int length = txt.length(); 168 if (end > start + length) end = length; 169 if (start > length-1) start = end = 0; 170 return txt.substring(start, end); 171 } 172 }); 173 174 focusedProperty().addListener((ob, o, n) -> { 175 if (n) { 176 if (getTextFormatter() != null) { 177 updateText(getTextFormatter()); 178 } 179 } else { 180 commitValue(); 181 } 182 }); 183 184 // Specify the default style class 185 getStyleClass().add("text-input"); 186 } 187 188 /*************************************************************************** 189 * * 190 * Properties * 191 * * 192 **************************************************************************/ 193 194 /** 195 * The default font to use for text in the TextInputControl. If the TextInputControl's text is 196 * rich text then this font may or may not be used depending on the font 197 * information embedded in the rich text, but in any case where a default 198 * font is required, this font will be used. 199 * @return the font property 200 * @since JavaFX 8.0 201 */ 202 public final ObjectProperty<Font> fontProperty() { 203 if (font == null) { 204 font = new StyleableObjectProperty<Font>(Font.getDefault()) { 205 206 207 private boolean fontSetByCss = false; 208 209 @Override 210 public void applyStyle(StyleOrigin newOrigin, Font value) { 211 212 // 213 // RT-20727 - if CSS is setting the font, then make sure invalidate doesn't call NodeHelper.reapplyCSS 214 // 215 try { 216 // super.applyStyle calls set which might throw if value is bound. 217 // Have to make sure fontSetByCss is reset. 218 fontSetByCss = true; 219 super.applyStyle(newOrigin, value); 220 } catch(Exception e) { 221 throw e; 222 } finally { 223 fontSetByCss = false; 224 } 225 226 } 227 228 229 @Override 230 public void set(Font value) { 231 final Font oldValue = get(); 232 if (value == null ? oldValue == null : value.equals(oldValue)) { 233 return; 234 } 235 super.set(value); 236 } 237 238 @Override 239 protected void invalidated() { 240 // RT-20727 - if font is changed by calling setFont, then 241 // css might need to be reapplied since font size affects 242 // calculated values for styles with relative values 243 if(fontSetByCss == false) { 244 NodeHelper.reapplyCSS(TextInputControl.this); 245 } 246 } 247 248 @Override 249 public CssMetaData<TextInputControl,Font> getCssMetaData() { 250 return StyleableProperties.FONT; 251 } 252 253 @Override 254 public Object getBean() { 255 return TextInputControl.this; 256 } 257 258 @Override 259 public String getName() { 260 return "font"; 261 } 262 }; 263 } 264 return font; 265 } 266 267 private ObjectProperty<Font> font; 268 public final void setFont(Font value) { fontProperty().setValue(value); } 269 public final Font getFont() { return font == null ? Font.getDefault() : font.getValue(); } 270 271 /** 272 * The prompt text to display in the {@code TextInputControl}. If set to null or an empty string, no 273 * prompt text is displayed. 274 * 275 * @defaultValue An empty String 276 * @since JavaFX 2.2 277 */ 278 private StringProperty promptText = new SimpleStringProperty(this, "promptText", "") { 279 @Override protected void invalidated() { 280 // Strip out newlines 281 String txt = get(); 282 if (txt != null && txt.contains("\n")) { 283 txt = txt.replace("\n", ""); 284 set(txt); 285 } 286 } 287 }; 288 public final StringProperty promptTextProperty() { return promptText; } 289 public final String getPromptText() { return promptText.get(); } 290 public final void setPromptText(String value) { promptText.set(value); } 291 292 293 /** 294 * The property contains currently attached {@link TextFormatter}. 295 * Since the value is part of the {@code Formatter}, changing the TextFormatter will update the text based on the new textFormatter. 296 * 297 * @defaultValue null 298 * @since JavaFX 8u40 299 */ 300 private final ObjectProperty<TextFormatter<?>> textFormatter = new ObjectPropertyBase<TextFormatter<?>>() { 301 302 private TextFormatter<?> oldFormatter = null; 303 304 @Override 305 public Object getBean() { 306 return TextInputControl.this; 307 } 308 309 @Override 310 public String getName() { 311 return "textFormatter"; 312 } 313 314 @Override 315 protected void invalidated() { 316 final TextFormatter<?> formatter = get(); 317 try { 318 if (formatter != null) { 319 try { 320 formatter.bindToControl(f -> updateText(f)); 321 } catch (IllegalStateException e) { 322 if (isBound()) { 323 unbind(); 324 } 325 set(null); 326 throw e; 327 } 328 if (!isFocused()) { 329 updateText(get()); 330 } 331 } 332 333 if (oldFormatter != null) { 334 oldFormatter.unbindFromControl(); 335 } 336 } finally { 337 oldFormatter = formatter; 338 } 339 } 340 }; 341 public final ObjectProperty<TextFormatter<?>> textFormatterProperty() { return textFormatter; } 342 public final TextFormatter<?> getTextFormatter() { return textFormatter.get(); } 343 public final void setTextFormatter(TextFormatter<?> value) { textFormatter.set(value); } 344 345 private final Content content; 346 /** 347 * Returns the text input's content model. 348 * @return the text input's content model 349 */ 350 protected final Content getContent() { 351 return content; 352 } 353 354 /** 355 * The textual content of this TextInputControl. 356 */ 357 private TextProperty text = new TextProperty(); 358 public final String getText() { return text.get(); } 359 public final void setText(String value) { text.set(value); } 360 public final StringProperty textProperty() { return text; } 361 362 /** 363 * The number of characters in the text input. 364 */ 365 private ReadOnlyIntegerWrapper length = new ReadOnlyIntegerWrapper(this, "length"); 366 public final int getLength() { return length.get(); } 367 public final ReadOnlyIntegerProperty lengthProperty() { return length.getReadOnlyProperty(); } 368 369 /** 370 * Indicates whether this TextInputControl can be edited by the user. 371 */ 372 private BooleanProperty editable = new SimpleBooleanProperty(this, "editable", true) { 373 @Override protected void invalidated() { 374 pseudoClassStateChanged(PSEUDO_CLASS_READONLY, ! get()); 375 } 376 }; 377 public final boolean isEditable() { return editable.getValue(); } 378 public final void setEditable(boolean value) { editable.setValue(value); } 379 public final BooleanProperty editableProperty() { return editable; } 380 381 /** 382 * The current selection. 383 */ 384 private ReadOnlyObjectWrapper<IndexRange> selection = new ReadOnlyObjectWrapper<IndexRange>(this, "selection", new IndexRange(0, 0)); 385 public final IndexRange getSelection() { return selection.getValue(); } 386 public final ReadOnlyObjectProperty<IndexRange> selectionProperty() { return selection.getReadOnlyProperty(); } 387 388 /** 389 * Defines the characters in the TextInputControl which are selected 390 */ 391 private ReadOnlyStringWrapper selectedText = new ReadOnlyStringWrapper(this, "selectedText"); 392 public final String getSelectedText() { return selectedText.get(); } 393 public final ReadOnlyStringProperty selectedTextProperty() { return selectedText.getReadOnlyProperty(); } 394 395 /** 396 * The <code>anchor</code> of the text selection. 397 * The <code>anchor</code> and <code>caretPosition</code> make up the selection 398 * range. Selection must always be specified in terms of begin <= end, but 399 * <code>anchor</code> may be less than, equal to, or greater than the 400 * <code>caretPosition</code>. Depending on how the user selects text, 401 * the anchor might represent the lower or upper bound of the selection. 402 */ 403 private ReadOnlyIntegerWrapper anchor = new ReadOnlyIntegerWrapper(this, "anchor", 0); 404 public final int getAnchor() { return anchor.get(); } 405 public final ReadOnlyIntegerProperty anchorProperty() { return anchor.getReadOnlyProperty(); } 406 407 /** 408 * The current position of the caret within the text. 409 * The <code>anchor</code> and <code>caretPosition</code> make up the selection 410 * range. Selection must always be specified in terms of begin <= end, but 411 * <code>anchor</code> may be less than, equal to, or greater than the 412 * <code>caretPosition</code>. Depending on how the user selects text, 413 * the caretPosition might represent the lower or upper bound of the selection. 414 */ 415 private ReadOnlyIntegerWrapper caretPosition = new ReadOnlyIntegerWrapper(this, "caretPosition", 0); 416 public final int getCaretPosition() { return caretPosition.get(); } 417 public final ReadOnlyIntegerProperty caretPositionProperty() { return caretPosition.getReadOnlyProperty(); } 418 419 private UndoRedoChange undoChangeHead = new UndoRedoChange(); 420 private UndoRedoChange undoChange = undoChangeHead; 421 private boolean createNewUndoRecord = false; 422 423 /** 424 * The property describes if it's currently possible to undo the latest change of the content that was done. 425 * @defaultValue false 426 * @since JavaFX 8u40 427 */ 428 private final ReadOnlyBooleanWrapper undoable = new ReadOnlyBooleanWrapper(this, "undoable", false); 429 public final boolean isUndoable() { return undoable.get(); } 430 public final ReadOnlyBooleanProperty undoableProperty() { return undoable.getReadOnlyProperty(); } 431 432 433 /** 434 * The property describes if it's currently possible to redo the latest change of the content that was undone. 435 * @defaultValue false 436 * @since JavaFX 8u40 437 */ 438 private final ReadOnlyBooleanWrapper redoable = new ReadOnlyBooleanWrapper(this, "redoable", false); 439 public final boolean isRedoable() { return redoable.get(); } 440 public final ReadOnlyBooleanProperty redoableProperty() { return redoable.getReadOnlyProperty(); } 441 442 /*************************************************************************** 443 * * 444 * Methods * 445 * * 446 **************************************************************************/ 447 448 /** 449 * Returns a subset of the text input's content. 450 * 451 * @param start must be a value between 0 and end - 1. 452 * @param end must be less than or equal to the length 453 * @return the subset of the text input's content 454 */ 455 public String getText(int start, int end) { 456 if (start > end) { 457 throw new IllegalArgumentException("The start must be <= the end"); 458 } 459 460 if (start < 0 461 || end > getLength()) { 462 throw new IndexOutOfBoundsException(); 463 } 464 465 return getContent().get(start, end); 466 } 467 468 /** 469 * Appends a sequence of characters to the content. 470 * 471 * @param text a non null String 472 */ 473 public void appendText(String text) { 474 insertText(getLength(), text); 475 } 476 477 /** 478 * Inserts a sequence of characters into the content. 479 * 480 * @param index The location to insert the text. 481 * @param text The text to insert. 482 */ 483 public void insertText(int index, String text) { 484 replaceText(index, index, text); 485 } 486 487 /** 488 * Removes a range of characters from the content. 489 * 490 * @param range The range of text to delete. The range object must not be null. 491 * 492 * @see #deleteText(int, int) 493 */ 494 public void deleteText(IndexRange range) { 495 replaceText(range, ""); 496 } 497 498 /** 499 * Removes a range of characters from the content. 500 * 501 * @param start The starting index in the range, inclusive. This must be >= 0 and < the end. 502 * @param end The ending index in the range, exclusive. This is one-past the last character to 503 * delete (consistent with the String manipulation methods). This must be > the start, 504 * and <= the length of the text. 505 */ 506 public void deleteText(int start, int end) { 507 replaceText(start, end, ""); 508 } 509 510 /** 511 * Replaces a range of characters with the given text. 512 * 513 * @param range The range of text to replace. The range object must not be null. 514 * @param text The text that is to replace the range. This must not be null. 515 * 516 * @see #replaceText(int, int, String) 517 */ 518 public void replaceText(IndexRange range, String text) { 519 final int start = range.getStart(); 520 final int end = start + range.getLength(); 521 replaceText(start, end, text); 522 } 523 524 /** 525 * Replaces a range of characters with the given text. 526 * 527 * @param start The starting index in the range, inclusive. This must be >= 0 and < the end. 528 * @param end The ending index in the range, exclusive. This is one-past the last character to 529 * delete (consistent with the String manipulation methods). This must be > the start, 530 * and <= the length of the text. 531 * @param text The text that is to replace the range. This must not be null. 532 */ 533 public void replaceText(final int start, final int end, final String text) { 534 if (start > end) { 535 throw new IllegalArgumentException(); 536 } 537 538 if (text == null) { 539 throw new NullPointerException(); 540 } 541 542 if (start < 0 543 || end > getLength()) { 544 throw new IndexOutOfBoundsException(); 545 } 546 547 if (!this.text.isBound()) { 548 final int oldLength = getLength(); 549 TextFormatter<?> formatter = getTextFormatter(); 550 TextFormatter.Change change = new TextFormatter.Change(this, getFormatterAccessor(), start, end, text); 551 if (formatter != null && formatter.getFilter() != null) { 552 change = formatter.getFilter().apply(change); 553 if (change == null) { 554 return; 555 } 556 } 557 558 // Update the content 559 updateContent(change, oldLength == 0); 560 561 } 562 } 563 564 private void updateContent(TextFormatter.Change change, boolean forceNewUndoRecord) { 565 final boolean nonEmptySelection = getSelection().getLength() > 0; 566 String oldText = getText(change.start, change.end); 567 int adjustmentAmount = replaceText(change.start, change.end, change.text, change.getAnchor(), change.getCaretPosition()); 568 String newText = getText(change.start, change.start + change.text.length() - adjustmentAmount); 569 if (newText.equals(oldText)) { 570 // Undo record not required as there is no change in the text. 571 return; 572 } 573 574 /* 575 * A new undo record is created, if 576 * 1. createNewUndoRecord is true, currently it is set to true for paste operation 577 * 2. Text is selected and a character is typed 578 * 3. This is the first operation to be added to undo record 579 * 4. forceNewUndoRecord is true, currently it is set to true if there is no text present 580 * 5. Space character is typed 581 * 6. 2500 milliseconds are elapsed since the undo record was created 582 * 7. Cursor position is changed and a character is typed 583 * 8. A range of text is replaced programmatically using replaceText() 584 * Otherwise, the last undo record is updated or discarded. 585 */ 586 587 int endOfUndoChange = undoChange == undoChangeHead ? -1 : undoChange.start + undoChange.newText.length(); 588 boolean isNewSpaceChar = false; 589 if (newText.equals(" ")) { 590 if (UndoRedoChange.isSpaceCharSequence()) { 591 isNewSpaceChar = false; 592 } else { 593 isNewSpaceChar = true; 594 UndoRedoChange.setSpaceCharSequence(true); 595 } 596 } else { 597 UndoRedoChange.setSpaceCharSequence(false); 598 } 599 if (createNewUndoRecord || nonEmptySelection || endOfUndoChange == -1 || forceNewUndoRecord || 600 isNewSpaceChar || UndoRedoChange.ifChangeDurationElapsed() || 601 (endOfUndoChange != change.start && endOfUndoChange != change.end) || change.end - change.start > 0) { 602 undoChange = undoChange.add(change.start, oldText, newText); 603 } else if (change.start != change.end && change.text.isEmpty()) { 604 // I know I am deleting, and am located at the end of the range of the current undo record 605 if (undoChange.newText.length() > 0) { 606 undoChange.newText = undoChange.newText.substring(0, change.start - undoChange.start); 607 if (undoChange.newText.isEmpty()) { 608 // throw away this undo change record 609 undoChange = undoChange.discard(); 610 } 611 } else { 612 if (change.start == endOfUndoChange) { 613 undoChange.oldText += oldText; 614 } else { // end == endOfUndoChange 615 undoChange.oldText = oldText + undoChange.oldText; 616 undoChange.start--; 617 } 618 } 619 } else { 620 // I know I am adding, and am located at the end of the range of the current undo record 621 undoChange.newText += newText; 622 } 623 updateUndoRedoState(); 624 } 625 626 /** 627 * Transfers the currently selected range in the text to the clipboard, 628 * removing the current selection. 629 */ 630 public void cut() { 631 copy(); 632 IndexRange selection = getSelection(); 633 deleteText(selection.getStart(), selection.getEnd()); 634 } 635 636 /** 637 * Transfers the currently selected range in the text to the clipboard, 638 * leaving the current selection. 639 */ 640 public void copy() { 641 final String selectedText = getSelectedText(); 642 if (selectedText.length() > 0) { 643 final ClipboardContent content = new ClipboardContent(); 644 content.putString(selectedText); 645 Clipboard.getSystemClipboard().setContent(content); 646 } 647 } 648 649 /** 650 * Transfers the contents in the clipboard into this text, 651 * replacing the current selection. If there is no selection, the contents 652 * in the clipboard is inserted at the current caret position. 653 */ 654 public void paste() { 655 final Clipboard clipboard = Clipboard.getSystemClipboard(); 656 if (clipboard.hasString()) { 657 final String text = clipboard.getString(); 658 if (text != null) { 659 createNewUndoRecord = true; 660 try { 661 replaceSelection(text); 662 } finally { 663 createNewUndoRecord = false; 664 } 665 } 666 } 667 } 668 669 /** 670 * Moves the selection backward one char in the text. This may have the 671 * effect of deselecting, depending on the location of the anchor relative 672 * to the caretPosition. This function effectively just moves the caretPosition. 673 */ 674 public void selectBackward() { 675 if (getCaretPosition() > 0 && getLength() > 0) { 676 // because the anchor stays put, by moving the caret to the left 677 // we ensure that a selection is registered and that it is correct 678 if (charIterator == null) { 679 charIterator = BreakIterator.getCharacterInstance(); 680 } 681 charIterator.setText(getText()); 682 selectRange(getAnchor(), charIterator.preceding(getCaretPosition())); 683 } 684 } 685 686 /** 687 * Moves the selection forward one char in the text. This may have the 688 * effect of deselecting, depending on the location of the anchor relative 689 * to the caretPosition. This function effectively just moves the caret forward. 690 */ 691 public void selectForward() { 692 final int textLength = getLength(); 693 if (textLength > 0 && getCaretPosition() < textLength) { 694 if (charIterator == null) { 695 charIterator = BreakIterator.getCharacterInstance(); 696 } 697 charIterator.setText(getText()); 698 selectRange(getAnchor(), charIterator.following(getCaretPosition())); 699 } 700 } 701 702 /** 703 * The break iterator instances for navigation over words and complex characters. 704 */ 705 private BreakIterator charIterator; 706 private BreakIterator wordIterator; 707 708 /** 709 * Moves the caret to the beginning of previous word. This function 710 * also has the effect of clearing the selection. 711 */ 712 public void previousWord() { 713 previousWord(false); 714 } 715 716 /** 717 * Moves the caret to the beginning of next word. This function 718 * also has the effect of clearing the selection. 719 */ 720 public void nextWord() { 721 nextWord(false); 722 } 723 724 /** 725 * Moves the caret to the end of the next word. This function 726 * also has the effect of clearing the selection. 727 */ 728 public void endOfNextWord() { 729 endOfNextWord(false); 730 } 731 732 /** 733 * Moves the caret to the beginning of previous word. This does not cause 734 * the selection to be cleared. Rather, the anchor stays put and the caretPosition is 735 * moved to the beginning of previous word. 736 */ 737 public void selectPreviousWord() { 738 previousWord(true); 739 } 740 741 /** 742 * Moves the caret to the beginning of next word. This does not cause 743 * the selection to be cleared. Rather, the anchor stays put and the caretPosition is 744 * moved to the beginning of next word. 745 */ 746 public void selectNextWord() { 747 nextWord(true); 748 } 749 750 /** 751 * Moves the caret to the end of the next word. This does not cause 752 * the selection to be cleared. 753 */ 754 public void selectEndOfNextWord() { 755 endOfNextWord(true); 756 } 757 758 private void previousWord(boolean select) { 759 final int textLength = getLength(); 760 final String text = getText(); 761 if (textLength <= 0) { 762 return; 763 } 764 765 if (wordIterator == null) { 766 wordIterator = BreakIterator.getWordInstance(); 767 } 768 wordIterator.setText(text); 769 770 int pos = wordIterator.preceding(Utils.clamp(0, getCaretPosition(), textLength)); 771 772 // Skip the non-word region, then move/select to the beginning of the word. 773 while (pos != BreakIterator.DONE && 774 !Character.isLetterOrDigit(text.charAt(Utils.clamp(0, pos, textLength-1)))) { 775 pos = wordIterator.preceding(Utils.clamp(0, pos, textLength)); 776 } 777 778 // move/select 779 selectRange(select ? getAnchor() : pos, pos); 780 } 781 782 private void nextWord(boolean select) { 783 final int textLength = getLength(); 784 final String text = getText(); 785 if (textLength <= 0) { 786 return; 787 } 788 789 if (wordIterator == null) { 790 wordIterator = BreakIterator.getWordInstance(); 791 } 792 wordIterator.setText(text); 793 794 int last = wordIterator.following(Utils.clamp(0, getCaretPosition(), textLength-1)); 795 int current = wordIterator.next(); 796 797 // Skip whitespace characters to the beginning of next word, but 798 // stop at newline. Then move the caret or select a range. 799 while (current != BreakIterator.DONE) { 800 for (int p=last; p<=current; p++) { 801 char ch = text.charAt(Utils.clamp(0, p, textLength-1)); 802 // Avoid using Character.isSpaceChar() and Character.isWhitespace(), 803 // because they include LINE_SEPARATOR, PARAGRAPH_SEPARATOR, etc. 804 if (ch != ' ' && ch != '\t') { 805 if (select) { 806 selectRange(getAnchor(), p); 807 } else { 808 selectRange(p, p); 809 } 810 return; 811 } 812 } 813 last = current; 814 current = wordIterator.next(); 815 } 816 817 // move/select to the end 818 if (select) { 819 selectRange(getAnchor(), textLength); 820 } else { 821 end(); 822 } 823 } 824 825 private void endOfNextWord(boolean select) { 826 final int textLength = getLength(); 827 final String text = getText(); 828 if (textLength <= 0) { 829 return; 830 } 831 832 if (wordIterator == null) { 833 wordIterator = BreakIterator.getWordInstance(); 834 } 835 wordIterator.setText(text); 836 837 int last = wordIterator.following(Utils.clamp(0, getCaretPosition(), textLength)); 838 int current = wordIterator.next(); 839 840 // skip the non-word region, then move/select to the end of the word. 841 while (current != BreakIterator.DONE) { 842 for (int p=last; p<=current; p++) { 843 if (!Character.isLetterOrDigit(text.charAt(Utils.clamp(0, p, textLength-1)))) { 844 if (select) { 845 selectRange(getAnchor(), p); 846 } else { 847 selectRange(p, p); 848 } 849 return; 850 } 851 } 852 last = current; 853 current = wordIterator.next(); 854 } 855 856 // move/select to the end 857 if (select) { 858 selectRange(getAnchor(), textLength); 859 } else { 860 end(); 861 } 862 } 863 864 /** 865 * Selects all text in the text input. 866 */ 867 public void selectAll() { 868 selectRange(0, getLength()); 869 } 870 871 /** 872 * Moves the caret to before the first char of the text. This function 873 * also has the effect of clearing the selection. 874 */ 875 public void home() { 876 // user wants to go to start 877 selectRange(0, 0); 878 } 879 880 /** 881 * Moves the caret to after the last char of the text. This function 882 * also has the effect of clearing the selection. 883 */ 884 public void end() { 885 // user wants to go to end 886 final int textLength = getLength(); 887 if (textLength > 0) { 888 selectRange(textLength, textLength); 889 } 890 } 891 892 /** 893 * Moves the caret to before the first char of text. This does not cause 894 * the selection to be cleared. Rather, the anchor stays put and the 895 * caretPosition is moved to before the first char. 896 */ 897 public void selectHome() { 898 selectRange(getAnchor(), 0); 899 } 900 901 /** 902 * Moves the caret to after the last char of text. This does not cause 903 * the selection to be cleared. Rather, the anchor stays put and the 904 * caretPosition is moved to after the last char. 905 */ 906 public void selectEnd() { 907 final int textLength = getLength(); 908 if (textLength > 0) selectRange(getAnchor(), textLength); 909 } 910 911 /** 912 * Deletes the character that precedes the current caret position from the 913 * text if there is no selection, or deletes the selection if there is one. 914 * This function returns true if the deletion succeeded, false otherwise. 915 * @return true if the deletion succeeded, false otherwise 916 */ 917 public boolean deletePreviousChar() { 918 boolean failed = true; 919 if (isEditable() && !isDisabled()) { 920 final String text = getText(); 921 final int dot = getCaretPosition(); 922 final int mark = getAnchor(); 923 if (dot != mark) { 924 // there is a selection of text to remove 925 replaceSelection(""); 926 failed = false; 927 } else if (dot > 0) { 928 // The caret is not at the beginning, so remove some characters. 929 // Typically you'd only be removing a single character, but 930 // in some cases you must remove two depending on the unicode 931 // characters 932 // Note: Do not use charIterator here, because we do want to 933 // break up clusters when deleting backwards. 934 int p = Character.offsetByCodePoints(text, dot, -1); 935 deleteText(p, dot); 936 failed = false; 937 } 938 } 939 return !failed; 940 } 941 942 /** 943 * Deletes the character that follows the current caret position from the 944 * text if there is no selection, or deletes the selection if there is one. 945 * This function returns true if the deletion succeeded, false otherwise. 946 * @return true if the deletion succeeded, false otherwise 947 */ 948 public boolean deleteNextChar() { 949 boolean failed = true; 950 if (isEditable() && !isDisabled()) { 951 final int textLength = getLength(); 952 final String text = getText(); 953 final int dot = getCaretPosition(); 954 final int mark = getAnchor(); 955 if (dot != mark) { 956 // there is a selection of text to remove 957 replaceSelection(""); 958 failed = false; 959 } else if (textLength > 0 && dot < textLength) { 960 // The caret is not at the end, so remove some characters. 961 // Typically you'd only be removing a single character, but 962 // in some cases you must remove two depending on the unicode 963 // characters 964 if (charIterator == null) { 965 charIterator = BreakIterator.getCharacterInstance(); 966 } 967 charIterator.setText(text); 968 int p = charIterator.following(dot); 969 deleteText(dot, p); 970 failed = false; 971 } 972 } 973 return !failed; 974 } 975 976 /** 977 * Moves the caret position forward. If there is no selection, then the 978 * caret position is moved one character forward. If there is a selection, 979 * then the caret position is moved to the end of the selection and 980 * the selection cleared. 981 */ 982 public void forward() { 983 // user has moved caret to the right 984 final int textLength = getLength(); 985 final int dot = getCaretPosition(); 986 final int mark = getAnchor(); 987 if (dot != mark) { 988 int pos = Math.max(dot, mark); 989 selectRange(pos, pos); 990 } else if (dot < textLength && textLength > 0) { 991 if (charIterator == null) { 992 charIterator = BreakIterator.getCharacterInstance(); 993 } 994 charIterator.setText(getText()); 995 int pos = charIterator.following(dot); 996 selectRange(pos, pos); 997 } 998 deselect(); 999 } 1000 1001 /** 1002 * Moves the caret position backward. If there is no selection, then the 1003 * caret position is moved one character backward. If there is a selection, 1004 * then the caret position is moved to the beginning of the selection and 1005 * the selection cleared. 1006 * 1007 * Note: This function is intended to be used by experts, primarily 1008 * by those implementing new Skins or Behaviors. It is not common 1009 * for developers or designers to access this function directly. 1010 */ 1011 public void backward() { 1012 // user has moved caret to the left 1013 final int textLength = getLength(); 1014 final int dot = getCaretPosition(); 1015 final int mark = getAnchor(); 1016 if (dot != mark) { 1017 int pos = Math.min(dot, mark); 1018 selectRange(pos, pos); 1019 } else if (dot > 0 && textLength > 0) { 1020 if (charIterator == null) { 1021 charIterator = BreakIterator.getCharacterInstance(); 1022 } 1023 charIterator.setText(getText()); 1024 int pos = charIterator.preceding(dot); 1025 selectRange(pos, pos); 1026 } 1027 deselect(); 1028 } 1029 1030 /** 1031 * Positions the caret to the position indicated by {@code pos}. This 1032 * function will also clear the selection. 1033 * @param pos the position 1034 */ 1035 public void positionCaret(int pos) { 1036 final int p = Utils.clamp(0, pos, getLength()); 1037 selectRange(p, p); 1038 } 1039 1040 /** 1041 * Positions the caret to the position indicated by {@code pos} and extends 1042 * the selection, if there is one. If there is no selection, then a 1043 * selection is formed where the anchor is at the current caret position 1044 * and the caretPosition is moved to pos. 1045 * @param pos the position 1046 */ 1047 public void selectPositionCaret(int pos) { 1048 selectRange(getAnchor(), Utils.clamp(0, pos, getLength())); 1049 } 1050 1051 /** 1052 * Positions the anchor and caretPosition explicitly. 1053 * @param anchor the anchor 1054 * @param caretPosition the caretPosition 1055 */ 1056 public void selectRange(int anchor, int caretPosition) { 1057 caretPosition = Utils.clamp(0, caretPosition, getLength()); 1058 anchor = Utils.clamp(0, anchor, getLength()); 1059 1060 TextFormatter.Change change = new TextFormatter.Change(this, getFormatterAccessor(), anchor, caretPosition); 1061 TextFormatter<?> formatter = getTextFormatter(); 1062 if (formatter != null && formatter.getFilter() != null) { 1063 change = formatter.getFilter().apply(change); 1064 if (change == null) { 1065 return; 1066 } 1067 } 1068 1069 updateContent(change, false); 1070 } 1071 1072 private void doSelectRange(int anchor, int caretPosition) { 1073 this.caretPosition.set(Utils.clamp(0, caretPosition, getLength())); 1074 this.anchor.set(Utils.clamp(0, anchor, getLength())); 1075 this.selection.set(IndexRange.normalize(getAnchor(), getCaretPosition())); 1076 notifyAccessibleAttributeChanged(AccessibleAttribute.SELECTION_START); 1077 } 1078 1079 /** 1080 * This function will extend the selection to include the specified pos. 1081 * This is different from selectPositionCaret in that it does not simply 1082 * move the caret. Rather, it will reposition the caret and anchor as necessary 1083 * to ensure that pos becomes the new caret and the far other end of the 1084 * selection becomes the anchor. 1085 * @param pos the position 1086 */ 1087 public void extendSelection(int pos) { 1088 final int p = Utils.clamp(0, pos, getLength()); 1089 final int dot = getCaretPosition(); 1090 final int mark = getAnchor(); 1091 int start = Math.min(dot, mark); 1092 int end = Math.max(dot, mark); 1093 if (p < start) { 1094 selectRange(end, p); 1095 } else { 1096 selectRange(start, p); 1097 } 1098 } 1099 1100 /** 1101 * Clears the text. 1102 */ 1103 public void clear() { 1104 deselect(); 1105 if (!text.isBound()) { 1106 setText(""); 1107 } 1108 } 1109 1110 /** 1111 * Clears the selection. 1112 */ 1113 public void deselect() { 1114 // set the anchor equal to the caret position, which clears the selection 1115 // while also preserving the caret position 1116 selectRange(getCaretPosition(), getCaretPosition()); 1117 } 1118 1119 /** 1120 * Replaces the selection with the given replacement String. If there is 1121 * no selection, then the replacement text is simply inserted at the current 1122 * caret position. If there was a selection, then the selection is cleared 1123 * and the given replacement text inserted. 1124 * @param replacement the replacement string 1125 */ 1126 public void replaceSelection(String replacement) { 1127 replaceText(getSelection(), replacement); 1128 } 1129 1130 /** 1131 * If possible, undoes the last modification. If {@link #isUndoable()} returns 1132 * false, then calling this method has no effect. 1133 * @since JavaFX 8u40 1134 */ 1135 public final void undo() { 1136 if (isUndoable()) { 1137 // Apply reverse change here 1138 final int start = undoChange.start; 1139 final String newText = undoChange.newText; 1140 final String oldText = undoChange.oldText; 1141 1142 if (newText != null) { 1143 getContent().delete(start, start + newText.length(), oldText.isEmpty()); 1144 } 1145 1146 if (oldText != null) { 1147 getContent().insert(start, oldText, true); 1148 doSelectRange(start, start + oldText.length()); 1149 } else { 1150 doSelectRange(start, start + newText.length()); 1151 } 1152 1153 undoChange = undoChange.prev; 1154 } 1155 updateUndoRedoState(); 1156 } 1157 1158 /** 1159 * If possible, redoes the last undone modification. If {@link #isRedoable()} returns 1160 * false, then calling this method has no effect. 1161 * @since JavaFX 8u40 1162 */ 1163 public final void redo() { 1164 if (isRedoable()) { 1165 // Apply change here 1166 undoChange = undoChange.next; 1167 final int start = undoChange.start; 1168 final String newText = undoChange.newText; 1169 final String oldText = undoChange.oldText; 1170 1171 if (oldText != null) { 1172 getContent().delete(start, start + oldText.length(), newText.isEmpty()); 1173 } 1174 1175 if (newText != null) { 1176 getContent().insert(start, newText, true); 1177 doSelectRange(start + newText.length(), start + newText.length()); 1178 } else { 1179 doSelectRange(start, start); 1180 } 1181 } 1182 updateUndoRedoState(); 1183 // else beep ? 1184 } 1185 1186 // Used by TextArea, although there are probably other better ways of 1187 // doing this. 1188 void textUpdated() { } 1189 1190 private void resetUndoRedoState() { 1191 undoChange = undoChangeHead; 1192 undoChange.next = null; 1193 updateUndoRedoState(); 1194 } 1195 1196 private void updateUndoRedoState() { 1197 undoable.set(undoChange != undoChangeHead); 1198 redoable.set(undoChange.next != null); 1199 } 1200 1201 private boolean filterAndSet(String value) { 1202 // Send the new value through the textFormatter, if one exists. 1203 TextFormatter<?> formatter = getTextFormatter(); 1204 int length = content.length(); 1205 if (formatter != null && formatter.getFilter() != null && !text.isBound()) { 1206 TextFormatter.Change change = new TextFormatter.Change( 1207 TextInputControl.this, getFormatterAccessor(), 0, length, value, 0, 0); 1208 change = formatter.getFilter().apply(change); 1209 if (change == null) { 1210 return false; 1211 } 1212 replaceText(change.start, change.end, change.text, change.getAnchor(), change.getCaretPosition()); 1213 } else { 1214 replaceText(0, length, value, 0, 0); 1215 } 1216 return true; 1217 } 1218 1219 /** 1220 * This is what is ultimately called by every code path that will update 1221 * the content (except for undo / redo). The input into this method has 1222 * already run through the textFormatter where appropriate. 1223 * 1224 * @param start The start index into the existing text which 1225 * will be replaced by the new value 1226 * @param end The end index into the existing text which will 1227 * be replaced by the new value. As with 1228 * String.replace this is a lastIndex+1 value 1229 * @param value The new text value 1230 * @param anchor The new selection anchor after the change is made 1231 * @param caretPosition The new selection caretPosition after the change 1232 * is made. 1233 * @return The amount of adjustment made to the end / anchor / caretPosition to 1234 * accommodate for subsequent filtering (such as the filtering of 1235 * new lines by the TextField) 1236 */ 1237 private int replaceText(int start, int end, String value, int anchor, int caretPosition) { 1238 // RT-16566: Need to take into account stripping of chars into the 1239 // final anchor & caret position 1240 int length = getLength(); 1241 int adjustmentAmount = 0; 1242 if (end != start) { 1243 getContent().delete(start, end, value.isEmpty()); 1244 length -= (end - start); 1245 } 1246 if (value != null) { 1247 getContent().insert(start, value, true); 1248 adjustmentAmount = value.length() - (getLength() - length); 1249 anchor -= adjustmentAmount; 1250 caretPosition -= adjustmentAmount; 1251 } 1252 doSelectRange(anchor, caretPosition); 1253 return adjustmentAmount; 1254 } 1255 1256 private <T> void updateText(TextFormatter<T> formatter) { 1257 T value = formatter.getValue(); 1258 StringConverter<T> converter = formatter.getValueConverter(); 1259 if (converter != null) { 1260 String text = converter.toString(value); 1261 if (text == null) text = ""; 1262 replaceText(0, getLength(), text, text.length(), text.length()); 1263 } 1264 } 1265 1266 /** 1267 * Commit the current text and convert it to a value. 1268 * @since JavaFX 8u40 1269 */ 1270 public final void commitValue() { 1271 if (getTextFormatter() != null) { 1272 getTextFormatter().updateValue(getText()); 1273 } 1274 } 1275 1276 /** 1277 * If the field is currently being edited, this call will set text to the last commited value. 1278 * @since JavaFX 8u40 1279 */ 1280 public final void cancelEdit() { 1281 if (getTextFormatter() != null) { 1282 updateText(getTextFormatter()); 1283 } 1284 } 1285 1286 private FormatterAccessor accessor; 1287 1288 private FormatterAccessor getFormatterAccessor() { 1289 if (accessor == null) { 1290 accessor = new TextInputControlFromatterAccessor(); 1291 } 1292 return accessor; 1293 } 1294 1295 1296 /** 1297 * A little utility method for stripping out unwanted characters. 1298 * 1299 * @param txt 1300 * @param stripNewlines 1301 * @param stripTabs 1302 * @return The string after having the unwanted characters stripped out. 1303 */ 1304 static String filterInput(String txt, boolean stripNewlines, boolean stripTabs) { 1305 // Most of the time, when text is inserted, there are no illegal 1306 // characters. So we'll do a "cheap" check for illegal characters. 1307 // If we find one, we'll do a longer replace algorithm. In the 1308 // case of illegal characters, this may at worst be an O(2n) solution. 1309 // Strip out any characters that are outside the printed range 1310 if (containsInvalidCharacters(txt, stripNewlines, stripTabs)) { 1311 StringBuilder s = new StringBuilder(txt.length()); 1312 for (int i=0; i<txt.length(); i++) { 1313 final char c = txt.charAt(i); 1314 if (!isInvalidCharacter(c, stripNewlines, stripTabs)) { 1315 s.append(c); 1316 } 1317 } 1318 txt = s.toString(); 1319 } 1320 return txt; 1321 } 1322 1323 static boolean containsInvalidCharacters(String txt, boolean newlineIllegal, boolean tabIllegal) { 1324 for (int i=0; i<txt.length(); i++) { 1325 final char c = txt.charAt(i); 1326 if (isInvalidCharacter(c, newlineIllegal, tabIllegal)) return true; 1327 } 1328 return false; 1329 } 1330 1331 private static boolean isInvalidCharacter(char c, boolean newlineIllegal, boolean tabIllegal) { 1332 if (c == 0x7F) return true; 1333 if (c == 0xA) return newlineIllegal; 1334 if (c == 0x9) return tabIllegal; 1335 if (c < 0x20) return true; 1336 return false; 1337 } 1338 1339 // It can be bound, in which case we will force it to be an eager 1340 // binding so that we update the content eagerly 1341 // It can be bidirectionally bound, which basically will just work 1342 // If somebody changes the content directly, it will be notified and 1343 // send an invalidation event. 1344 private class TextProperty extends StringProperty { 1345 // This is used only when the property is bound 1346 private ObservableValue<? extends String> observable = null; 1347 // Added to the observable when bound 1348 private InvalidationListener listener = null; 1349 // Used for event handling 1350 private ExpressionHelper<String> helper = null; 1351 // The developer my set the Text property to null. Although 1352 // the Content must be given an empty String, we must still 1353 // treat the value as though it were null, so that a subsequent 1354 // getText() will return null. 1355 private boolean textIsNull = false; 1356 1357 @Override public String get() { 1358 // Since we force eager binding and content is always up to date, 1359 // we just need to get it from content and not through the binding 1360 return textIsNull ? null : content.get(); 1361 } 1362 1363 @Override public void set(String value) { 1364 if (isBound()) { 1365 throw new java.lang.RuntimeException("A bound value cannot be set."); 1366 } 1367 doSet(value); 1368 markInvalid(); 1369 } 1370 1371 /** 1372 * Called whenever the content on the control has changed (as determined 1373 * by a listener on the content). 1374 */ 1375 private void controlContentHasChanged() { 1376 markInvalid(); 1377 notifyAccessibleAttributeChanged(AccessibleAttribute.TEXT); 1378 } 1379 1380 @Override public void bind(ObservableValue<? extends String> observable) { 1381 if (observable == null) { 1382 throw new NullPointerException("Cannot bind to null"); 1383 } 1384 if (!observable.equals(this.observable)) { 1385 unbind(); 1386 this.observable = observable; 1387 if (listener == null) { 1388 listener = new Listener(); 1389 } 1390 this.observable.addListener(listener); 1391 markInvalid(); 1392 doSet(observable.getValue()); 1393 } 1394 } 1395 1396 @Override public void unbind() { 1397 if (observable != null) { 1398 doSet(observable.getValue()); 1399 observable.removeListener(listener); 1400 observable = null; 1401 } 1402 } 1403 1404 @Override public boolean isBound() { 1405 return observable != null; 1406 } 1407 1408 @Override public void addListener(InvalidationListener listener) { 1409 helper = ExpressionHelper.addListener(helper, this, listener); 1410 } 1411 1412 @Override public void removeListener(InvalidationListener listener) { 1413 helper = ExpressionHelper.removeListener(helper, listener); 1414 } 1415 1416 @Override public void addListener(ChangeListener<? super String> listener) { 1417 helper = ExpressionHelper.addListener(helper, this, listener); 1418 } 1419 1420 @Override public void removeListener(ChangeListener<? super String> listener) { 1421 helper = ExpressionHelper.removeListener(helper, listener); 1422 } 1423 1424 @Override public Object getBean() { 1425 return TextInputControl.this; 1426 } 1427 1428 @Override public String getName() { 1429 return "text"; 1430 } 1431 1432 private void fireValueChangedEvent() { 1433 ExpressionHelper.fireValueChangedEvent(helper); 1434 } 1435 1436 private void markInvalid() { 1437 fireValueChangedEvent(); 1438 } 1439 1440 /** 1441 * doSet is called whenever the setText() method was called directly 1442 * on the TextInputControl, or when the text property was bound, 1443 * unbound, or reacted to a binding invalidation. It is *not* called 1444 * when modifications to the content happened indirectly, such as 1445 * through the replaceText / replaceSelection methods. 1446 * 1447 * @param value The new value 1448 */ 1449 private void doSet(String value) { 1450 // Guard against the null value. 1451 textIsNull = value == null; 1452 if (value == null) value = ""; 1453 1454 if (!filterAndSet(value)) return; 1455 1456 if (getTextFormatter() != null) { 1457 getTextFormatter().updateValue(getText()); 1458 } 1459 1460 textUpdated(); 1461 1462 // If the programmer has directly manipulated the text property 1463 // or has it bound up, then we will clear out any modifications 1464 // from the undo manager as we must suppose that the control is 1465 // being reused, for example, between forms. 1466 resetUndoRedoState(); 1467 } 1468 1469 private class Listener implements InvalidationListener { 1470 @Override 1471 public void invalidated(Observable valueModel) { 1472 // We now need to force it to be eagerly recomputed 1473 // because we need to push these changes to the 1474 // content model. Because changing the model ends 1475 // up calling invalidate and markInvalid, the 1476 // listeners will all be notified. 1477 doSet(observable.getValue()); 1478 } 1479 } 1480 } 1481 1482 /** 1483 * Used to form a linked-list of Undo / Redo changes. Each UndoRedoChange 1484 * records the old and new text, and the start index. It also has 1485 * the links to the previous and next Changes in the chain. There 1486 * are two special UndoRedoChange objects in this chain representing the 1487 * head and the tail so we can have beforeFirst and afterLast 1488 * behavior as necessary. 1489 */ 1490 static class UndoRedoChange { 1491 static long prevRecordTime; 1492 static final long CHANGE_DURATION = 2500; // milliseconds 1493 static boolean spaceCharSequence = false; 1494 int start; 1495 String oldText; 1496 String newText; 1497 UndoRedoChange prev; 1498 UndoRedoChange next; 1499 1500 UndoRedoChange() { } 1501 1502 public UndoRedoChange add(int start, String oldText, String newText) { 1503 UndoRedoChange c = new UndoRedoChange(); 1504 c.start = start; 1505 c.oldText = oldText; 1506 c.newText = newText; 1507 c.prev = this; 1508 next = c; 1509 prevRecordTime = (new Date()).getTime(); 1510 return c; 1511 } 1512 1513 static boolean ifChangeDurationElapsed() { 1514 return ((new Date()).getTime() - prevRecordTime > CHANGE_DURATION) ; 1515 } 1516 1517 static void setSpaceCharSequence(boolean value) { 1518 spaceCharSequence = value; 1519 } 1520 static boolean isSpaceCharSequence() { 1521 return spaceCharSequence; 1522 } 1523 1524 public UndoRedoChange discard() { 1525 prev.next = next; 1526 return prev; 1527 } 1528 1529 // Handy to use when debugging, just put it in undo or redo 1530 // method or replaceText to see what is happening to the undo 1531 // history as it occurs. 1532 void debugPrint() { 1533 UndoRedoChange c = this; 1534 System.out.print("["); 1535 while (c != null) { 1536 System.out.print(c.toString()); 1537 if (c.next != null) System.out.print(", "); 1538 c = c.next; 1539 } 1540 System.out.println("]"); 1541 } 1542 1543 @Override public String toString() { 1544 if (oldText == null && newText == null) { 1545 return "head"; 1546 } 1547 if (oldText.isEmpty() && !newText.isEmpty()) { 1548 return "added '" + newText + "' at index " + start; 1549 } else if (!oldText.isEmpty() && !newText.isEmpty()) { 1550 return "replaced '" + oldText + "' with '" + newText + "' at index " + start; 1551 } else { 1552 return "deleted '" + oldText + "' at index " + start; 1553 } 1554 } 1555 } 1556 1557 /*************************************************************************** 1558 * * 1559 * Stylesheet Handling * 1560 * * 1561 **************************************************************************/ 1562 1563 1564 private static final PseudoClass PSEUDO_CLASS_READONLY 1565 = PseudoClass.getPseudoClass("readonly"); 1566 1567 private static class StyleableProperties { 1568 private static final FontCssMetaData<TextInputControl> FONT = 1569 new FontCssMetaData<TextInputControl>("-fx-font", Font.getDefault()) { 1570 1571 @Override 1572 public boolean isSettable(TextInputControl n) { 1573 return n.font == null || !n.font.isBound(); 1574 } 1575 1576 @Override 1577 public StyleableProperty<Font> getStyleableProperty(TextInputControl n) { 1578 return (StyleableProperty<Font>)(WritableValue<Font>)n.fontProperty(); 1579 } 1580 }; 1581 1582 private static final List<CssMetaData<? extends Styleable, ?>> STYLEABLES; 1583 static { 1584 final List<CssMetaData<? extends Styleable, ?>> styleables = 1585 new ArrayList<CssMetaData<? extends Styleable, ?>>(Control.getClassCssMetaData()); 1586 styleables.add(FONT); 1587 STYLEABLES = Collections.unmodifiableList(styleables); 1588 } 1589 } 1590 1591 /** 1592 * @return The CssMetaData associated with this class, which may include the 1593 * CssMetaData of its superclasses. 1594 * @since JavaFX 8.0 1595 */ 1596 public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() { 1597 return StyleableProperties.STYLEABLES; 1598 } 1599 1600 /** 1601 * {@inheritDoc} 1602 * @since JavaFX 8.0 1603 */ 1604 @Override 1605 public List<CssMetaData<? extends Styleable, ?>> getControlCssMetaData() { 1606 return getClassCssMetaData(); 1607 } 1608 1609 1610 /*************************************************************************** 1611 * * 1612 * Accessibility handling * 1613 * * 1614 **************************************************************************/ 1615 1616 /** {@inheritDoc} */ 1617 @Override 1618 public Object queryAccessibleAttribute(AccessibleAttribute attribute, Object... parameters) { 1619 switch (attribute) { 1620 case TEXT: { 1621 String accText = getAccessibleText(); 1622 if (accText != null && !accText.isEmpty()) return accText; 1623 1624 String text = getText(); 1625 if (text == null || text.isEmpty()) { 1626 text = getPromptText(); 1627 } 1628 return text; 1629 } 1630 case EDITABLE: return isEditable(); 1631 case SELECTION_START: return getSelection().getStart(); 1632 case SELECTION_END: return getSelection().getEnd(); 1633 case CARET_OFFSET: return getCaretPosition(); 1634 case FONT: return getFont(); 1635 default: return super.queryAccessibleAttribute(attribute, parameters); 1636 } 1637 } 1638 1639 /** {@inheritDoc} */ 1640 @Override 1641 public void executeAccessibleAction(AccessibleAction action, Object... parameters) { 1642 switch (action) { 1643 case SET_TEXT: { 1644 String value = (String) parameters[0]; 1645 if (value != null) setText(value); 1646 break; 1647 } 1648 case SET_TEXT_SELECTION: { 1649 Integer start = (Integer) parameters[0]; 1650 Integer end = (Integer) parameters[1]; 1651 if (start != null && end != null) { 1652 selectRange(start, end); 1653 } 1654 break; 1655 } 1656 default: super.executeAccessibleAction(action, parameters); 1657 } 1658 } 1659 1660 private class TextInputControlFromatterAccessor implements FormatterAccessor { 1661 @Override 1662 public int getTextLength() { 1663 return TextInputControl.this.getLength(); 1664 } 1665 1666 @Override 1667 public String getText(int begin, int end) { 1668 return TextInputControl.this.getText(begin, end); 1669 } 1670 1671 @Override 1672 public int getCaret() { 1673 return TextInputControl.this.getCaretPosition(); 1674 } 1675 1676 @Override 1677 public int getAnchor() { 1678 return TextInputControl.this.getAnchor(); 1679 } 1680 } 1681 1682 }