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