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

Print this page
rev 9240 : 8076423: JEP 253: Prepare JavaFX UI Controls & CSS APIs for Modularization

@@ -21,12 +21,16 @@
  * 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.behavior.BehaviorBase;
+import com.sun.javafx.scene.control.behavior.TextAreaBehavior;
+import com.sun.javafx.scene.control.behavior.TextInputControlBehavior;
+import com.sun.javafx.scene.control.skin.Utils;
 import javafx.beans.binding.BooleanBinding;
 import javafx.beans.binding.DoubleBinding;
 import javafx.beans.binding.ObjectBinding;
 import javafx.beans.binding.StringBinding;
 import javafx.beans.property.DoubleProperty;

@@ -39,10 +43,13 @@
 import javafx.geometry.Point2D;
 import javafx.geometry.Rectangle2D;
 import javafx.scene.AccessibleAttribute;
 import javafx.scene.Group;
 import javafx.scene.Node;
+import javafx.scene.control.Accordion;
+import javafx.scene.control.Button;
+import javafx.scene.control.Control;
 import javafx.scene.control.IndexRange;
 import javafx.scene.control.PasswordField;
 import javafx.scene.control.TextField;
 import javafx.scene.input.MouseEvent;
 import javafx.scene.layout.Pane;

@@ -56,13 +63,25 @@
 import com.sun.javafx.scene.control.behavior.TextFieldBehavior;
 import com.sun.javafx.scene.control.behavior.PasswordFieldBehavior;
 import com.sun.javafx.scene.text.HitInfo;
 
 /**
- * Text field skin.
+ * Default skin implementation for the {@link TextField} control.
+ *
+ * @see TextField
+ * @since 9
  */
