1 /*
   2  * Copyright (c) 2011, 2014, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 
  26 package javafx.scene.control.skin;
  27 
  28 import com.sun.javafx.scene.control.behavior.BehaviorBase;
  29 import com.sun.javafx.scene.control.behavior.TextAreaBehavior;
  30 import com.sun.javafx.scene.control.behavior.TextInputControlBehavior;
  31 import com.sun.javafx.scene.control.skin.Utils;
  32 import javafx.beans.binding.BooleanBinding;
  33 import javafx.beans.binding.DoubleBinding;
  34 import javafx.beans.binding.ObjectBinding;
  35 import javafx.beans.binding.StringBinding;
  36 import javafx.beans.property.DoubleProperty;
  37 import javafx.beans.property.SimpleDoubleProperty;
  38 import javafx.beans.value.ObservableBooleanValue;
  39 import javafx.beans.value.ObservableDoubleValue;
  40 import javafx.event.EventHandler;
  41 import javafx.geometry.Bounds;
  42 import javafx.geometry.HPos;
  43 import javafx.geometry.Point2D;
  44 import javafx.geometry.Rectangle2D;
  45 import javafx.scene.AccessibleAttribute;
  46 import javafx.scene.Group;
  47 import javafx.scene.Node;
  48 import javafx.scene.control.Accordion;
  49 import javafx.scene.control.Button;
  50 import javafx.scene.control.Control;
  51 import javafx.scene.control.IndexRange;
  52 import javafx.scene.control.PasswordField;
  53 import javafx.scene.control.TextField;
  54 import javafx.scene.input.MouseEvent;
  55 import javafx.scene.layout.Pane;
  56 import javafx.scene.paint.Color;
  57 import javafx.scene.paint.Paint;
  58 import javafx.scene.shape.Path;
  59 import javafx.scene.shape.PathElement;
  60 import javafx.scene.shape.Rectangle;
  61 import javafx.scene.text.Text;
  62 import java.util.List;
  63 import com.sun.javafx.scene.control.behavior.TextFieldBehavior;
  64 import com.sun.javafx.scene.control.behavior.PasswordFieldBehavior;
  65 import com.sun.javafx.scene.text.HitInfo;
  66 
  67 /**
  68  * Default skin implementation for the {@link TextField} control.
  69  *
  70  * @see TextField
  71  * @since 9
  72  */
  73 public class TextFieldSkin extends TextInputControlSkin<TextField> {
  74 
  75     /**************************************************************************
  76      *
  77      * Private fields
  78      *
  79      **************************************************************************/
  80 
  81     private final TextFieldBehavior behavior;
  82 
  83     /**
  84      * This group contains the text, caret, and selection rectangle.
  85      * It is clipped. The textNode, selectionHighlightPath, and
  86      * caret are each translated individually when horizontal
  87      * translation is needed to keep the caretPosition visible.
  88      */
  89     private Pane textGroup = new Pane();
  90     private Group handleGroup;
  91 
  92     /**
  93      * The clip, applied to the textGroup. This makes sure that any
  94      * text / selection wandering off the text box is clipped
  95      */
  96     private Rectangle clip = new Rectangle();
  97     /**
  98      * The node actually displaying the text. Note that it has the
  99      * ability to render both the normal fill as well as the highlight
 100      * fill, to perform hit testing, fetching of the selection
 101      * highlight, and other such duties.
 102      */
 103     private Text textNode = new Text();
 104     /**
 105      *
 106      * The node used for showing the prompt text.
 107      */
 108     private Text promptNode;
 109     /**
 110      * A path, provided by the textNode, which represents the area
 111      * which is selected. The path elements which make up the
 112      * selection must be updated whenever the selection changes. We
 113      * don't need to keep track of text changes because those will
 114      * force the selection to be updated.
 115      */
 116     private Path selectionHighlightPath = new Path();
 117 
 118     private Path characterBoundingPath = new Path();
 119     private ObservableBooleanValue usePromptText;
 120     private DoubleProperty textTranslateX = new SimpleDoubleProperty(this, "textTranslateX");
 121     private double caretWidth;
 122 
 123     /**
 124      * Right edge of the text region sans padding
 125      */
 126     private ObservableDoubleValue textRight;
 127 
 128     private double pressX, pressY; // For dragging handles on embedded
 129 
 130     // For use with PasswordField
 131     static final char BULLET = '\u25cf';
 132 
 133 
 134 
 135     /**************************************************************************
 136      *
 137      * Constructors
 138      *
 139      **************************************************************************/
 140 
 141     /**
 142      * Creates a new TextFieldSkin instance, installing the necessary child
 143      * nodes into the Control {@link Control#getChildren() children} list, as
 144      * well as the necessary input mappings for handling key, mouse, etc events.
 145      *
 146      * @param control The control that this skin should be installed onto.
 147      */
 148     public TextFieldSkin(final TextField control) {
 149         super(control);
 150 
 151         // install default input map for the text field control
 152         this.behavior = (control instanceof PasswordField)
 153                 ? new PasswordFieldBehavior((PasswordField)control)
 154                 : new TextFieldBehavior(control);
 155         this.behavior.setTextFieldSkin(this);
 156 //        control.setInputMap(behavior.getInputMap());
 157 
 158         control.caretPositionProperty().addListener((observable, oldValue, newValue) -> {
 159             if (control.getWidth() > 0) {
 160                 updateTextNodeCaretPos(control.getCaretPosition());
 161                 if (!isForwardBias()) {
 162                     setForwardBias(true);
 163                 }
 164                 updateCaretOff();
 165             }
 166         });
 167 
 168         forwardBiasProperty().addListener(observable -> {
 169             if (control.getWidth() > 0) {
 170                 updateTextNodeCaretPos(control.getCaretPosition());
 171                 updateCaretOff();
 172             }
 173         });
 174 
 175         textRight = new DoubleBinding() {
 176             { bind(textGroup.widthProperty()); }
 177             @Override protected double computeValue() {
 178                 return textGroup.getWidth();
 179             }
 180         };
 181 
 182         // Once this was crucial for performance, not sure now.
 183         clip.setSmooth(false);
 184         clip.setX(0);
 185         clip.widthProperty().bind(textGroup.widthProperty());
 186         clip.heightProperty().bind(textGroup.heightProperty());
 187 
 188         // Add content
 189         textGroup.setClip(clip);
 190         // Hack to defeat the fact that otherwise when the caret blinks the parent group
 191         // bounds are completely invalidated and therefore the dirty region is much
 192         // larger than necessary.
 193         textGroup.getChildren().addAll(selectionHighlightPath, textNode, new Group(caretPath));
 194         getChildren().add(textGroup);
 195         if (SHOW_HANDLES) {
 196             handleGroup = new Group();
 197             handleGroup.setManaged(false);
 198             handleGroup.getChildren().addAll(caretHandle, selectionHandle1, selectionHandle2);
 199             getChildren().add(handleGroup);
 200         }
 201 
 202         // Add text
 203         textNode.setManaged(false);
 204         textNode.getStyleClass().add("text");
 205         textNode.fontProperty().bind(control.fontProperty());
 206 
 207         textNode.layoutXProperty().bind(textTranslateX);
 208         textNode.textProperty().bind(new StringBinding() {
 209             { bind(control.textProperty()); }
 210             @Override protected String computeValue() {
 211                 return maskText(control.textProperty().getValueSafe());
 212             }
 213         });
 214         textNode.fillProperty().bind(textFillProperty());
 215         textNode.impl_selectionFillProperty().bind(new ObjectBinding<Paint>() {
 216             { bind(highlightTextFillProperty(), textFillProperty(), control.focusedProperty()); }
 217             @Override protected Paint computeValue() {
 218                 return control.isFocused() ? highlightTextFillProperty().get() : textFillProperty().get();
 219             }
 220         });
 221         // updated by listener on caretPosition to ensure order
 222         updateTextNodeCaretPos(control.getCaretPosition());
 223         control.selectionProperty().addListener(observable -> {
 224             updateSelection();
 225         });
 226 
 227         // Add selection
 228         selectionHighlightPath.setManaged(false);
 229         selectionHighlightPath.setStroke(null);
 230         selectionHighlightPath.layoutXProperty().bind(textTranslateX);
 231         selectionHighlightPath.visibleProperty().bind(control.anchorProperty().isNotEqualTo(control.caretPositionProperty()).and(control.focusedProperty()));
 232         selectionHighlightPath.fillProperty().bind(highlightFillProperty());
 233         textNode.impl_selectionShapeProperty().addListener(observable -> {
 234             updateSelection();
 235         });
 236 
 237         // Add caret
 238         caretPath.setManaged(false);
 239         caretPath.setStrokeWidth(1);
 240         caretPath.fillProperty().bind(textFillProperty());
 241         caretPath.strokeProperty().bind(textFillProperty());
 242         
 243         // modifying visibility of the caret forces a layout-pass (RT-32373), so
 244         // instead we modify the opacity.
 245         caretPath.opacityProperty().bind(new DoubleBinding() {
 246             { bind(caretVisibleProperty()); }
 247             @Override protected double computeValue() {
 248                 return caretVisibleProperty().get() ? 1.0 : 0.0;
 249             }
 250         });
 251         caretPath.layoutXProperty().bind(textTranslateX);
 252         textNode.impl_caretShapeProperty().addListener(observable -> {
 253             caretPath.getElements().setAll(textNode.impl_caretShapeProperty().get());
 254             if (caretPath.getElements().size() == 0) {
 255                 // The caret pos is invalid.
 256                 updateTextNodeCaretPos(control.getCaretPosition());
 257             } else if (caretPath.getElements().size() == 4) {
 258                 // The caret is split. Ignore and keep the previous width value.
 259             } else {
 260                 caretWidth = Math.round(caretPath.getLayoutBounds().getWidth());
 261             }
 262         });
 263 
 264         // Be sure to get the control to request layout when the font changes,
 265         // since this will affect the pref height and pref width.
 266         control.fontProperty().addListener(observable -> {
 267             // I do both so that any cached values for prefWidth/height are cleared.
 268             // The problem is that the skin is unmanaged and so calling request layout
 269             // doesn't walk up the tree all the way. I think....
 270             control.requestLayout();
 271             getSkinnable().requestLayout();
 272         });
 273 
 274         registerChangeListener(control.prefColumnCountProperty(), e -> getSkinnable().requestLayout());
 275         if (control.isFocused()) setCaretAnimating(true);
 276 
 277         control.alignmentProperty().addListener(observable -> {
 278             if (control.getWidth() > 0) {
 279                 updateTextPos();
 280                 updateCaretOff();
 281                 control.requestLayout();
 282             }
 283         });
 284 
 285         usePromptText = new BooleanBinding() {
 286             { bind(control.textProperty(),
 287                    control.promptTextProperty(),
 288                    promptTextFillProperty()); }
 289             @Override protected boolean computeValue() {
 290                 String txt = control.getText();
 291                 String promptTxt = control.getPromptText();
 292                 return ((txt == null || txt.isEmpty()) &&
 293                         promptTxt != null && !promptTxt.isEmpty() &&
 294                         !getPromptTextFill().equals(Color.TRANSPARENT));
 295             }
 296         };
 297 
 298         promptTextFillProperty().addListener(observable -> {
 299             updateTextPos();
 300         });
 301 
 302         control.textProperty().addListener(observable -> {
 303             if (!behavior.isEditing()) {
 304                 // Text changed, but not by user action
 305                 updateTextPos();
 306             }
 307         });
 308 
 309         if (usePromptText.get()) {
 310             createPromptNode();
 311         }
 312 
 313         usePromptText.addListener(observable -> {
 314             createPromptNode();
 315             control.requestLayout();
 316         });
 317 
 318         if (SHOW_HANDLES) {
 319             selectionHandle1.setRotate(180);
 320 
 321             EventHandler<MouseEvent> handlePressHandler = e -> {
 322                 pressX = e.getX();
 323                 pressY = e.getY();
 324                 e.consume();
 325             };
 326 
 327             caretHandle.setOnMousePressed(handlePressHandler);
 328             selectionHandle1.setOnMousePressed(handlePressHandler);
 329             selectionHandle2.setOnMousePressed(handlePressHandler);
 330 
 331             caretHandle.setOnMouseDragged(e -> {
 332                 Point2D p = new Point2D(caretHandle.getLayoutX() + e.getX() + pressX - textNode.getLayoutX(),
 333                                         caretHandle.getLayoutY() + e.getY() - pressY - 6);
 334                 HitInfo hit = textNode.impl_hitTestChar(p);
 335                 positionCaret(hit, false);
 336                 e.consume();
 337             });
 338 
 339             selectionHandle1.setOnMouseDragged(new EventHandler<MouseEvent>() {
 340                 @Override public void handle(MouseEvent e) {
 341                     TextField control = getSkinnable();
 342                     Point2D tp = textNode.localToScene(0, 0);
 343                     Point2D p = new Point2D(e.getSceneX() - tp.getX() + 10/*??*/ - pressX + selectionHandle1.getWidth() / 2,
 344                                             e.getSceneY() - tp.getY() - pressY - 6);
 345                     HitInfo hit = textNode.impl_hitTestChar(p);
 346                     int pos = hit.getCharIndex();
 347                     if (control.getAnchor() < control.getCaretPosition()) {
 348                         // Swap caret and anchor
 349                         control.selectRange(control.getCaretPosition(), control.getAnchor());
 350                     }
 351                     if (pos >= 0) {
 352                         if (pos >= control.getAnchor() - 1) {
 353                             hit.setCharIndex(Math.max(0, control.getAnchor() - 1));
 354                         }
 355                         positionCaret(hit, true);
 356                     }
 357                     e.consume();
 358                 }
 359             });
 360 
 361             selectionHandle2.setOnMouseDragged(new EventHandler<MouseEvent>() {
 362                 @Override public void handle(MouseEvent e) {
 363                     TextField control = getSkinnable();
 364                     Point2D tp = textNode.localToScene(0, 0);
 365                     Point2D p = new Point2D(e.getSceneX() - tp.getX() + 10/*??*/ - pressX + selectionHandle2.getWidth() / 2,
 366                                             e.getSceneY() - tp.getY() - pressY - 6);
 367                     HitInfo hit = textNode.impl_hitTestChar(p);
 368                     int pos = hit.getCharIndex();
 369                     if (control.getAnchor() > control.getCaretPosition()) {
 370                         // Swap caret and anchor
 371                         control.selectRange(control.getCaretPosition(), control.getAnchor());
 372                     }
 373                     if (pos > 0) {
 374                         if (pos <= control.getAnchor()) {
 375                             hit.setCharIndex(Math.min(control.getAnchor() + 1, control.getLength()));
 376                         }
 377                         positionCaret(hit, true);
 378                     }
 379                     e.consume();
 380                 }
 381             });
 382         }
 383     }
 384 
 385 
 386 
 387     /***************************************************************************
 388      *                                                                         *
 389      * Public API                                                              *
 390      *                                                                         *
 391      **************************************************************************/
 392 
 393     /** {@inheritDoc} */
 394     @Override public void dispose() {
 395         super.dispose();
 396 
 397         if (behavior != null) {
 398             behavior.dispose();
 399         }
 400     }
 401 
 402     /** {@inheritDoc} */
 403     @Override protected double computePrefWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
 404         TextField textField = getSkinnable();
 405 
 406         double characterWidth = fontMetrics.get().computeStringWidth("W");
 407 
 408         int columnCount = textField.getPrefColumnCount();
 409 
 410         return columnCount * characterWidth + leftInset + rightInset;
 411     }
 412 
 413     /** {@inheritDoc} */
 414     @Override protected double computeMinHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
 415         return computePrefHeight(width, topInset, rightInset, bottomInset, leftInset);
 416     }
 417 
 418     /** {@inheritDoc} */
 419     @Override protected double computePrefHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
 420         return topInset + textNode.getLayoutBounds().getHeight() + bottomInset;
 421     }
 422 
 423     /** {@inheritDoc} */
 424     @Override protected double computeMaxHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
 425         return getSkinnable().prefHeight(width);
 426     }
 427 
 428     /** {@inheritDoc} */
 429     @Override public double computeBaselineOffset(double topInset, double rightInset, double bottomInset, double leftInset) {
 430         return topInset + textNode.getBaselineOffset();
 431     }
 432 
 433     // Public for behavior
 434     /**
 435      * Replaces a range of characters with the given text.
 436      *
 437      * Call this implementation from behavior classes instead of the
 438      * one provided on TextInputControl to ensure that the text
 439      * scrolls as needed.
 440      *
 441      * @param start The starting index in the range, inclusive. This must be &gt;= 0 and &lt; the end.
 442      * @param end The ending index in the range, exclusive. This is one-past the last character to
 443      *            delete (consistent with the String manipulation methods). This must be &gt; the start,
 444      *            and &lt;= the length of the text.
 445      * @param text The text that is to replace the range. This must not be null.
 446      * @see TextField#replaceText(int, int, String)
 447      */
 448     public void replaceText(int start, int end, String txt) {
 449         final double textMaxXOld = textNode.getBoundsInParent().getMaxX();
 450         final double caretMaxXOld = caretPath.getLayoutBounds().getMaxX() + textTranslateX.get();
 451         getSkinnable().replaceText(start, end, txt);
 452         scrollAfterDelete(textMaxXOld, caretMaxXOld);
 453     }
 454 
 455     // Public for behavior
 456     /**
 457      * Deletes the character that follows or precedes the current
 458      * caret position from the text if there is no selection, or
 459      * deletes the selection if there is one.
 460      *
 461      * Call this implementation from behavior classes instead of the
 462      * one provided on TextInputControl to ensure that the text
 463      * scrolls as needed.
 464      *
 465      * @param previous whether to delete the preceding character.
 466      */
 467     public void deleteChar(boolean previous) {
 468         final double textMaxXOld = textNode.getBoundsInParent().getMaxX();
 469         final double caretMaxXOld = caretPath.getLayoutBounds().getMaxX() + textTranslateX.get();
 470         if (previous ? getSkinnable().deletePreviousChar() : getSkinnable().deleteNextChar()) {
 471             scrollAfterDelete(textMaxXOld, caretMaxXOld);
 472         }
 473     }
 474 
 475     // Public for behavior
 476     /**
 477      * Performs a hit test, mapping point to index in the content.
 478      *
 479      * @param x the x coordinate of the point.
 480      * @param y the y coordinate of the point.
 481      * @return a {@code TextPosInfo} object describing the index and forward bias.
 482      */
 483     public TextPosInfo getIndex(double x, double y) {
 484         // adjust the event to be in the same coordinate space as the
 485         // text content of the textInputControl
 486         Point2D p = new Point2D(x - textTranslateX.get() - snappedLeftInset(),
 487                                 y - snappedTopInset());
 488         return new TextPosInfo(textNode.impl_hitTestChar(p));
 489     }
 490 
 491     // Public for behavior
 492     /**
 493      * Moves the caret to the specified position.
 494      *
 495      * @param hit the new position and forward bias of the caret.
 496      * @param select whether to extend selection to the new position.
 497      */
 498     public void positionCaret(TextPosInfo hit, boolean select) {
 499         TextField textField = getSkinnable();
 500         int pos = Utils.getHitInsertionIndex(hit, textField.textProperty().getValueSafe());
 501 
 502         if (select) {
 503             textField.selectPositionCaret(pos);
 504         } else {
 505             textField.positionCaret(pos);
 506         }
 507 
 508         setForwardBias(hit.isLeading());
 509     }
 510 
 511     private void positionCaret(HitInfo hit, boolean select) {
 512         positionCaret(new TextPosInfo(hit), select);
 513     }
 514 
 515     /** {@inheritDoc} */
 516     @Override public Rectangle2D getCharacterBounds(int index) {
 517         double x, y;
 518         double width, height;
 519         if (index == textNode.getText().length()) {
 520             Bounds textNodeBounds = textNode.getBoundsInLocal();
 521             x = textNodeBounds.getMaxX();
 522             y = 0;
 523             width = 0;
 524             height = textNodeBounds.getMaxY();
 525         } else {
 526             characterBoundingPath.getElements().clear();
 527             characterBoundingPath.getElements().addAll(textNode.impl_getRangeShape(index, index + 1));
 528             characterBoundingPath.setLayoutX(textNode.getLayoutX());
 529             characterBoundingPath.setLayoutY(textNode.getLayoutY());
 530 
 531             Bounds bounds = characterBoundingPath.getBoundsInLocal();
 532 
 533             x = bounds.getMinX();
 534             y = bounds.getMinY();
 535             // Sometimes the bounds is empty, in which case we must ignore the width/height
 536             width  = bounds.isEmpty() ? 0 : bounds.getWidth();
 537             height = bounds.isEmpty() ? 0 : bounds.getHeight();
 538         }
 539 
 540         Bounds textBounds = textGroup.getBoundsInParent();
 541 
 542         return new Rectangle2D(x + textBounds.getMinX() + textTranslateX.get(),
 543                 y + textBounds.getMinY(), width, height);
 544     }
 545 
 546     /** {@inheritDoc} */
 547     @Override protected PathElement[] getUnderlineShape(int start, int end) {
 548         return textNode.impl_getUnderlineShape(start, end);
 549     }
 550 
 551     /** {@inheritDoc} */
 552     @Override protected PathElement[] getRangeShape(int start, int end) {
 553         return textNode.impl_getRangeShape(start, end);
 554     }
 555 
 556     /** {@inheritDoc} */
 557     @Override protected void addHighlight(List<? extends Node> nodes, int start) {
 558         textGroup.getChildren().addAll(nodes);
 559     }
 560 
 561     /** {@inheritDoc} */
 562     @Override protected void removeHighlight(List<? extends Node> nodes) {
 563         textGroup.getChildren().removeAll(nodes);
 564     }
 565 
 566     /** {@inheritDoc} */
 567     @Override public void moveCaret(TextUnit unit, Direction dir, boolean select) {
 568         switch (unit) {
 569             case CHARACTER:
 570                 switch (dir) {
 571                     case LEFT:
 572                     case RIGHT:
 573                         nextCharacterVisually(dir == Direction.RIGHT);
 574                         break;
 575                     default:
 576                         throw new IllegalArgumentException(""+dir);
 577                 }
 578                 break;
 579             default:
 580                 throw new IllegalArgumentException(""+unit);
 581         }
 582     }
 583 
 584     private void nextCharacterVisually(boolean moveRight) {
 585         if (isRTL()) {
 586             // Text node is mirrored.
 587             moveRight = !moveRight;
 588         }
 589 
 590         Bounds caretBounds = caretPath.getLayoutBounds();
 591         if (caretPath.getElements().size() == 4) {
 592             // The caret is split
 593             // TODO: Find a better way to get the primary caret position
 594             // instead of depending on the internal implementation.
 595             // See RT-25465.
 596             caretBounds = new Path(caretPath.getElements().get(0), caretPath.getElements().get(1)).getLayoutBounds();
 597         }
 598         double hitX = moveRight ? caretBounds.getMaxX() : caretBounds.getMinX();
 599         double hitY = (caretBounds.getMinY() + caretBounds.getMaxY()) / 2;
 600         HitInfo hit = textNode.impl_hitTestChar(new Point2D(hitX, hitY));
 601         Path charShape = new Path(textNode.impl_getRangeShape(hit.getCharIndex(), hit.getCharIndex() + 1));
 602         if ((moveRight && charShape.getLayoutBounds().getMaxX() > caretBounds.getMaxX()) ||
 603                 (!moveRight && charShape.getLayoutBounds().getMinX() < caretBounds.getMinX())) {
 604             hit.setLeading(!hit.isLeading());
 605         }
 606         positionCaret(hit, false);
 607     }
 608 
 609     /** {@inheritDoc} */
 610     @Override protected void layoutChildren(final double x, final double y,
 611                                             final double w, final double h) {
 612         super.layoutChildren(x, y, w, h);
 613 
 614         if (textNode != null) {
 615             double textY;
 616             final Bounds textNodeBounds = textNode.getLayoutBounds();
 617             final double ascent = textNode.getBaselineOffset();
 618             final double descent = textNodeBounds.getHeight() - ascent;
 619 
 620             switch (getSkinnable().getAlignment().getVpos()) {
 621                 case TOP:
 622                     textY = ascent;
 623                     break;
 624 
 625                 case CENTER:
 626                     textY = (ascent + textGroup.getHeight() - descent) / 2;
 627                     break;
 628 
 629                 case BOTTOM:
 630                 default:
 631                     textY = textGroup.getHeight() - descent;
 632             }
 633             textNode.setY(textY);
 634             if (promptNode != null) {
 635                 promptNode.setY(textY);
 636             }
 637 
 638             if (getSkinnable().getWidth() > 0) {
 639                 updateTextPos();
 640                 updateCaretOff();
 641             }
 642         }
 643 
 644         if (SHOW_HANDLES) {
 645             handleGroup.setLayoutX(x + textTranslateX.get());
 646             handleGroup.setLayoutY(y);
 647 
 648             // Resize handles for caret and anchor.
 649 //            IndexRange selection = textField.getSelection();
 650             selectionHandle1.resize(selectionHandle1.prefWidth(-1),
 651                     selectionHandle1.prefHeight(-1));
 652             selectionHandle2.resize(selectionHandle2.prefWidth(-1),
 653                     selectionHandle2.prefHeight(-1));
 654             caretHandle.resize(caretHandle.prefWidth(-1),
 655                     caretHandle.prefHeight(-1));
 656 
 657             Bounds b = caretPath.getBoundsInParent();
 658             caretHandle.setLayoutY(b.getMaxY() - 1);
 659             //selectionHandle1.setLayoutY(b.getMaxY() - 1);
 660             selectionHandle1.setLayoutY(b.getMinY() - selectionHandle1.getHeight() + 1);
 661             selectionHandle2.setLayoutY(b.getMaxY() - 1);
 662         }
 663     }
 664 
 665     private HPos getHAlignment() {
 666         HPos hPos = getSkinnable().getAlignment().getHpos();
 667         return hPos;
 668     }
 669 
 670     /** {@inheritDoc} */
 671     @Override public Point2D getMenuPosition() {
 672         Point2D p = super.getMenuPosition();
 673         if (p != null) {
 674             p = new Point2D(Math.max(0, p.getX() - textNode.getLayoutX() - snappedLeftInset() + textTranslateX.get()),
 675                     Math.max(0, p.getY() - textNode.getLayoutY() - snappedTopInset()));
 676         }
 677         return p;
 678     }
 679 
 680     /** {@inheritDoc} */
 681     @Override protected String maskText(String txt) {
 682         if (getSkinnable() instanceof PasswordField) {
 683             int n = txt.length();
 684             StringBuilder passwordBuilder = new StringBuilder(n);
 685             for (int i = 0; i < n; i++) {
 686                 passwordBuilder.append(BULLET);
 687             }
 688 
 689             return passwordBuilder.toString();
 690         } else {
 691             return txt;
 692         }
 693     }
 694 
 695     /** {@inheritDoc} */
 696     @Override protected Object queryAccessibleAttribute(AccessibleAttribute attribute, Object... parameters) {
 697         switch (attribute) {
 698             case BOUNDS_FOR_RANGE:
 699             case OFFSET_AT_POINT:
 700                 return textNode.queryAccessibleAttribute(attribute, parameters);
 701             default: return super.queryAccessibleAttribute(attribute, parameters);
 702         }
 703     }
 704 
 705 
 706 
 707     /**************************************************************************
 708      *
 709      * Private implementation
 710      *
 711      **************************************************************************/
 712 
 713     TextInputControlBehavior getBehavior() {
 714         return behavior;
 715     }
 716 
 717     private void updateTextNodeCaretPos(int pos) {
 718         if (pos == 0 || isForwardBias()) {
 719             textNode.setImpl_caretPosition(pos);
 720         } else {
 721             textNode.setImpl_caretPosition(pos - 1);
 722         }
 723         textNode.impl_caretBiasProperty().set(isForwardBias());
 724     }
 725 
 726     private void createPromptNode() {
 727         if (promptNode != null || !usePromptText.get()) return;
 728 
 729         promptNode = new Text();
 730         textGroup.getChildren().add(0, promptNode);
 731         promptNode.setManaged(false);
 732         promptNode.getStyleClass().add("text");
 733         promptNode.visibleProperty().bind(usePromptText);
 734         promptNode.fontProperty().bind(getSkinnable().fontProperty());
 735 
 736         promptNode.textProperty().bind(getSkinnable().promptTextProperty());
 737         promptNode.fillProperty().bind(promptTextFillProperty());
 738         updateSelection();
 739     }
 740 
 741     private void updateSelection() {
 742         TextField textField = getSkinnable();
 743         IndexRange newValue = textField.getSelection();
 744 
 745         if (newValue == null || newValue.getLength() == 0) {
 746             textNode.impl_selectionStartProperty().set(-1);
 747             textNode.impl_selectionEndProperty().set(-1);
 748         } else {
 749             textNode.impl_selectionStartProperty().set(newValue.getStart());
 750             // This intermediate value is needed to force selection shape layout.
 751             textNode.impl_selectionEndProperty().set(newValue.getStart());
 752             textNode.impl_selectionEndProperty().set(newValue.getEnd());
 753         }
 754 
 755         PathElement[] elements = textNode.impl_selectionShapeProperty().get();
 756         if (elements == null) {
 757             selectionHighlightPath.getElements().clear();
 758         } else {
 759             selectionHighlightPath.getElements().setAll(elements);
 760         }
 761 
 762         if (SHOW_HANDLES && newValue != null && newValue.getLength() > 0) {
 763             int caretPos = textField.getCaretPosition();
 764             int anchorPos = textField.getAnchor();
 765 
 766             {
 767                 // Position the handle for the anchor. This could be handle1 or handle2.
 768                 // Do this before positioning the handle for the caret.
 769                 updateTextNodeCaretPos(anchorPos);
 770                 Bounds b = caretPath.getBoundsInParent();
 771                 if (caretPos < anchorPos) {
 772                     selectionHandle2.setLayoutX(b.getMinX() - selectionHandle2.getWidth() / 2);
 773                 } else {
 774                     selectionHandle1.setLayoutX(b.getMinX() - selectionHandle1.getWidth() / 2);
 775                 }
 776             }
 777 
 778             {
 779                 // Position handle for the caret. This could be handle1 or handle2.
 780                 updateTextNodeCaretPos(caretPos);
 781                 Bounds b = caretPath.getBoundsInParent();
 782                 if (caretPos < anchorPos) {
 783                     selectionHandle1.setLayoutX(b.getMinX() - selectionHandle1.getWidth() / 2);
 784                 } else {
 785                     selectionHandle2.setLayoutX(b.getMinX() - selectionHandle2.getWidth() / 2);
 786                 }
 787             }
 788         }
 789     }
 790 
 791     /**
 792      * Updates the textTranslateX value for the Text node position. This is
 793      * done for general layout, but care is taken to avoid resetting the
 794      * position when there's a need to scroll the text due to caret movement,
 795      * or when editing text that overflows on either side.
 796      */
 797     private void updateTextPos() {
 798         double oldX = textTranslateX.get();
 799         double newX;
 800         double textNodeWidth = textNode.getLayoutBounds().getWidth();
 801 
 802         switch (getHAlignment()) {
 803           case CENTER:
 804             double midPoint = textRight.get() / 2;
 805             if (usePromptText.get()) {
 806                 // If a prompt is shown (which implies that the text is
 807                 // empty), then we align the Text node so that the caret will
 808                 // appear at the left of the centered prompt.
 809                 newX = midPoint - promptNode.getLayoutBounds().getWidth() / 2;
 810                 promptNode.setLayoutX(newX);
 811             } else {
 812                 newX = midPoint - textNodeWidth / 2;
 813             }
 814             // Update if there is space on the right
 815             if (newX + textNodeWidth <= textRight.get()) {
 816                 textTranslateX.set(newX);
 817             }
 818             break;
 819 
 820           case RIGHT:
 821             newX = textRight.get() - textNodeWidth - caretWidth / 2;
 822             // Update if there is space on the right
 823             if (newX > oldX || newX > 0) {
 824                 textTranslateX.set(newX);
 825             }
 826             if (usePromptText.get()) {
 827                 promptNode.setLayoutX(textRight.get() - promptNode.getLayoutBounds().getWidth() -
 828                                       caretWidth / 2);
 829             }
 830             break;
 831 
 832           case LEFT:
 833           default:
 834             newX = caretWidth / 2;
 835             // Update if there is space on either side.
 836             if (newX < oldX || newX + textNodeWidth <= textRight.get()) {
 837                 textTranslateX.set(newX);
 838             }
 839             if (usePromptText.get()) {
 840                 promptNode.layoutXProperty().set(newX);
 841             }
 842         }
 843     }
 844 
 845     // should be called when the padding changes, or the text box width, or
 846     // the dot moves
 847     private void updateCaretOff() {
 848         double delta = 0.0;
 849         double caretX = caretPath.getLayoutBounds().getMinX() + textTranslateX.get();
 850         // If the caret position is less than or equal to the left edge of the
 851         // clip then the caret will be clipped. We want the caret to end up
 852         // being positioned one pixel right of the clip's left edge. The same
 853         // applies on the right edge (but going the other direction of course).
 854         if (caretX < 0) {
 855             // I'll end up with a negative number
 856             delta = caretX;
 857         } else if (caretX > (textRight.get() - caretWidth)) {
 858             // I'll end up with a positive number
 859             delta = caretX - (textRight.get() - caretWidth);
 860         }
 861 
 862         // If delta is negative, then translate in the negative direction
 863         // to cause the text to scroll to the right. Vice-versa for positive.
 864         switch (getHAlignment()) {
 865           case CENTER:
 866             textTranslateX.set(textTranslateX.get() - delta);
 867             break;
 868 
 869           case RIGHT:
 870             textTranslateX.set(Math.max(textTranslateX.get() - delta,
 871                                         textRight.get() - textNode.getLayoutBounds().getWidth() -
 872                                         caretWidth / 2));
 873             break;
 874 
 875           case LEFT:
 876           default:
 877             textTranslateX.set(Math.min(textTranslateX.get() - delta,
 878                                         caretWidth / 2));
 879         }
 880         if (SHOW_HANDLES) {
 881             caretHandle.setLayoutX(caretX - caretHandle.getWidth() / 2 + 1);
 882         }
 883     }
 884 
 885     private void scrollAfterDelete(double textMaxXOld, double caretMaxXOld) {
 886         final Bounds textLayoutBounds = textNode.getLayoutBounds();
 887         final Bounds textBounds = textNode.localToParent(textLayoutBounds);
 888         final Bounds clipBounds = clip.getBoundsInParent();
 889         final Bounds caretBounds = caretPath.getLayoutBounds();
 890 
 891         switch (getHAlignment()) {
 892           case RIGHT:
 893             if (textBounds.getMaxX() > clipBounds.getMaxX()) {
 894                 double delta = caretMaxXOld - caretBounds.getMaxX() - textTranslateX.get();
 895                 if (textBounds.getMaxX() + delta < clipBounds.getMaxX()) {
 896                     if (textMaxXOld <= clipBounds.getMaxX()) {
 897                         delta = textMaxXOld - textBounds.getMaxX();
 898                     } else {
 899                         delta = clipBounds.getMaxX() - textBounds.getMaxX();
 900                     }
 901                 }
 902                 textTranslateX.set(textTranslateX.get() + delta);
 903             } else {
 904                 updateTextPos();
 905             }
 906             break;
 907 
 908           case LEFT:
 909           case CENTER:
 910           default:
 911             if (textBounds.getMinX() < clipBounds.getMinX() + caretWidth / 2 &&
 912                 textBounds.getMaxX() <= clipBounds.getMaxX()) {
 913                 double delta = caretMaxXOld - caretBounds.getMaxX() - textTranslateX.get();
 914                 if (textBounds.getMaxX() + delta < clipBounds.getMaxX()) {
 915                     if (textMaxXOld <= clipBounds.getMaxX()) {
 916                         delta = textMaxXOld - textBounds.getMaxX();
 917                     } else {
 918                         delta = clipBounds.getMaxX() - textBounds.getMaxX();
 919                     }
 920                 }
 921                 textTranslateX.set(textTranslateX.get() + delta);
 922             }
 923         }
 924 
 925         updateCaretOff();
 926     }
 927 }