1 /*
   2  * Copyright (c) 2011, 2014, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 
  26 package com.sun.javafx.scene.control.skin;
  27 
  28 import com.sun.javafx.scene.input.ExtendedInputMethodRequests;
  29 import javafx.animation.KeyFrame;
  30 import javafx.animation.Timeline;
  31 import javafx.application.ConditionalFeature;
  32 import javafx.application.Platform;
  33 import javafx.beans.binding.BooleanBinding;
  34 import javafx.beans.binding.ObjectBinding;
  35 import javafx.beans.property.BooleanProperty;
  36 import javafx.beans.property.ObjectProperty;
  37 import javafx.beans.property.SimpleBooleanProperty;
  38 import javafx.beans.value.ObservableBooleanValue;
  39 import javafx.beans.value.ObservableObjectValue;
  40 import javafx.collections.ObservableList;
  41 import javafx.css.CssMetaData;
  42 import javafx.css.Styleable;
  43 import javafx.css.StyleableBooleanProperty;
  44 import javafx.css.StyleableObjectProperty;
  45 import javafx.css.StyleableProperty;
  46 import javafx.geometry.NodeOrientation;
  47 import javafx.geometry.Point2D;
  48 import javafx.geometry.Rectangle2D;
  49 import javafx.scene.AccessibleAction;
  50 import javafx.scene.Node;
  51 import javafx.scene.Scene;
  52 import javafx.scene.control.ContextMenu;
  53 import javafx.scene.control.IndexRange;
  54 import javafx.scene.control.MenuItem;
  55 import javafx.scene.control.SeparatorMenuItem;
  56 import javafx.scene.control.SkinBase;
  57 import javafx.scene.control.TextInputControl;
  58 import javafx.scene.input.Clipboard;
  59 import javafx.scene.input.InputMethodEvent;
  60 import javafx.scene.input.InputMethodHighlight;
  61 import javafx.scene.input.InputMethodTextRun;
  62 import javafx.scene.layout.StackPane;
  63 import javafx.scene.paint.Color;
  64 import javafx.scene.paint.Paint;
  65 import javafx.scene.shape.ClosePath;
  66 import javafx.scene.shape.HLineTo;
  67 import javafx.scene.shape.Line;
  68 import javafx.scene.shape.LineTo;
  69 import javafx.scene.shape.MoveTo;
  70 import javafx.scene.shape.Path;
  71 import javafx.scene.shape.PathElement;
  72 import javafx.scene.shape.Shape;
  73 import javafx.scene.shape.VLineTo;
  74 import javafx.stage.Window;
  75 import javafx.util.Duration;
  76 import java.lang.ref.WeakReference;
  77 import java.util.ArrayList;
  78 import java.util.Collections;
  79 import java.util.List;
  80 import com.sun.javafx.PlatformUtil;
  81 import com.sun.javafx.css.converters.BooleanConverter;
  82 import com.sun.javafx.css.converters.PaintConverter;
  83 import com.sun.javafx.scene.control.behavior.TextInputControlBehavior;
  84 import com.sun.javafx.tk.FontMetrics;
  85 import com.sun.javafx.tk.Toolkit;
  86 import static com.sun.javafx.PlatformUtil.isWindows;
  87 import static com.sun.javafx.scene.control.skin.resources.ControlResources.getString;
  88 import java.security.AccessController;
  89 import java.security.PrivilegedAction;
  90 
  91 /**
  92  * Abstract base class for text input skins.
  93  */
  94 public abstract class TextInputControlSkin<T extends TextInputControl, B extends TextInputControlBehavior<T>> extends BehaviorSkinBase<T, B> {
  95 
  96     static boolean preload = false;
  97     static {
  98         AccessController.doPrivileged((PrivilegedAction<Void>) () -> {
  99             String s = System.getProperty("com.sun.javafx.virtualKeyboard.preload");
 100             if (s != null) {
 101                 if (s.equalsIgnoreCase("PRERENDER")) {
 102                     preload = true;
 103                 }
 104             }
 105             return null;
 106         });
 107     }    
 108 
 109     /**
 110      * Specifies whether we ought to show handles. We should do it on touch platforms, but not
 111      * iOS (and maybe not Android either?)
 112      */
 113     protected static final boolean SHOW_HANDLES = IS_TOUCH_SUPPORTED && !PlatformUtil.isIOS();
 114 
 115     protected final ObservableObjectValue<FontMetrics> fontMetrics;
 116 
 117     /**
 118      * The fill to use for the text under normal conditions
 119      */
 120     protected final ObjectProperty<Paint> textFill = new StyleableObjectProperty<Paint>(Color.BLACK) {
 121         @Override public Object getBean() {
 122             return TextInputControlSkin.this;
 123         }
 124 
 125         @Override public String getName() {
 126             return "textFill";
 127         }
 128 
 129         @Override public CssMetaData<TextInputControl,Paint> getCssMetaData() {
 130             return StyleableProperties.TEXT_FILL;
 131         }
 132     };
 133     
 134     protected final ObjectProperty<Paint> promptTextFill = new StyleableObjectProperty<Paint>(Color.GRAY) {
 135         @Override public Object getBean() {
 136             return TextInputControlSkin.this;
 137         }
 138 
 139         @Override public String getName() {
 140             return "promptTextFill";
 141         }
 142 
 143         @Override public CssMetaData<TextInputControl,Paint> getCssMetaData() {
 144             return StyleableProperties.PROMPT_TEXT_FILL;
 145         }
 146     };
 147     
 148     /**
 149      * The fill to use for the text when highlighted.
 150      */
 151     protected final ObjectProperty<Paint> highlightFill = new StyleableObjectProperty<Paint>(Color.DODGERBLUE) {
 152         @Override protected void invalidated() {
 153             updateHighlightFill();
 154         }
 155 
 156         @Override public Object getBean() {
 157             return TextInputControlSkin.this;
 158         }
 159 
 160         @Override public String getName() {
 161             return "highlightFill";
 162         }
 163 
 164         @Override public CssMetaData<TextInputControl,Paint> getCssMetaData() {
 165             return StyleableProperties.HIGHLIGHT_FILL;
 166         }
 167     };
 168     
 169     protected final ObjectProperty<Paint> highlightTextFill = new StyleableObjectProperty<Paint>(Color.WHITE) {
 170         @Override protected void invalidated() {
 171             updateHighlightTextFill();
 172         }
 173 
 174         @Override public Object getBean() {
 175             return TextInputControlSkin.this;
 176         }
 177 
 178         @Override public String getName() {
 179             return "highlightTextFill";
 180         }
 181 
 182         @Override public CssMetaData<TextInputControl,Paint> getCssMetaData() {
 183             return StyleableProperties.HIGHLIGHT_TEXT_FILL;
 184         }
 185     };
 186     
 187     protected final BooleanProperty displayCaret = new StyleableBooleanProperty(true) {
 188         @Override public Object getBean() {
 189             return TextInputControlSkin.this;
 190         }
 191 
 192         @Override public String getName() {
 193             return "displayCaret";
 194         }
 195 
 196         @Override public CssMetaData<TextInputControl,Boolean> getCssMetaData() {
 197             return StyleableProperties.DISPLAY_CARET;
 198         }
 199     };
 200 
 201     private BooleanProperty forwardBias = new SimpleBooleanProperty(this, "forwardBias", true);
 202     public BooleanProperty forwardBiasProperty() {
 203         return forwardBias;
 204     }
 205     public void setForwardBias(boolean isLeading) {
 206         forwardBias.set(isLeading);
 207     }
 208     public boolean isForwardBias() {
 209         return forwardBias.get();
 210     }
 211 
 212     private BooleanProperty blink = new SimpleBooleanProperty(this, "blink", true);
 213     protected ObservableBooleanValue caretVisible;
 214     private CaretBlinking caretBlinking = new CaretBlinking(blink);
 215 
 216     /**
 217      * A path, provided by the textNode, which represents the caret.
 218      * I assume this has to be updated whenever the caretPosition
 219      * changes. Perhaps more frequently (including text changes),
 220      * but I'm not sure.
 221      */
 222     protected final Path caretPath = new Path();
 223 
 224     protected StackPane caretHandle = null;
 225     protected StackPane selectionHandle1 = null;
 226     protected StackPane selectionHandle2 = null;
 227 
 228     public Point2D getMenuPosition() {
 229         if (SHOW_HANDLES) {
 230             if (caretHandle.isVisible()) {
 231                 return new Point2D(caretHandle.getLayoutX() + caretHandle.getWidth() / 2,
 232                                    caretHandle.getLayoutY());
 233             } else if (selectionHandle1.isVisible() && selectionHandle2.isVisible()) {
 234                 return new Point2D((selectionHandle1.getLayoutX() + selectionHandle1.getWidth() / 2 +
 235                                     selectionHandle2.getLayoutX() + selectionHandle2.getWidth() / 2) / 2,
 236                                    selectionHandle2.getLayoutY() + selectionHandle2.getHeight() / 2);
 237             } else {
 238                 return null;
 239             }
 240         } else {
 241             throw new UnsupportedOperationException();
 242         }
 243     }
 244 
 245 
 246     private final static boolean IS_FXVK_SUPPORTED = Platform.isSupported(ConditionalFeature.VIRTUAL_KEYBOARD);
 247     private static boolean USE_FXVK = IS_FXVK_SUPPORTED;
 248 
 249     /* For testing only */
 250     static int vkType = -1;
 251     public void toggleUseVK() {
 252         vkType++;
 253         if (vkType < 4) {
 254             USE_FXVK = true;
 255             getSkinnable().getProperties().put(FXVK.VK_TYPE_PROP_KEY, FXVK.VK_TYPE_NAMES[vkType]);
 256             FXVK.attach(getSkinnable());
 257         } else {
 258             FXVK.detach();
 259             vkType = -1;
 260             USE_FXVK = false;
 261         }
 262     }
 263 
 264 
 265     public TextInputControlSkin(final T textInput, final B behavior) {
 266         super(textInput, behavior);
 267 
 268         fontMetrics = new ObjectBinding<FontMetrics>() {
 269             { bind(textInput.fontProperty()); }
 270             @Override protected FontMetrics computeValue() {
 271                 invalidateMetrics();
 272                 return Toolkit.getToolkit().getFontLoader().getFontMetrics(textInput.getFont());
 273             }
 274         };
 275 
 276         /**
 277          * The caret is visible when the text box is focused AND when the selection
 278          * is empty. If the selection is non empty or the text box is not focused
 279          * then we don't want to show the caret. Also, we show the caret while
 280          * performing some operations such as most key strokes. In that case we
 281          * simply toggle its opacity.
 282          * <p>
 283          */
 284         caretVisible = new BooleanBinding() {
 285             { bind(textInput.focusedProperty(), textInput.anchorProperty(), textInput.caretPositionProperty(),
 286                     textInput.disabledProperty(), textInput.editableProperty(), displayCaret, blink);}
 287             @Override protected boolean computeValue() {
 288                 // RT-10682: On Windows, we show the caret during selection, but on others we hide it
 289                 return !blink.get() && displayCaret.get() && textInput.isFocused() &&
 290                         (isWindows() || (textInput.getCaretPosition() == textInput.getAnchor())) &&
 291                         !textInput.isDisabled() &&
 292                         textInput.isEditable();
 293             }
 294         };
 295 
 296         if (SHOW_HANDLES) {
 297             caretHandle      = new StackPane();
 298             selectionHandle1 = new StackPane();
 299             selectionHandle2 = new StackPane();
 300 
 301             caretHandle.setManaged(false);
 302             selectionHandle1.setManaged(false);
 303             selectionHandle2.setManaged(false);
 304 
 305             caretHandle.visibleProperty().bind(new BooleanBinding() {
 306                 { bind(textInput.focusedProperty(), textInput.anchorProperty(),
 307                        textInput.caretPositionProperty(), textInput.disabledProperty(),
 308                        textInput.editableProperty(), textInput.lengthProperty(), displayCaret);}
 309                 @Override protected boolean computeValue() {
 310                     return (displayCaret.get() && textInput.isFocused() &&
 311                             textInput.getCaretPosition() == textInput.getAnchor() &&
 312                             !textInput.isDisabled() && textInput.isEditable() &&
 313                             textInput.getLength() > 0);
 314                 }
 315             });
 316 
 317 
 318             selectionHandle1.visibleProperty().bind(new BooleanBinding() {
 319                 { bind(textInput.focusedProperty(), textInput.anchorProperty(), textInput.caretPositionProperty(),
 320                        textInput.disabledProperty(), displayCaret);}
 321                 @Override protected boolean computeValue() {
 322                     return (displayCaret.get() && textInput.isFocused() &&
 323                             textInput.getCaretPosition() != textInput.getAnchor() &&
 324                             !textInput.isDisabled());
 325                 }
 326             });
 327 
 328 
 329             selectionHandle2.visibleProperty().bind(new BooleanBinding() {
 330                 { bind(textInput.focusedProperty(), textInput.anchorProperty(), textInput.caretPositionProperty(),
 331                        textInput.disabledProperty(), displayCaret);}
 332                 @Override protected boolean computeValue() {
 333                     return (displayCaret.get() && textInput.isFocused() &&
 334                             textInput.getCaretPosition() != textInput.getAnchor() &&
 335                             !textInput.isDisabled());
 336                 }
 337             });
 338 
 339 
 340             caretHandle.getStyleClass().setAll("caret-handle");
 341             selectionHandle1.getStyleClass().setAll("selection-handle");
 342             selectionHandle2.getStyleClass().setAll("selection-handle");
 343 
 344             selectionHandle1.setId("selection-handle-1");
 345             selectionHandle2.setId("selection-handle-2");
 346         }
 347 
 348         if (IS_FXVK_SUPPORTED) {
 349             if (preload) {
 350                 Scene scene = textInput.getScene();
 351                 if (scene != null) {
 352                     Window window = scene.getWindow();
 353                     if (window != null) {
 354                         FXVK.init(textInput);
 355                     }
 356                 }
 357             }
 358             textInput.focusedProperty().addListener(observable -> {
 359                 if (USE_FXVK) {
 360                     Scene scene = getSkinnable().getScene();
 361                     if (textInput.isEditable() && textInput.isFocused()) {
 362                         FXVK.attach(textInput);
 363                     } else if (scene == null ||
 364                                scene.getWindow() == null ||
 365                                !scene.getWindow().isFocused() ||
 366                                !(scene.getFocusOwner() instanceof TextInputControl &&
 367                                  ((TextInputControl)scene.getFocusOwner()).isEditable())) {
 368                         FXVK.detach();
 369                     }
 370                 }
 371             });
 372         }
 373 
 374         if (textInput.getOnInputMethodTextChanged() == null) {
 375             textInput.setOnInputMethodTextChanged(event -> {
 376                 handleInputMethodEvent(event);
 377             });
 378         }
 379 
 380         textInput.setInputMethodRequests(new ExtendedInputMethodRequests() {
 381             @Override public Point2D getTextLocation(int offset) {
 382                 Scene scene = getSkinnable().getScene();
 383                 Window window = scene.getWindow();
 384                 // Don't use imstart here because it isn't initialized yet.
 385                 Rectangle2D characterBounds = getCharacterBounds(textInput.getSelection().getStart() + offset);
 386                 Point2D p = getSkinnable().localToScene(characterBounds.getMinX(), characterBounds.getMaxY());
 387                 Point2D location = new Point2D(window.getX() + scene.getX() + p.getX(),
 388                                                window.getY() + scene.getY() + p.getY());
 389                 return location;
 390             }
 391 
 392             @Override
 393             public int getLocationOffset(int x, int y) {
 394                 return getInsertionPoint(x, y);
 395             }
 396 
 397             @Override
 398             public void cancelLatestCommittedText() {
 399                 // TODO
 400             }
 401 
 402             @Override
 403             public String getSelectedText() {
 404                 TextInputControl textInput = getSkinnable();
 405                 IndexRange selection = textInput.getSelection();
 406 
 407                 return textInput.getText(selection.getStart(), selection.getEnd());
 408             }
 409 
 410             @Override
 411             public int getInsertPositionOffset() {
 412                 int caretPosition = getSkinnable().getCaretPosition();
 413                 if (caretPosition < imstart) {
 414                     return caretPosition;
 415                 } else if (caretPosition < imstart + imlength) {
 416                     return imstart;
 417                 } else {
 418                     return caretPosition - imlength;
 419                 }
 420             }
 421 
 422             @Override
 423             public String getCommittedText(int begin, int end) {
 424                 TextInputControl textInput = getSkinnable();
 425                 if (begin < imstart) {
 426                     if (end <= imstart) {
 427                         return textInput.getText(begin, end);
 428                     } else {
 429                         return textInput.getText(begin, imstart) + textInput.getText(imstart + imlength, end + imlength);
 430                     }
 431                 } else {
 432                     return textInput.getText(begin + imlength, end + imlength);
 433                 }
 434             }
 435 
 436             @Override
 437             public int getCommittedTextLength() {
 438                 return getSkinnable().getText().length() - imlength;
 439             }
 440         });
 441     }
 442 
 443     // For use with PasswordField in TextFieldSkin
 444     protected String maskText(String txt) {
 445         return txt;
 446     }
 447 
 448  
 449     /**
 450      * Returns the character at a given offset.
 451      *
 452      * @param index
 453      */
 454     public char getCharacter(int index) { return '\0'; }
 455 
 456     /**
 457      * Returns the insertion point for a given location.
 458      *
 459      * @param x
 460      * @param y
 461      */
 462     public int getInsertionPoint(double x, double y) { return 0; }
 463 
 464     /**
 465      * Returns the bounds of the character at a given index.
 466      *
 467      * @param index
 468      */
 469     public Rectangle2D getCharacterBounds(int index) { return null; }
 470 
 471     /**
 472      * Ensures that the character at a given index is visible.
 473      *
 474      * @param index
 475      */
 476     public void scrollCharacterToVisible(int index) {}
 477 
 478     protected void invalidateMetrics() {
 479     }
 480 
 481     protected void updateTextFill() {};
 482     protected void updateHighlightFill() {};
 483     protected void updateHighlightTextFill() {};
 484 
 485     // Start/Length of the text under input method composition
 486     private int imstart;
 487     private int imlength;
 488     // Holds concrete attributes for the composition runs
 489     private List<Shape> imattrs = new java.util.ArrayList<Shape>();
 490 
 491     protected void handleInputMethodEvent(InputMethodEvent event) {
 492         final TextInputControl textInput = getSkinnable();
 493         if (textInput.isEditable() && !textInput.textProperty().isBound() && !textInput.isDisabled()) {
 494 
 495             // just replace the text on iOS
 496             if (PlatformUtil.isIOS()) {
 497                textInput.setText(event.getCommitted());
 498                return;
 499             }
 500             
 501             // remove previous input method text (if any) or selected text
 502             if (imlength != 0) {
 503                 removeHighlight(imattrs);
 504                 imattrs.clear();
 505                 textInput.selectRange(imstart, imstart + imlength);
 506             }
 507 
 508             // Insert committed text
 509             if (event.getCommitted().length() != 0) {
 510                 String committed = event.getCommitted();
 511                 textInput.replaceText(textInput.getSelection(), committed);
 512             }
 513 
 514             // Replace composed text
 515             imstart = textInput.getSelection().getStart();
 516             StringBuilder composed = new StringBuilder();
 517             for (InputMethodTextRun run : event.getComposed()) {
 518                 composed.append(run.getText());
 519             }
 520             textInput.replaceText(textInput.getSelection(), composed.toString());
 521             imlength = composed.length();
 522             if (imlength != 0) {
 523                 int pos = imstart;
 524                 for (InputMethodTextRun run : event.getComposed()) {
 525                     int endPos = pos + run.getText().length();
 526                     createInputMethodAttributes(run.getHighlight(), pos, endPos);
 527                     pos = endPos;
 528                 }
 529                 addHighlight(imattrs, imstart);
 530 
 531                 // Set caret position in composed text
 532                 int caretPos = event.getCaretPosition();
 533                 if (caretPos >= 0 && caretPos < imlength) {
 534                     textInput.selectRange(imstart + caretPos, imstart + caretPos);
 535                 }
 536             }
 537         }
 538     }
 539 
 540     protected abstract PathElement[] getUnderlineShape(int start, int end);
 541     protected abstract PathElement[] getRangeShape(int start, int end);
 542     protected abstract void addHighlight(List<? extends Node> nodes, int start);
 543     protected abstract void removeHighlight(List<? extends Node> nodes);
 544     public abstract void nextCharacterVisually(boolean moveRight);
 545 
 546     private void createInputMethodAttributes(InputMethodHighlight highlight, int start, int end) {
 547         double minX = 0f;
 548         double maxX = 0f;
 549         double minY = 0f;
 550         double maxY = 0f;
 551 
 552         PathElement elements[] = getUnderlineShape(start, end);
 553         for (int i = 0; i < elements.length; i++) {
 554             PathElement pe = elements[i];
 555             if (pe instanceof MoveTo) {
 556                 minX = maxX = ((MoveTo)pe).getX();
 557                 minY = maxY = ((MoveTo)pe).getY();
 558             } else if (pe instanceof LineTo) {
 559                 minX = (minX < ((LineTo)pe).getX() ? minX : ((LineTo)pe).getX());
 560                 maxX = (maxX > ((LineTo)pe).getX() ? maxX : ((LineTo)pe).getX());
 561                 minY = (minY < ((LineTo)pe).getY() ? minY : ((LineTo)pe).getY());
 562                 maxY = (maxY > ((LineTo)pe).getY() ? maxY : ((LineTo)pe).getY());
 563             } else if (pe instanceof HLineTo) {
 564                 minX = (minX < ((HLineTo)pe).getX() ? minX : ((HLineTo)pe).getX());
 565                 maxX = (maxX > ((HLineTo)pe).getX() ? maxX : ((HLineTo)pe).getX());
 566             } else if (pe instanceof VLineTo) {
 567                 minY = (minY < ((VLineTo)pe).getY() ? minY : ((VLineTo)pe).getY());
 568                 maxY = (maxY > ((VLineTo)pe).getY() ? maxY : ((VLineTo)pe).getY());
 569             }
 570             // Don't assume that shapes are ended with ClosePath.
 571             if (pe instanceof ClosePath ||
 572                 i == elements.length - 1 ||
 573                 (i < elements.length - 1 && elements[i+1] instanceof MoveTo)) {
 574                 // Now, create the attribute.
 575                 Shape attr = null;
 576                 if (highlight == InputMethodHighlight.SELECTED_RAW) {
 577                     // blue background
 578                     attr = new Path();
 579                     ((Path)attr).getElements().addAll(getRangeShape(start, end));
 580                     attr.setFill(Color.BLUE);
 581                     attr.setOpacity(0.3f);
 582                 } else if (highlight == InputMethodHighlight.UNSELECTED_RAW) {
 583                     // dash underline.
 584                     attr = new Line(minX + 2, maxY + 1, maxX - 2, maxY + 1);
 585                     attr.setStroke(textFill.get());
 586                     attr.setStrokeWidth(maxY - minY);
 587                     ObservableList<Double> dashArray = attr.getStrokeDashArray();
 588                     dashArray.add(Double.valueOf(2f));
 589                     dashArray.add(Double.valueOf(2f));
 590                 } else if (highlight == InputMethodHighlight.SELECTED_CONVERTED) {
 591                     // thick underline.
 592                     attr = new Line(minX + 2, maxY + 1, maxX - 2, maxY + 1);
 593                     attr.setStroke(textFill.get());
 594                     attr.setStrokeWidth((maxY - minY) * 3);
 595                 } else if (highlight == InputMethodHighlight.UNSELECTED_CONVERTED) {
 596                     // single underline.
 597                     attr = new Line(minX + 2, maxY + 1, maxX - 2, maxY + 1);
 598                     attr.setStroke(textFill.get());
 599                     attr.setStrokeWidth(maxY - minY);
 600                 }
 601                 
 602                 if (attr != null) {
 603                     attr.setManaged(false);
 604                     imattrs.add(attr);
 605                 }
 606             }
 607         }
 608     }
 609 
 610     protected boolean isRTL() {
 611         return (getSkinnable().getEffectiveNodeOrientation() == NodeOrientation.RIGHT_TO_LEFT);
 612     };
 613 
 614     public void setCaretAnimating(boolean value) {
 615         if (value) {
 616             caretBlinking.start();
 617         } else {
 618             caretBlinking.stop();
 619             blink.set(true);
 620         }
 621     }
 622 
 623     private static final class CaretBlinking {
 624         private final Timeline caretTimeline;
 625         private final WeakReference<BooleanProperty> blinkPropertyRef;
 626 
 627         public CaretBlinking(final BooleanProperty blinkProperty) {
 628             blinkPropertyRef =
 629                     new WeakReference<BooleanProperty>(blinkProperty);
 630 
 631             caretTimeline = new Timeline();
 632             caretTimeline.setCycleCount(Timeline.INDEFINITE);
 633             caretTimeline.getKeyFrames().addAll(
 634                 new KeyFrame(Duration.ZERO,
 635                         event -> {
 636                             setBlink(false);
 637                         }
 638                 ),
 639                 new KeyFrame(Duration.seconds(.5),
 640                         event -> {
 641                             setBlink(true);
 642                         }
 643                 ),
 644                 new KeyFrame(Duration.seconds(1)));
 645         }
 646 
 647         public void start() {
 648             caretTimeline.play();
 649         }
 650 
 651         public void stop() {
 652             caretTimeline.stop();
 653         }
 654 
 655         private void setBlink(final boolean value) {
 656             final BooleanProperty blinkProperty = blinkPropertyRef.get();
 657             if (blinkProperty == null) {
 658                 caretTimeline.stop();
 659                 return;
 660             }
 661 
 662             blinkProperty.set(value);
 663         }
 664     }
 665 
 666     class ContextMenuItem extends MenuItem {
 667         ContextMenuItem(final String action) {
 668             super(getString("TextInputControl.menu." + action));
 669             setOnAction(e -> {
 670                 getBehavior().callAction(action);
 671             });
 672         }
 673     }
 674 
 675     final MenuItem undoMI   = new ContextMenuItem("Undo");
 676     final MenuItem redoMI   = new ContextMenuItem("Redo");
 677     final MenuItem cutMI    = new ContextMenuItem("Cut");
 678     final MenuItem copyMI   = new ContextMenuItem("Copy");
 679     final MenuItem pasteMI  = new ContextMenuItem("Paste");
 680     final MenuItem deleteMI = new ContextMenuItem("DeleteSelection");
 681     final MenuItem selectWordMI = new ContextMenuItem("SelectWord");
 682     final MenuItem selectAllMI = new ContextMenuItem("SelectAll");
 683     final MenuItem separatorMI = new SeparatorMenuItem();
 684 
 685     public void populateContextMenu(ContextMenu contextMenu) {
 686         TextInputControl textInputControl = getSkinnable();
 687         boolean editable = textInputControl.isEditable();
 688         boolean hasText = (textInputControl.getLength() > 0);
 689         boolean hasSelection = (textInputControl.getSelection().getLength() > 0);
 690         boolean maskText = (maskText("A") != "A");
 691         ObservableList<MenuItem> items = contextMenu.getItems();
 692 
 693         if (SHOW_HANDLES) {
 694             items.clear();
 695             if (!maskText && hasSelection) {
 696                 if (editable) {
 697                     items.add(cutMI);
 698                 }
 699                 items.add(copyMI);
 700             }
 701             if (editable && Clipboard.getSystemClipboard().hasString()) {
 702                 items.add(pasteMI);
 703             }
 704             if (hasText) {
 705                 if (!hasSelection) {
 706                     items.add(selectWordMI);
 707                 }
 708                 items.add(selectAllMI);
 709             }
 710             selectWordMI.getProperties().put("refreshMenu", Boolean.TRUE);
 711             selectAllMI.getProperties().put("refreshMenu", Boolean.TRUE);
 712         } else {
 713             if (editable) {
 714                 items.setAll(undoMI, redoMI, cutMI, copyMI, pasteMI, deleteMI,
 715                              separatorMI, selectAllMI);
 716             } else {
 717                 items.setAll(copyMI, separatorMI, selectAllMI);
 718             }
 719             undoMI.setDisable(!getSkinnable().isUndoable());
 720             redoMI.setDisable(!getSkinnable().isRedoable());
 721             cutMI.setDisable(maskText || !hasSelection);
 722             copyMI.setDisable(maskText || !hasSelection);
 723             pasteMI.setDisable(!Clipboard.getSystemClipboard().hasString());
 724             deleteMI.setDisable(!hasSelection);
 725         }
 726     }
 727 
 728     private static class StyleableProperties {
 729         private static final CssMetaData<TextInputControl,Paint> TEXT_FILL =
 730             new CssMetaData<TextInputControl,Paint>("-fx-text-fill",
 731                 PaintConverter.getInstance(), Color.BLACK) {
 732 
 733             @Override
 734             public boolean isSettable(TextInputControl n) {
 735                 final TextInputControlSkin<?,?> skin = (TextInputControlSkin<?,?>) n.getSkin();
 736                 return skin.textFill == null || !skin.textFill.isBound();
 737             }
 738 
 739             @Override @SuppressWarnings("unchecked") 
 740             public StyleableProperty<Paint> getStyleableProperty(TextInputControl n) {
 741                 final TextInputControlSkin<?,?> skin = (TextInputControlSkin<?,?>) n.getSkin();                
 742                 return (StyleableProperty<Paint>)skin.textFill;
 743             }
 744         };
 745        
 746         private static final CssMetaData<TextInputControl,Paint> PROMPT_TEXT_FILL =
 747             new CssMetaData<TextInputControl,Paint>("-fx-prompt-text-fill",
 748                 PaintConverter.getInstance(), Color.GRAY) {
 749 
 750             @Override
 751             public boolean isSettable(TextInputControl n) {
 752                 final TextInputControlSkin<?,?> skin = (TextInputControlSkin<?,?>) n.getSkin();
 753                 return skin.promptTextFill == null || !skin.promptTextFill.isBound();
 754             }
 755 
 756             @Override @SuppressWarnings("unchecked") 
 757             public StyleableProperty<Paint> getStyleableProperty(TextInputControl n) {
 758                 final TextInputControlSkin<?,?> skin = (TextInputControlSkin<?,?>) n.getSkin();
 759                 return (StyleableProperty<Paint>)skin.promptTextFill;
 760             }
 761         };
 762         
 763         private static final CssMetaData<TextInputControl,Paint> HIGHLIGHT_FILL =
 764             new CssMetaData<TextInputControl,Paint>("-fx-highlight-fill",
 765                 PaintConverter.getInstance(), Color.DODGERBLUE) {
 766 
 767             @Override
 768             public boolean isSettable(TextInputControl n) {
 769                 final TextInputControlSkin<?,?> skin = (TextInputControlSkin<?,?>) n.getSkin();
 770                 return skin.highlightFill == null || !skin.highlightFill.isBound();
 771             }
 772 
 773             @Override @SuppressWarnings("unchecked") 
 774             public StyleableProperty<Paint> getStyleableProperty(TextInputControl n) {
 775                 final TextInputControlSkin<?,?> skin = (TextInputControlSkin<?,?>) n.getSkin();
 776                 return (StyleableProperty<Paint>)skin.highlightFill;
 777             }
 778         };
 779         
 780         private static final CssMetaData<TextInputControl,Paint> HIGHLIGHT_TEXT_FILL =
 781             new CssMetaData<TextInputControl,Paint>("-fx-highlight-text-fill",
 782                 PaintConverter.getInstance(), Color.WHITE) {
 783 
 784             @Override
 785             public boolean isSettable(TextInputControl n) {
 786                 final TextInputControlSkin<?,?> skin = (TextInputControlSkin<?,?>) n.getSkin();
 787                 return skin.highlightTextFill == null || !skin.highlightTextFill.isBound();
 788             }
 789 
 790             @Override @SuppressWarnings("unchecked") 
 791             public StyleableProperty<Paint> getStyleableProperty(TextInputControl n) {
 792                 final TextInputControlSkin<?,?> skin = (TextInputControlSkin<?,?>) n.getSkin();
 793                 return (StyleableProperty<Paint>)skin.highlightTextFill;
 794             }
 795         };
 796         
 797         private static final CssMetaData<TextInputControl,Boolean> DISPLAY_CARET =
 798             new CssMetaData<TextInputControl,Boolean>("-fx-display-caret",
 799                 BooleanConverter.getInstance(), Boolean.TRUE) {
 800 
 801             @Override
 802             public boolean isSettable(TextInputControl n) {
 803                 final TextInputControlSkin<?,?> skin = (TextInputControlSkin<?,?>) n.getSkin();
 804                 return skin.displayCaret == null || !skin.displayCaret.isBound();
 805             }
 806 
 807             @Override @SuppressWarnings("unchecked") 
 808             public StyleableProperty<Boolean> getStyleableProperty(TextInputControl n) {
 809                 final TextInputControlSkin<?,?> skin = (TextInputControlSkin<?,?>) n.getSkin();
 810                 return (StyleableProperty<Boolean>)skin.displayCaret;
 811             }
 812         };
 813 
 814         private static final List<CssMetaData<? extends Styleable, ?>> STYLEABLES;
 815         static {
 816             List<CssMetaData<? extends Styleable, ?>> styleables = 
 817                 new ArrayList<CssMetaData<? extends Styleable, ?>>(SkinBase.getClassCssMetaData());
 818             styleables.add(TEXT_FILL);
 819             styleables.add(PROMPT_TEXT_FILL);
 820             styleables.add(HIGHLIGHT_FILL);
 821             styleables.add(HIGHLIGHT_TEXT_FILL);
 822             styleables.add(DISPLAY_CARET);
 823 
 824             STYLEABLES = Collections.unmodifiableList(styleables);
 825         }
 826     }
 827 
 828     /**
 829      * @return The CssMetaData associated with this class, which may include the
 830      * CssMetaData of its super classes.
 831      */
 832     public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
 833         return StyleableProperties.STYLEABLES;
 834     }
 835 
 836     /**
 837      * {@inheritDoc}
 838      */
 839     @Override
 840     public List<CssMetaData<? extends Styleable, ?>> getCssMetaData() {
 841         return getClassCssMetaData();
 842     }
 843 
 844     protected void executeAccessibleAction(AccessibleAction action, Object... parameters) {
 845         switch (action) {
 846             case SHOW_TEXT_RANGE: {
 847                 Integer start = (Integer)parameters[0];
 848                 Integer end = (Integer)parameters[1];
 849                 if (start != null && end != null) {
 850                     scrollCharacterToVisible(end);
 851                     scrollCharacterToVisible(start);
 852                     scrollCharacterToVisible(end);
 853                 }
 854                 break;
 855             } 
 856             default: super.executeAccessibleAction(action, parameters);
 857         }
 858     }
 859 }