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     /**
 475      * Updates the textTranslateX value for the Text node position. This is
 476      * done for general layout, but care is taken to avoid resetting the
 477      * position when there's a need to scroll the text due to caret movement,
 478      * or when editing text that overflows on either side.
 479      */
 480     private void updateTextPos() {
 481         double oldX = textTranslateX.get();
 482         double newX;
 483         double textNodeWidth = textNode.getLayoutBounds().getWidth();
 484 
 485         switch (getHAlignment()) {
 486           case CENTER:
 487             double midPoint = textRight.get() / 2;
 488             if (usePromptText.get()) {
 489                 // If a prompt is shown (which implies that the text is
 490                 // empty), then we align the Text node so that the caret will
 491                 // appear at the left of the centered prompt.
 492                 newX = midPoint - promptNode.getLayoutBounds().getWidth() / 2;
 493                 promptNode.setLayoutX(newX);
 494             } else {
 495                 newX = midPoint - textNodeWidth / 2;
 496             }
 497             // Update if there is space on the right
 498             if (newX + textNodeWidth <= textRight.get()) {
 499                 textTranslateX.set(newX);
 500             }
 501             break;
 502 
 503           case RIGHT:
 504             newX = textRight.get() - textNodeWidth - caretWidth / 2;
 505             // Update if there is space on the right
 506             if (newX > oldX || newX > 0) {
 507                 textTranslateX.set(newX);
 508             }
 509             if (usePromptText.get()) {
 510                 promptNode.setLayoutX(textRight.get() - promptNode.getLayoutBounds().getWidth() -
 511                                       caretWidth / 2);
 512             }
 513             break;
 514 
 515           case LEFT:
 516           default:
 517             newX = caretWidth / 2;
 518             // Update if there is space on either side.
 519             if (newX < oldX || newX + textNodeWidth <= textRight.get()) {
 520                 textTranslateX.set(newX);
 521             }
 522             if (usePromptText.get()) {
 523                 promptNode.layoutXProperty().set(newX);
 524             }
 525         }
 526     }
 527 
 528     // should be called when the padding changes, or the text box width, or
 529     // the dot moves
 530     protected void updateCaretOff() {
 531         double delta = 0.0;
 532         double caretX = caretPath.getLayoutBounds().getMinX() + textTranslateX.get();
 533         // If the caret position is less than or equal to the left edge of the
 534         // clip then the caret will be clipped. We want the caret to end up
 535         // being positioned one pixel right of the clip's left edge. The same
 536         // applies on the right edge (but going the other direction of course).
 537         if (caretX < 0) {
 538             // I'll end up with a negative number
 539             delta = caretX;
 540         } else if (caretX > (textRight.get() - caretWidth)) {
 541             // I'll end up with a positive number
 542             delta = caretX - (textRight.get() - caretWidth);
 543         }
 544 
 545         // If delta is negative, then translate in the negative direction
 546         // to cause the text to scroll to the right. Vice-versa for positive.
 547         switch (getHAlignment()) {
 548           case CENTER:
 549             textTranslateX.set(textTranslateX.get() - delta);
 550             break;
 551 
 552           case RIGHT:
 553             textTranslateX.set(Math.max(textTranslateX.get() - delta,
 554                                         textRight.get() - textNode.getLayoutBounds().getWidth() -
 555                                         caretWidth / 2));
 556             break;
 557 
 558           case LEFT:
 559           default:
 560             textTranslateX.set(Math.min(textTranslateX.get() - delta,
 561                                         caretWidth / 2));
 562         }
 563         if (SHOW_HANDLES) {
 564             caretHandle.setLayoutX(caretX - caretHandle.getWidth() / 2 + 1);
 565         }
 566     }
 567 
 568     /**
 569      * Use this implementation instead of the one provided on TextInputControl.
 570      * updateCaretOff would get called to position the caret, but the text needs
 571      * to be scrolled appropriately.
 572      */
 573     public void replaceText(int start, int end, String txt) {
 574         final double textMaxXOld = textNode.getBoundsInParent().getMaxX();
 575         final double caretMaxXOld = caretPath.getLayoutBounds().getMaxX() + textTranslateX.get();
 576         getSkinnable().replaceText(start, end, txt);
 577         scrollAfterDelete(textMaxXOld, caretMaxXOld);
 578     }
 579 
 580     /**
 581      * Use this implementation instead of the one provided on TextInputControl
 582      * Simply calls into TextInputControl.deletePrevious/NextChar and responds appropriately
 583      * based on the return value.
 584      */
 585     public void deleteChar(boolean previous) {
 586         final double textMaxXOld = textNode.getBoundsInParent().getMaxX();
 587         final double caretMaxXOld = caretPath.getLayoutBounds().getMaxX() + textTranslateX.get();
 588         final boolean shouldBeep = previous ?
 589                 !getSkinnable().deletePreviousChar() :
 590                 !getSkinnable().deleteNextChar();
 591 
 592         if (shouldBeep) {
 593 //            beep();
 594         } else {
 595             scrollAfterDelete(textMaxXOld, caretMaxXOld);
 596         }
 597     }
 598 
 599     public void scrollAfterDelete(double textMaxXOld, double caretMaxXOld) {
 600         final Bounds textLayoutBounds = textNode.getLayoutBounds();
 601         final Bounds textBounds = textNode.localToParent(textLayoutBounds);
 602         final Bounds clipBounds = clip.getBoundsInParent();
 603         final Bounds caretBounds = caretPath.getLayoutBounds();
 604 
 605         switch (getHAlignment()) {
 606           case RIGHT:
 607             if (textBounds.getMaxX() > clipBounds.getMaxX()) {
 608                 double delta = caretMaxXOld - caretBounds.getMaxX() - textTranslateX.get();
 609                 if (textBounds.getMaxX() + delta < clipBounds.getMaxX()) {
 610                     if (textMaxXOld <= clipBounds.getMaxX()) {
 611                         delta = textMaxXOld - textBounds.getMaxX();
 612                     } else {
 613                         delta = clipBounds.getMaxX() - textBounds.getMaxX();
 614                     }
 615                 }
 616                 textTranslateX.set(textTranslateX.get() + delta);
 617             } else {
 618                 updateTextPos();
 619             }
 620             break;
 621 
 622           case LEFT:
 623           case CENTER:
 624           default:
 625             if (textBounds.getMinX() < clipBounds.getMinX() + caretWidth / 2 &&
 626                 textBounds.getMaxX() <= clipBounds.getMaxX()) {
 627                 double delta = caretMaxXOld - caretBounds.getMaxX() - textTranslateX.get();
 628                 if (textBounds.getMaxX() + delta < clipBounds.getMaxX()) {
 629                     if (textMaxXOld <= clipBounds.getMaxX()) {
 630                         delta = textMaxXOld - textBounds.getMaxX();
 631                     } else {
 632                         delta = clipBounds.getMaxX() - textBounds.getMaxX();
 633                     }
 634                 }
 635                 textTranslateX.set(textTranslateX.get() + delta);
 636             }
 637         }
 638 
 639         updateCaretOff();
 640     }
 641 
 642     public HitInfo getIndex(double x, double y) {
 643         // adjust the event to be in the same coordinate space as the
 644         // text content of the textInputControl
 645         Point2D p;
 646 
 647         p = new Point2D(x - textTranslateX.get() - snappedLeftInset(),
 648                         y - snappedTopInset());
 649         return textNode.impl_hitTestChar(translateCaretPosition(p));
 650     }
 651 
 652     public void positionCaret(HitInfo hit, boolean select) {
 653         TextField textField = getSkinnable();
 654         int pos = Utils.getHitInsertionIndex(hit, textField.textProperty().getValueSafe());
 655 
 656         if (select) {
 657             textField.selectPositionCaret(pos);
 658         } else {
 659             textField.positionCaret(pos);
 660         }
 661 
 662         setForwardBias(hit.isLeading());
 663     }
 664 
 665     @Override public Rectangle2D getCharacterBounds(int index) {
 666         double x, y;
 667         double width, height;
 668         if (index == textNode.getText().length()) {
 669             Bounds textNodeBounds = textNode.getBoundsInLocal();
 670             x = textNodeBounds.getMaxX();
 671             y = 0;
 672             width = 0;
 673             height = textNodeBounds.getMaxY();
 674         } else {
 675             characterBoundingPath.getElements().clear();
 676             characterBoundingPath.getElements().addAll(textNode.impl_getRangeShape(index, index + 1));
 677             characterBoundingPath.setLayoutX(textNode.getLayoutX());
 678             characterBoundingPath.setLayoutY(textNode.getLayoutY());
 679 
 680             Bounds bounds = characterBoundingPath.getBoundsInLocal();
 681 
 682             x = bounds.getMinX();
 683             y = bounds.getMinY();
 684             // Sometimes the bounds is empty, in which case we must ignore the width/height
 685             width  = bounds.isEmpty() ? 0 : bounds.getWidth();
 686             height = bounds.isEmpty() ? 0 : bounds.getHeight();
 687         }
 688 
 689         Bounds textBounds = textGroup.getBoundsInParent();
 690 
 691         return new Rectangle2D(x + textBounds.getMinX() + textTranslateX.get(),
 692                                y + textBounds.getMinY(), width, height);
 693     }
 694 
 695     @Override protected PathElement[] getUnderlineShape(int start, int end) {
 696         return textNode.impl_getUnderlineShape(start, end);
 697     }
 698 
 699     @Override protected PathElement[] getRangeShape(int start, int end) {
 700         return textNode.impl_getRangeShape(start, end);
 701     }
 702 
 703     @Override protected void addHighlight(List<? extends Node> nodes, int start) {
 704         textGroup.getChildren().addAll(nodes);
 705     }
 706 
 707     @Override protected void removeHighlight(List<? extends Node> nodes) {
 708         textGroup.getChildren().removeAll(nodes);
 709     }
 710 
 711     @Override public void nextCharacterVisually(boolean moveRight) {
 712         if (isRTL()) {
 713             // Text node is mirrored.
 714             moveRight = !moveRight;
 715         }
 716 
 717         Bounds caretBounds = caretPath.getLayoutBounds();
 718         if (caretPath.getElements().size() == 4) {
 719             // The caret is split
 720             // TODO: Find a better way to get the primary caret position
 721             // instead of depending on the internal implementation.
 722             // See RT-25465.
 723             caretBounds = new Path(caretPath.getElements().get(0), caretPath.getElements().get(1)).getLayoutBounds();
 724         }
 725         double hitX = moveRight ? caretBounds.getMaxX() : caretBounds.getMinX();
 726         double hitY = (caretBounds.getMinY() + caretBounds.getMaxY()) / 2;
 727         HitInfo hit = textNode.impl_hitTestChar(translateCaretPosition(new Point2D(hitX, hitY)));
 728         Path charShape = new Path(textNode.impl_getRangeShape(hit.getCharIndex(), hit.getCharIndex() + 1));
 729         if ((moveRight && charShape.getLayoutBounds().getMaxX() > caretBounds.getMaxX()) ||
 730             (!moveRight && charShape.getLayoutBounds().getMinX() < caretBounds.getMinX())) {
 731             hit.setLeading(!hit.isLeading());
 732         }
 733         positionCaret(hit, false);
 734     }
 735 
 736     @Override protected void layoutChildren(final double x, final double y,
 737                                             final double w, final double h) {
 738         super.layoutChildren(x, y, w, h);
 739 
 740         if (textNode != null) {
 741             double textY;
 742             final Bounds textNodeBounds = textNode.getLayoutBounds();
 743             final double ascent = textNode.getBaselineOffset();
 744             final double descent = textNodeBounds.getHeight() - ascent;
 745 
 746             switch (getSkinnable().getAlignment().getVpos()) {
 747                 case TOP:
 748                 textY = ascent;
 749                 break;
 750 
 751               case CENTER:
 752                 textY = (ascent + textGroup.getHeight() - descent) / 2;
 753                 break;
 754 
 755               case BOTTOM:
 756               default:
 757                 textY = textGroup.getHeight() - descent;
 758             }
 759             textNode.setY(textY);
 760             if (promptNode != null) {
 761                 promptNode.setY(textY);
 762             }
 763 
 764             if (getSkinnable().getWidth() > 0) {
 765                 updateTextPos();
 766                 updateCaretOff();
 767             }
 768         }
 769 
 770         if (SHOW_HANDLES) {
 771             handleGroup.setLayoutX(x + textTranslateX.get());
 772             handleGroup.setLayoutY(y);
 773 
 774             // Resize handles for caret and anchor.
 775 //            IndexRange selection = textField.getSelection();
 776             selectionHandle1.resize(selectionHandle1.prefWidth(-1),
 777                                     selectionHandle1.prefHeight(-1));
 778             selectionHandle2.resize(selectionHandle2.prefWidth(-1),
 779                                     selectionHandle2.prefHeight(-1));
 780             caretHandle.resize(caretHandle.prefWidth(-1),
 781                                caretHandle.prefHeight(-1));
 782 
 783             Bounds b = caretPath.getBoundsInParent();
 784             caretHandle.setLayoutY(b.getMaxY() - 1);
 785             //selectionHandle1.setLayoutY(b.getMaxY() - 1);
 786             selectionHandle1.setLayoutY(b.getMinY() - selectionHandle1.getHeight() + 1);
 787             selectionHandle2.setLayoutY(b.getMaxY() - 1);
 788         }
 789     }
 790 
 791     protected HPos getHAlignment() {
 792         HPos hPos = getSkinnable().getAlignment().getHpos();
 793         return hPos;
 794     }
 795 
 796     @Override public Point2D getMenuPosition() {
 797         Point2D p = super.getMenuPosition();
 798         if (p != null) {
 799             p = new Point2D(Math.max(0, p.getX() - textNode.getLayoutX() - snappedLeftInset() + textTranslateX.get()),
 800                             Math.max(0, p.getY() - textNode.getLayoutY() - snappedTopInset()));
 801         }
 802         return p;
 803     }
 804 
 805     @Override protected String maskText(String txt) {
 806         if (getSkinnable() instanceof PasswordField) {
 807             int n = txt.length();
 808             StringBuilder passwordBuilder = new StringBuilder(n);
 809             for (int i = 0; i < n; i++) {
 810                 passwordBuilder.append(BULLET);
 811             }
 812 
 813             return passwordBuilder.toString();
 814         } else {
 815             return txt;
 816         }
 817     }
 818 
 819     @Override
 820     protected Object accGetAttribute(Attribute attribute, Object... parameters) {
 821         switch (attribute) {
 822             case BOUNDS_FOR_RANGE:
 823             case OFFSET_AT_POINT:
 824                 return textNode.accGetAttribute(attribute, parameters);
 825             default: return super.accGetAttribute(attribute, parameters);
 826         }
 827     }
 828 }