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 }