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