1 /* 2 * Copyright (c) 2011, 2015, 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.stage.Window; 73 import javafx.util.Duration; 74 import java.lang.ref.WeakReference; 75 import java.util.ArrayList; 76 import java.util.Collections; 77 import java.util.List; 78 import com.sun.javafx.PlatformUtil; 79 import javafx.css.converter.BooleanConverter; 80 import javafx.css.converter.PaintConverter; 81 import com.sun.javafx.scene.control.behavior.TextInputControlBehavior; 82 import com.sun.javafx.scene.text.HitInfo; 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 */ 402 protected final void setTextFill(Paint value) { 403 textFill.set(value); 404 } 405 protected final Paint getTextFill() { 406 return textFill.get(); 407 } 408 protected final ObjectProperty<Paint> textFillProperty() { 409 return textFill; 410 } 411 412 // --- prompt text fill 413 private final ObjectProperty<Paint> promptTextFill = new StyleableObjectProperty<Paint>(Color.GRAY) { 414 @Override public Object getBean() { 415 return TextInputControlSkin.this; 416 } 417 418 @Override public String getName() { 419 return "promptTextFill"; 420 } 421 422 @Override public CssMetaData<TextInputControl,Paint> getCssMetaData() { 423 return StyleableProperties.PROMPT_TEXT_FILL; 424 } 425 }; 426 427 /** 428 * The fill {@code Paint} used for the foreground prompt text color. 429 */ 430 protected final void setPromptTextFill(Paint value) { 431 promptTextFill.set(value); 432 } 433 protected final Paint getPromptTextFill() { 434 return promptTextFill.get(); 435 } 436 protected final ObjectProperty<Paint> promptTextFillProperty() { 437 return promptTextFill; 438 } 439 440 // --- hightlight fill 441 /** 442 * The fill to use for the text when highlighted. 443 */ 444 private final ObjectProperty<Paint> highlightFill = new StyleableObjectProperty<Paint>(Color.DODGERBLUE) { 445 @Override protected void invalidated() { 446 updateHighlightFill(); 447 } 448 449 @Override public Object getBean() { 450 return TextInputControlSkin.this; 451 } 452 453 @Override public String getName() { 454 return "highlightFill"; 455 } 456 457 @Override public CssMetaData<TextInputControl,Paint> getCssMetaData() { 458 return StyleableProperties.HIGHLIGHT_FILL; 459 } 460 }; 461 462 /** 463 * The fill {@code Paint} used for the background of selected text. 464 */ 465 protected final void setHighlightFill(Paint value) { 466 highlightFill.set(value); 467 } 468 protected final Paint getHighlightFill() { 469 return highlightFill.get(); 470 } 471 protected final ObjectProperty<Paint> highlightFillProperty() { 472 return highlightFill; 473 } 474 475 // --- highlight text fill 476 private final ObjectProperty<Paint> highlightTextFill = new StyleableObjectProperty<Paint>(Color.WHITE) { 477 @Override protected void invalidated() { 478 updateHighlightTextFill(); 479 } 480 481 @Override public Object getBean() { 482 return TextInputControlSkin.this; 483 } 484 485 @Override public String getName() { 486 return "highlightTextFill"; 487 } 488 489 @Override public CssMetaData<TextInputControl,Paint> getCssMetaData() { 490 return StyleableProperties.HIGHLIGHT_TEXT_FILL; 491 } 492 }; 493 494 /** 495 * The fill {@code Paint} used for the foreground of selected text. 496 */ 497 protected final void setHighlightTextFill(Paint value) { 498 highlightTextFill.set(value); 499 } 500 protected final Paint getHighlightTextFill() { 501 return highlightTextFill.get(); 502 } 503 protected final ObjectProperty<Paint> highlightTextFillProperty() { 504 return highlightTextFill; 505 } 506 507 // --- display caret 508 private final BooleanProperty displayCaret = new StyleableBooleanProperty(true) { 509 @Override public Object getBean() { 510 return TextInputControlSkin.this; 511 } 512 513 @Override public String getName() { 514 return "displayCaret"; 515 } 516 517 @Override public CssMetaData<TextInputControl,Boolean> getCssMetaData() { 518 return StyleableProperties.DISPLAY_CARET; 519 } 520 }; 521 522 private final void setDisplayCaret(boolean value) { 523 displayCaret.set(value); 524 } 525 private final boolean isDisplayCaret() { 526 return displayCaret.get(); 527 } 528 private final BooleanProperty displayCaretProperty() { 529 return displayCaret; 530 } 531 532 533 /** 534 * Caret bias in the content. true means a bias towards forward character 535 * (true=leading/false=trailing) 536 */ 537 private BooleanProperty forwardBias = new SimpleBooleanProperty(this, "forwardBias", true); 538 protected final BooleanProperty forwardBiasProperty() { 539 return forwardBias; 540 } 541 // Public for behavior 542 public final void setForwardBias(boolean isLeading) { 543 forwardBias.set(isLeading); 544 } 545 protected final boolean isForwardBias() { 546 return forwardBias.get(); 547 } 548 549 550 551 /************************************************************************** 552 * 553 * Abstract API 554 * 555 **************************************************************************/ 556 557 /** 558 * @return the path elements describing the shape of the underline for the given range. 559 */ 560 protected abstract PathElement[] getUnderlineShape(int start, int end); 561 /** 562 * @return the path elements describing the bounding rectangles for the given range of text. 563 */ 564 protected abstract PathElement[] getRangeShape(int start, int end); 565 /** 566 * Adds highlight for composed text from Input Method. 567 */ 568 protected abstract void addHighlight(List<? extends Node> nodes, int start); 569 /** 570 * Removes highlight for composed text from Input Method. 571 */ 572 protected abstract void removeHighlight(List<? extends Node> nodes); 573 574 // Public for behavior 575 /** 576 * Moves the caret by one of the given text unit, in the given 577 * direction. Note that only certain combinations are valid, 578 * depending on the implementing subclass. 579 * 580 * @param unit the unit of text to move by. 581 * @param dir the direction of movement. 582 * @param select whether to extends the selection to the new posititon. 583 */ 584 public abstract void moveCaret(TextUnit unit, Direction dir, boolean select); 585 586 /************************************************************************** 587 * 588 * Public API 589 * 590 **************************************************************************/ 591 592 593 // Public for behavior 594 /** 595 * Returns the position to be used for a context menu, based on the location 596 * of the caret handle or selection handles. This is supported only on touch 597 * displays and does not use the location of the mouse. 598 */ 599 public Point2D getMenuPosition() { 600 if (SHOW_HANDLES) { 601 if (caretHandle.isVisible()) { 602 return new Point2D(caretHandle.getLayoutX() + caretHandle.getWidth() / 2, 603 caretHandle.getLayoutY()); 604 } else if (selectionHandle1.isVisible() && selectionHandle2.isVisible()) { 605 return new Point2D((selectionHandle1.getLayoutX() + selectionHandle1.getWidth() / 2 + 606 selectionHandle2.getLayoutX() + selectionHandle2.getWidth() / 2) / 2, 607 selectionHandle2.getLayoutY() + selectionHandle2.getHeight() / 2); 608 } else { 609 return null; 610 } 611 } else { 612 throw new UnsupportedOperationException(); 613 } 614 } 615 616 // For use with PasswordField in TextFieldSkin 617 /** 618 * This method may be overridden by subclasses to replace the displayed 619 * characters without affecting the actual text content. This is used to 620 * display bullet characters in PasswordField. 621 * 622 * @param txt the content that may need to be masked. 623 * @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. 624 */ 625 protected String maskText(String txt) { 626 return txt; 627 } 628 629 /** 630 * Returns the insertion point for a given location. 631 * 632 * @param x 633 * @param y 634 */ 635 protected int getInsertionPoint(double x, double y) { return 0; } 636 637 /** 638 * Returns the bounds of the character at a given index. 639 * 640 * @param index 641 */ 642 public Rectangle2D getCharacterBounds(int index) { return null; } 643 644 /** 645 * Ensures that the character at a given index is visible. 646 * 647 * @param index 648 */ 649 protected void scrollCharacterToVisible(int index) {} 650 651 /** 652 * Invalidates cached min and pref sizes for the TextInputControl. 653 */ 654 protected void invalidateMetrics() { 655 } 656 657 /** 658 * Called when textFill property changes. 659 */ 660 protected void updateTextFill() {}; 661 662 /** 663 * Called when highlightFill property changes. 664 */ 665 protected void updateHighlightFill() {}; 666 667 /** 668 * Called when highlightTextFill property changes. 669 */ 670 protected void updateHighlightTextFill() {}; 671 672 protected void handleInputMethodEvent(InputMethodEvent event) { 673 final TextInputControl textInput = getSkinnable(); 674 if (textInput.isEditable() && !textInput.textProperty().isBound() && !textInput.isDisabled()) { 675 676 // just replace the text on iOS 677 if (PlatformUtil.isIOS()) { 678 textInput.setText(event.getCommitted()); 679 return; 680 } 681 682 // remove previous input method text (if any) or selected text 683 if (imlength != 0) { 684 removeHighlight(imattrs); 685 imattrs.clear(); 686 textInput.selectRange(imstart, imstart + imlength); 687 } 688 689 // Insert committed text 690 if (event.getCommitted().length() != 0) { 691 String committed = event.getCommitted(); 692 textInput.replaceText(textInput.getSelection(), committed); 693 } 694 695 // Replace composed text 696 imstart = textInput.getSelection().getStart(); 697 StringBuilder composed = new StringBuilder(); 698 for (InputMethodTextRun run : event.getComposed()) { 699 composed.append(run.getText()); 700 } 701 textInput.replaceText(textInput.getSelection(), composed.toString()); 702 imlength = composed.length(); 703 if (imlength != 0) { 704 int pos = imstart; 705 for (InputMethodTextRun run : event.getComposed()) { 706 int endPos = pos + run.getText().length(); 707 createInputMethodAttributes(run.getHighlight(), pos, endPos); 708 pos = endPos; 709 } 710 addHighlight(imattrs, imstart); 711 712 // Set caret position in composed text 713 int caretPos = event.getCaretPosition(); 714 if (caretPos >= 0 && caretPos < imlength) { 715 textInput.selectRange(imstart + caretPos, imstart + caretPos); 716 } 717 } 718 } 719 } 720 721 // Public for behavior 722 /** 723 * Starts or stops caret blinking. The behavior classes use this to temporarily 724 * pause blinking while user is typing or otherwise moving the caret. 725 * 726 * @param value whether caret should be blinking. 727 */ 728 public void setCaretAnimating(boolean value) { 729 if (value) { 730 caretBlinking.start(); 731 } else { 732 caretBlinking.stop(); 733 blinkProperty().set(true); 734 } 735 } 736 737 738 739 /************************************************************************** 740 * 741 * Private implementation 742 * 743 **************************************************************************/ 744 745 TextInputControlBehavior getBehavior() { 746 return null; 747 } 748 749 ObservableBooleanValue caretVisibleProperty() { 750 return caretVisible; 751 } 752 753 boolean isRTL() { 754 return (getSkinnable().getEffectiveNodeOrientation() == NodeOrientation.RIGHT_TO_LEFT); 755 }; 756 757 private void createInputMethodAttributes(InputMethodHighlight highlight, int start, int end) { 758 double minX = 0f; 759 double maxX = 0f; 760 double minY = 0f; 761 double maxY = 0f; 762 763 PathElement elements[] = getUnderlineShape(start, end); 764 for (int i = 0; i < elements.length; i++) { 765 PathElement pe = elements[i]; 766 if (pe instanceof MoveTo) { 767 minX = maxX = ((MoveTo)pe).getX(); 768 minY = maxY = ((MoveTo)pe).getY(); 769 } else if (pe instanceof LineTo) { 770 minX = (minX < ((LineTo)pe).getX() ? minX : ((LineTo)pe).getX()); 771 maxX = (maxX > ((LineTo)pe).getX() ? maxX : ((LineTo)pe).getX()); 772 minY = (minY < ((LineTo)pe).getY() ? minY : ((LineTo)pe).getY()); 773 maxY = (maxY > ((LineTo)pe).getY() ? maxY : ((LineTo)pe).getY()); 774 } else if (pe instanceof HLineTo) { 775 minX = (minX < ((HLineTo)pe).getX() ? minX : ((HLineTo)pe).getX()); 776 maxX = (maxX > ((HLineTo)pe).getX() ? maxX : ((HLineTo)pe).getX()); 777 } else if (pe instanceof VLineTo) { 778 minY = (minY < ((VLineTo)pe).getY() ? minY : ((VLineTo)pe).getY()); 779 maxY = (maxY > ((VLineTo)pe).getY() ? maxY : ((VLineTo)pe).getY()); 780 } 781 // Don't assume that shapes are ended with ClosePath. 782 if (pe instanceof ClosePath || 783 i == elements.length - 1 || 784 (i < elements.length - 1 && elements[i+1] instanceof MoveTo)) { 785 // Now, create the attribute. 786 Shape attr = null; 787 if (highlight == InputMethodHighlight.SELECTED_RAW) { 788 // blue background 789 attr = new Path(); 790 ((Path)attr).getElements().addAll(getRangeShape(start, end)); 791 attr.setFill(Color.BLUE); 792 attr.setOpacity(0.3f); 793 } else if (highlight == InputMethodHighlight.UNSELECTED_RAW) { 794 // dash underline. 795 attr = new Line(minX + 2, maxY + 1, maxX - 2, maxY + 1); 796 attr.setStroke(textFill.get()); 797 attr.setStrokeWidth(maxY - minY); 798 ObservableList<Double> dashArray = attr.getStrokeDashArray(); 799 dashArray.add(Double.valueOf(2f)); 800 dashArray.add(Double.valueOf(2f)); 801 } else if (highlight == InputMethodHighlight.SELECTED_CONVERTED) { 802 // thick underline. 803 attr = new Line(minX + 2, maxY + 1, maxX - 2, maxY + 1); 804 attr.setStroke(textFill.get()); 805 attr.setStrokeWidth((maxY - minY) * 3); 806 } else if (highlight == InputMethodHighlight.UNSELECTED_CONVERTED) { 807 // single underline. 808 attr = new Line(minX + 2, maxY + 1, maxX - 2, maxY + 1); 809 attr.setStroke(textFill.get()); 810 attr.setStrokeWidth(maxY - minY); 811 } 812 813 if (attr != null) { 814 attr.setManaged(false); 815 imattrs.add(attr); 816 } 817 } 818 } 819 } 820 821 822 823 /************************************************************************** 824 * 825 * Support classes 826 * 827 **************************************************************************/ 828 829 private static final class CaretBlinking { 830 private final Timeline caretTimeline; 831 private final WeakReference<BooleanProperty> blinkPropertyRef; 832 833 public CaretBlinking(final BooleanProperty blinkProperty) { 834 blinkPropertyRef = new WeakReference<>(blinkProperty); 835 836 caretTimeline = new Timeline(); 837 caretTimeline.setCycleCount(Timeline.INDEFINITE); 838 caretTimeline.getKeyFrames().addAll( 839 new KeyFrame(Duration.ZERO, e -> setBlink(false)), 840 new KeyFrame(Duration.seconds(.5), e -> setBlink(true)), 841 new KeyFrame(Duration.seconds(1))); 842 } 843 844 public void start() { 845 caretTimeline.play(); 846 } 847 848 public void stop() { 849 caretTimeline.stop(); 850 } 851 852 private void setBlink(final boolean value) { 853 final BooleanProperty blinkProperty = blinkPropertyRef.get(); 854 if (blinkProperty == null) { 855 caretTimeline.stop(); 856 return; 857 } 858 859 blinkProperty.set(value); 860 } 861 } 862 863 864 private static class StyleableProperties { 865 private static final CssMetaData<TextInputControl,Paint> TEXT_FILL = 866 new CssMetaData<TextInputControl,Paint>("-fx-text-fill", 867 PaintConverter.getInstance(), Color.BLACK) { 868 869 @Override public boolean isSettable(TextInputControl n) { 870 final TextInputControlSkin<?> skin = (TextInputControlSkin<?>) n.getSkin(); 871 return skin.textFill == null || !skin.textFill.isBound(); 872 } 873 874 @Override @SuppressWarnings("unchecked") 875 public StyleableProperty<Paint> getStyleableProperty(TextInputControl n) { 876 final TextInputControlSkin<?> skin = (TextInputControlSkin<?>) n.getSkin(); 877 return (StyleableProperty<Paint>)skin.textFill; 878 } 879 }; 880 881 private static final CssMetaData<TextInputControl,Paint> PROMPT_TEXT_FILL = 882 new CssMetaData<TextInputControl,Paint>("-fx-prompt-text-fill", 883 PaintConverter.getInstance(), Color.GRAY) { 884 885 @Override public boolean isSettable(TextInputControl n) { 886 final TextInputControlSkin<?> skin = (TextInputControlSkin<?>) n.getSkin(); 887 return skin.promptTextFill == null || !skin.promptTextFill.isBound(); 888 } 889 890 @Override @SuppressWarnings("unchecked") 891 public StyleableProperty<Paint> getStyleableProperty(TextInputControl n) { 892 final TextInputControlSkin<?> skin = (TextInputControlSkin<?>) n.getSkin(); 893 return (StyleableProperty<Paint>)skin.promptTextFill; 894 } 895 }; 896 897 private static final CssMetaData<TextInputControl,Paint> HIGHLIGHT_FILL = 898 new CssMetaData<TextInputControl,Paint>("-fx-highlight-fill", 899 PaintConverter.getInstance(), Color.DODGERBLUE) { 900 901 @Override public boolean isSettable(TextInputControl n) { 902 final TextInputControlSkin<?> skin = (TextInputControlSkin<?>) n.getSkin(); 903 return skin.highlightFill == null || !skin.highlightFill.isBound(); 904 } 905 906 @Override @SuppressWarnings("unchecked") 907 public StyleableProperty<Paint> getStyleableProperty(TextInputControl n) { 908 final TextInputControlSkin<?> skin = (TextInputControlSkin<?>) n.getSkin(); 909 return (StyleableProperty<Paint>)skin.highlightFill; 910 } 911 }; 912 913 private static final CssMetaData<TextInputControl,Paint> HIGHLIGHT_TEXT_FILL = 914 new CssMetaData<TextInputControl,Paint>("-fx-highlight-text-fill", 915 PaintConverter.getInstance(), Color.WHITE) { 916 917 @Override public boolean isSettable(TextInputControl n) { 918 final TextInputControlSkin<?> skin = (TextInputControlSkin<?>) n.getSkin(); 919 return skin.highlightTextFill == null || !skin.highlightTextFill.isBound(); 920 } 921 922 @Override @SuppressWarnings("unchecked") 923 public StyleableProperty<Paint> getStyleableProperty(TextInputControl n) { 924 final TextInputControlSkin<?> skin = (TextInputControlSkin<?>) n.getSkin(); 925 return (StyleableProperty<Paint>)skin.highlightTextFill; 926 } 927 }; 928 929 private static final CssMetaData<TextInputControl,Boolean> DISPLAY_CARET = 930 new CssMetaData<TextInputControl,Boolean>("-fx-display-caret", 931 BooleanConverter.getInstance(), Boolean.TRUE) { 932 933 @Override public boolean isSettable(TextInputControl n) { 934 final TextInputControlSkin<?> skin = (TextInputControlSkin<?>) n.getSkin(); 935 return skin.displayCaret == null || !skin.displayCaret.isBound(); 936 } 937 938 @Override @SuppressWarnings("unchecked") 939 public StyleableProperty<Boolean> getStyleableProperty(TextInputControl n) { 940 final TextInputControlSkin<?> skin = (TextInputControlSkin<?>) n.getSkin(); 941 return (StyleableProperty<Boolean>)skin.displayCaret; 942 } 943 }; 944 945 private static final List<CssMetaData<? extends Styleable, ?>> STYLEABLES; 946 static { 947 List<CssMetaData<? extends Styleable, ?>> styleables = 948 new ArrayList<CssMetaData<? extends Styleable, ?>>(SkinBase.getClassCssMetaData()); 949 styleables.add(TEXT_FILL); 950 styleables.add(PROMPT_TEXT_FILL); 951 styleables.add(HIGHLIGHT_FILL); 952 styleables.add(HIGHLIGHT_TEXT_FILL); 953 styleables.add(DISPLAY_CARET); 954 955 STYLEABLES = Collections.unmodifiableList(styleables); 956 } 957 } 958 959 /** 960 * Returns the CssMetaData associated with this class, which may include the 961 * CssMetaData of its super classes. 962 */ 963 public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() { 964 return StyleableProperties.STYLEABLES; 965 } 966 967 /** 968 * {@inheritDoc} 969 */ 970 @Override public List<CssMetaData<? extends Styleable, ?>> getCssMetaData() { 971 return getClassCssMetaData(); 972 } 973 974 @Override protected void executeAccessibleAction(AccessibleAction action, Object... parameters) { 975 switch (action) { 976 case SHOW_TEXT_RANGE: { 977 Integer start = (Integer)parameters[0]; 978 Integer end = (Integer)parameters[1]; 979 if (start != null && end != null) { 980 scrollCharacterToVisible(end); 981 scrollCharacterToVisible(start); 982 scrollCharacterToVisible(end); 983 } 984 break; 985 } 986 default: super.executeAccessibleAction(action, parameters); 987 } 988 } 989 990 /** 991 * This class represents the hit information for a Text node. 992 */ 993 public static class TextPosInfo { 994 995 TextPosInfo(HitInfo hit) { 996 this(hit.getCharIndex(), hit.isLeading()); 997 } 998 999 /** 1000 * Create a TextPosInfo object representing a text index and forward bias. 1001 * 1002 * @param charIndex the character index. 1003 * @param leading whether the hit is on the leading edge of the character. If it is false, it represents the trailing edge. 1004 */ 1005 public TextPosInfo(int charIndex, boolean leading) { 1006 setCharIndex(charIndex); 1007 setLeading(leading); 1008 } 1009 1010 /** 1011 * The index of the character which this hit information refers to. 1012 */ 1013 private int charIndex; 1014 public int getCharIndex() { return charIndex; } 1015 void setCharIndex(int charIndex) { this.charIndex = charIndex; } 1016 1017 /** 1018 * Indicates whether the hit is on the leading edge of the character. 1019 * If it is false, it represents the trailing edge. 1020 */ 1021 private boolean leading; 1022 public boolean isLeading() { return leading; } 1023 void setLeading(boolean leading) { this.leading = leading; } 1024 1025 /** 1026 * Returns the index of the insertion position. 1027 */ 1028 public int getInsertionIndex() { 1029 return leading ? charIndex : charIndex + 1; 1030 } 1031 1032 @Override public String toString() { 1033 return "charIndex: " + charIndex + ", isLeading: " + leading; 1034 } 1035 } 1036 }