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.skin.Utils;
  31 import javafx.animation.KeyFrame;
  32 import javafx.animation.Timeline;
  33 import javafx.application.Platform;
  34 import javafx.beans.binding.BooleanBinding;
  35 import javafx.beans.binding.DoubleBinding;
  36 import javafx.beans.binding.IntegerBinding;
  37 import javafx.beans.value.ObservableBooleanValue;
  38 import javafx.beans.value.ObservableIntegerValue;
  39 import javafx.collections.ListChangeListener;
  40 import javafx.collections.ObservableList;
  41 import javafx.event.ActionEvent;
  42 import javafx.event.EventHandler;
  43 import javafx.geometry.Bounds;
  44 import javafx.geometry.Orientation;
  45 import javafx.geometry.Point2D;
  46 import javafx.geometry.Rectangle2D;
  47 import javafx.geometry.VPos;
  48 import javafx.geometry.VerticalDirection;
  49 import javafx.scene.AccessibleAttribute;
  50 import javafx.scene.Group;
  51 import javafx.scene.Node;
  52 import javafx.scene.control.Accordion;
  53 import javafx.scene.control.Button;
  54 import javafx.scene.control.Control;
  55 import javafx.scene.control.IndexRange;
  56 import javafx.scene.control.ScrollPane;
  57 import javafx.scene.control.TextArea;
  58 import javafx.scene.input.MouseEvent;
  59 import javafx.scene.input.ScrollEvent;
  60 import javafx.scene.layout.Region;
  61 import javafx.scene.shape.MoveTo;
  62 import javafx.scene.shape.Path;
  63 import javafx.scene.shape.PathElement;
  64 import javafx.scene.text.Text;
  65 import javafx.scene.text.HitInfo;
  66 import javafx.util.Duration;
  67 
  68 import java.util.List;
  69 
  70 import static com.sun.javafx.PlatformUtil.isMac;
  71 import static com.sun.javafx.PlatformUtil.isWindows;
  72 
  73 /**
  74  * Default skin implementation for the {@link TextArea} control.
  75  *
  76  * @see TextArea
  77  * @since 9
  78  */
  79 public class TextAreaSkin extends TextInputControlSkin<TextArea> {
  80 
  81     /**************************************************************************
  82      *
  83      * Static fields
  84      *
  85      **************************************************************************/
  86 
  87     /** A shared helper object, used only by downLines(). */
  88     private static final Path tmpCaretPath = new Path();
  89 
  90 
  91 
  92     /**************************************************************************
  93      *
  94      * Private fields
  95      *
  96      **************************************************************************/
  97 
  98     final private TextArea textArea;
  99 
 100     // *** NOTE: Multiple node mode is not yet fully implemented *** //
 101     private final boolean USE_MULTIPLE_NODES = false;
 102 
 103     private final TextAreaBehavior behavior;
 104 
 105     private double computedMinWidth = Double.NEGATIVE_INFINITY;
 106     private double computedMinHeight = Double.NEGATIVE_INFINITY;
 107     private double computedPrefWidth = Double.NEGATIVE_INFINITY;
 108     private double computedPrefHeight = Double.NEGATIVE_INFINITY;
 109     private double widthForComputedPrefHeight = Double.NEGATIVE_INFINITY;
 110     private double characterWidth;
 111     private double lineHeight;
 112 
 113     private ContentView contentView = new ContentView();
 114     private Group paragraphNodes = new Group();
 115 
 116     private Text promptNode;
 117     private ObservableBooleanValue usePromptText;
 118 
 119     private ObservableIntegerValue caretPosition;
 120     private Group selectionHighlightGroup = new Group();
 121 
 122     private ScrollPane scrollPane;
 123     private Bounds oldViewportBounds;
 124 
 125     private VerticalDirection scrollDirection = null;
 126 
 127     private Path characterBoundingPath = new Path();
 128 
 129     private Timeline scrollSelectionTimeline = new Timeline();
 130     private EventHandler<ActionEvent> scrollSelectionHandler = event -> {
 131         switch (scrollDirection) {
 132             case UP: {
 133                 // TODO Get previous offset
 134                 break;
 135             }
 136 
 137             case DOWN: {
 138                 // TODO Get next offset
 139                 break;
 140             }
 141         }
 142     };
 143 
 144     private double pressX, pressY; // For dragging handles on embedded
 145     private boolean handlePressed;
 146 
 147     /**
 148      * Remembers horizontal position when traversing up / down.
 149      */
 150     double targetCaretX = -1;
 151 
 152 
 153 
 154     /**************************************************************************
 155      *
 156      * Constructors
 157      *
 158      **************************************************************************/
 159 
 160     /**
 161      * Creates a new TextAreaSkin instance, installing the necessary child
 162      * nodes into the Control {@link Control#getChildren() children} list, as
 163      * well as the necessary input mappings for handling key, mouse, etc events.
 164      *
 165      * @param control The control that this skin should be installed onto.
 166      */
 167     public TextAreaSkin(final TextArea control) {
 168         super(control);
 169 
 170         // install default input map for the text area control
 171         this.behavior = new TextAreaBehavior(control);
 172         this.behavior.setTextAreaSkin(this);
 173 //        control.setInputMap(behavior.getInputMap());
 174 
 175         this.textArea = control;
 176 
 177         caretPosition = new IntegerBinding() {
 178             { bind(control.caretPositionProperty()); }
 179             @Override protected int computeValue() {
 180                 return control.getCaretPosition();
 181             }
 182         };
 183         caretPosition.addListener((observable, oldValue, newValue) -> {
 184             targetCaretX = -1;
 185             if (newValue.intValue() > oldValue.intValue()) {
 186                 setForwardBias(true);
 187             }
 188         });
 189 
 190         forwardBiasProperty().addListener(observable -> {
 191             if (control.getWidth() > 0) {
 192                 updateTextNodeCaretPos(control.getCaretPosition());
 193             }
 194         });
 195 
 196 //        setManaged(false);
 197 
 198         // Initialize content
 199         scrollPane = new ScrollPane();
 200         scrollPane.setFitToWidth(control.isWrapText());
 201         scrollPane.setContent(contentView);
 202         getChildren().add(scrollPane);
 203 
 204         getSkinnable().addEventFilter(ScrollEvent.ANY, event -> {
 205             if (event.isDirect() && handlePressed) {
 206                 event.consume();
 207             }
 208         });
 209 
 210         // Add selection
 211         selectionHighlightGroup.setManaged(false);
 212         selectionHighlightGroup.setVisible(false);
 213         contentView.getChildren().add(selectionHighlightGroup);
 214 
 215         // Add content view
 216         paragraphNodes.setManaged(false);
 217         contentView.getChildren().add(paragraphNodes);
 218 
 219         // Add caret
 220         caretPath.setManaged(false);
 221         caretPath.setStrokeWidth(1);
 222         caretPath.fillProperty().bind(textFillProperty());
 223         caretPath.strokeProperty().bind(textFillProperty());
 224         // modifying visibility of the caret forces a layout-pass (RT-32373), so
 225         // instead we modify the opacity.
 226         caretPath.opacityProperty().bind(new DoubleBinding() {
 227             { bind(caretVisibleProperty()); }
 228             @Override protected double computeValue() {
 229                 return caretVisibleProperty().get() ? 1.0 : 0.0;
 230             }
 231         });
 232         contentView.getChildren().add(caretPath);
 233 
 234         if (SHOW_HANDLES) {
 235             contentView.getChildren().addAll(caretHandle, selectionHandle1, selectionHandle2);
 236         }
 237 
 238         scrollPane.hvalueProperty().addListener((observable, oldValue, newValue) -> {
 239             getSkinnable().setScrollLeft(newValue.doubleValue() * getScrollLeftMax());
 240         });
 241 
 242         scrollPane.vvalueProperty().addListener((observable, oldValue, newValue) -> {
 243             getSkinnable().setScrollTop(newValue.doubleValue() * getScrollTopMax());
 244         });
 245 
 246         // Initialize the scroll selection timeline
 247         scrollSelectionTimeline.setCycleCount(Timeline.INDEFINITE);
 248         List<KeyFrame> scrollSelectionFrames = scrollSelectionTimeline.getKeyFrames();
 249         scrollSelectionFrames.clear();
 250         scrollSelectionFrames.add(new KeyFrame(Duration.millis(350), scrollSelectionHandler));
 251 
 252         // Add initial text content
 253         for (int i = 0, n = USE_MULTIPLE_NODES ? control.getParagraphs().size() : 1; i < n; i++) {
 254             CharSequence paragraph = (n == 1) ? control.textProperty().getValueSafe() : control.getParagraphs().get(i);
 255             addParagraphNode(i, paragraph.toString());
 256         }
 257 
 258         control.selectionProperty().addListener((observable, oldValue, newValue) -> {
 259             // TODO Why do we need two calls here?
 260             control.requestLayout();
 261             contentView.requestLayout();
 262         });
 263 
 264         control.wrapTextProperty().addListener((observable, oldValue, newValue) -> {
 265             invalidateMetrics();
 266             scrollPane.setFitToWidth(newValue);
 267         });
 268 
 269         control.prefColumnCountProperty().addListener((observable, oldValue, newValue) -> {
 270             invalidateMetrics();
 271             updatePrefViewportWidth();
 272         });
 273 
 274         control.prefRowCountProperty().addListener((observable, oldValue, newValue) -> {
 275             invalidateMetrics();
 276             updatePrefViewportHeight();
 277         });
 278 
 279         updateFontMetrics();
 280         fontMetrics.addListener(valueModel -> {
 281             updateFontMetrics();
 282         });
 283 
 284         contentView.paddingProperty().addListener(valueModel -> {
 285             updatePrefViewportWidth();
 286             updatePrefViewportHeight();
 287         });
 288 
 289         scrollPane.viewportBoundsProperty().addListener(valueModel -> {
 290             if (scrollPane.getViewportBounds() != null) {
 291                 // ScrollPane creates a new Bounds instance for each
 292                 // layout pass, so we need to check if the width/height
 293                 // have really changed to avoid infinite layout requests.
 294                 Bounds newViewportBounds = scrollPane.getViewportBounds();
 295                 if (oldViewportBounds == null ||
 296                     oldViewportBounds.getWidth() != newViewportBounds.getWidth() ||
 297                     oldViewportBounds.getHeight() != newViewportBounds.getHeight()) {
 298 
 299                     invalidateMetrics();
 300                     oldViewportBounds = newViewportBounds;
 301                     contentView.requestLayout();
 302                 }
 303             }
 304         });
 305 
 306         control.scrollTopProperty().addListener((observable, oldValue, newValue) -> {
 307             double vValue = (newValue.doubleValue() < getScrollTopMax())
 308                                ? (newValue.doubleValue() / getScrollTopMax()) : 1.0;
 309             scrollPane.setVvalue(vValue);
 310         });
 311 
 312         control.scrollLeftProperty().addListener((observable, oldValue, newValue) -> {
 313             double hValue = (newValue.doubleValue() < getScrollLeftMax())
 314                                ? (newValue.doubleValue() / getScrollLeftMax()) : 1.0;
 315             scrollPane.setHvalue(hValue);
 316         });
 317 
 318         if (USE_MULTIPLE_NODES) {
 319             control.getParagraphs().addListener((ListChangeListener.Change<? extends CharSequence> change) -> {
 320                 while (change.next()) {
 321                     int from = change.getFrom();
 322                     int to = change.getTo();
 323                     List<? extends CharSequence> removed = change.getRemoved();
 324                     if (from < to) {
 325 
 326                         if (removed.isEmpty()) {
 327                             // This is an add
 328                             for (int i = from, n = to; i < n; i++) {
 329                                 addParagraphNode(i, change.getList().get(i).toString());
 330                             }
 331                         } else {
 332                             // This is an update
 333                             for (int i = from, n = to; i < n; i++) {
 334                                 Node node = paragraphNodes.getChildren().get(i);
 335                                 Text paragraphNode = (Text) node;
 336                                 paragraphNode.setText(change.getList().get(i).toString());
 337                             }
 338                         }
 339                     } else {
 340                         // This is a remove
 341                         paragraphNodes.getChildren().subList(from, from + removed.size()).clear();
 342                     }
 343                 }
 344             });
 345         } else {
 346             control.textProperty().addListener(observable -> {
 347                 invalidateMetrics();
 348                 ((Text)paragraphNodes.getChildren().get(0)).setText(control.textProperty().getValueSafe());
 349                 contentView.requestLayout();
 350             });
 351         }
 352 
 353         usePromptText = new BooleanBinding() {
 354             { bind(control.textProperty(), control.promptTextProperty()); }
 355             @Override protected boolean computeValue() {
 356                 String txt = control.getText();
 357                 String promptTxt = control.getPromptText();
 358                 return ((txt == null || txt.isEmpty()) &&
 359                         promptTxt != null && !promptTxt.isEmpty());
 360             }
 361         };
 362 
 363         if (usePromptText.get()) {
 364             createPromptNode();
 365         }
 366 
 367         usePromptText.addListener(observable -> {
 368             createPromptNode();
 369             control.requestLayout();
 370         });
 371 
 372         updateHighlightFill();
 373         updatePrefViewportWidth();
 374         updatePrefViewportHeight();
 375         if (control.isFocused()) setCaretAnimating(true);
 376 
 377         if (SHOW_HANDLES) {
 378             selectionHandle1.setRotate(180);
 379 
 380             EventHandler<MouseEvent> handlePressHandler = e -> {
 381                 pressX = e.getX();
 382                 pressY = e.getY();
 383                 handlePressed = true;
 384                 e.consume();
 385             };
 386 
 387             EventHandler<MouseEvent> handleReleaseHandler = event -> {
 388                 handlePressed = false;
 389             };
 390 
 391             caretHandle.setOnMousePressed(handlePressHandler);
 392             selectionHandle1.setOnMousePressed(handlePressHandler);
 393             selectionHandle2.setOnMousePressed(handlePressHandler);
 394 
 395             caretHandle.setOnMouseReleased(handleReleaseHandler);
 396             selectionHandle1.setOnMouseReleased(handleReleaseHandler);
 397             selectionHandle2.setOnMouseReleased(handleReleaseHandler);
 398 
 399             caretHandle.setOnMouseDragged(e -> {
 400                 Text textNode = getTextNode();
 401                 Point2D tp = textNode.localToScene(0, 0);
 402                 Point2D p = new Point2D(e.getSceneX() - tp.getX() - pressX + caretHandle.getWidth() / 2,
 403                                         e.getSceneY() - tp.getY() - pressY - 6);
 404                 HitInfo hit = textNode.hitTest(translateCaretPosition(p));
 405                 positionCaret(hit, false);
 406                 e.consume();
 407             });
 408 
 409             selectionHandle1.setOnMouseDragged(e -> {
 410                 TextArea control1 = getSkinnable();
 411                 Text textNode = getTextNode();
 412                 Point2D tp = textNode.localToScene(0, 0);
 413                 Point2D p = new Point2D(e.getSceneX() - tp.getX() - pressX + selectionHandle1.getWidth() / 2,
 414                                         e.getSceneY() - tp.getY() - pressY + selectionHandle1.getHeight() + 5);
 415                 HitInfo hit = textNode.hitTest(translateCaretPosition(p));
 416                 if (control1.getAnchor() < control1.getCaretPosition()) {
 417                     // Swap caret and anchor
 418                     control1.selectRange(control1.getCaretPosition(), control1.getAnchor());
 419                 }
 420                 int pos = hit.getCharIndex();
 421                 if (pos > 0) {
 422                     if (pos >= control1.getAnchor()) {
 423                         pos = control1.getAnchor();
 424                     }
 425                 }
 426                 positionCaret(hit, true);
 427                 e.consume();
 428             });
 429 
 430             selectionHandle2.setOnMouseDragged(e -> {
 431                 TextArea control1 = getSkinnable();
 432                 Text textNode = getTextNode();
 433                 Point2D tp = textNode.localToScene(0, 0);
 434                 Point2D p = new Point2D(e.getSceneX() - tp.getX() - pressX + selectionHandle2.getWidth() / 2,
 435                                         e.getSceneY() - tp.getY() - pressY - 6);
 436                 HitInfo hit = textNode.hitTest(translateCaretPosition(p));
 437                 if (control1.getAnchor() > control1.getCaretPosition()) {
 438                     // Swap caret and anchor
 439                     control1.selectRange(control1.getCaretPosition(), control1.getAnchor());
 440                 }
 441                 int pos = hit.getCharIndex();
 442                 if (pos > 0) {
 443                     if (pos <= control1.getAnchor() + 1) {
 444                         pos = Math.min(control1.getAnchor() + 2, control1.getLength());
 445                     }
 446                     positionCaret(hit, true);
 447                 }
 448                 e.consume();
 449             });
 450         }
 451     }
 452 
 453 
 454 
 455     /***************************************************************************
 456      *                                                                         *
 457      * Public API                                                              *
 458      *                                                                         *
 459      **************************************************************************/
 460 
 461     /** {@inheritDoc} */
 462     @Override protected void invalidateMetrics() {
 463         computedMinWidth = Double.NEGATIVE_INFINITY;
 464         computedMinHeight = Double.NEGATIVE_INFINITY;
 465         computedPrefWidth = Double.NEGATIVE_INFINITY;
 466         computedPrefHeight = Double.NEGATIVE_INFINITY;
 467     }
 468 
 469     /** {@inheritDoc} */
 470     @Override protected void layoutChildren(double contentX, double contentY, double contentWidth, double contentHeight) {
 471         scrollPane.resizeRelocate(contentX, contentY, contentWidth, contentHeight);
 472     }
 473 
 474     /** {@inheritDoc} */
 475     @Override protected void updateHighlightFill() {
 476         for (Node node : selectionHighlightGroup.getChildren()) {
 477             Path selectionHighlightPath = (Path)node;
 478             selectionHighlightPath.setFill(highlightFillProperty().get());
 479         }
 480     }
 481 
 482     // Public for behavior
 483     /**
 484      * Performs a hit test, mapping point to index in the content.
 485      *
 486      * @param x the x coordinate of the point.
 487      * @param y the y coordinate of the point.
 488      * @return a {@code HitInfo} object describing the index and forward bias.
 489      */
 490     public HitInfo getIndex(double x, double y) {
 491         // adjust the event to be in the same coordinate space as the
 492         // text content of the textInputControl
 493         Text textNode = getTextNode();
 494         Point2D p = new Point2D(x - textNode.getLayoutX(), y - getTextTranslateY());
 495         HitInfo hit = textNode.hitTest(translateCaretPosition(p));
 496         return hit;
 497     };
 498 
 499     /** {@inheritDoc} */
 500     @Override public void moveCaret(TextUnit unit, Direction dir, boolean select) {
 501         switch (unit) {
 502             case CHARACTER:
 503                 switch (dir) {
 504                     case LEFT:
 505                     case RIGHT:
 506                         nextCharacterVisually(dir == Direction.RIGHT);
 507                         break;
 508                     default:
 509                         throw new IllegalArgumentException(""+dir);
 510                 }
 511                 break;
 512 
 513             case LINE:
 514                 switch (dir) {
 515                     case UP:
 516                         previousLine(select);
 517                         break;
 518                     case DOWN:
 519                         nextLine(select);
 520                         break;
 521                     case BEGINNING:
 522                         lineStart(select, select && isMac());
 523                         break;
 524                     case END:
 525                         lineEnd(select, select && isMac());
 526                         break;
 527                     default:
 528                         throw new IllegalArgumentException(""+dir);
 529                 }
 530                 break;
 531 
 532             case PAGE:
 533                 switch (dir) {
 534                     case UP:
 535                         previousPage(select);
 536                         break;
 537                     case DOWN:
 538                         nextPage(select);
 539                         break;
 540                     default:
 541                         throw new IllegalArgumentException(""+dir);
 542                 }
 543                 break;
 544 
 545             case PARAGRAPH:
 546                 switch (dir) {
 547                     case UP:
 548                         paragraphStart(true, select);
 549                         break;
 550                     case DOWN:
 551                         paragraphEnd(true, select);
 552                         break;
 553                     case BEGINNING:
 554                         paragraphStart(false, select);
 555                         break;
 556                     case END:
 557                         paragraphEnd(false, select);
 558                         break;
 559                     default:
 560                         throw new IllegalArgumentException(""+dir);
 561                 }
 562                 break;
 563 
 564             default:
 565                 throw new IllegalArgumentException(""+unit);
 566         }
 567     }
 568 
 569     private void nextCharacterVisually(boolean moveRight) {
 570         if (isRTL()) {
 571             // Text node is mirrored.
 572             moveRight = !moveRight;
 573         }
 574 
 575         Text textNode = getTextNode();
 576         Bounds caretBounds = caretPath.getLayoutBounds();
 577         if (caretPath.getElements().size() == 4) {
 578             // The caret is split
 579             // TODO: Find a better way to get the primary caret position
 580             // instead of depending on the internal implementation.
 581             // See RT-25465.
 582             caretBounds = new Path(caretPath.getElements().get(0), caretPath.getElements().get(1)).getLayoutBounds();
 583         }
 584         double hitX = moveRight ? caretBounds.getMaxX() : caretBounds.getMinX();
 585         double hitY = (caretBounds.getMinY() + caretBounds.getMaxY()) / 2;
 586         HitInfo hit = textNode.hitTest(new Point2D(hitX, hitY));
 587         boolean leading = hit.isLeading();
 588         Path charShape = new Path(textNode.rangeShape(hit.getCharIndex(), hit.getCharIndex() + 1));
 589         if ((moveRight && charShape.getLayoutBounds().getMaxX() > caretBounds.getMaxX()) ||
 590                 (!moveRight && charShape.getLayoutBounds().getMinX() < caretBounds.getMinX())) {
 591             leading = !leading;
 592             positionCaret(hit.getInsertionIndex(), leading, false, false);
 593         } else {
 594             // We're at beginning or end of line. Try moving up / down.
 595             int dot = textArea.getCaretPosition();
 596             targetCaretX = moveRight ? 0 : Double.MAX_VALUE;
 597             // TODO: Use Bidi sniffing instead of assuming right means forward here?
 598             downLines(moveRight ? 1 : -1, false, false);
 599             targetCaretX = -1;
 600             if (dot == textArea.getCaretPosition()) {
 601                 if (moveRight) {
 602                     textArea.forward();
 603                 } else {
 604                     textArea.backward();
 605                 }
 606             }
 607         }
 608     }
 609 
 610     private void downLines(int nLines, boolean select, boolean extendSelection) {
 611         Text textNode = getTextNode();
 612         Bounds caretBounds = caretPath.getLayoutBounds();
 613 
 614         // The middle y coordinate of the the line we want to go to.
 615         double targetLineMidY = (caretBounds.getMinY() + caretBounds.getMaxY()) / 2 + nLines * lineHeight;
 616         if (targetLineMidY < 0) {
 617             targetLineMidY = 0;
 618         }
 619 
 620         // The target x for the caret. This may have been set during a
 621         // previous call.
 622         double x = (targetCaretX >= 0) ? targetCaretX : (caretBounds.getMaxX());
 623 
 624         // Find a text position for the target x,y.
 625         HitInfo hit = textNode.hitTest(translateCaretPosition(new Point2D(x, targetLineMidY)));
 626         int pos = hit.getCharIndex();
 627 
 628         // Save the old pos temporarily while testing the new one.
 629         int oldPos = textNode.getCaretPosition();
 630         boolean oldBias = textNode.isCaretBias();
 631         textNode.setCaretBias(hit.isLeading());
 632         textNode.setCaretPosition(pos);
 633         tmpCaretPath.getElements().clear();
 634         tmpCaretPath.getElements().addAll(textNode.getCaretShape());
 635         tmpCaretPath.setLayoutX(textNode.getLayoutX());
 636         tmpCaretPath.setLayoutY(textNode.getLayoutY());
 637         Bounds tmpCaretBounds = tmpCaretPath.getLayoutBounds();
 638         // The y for the middle of the row we found.
 639         double foundLineMidY = (tmpCaretBounds.getMinY() + tmpCaretBounds.getMaxY()) / 2;
 640         textNode.setCaretBias(oldBias);
 641         textNode.setCaretPosition(oldPos);
 642 
 643         // Test if the found line is in the correct direction and move
 644         // the caret.
 645         if (nLines == 0 ||
 646                 (nLines > 0 && foundLineMidY > caretBounds.getMaxY()) ||
 647                 (nLines < 0 && foundLineMidY < caretBounds.getMinY())) {
 648 
 649             positionCaret(hit.getInsertionIndex(), hit.isLeading(), select, extendSelection);
 650             targetCaretX = x;
 651         }
 652     }
 653 
 654     private void previousLine(boolean select) {
 655         downLines(-1, select, false);
 656     }
 657 
 658     private void nextLine(boolean select) {
 659         downLines(1, select, false);
 660     }
 661 
 662     private void previousPage(boolean select) {
 663         downLines(-(int)(scrollPane.getViewportBounds().getHeight() / lineHeight),
 664                 select, false);
 665     }
 666 
 667     private void nextPage(boolean select) {
 668         downLines((int)(scrollPane.getViewportBounds().getHeight() / lineHeight),
 669                 select, false);
 670     }
 671 
 672     private void lineStart(boolean select, boolean extendSelection) {
 673         targetCaretX = 0;
 674         downLines(0, select, extendSelection);
 675         targetCaretX = -1;
 676     }
 677 
 678     private void lineEnd(boolean select, boolean extendSelection) {
 679         targetCaretX = Double.MAX_VALUE;
 680         downLines(0, select, extendSelection);
 681         targetCaretX = -1;
 682     }
 683 
 684 
 685     private void paragraphStart(boolean previousIfAtStart, boolean select) {
 686         TextArea textArea = getSkinnable();
 687         String text = textArea.textProperty().getValueSafe();
 688         int pos = textArea.getCaretPosition();
 689 
 690         if (pos > 0) {
 691             if (previousIfAtStart && text.codePointAt(pos-1) == 0x0a) {
 692                 // We are at the beginning of a paragraph.
 693                 // Back up to the previous paragraph.
 694                 pos--;
 695             }
 696             // Back up to the beginning of this paragraph
 697             while (pos > 0 && text.codePointAt(pos-1) != 0x0a) {
 698                 pos--;
 699             }
 700             if (select) {
 701                 textArea.selectPositionCaret(pos);
 702             } else {
 703                 textArea.positionCaret(pos);
 704                 setForwardBias(true);
 705             }
 706         }
 707     }
 708 
 709     private void paragraphEnd(boolean goPastInitialNewline, boolean select) {
 710         TextArea textArea = getSkinnable();
 711         String text = textArea.textProperty().getValueSafe();
 712         int pos = textArea.getCaretPosition();
 713         int len = text.length();
 714         boolean wentPastInitialNewline = false;
 715         boolean goPastTrailingNewline = isWindows();
 716 
 717         if (pos < len) {
 718             if (goPastInitialNewline && text.codePointAt(pos) == 0x0a) {
 719                 // We are at the end of a paragraph, start by moving to the
 720                 // next paragraph.
 721                 pos++;
 722                 wentPastInitialNewline = true;
 723             }
 724             if (!(goPastTrailingNewline && wentPastInitialNewline)) {
 725                 // Go to the end of this paragraph
 726                 while (pos < len && text.codePointAt(pos) != 0x0a) {
 727                     pos++;
 728                 }
 729                 if (goPastTrailingNewline && pos < len) {
 730                     // We are at the end of a paragraph, finish by moving to
 731                     // the beginning of the next paragraph (Windows behavior).
 732                     pos++;
 733                 }
 734             }
 735             if (select) {
 736                 textArea.selectPositionCaret(pos);
 737             } else {
 738                 textArea.positionCaret(pos);
 739             }
 740         }
 741     }
 742 
 743     /** {@inheritDoc} */
 744     @Override protected PathElement[] getUnderlineShape(int start, int end) {
 745         int pStart = 0;
 746         for (Node node : paragraphNodes.getChildren()) {
 747             Text p = (Text)node;
 748             int pEnd = pStart + p.textProperty().getValueSafe().length();
 749             if (pEnd >= start) {
 750                 return p.underlineShape(start - pStart, end - pStart);
 751             }
 752             pStart = pEnd + 1;
 753         }
 754         return null;
 755     }
 756 
 757     /** {@inheritDoc} */
 758     @Override protected PathElement[] getRangeShape(int start, int end) {
 759         int pStart = 0;
 760         for (Node node : paragraphNodes.getChildren()) {
 761             Text p = (Text)node;
 762             int pEnd = pStart + p.textProperty().getValueSafe().length();
 763             if (pEnd >= start) {
 764                 return p.rangeShape(start - pStart, end - pStart);
 765             }
 766             pStart = pEnd + 1;
 767         }
 768         return null;
 769     }
 770 
 771     /** {@inheritDoc} */
 772     @Override protected void addHighlight(List<? extends Node> nodes, int start) {
 773         int pStart = 0;
 774         Text paragraphNode = null;
 775         for (Node node : paragraphNodes.getChildren()) {
 776             Text p = (Text)node;
 777             int pEnd = pStart + p.textProperty().getValueSafe().length();
 778             if (pEnd >= start) {
 779                 paragraphNode = p;
 780                 break;
 781             }
 782             pStart = pEnd + 1;
 783         }
 784 
 785         if (paragraphNode != null) {
 786             for (Node node : nodes) {
 787                 node.setLayoutX(paragraphNode.getLayoutX());
 788                 node.setLayoutY(paragraphNode.getLayoutY());
 789             }
 790         }
 791         contentView.getChildren().addAll(nodes);
 792     }
 793 
 794     /** {@inheritDoc} */
 795     @Override protected void removeHighlight(List<? extends Node> nodes) {
 796         contentView.getChildren().removeAll(nodes);
 797     }
 798 
 799     /** {@inheritDoc} */
 800     @Override public Point2D getMenuPosition() {
 801         contentView.layoutChildren();
 802         Point2D p = super.getMenuPosition();
 803         if (p != null) {
 804             p = new Point2D(Math.max(0, p.getX() - contentView.snappedLeftInset() - getSkinnable().getScrollLeft()),
 805                     Math.max(0, p.getY() - contentView.snappedTopInset() - getSkinnable().getScrollTop()));
 806         }
 807         return p;
 808     }
 809 
 810     // Public for FXVKSkin
 811     /**
 812      * @return the {@code Bounds} of the caret shape, relative to the {@code TextArea}.
 813      */
 814     public Bounds getCaretBounds() {
 815         return getSkinnable().sceneToLocal(caretPath.localToScene(caretPath.getBoundsInLocal()));
 816     }
 817 
 818     /** {@inheritDoc} */
 819     @Override protected Object queryAccessibleAttribute(AccessibleAttribute attribute, Object... parameters) {
 820         switch (attribute) {
 821             case LINE_FOR_OFFSET:
 822             case LINE_START:
 823             case LINE_END:
 824             case BOUNDS_FOR_RANGE:
 825             case OFFSET_AT_POINT:
 826                 Text text = getTextNode();
 827                 return text.queryAccessibleAttribute(attribute, parameters);
 828             default: return super.queryAccessibleAttribute(attribute, parameters);
 829         }
 830     }
 831 
 832     /** {@inheritDoc} */
 833     @Override public void dispose() {
 834         super.dispose();
 835 
 836         if (behavior != null) {
 837             behavior.dispose();
 838         }
 839 
 840         // TODO Unregister listeners on text editor, paragraph list
 841         throw new UnsupportedOperationException();
 842     }
 843 
 844     /** {@inheritDoc} */
 845     @Override public double computeBaselineOffset(double topInset, double rightInset, double bottomInset, double leftInset) {
 846         Text firstParagraph = (Text) paragraphNodes.getChildren().get(0);
 847         return Utils.getAscent(getSkinnable().getFont(), firstParagraph.getBoundsType())
 848                 + contentView.snappedTopInset() + textArea.snappedTopInset();
 849     }
 850 
 851     private char getCharacter(int index) {
 852         int n = paragraphNodes.getChildren().size();
 853 
 854         int paragraphIndex = 0;
 855         int offset = index;
 856 
 857         String paragraph = null;
 858         while (paragraphIndex < n) {
 859             Text paragraphNode = (Text)paragraphNodes.getChildren().get(paragraphIndex);
 860             paragraph = paragraphNode.getText();
 861             int count = paragraph.length() + 1;
 862 
 863             if (offset < count) {
 864                 break;
 865             }
 866 
 867             offset -= count;
 868             paragraphIndex++;
 869         }
 870 
 871         return offset == paragraph.length() ? '\n' : paragraph.charAt(offset);
 872     }
 873 
 874     /** {@inheritDoc} */
 875     @Override protected int getInsertionPoint(double x, double y) {
 876         TextArea textArea = getSkinnable();
 877 
 878         int n = paragraphNodes.getChildren().size();
 879         int index = -1;
 880 
 881         if (n > 0) {
 882             if (y < contentView.snappedTopInset()) {
 883                 // Select the character at x in the first row
 884                 Text paragraphNode = (Text)paragraphNodes.getChildren().get(0);
 885                 index = getNextInsertionPoint(paragraphNode, x, -1, VerticalDirection.DOWN);
 886             } else if (y > contentView.snappedTopInset() + contentView.getHeight()) {
 887                 // Select the character at x in the last row
 888                 int lastParagraphIndex = n - 1;
 889                 Text lastParagraphView = (Text)paragraphNodes.getChildren().get(lastParagraphIndex);
 890 
 891                 index = getNextInsertionPoint(lastParagraphView, x, -1, VerticalDirection.UP)
 892                         + (textArea.getLength() - lastParagraphView.getText().length());
 893             } else {
 894                 // Select the character at x in the row at y
 895                 int paragraphOffset = 0;
 896                 for (int i = 0; i < n; i++) {
 897                     Text paragraphNode = (Text)paragraphNodes.getChildren().get(i);
 898 
 899                     Bounds bounds = paragraphNode.getBoundsInLocal();
 900                     double paragraphViewY = paragraphNode.getLayoutY() + bounds.getMinY();
 901                     if (y >= paragraphViewY
 902                             && y < paragraphViewY + paragraphNode.getBoundsInLocal().getHeight()) {
 903                         index = getInsertionPoint(paragraphNode,
 904                                 x - paragraphNode.getLayoutX(),
 905                                 y - paragraphNode.getLayoutY()) + paragraphOffset;
 906                         break;
 907                     }
 908 
 909                     paragraphOffset += paragraphNode.getText().length() + 1;
 910                 }
 911             }
 912         }
 913 
 914         return index;
 915     }
 916 
 917     // Public for behavior
 918     /**
 919      * Moves the caret to the specified position.
 920      *
 921      * @param hit the new position and forward bias of the caret.
 922      * @param select whether to extend selection to the new position.
 923      */
 924     public void positionCaret(HitInfo hit, boolean select) {
 925         positionCaret(hit.getInsertionIndex(), hit.isLeading(), select, false);
 926     }
 927 
 928     private void positionCaret(int pos, boolean leading, boolean select, boolean extendSelection) {
 929         boolean isNewLine =
 930                 (pos > 0 &&
 931                         pos <= getSkinnable().getLength() &&
 932                         getSkinnable().getText().codePointAt(pos-1) == 0x0a);
 933 
 934         // special handling for a new line
 935         if (!leading && isNewLine) {
 936             leading = true;
 937             pos -= 1;
 938         }
 939 
 940         if (select) {
 941             if (extendSelection) {
 942                 getSkinnable().extendSelection(pos);
 943             } else {
 944                 getSkinnable().selectPositionCaret(pos);
 945             }
 946         } else {
 947             getSkinnable().positionCaret(pos);
 948         }
 949 
 950         setForwardBias(leading);
 951     }
 952 
 953     /** {@inheritDoc} */
 954     @Override public Rectangle2D getCharacterBounds(int index) {
 955         TextArea textArea = getSkinnable();
 956 
 957         int paragraphIndex = paragraphNodes.getChildren().size();
 958         int paragraphOffset = textArea.getLength() + 1;
 959 
 960         Text paragraphNode = null;
 961         do {
 962             paragraphNode = (Text)paragraphNodes.getChildren().get(--paragraphIndex);
 963             paragraphOffset -= paragraphNode.getText().length() + 1;
 964         } while (index < paragraphOffset);
 965 
 966         int characterIndex = index - paragraphOffset;
 967         boolean terminator = false;
 968 
 969         if (characterIndex == paragraphNode.getText().length()) {
 970             characterIndex--;
 971             terminator = true;
 972         }
 973 
 974         characterBoundingPath.getElements().clear();
 975         characterBoundingPath.getElements().addAll(paragraphNode.rangeShape(characterIndex, characterIndex + 1));
 976         characterBoundingPath.setLayoutX(paragraphNode.getLayoutX());
 977         characterBoundingPath.setLayoutY(paragraphNode.getLayoutY());
 978 
 979         Bounds bounds = characterBoundingPath.getBoundsInLocal();
 980 
 981         double x = bounds.getMinX() + paragraphNode.getLayoutX() - textArea.getScrollLeft();
 982         double y = bounds.getMinY() + paragraphNode.getLayoutY() - textArea.getScrollTop();
 983 
 984         // Sometimes the bounds is empty, in which case we must ignore the width/height
 985         double width = bounds.isEmpty() ? 0 : bounds.getWidth();
 986         double height = bounds.isEmpty() ? 0 : bounds.getHeight();
 987 
 988         if (terminator) {
 989             x += width;
 990             width = 0;
 991         }
 992 
 993         return new Rectangle2D(x, y, width, height);
 994     }
 995 
 996     /** {@inheritDoc} */
 997     @Override protected void scrollCharacterToVisible(final int index) {
 998         // TODO We queue a callback because when characters are added or
 999         // removed the bounds are not immediately updated; is this really
1000         // necessary?
1001 
1002         Platform.runLater(() -> {
1003             if (getSkinnable().getLength() == 0) {
1004                 return;
1005             }
1006             Rectangle2D characterBounds = getCharacterBounds(index);
1007             scrollBoundsToVisible(characterBounds);
1008         });
1009     }
1010 
1011 
1012 
1013     /**************************************************************************
1014      *
1015      * Private implementation
1016      *
1017      **************************************************************************/
1018 
1019     TextAreaBehavior getBehavior() {
1020         return behavior;
1021     }
1022 
1023     private void createPromptNode() {
1024         if (promptNode == null && usePromptText.get()) {
1025             promptNode = new Text();
1026             contentView.getChildren().add(0, promptNode);
1027             promptNode.setManaged(false);
1028             promptNode.getStyleClass().add("text");
1029             promptNode.visibleProperty().bind(usePromptText);
1030             promptNode.fontProperty().bind(getSkinnable().fontProperty());
1031             promptNode.textProperty().bind(getSkinnable().promptTextProperty());
1032             promptNode.fillProperty().bind(promptTextFillProperty());
1033         }
1034     }
1035 
1036     private void addParagraphNode(int i, String string) {
1037         final TextArea textArea = getSkinnable();
1038         Text paragraphNode = new Text(string);
1039         paragraphNode.setTextOrigin(VPos.TOP);
1040         paragraphNode.setManaged(false);
1041         paragraphNode.getStyleClass().add("text");
1042         paragraphNode.boundsTypeProperty().addListener((observable, oldValue, newValue) -> {
1043             invalidateMetrics();
1044             updateFontMetrics();
1045         });
1046         paragraphNodes.getChildren().add(i, paragraphNode);
1047 
1048         paragraphNode.fontProperty().bind(textArea.fontProperty());
1049         paragraphNode.fillProperty().bind(textFillProperty());
1050         paragraphNode.selectionFillProperty().bind(highlightTextFillProperty());
1051     }
1052 
1053     private double getScrollTopMax() {
1054         return Math.max(0, contentView.getHeight() - scrollPane.getViewportBounds().getHeight());
1055     }
1056 
1057     private double getScrollLeftMax() {
1058         return Math.max(0, contentView.getWidth() - scrollPane.getViewportBounds().getWidth());
1059     }
1060 
1061     private int getInsertionPoint(Text paragraphNode, double x, double y) {
1062         HitInfo hitInfo = paragraphNode.hitTest(new Point2D(x, y));
1063         return hitInfo.getInsertionIndex();
1064     }
1065 
1066     private int getNextInsertionPoint(Text paragraphNode, double x, int from,
1067         VerticalDirection scrollDirection) {
1068         // TODO
1069         return 0;
1070     }
1071 
1072     private void scrollCaretToVisible() {
1073         TextArea textArea = getSkinnable();
1074         Bounds bounds = caretPath.getLayoutBounds();
1075         double x = bounds.getMinX() - textArea.getScrollLeft();
1076         double y = bounds.getMinY() - textArea.getScrollTop();
1077         double w = bounds.getWidth();
1078         double h = bounds.getHeight();
1079 
1080         if (SHOW_HANDLES) {
1081             if (caretHandle.isVisible()) {
1082                 h += caretHandle.getHeight();
1083             } else if (selectionHandle1.isVisible() && selectionHandle2.isVisible()) {
1084                 x -= selectionHandle1.getWidth() / 2;
1085                 y -= selectionHandle1.getHeight();
1086                 w += selectionHandle1.getWidth() / 2 + selectionHandle2.getWidth() / 2;
1087                 h += selectionHandle1.getHeight() + selectionHandle2.getHeight();
1088             }
1089         }
1090 
1091         if (w > 0 && h > 0) {
1092             scrollBoundsToVisible(new Rectangle2D(x, y, w, h));
1093         }
1094     }
1095 
1096     private void scrollBoundsToVisible(Rectangle2D bounds) {
1097         TextArea textArea = getSkinnable();
1098         Bounds viewportBounds = scrollPane.getViewportBounds();
1099 
1100         double viewportWidth = viewportBounds.getWidth();
1101         double viewportHeight = viewportBounds.getHeight();
1102         double scrollTop = textArea.getScrollTop();
1103         double scrollLeft = textArea.getScrollLeft();
1104         double slop = 6.0;
1105 
1106         if (bounds.getMinY() < 0) {
1107             double y = scrollTop + bounds.getMinY();
1108             if (y <= contentView.snappedTopInset()) {
1109                 y = 0;
1110             }
1111             textArea.setScrollTop(y);
1112         } else if (contentView.snappedTopInset() + bounds.getMaxY() > viewportHeight) {
1113             double y = scrollTop + contentView.snappedTopInset() + bounds.getMaxY() - viewportHeight;
1114             if (y >= getScrollTopMax() - contentView.snappedBottomInset()) {
1115                 y = getScrollTopMax();
1116             }
1117             textArea.setScrollTop(y);
1118         }
1119 
1120 
1121         if (bounds.getMinX() < 0) {
1122             double x = scrollLeft + bounds.getMinX() - slop;
1123             if (x <= contentView.snappedLeftInset() + slop) {
1124                 x = 0;
1125             }
1126             textArea.setScrollLeft(x);
1127         } else if (contentView.snappedLeftInset() + bounds.getMaxX() > viewportWidth) {
1128             double x = scrollLeft + contentView.snappedLeftInset() + bounds.getMaxX() - viewportWidth + slop;
1129             if (x >= getScrollLeftMax() - contentView.snappedRightInset() - slop) {
1130                 x = getScrollLeftMax();
1131             }
1132             textArea.setScrollLeft(x);
1133         }
1134     }
1135 
1136     private void updatePrefViewportWidth() {
1137         int columnCount = getSkinnable().getPrefColumnCount();
1138         scrollPane.setPrefViewportWidth(columnCount * characterWidth + contentView.snappedLeftInset() + contentView.snappedRightInset());
1139         scrollPane.setMinViewportWidth(characterWidth + contentView.snappedLeftInset() + contentView.snappedRightInset());
1140     }
1141 
1142     private void updatePrefViewportHeight() {
1143         int rowCount = getSkinnable().getPrefRowCount();
1144         scrollPane.setPrefViewportHeight(rowCount * lineHeight + contentView.snappedTopInset() + contentView.snappedBottomInset());
1145         scrollPane.setMinViewportHeight(lineHeight + contentView.snappedTopInset() + contentView.snappedBottomInset());
1146     }
1147 
1148     private void updateFontMetrics() {
1149         Text firstParagraph = (Text)paragraphNodes.getChildren().get(0);
1150         lineHeight = Utils.getLineHeight(getSkinnable().getFont(), firstParagraph.getBoundsType());
1151         characterWidth = fontMetrics.get().computeStringWidth("W");
1152     }
1153 
1154     private double getTextTranslateX() {
1155         return contentView.snappedLeftInset();
1156     }
1157 
1158     private double getTextTranslateY() {
1159         return contentView.snappedTopInset();
1160     }
1161 
1162     private double getTextLeft() {
1163         return 0;
1164     }
1165 
1166     private Point2D translateCaretPosition(Point2D p) {
1167         return p;
1168     }
1169 
1170     private Text getTextNode() {
1171         if (USE_MULTIPLE_NODES) {
1172             throw new IllegalArgumentException("Multiple node traversal is not yet implemented.");
1173         }
1174         return (Text)paragraphNodes.getChildren().get(0);
1175     }
1176 
1177     private void updateTextNodeCaretPos(int pos) {
1178         Text textNode = getTextNode();
1179         if (isForwardBias()) {
1180             textNode.setCaretPosition(pos);
1181         } else {
1182             textNode.setCaretPosition(pos - 1);
1183         }
1184         textNode.caretBiasProperty().set(isForwardBias());
1185     }
1186 
1187 
1188 
1189     /**************************************************************************
1190      *
1191      * Support classes
1192      *
1193      **************************************************************************/
1194 
1195     private class ContentView extends Region {
1196         {
1197             getStyleClass().add("content");
1198 
1199             addEventHandler(MouseEvent.MOUSE_PRESSED, event -> {
1200                 behavior.mousePressed(event);
1201                 event.consume();
1202             });
1203 
1204             addEventHandler(MouseEvent.MOUSE_RELEASED, event -> {
1205                 behavior.mouseReleased(event);
1206                 event.consume();
1207             });
1208 
1209             addEventHandler(MouseEvent.MOUSE_DRAGGED, event -> {
1210                 behavior.mouseDragged(event);
1211                 event.consume();
1212             });
1213         }
1214 
1215         @Override protected ObservableList<Node> getChildren() {
1216             return super.getChildren();
1217         }
1218 
1219         @Override public Orientation getContentBias() {
1220             return Orientation.HORIZONTAL;
1221         }
1222 
1223         @Override protected double computePrefWidth(double height) {
1224             if (computedPrefWidth < 0) {
1225                 double prefWidth = 0;
1226 
1227                 for (Node node : paragraphNodes.getChildren()) {
1228                     Text paragraphNode = (Text)node;
1229                     prefWidth = Math.max(prefWidth,
1230                             Utils.computeTextWidth(paragraphNode.getFont(),
1231                                     paragraphNode.getText(), 0));
1232                 }
1233 
1234                 prefWidth += snappedLeftInset() + snappedRightInset();
1235 
1236                 Bounds viewPortBounds = scrollPane.getViewportBounds();
1237                 computedPrefWidth = Math.max(prefWidth, (viewPortBounds != null) ? viewPortBounds.getWidth() : 0);
1238             }
1239             return computedPrefWidth;
1240         }
1241 
1242         @Override protected double computePrefHeight(double width) {
1243             if (width != widthForComputedPrefHeight) {
1244                 invalidateMetrics();
1245                 widthForComputedPrefHeight = width;
1246             }
1247 
1248             if (computedPrefHeight < 0) {
1249                 double wrappingWidth;
1250                 if (width == -1) {
1251                     wrappingWidth = 0;
1252                 } else {
1253                     wrappingWidth = Math.max(width - (snappedLeftInset() + snappedRightInset()), 0);
1254                 }
1255 
1256                 double prefHeight = 0;
1257 
1258                 for (Node node : paragraphNodes.getChildren()) {
1259                     Text paragraphNode = (Text)node;
1260                     prefHeight += Utils.computeTextHeight(
1261                             paragraphNode.getFont(),
1262                             paragraphNode.getText(),
1263                             wrappingWidth,
1264                             paragraphNode.getBoundsType());
1265                 }
1266 
1267                 prefHeight += snappedTopInset() + snappedBottomInset();
1268 
1269                 Bounds viewPortBounds = scrollPane.getViewportBounds();
1270                 computedPrefHeight = Math.max(prefHeight, (viewPortBounds != null) ? viewPortBounds.getHeight() : 0);
1271             }
1272             return computedPrefHeight;
1273         }
1274 
1275         @Override protected double computeMinWidth(double height) {
1276             if (computedMinWidth < 0) {
1277                 double hInsets = snappedLeftInset() + snappedRightInset();
1278                 computedMinWidth = Math.min(characterWidth + hInsets, computePrefWidth(height));
1279             }
1280             return computedMinWidth;
1281         }
1282 
1283         @Override protected double computeMinHeight(double width) {
1284             if (computedMinHeight < 0) {
1285                 double vInsets = snappedTopInset() + snappedBottomInset();
1286                 computedMinHeight = Math.min(lineHeight + vInsets, computePrefHeight(width));
1287             }
1288             return computedMinHeight;
1289         }
1290 
1291         @Override public void layoutChildren() {
1292             TextArea textArea = getSkinnable();
1293             double width = getWidth();
1294 
1295             // Lay out paragraphs
1296             final double topPadding = snappedTopInset();
1297             final double leftPadding = snappedLeftInset();
1298 
1299             double wrappingWidth = Math.max(width - (leftPadding + snappedRightInset()), 0);
1300 
1301             double y = topPadding;
1302 
1303             final List<Node> paragraphNodesChildren = paragraphNodes.getChildren();
1304 
1305             for (int i = 0; i < paragraphNodesChildren.size(); i++) {
1306                 Node node = paragraphNodesChildren.get(i);
1307                 Text paragraphNode = (Text)node;
1308                 paragraphNode.setWrappingWidth(wrappingWidth);
1309 
1310                 Bounds bounds = paragraphNode.getBoundsInLocal();
1311                 paragraphNode.setLayoutX(leftPadding);
1312                 paragraphNode.setLayoutY(y);
1313 
1314                 y += bounds.getHeight();
1315             }
1316 
1317             if (promptNode != null) {
1318                 promptNode.setLayoutX(leftPadding);
1319                 promptNode.setLayoutY(topPadding + promptNode.getBaselineOffset());
1320                 promptNode.setWrappingWidth(wrappingWidth);
1321             }
1322 
1323             // Update the selection
1324             IndexRange selection = textArea.getSelection();
1325             Bounds oldCaretBounds = caretPath.getBoundsInParent();
1326 
1327             selectionHighlightGroup.getChildren().clear();
1328 
1329             int caretPos = textArea.getCaretPosition();
1330             int anchorPos = textArea.getAnchor();
1331 
1332             if (SHOW_HANDLES) {
1333                 // Install and resize the handles for caret and anchor.
1334                 if (selection.getLength() > 0) {
1335                     selectionHandle1.resize(selectionHandle1.prefWidth(-1),
1336                             selectionHandle1.prefHeight(-1));
1337                     selectionHandle2.resize(selectionHandle2.prefWidth(-1),
1338                             selectionHandle2.prefHeight(-1));
1339                 } else {
1340                     caretHandle.resize(caretHandle.prefWidth(-1),
1341                             caretHandle.prefHeight(-1));
1342                 }
1343 
1344                 // Position the handle for the anchor. This could be handle1 or handle2.
1345                 // Do this before positioning the actual caret.
1346                 if (selection.getLength() > 0) {
1347                     int paragraphIndex = paragraphNodesChildren.size();
1348                     int paragraphOffset = textArea.getLength() + 1;
1349                     Text paragraphNode = null;
1350                     do {
1351                         paragraphNode = (Text)paragraphNodesChildren.get(--paragraphIndex);
1352                         paragraphOffset -= paragraphNode.getText().length() + 1;
1353                     } while (anchorPos < paragraphOffset);
1354 
1355                     updateTextNodeCaretPos(anchorPos - paragraphOffset);
1356                     caretPath.getElements().clear();
1357                     caretPath.getElements().addAll(paragraphNode.getCaretShape());
1358                     caretPath.setLayoutX(paragraphNode.getLayoutX());
1359                     caretPath.setLayoutY(paragraphNode.getLayoutY());
1360 
1361                     Bounds b = caretPath.getBoundsInParent();
1362                     if (caretPos < anchorPos) {
1363                         selectionHandle2.setLayoutX(b.getMinX() - selectionHandle2.getWidth() / 2);
1364                         selectionHandle2.setLayoutY(b.getMaxY() - 1);
1365                     } else {
1366                         selectionHandle1.setLayoutX(b.getMinX() - selectionHandle1.getWidth() / 2);
1367                         selectionHandle1.setLayoutY(b.getMinY() - selectionHandle1.getHeight() + 1);
1368                     }
1369                 }
1370             }
1371 
1372             {
1373                 // Position caret
1374                 int paragraphIndex = paragraphNodesChildren.size();
1375                 int paragraphOffset = textArea.getLength() + 1;
1376 
1377                 Text paragraphNode = null;
1378                 do {
1379                     paragraphNode = (Text)paragraphNodesChildren.get(--paragraphIndex);
1380                     paragraphOffset -= paragraphNode.getText().length() + 1;
1381                 } while (caretPos < paragraphOffset);
1382 
1383                 updateTextNodeCaretPos(caretPos - paragraphOffset);
1384 
1385                 caretPath.getElements().clear();
1386                 caretPath.getElements().addAll(paragraphNode.getCaretShape());
1387 
1388                 caretPath.setLayoutX(paragraphNode.getLayoutX());
1389 
1390                 // TODO: Remove this temporary workaround for RT-27533
1391                 paragraphNode.setLayoutX(2 * paragraphNode.getLayoutX() - paragraphNode.getBoundsInParent().getMinX());
1392 
1393                 caretPath.setLayoutY(paragraphNode.getLayoutY());
1394                 if (oldCaretBounds == null || !oldCaretBounds.equals(caretPath.getBoundsInParent())) {
1395                     scrollCaretToVisible();
1396                 }
1397             }
1398 
1399             // Update selection fg and bg
1400             int start = selection.getStart();
1401             int end = selection.getEnd();
1402             for (int i = 0, max = paragraphNodesChildren.size(); i < max; i++) {
1403                 Node paragraphNode = paragraphNodesChildren.get(i);
1404                 Text textNode = (Text)paragraphNode;
1405                 int paragraphLength = textNode.getText().length() + 1;
1406                 if (end > start && start < paragraphLength) {
1407                     textNode.setSelectionStart(start);
1408                     textNode.setSelectionEnd(Math.min(end, paragraphLength));
1409 
1410                     Path selectionHighlightPath = new Path();
1411                     selectionHighlightPath.setManaged(false);
1412                     selectionHighlightPath.setStroke(null);
1413                     PathElement[] selectionShape = textNode.getSelectionShape();
1414                     if (selectionShape != null) {
1415                         selectionHighlightPath.getElements().addAll(selectionShape);
1416                     }
1417                     selectionHighlightGroup.getChildren().add(selectionHighlightPath);
1418                     selectionHighlightGroup.setVisible(true);
1419                     selectionHighlightPath.setLayoutX(textNode.getLayoutX());
1420                     selectionHighlightPath.setLayoutY(textNode.getLayoutY());
1421                     updateHighlightFill();
1422                 } else {
1423                     textNode.setSelectionStart(-1);
1424                     textNode.setSelectionEnd(-1);
1425                     selectionHighlightGroup.setVisible(false);
1426                 }
1427                 start = Math.max(0, start - paragraphLength);
1428                 end   = Math.max(0, end   - paragraphLength);
1429             }
1430 
1431             if (SHOW_HANDLES) {
1432                 // Position handle for the caret. This could be handle1 or handle2 when
1433                 // a selection is active.
1434                 Bounds b = caretPath.getBoundsInParent();
1435                 if (selection.getLength() > 0) {
1436                     if (caretPos < anchorPos) {
1437                         selectionHandle1.setLayoutX(b.getMinX() - selectionHandle1.getWidth() / 2);
1438                         selectionHandle1.setLayoutY(b.getMinY() - selectionHandle1.getHeight() + 1);
1439                     } else {
1440                         selectionHandle2.setLayoutX(b.getMinX() - selectionHandle2.getWidth() / 2);
1441                         selectionHandle2.setLayoutY(b.getMaxY() - 1);
1442                     }
1443                 } else {
1444                     caretHandle.setLayoutX(b.getMinX() - caretHandle.getWidth() / 2 + 1);
1445                     caretHandle.setLayoutY(b.getMaxY());
1446                 }
1447             }
1448 
1449             if (scrollPane.getPrefViewportWidth() == 0
1450                     || scrollPane.getPrefViewportHeight() == 0) {
1451                 updatePrefViewportWidth();
1452                 updatePrefViewportHeight();
1453                 if (getParent() != null && scrollPane.getPrefViewportWidth() > 0
1454                         || scrollPane.getPrefViewportHeight() > 0) {
1455                     // Force layout of viewRect in ScrollPaneSkin
1456                     getParent().requestLayout();
1457                 }
1458             }
1459 
1460             // RT-36454: Fit to width/height only if smaller than viewport.
1461             // That is, grow to fit but don't shrink to fit.
1462             Bounds viewportBounds = scrollPane.getViewportBounds();
1463             boolean wasFitToWidth = scrollPane.isFitToWidth();
1464             boolean wasFitToHeight = scrollPane.isFitToHeight();
1465             boolean setFitToWidth = textArea.isWrapText() || computePrefWidth(-1) <= viewportBounds.getWidth();
1466             boolean setFitToHeight = computePrefHeight(width) <= viewportBounds.getHeight();
1467             if (wasFitToWidth != setFitToWidth || wasFitToHeight != setFitToHeight) {
1468                 Platform.runLater(() -> {
1469                     scrollPane.setFitToWidth(setFitToWidth);
1470                     scrollPane.setFitToHeight(setFitToHeight);
1471                 });
1472                 getParent().requestLayout();
1473             }
1474         }
1475     }
1476 }