1 /* 2 * Copyright (c) 2011, 2016, 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.skin; 27 28 import com.sun.javafx.scene.control.Properties; 29 import com.sun.javafx.scene.control.skin.FXVK; 30 import com.sun.javafx.scene.input.ExtendedInputMethodRequests; 31 import javafx.animation.KeyFrame; 32 import javafx.animation.Timeline; 33 import javafx.application.ConditionalFeature; 34 import javafx.application.Platform; 35 import javafx.beans.binding.BooleanBinding; 36 import javafx.beans.binding.ObjectBinding; 37 import javafx.beans.property.BooleanProperty; 38 import javafx.beans.property.ObjectProperty; 39 import javafx.beans.property.SimpleBooleanProperty; 40 import javafx.beans.value.ObservableBooleanValue; 41 import javafx.beans.value.ObservableObjectValue; 42 import javafx.collections.ObservableList; 43 import javafx.css.CssMetaData; 44 import javafx.css.Styleable; 45 import javafx.css.StyleableBooleanProperty; 46 import javafx.css.StyleableObjectProperty; 47 import javafx.css.StyleableProperty; 48 import javafx.geometry.NodeOrientation; 49 import javafx.geometry.Point2D; 50 import javafx.geometry.Rectangle2D; 51 import javafx.scene.AccessibleAction; 52 import javafx.scene.Node; 53 import javafx.scene.Scene; 54 import javafx.scene.control.IndexRange; 55 import javafx.scene.control.SkinBase; 56 import javafx.scene.control.TextInputControl; 57 import javafx.scene.input.InputMethodEvent; 58 import javafx.scene.input.InputMethodHighlight; 59 import javafx.scene.input.InputMethodTextRun; 60 import javafx.scene.layout.StackPane; 61 import javafx.scene.paint.Color; 62 import javafx.scene.paint.Paint; 63 import javafx.scene.shape.ClosePath; 64 import javafx.scene.shape.HLineTo; 65 import javafx.scene.shape.Line; 66 import javafx.scene.shape.LineTo; 67 import javafx.scene.shape.MoveTo; 68 import javafx.scene.shape.Path; 69 import javafx.scene.shape.PathElement; 70 import javafx.scene.shape.Shape; 71 import javafx.scene.shape.VLineTo; 72 import javafx.scene.text.HitInfo; 73 import javafx.stage.Window; 74 import javafx.util.Duration; 75 import java.lang.ref.WeakReference; 76 import java.util.ArrayList; 77 import java.util.Collections; 78 import java.util.List; 79 import com.sun.javafx.PlatformUtil; 80 import javafx.css.converter.BooleanConverter; 81 import javafx.css.converter.PaintConverter; 82 import com.sun.javafx.scene.control.behavior.TextInputControlBehavior; 83 import com.sun.javafx.tk.FontMetrics; 84 import com.sun.javafx.tk.Toolkit; 85 import static com.sun.javafx.PlatformUtil.isWindows; 86 import java.security.AccessController; 87 import java.security.PrivilegedAction; 88 89 /** 90 * Abstract base class for text input skins. 91 * 92 * @since 9 93 * @see TextFieldSkin 94 * @see TextAreaSkin 95 */ 96 public abstract class TextInputControlSkin<T extends TextInputControl> extends SkinBase<T> { 97 98 /************************************************************************** 99 * 100 * Static fields / blocks 101 * 102 **************************************************************************/ 103 104 /** 105 * Unit names for caret movement. 106 * 107 * @see #moveCaret(TextUnit, Direction, boolean) 108 */ 109 public static enum TextUnit { CHARACTER, WORD, LINE, PARAGRAPH, PAGE }; 110 111 /** 112 * Direction names for caret movement. 113 * 114 * @see #moveCaret(TextUnit, Direction, boolean) 115 */ 116 public static enum Direction { LEFT, RIGHT, UP, DOWN, BEGINNING, END }; 117 118 static boolean preload = false; 119 static { 120 AccessController.doPrivileged((PrivilegedAction<Void>) () -> { 121 String s = System.getProperty("com.sun.javafx.virtualKeyboard.preload"); 122 if (s != null) { 123 if (s.equalsIgnoreCase("PRERENDER")) { 124 preload = true; 125 } 126 } 127 return null; 128 }); 129 } 130 131 /** 132 * Specifies whether we ought to show handles. We should do it on touch platforms, but not 133 * iOS (and maybe not Android either?) 134 */ 135 static final boolean SHOW_HANDLES = Properties.IS_TOUCH_SUPPORTED && !PlatformUtil.isIOS(); 136 137 private final static boolean IS_FXVK_SUPPORTED = Platform.isSupported(ConditionalFeature.VIRTUAL_KEYBOARD); 138 139 /************************************************************************** 140 * 141 * Private fields 142 * 143 **************************************************************************/ 144 145 final ObservableObjectValue<FontMetrics> fontMetrics; 146 private ObservableBooleanValue caretVisible; 147 private CaretBlinking caretBlinking = new CaretBlinking(blinkProperty()); 148 149 /** 150 * A path, provided by the textNode, which represents the caret. 151 * I assume this has to be updated whenever the caretPosition 152 * changes. Perhaps more frequently (including text changes), 153 * but I'm not sure. 154 */ 155 final Path caretPath = new Path(); 156 157 StackPane caretHandle = null; 158 StackPane selectionHandle1 = null; 159 StackPane selectionHandle2 = null; 160 161 // Start/Length of the text under input method composition 162 private int imstart; 163 private int imlength; 164 // Holds concrete attributes for the composition runs 165 private List<Shape> imattrs = new java.util.ArrayList<Shape>(); 166 167 168 169 /************************************************************************** 170 * 171 * Constructors 172 * 173 **************************************************************************/ 174 175 /** 176 * Creates a new instance of TextInputControlSkin, although note that this 177 * instance does not handle any behavior / input mappings - this needs to be 178 * handled appropriately by subclasses. 179 * 180 * @param control The control that this skin should be installed onto. 181 */ 182 public TextInputControlSkin(final T control) { 183 super(control); 184 185 fontMetrics = new ObjectBinding<FontMetrics>() { 186 { bind(control.fontProperty()); } 187 @Override protected FontMetrics computeValue() { 188 invalidateMetrics(); 189 return Toolkit.getToolkit().getFontLoader().getFontMetrics(control.getFont()); 190 } 191 }; 192 193 /** 194 * The caret is visible when the text box is focused AND when the selection 195 * is empty. If the selection is non empty or the text box is not focused 196 * then we don't want to show the caret. Also, we show the caret while 197 * performing some operations such as most key strokes. In that case we 198 * simply toggle its opacity. 199 * <p> 200 */ 201 caretVisible = new BooleanBinding() { 202 { bind(control.focusedProperty(), control.anchorProperty(), control.caretPositionProperty(), 203 control.disabledProperty(), control.editableProperty(), displayCaret, blinkProperty());} 204 @Override protected boolean computeValue() { 205 // RT-10682: On Windows, we show the caret during selection, but on others we hide it 206 return !blinkProperty().get() && displayCaret.get() && control.isFocused() && 207 (isWindows() || (control.getCaretPosition() == control.getAnchor())) && 208 !control.isDisabled() && 209 control.isEditable(); 210 } 211 }; 212 213 if (SHOW_HANDLES) { 214 caretHandle = new StackPane(); 215 selectionHandle1 = new StackPane(); 216 selectionHandle2 = new StackPane(); 217 218 caretHandle.setManaged(false); 219 selectionHandle1.setManaged(false); 220 selectionHandle2.setManaged(false); 221 222 caretHandle.visibleProperty().bind(new BooleanBinding() { 223 { bind(control.focusedProperty(), control.anchorProperty(), 224 control.caretPositionProperty(), control.disabledProperty(), 225 control.editableProperty(), control.lengthProperty(), displayCaret);} 226 @Override protected boolean computeValue() { 227 return (displayCaret.get() && control.isFocused() && 228 control.getCaretPosition() == control.getAnchor() && 229 !control.isDisabled() && control.isEditable() && 230 control.getLength() > 0); 231 } 232 }); 233 234 235 selectionHandle1.visibleProperty().bind(new BooleanBinding() { 236 { bind(control.focusedProperty(), control.anchorProperty(), control.caretPositionProperty(), 237 control.disabledProperty(), displayCaret);} 238 @Override protected boolean computeValue() { 239 return (displayCaret.get() && control.isFocused() && 240 control.getCaretPosition() != control.getAnchor() && 241 !control.isDisabled()); 242 } 243 }); 244 245 246 selectionHandle2.visibleProperty().bind(new BooleanBinding() { 247 { bind(control.focusedProperty(), control.anchorProperty(), control.caretPositionProperty(), 248 control.disabledProperty(), displayCaret);} 249 @Override protected boolean computeValue() { 250 return (displayCaret.get() && control.isFocused() && 251 control.getCaretPosition() != control.getAnchor() && 252 !control.isDisabled()); 253 } 254 }); 255 256 257 caretHandle.getStyleClass().setAll("caret-handle"); 258 selectionHandle1.getStyleClass().setAll("selection-handle"); 259 selectionHandle2.getStyleClass().setAll("selection-handle"); 260 261 selectionHandle1.setId("selection-handle-1"); 262 selectionHandle2.setId("selection-handle-2"); 263 } 264 265 if (IS_FXVK_SUPPORTED) { 266 if (preload) { 267 Scene scene = control.getScene(); 268 if (scene != null) { 269 Window window = scene.getWindow(); 270 if (window != null) { 271 FXVK.init(control); 272 } 273 } 274 } 275 control.focusedProperty().addListener(observable -> { 276 if (FXVK.useFXVK()) { 277 Scene scene = getSkinnable().getScene(); 278 if (control.isEditable() && control.isFocused()) { 279 FXVK.attach(control); 280 } else if (scene == null || 281 scene.getWindow() == null || 282 !scene.getWindow().isFocused() || 283 !(scene.getFocusOwner() instanceof TextInputControl && 284 ((TextInputControl)scene.getFocusOwner()).isEditable())) { 285 FXVK.detach(); 286 } 287 } 288 }); 289 } 290 291 if (control.getOnInputMethodTextChanged() == null) { 292 control.setOnInputMethodTextChanged(event -> { 293 handleInputMethodEvent(event); 294 }); 295 } 296 297 control.setInputMethodRequests(new ExtendedInputMethodRequests() { 298 @Override public Point2D getTextLocation(int offset) { 299 Scene scene = getSkinnable().getScene(); 300 Window window = scene.getWindow(); 301 // Don't use imstart here because it isn't initialized yet. 302 Rectangle2D characterBounds = getCharacterBounds(control.getSelection().getStart() + offset); 303 Point2D p = getSkinnable().localToScene(characterBounds.getMinX(), characterBounds.getMaxY()); 304 Point2D location = new Point2D(window.getX() + scene.getX() + p.getX(), 305 window.getY() + scene.getY() + p.getY()); 306 return location; 307 } 308 309 @Override public int getLocationOffset(int x, int y) { 310 return getInsertionPoint(x, y); 311 } 312 313 @Override public void cancelLatestCommittedText() { 314 // TODO 315 } 316 317 @Override public String getSelectedText() { 318 TextInputControl control = getSkinnable(); 319 IndexRange selection = control.getSelection(); 320 321 return control.getText(selection.getStart(), selection.getEnd()); 322 } 323 324 @Override public int getInsertPositionOffset() { 325 int caretPosition = getSkinnable().getCaretPosition(); 326 if (caretPosition < imstart) { 327 return caretPosition; 328 } else if (caretPosition < imstart + imlength) { 329 return imstart; 330 } else { 331 return caretPosition - imlength; 332 } 333 } 334 335 @Override public String getCommittedText(int begin, int end) { 336 TextInputControl control = getSkinnable(); 337 if (begin < imstart) { 338 if (end <= imstart) { 339 return control.getText(begin, end); 340 } else { 341 return control.getText(begin, imstart) + control.getText(imstart + imlength, end + imlength); 342 } 343 } else { 344 return control.getText(begin + imlength, end + imlength); 345 } 346 } 347 348 @Override public int getCommittedTextLength() { 349 return getSkinnable().getText().length() - imlength; 350 } 351 }); 352 } 353 354 355 356 /************************************************************************** 357 * 358 * Properties 359 * 360 **************************************************************************/ 361 362 // --- blink 363 private BooleanProperty blink; 364 private final void setBlink(boolean value) { 365 blinkProperty().set(value); 366 } 367 private final boolean isBlink() { 368 return blinkProperty().get(); 369 } 370 private final BooleanProperty blinkProperty() { 371 if (blink == null) { 372 blink = new SimpleBooleanProperty(this, "blink", true); 373 } 374 return blink; 375 } 376 377 // --- text fill 378 /** 379 * The fill to use for the text under normal conditions 380 */ 381 private final ObjectProperty<Paint> textFill = new StyleableObjectProperty<Paint>(Color.BLACK) { 382 @Override protected void invalidated() { 383 updateTextFill(); 384 } 385 386 @Override public Object getBean() { 387 return TextInputControlSkin.this; 388 } 389 390 @Override public String getName() { 391 return "textFill"; 392 } 393 394 @Override public CssMetaData<TextInputControl,Paint> getCssMetaData() { 395 return StyleableProperties.TEXT_FILL; 396 } 397 }; 398 399 /** 400 * The fill {@code Paint} used for the foreground text color. 401 * @param value the text fill 402 */ 403 protected final void setTextFill(Paint value) { 404 textFill.set(value); 405 } 406 protected final Paint getTextFill() { 407 return textFill.get(); 408 } 409 protected final ObjectProperty<Paint> textFillProperty() { 410 return textFill; 411 } 412 413 // --- prompt text fill 414 private final ObjectProperty<Paint> promptTextFill = new StyleableObjectProperty<Paint>(Color.GRAY) { 415 @Override public Object getBean() { 416 return TextInputControlSkin.this; 417 } 418 419 @Override public String getName() { 420 return "promptTextFill"; 421 } 422 423 @Override public CssMetaData<TextInputControl,Paint> getCssMetaData() { 424 return StyleableProperties.PROMPT_TEXT_FILL; 425 } 426 }; 427 428 /** 429 * The fill {@code Paint} used for the foreground prompt text color. 430 * @param value the prompt text fill 431 */ 432 protected final void setPromptTextFill(Paint value) { 433 promptTextFill.set(value); 434 } 435 protected final Paint getPromptTextFill() { 436 return promptTextFill.get(); 437 } 438 protected final ObjectProperty<Paint> promptTextFillProperty() { 439 return promptTextFill; 440 } 441 442 // --- hightlight fill 443 /** 444 * The fill to use for the text when highlighted. 445 */ 446 private final ObjectProperty<Paint> highlightFill = new StyleableObjectProperty<Paint>(Color.DODGERBLUE) { 447 @Override protected void invalidated() { 448 updateHighlightFill(); 449 } 450 451 @Override public Object getBean() { 452 return TextInputControlSkin.this; 453 } 454 455 @Override public String getName() { 456 return "highlightFill"; 457 } 458 459 @Override public CssMetaData<TextInputControl,Paint> getCssMetaData() { 460 return StyleableProperties.HIGHLIGHT_FILL; 461 } 462 }; 463 464 /** 465 * The fill {@code Paint} used for the background of selected text. 466 * @param value the highlight fill 467 */ 468 protected final void setHighlightFill(Paint value) { 469 highlightFill.set(value); 470 } 471 protected final Paint getHighlightFill() { 472 return highlightFill.get(); 473 } 474 protected final ObjectProperty<Paint> highlightFillProperty() { 475 return highlightFill; 476 } 477 478 // --- highlight text fill 479 private final ObjectProperty<Paint> highlightTextFill = new StyleableObjectProperty<Paint>(Color.WHITE) { 480 @Override protected void invalidated() { 481 updateHighlightTextFill(); 482 } 483 484 @Override public Object getBean() { 485 return TextInputControlSkin.this; 486 } 487 488 @Override public String getName() { 489 return "highlightTextFill"; 490 } 491 492 @Override public CssMetaData<TextInputControl,Paint> getCssMetaData() { 493 return StyleableProperties.HIGHLIGHT_TEXT_FILL; 494 } 495 }; 496 497 /** 498 * The fill {@code Paint} used for the foreground of selected text. 499 * @param value the highlight text fill 500 */ 501 protected final void setHighlightTextFill(Paint value) { 502 highlightTextFill.set(value); 503 } 504 protected final Paint getHighlightTextFill() { 505 return highlightTextFill.get(); 506 } 507 protected final ObjectProperty<Paint> highlightTextFillProperty() { 508 return highlightTextFill; 509 } 510 511 // --- display caret 512 private final BooleanProperty displayCaret = new StyleableBooleanProperty(true) { 513 @Override public Object getBean() { 514 return TextInputControlSkin.this; 515 } 516 517 @Override public String getName() { 518 return "displayCaret"; 519 } 520 521 @Override public CssMetaData<TextInputControl,Boolean> getCssMetaData() { 522 return StyleableProperties.DISPLAY_CARET; 523 } 524 }; 525 526 private final void setDisplayCaret(boolean value) { 527 displayCaret.set(value); 528 } 529 private final boolean isDisplayCaret() { 530 return displayCaret.get(); 531 } 532 private final BooleanProperty displayCaretProperty() { 533 return displayCaret; 534 } 535 536 537 /** 538 * Caret bias in the content. true means a bias towards forward character 539 * (true=leading/false=trailing) 540 */ 541 private BooleanProperty forwardBias = new SimpleBooleanProperty(this, "forwardBias", true); 542 protected final BooleanProperty forwardBiasProperty() { 543 return forwardBias; 544 } 545 // Public for behavior 546 public final void setForwardBias(boolean isLeading) { 547 forwardBias.set(isLeading); 548 } 549 protected final boolean isForwardBias() { 550 return forwardBias.get(); 551 } 552 553 554 555 /************************************************************************** 556 * 557 * Abstract API 558 * 559 **************************************************************************/ 560 561 /** 562 * @param start the start 563 * @param end the end 564 * @return the path elements describing the shape of the underline for the given range. 565 */ 566 protected abstract PathElement[] getUnderlineShape(int start, int end); 567 /** 568 * @param start the start 569 * @param end the end 570 * @return the path elements describing the bounding rectangles for the given range of text. 571 */ 572 protected abstract PathElement[] getRangeShape(int start, int end); 573 /** 574 * Adds highlight for composed text from Input Method. 575 * @param nodes the list of nodes 576 * @param start the start 577 */ 578 protected abstract void addHighlight(List<? extends Node> nodes, int start); 579 /** 580 * Removes highlight for composed text from Input Method. 581 * @param nodes the list of nodes 582 */ 583 protected abstract void removeHighlight(List<? extends Node> nodes); 584 585 // Public for behavior 586 /** 587 * Moves the caret by one of the given text unit, in the given 588 * direction. Note that only certain combinations are valid, 589 * depending on the implementing subclass. 590 * 591 * @param unit the unit of text to move by. 592 * @param dir the direction of movement. 593 * @param select whether to extends the selection to the new posititon. 594 */ 595 public abstract void moveCaret(TextUnit unit, Direction dir, boolean select); 596 597 /************************************************************************** 598 * 599 * Public API 600 * 601 **************************************************************************/ 602 603 604 // Public for behavior 605 /** 606 * Returns the position to be used for a context menu, based on the location 607 * of the caret handle or selection handles. This is supported only on touch 608 * displays and does not use the location of the mouse. 609 * @return the position to be used for this context menu 610 */ 611 public Point2D getMenuPosition() { 612 if (SHOW_HANDLES) { 613 if (caretHandle.isVisible()) { 614 return new Point2D(caretHandle.getLayoutX() + caretHandle.getWidth() / 2, 615 caretHandle.getLayoutY()); 616 } else if (selectionHandle1.isVisible() && selectionHandle2.isVisible()) { 617 return new Point2D((selectionHandle1.getLayoutX() + selectionHandle1.getWidth() / 2 + 618 selectionHandle2.getLayoutX() + selectionHandle2.getWidth() / 2) / 2, 619 selectionHandle2.getLayoutY() + selectionHandle2.getHeight() / 2); 620 } else { 621 return null; 622 } 623 } else { 624 throw new UnsupportedOperationException(); 625 } 626 } 627 628 // For use with PasswordField in TextFieldSkin 629 /** 630 * This method may be overridden by subclasses to replace the displayed 631 * characters without affecting the actual text content. This is used to 632 * display bullet characters in PasswordField. 633 * 634 * @param txt the content that may need to be masked. 635 * @return the replacement string. This may just be the input string, or may be a string of replacement characters with the same length as the input string. 636 */ 637 protected String maskText(String txt) { 638 return txt; 639 } 640 641 /** 642 * Returns the insertion point for a given location. 643 * 644 * @param x the x location 645 * @param y the y location 646 * @return the insertion point for a given location 647 */ 648 protected int getInsertionPoint(double x, double y) { return 0; } 649 650 /** 651 * Returns the bounds of the character at a given index. 652 * 653 * @param index the index 654 * @return the bounds of the character at a given index 655 */ 656 public Rectangle2D getCharacterBounds(int index) { return null; } 657 658 /** 659 * Ensures that the character at a given index is visible. 660 * 661 * @param index the index 662 */ 663 protected void scrollCharacterToVisible(int index) {} 664 665 /** 666 * Invalidates cached min and pref sizes for the TextInputControl. 667 */ 668 protected void invalidateMetrics() { 669 } 670 671 /** 672 * Called when textFill property changes. 673 */ 674 protected void updateTextFill() {}; 675 676 /** 677 * Called when highlightFill property changes. 678 */ 679 protected void updateHighlightFill() {}; 680 681 /** 682 * Called when highlightTextFill property changes. 683 */ 684 protected void updateHighlightTextFill() {}; 685 686 protected void handleInputMethodEvent(InputMethodEvent event) { 687 final TextInputControl textInput = getSkinnable(); 688 if (textInput.isEditable() && !textInput.textProperty().isBound() && !textInput.isDisabled()) { 689 690 // just replace the text on iOS 691 if (PlatformUtil.isIOS()) { 692 textInput.setText(event.getCommitted()); 693 return; 694 } 695 696 // remove previous input method text (if any) or selected text 697 if (imlength != 0) { 698 removeHighlight(imattrs); 699 imattrs.clear(); 700 textInput.selectRange(imstart, imstart + imlength); 701 } 702 703 // Insert committed text 704 if (event.getCommitted().length() != 0) { 705 String committed = event.getCommitted(); 706 textInput.replaceText(textInput.getSelection(), committed); 707 } 708 709 // Replace composed text 710 imstart = textInput.getSelection().getStart(); 711 StringBuilder composed = new StringBuilder(); 712 for (InputMethodTextRun run : event.getComposed()) { 713 composed.append(run.getText()); 714 } 715 textInput.replaceText(textInput.getSelection(), composed.toString()); 716 imlength = composed.length(); 717 if (imlength != 0) { 718 int pos = imstart; 719 for (InputMethodTextRun run : event.getComposed()) { 720 int endPos = pos + run.getText().length(); 721 createInputMethodAttributes(run.getHighlight(), pos, endPos); 722 pos = endPos; 723 } 724 addHighlight(imattrs, imstart); 725 726 // Set caret position in composed text 727 int caretPos = event.getCaretPosition(); 728 if (caretPos >= 0 && caretPos < imlength) { 729 textInput.selectRange(imstart + caretPos, imstart + caretPos); 730 } 731 } 732 } 733 } 734 735 // Public for behavior 736 /** 737 * Starts or stops caret blinking. The behavior classes use this to temporarily 738 * pause blinking while user is typing or otherwise moving the caret. 739 * 740 * @param value whether caret should be blinking. 741 */ 742 public void setCaretAnimating(boolean value) { 743 if (value) { 744 caretBlinking.start(); 745 } else { 746 caretBlinking.stop(); 747 blinkProperty().set(true); 748 } 749 } 750 751 752 753 /************************************************************************** 754 * 755 * Private implementation 756 * 757 **************************************************************************/ 758 759 TextInputControlBehavior getBehavior() { 760 return null; 761 } 762 763 ObservableBooleanValue caretVisibleProperty() { 764 return caretVisible; 765 } 766 767 boolean isRTL() { 768 return (getSkinnable().getEffectiveNodeOrientation() == NodeOrientation.RIGHT_TO_LEFT); 769 }; 770 771 private void createInputMethodAttributes(InputMethodHighlight highlight, int start, int end) { 772 double minX = 0f; 773 double maxX = 0f; 774 double minY = 0f; 775 double maxY = 0f; 776 777 PathElement elements[] = getUnderlineShape(start, end); 778 for (int i = 0; i < elements.length; i++) { 779 PathElement pe = elements[i]; 780 if (pe instanceof MoveTo) { 781 minX = maxX = ((MoveTo)pe).getX(); 782 minY = maxY = ((MoveTo)pe).getY(); 783 } else if (pe instanceof LineTo) { 784 minX = (minX < ((LineTo)pe).getX() ? minX : ((LineTo)pe).getX()); 785 maxX = (maxX > ((LineTo)pe).getX() ? maxX : ((LineTo)pe).getX()); 786 minY = (minY < ((LineTo)pe).getY() ? minY : ((LineTo)pe).getY()); 787 maxY = (maxY > ((LineTo)pe).getY() ? maxY : ((LineTo)pe).getY()); 788 } else if (pe instanceof HLineTo) { 789 minX = (minX < ((HLineTo)pe).getX() ? minX : ((HLineTo)pe).getX()); 790 maxX = (maxX > ((HLineTo)pe).getX() ? maxX : ((HLineTo)pe).getX()); 791 } else if (pe instanceof VLineTo) { 792 minY = (minY < ((VLineTo)pe).getY() ? minY : ((VLineTo)pe).getY()); 793 maxY = (maxY > ((VLineTo)pe).getY() ? maxY : ((VLineTo)pe).getY()); 794 } 795 // Don't assume that shapes are ended with ClosePath. 796 if (pe instanceof ClosePath || 797 i == elements.length - 1 || 798 (i < elements.length - 1 && elements[i+1] instanceof MoveTo)) { 799 // Now, create the attribute. 800 Shape attr = null; 801 if (highlight == InputMethodHighlight.SELECTED_RAW) { 802 // blue background 803 attr = new Path(); 804 ((Path)attr).getElements().addAll(getRangeShape(start, end)); 805 attr.setFill(Color.BLUE); 806 attr.setOpacity(0.3f); 807 } else if (highlight == InputMethodHighlight.UNSELECTED_RAW) { 808 // dash underline. 809 attr = new Line(minX + 2, maxY + 1, maxX - 2, maxY + 1); 810 attr.setStroke(textFill.get()); 811 attr.setStrokeWidth(maxY - minY); 812 ObservableList<Double> dashArray = attr.getStrokeDashArray(); 813 dashArray.add(Double.valueOf(2f)); 814 dashArray.add(Double.valueOf(2f)); 815 } else if (highlight == InputMethodHighlight.SELECTED_CONVERTED) { 816 // thick underline. 817 attr = new Line(minX + 2, maxY + 1, maxX - 2, maxY + 1); 818 attr.setStroke(textFill.get()); 819 attr.setStrokeWidth((maxY - minY) * 3); 820 } else if (highlight == InputMethodHighlight.UNSELECTED_CONVERTED) { 821 // single underline. 822 attr = new Line(minX + 2, maxY + 1, maxX - 2, maxY + 1); 823 attr.setStroke(textFill.get()); 824 attr.setStrokeWidth(maxY - minY); 825 } 826 827 if (attr != null) { 828 attr.setManaged(false); 829 imattrs.add(attr); 830 } 831 } 832 } 833 } 834 835 836 837 /************************************************************************** 838 * 839 * Support classes 840 * 841 **************************************************************************/ 842 843 private static final class CaretBlinking { 844 private final Timeline caretTimeline; 845 private final WeakReference<BooleanProperty> blinkPropertyRef; 846 847 public CaretBlinking(final BooleanProperty blinkProperty) { 848 blinkPropertyRef = new WeakReference<>(blinkProperty); 849 850 caretTimeline = new Timeline(); 851 caretTimeline.setCycleCount(Timeline.INDEFINITE); 852 caretTimeline.getKeyFrames().addAll( 853 new KeyFrame(Duration.ZERO, e -> setBlink(false)), 854 new KeyFrame(Duration.seconds(.5), e -> setBlink(true)), 855 new KeyFrame(Duration.seconds(1))); 856 } 857 858 public void start() { 859 caretTimeline.play(); 860 } 861 862 public void stop() { 863 caretTimeline.stop(); 864 } 865 866 private void setBlink(final boolean value) { 867 final BooleanProperty blinkProperty = blinkPropertyRef.get(); 868 if (blinkProperty == null) { 869 caretTimeline.stop(); 870 return; 871 } 872 873 blinkProperty.set(value); 874 } 875 } 876 877 878 private static class StyleableProperties { 879 private static final CssMetaData<TextInputControl,Paint> TEXT_FILL = 880 new CssMetaData<TextInputControl,Paint>("-fx-text-fill", 881 PaintConverter.getInstance(), Color.BLACK) { 882 883 @Override public boolean isSettable(TextInputControl n) { 884 final TextInputControlSkin<?> skin = (TextInputControlSkin<?>) n.getSkin(); 885 return skin.textFill == null || !skin.textFill.isBound(); 886 } 887 888 @Override @SuppressWarnings("unchecked") 889 public StyleableProperty<Paint> getStyleableProperty(TextInputControl n) { 890 final TextInputControlSkin<?> skin = (TextInputControlSkin<?>) n.getSkin(); 891 return (StyleableProperty<Paint>)skin.textFill; 892 } 893 }; 894 895 private static final CssMetaData<TextInputControl,Paint> PROMPT_TEXT_FILL = 896 new CssMetaData<TextInputControl,Paint>("-fx-prompt-text-fill", 897 PaintConverter.getInstance(), Color.GRAY) { 898 899 @Override public boolean isSettable(TextInputControl n) { 900 final TextInputControlSkin<?> skin = (TextInputControlSkin<?>) n.getSkin(); 901 return skin.promptTextFill == null || !skin.promptTextFill.isBound(); 902 } 903 904 @Override @SuppressWarnings("unchecked") 905 public StyleableProperty<Paint> getStyleableProperty(TextInputControl n) { 906 final TextInputControlSkin<?> skin = (TextInputControlSkin<?>) n.getSkin(); 907 return (StyleableProperty<Paint>)skin.promptTextFill; 908 } 909 }; 910 911 private static final CssMetaData<TextInputControl,Paint> HIGHLIGHT_FILL = 912 new CssMetaData<TextInputControl,Paint>("-fx-highlight-fill", 913 PaintConverter.getInstance(), Color.DODGERBLUE) { 914 915 @Override public boolean isSettable(TextInputControl n) { 916 final TextInputControlSkin<?> skin = (TextInputControlSkin<?>) n.getSkin(); 917 return skin.highlightFill == null || !skin.highlightFill.isBound(); 918 } 919 920 @Override @SuppressWarnings("unchecked") 921 public StyleableProperty<Paint> getStyleableProperty(TextInputControl n) { 922 final TextInputControlSkin<?> skin = (TextInputControlSkin<?>) n.getSkin(); 923 return (StyleableProperty<Paint>)skin.highlightFill; 924 } 925 }; 926 927 private static final CssMetaData<TextInputControl,Paint> HIGHLIGHT_TEXT_FILL = 928 new CssMetaData<TextInputControl,Paint>("-fx-highlight-text-fill", 929 PaintConverter.getInstance(), Color.WHITE) { 930 931 @Override public boolean isSettable(TextInputControl n) { 932 final TextInputControlSkin<?> skin = (TextInputControlSkin<?>) n.getSkin(); 933 return skin.highlightTextFill == null || !skin.highlightTextFill.isBound(); 934 } 935 936 @Override @SuppressWarnings("unchecked") 937 public StyleableProperty<Paint> getStyleableProperty(TextInputControl n) { 938 final TextInputControlSkin<?> skin = (TextInputControlSkin<?>) n.getSkin(); 939 return (StyleableProperty<Paint>)skin.highlightTextFill; 940 } 941 }; 942 943 private static final CssMetaData<TextInputControl,Boolean> DISPLAY_CARET = 944 new CssMetaData<TextInputControl,Boolean>("-fx-display-caret", 945 BooleanConverter.getInstance(), Boolean.TRUE) { 946 947 @Override public boolean isSettable(TextInputControl n) { 948 final TextInputControlSkin<?> skin = (TextInputControlSkin<?>) n.getSkin(); 949 return skin.displayCaret == null || !skin.displayCaret.isBound(); 950 } 951 952 @Override @SuppressWarnings("unchecked") 953 public StyleableProperty<Boolean> getStyleableProperty(TextInputControl n) { 954 final TextInputControlSkin<?> skin = (TextInputControlSkin<?>) n.getSkin(); 955 return (StyleableProperty<Boolean>)skin.displayCaret; 956 } 957 }; 958 959 private static final List<CssMetaData<? extends Styleable, ?>> STYLEABLES; 960 static { 961 List<CssMetaData<? extends Styleable, ?>> styleables = 962 new ArrayList<CssMetaData<? extends Styleable, ?>>(SkinBase.getClassCssMetaData()); 963 styleables.add(TEXT_FILL); 964 styleables.add(PROMPT_TEXT_FILL); 965 styleables.add(HIGHLIGHT_FILL); 966 styleables.add(HIGHLIGHT_TEXT_FILL); 967 styleables.add(DISPLAY_CARET); 968 969 STYLEABLES = Collections.unmodifiableList(styleables); 970 } 971 } 972 973 /** 974 * Returns the CssMetaData associated with this class, which may include the 975 * CssMetaData of its superclasses. 976 * @return the CssMetaData associated with this class, which may include the 977 * CssMetaData of its superclasses 978 */ 979 public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() { 980 return StyleableProperties.STYLEABLES; 981 } 982 983 /** 984 * {@inheritDoc} 985 */ 986 @Override public List<CssMetaData<? extends Styleable, ?>> getCssMetaData() { 987 return getClassCssMetaData(); 988 } 989 990 @Override protected void executeAccessibleAction(AccessibleAction action, Object... parameters) { 991 switch (action) { 992 case SHOW_TEXT_RANGE: { 993 Integer start = (Integer)parameters[0]; 994 Integer end = (Integer)parameters[1]; 995 if (start != null && end != null) { 996 scrollCharacterToVisible(end); 997 scrollCharacterToVisible(start); 998 scrollCharacterToVisible(end); 999 } 1000 break; 1001 } 1002 default: super.executeAccessibleAction(action, parameters); 1003 } 1004 } 1005 }