modules/controls/src/main/java/javafx/scene/control/skin/TextInputControlSkin.java

Print this page
rev 9240 : 8076423: JEP 253: Prepare JavaFX UI Controls & CSS APIs for Modularization
   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) {


 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());


 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 }
   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) {


 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());


 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 }