-public class TextFieldSkin extends TextInputControlSkin<TextField, TextFieldBehavior> {
+public class TextFieldSkin extends TextInputControlSkin<TextField> {
+
+    /**************************************************************************
+     *
+     * Private fields
+     *
+     **************************************************************************/
+
+    private final TextFieldBehavior behavior;
+
     /**
      * This group contains the text, caret, and selection rectangle.
      * It is clipped. The textNode, selectionHighlightPath, and
      * caret are each translated individually when horizontal
      * translation is needed to keep the caretPosition visible.

@@ -100,56 +119,57 @@
     private ObservableBooleanValue usePromptText;
     private DoubleProperty textTranslateX = new SimpleDoubleProperty(this, "textTranslateX");
     private double caretWidth;
 
     /**
-     * Function to translate the text control's "dot" into the caret
-     * position in the Text node.  This is possibly only meaningful for
-     * the PasswordField where the echoChar could be more than one
-     * character.
-     */
-    protected int translateCaretPosition(int cp) { return cp; }
-    protected Point2D translateCaretPosition(Point2D p) { return p; }
-
-    /**
      * Right edge of the text region sans padding
      */
-    protected ObservableDoubleValue textRight;
+    private ObservableDoubleValue textRight;
 
     private double pressX, pressY; // For dragging handles on embedded
 
     // For use with PasswordField
-    public static final char BULLET = '\u25cf';
+    static final char BULLET = '\u25cf';
 
-    /**
-     * Create a new TextFieldSkin.
-     * @param textField not null
-     */
-    public TextFieldSkin(final TextField textField) {
-        this(textField, (textField instanceof PasswordField)
-                                     ? new PasswordFieldBehavior((PasswordField)textField)
-                                     : new TextFieldBehavior(textField));
-    }
 
-    public TextFieldSkin(final TextField textField, final TextFieldBehavior behavior) {
-        super(textField, behavior);
-        behavior.setTextFieldSkin(this);
 
+    /**************************************************************************
+     *
+     * Constructors
+     *
+     **************************************************************************/
+
+    /**
+     * Creates a new TextFieldSkin instance, installing the necessary child
+     * nodes into the Control {@link Control#getChildren() children} list, as
+     * well as the necessary input mappings for handling key, mouse, etc events.
+     *
+     * @param control The control that this skin should be installed onto.
+     */
+    public TextFieldSkin(final TextField control) {
+        super(control);
 
-        textField.caretPositionProperty().addListener((observable, oldValue, newValue) -> {
-            if (textField.getWidth() > 0) {
-                updateTextNodeCaretPos(textField.getCaretPosition());
+        // install default input map for the text field control
+        this.behavior = (control instanceof PasswordField)
+                ? new PasswordFieldBehavior((PasswordField)control)
+                : new TextFieldBehavior(control);
+        this.behavior.setTextFieldSkin(this);
+//        control.setInputMap(behavior.getInputMap());
+
+        control.caretPositionProperty().addListener((observable, oldValue, newValue) -> {
+            if (control.getWidth() > 0) {
+                updateTextNodeCaretPos(control.getCaretPosition());
                 if (!isForwardBias()) {
                     setForwardBias(true);
                 }
                 updateCaretOff();
             }
         });
 
         forwardBiasProperty().addListener(observable -> {
-            if (textField.getWidth() > 0) {
-                updateTextNodeCaretPos(textField.getCaretPosition());
+            if (control.getWidth() > 0) {
+                updateTextNodeCaretPos(control.getCaretPosition());
                 updateCaretOff();
             }
         });
 
         textRight = new DoubleBinding() {

@@ -180,109 +200,109 @@
         }
 
         // Add text
         textNode.setManaged(false);
         textNode.getStyleClass().add("text");
-        textNode.fontProperty().bind(textField.fontProperty());
+        textNode.fontProperty().bind(control.fontProperty());
 
         textNode.layoutXProperty().bind(textTranslateX);
         textNode.textProperty().bind(new StringBinding() {
-            { bind(textField.textProperty()); }
+            { bind(control.textProperty()); }
             @Override protected String computeValue() {
-                return maskText(textField.textProperty().getValueSafe());
+                return maskText(control.textProperty().getValueSafe());
             }
         });
-        textNode.fillProperty().bind(textFill);
+        textNode.fillProperty().bind(textFillProperty());
         textNode.impl_selectionFillProperty().bind(new ObjectBinding<Paint>() {
-            { bind(highlightTextFill, textFill, textField.focusedProperty()); }
+            { bind(highlightTextFillProperty(), textFillProperty(), control.focusedProperty()); }
             @Override protected Paint computeValue() {
-                return textField.isFocused() ? highlightTextFill.get() : textFill.get();
+                return control.isFocused() ? highlightTextFillProperty().get() : textFillProperty().get();
             }
         });
         // updated by listener on caretPosition to ensure order
-        updateTextNodeCaretPos(textField.getCaretPosition());
-        textField.selectionProperty().addListener(observable -> {
+        updateTextNodeCaretPos(control.getCaretPosition());
+        control.selectionProperty().addListener(observable -> {
             updateSelection();
         });
 
         // Add selection
         selectionHighlightPath.setManaged(false);
         selectionHighlightPath.setStroke(null);
         selectionHighlightPath.layoutXProperty().bind(textTranslateX);
-        selectionHighlightPath.visibleProperty().bind(textField.anchorProperty().isNotEqualTo(textField.caretPositionProperty()).and(textField.focusedProperty()));
-        selectionHighlightPath.fillProperty().bind(highlightFill);
+        selectionHighlightPath.visibleProperty().bind(control.anchorProperty().isNotEqualTo(control.caretPositionProperty()).and(control.focusedProperty()));
+        selectionHighlightPath.fillProperty().bind(highlightFillProperty());
         textNode.impl_selectionShapeProperty().addListener(observable -> {
             updateSelection();
         });
 
         // Add caret
         caretPath.setManaged(false);
         caretPath.setStrokeWidth(1);
-        caretPath.fillProperty().bind(textFill);
-        caretPath.strokeProperty().bind(textFill);
+        caretPath.fillProperty().bind(textFillProperty());
+        caretPath.strokeProperty().bind(textFillProperty());
         
         // modifying visibility of the caret forces a layout-pass (RT-32373), so
         // instead we modify the opacity.
         caretPath.opacityProperty().bind(new DoubleBinding() {
-            { bind(caretVisible); }
+            { bind(caretVisibleProperty()); }
             @Override protected double computeValue() {
-                return caretVisible.get() ? 1.0 : 0.0;
+                return caretVisibleProperty().get() ? 1.0 : 0.0;
             }
         });
         caretPath.layoutXProperty().bind(textTranslateX);
         textNode.impl_caretShapeProperty().addListener(observable -> {
             caretPath.getElements().setAll(textNode.impl_caretShapeProperty().get());
             if (caretPath.getElements().size() == 0) {
                 // The caret pos is invalid.
-                updateTextNodeCaretPos(textField.getCaretPosition());
+                updateTextNodeCaretPos(control.getCaretPosition());
             } else if (caretPath.getElements().size() == 4) {
                 // The caret is split. Ignore and keep the previous width value.
             } else {
                 caretWidth = Math.round(caretPath.getLayoutBounds().getWidth());
             }
         });
 
         // Be sure to get the control to request layout when the font changes,
         // since this will affect the pref height and pref width.
-        textField.fontProperty().addListener(observable -> {
+        control.fontProperty().addListener(observable -> {
             // I do both so that any cached values for prefWidth/height are cleared.
             // The problem is that the skin is unmanaged and so calling request layout
             // doesn't walk up the tree all the way. I think....
-            textField.requestLayout();
+            control.requestLayout();
             getSkinnable().requestLayout();
         });
 
-        registerChangeListener(textField.prefColumnCountProperty(), "prefColumnCount");
-        if (textField.isFocused()) setCaretAnimating(true);
+        registerChangeListener(control.prefColumnCountProperty(), e -> getSkinnable().requestLayout());
+        if (control.isFocused()) setCaretAnimating(true);
 
-        textField.alignmentProperty().addListener(observable -> {
-            if (textField.getWidth() > 0) {
+        control.alignmentProperty().addListener(observable -> {
+            if (control.getWidth() > 0) {
                 updateTextPos();
                 updateCaretOff();
-                textField.requestLayout();
+                control.requestLayout();
             }
         });
 
         usePromptText = new BooleanBinding() {
-            { bind(textField.textProperty(),
-                   textField.promptTextProperty(),
-                   promptTextFill); }
+            { bind(control.textProperty(),
+                   control.promptTextProperty(),
+                   promptTextFillProperty()); }
             @Override protected boolean computeValue() {
-                String txt = textField.getText();
-                String promptTxt = textField.getPromptText();
+                String txt = control.getText();
+                String promptTxt = control.getPromptText();
                 return ((txt == null || txt.isEmpty()) &&
                         promptTxt != null && !promptTxt.isEmpty() &&
-                        !promptTextFill.get().equals(Color.TRANSPARENT));
+                        !getPromptTextFill().equals(Color.TRANSPARENT));
             }
         };
 
-        promptTextFill.addListener(observable -> {
+        promptTextFillProperty().addListener(observable -> {
             updateTextPos();
         });
 
-        textField.textProperty().addListener(observable -> {
-            if (!getBehavior().isEditing()) {
+        control.textProperty().addListener(observable -> {
+            if (!behavior.isEditing()) {
                 // Text changed, but not by user action
                 updateTextPos();
             }
         });
 

@@ -290,11 +310,11 @@
             createPromptNode();
         }
 
         usePromptText.addListener(observable -> {
             createPromptNode();
-            textField.requestLayout();
+            control.requestLayout();
         });
 
         if (SHOW_HANDLES) {
             selectionHandle1.setRotate(180);
 

@@ -309,349 +329,175 @@
             selectionHandle2.setOnMousePressed(handlePressHandler);
 
             caretHandle.setOnMouseDragged(e -> {
                 Point2D p = new Point2D(caretHandle.getLayoutX() + e.getX() + pressX - textNode.getLayoutX(),
                                         caretHandle.getLayoutY() + e.getY() - pressY - 6);
-                HitInfo hit = textNode.impl_hitTestChar(translateCaretPosition(p));
+                HitInfo hit = textNode.impl_hitTestChar(p);
                 positionCaret(hit, false);
                 e.consume();
             });
 
             selectionHandle1.setOnMouseDragged(new EventHandler<MouseEvent>() {
                 @Override public void handle(MouseEvent e) {
-                    TextField textField = getSkinnable();
+                    TextField control = getSkinnable();
                     Point2D tp = textNode.localToScene(0, 0);
                     Point2D p = new Point2D(e.getSceneX() - tp.getX() + 10/*??*/ - pressX + selectionHandle1.getWidth() / 2,
                                             e.getSceneY() - tp.getY() - pressY - 6);
-                    HitInfo hit = textNode.impl_hitTestChar(translateCaretPosition(p));
+                    HitInfo hit = textNode.impl_hitTestChar(p);
                     int pos = hit.getCharIndex();
-                    if (textField.getAnchor() < textField.getCaretPosition()) {
+                    if (control.getAnchor() < control.getCaretPosition()) {
                         // Swap caret and anchor
-                        textField.selectRange(textField.getCaretPosition(), textField.getAnchor());
+                        control.selectRange(control.getCaretPosition(), control.getAnchor());
                     }
                     if (pos >= 0) {
-                        if (pos >= textField.getAnchor() - 1) {
-                            hit.setCharIndex(Math.max(0, textField.getAnchor() - 1));
+                        if (pos >= control.getAnchor() - 1) {
+                            hit.setCharIndex(Math.max(0, control.getAnchor() - 1));
                         }
                         positionCaret(hit, true);
                     }
                     e.consume();
                 }
             });
 
             selectionHandle2.setOnMouseDragged(new EventHandler<MouseEvent>() {
                 @Override public void handle(MouseEvent e) {
-                    TextField textField = getSkinnable();
+                    TextField control = getSkinnable();
                     Point2D tp = textNode.localToScene(0, 0);
                     Point2D p = new Point2D(e.getSceneX() - tp.getX() + 10/*??*/ - pressX + selectionHandle2.getWidth() / 2,
                                             e.getSceneY() - tp.getY() - pressY - 6);
-                    HitInfo hit = textNode.impl_hitTestChar(translateCaretPosition(p));
+                    HitInfo hit = textNode.impl_hitTestChar(p);
                     int pos = hit.getCharIndex();
-                    if (textField.getAnchor() > textField.getCaretPosition()) {
+                    if (control.getAnchor() > control.getCaretPosition()) {
                         // Swap caret and anchor
-                        textField.selectRange(textField.getCaretPosition(), textField.getAnchor());
+                        control.selectRange(control.getCaretPosition(), control.getAnchor());
                     }
                     if (pos > 0) {
-                        if (pos <= textField.getAnchor()) {
-                            hit.setCharIndex(Math.min(textField.getAnchor() + 1, textField.getLength()));
+                        if (pos <= control.getAnchor()) {
+                            hit.setCharIndex(Math.min(control.getAnchor() + 1, control.getLength()));
                         }
                         positionCaret(hit, true);
                     }
                     e.consume();
                 }
             });
         }
     }
 
-    private void updateTextNodeCaretPos(int pos) {
-        if (pos == 0 || isForwardBias()) {
-            textNode.setImpl_caretPosition(pos);
-        } else {
-            textNode.setImpl_caretPosition(pos - 1);
-        }
-        textNode.impl_caretBiasProperty().set(isForwardBias());
-    }
 
-    private void createPromptNode() {
-        if (promptNode != null || !usePromptText.get()) return;
 
-        promptNode = new Text();
-        textGroup.getChildren().add(0, promptNode);
-        promptNode.setManaged(false);
-        promptNode.getStyleClass().add("text");
-        promptNode.visibleProperty().bind(usePromptText);
-        promptNode.fontProperty().bind(getSkinnable().fontProperty());
+    /***************************************************************************
+     *                                                                         *
+     * Public API                                                              *
+     *                                                                         *
+     **************************************************************************/
 
-        promptNode.textProperty().bind(getSkinnable().promptTextProperty());
-        promptNode.fillProperty().bind(promptTextFill);
-        updateSelection();
-    }
+    /** {@inheritDoc} */
+    @Override public void dispose() {
+        super.dispose();
 
-    private void updateSelection() {
-        TextField textField = getSkinnable();
-        IndexRange newValue = textField.getSelection();
-
-        if (newValue == null || newValue.getLength() == 0) {
-            textNode.impl_selectionStartProperty().set(-1);
-            textNode.impl_selectionEndProperty().set(-1);
-        } else {
-            textNode.impl_selectionStartProperty().set(newValue.getStart());
-            // This intermediate value is needed to force selection shape layout.
-            textNode.impl_selectionEndProperty().set(newValue.getStart());
-            textNode.impl_selectionEndProperty().set(newValue.getEnd());
+        if (behavior != null) {
+            behavior.dispose();
         }
-
-        PathElement[] elements = textNode.impl_selectionShapeProperty().get();
-        if (elements == null) {
-            selectionHighlightPath.getElements().clear();
-        } else {
-            selectionHighlightPath.getElements().setAll(elements);
         }
 
-        if (SHOW_HANDLES && newValue != null && newValue.getLength() > 0) {
-            int caretPos = textField.getCaretPosition();
-            int anchorPos = textField.getAnchor();
-
-            {
-                // Position the handle for the anchor. This could be handle1 or handle2.
-                // Do this before positioning the handle for the caret.
-                updateTextNodeCaretPos(anchorPos);
-                Bounds b = caretPath.getBoundsInParent();
-                if (caretPos < anchorPos) {
-                    selectionHandle2.setLayoutX(b.getMinX() - selectionHandle2.getWidth() / 2);
-                } else {
-                    selectionHandle1.setLayoutX(b.getMinX() - selectionHandle1.getWidth() / 2);
-                }
-            }
-
-            {
-                // Position handle for the caret. This could be handle1 or handle2.
-                updateTextNodeCaretPos(caretPos);
-                Bounds b = caretPath.getBoundsInParent();
-                if (caretPos < anchorPos) {
-                    selectionHandle1.setLayoutX(b.getMinX() - selectionHandle1.getWidth() / 2);
-                } else {
-                    selectionHandle2.setLayoutX(b.getMinX() - selectionHandle2.getWidth() / 2);
-                }
-            }
-        }
-    }
-
-    @Override protected void handleControlPropertyChanged(String propertyReference) {
-        if ("prefColumnCount".equals(propertyReference)) {
-            getSkinnable().requestLayout();
-        } else {
-            super.handleControlPropertyChanged(propertyReference);
-        }
-    }
-
-    @Override
-    protected double computePrefWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
+    /** {@inheritDoc} */
+    @Override protected double computePrefWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
         TextField textField = getSkinnable();
 
         double characterWidth = fontMetrics.get().computeStringWidth("W");
 
         int columnCount = textField.getPrefColumnCount();
 
         return columnCount * characterWidth + leftInset + rightInset;
     }
 
+    /** {@inheritDoc} */
     @Override protected double computeMinHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
         return computePrefHeight(width, topInset, rightInset, bottomInset, leftInset);
     }
 
+    /** {@inheritDoc} */
     @Override protected double computePrefHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
         return topInset + textNode.getLayoutBounds().getHeight() + bottomInset;
     }
 
+    /** {@inheritDoc} */
     @Override protected double computeMaxHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
         return getSkinnable().prefHeight(width);
     }
 
+    /** {@inheritDoc} */
     @Override public double computeBaselineOffset(double topInset, double rightInset, double bottomInset, double leftInset) {
         return topInset + textNode.getBaselineOffset();
     }
 
+    // Public for behavior
     /**
-     * Updates the textTranslateX value for the Text node position. This is
-     * done for general layout, but care is taken to avoid resetting the
-     * position when there's a need to scroll the text due to caret movement,
-     * or when editing text that overflows on either side.
-     */
-    private void updateTextPos() {
-        double oldX = textTranslateX.get();
-        double newX;
-        double textNodeWidth = textNode.getLayoutBounds().getWidth();
-
-        switch (getHAlignment()) {
-          case CENTER:
-            double midPoint = textRight.get() / 2;
-            if (usePromptText.get()) {
-                // If a prompt is shown (which implies that the text is
-                // empty), then we align the Text node so that the caret will
-                // appear at the left of the centered prompt.
-                newX = midPoint - promptNode.getLayoutBounds().getWidth() / 2;
-                promptNode.setLayoutX(newX);
-            } else {
-                newX = midPoint - textNodeWidth / 2;
-            }
-            // Update if there is space on the right
-            if (newX + textNodeWidth <= textRight.get()) {
-                textTranslateX.set(newX);
-            }
-            break;
-
-          case RIGHT:
-            newX = textRight.get() - textNodeWidth - caretWidth / 2;
-            // Update if there is space on the right
-            if (newX > oldX || newX > 0) {
-                textTranslateX.set(newX);
-            }
-            if (usePromptText.get()) {
-                promptNode.setLayoutX(textRight.get() - promptNode.getLayoutBounds().getWidth() -
-                                      caretWidth / 2);
-            }
-            break;
-
-          case LEFT:
-          default:
-            newX = caretWidth / 2;
-            // Update if there is space on either side.
-            if (newX < oldX || newX + textNodeWidth <= textRight.get()) {
-                textTranslateX.set(newX);
-            }
-            if (usePromptText.get()) {
-                promptNode.layoutXProperty().set(newX);
-            }
-        }
-    }
-
-    // should be called when the padding changes, or the text box width, or
-    // the dot moves
-    protected void updateCaretOff() {
-        double delta = 0.0;
-        double caretX = caretPath.getLayoutBounds().getMinX() + textTranslateX.get();
-        // If the caret position is less than or equal to the left edge of the
-        // clip then the caret will be clipped. We want the caret to end up
-        // being positioned one pixel right of the clip's left edge. The same
-        // applies on the right edge (but going the other direction of course).
-        if (caretX < 0) {
-            // I'll end up with a negative number
-            delta = caretX;
-        } else if (caretX > (textRight.get() - caretWidth)) {
-            // I'll end up with a positive number
-            delta = caretX - (textRight.get() - caretWidth);
-        }
-
-        // If delta is negative, then translate in the negative direction
-        // to cause the text to scroll to the right. Vice-versa for positive.
-        switch (getHAlignment()) {
-          case CENTER:
-            textTranslateX.set(textTranslateX.get() - delta);
-            break;
-
-          case RIGHT:
-            textTranslateX.set(Math.max(textTranslateX.get() - delta,
-                                        textRight.get() - textNode.getLayoutBounds().getWidth() -
-                                        caretWidth / 2));
-            break;
-
-          case LEFT:
-          default:
-            textTranslateX.set(Math.min(textTranslateX.get() - delta,
-                                        caretWidth / 2));
-        }
-        if (SHOW_HANDLES) {
-            caretHandle.setLayoutX(caretX - caretHandle.getWidth() / 2 + 1);
-        }
-    }
-
-    /**
-     * Use this implementation instead of the one provided on TextInputControl.
-     * updateCaretOff would get called to position the caret, but the text needs
-     * to be scrolled appropriately.
+     * Replaces a range of characters with the given text.
+     *
+     * Call this implementation from behavior classes instead of the
+     * one provided on TextInputControl to ensure that the text
+     * scrolls as needed.
+     *
+     * @param start The starting index in the range, inclusive. This must be &gt;= 0 and &lt; the end.
+     * @param end The ending index in the range, exclusive. This is one-past the last character to
+     *            delete (consistent with the String manipulation methods). This must be &gt; the start,
+     *            and &lt;= the length of the text.
+     * @param text The text that is to replace the range. This must not be null.
+     * @see TextField#replaceText(int, int, String)
      */
     public void replaceText(int start, int end, String txt) {
         final double textMaxXOld = textNode.getBoundsInParent().getMaxX();
         final double caretMaxXOld = caretPath.getLayoutBounds().getMaxX() + textTranslateX.get();
         getSkinnable().replaceText(start, end, txt);
         scrollAfterDelete(textMaxXOld, caretMaxXOld);
     }
 
+    // Public for behavior
     /**
-     * Use this implementation instead of the one provided on TextInputControl
-     * Simply calls into TextInputControl.deletePrevious/NextChar and responds appropriately
-     * based on the return value.
+     * Deletes the character that follows or precedes the current
+     * caret position from the text if there is no selection, or
+     * deletes the selection if there is one.
+     *
+     * Call this implementation from behavior classes instead of the
+     * one provided on TextInputControl to ensure that the text
+     * scrolls as needed.
+     *
+     * @param previous whether to delete the preceding character.
      */
     public void deleteChar(boolean previous) {
         final double textMaxXOld = textNode.getBoundsInParent().getMaxX();
         final double caretMaxXOld = caretPath.getLayoutBounds().getMaxX() + textTranslateX.get();
-        final boolean shouldBeep = previous ?
-                !getSkinnable().deletePreviousChar() :
-                !getSkinnable().deleteNextChar();
-
-        if (shouldBeep) {
-//            beep();
-        } else {
+        if (previous ? getSkinnable().deletePreviousChar() : getSkinnable().deleteNextChar()) {
             scrollAfterDelete(textMaxXOld, caretMaxXOld);
         }
     }
 
-    public void scrollAfterDelete(double textMaxXOld, double caretMaxXOld) {
-        final Bounds textLayoutBounds = textNode.getLayoutBounds();
-        final Bounds textBounds = textNode.localToParent(textLayoutBounds);
-        final Bounds clipBounds = clip.getBoundsInParent();
-        final Bounds caretBounds = caretPath.getLayoutBounds();
-
-        switch (getHAlignment()) {
-          case RIGHT:
-            if (textBounds.getMaxX() > clipBounds.getMaxX()) {
-                double delta = caretMaxXOld - caretBounds.getMaxX() - textTranslateX.get();
-                if (textBounds.getMaxX() + delta < clipBounds.getMaxX()) {
-                    if (textMaxXOld <= clipBounds.getMaxX()) {
-                        delta = textMaxXOld - textBounds.getMaxX();
-                    } else {
-                        delta = clipBounds.getMaxX() - textBounds.getMaxX();
-                    }
-                }
-                textTranslateX.set(textTranslateX.get() + delta);
-            } else {
-                updateTextPos();
-            }
-            break;
-
-          case LEFT:
-          case CENTER:
-          default:
-            if (textBounds.getMinX() < clipBounds.getMinX() + caretWidth / 2 &&
-                textBounds.getMaxX() <= clipBounds.getMaxX()) {
-                double delta = caretMaxXOld - caretBounds.getMaxX() - textTranslateX.get();
-                if (textBounds.getMaxX() + delta < clipBounds.getMaxX()) {
-                    if (textMaxXOld <= clipBounds.getMaxX()) {
-                        delta = textMaxXOld - textBounds.getMaxX();
-                    } else {
-                        delta = clipBounds.getMaxX() - textBounds.getMaxX();
-                    }
-                }
-                textTranslateX.set(textTranslateX.get() + delta);
-            }
-        }
-
-        updateCaretOff();
-    }
-
-    public HitInfo getIndex(double x, double y) {
+    // Public for behavior
+    /**
+     * Performs a hit test, mapping point to index in the content.
+     *
+     * @param x the x coordinate of the point.
+     * @param y the y coordinate of the point.
+     * @return a {@code TextPosInfo} object describing the index and forward bias.
+     */
+    public TextPosInfo getIndex(double x, double y) {
         // adjust the event to be in the same coordinate space as the
         // text content of the textInputControl
-        Point2D p;
-
-        p = new Point2D(x - textTranslateX.get() - snappedLeftInset(),
+        Point2D p = new Point2D(x - textTranslateX.get() - snappedLeftInset(),
                         y - snappedTopInset());
-        return textNode.impl_hitTestChar(translateCaretPosition(p));
+        return new TextPosInfo(textNode.impl_hitTestChar(p));
     }
 
-    public void positionCaret(HitInfo hit, boolean select) {
+    // Public for behavior
+    /**
+     * Moves the caret to the specified position.
+     *
+     * @param hit the new position and forward bias of the caret.
+     * @param select whether to extend selection to the new position.
+     */
+    public void positionCaret(TextPosInfo hit, boolean select) {
         TextField textField = getSkinnable();
         int pos = Utils.getHitInsertionIndex(hit, textField.textProperty().getValueSafe());
 
         if (select) {
             textField.selectPositionCaret(pos);

@@ -660,10 +506,15 @@
         }
 
         setForwardBias(hit.isLeading());
     }
 
+    private void positionCaret(HitInfo hit, boolean select) {
+        positionCaret(new TextPosInfo(hit), select);
+    }
+
+    /** {@inheritDoc} */
     @Override public Rectangle2D getCharacterBounds(int index) {
         double x, y;
         double width, height;
         if (index == textNode.getText().length()) {
             Bounds textNodeBounds = textNode.getBoundsInLocal();

@@ -690,27 +541,49 @@
 
         return new Rectangle2D(x + textBounds.getMinX() + textTranslateX.get(),
                                y + textBounds.getMinY(), width, height);
     }
 
+    /** {@inheritDoc} */
     @Override protected PathElement[] getUnderlineShape(int start, int end) {
         return textNode.impl_getUnderlineShape(start, end);
     }
 
+    /** {@inheritDoc} */
     @Override protected PathElement[] getRangeShape(int start, int end) {
         return textNode.impl_getRangeShape(start, end);
     }
 
+    /** {@inheritDoc} */
     @Override protected void addHighlight(List<? extends Node> nodes, int start) {
         textGroup.getChildren().addAll(nodes);
     }
 
+    /** {@inheritDoc} */
     @Override protected void removeHighlight(List<? extends Node> nodes) {
         textGroup.getChildren().removeAll(nodes);
     }
 
-    @Override public void nextCharacterVisually(boolean moveRight) {
+    /** {@inheritDoc} */
+    @Override public void moveCaret(TextUnit unit, Direction dir, boolean select) {
+        switch (unit) {
+            case CHARACTER:
+                switch (dir) {
+                    case LEFT:
+                    case RIGHT:
+                        nextCharacterVisually(dir == Direction.RIGHT);
+                        break;
+                    default:
+                        throw new IllegalArgumentException(""+dir);
+                }
+                break;
+            default:
+                throw new IllegalArgumentException(""+unit);
+        }
+    }
+
+    private void nextCharacterVisually(boolean moveRight) {
         if (isRTL()) {
             // Text node is mirrored.
             moveRight = !moveRight;
         }
 

@@ -722,19 +595,20 @@
             // See RT-25465.
             caretBounds = new Path(caretPath.getElements().get(0), caretPath.getElements().get(1)).getLayoutBounds();
         }
         double hitX = moveRight ? caretBounds.getMaxX() : caretBounds.getMinX();
         double hitY = (caretBounds.getMinY() + caretBounds.getMaxY()) / 2;
-        HitInfo hit = textNode.impl_hitTestChar(translateCaretPosition(new Point2D(hitX, hitY)));
+        HitInfo hit = textNode.impl_hitTestChar(new Point2D(hitX, hitY));
         Path charShape = new Path(textNode.impl_getRangeShape(hit.getCharIndex(), hit.getCharIndex() + 1));
         if ((moveRight && charShape.getLayoutBounds().getMaxX() > caretBounds.getMaxX()) ||
             (!moveRight && charShape.getLayoutBounds().getMinX() < caretBounds.getMinX())) {
             hit.setLeading(!hit.isLeading());
         }
         positionCaret(hit, false);
     }
 
+    /** {@inheritDoc} */
     @Override protected void layoutChildren(final double x, final double y,
                                             final double w, final double h) {
         super.layoutChildren(x, y, w, h);
 
         if (textNode != null) {

@@ -786,24 +660,26 @@
             selectionHandle1.setLayoutY(b.getMinY() - selectionHandle1.getHeight() + 1);
             selectionHandle2.setLayoutY(b.getMaxY() - 1);
         }
     }
 
-    protected HPos getHAlignment() {
+    private HPos getHAlignment() {
         HPos hPos = getSkinnable().getAlignment().getHpos();
         return hPos;
     }
 
+    /** {@inheritDoc} */
     @Override public Point2D getMenuPosition() {
         Point2D p = super.getMenuPosition();
         if (p != null) {
             p = new Point2D(Math.max(0, p.getX() - textNode.getLayoutX() - snappedLeftInset() + textTranslateX.get()),
                             Math.max(0, p.getY() - textNode.getLayoutY() - snappedTopInset()));
         }
         return p;
     }
 
+    /** {@inheritDoc} */
     @Override protected String maskText(String txt) {
         if (getSkinnable() instanceof PasswordField) {
             int n = txt.length();
             StringBuilder passwordBuilder = new StringBuilder(n);
             for (int i = 0; i < n; i++) {

@@ -814,15 +690,238 @@
         } else {
             return txt;
         }
     }
 
-    @Override
-    protected Object queryAccessibleAttribute(AccessibleAttribute attribute, Object... parameters) {
+    /** {@inheritDoc} */
+    @Override protected Object queryAccessibleAttribute(AccessibleAttribute attribute, Object... parameters) {
         switch (attribute) {
             case BOUNDS_FOR_RANGE:
             case OFFSET_AT_POINT:
                 return textNode.queryAccessibleAttribute(attribute, parameters);
             default: return super.queryAccessibleAttribute(attribute, parameters);
         }
     }
+
+
+
+    /**************************************************************************
+     *
+     * Private implementation
+     *
+     **************************************************************************/
+
+    TextInputControlBehavior getBehavior() {
+        return behavior;
+    }
+
+    private void updateTextNodeCaretPos(int pos) {
+        if (pos == 0 || isForwardBias()) {
+            textNode.setImpl_caretPosition(pos);
+        } else {
+            textNode.setImpl_caretPosition(pos - 1);
+        }
+        textNode.impl_caretBiasProperty().set(isForwardBias());
+    }
+
+    private void createPromptNode() {
+        if (promptNode != null || !usePromptText.get()) return;
+
+        promptNode = new Text();
+        textGroup.getChildren().add(0, promptNode);
+        promptNode.setManaged(false);
+        promptNode.getStyleClass().add("text");
+        promptNode.visibleProperty().bind(usePromptText);
+        promptNode.fontProperty().bind(getSkinnable().fontProperty());
+
+        promptNode.textProperty().bind(getSkinnable().promptTextProperty());
+        promptNode.fillProperty().bind(promptTextFillProperty());
+        updateSelection();
+    }
+
+    private void updateSelection() {
+        TextField textField = getSkinnable();
+        IndexRange newValue = textField.getSelection();
+
+        if (newValue == null || newValue.getLength() == 0) {
+            textNode.impl_selectionStartProperty().set(-1);
+            textNode.impl_selectionEndProperty().set(-1);
+        } else {
+            textNode.impl_selectionStartProperty().set(newValue.getStart());
+            // This intermediate value is needed to force selection shape layout.
+            textNode.impl_selectionEndProperty().set(newValue.getStart());
+            textNode.impl_selectionEndProperty().set(newValue.getEnd());
+        }
+
+        PathElement[] elements = textNode.impl_selectionShapeProperty().get();
+        if (elements == null) {
+            selectionHighlightPath.getElements().clear();
+        } else {
+            selectionHighlightPath.getElements().setAll(elements);
+        }
+
+        if (SHOW_HANDLES && newValue != null && newValue.getLength() > 0) {
+            int caretPos = textField.getCaretPosition();
+            int anchorPos = textField.getAnchor();
+
+            {
+                // Position the handle for the anchor. This could be handle1 or handle2.
+                // Do this before positioning the handle for the caret.
+                updateTextNodeCaretPos(anchorPos);
+                Bounds b = caretPath.getBoundsInParent();
+                if (caretPos < anchorPos) {
+                    selectionHandle2.setLayoutX(b.getMinX() - selectionHandle2.getWidth() / 2);
+                } else {
+                    selectionHandle1.setLayoutX(b.getMinX() - selectionHandle1.getWidth() / 2);
+                }
+            }
+
+            {
+                // Position handle for the caret. This could be handle1 or handle2.
+                updateTextNodeCaretPos(caretPos);
+                Bounds b = caretPath.getBoundsInParent();
+                if (caretPos < anchorPos) {
+                    selectionHandle1.setLayoutX(b.getMinX() - selectionHandle1.getWidth() / 2);
+                } else {
+                    selectionHandle2.setLayoutX(b.getMinX() - selectionHandle2.getWidth() / 2);
+                }
+            }
+        }
+    }
+
+    /**
+     * Updates the textTranslateX value for the Text node position. This is
+     * done for general layout, but care is taken to avoid resetting the
+     * position when there's a need to scroll the text due to caret movement,
+     * or when editing text that overflows on either side.
+     */
+    private void updateTextPos() {
+        double oldX = textTranslateX.get();
+        double newX;
+        double textNodeWidth = textNode.getLayoutBounds().getWidth();
+
+        switch (getHAlignment()) {
+          case CENTER:
+            double midPoint = textRight.get() / 2;
+            if (usePromptText.get()) {
+                // If a prompt is shown (which implies that the text is
+                // empty), then we align the Text node so that the caret will
+                // appear at the left of the centered prompt.
+                newX = midPoint - promptNode.getLayoutBounds().getWidth() / 2;
+                promptNode.setLayoutX(newX);
+            } else {
+                newX = midPoint - textNodeWidth / 2;
+            }
+            // Update if there is space on the right
+            if (newX + textNodeWidth <= textRight.get()) {
+                textTranslateX.set(newX);
+            }
+            break;
+
+          case RIGHT:
+            newX = textRight.get() - textNodeWidth - caretWidth / 2;
+            // Update if there is space on the right
+            if (newX > oldX || newX > 0) {
+                textTranslateX.set(newX);
+            }
+            if (usePromptText.get()) {
+                promptNode.setLayoutX(textRight.get() - promptNode.getLayoutBounds().getWidth() -
+                                      caretWidth / 2);
+            }
+            break;
+
+          case LEFT:
+          default:
+            newX = caretWidth / 2;
+            // Update if there is space on either side.
+            if (newX < oldX || newX + textNodeWidth <= textRight.get()) {
+                textTranslateX.set(newX);
+            }
+            if (usePromptText.get()) {
+                promptNode.layoutXProperty().set(newX);
+            }
+        }
+    }
+
+    // should be called when the padding changes, or the text box width, or
+    // the dot moves
+    private void updateCaretOff() {
+        double delta = 0.0;
+        double caretX = caretPath.getLayoutBounds().getMinX() + textTranslateX.get();
+        // If the caret position is less than or equal to the left edge of the
+        // clip then the caret will be clipped. We want the caret to end up
+        // being positioned one pixel right of the clip's left edge. The same
+        // applies on the right edge (but going the other direction of course).
+        if (caretX < 0) {
+            // I'll end up with a negative number
+            delta = caretX;
+        } else if (caretX > (textRight.get() - caretWidth)) {
+            // I'll end up with a positive number
+            delta = caretX - (textRight.get() - caretWidth);
+        }
+
+        // If delta is negative, then translate in the negative direction
+        // to cause the text to scroll to the right. Vice-versa for positive.
+        switch (getHAlignment()) {
+          case CENTER:
+            textTranslateX.set(textTranslateX.get() - delta);
+            break;
+
+          case RIGHT:
+            textTranslateX.set(Math.max(textTranslateX.get() - delta,
+                                        textRight.get() - textNode.getLayoutBounds().getWidth() -
+                                        caretWidth / 2));
+            break;
+
+          case LEFT:
+          default:
+            textTranslateX.set(Math.min(textTranslateX.get() - delta,
+                                        caretWidth / 2));
+        }
+        if (SHOW_HANDLES) {
+            caretHandle.setLayoutX(caretX - caretHandle.getWidth() / 2 + 1);
+        }
+    }
+
+    private void scrollAfterDelete(double textMaxXOld, double caretMaxXOld) {
+        final Bounds textLayoutBounds = textNode.getLayoutBounds();
+        final Bounds textBounds = textNode.localToParent(textLayoutBounds);
+        final Bounds clipBounds = clip.getBoundsInParent();
+        final Bounds caretBounds = caretPath.getLayoutBounds();
+
+        switch (getHAlignment()) {
+          case RIGHT:
+            if (textBounds.getMaxX() > clipBounds.getMaxX()) {
+                double delta = caretMaxXOld - caretBounds.getMaxX() - textTranslateX.get();
+                if (textBounds.getMaxX() + delta < clipBounds.getMaxX()) {
+                    if (textMaxXOld <= clipBounds.getMaxX()) {
+                        delta = textMaxXOld - textBounds.getMaxX();
+                    } else {
+                        delta = clipBounds.getMaxX() - textBounds.getMaxX();
+                    }
+                }
+                textTranslateX.set(textTranslateX.get() + delta);
+            } else {
+                updateTextPos();
+            }
+            break;
+
+          case LEFT:
+          case CENTER:
+          default:
+            if (textBounds.getMinX() < clipBounds.getMinX() + caretWidth / 2 &&
+                textBounds.getMaxX() <= clipBounds.getMaxX()) {
+                double delta = caretMaxXOld - caretBounds.getMaxX() - textTranslateX.get();
+                if (textBounds.getMaxX() + delta < clipBounds.getMaxX()) {
+                    if (textMaxXOld <= clipBounds.getMaxX()) {
+                        delta = textMaxXOld - textBounds.getMaxX();
+                    } else {
+                        delta = clipBounds.getMaxX() - textBounds.getMaxX();
+                    }
+                }
+                textTranslateX.set(textTranslateX.get() + delta);
+            }
+        }
+
+        updateCaretOff();
+    }
 }