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,7 +1,7 @@
 /*
- * Copyright (c) 2011, 2014, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2011, 2015, Oracle and/or its affiliates. All rights reserved.
  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  *
  * This code is free software; you can redistribute it and/or modify it
  * under the terms of the GNU General Public License version 2 only, as
  * published by the Free Software Foundation.  Oracle designates this

@@ -21,12 +21,14 @@
  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  * or visit www.oracle.com if you need additional information or have any
  * questions.
  */
 
-package com.sun.javafx.scene.control.skin;
+package javafx.scene.control.skin;
 
+import com.sun.javafx.scene.control.Properties;
+import com.sun.javafx.scene.control.skin.FXVK;
 import com.sun.javafx.scene.input.ExtendedInputMethodRequests;
 import javafx.animation.KeyFrame;
 import javafx.animation.Timeline;
 import javafx.application.ConditionalFeature;
 import javafx.application.Platform;

@@ -47,17 +49,13 @@
 import javafx.geometry.Point2D;
 import javafx.geometry.Rectangle2D;
 import javafx.scene.AccessibleAction;
 import javafx.scene.Node;
 import javafx.scene.Scene;
-import javafx.scene.control.ContextMenu;
 import javafx.scene.control.IndexRange;
-import javafx.scene.control.MenuItem;
-import javafx.scene.control.SeparatorMenuItem;
 import javafx.scene.control.SkinBase;
 import javafx.scene.control.TextInputControl;
-import javafx.scene.input.Clipboard;
 import javafx.scene.input.InputMethodEvent;
 import javafx.scene.input.InputMethodHighlight;
 import javafx.scene.input.InputMethodTextRun;
 import javafx.scene.layout.StackPane;
 import javafx.scene.paint.Color;

@@ -76,24 +74,48 @@
 import java.lang.ref.WeakReference;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
 import com.sun.javafx.PlatformUtil;
-import com.sun.javafx.css.converters.BooleanConverter;
-import com.sun.javafx.css.converters.PaintConverter;
+import javafx.css.converter.BooleanConverter;
+import javafx.css.converter.PaintConverter;
 import com.sun.javafx.scene.control.behavior.TextInputControlBehavior;
+import com.sun.javafx.scene.text.HitInfo;
 import com.sun.javafx.tk.FontMetrics;
 import com.sun.javafx.tk.Toolkit;
 import static com.sun.javafx.PlatformUtil.isWindows;
-import static com.sun.javafx.scene.control.skin.resources.ControlResources.getString;
 import java.security.AccessController;
 import java.security.PrivilegedAction;
 
 /**
  * Abstract base class for text input skins.
+ *
+ * @since 9
+ * @see TextFieldSkin
+ * @see TextAreaSkin
  */
-public abstract class TextInputControlSkin<T extends TextInputControl, B extends TextInputControlBehavior<T>> extends BehaviorSkinBase<T, B> {
+public abstract class TextInputControlSkin<T extends TextInputControl> extends SkinBase<T> {
+    
+    /**************************************************************************
+     *
+     * Static fields / blocks
+     *
+     **************************************************************************/
+
+    /**
+     * Unit names for caret movement.
+     *
+     * @see #moveCaret(TextUnit, Direction, boolean)
+     */
+    public static enum TextUnit { CHARACTER, WORD, LINE, PARAGRAPH, PAGE };
+
+    /**
+     * Direction names for caret movement.
+     *
+     * @see #moveCaret(TextUnit, Direction, boolean)
+     */
+    public static enum Direction { LEFT, RIGHT, UP, DOWN, BEGINNING, END };
 
     static boolean preload = false;
     static {
         AccessController.doPrivileged((PrivilegedAction<Void>) () -> {
             String s = System.getProperty("com.sun.javafx.virtualKeyboard.preload");

@@ -108,170 +130,65 @@
 
     /**
      * Specifies whether we ought to show handles. We should do it on touch platforms, but not
      * iOS (and maybe not Android either?)
      */
-    protected static final boolean SHOW_HANDLES = IS_TOUCH_SUPPORTED && !PlatformUtil.isIOS();
-
-    protected final ObservableObjectValue<FontMetrics> fontMetrics;
-
-    /**
-     * The fill to use for the text under normal conditions
-     */
-    protected final ObjectProperty<Paint> textFill = new StyleableObjectProperty<Paint>(Color.BLACK) {
-        @Override public Object getBean() {
-            return TextInputControlSkin.this;
-        }
-
-        @Override public String getName() {
-            return "textFill";
-        }
-
-        @Override public CssMetaData<TextInputControl,Paint> getCssMetaData() {
-            return StyleableProperties.TEXT_FILL;
-        }
-    };
-    
-    protected final ObjectProperty<Paint> promptTextFill = new StyleableObjectProperty<Paint>(Color.GRAY) {
-        @Override public Object getBean() {
-            return TextInputControlSkin.this;
-        }
-
-        @Override public String getName() {
-            return "promptTextFill";
-        }
-
-        @Override public CssMetaData<TextInputControl,Paint> getCssMetaData() {
-            return StyleableProperties.PROMPT_TEXT_FILL;
-        }
-    };
-    
-    /**
-     * The fill to use for the text when highlighted.
-     */
-    protected final ObjectProperty<Paint> highlightFill = new StyleableObjectProperty<Paint>(Color.DODGERBLUE) {
-        @Override protected void invalidated() {
-            updateHighlightFill();
-        }
+    static final boolean SHOW_HANDLES = Properties.IS_TOUCH_SUPPORTED && !PlatformUtil.isIOS();
 
-        @Override public Object getBean() {
-            return TextInputControlSkin.this;
-        }
-
-        @Override public String getName() {
-            return "highlightFill";
-        }
-
-        @Override public CssMetaData<TextInputControl,Paint> getCssMetaData() {
-            return StyleableProperties.HIGHLIGHT_FILL;
-        }
-    };
-    
-    protected final ObjectProperty<Paint> highlightTextFill = new StyleableObjectProperty<Paint>(Color.WHITE) {
-        @Override protected void invalidated() {
-            updateHighlightTextFill();
-        }
-
-        @Override public Object getBean() {
-            return TextInputControlSkin.this;
-        }
-
-        @Override public String getName() {
-            return "highlightTextFill";
-        }
-
-        @Override public CssMetaData<TextInputControl,Paint> getCssMetaData() {
-            return StyleableProperties.HIGHLIGHT_TEXT_FILL;
-        }
-    };
-    
-    protected final BooleanProperty displayCaret = new StyleableBooleanProperty(true) {
-        @Override public Object getBean() {
-            return TextInputControlSkin.this;
-        }
-
-        @Override public String getName() {
-            return "displayCaret";
-        }
-
-        @Override public CssMetaData<TextInputControl,Boolean> getCssMetaData() {
-            return StyleableProperties.DISPLAY_CARET;
-        }
-    };
+    private final static boolean IS_FXVK_SUPPORTED = Platform.isSupported(ConditionalFeature.VIRTUAL_KEYBOARD);
 
-    private BooleanProperty forwardBias = new SimpleBooleanProperty(this, "forwardBias", true);
-    public BooleanProperty forwardBiasProperty() {
-        return forwardBias;
-    }
-    public void setForwardBias(boolean isLeading) {
-        forwardBias.set(isLeading);
-    }
-    public boolean isForwardBias() {
-        return forwardBias.get();
-    }
+    /**************************************************************************
+     *
+     * Private fields
+     *
+     **************************************************************************/
 
-    private BooleanProperty blink = new SimpleBooleanProperty(this, "blink", true);
-    protected ObservableBooleanValue caretVisible;
-    private CaretBlinking caretBlinking = new CaretBlinking(blink);
+    final ObservableObjectValue<FontMetrics> fontMetrics;
+    private ObservableBooleanValue caretVisible;
+    private CaretBlinking caretBlinking = new CaretBlinking(blinkProperty());
 
     /**
      * A path, provided by the textNode, which represents the caret.
      * I assume this has to be updated whenever the caretPosition
      * changes. Perhaps more frequently (including text changes),
      * but I'm not sure.
      */
-    protected final Path caretPath = new Path();
+    final Path caretPath = new Path();
 
-    protected StackPane caretHandle = null;
-    protected StackPane selectionHandle1 = null;
-    protected StackPane selectionHandle2 = null;
+    StackPane caretHandle = null;
+    StackPane selectionHandle1 = null;
+    StackPane selectionHandle2 = null;
 
-    public Point2D getMenuPosition() {
-        if (SHOW_HANDLES) {
-            if (caretHandle.isVisible()) {
-                return new Point2D(caretHandle.getLayoutX() + caretHandle.getWidth() / 2,
-                                   caretHandle.getLayoutY());
-            } else if (selectionHandle1.isVisible() && selectionHandle2.isVisible()) {
-                return new Point2D((selectionHandle1.getLayoutX() + selectionHandle1.getWidth() / 2 +
-                                    selectionHandle2.getLayoutX() + selectionHandle2.getWidth() / 2) / 2,
-                                   selectionHandle2.getLayoutY() + selectionHandle2.getHeight() / 2);
-            } else {
-                return null;
-            }
-        } else {
-            throw new UnsupportedOperationException();
-        }
-    }
+    // Start/Length of the text under input method composition
+    private int imstart;
+    private int imlength;
+    // Holds concrete attributes for the composition runs
+    private List<Shape> imattrs = new java.util.ArrayList<Shape>();
 
 
-    private final static boolean IS_FXVK_SUPPORTED = Platform.isSupported(ConditionalFeature.VIRTUAL_KEYBOARD);
-    private static boolean USE_FXVK = IS_FXVK_SUPPORTED;
-
-    /* For testing only */
-    static int vkType = -1;
-    public void toggleUseVK() {
-        vkType++;
-        if (vkType < 4) {
-            USE_FXVK = true;
-            getSkinnable().getProperties().put(FXVK.VK_TYPE_PROP_KEY, FXVK.VK_TYPE_NAMES[vkType]);
-            FXVK.attach(getSkinnable());
-        } else {
-            FXVK.detach();
-            vkType = -1;
-            USE_FXVK = false;
-        }
-    }
 
+    /**************************************************************************
+     *
+     * Constructors
+     *
+     **************************************************************************/
 
-    public TextInputControlSkin(final T textInput, final B behavior) {
-        super(textInput, behavior);
+    /**
+     * Creates a new instance of TextInputControlSkin, although note that this
+     * instance does not handle any behavior / input mappings - this needs to be
+     * handled appropriately by subclasses.
+     *
+     * @param control The control that this skin should be installed onto.
+     */
+    public TextInputControlSkin(final T control) {
+        super(control);
 
         fontMetrics = new ObjectBinding<FontMetrics>() {
-            { bind(textInput.fontProperty()); }
+            { bind(control.fontProperty()); }
             @Override protected FontMetrics computeValue() {
                 invalidateMetrics();
-                return Toolkit.getToolkit().getFontLoader().getFontMetrics(textInput.getFont());
+                return Toolkit.getToolkit().getFontLoader().getFontMetrics(control.getFont());
             }
         };
 
         /**
          * The caret is visible when the text box is focused AND when the selection

@@ -280,18 +197,18 @@
          * performing some operations such as most key strokes. In that case we
          * simply toggle its opacity.
          * <p>
          */
         caretVisible = new BooleanBinding() {
-            { bind(textInput.focusedProperty(), textInput.anchorProperty(), textInput.caretPositionProperty(),
-                    textInput.disabledProperty(), textInput.editableProperty(), displayCaret, blink);}
+            { bind(control.focusedProperty(), control.anchorProperty(), control.caretPositionProperty(),
+                    control.disabledProperty(), control.editableProperty(), displayCaret, blinkProperty());}
             @Override protected boolean computeValue() {
                 // RT-10682: On Windows, we show the caret during selection, but on others we hide it
-                return !blink.get() && displayCaret.get() && textInput.isFocused() &&
-                        (isWindows() || (textInput.getCaretPosition() == textInput.getAnchor())) &&
-                        !textInput.isDisabled() &&
-                        textInput.isEditable();
+                return !blinkProperty().get() && displayCaret.get() && control.isFocused() &&
+                        (isWindows() || (control.getCaretPosition() == control.getAnchor())) &&
+                        !control.isDisabled() &&
+                        control.isEditable();
             }
         };
 
         if (SHOW_HANDLES) {
             caretHandle      = new StackPane();

@@ -301,40 +218,40 @@
             caretHandle.setManaged(false);
             selectionHandle1.setManaged(false);
             selectionHandle2.setManaged(false);
 
             caretHandle.visibleProperty().bind(new BooleanBinding() {
-                { bind(textInput.focusedProperty(), textInput.anchorProperty(),
-                       textInput.caretPositionProperty(), textInput.disabledProperty(),
-                       textInput.editableProperty(), textInput.lengthProperty(), displayCaret);}
+                { bind(control.focusedProperty(), control.anchorProperty(),
+                        control.caretPositionProperty(), control.disabledProperty(),
+                        control.editableProperty(), control.lengthProperty(), displayCaret);}
                 @Override protected boolean computeValue() {
-                    return (displayCaret.get() && textInput.isFocused() &&
-                            textInput.getCaretPosition() == textInput.getAnchor() &&
-                            !textInput.isDisabled() && textInput.isEditable() &&
-                            textInput.getLength() > 0);
+                    return (displayCaret.get() && control.isFocused() &&
+                            control.getCaretPosition() == control.getAnchor() &&
+                            !control.isDisabled() && control.isEditable() &&
+                            control.getLength() > 0);
                 }
             });
 
 
             selectionHandle1.visibleProperty().bind(new BooleanBinding() {
-                { bind(textInput.focusedProperty(), textInput.anchorProperty(), textInput.caretPositionProperty(),
-                       textInput.disabledProperty(), displayCaret);}
+                { bind(control.focusedProperty(), control.anchorProperty(), control.caretPositionProperty(),
+                        control.disabledProperty(), displayCaret);}
                 @Override protected boolean computeValue() {
-                    return (displayCaret.get() && textInput.isFocused() &&
-                            textInput.getCaretPosition() != textInput.getAnchor() &&
-                            !textInput.isDisabled());
+                    return (displayCaret.get() && control.isFocused() &&
+                            control.getCaretPosition() != control.getAnchor() &&
+                            !control.isDisabled());
                 }
             });
 
 
             selectionHandle2.visibleProperty().bind(new BooleanBinding() {
-                { bind(textInput.focusedProperty(), textInput.anchorProperty(), textInput.caretPositionProperty(),
-                       textInput.disabledProperty(), displayCaret);}
+                { bind(control.focusedProperty(), control.anchorProperty(), control.caretPositionProperty(),
+                        control.disabledProperty(), displayCaret);}
                 @Override protected boolean computeValue() {
-                    return (displayCaret.get() && textInput.isFocused() &&
-                            textInput.getCaretPosition() != textInput.getAnchor() &&
-                            !textInput.isDisabled());
+                    return (displayCaret.get() && control.isFocused() &&
+                            control.getCaretPosition() != control.getAnchor() &&
+                            !control.isDisabled());
                 }
             });
 
 
             caretHandle.getStyleClass().setAll("caret-handle");

@@ -345,23 +262,23 @@
             selectionHandle2.setId("selection-handle-2");
         }
 
         if (IS_FXVK_SUPPORTED) {
             if (preload) {
-                Scene scene = textInput.getScene();
+                Scene scene = control.getScene();
                 if (scene != null) {
                     Window window = scene.getWindow();
                     if (window != null) {
-                        FXVK.init(textInput);
+                        FXVK.init(control);
                     }
                 }
             }
-            textInput.focusedProperty().addListener(observable -> {
-                if (USE_FXVK) {
+            control.focusedProperty().addListener(observable -> {
+                if (FXVK.useFXVK()) {
                     Scene scene = getSkinnable().getScene();
-                    if (textInput.isEditable() && textInput.isFocused()) {
-                        FXVK.attach(textInput);
+                    if (control.isEditable() && control.isFocused()) {
+                        FXVK.attach(control);
                     } else if (scene == null ||
                                scene.getWindow() == null ||
                                !scene.getWindow().isFocused() ||
                                !(scene.getFocusOwner() instanceof TextInputControl &&
                                  ((TextInputControl)scene.getFocusOwner()).isEditable())) {

@@ -369,99 +286,355 @@
                     }
                 }
             });
         }
 
-        if (textInput.getOnInputMethodTextChanged() == null) {
-            textInput.setOnInputMethodTextChanged(event -> {
+        if (control.getOnInputMethodTextChanged() == null) {
+            control.setOnInputMethodTextChanged(event -> {
                 handleInputMethodEvent(event);
             });
         }
 
-        textInput.setInputMethodRequests(new ExtendedInputMethodRequests() {
+        control.setInputMethodRequests(new ExtendedInputMethodRequests() {
             @Override public Point2D getTextLocation(int offset) {
                 Scene scene = getSkinnable().getScene();
                 Window window = scene.getWindow();
                 // Don't use imstart here because it isn't initialized yet.
-                Rectangle2D characterBounds = getCharacterBounds(textInput.getSelection().getStart() + offset);
+                Rectangle2D characterBounds = getCharacterBounds(control.getSelection().getStart() + offset);
                 Point2D p = getSkinnable().localToScene(characterBounds.getMinX(), characterBounds.getMaxY());
                 Point2D location = new Point2D(window.getX() + scene.getX() + p.getX(),
                                                window.getY() + scene.getY() + p.getY());
                 return location;
             }
 
-            @Override
-            public int getLocationOffset(int x, int y) {
+            @Override public int getLocationOffset(int x, int y) {
                 return getInsertionPoint(x, y);
             }
 
-            @Override
-            public void cancelLatestCommittedText() {
+            @Override public void cancelLatestCommittedText() {
                 // TODO
             }
 
-            @Override
-            public String getSelectedText() {
-                TextInputControl textInput = getSkinnable();
-                IndexRange selection = textInput.getSelection();
+            @Override public String getSelectedText() {
+                TextInputControl control = getSkinnable();
+                IndexRange selection = control.getSelection();
 
-                return textInput.getText(selection.getStart(), selection.getEnd());
+                return control.getText(selection.getStart(), selection.getEnd());
             }
 
-            @Override
-            public int getInsertPositionOffset() {
+            @Override public int getInsertPositionOffset() {
                 int caretPosition = getSkinnable().getCaretPosition();
                 if (caretPosition < imstart) {
                     return caretPosition;
                 } else if (caretPosition < imstart + imlength) {
                     return imstart;
                 } else {
                     return caretPosition - imlength;
                 }
             }
 
-            @Override
-            public String getCommittedText(int begin, int end) {
-                TextInputControl textInput = getSkinnable();
+            @Override public String getCommittedText(int begin, int end) {
+                TextInputControl control = getSkinnable();
                 if (begin < imstart) {
                     if (end <= imstart) {
-                        return textInput.getText(begin, end);
+                        return control.getText(begin, end);
                     } else {
-                        return textInput.getText(begin, imstart) + textInput.getText(imstart + imlength, end + imlength);
+                        return control.getText(begin, imstart) + control.getText(imstart + imlength, end + imlength);
                     }
                 } else {
-                    return textInput.getText(begin + imlength, end + imlength);
+                    return control.getText(begin + imlength, end + imlength);
                 }
             }
 
-            @Override
-            public int getCommittedTextLength() {
+            @Override public int getCommittedTextLength() {
                 return getSkinnable().getText().length() - imlength;
             }
         });
     }
 
-    // For use with PasswordField in TextFieldSkin
-    protected String maskText(String txt) {
-        return txt;
+
+
+    /**************************************************************************
+     *
+     * Properties
+     *
+     **************************************************************************/
+
+    // --- blink
+    private BooleanProperty blink;
+    private final void setBlink(boolean value) {
+        blinkProperty().set(value);
+    }
+    private final boolean isBlink() {
+        return blinkProperty().get();
+    }
+    private final BooleanProperty blinkProperty() {
+        if (blink == null) {
+            blink = new SimpleBooleanProperty(this, "blink", true);
+        }
+        return blink;
+    }
+
+    // --- text fill
+    /**
+     * The fill to use for the text under normal conditions
+     */
+    private final ObjectProperty<Paint> textFill = new StyleableObjectProperty<Paint>(Color.BLACK) {
+        @Override protected void invalidated() {
+            updateTextFill();
+        }
+
+        @Override public Object getBean() {
+            return TextInputControlSkin.this;
     }
 
+        @Override public String getName() {
+            return "textFill";
+        }
+
+        @Override public CssMetaData<TextInputControl,Paint> getCssMetaData() {
+            return StyleableProperties.TEXT_FILL;
+        }
+    };
  
     /**
-     * Returns the character at a given offset.
+     * The fill {@code Paint} used for the foreground text color.
+     */
+    protected final void setTextFill(Paint value) {
+        textFill.set(value);
+    }
+    protected final Paint getTextFill() {
+        return textFill.get();
+    }
+    protected final ObjectProperty<Paint> textFillProperty() {
+        return textFill;
+    }
+
+    // --- prompt text fill
+    private final ObjectProperty<Paint> promptTextFill = new StyleableObjectProperty<Paint>(Color.GRAY) {
+        @Override public Object getBean() {
+            return TextInputControlSkin.this;
+        }
+
+        @Override public String getName() {
+            return "promptTextFill";
+        }
+
+        @Override public CssMetaData<TextInputControl,Paint> getCssMetaData() {
+            return StyleableProperties.PROMPT_TEXT_FILL;
+        }
+    };
+
+    /**
+     * The fill {@code Paint} used for the foreground prompt text color.
+     */
+    protected final void setPromptTextFill(Paint value) {
+        promptTextFill.set(value);
+    }
+    protected final Paint getPromptTextFill() {
+        return promptTextFill.get();
+    }
+    protected final ObjectProperty<Paint> promptTextFillProperty() {
+        return promptTextFill;
+    }
+
+    // --- hightlight fill
+    /**
+     * The fill to use for the text when highlighted.
+     */
+    private final ObjectProperty<Paint> highlightFill = new StyleableObjectProperty<Paint>(Color.DODGERBLUE) {
+        @Override protected void invalidated() {
+            updateHighlightFill();
+        }
+
+        @Override public Object getBean() {
+            return TextInputControlSkin.this;
+        }
+
+        @Override public String getName() {
+            return "highlightFill";
+        }
+
+        @Override public CssMetaData<TextInputControl,Paint> getCssMetaData() {
+            return StyleableProperties.HIGHLIGHT_FILL;
+        }
+    };
+
+    /**
+     * The fill {@code Paint} used for the background of selected text.
+     */
+    protected final void setHighlightFill(Paint value) {
+        highlightFill.set(value);
+    }
+    protected final Paint getHighlightFill() {
+        return highlightFill.get();
+    }
+    protected final ObjectProperty<Paint> highlightFillProperty() {
+        return highlightFill;
+    }
+
+    // --- highlight text fill
+    private final ObjectProperty<Paint> highlightTextFill = new StyleableObjectProperty<Paint>(Color.WHITE) {
+        @Override protected void invalidated() {
+            updateHighlightTextFill();
+        }
+
+        @Override public Object getBean() {
+            return TextInputControlSkin.this;
+        }
+
+        @Override public String getName() {
+            return "highlightTextFill";
+        }
+
+        @Override public CssMetaData<TextInputControl,Paint> getCssMetaData() {
+            return StyleableProperties.HIGHLIGHT_TEXT_FILL;
+        }
+    };
+
+    /**
+     * The fill {@code Paint} used for the foreground of selected text.
+     */
+    protected final void setHighlightTextFill(Paint value) {
+        highlightTextFill.set(value);
+    }
+    protected final Paint getHighlightTextFill() {
+        return highlightTextFill.get();
+    }
+    protected final ObjectProperty<Paint> highlightTextFillProperty() {
+        return highlightTextFill;
+    }
+
+    // --- display caret
+    private final BooleanProperty displayCaret = new StyleableBooleanProperty(true) {
+        @Override public Object getBean() {
+            return TextInputControlSkin.this;
+        }
+
+        @Override public String getName() {
+            return "displayCaret";
+        }
+
+        @Override public CssMetaData<TextInputControl,Boolean> getCssMetaData() {
+            return StyleableProperties.DISPLAY_CARET;
+        }
+    };
+
+    private final void setDisplayCaret(boolean value) {
+        displayCaret.set(value);
+    }
+    private final boolean isDisplayCaret() {
+        return displayCaret.get();
+    }
+    private final BooleanProperty displayCaretProperty() {
+        return displayCaret;
+    }
+
+
+    /**
+     * Caret bias in the content. true means a bias towards forward character
+     * (true=leading/false=trailing)
+     */
+    private BooleanProperty forwardBias = new SimpleBooleanProperty(this, "forwardBias", true);
+    protected final BooleanProperty forwardBiasProperty() {
+        return forwardBias;
+    }
+    // Public for behavior
+    public final void setForwardBias(boolean isLeading) {
+        forwardBias.set(isLeading);
+    }
+    protected final boolean isForwardBias() {
+        return forwardBias.get();
+    }
+
+
+
+    /**************************************************************************
      *
-     * @param index
+     * Abstract API
+     *
+     **************************************************************************/
+
+    /**
+     * @return the path elements describing the shape of the underline for the given range.
+     */
+    protected abstract PathElement[] getUnderlineShape(int start, int end);
+    /**
+     * @return the path elements describing the bounding rectangles for the given range of text.
+     */
+    protected abstract PathElement[] getRangeShape(int start, int end);
+    /**
+     * Adds highlight for composed text from Input Method.
+     */
+    protected abstract void addHighlight(List<? extends Node> nodes, int start);
+    /**
+     * Removes highlight for composed text from Input Method.
+     */
+    protected abstract void removeHighlight(List<? extends Node> nodes);
+
+    // Public for behavior
+    /**
+     * Moves the caret by one of the given text unit, in the given
+     * direction. Note that only certain combinations are valid,
+     * depending on the implementing subclass.
+     *
+     * @param unit the unit of text to move by.
+     * @param dir the direction of movement.
+     * @param select whether to extends the selection to the new posititon.
+     */
+    public abstract void moveCaret(TextUnit unit, Direction dir, boolean select);
+
+    /**************************************************************************
+     *
+     * Public API
+     *
+     **************************************************************************/
+
+
+    // Public for behavior
+    /**
+     * Returns the position to be used for a context menu, based on the location
+     * of the caret handle or selection handles. This is supported only on touch
+     * displays and does not use the location of the mouse.
+     */
+    public Point2D getMenuPosition() {
+        if (SHOW_HANDLES) {
+            if (caretHandle.isVisible()) {
+                return new Point2D(caretHandle.getLayoutX() + caretHandle.getWidth() / 2,
+                                   caretHandle.getLayoutY());
+            } else if (selectionHandle1.isVisible() && selectionHandle2.isVisible()) {
+                return new Point2D((selectionHandle1.getLayoutX() + selectionHandle1.getWidth() / 2 +
+                                    selectionHandle2.getLayoutX() + selectionHandle2.getWidth() / 2) / 2,
+                                   selectionHandle2.getLayoutY() + selectionHandle2.getHeight() / 2);
+            } else {
+                return null;
+            }
+        } else {
+            throw new UnsupportedOperationException();
+        }
+    }
+
+    // For use with PasswordField in TextFieldSkin
+    /**
+     * This method may be overridden by subclasses to replace the displayed
+     * characters without affecting the actual text content. This is used to
+     * display bullet characters in PasswordField.
+     *
+     * @param txt the content that may need to be masked.
+     * @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.
      */
-    public char getCharacter(int index) { return '\0'; }
+    protected String maskText(String txt) {
+        return txt;
+    }
 
     /**
      * Returns the insertion point for a given location.
      *
      * @param x
      * @param y
      */
-    public int getInsertionPoint(double x, double y) { return 0; }
+    protected int getInsertionPoint(double x, double y) { return 0; }
 
     /**
      * Returns the bounds of the character at a given index.
      *
      * @param index

@@ -471,24 +644,32 @@
     /**
      * Ensures that the character at a given index is visible.
      *
      * @param index
      */
-    public void scrollCharacterToVisible(int index) {}
+    protected void scrollCharacterToVisible(int index) {}
 
+    /**
+     * Invalidates cached min and pref sizes for the TextInputControl.
+     */
     protected void invalidateMetrics() {
     }
 
+    /**
+     * Called when textFill property changes.
+     */
     protected void updateTextFill() {};
+
+    /**
+     * Called when highlightFill property changes.
+     */
     protected void updateHighlightFill() {};
-    protected void updateHighlightTextFill() {};
 
-    // Start/Length of the text under input method composition
-    private int imstart;
-    private int imlength;
-    // Holds concrete attributes for the composition runs
-    private List<Shape> imattrs = new java.util.ArrayList<Shape>();
+    /**
+     * Called when highlightTextFill property changes.
+     */
+    protected void updateHighlightTextFill() {};
 
     protected void handleInputMethodEvent(InputMethodEvent event) {
         final TextInputControl textInput = getSkinnable();
         if (textInput.isEditable() && !textInput.textProperty().isBound() && !textInput.isDisabled()) {
 

@@ -535,15 +716,45 @@
                 }
             }
         }
     }
 
-    protected abstract PathElement[] getUnderlineShape(int start, int end);
-    protected abstract PathElement[] getRangeShape(int start, int end);
-    protected abstract void addHighlight(List<? extends Node> nodes, int start);
-    protected abstract void removeHighlight(List<? extends Node> nodes);
-    public abstract void nextCharacterVisually(boolean moveRight);
+    // Public for behavior
+    /**
+     * Starts or stops caret blinking. The behavior classes use this to temporarily
+     * pause blinking while user is typing or otherwise moving the caret.
+     *
+     * @param value whether caret should be blinking.
+     */
+    public void setCaretAnimating(boolean value) {
+        if (value) {
+            caretBlinking.start();
+        } else {
+            caretBlinking.stop();
+            blinkProperty().set(true);
+        }
+    }
+
+
+
+    /**************************************************************************
+     *
+     * Private implementation
+     *
+     **************************************************************************/
+
+    TextInputControlBehavior getBehavior() {
+        return null;
+    }
+
+    ObservableBooleanValue caretVisibleProperty() {
+        return caretVisible;
+    }
+
+    boolean isRTL() {
+        return (getSkinnable().getEffectiveNodeOrientation() == NodeOrientation.RIGHT_TO_LEFT);
+    };
 
     private void createInputMethodAttributes(InputMethodHighlight highlight, int start, int end) {
         double minX = 0f;
         double maxX = 0f;
         double minY = 0f;

@@ -605,44 +816,30 @@
                 }
             }
         }
     }
 
-    protected boolean isRTL() {
-        return (getSkinnable().getEffectiveNodeOrientation() == NodeOrientation.RIGHT_TO_LEFT);
-    };
 
-    public void setCaretAnimating(boolean value) {
-        if (value) {
-            caretBlinking.start();
-        } else {
-            caretBlinking.stop();
-            blink.set(true);
-        }
-    }
+
+    /**************************************************************************
+     *
+     * Support classes
+     *
+     **************************************************************************/
 
     private static final class CaretBlinking {
         private final Timeline caretTimeline;
         private final WeakReference<BooleanProperty> blinkPropertyRef;
 
         public CaretBlinking(final BooleanProperty blinkProperty) {
-            blinkPropertyRef =
-                    new WeakReference<BooleanProperty>(blinkProperty);
+            blinkPropertyRef = new WeakReference<>(blinkProperty);
 
             caretTimeline = new Timeline();
             caretTimeline.setCycleCount(Timeline.INDEFINITE);
             caretTimeline.getKeyFrames().addAll(
-                new KeyFrame(Duration.ZERO,
-                        event -> {
-                            setBlink(false);
-                        }
-                ),
-                new KeyFrame(Duration.seconds(.5),
-                        event -> {
-                            setBlink(true);
-                        }
-                ),
+                new KeyFrame(Duration.ZERO, e -> setBlink(false)),
+                new KeyFrame(Duration.seconds(.5), e -> setBlink(true)),
                 new KeyFrame(Duration.seconds(1)));
         }
 
         public void start() {
             caretTimeline.play();

@@ -661,154 +858,88 @@
 
             blinkProperty.set(value);
         }
     }
 
-    class ContextMenuItem extends MenuItem {
-        ContextMenuItem(final String action) {
-            super(getString("TextInputControl.menu." + action));
-            setOnAction(e -> {
-                getBehavior().callAction(action);
-            });
-        }
-    }
-
-    final MenuItem undoMI   = new ContextMenuItem("Undo");
-    final MenuItem redoMI   = new ContextMenuItem("Redo");
-    final MenuItem cutMI    = new ContextMenuItem("Cut");
-    final MenuItem copyMI   = new ContextMenuItem("Copy");
-    final MenuItem pasteMI  = new ContextMenuItem("Paste");
-    final MenuItem deleteMI = new ContextMenuItem("DeleteSelection");
-    final MenuItem selectWordMI = new ContextMenuItem("SelectWord");
-    final MenuItem selectAllMI = new ContextMenuItem("SelectAll");
-    final MenuItem separatorMI = new SeparatorMenuItem();
-
-    public void populateContextMenu(ContextMenu contextMenu) {
-        TextInputControl textInputControl = getSkinnable();
-        boolean editable = textInputControl.isEditable();
-        boolean hasText = (textInputControl.getLength() > 0);
-        boolean hasSelection = (textInputControl.getSelection().getLength() > 0);
-        boolean maskText = (maskText("A") != "A");
-        ObservableList<MenuItem> items = contextMenu.getItems();
-
-        if (SHOW_HANDLES) {
-            items.clear();
-            if (!maskText && hasSelection) {
-                if (editable) {
-                    items.add(cutMI);
-                }
-                items.add(copyMI);
-            }
-            if (editable && Clipboard.getSystemClipboard().hasString()) {
-                items.add(pasteMI);
-            }
-            if (hasText) {
-                if (!hasSelection) {
-                    items.add(selectWordMI);
-                }
-                items.add(selectAllMI);
-            }
-            selectWordMI.getProperties().put("refreshMenu", Boolean.TRUE);
-            selectAllMI.getProperties().put("refreshMenu", Boolean.TRUE);
-        } else {
-            if (editable) {
-                items.setAll(undoMI, redoMI, cutMI, copyMI, pasteMI, deleteMI,
-                             separatorMI, selectAllMI);
-            } else {
-                items.setAll(copyMI, separatorMI, selectAllMI);
-            }
-            undoMI.setDisable(!getSkinnable().isUndoable());
-            redoMI.setDisable(!getSkinnable().isRedoable());
-            cutMI.setDisable(maskText || !hasSelection);
-            copyMI.setDisable(maskText || !hasSelection);
-            pasteMI.setDisable(!Clipboard.getSystemClipboard().hasString());
-            deleteMI.setDisable(!hasSelection);
-        }
-    }
 
     private static class StyleableProperties {
         private static final CssMetaData<TextInputControl,Paint> TEXT_FILL =
             new CssMetaData<TextInputControl,Paint>("-fx-text-fill",
                 PaintConverter.getInstance(), Color.BLACK) {
 
-            @Override
-            public boolean isSettable(TextInputControl n) {
-                final TextInputControlSkin<?,?> skin = (TextInputControlSkin<?,?>) n.getSkin();
+            @Override public boolean isSettable(TextInputControl n) {
+                final TextInputControlSkin<?> skin = (TextInputControlSkin<?>) n.getSkin();
                 return skin.textFill == null || !skin.textFill.isBound();
             }
 
             @Override @SuppressWarnings("unchecked") 
             public StyleableProperty<Paint> getStyleableProperty(TextInputControl n) {
-                final TextInputControlSkin<?,?> skin = (TextInputControlSkin<?,?>) n.getSkin();                
+                final TextInputControlSkin<?> skin = (TextInputControlSkin<?>) n.getSkin();                
                 return (StyleableProperty<Paint>)skin.textFill;
             }
         };
        
         private static final CssMetaData<TextInputControl,Paint> PROMPT_TEXT_FILL =
             new CssMetaData<TextInputControl,Paint>("-fx-prompt-text-fill",
                 PaintConverter.getInstance(), Color.GRAY) {
 
-            @Override
-            public boolean isSettable(TextInputControl n) {
-                final TextInputControlSkin<?,?> skin = (TextInputControlSkin<?,?>) n.getSkin();
+            @Override public boolean isSettable(TextInputControl n) {
+                final TextInputControlSkin<?> skin = (TextInputControlSkin<?>) n.getSkin();
                 return skin.promptTextFill == null || !skin.promptTextFill.isBound();
             }
 
             @Override @SuppressWarnings("unchecked") 
             public StyleableProperty<Paint> getStyleableProperty(TextInputControl n) {
-                final TextInputControlSkin<?,?> skin = (TextInputControlSkin<?,?>) n.getSkin();
+                final TextInputControlSkin<?> skin = (TextInputControlSkin<?>) n.getSkin();
                 return (StyleableProperty<Paint>)skin.promptTextFill;
             }
         };
         
         private static final CssMetaData<TextInputControl,Paint> HIGHLIGHT_FILL =
             new CssMetaData<TextInputControl,Paint>("-fx-highlight-fill",
                 PaintConverter.getInstance(), Color.DODGERBLUE) {
 
-            @Override
-            public boolean isSettable(TextInputControl n) {
-                final TextInputControlSkin<?,?> skin = (TextInputControlSkin<?,?>) n.getSkin();
+            @Override public boolean isSettable(TextInputControl n) {
+                final TextInputControlSkin<?> skin = (TextInputControlSkin<?>) n.getSkin();
                 return skin.highlightFill == null || !skin.highlightFill.isBound();
             }
 
             @Override @SuppressWarnings("unchecked") 
             public StyleableProperty<Paint> getStyleableProperty(TextInputControl n) {
-                final TextInputControlSkin<?,?> skin = (TextInputControlSkin<?,?>) n.getSkin();
+                final TextInputControlSkin<?> skin = (TextInputControlSkin<?>) n.getSkin();
                 return (StyleableProperty<Paint>)skin.highlightFill;
             }
         };
         
         private static final CssMetaData<TextInputControl,Paint> HIGHLIGHT_TEXT_FILL =
             new CssMetaData<TextInputControl,Paint>("-fx-highlight-text-fill",
                 PaintConverter.getInstance(), Color.WHITE) {
 
-            @Override
-            public boolean isSettable(TextInputControl n) {
-                final TextInputControlSkin<?,?> skin = (TextInputControlSkin<?,?>) n.getSkin();
+            @Override public boolean isSettable(TextInputControl n) {
+                final TextInputControlSkin<?> skin = (TextInputControlSkin<?>) n.getSkin();
                 return skin.highlightTextFill == null || !skin.highlightTextFill.isBound();
             }
 
             @Override @SuppressWarnings("unchecked") 
             public StyleableProperty<Paint> getStyleableProperty(TextInputControl n) {
-                final TextInputControlSkin<?,?> skin = (TextInputControlSkin<?,?>) n.getSkin();
+                final TextInputControlSkin<?> skin = (TextInputControlSkin<?>) n.getSkin();
                 return (StyleableProperty<Paint>)skin.highlightTextFill;
             }
         };
         
         private static final CssMetaData<TextInputControl,Boolean> DISPLAY_CARET =
             new CssMetaData<TextInputControl,Boolean>("-fx-display-caret",
                 BooleanConverter.getInstance(), Boolean.TRUE) {
 
-            @Override
-            public boolean isSettable(TextInputControl n) {
-                final TextInputControlSkin<?,?> skin = (TextInputControlSkin<?,?>) n.getSkin();
+            @Override public boolean isSettable(TextInputControl n) {
+                final TextInputControlSkin<?> skin = (TextInputControlSkin<?>) n.getSkin();
                 return skin.displayCaret == null || !skin.displayCaret.isBound();
             }
 
             @Override @SuppressWarnings("unchecked") 
             public StyleableProperty<Boolean> getStyleableProperty(TextInputControl n) {
-                final TextInputControlSkin<?,?> skin = (TextInputControlSkin<?,?>) n.getSkin();
+                final TextInputControlSkin<?> skin = (TextInputControlSkin<?>) n.getSkin();
                 return (StyleableProperty<Boolean>)skin.displayCaret;
             }
         };
 
         private static final List<CssMetaData<? extends Styleable, ?>> STYLEABLES;

@@ -824,26 +955,25 @@
             STYLEABLES = Collections.unmodifiableList(styleables);
         }
     }
 
     /**
-     * @return The CssMetaData associated with this class, which may include the
+     * Returns the CssMetaData associated with this class, which may include the
      * CssMetaData of its super classes.
      */
     public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
         return StyleableProperties.STYLEABLES;
     }
 
     /**
      * {@inheritDoc}
      */
-    @Override
-    public List<CssMetaData<? extends Styleable, ?>> getCssMetaData() {
+    @Override public List<CssMetaData<? extends Styleable, ?>> getCssMetaData() {
         return getClassCssMetaData();
     }
 
-    protected void executeAccessibleAction(AccessibleAction action, Object... parameters) {
+    @Override protected void executeAccessibleAction(AccessibleAction action, Object... parameters) {
         switch (action) {
             case SHOW_TEXT_RANGE: {
                 Integer start = (Integer)parameters[0];
                 Integer end = (Integer)parameters[1];
                 if (start != null && end != null) {

@@ -854,6 +984,53 @@
                 break;
             } 
             default: super.executeAccessibleAction(action, parameters);
         }
     }
+
+    /**
+     * This class represents the hit information for a Text node.
+     */
+    public static class TextPosInfo {
+
+        TextPosInfo(HitInfo hit) {
+            this(hit.getCharIndex(), hit.isLeading());
+        }
+
+        /**
+         * Create a TextPosInfo object representing a text index and forward bias.
+         *
+         * @param charIndex the character index.
+         * @param leading whether the hit is on the leading edge of the character. If it is false, it represents the trailing edge.
+         */
+        public TextPosInfo(int charIndex, boolean leading) {
+            setCharIndex(charIndex);
+            setLeading(leading);
+        }
+
+        /**
+         * The index of the character which this hit information refers to.
+         */
+        private int charIndex;
+        public int getCharIndex() { return charIndex; }
+        void setCharIndex(int charIndex) { this.charIndex = charIndex; }
+
+        /**
+         * Indicates whether the hit is on the leading edge of the character.
+         * If it is false, it represents the trailing edge.
+         */
+        private boolean leading;
+        public boolean isLeading() { return leading; }
+        void setLeading(boolean leading) { this.leading = leading; }
+
+        /**
+         * Returns the index of the insertion position.
+         */
+        public int getInsertionIndex() {
+            return leading ? charIndex : charIndex + 1;
+        }
+
+        @Override public String toString() {
+            return "charIndex: " + charIndex + ", isLeading: " + leading;
+        }
+    }
 }