1 /*
   2  * Copyright (c) 2011, 2017, 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 static 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 (control.getWidth() > 0) {
 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      * Gets the {@code Bounds} of the caret of the skinned {@code TextArea}.
 813      * @return the {@code Bounds} of the caret shape, relative to the {@code TextArea}.
 814      */
 815     public Bounds getCaretBounds() {
 816         return getSkinnable().sceneToLocal(caretPath.localToScene(caretPath.getBoundsInLocal()));
 817     }
 818 
 819     /** {@inheritDoc} */
 820     @Override protected Object queryAccessibleAttribute(AccessibleAttribute attribute, Object... parameters) {
 821         switch (attribute) {
 822             case LINE_FOR_OFFSET:
 823             case LINE_START:
 824             case LINE_END:
 825             case BOUNDS_FOR_RANGE:
 826             case OFFSET_AT_POINT:
 827                 Text text = getTextNode();
 828                 return text.queryAccessibleAttribute(attribute, parameters);
 829             default: return super.queryAccessibleAttribute(attribute, parameters);
 830         }
 831     }
 832 
 833     /** {@inheritDoc} */
 834     @Override public void dispose() {
 835         super.dispose();
 836 
 837         if (behavior != null) {
 838             behavior.dispose();
 839         }
 840 
 841         // TODO Unregister listeners on text editor, paragraph list
 842         throw new UnsupportedOperationException();
 843     }
 844 
 845     /** {@inheritDoc} */
 846     @Override public double computeBaselineOffset(double topInset, double rightInset, double bottomInset, double leftInset) {
 847         Text firstParagraph = (Text) paragraphNodes.getChildren().get(0);
 848         return Utils.getAscent(getSkinnable().getFont(), firstParagraph.getBoundsType())
 849                 + contentView.snappedTopInset() + textArea.snappedTopInset();
 850     }
 851 
 852     private char getCharacter(int index) {
 853         int n = paragraphNodes.getChildren().size();
 854 
 855         int paragraphIndex = 0;
 856         int offset = index;
 857 
 858         String paragraph = null;
 859         while (paragraphIndex < n) {
 860             Text paragraphNode = (Text)paragraphNodes.getChildren().get(paragraphIndex);
 861             paragraph = paragraphNode.getText();
 862             int count = paragraph.length() + 1;
 863 
 864             if (offset < count) {
 865                 break;
 866             }
 867 
 868             offset -= count;
 869             paragraphIndex++;
 870         }
 871 
 872         return offset == paragraph.length() ? '\n' : paragraph.charAt(offset);
 873     }
 874 
 875     /** {@inheritDoc} */
 876     @Override protected int getInsertionPoint(double x, double y) {
 877         TextArea textArea = getSkinnable();
 878 
 879         int n = paragraphNodes.getChildren().size();
 880         int index = -1;
 881 
 882         if (n > 0) {
 883             if (y < contentView.snappedTopInset()) {
 884                 // Select the character at x in the first row
 885                 Text paragraphNode = (Text)paragraphNodes.getChildren().get(0);
 886                 index = getNextInsertionPoint(paragraphNode, x, -1, VerticalDirection.DOWN);
 887             } else if (y > contentView.snappedTopInset() + contentView.getHeight()) {
 888                 // Select the character at x in the last row
 889                 int lastParagraphIndex = n - 1;
 890                 Text lastParagraphView = (Text)paragraphNodes.getChildren().get(lastParagraphIndex);
 891 
 892                 index = getNextInsertionPoint(lastParagraphView, x, -1, VerticalDirection.UP)
 893                         + (textArea.getLength() - lastParagraphView.getText().length());
 894             } else {
 895                 // Select the character at x in the row at y
 896                 int paragraphOffset = 0;
 897                 for (int i = 0; i < n; i++) {
 898                     Text paragraphNode = (Text)paragraphNodes.getChildren().get(i);
 899 
 900                     Bounds bounds = paragraphNode.getBoundsInLocal();
 901                     double paragraphViewY = paragraphNode.getLayoutY() + bounds.getMinY();
 902                     if (y >= paragraphViewY
 903                             && y < paragraphViewY + paragraphNode.getBoundsInLocal().getHeight()) {
 904                         index = getInsertionPoint(paragraphNode,
 905                                 x - paragraphNode.getLayoutX(),
 906                                 y - paragraphNode.getLayoutY()) + paragraphOffset;
 907                         break;
 908                     }
 909 
 910                     paragraphOffset += paragraphNode.getText().length() + 1;
 911                 }
 912             }
 913         }
 914 
 915         return index;
 916     }
 917 
 918     // Public for behavior
 919     /**
 920      * Moves the caret to the specified position.
 921      *
 922      * @param hit the new position and forward bias of the caret.
 923      * @param select whether to extend selection to the new position.
 924      */
 925     public void positionCaret(HitInfo hit, boolean select) {
 926         positionCaret(hit.getInsertionIndex(), hit.isLeading(), select, false);
 927     }
 928 
 929     private void positionCaret(int pos, boolean leading, boolean select, boolean extendSelection) {
 930         boolean isNewLine =
 931                 (pos > 0 &&
 932                         pos <= getSkinnable().getLength() &&
 933                         getSkinnable().getText().codePointAt(pos-1) == 0x0a);
 934 
 935         // special handling for a new line
 936         if (!leading && isNewLine) {
 937             leading = true;
 938             pos -= 1;
 939         }
 940 
 941         if (select) {
 942             if (extendSelection) {
 943                 getSkinnable().extendSelection(pos);
 944             } else {
 945                 getSkinnable().selectPositionCaret(pos);
 946             }
 947         } else {
 948             getSkinnable().positionCaret(pos);
 949         }
 950 
 951         setForwardBias(leading);
 952     }
 953 
 954     /** {@inheritDoc} */
 955     @Override public Rectangle2D getCharacterBounds(int index) {
 956         TextArea textArea = getSkinnable();
 957 
 958         int paragraphIndex = paragraphNodes.getChildren().size();
 959         int paragraphOffset = textArea.getLength() + 1;
 960 
 961         Text paragraphNode = null;
 962         do {
 963             paragraphNode = (Text)paragraphNodes.getChildren().get(--paragraphIndex);
 964             paragraphOffset -= paragraphNode.getText().length() + 1;
 965         } while (index < paragraphOffset);
 966 
 967         int characterIndex = index - paragraphOffset;
 968         boolean terminator = false;
 969 
 970         if (characterIndex == paragraphNode.getText().length()) {
 971             characterIndex--;
 972             terminator = true;
 973         }
 974 
 975         characterBoundingPath.getElements().clear();
 976         characterBoundingPath.getElements().addAll(paragraphNode.rangeShape(characterIndex, characterIndex + 1));
 977         characterBoundingPath.setLayoutX(paragraphNode.getLayoutX());
 978         characterBoundingPath.setLayoutY(paragraphNode.getLayoutY());
 979 
 980         Bounds bounds = characterBoundingPath.getBoundsInLocal();
 981 
 982         double x = bounds.getMinX() + paragraphNode.getLayoutX() - textArea.getScrollLeft();
 983         double y = bounds.getMinY() + paragraphNode.getLayoutY() - textArea.getScrollTop();
 984 
 985         // Sometimes the bounds is empty, in which case we must ignore the width/height
 986         double width = bounds.isEmpty() ? 0 : bounds.getWidth();
 987         double height = bounds.isEmpty() ? 0 : bounds.getHeight();
 988 
 989         if (terminator) {
 990             x += width;
 991             width = 0;
 992         }
 993 
 994         return new Rectangle2D(x, y, width, height);
 995     }
 996 
 997     /** {@inheritDoc} */
 998     @Override protected void scrollCharacterToVisible(final int index) {
 999         // TODO We queue a callback because when characters are added or
1000         // removed the bounds are not immediately updated; is this really
1001         // necessary?
1002 
1003         Platform.runLater(() -> {
1004             if (getSkinnable().getLength() == 0) {
1005                 return;
1006             }
1007             Rectangle2D characterBounds = getCharacterBounds(index);
1008             scrollBoundsToVisible(characterBounds);
1009         });
1010     }
1011 
1012 
1013 
1014     /**************************************************************************
1015      *
1016      * Private implementation
1017      *
1018      **************************************************************************/
1019 
1020     TextAreaBehavior getBehavior() {
1021         return behavior;
1022     }
1023 
1024     private void createPromptNode() {
1025         if (promptNode == null && usePromptText.get()) {
1026             promptNode = new Text();
1027             contentView.getChildren().add(0, promptNode);
1028             promptNode.setManaged(false);
1029             promptNode.getStyleClass().add("text");
1030             promptNode.visibleProperty().bind(usePromptText);
1031             promptNode.fontProperty().bind(getSkinnable().fontProperty());
1032             promptNode.textProperty().bind(getSkinnable().promptTextProperty());
1033             promptNode.fillProperty().bind(promptTextFillProperty());
1034         }
1035     }
1036 
1037     private void addParagraphNode(int i, String string) {
1038         final TextArea textArea = getSkinnable();
1039         Text paragraphNode = new Text(string);
1040         paragraphNode.setTextOrigin(VPos.TOP);
1041         paragraphNode.setManaged(false);
1042         paragraphNode.getStyleClass().add("text");
1043         paragraphNode.boundsTypeProperty().addListener((observable, oldValue, newValue) -> {
1044             invalidateMetrics();
1045             updateFontMetrics();
1046         });
1047         paragraphNodes.getChildren().add(i, paragraphNode);
1048 
1049         paragraphNode.fontProperty().bind(textArea.fontProperty());
1050         paragraphNode.fillProperty().bind(textFillProperty());
1051         paragraphNode.selectionFillProperty().bind(highlightTextFillProperty());
1052     }
1053 
1054     private double getScrollTopMax() {
1055         return Math.max(0, contentView.getHeight() - scrollPane.getViewportBounds().getHeight());
1056     }
1057 
1058     private double getScrollLeftMax() {
1059         return Math.max(0, contentView.getWidth() - scrollPane.getViewportBounds().getWidth());
1060     }
1061 
1062     private int getInsertionPoint(Text paragraphNode, double x, double y) {
1063         HitInfo hitInfo = paragraphNode.hitTest(new Point2D(x, y));
1064         return hitInfo.getInsertionIndex();
1065     }
1066 
1067     private int getNextInsertionPoint(Text paragraphNode, double x, int from,
1068         VerticalDirection scrollDirection) {
1069         // TODO
1070         return 0;
1071     }
1072 
1073     private void scrollCaretToVisible() {
1074         TextArea textArea = getSkinnable();
1075         Bounds bounds = caretPath.getLayoutBounds();
1076         double x = bounds.getMinX() - textArea.getScrollLeft();
1077         double y = bounds.getMinY() - textArea.getScrollTop();
1078         double w = bounds.getWidth();
1079         double h = bounds.getHeight();
1080 
1081         if (SHOW_HANDLES) {
1082             if (caretHandle.isVisible()) {
1083                 h += caretHandle.getHeight();
1084             } else if (selectionHandle1.isVisible() && selectionHandle2.isVisible()) {
1085                 x -= selectionHandle1.getWidth() / 2;
1086                 y -= selectionHandle1.getHeight();
1087                 w += selectionHandle1.getWidth() / 2 + selectionHandle2.getWidth() / 2;
1088                 h += selectionHandle1.getHeight() + selectionHandle2.getHeight();
1089             }
1090         }
1091 
1092         if (w > 0 && h > 0) {
1093             scrollBoundsToVisible(new Rectangle2D(x, y, w, h));
1094         }
1095     }
1096 
1097     private void scrollBoundsToVisible(Rectangle2D bounds) {
1098         TextArea textArea = getSkinnable();
1099         Bounds viewportBounds = scrollPane.getViewportBounds();
1100 
1101         double viewportWidth = viewportBounds.getWidth();
1102         double viewportHeight = viewportBounds.getHeight();
1103         double scrollTop = textArea.getScrollTop();
1104         double scrollLeft = textArea.getScrollLeft();
1105         double slop = 6.0;
1106 
1107         if (bounds.getMinY() < 0) {
1108             double y = scrollTop + bounds.getMinY();
1109             if (y <= contentView.snappedTopInset()) {
1110                 y = 0;
1111             }
1112             textArea.setScrollTop(y);
1113         } else if (contentView.snappedTopInset() + bounds.getMaxY() > viewportHeight) {
1114             double y = scrollTop + contentView.snappedTopInset() + bounds.getMaxY() - viewportHeight;
1115             if (y >= getScrollTopMax() - contentView.snappedBottomInset()) {
1116                 y = getScrollTopMax();
1117             }
1118             textArea.setScrollTop(y);
1119         }
1120 
1121 
1122         if (bounds.getMinX() < 0) {
1123             double x = scrollLeft + bounds.getMinX() - slop;
1124             if (x <= contentView.snappedLeftInset() + slop) {
1125                 x = 0;
1126             }
1127             textArea.setScrollLeft(x);
1128         } else if (contentView.snappedLeftInset() + bounds.getMaxX() > viewportWidth) {
1129             double x = scrollLeft + contentView.snappedLeftInset() + bounds.getMaxX() - viewportWidth + slop;
1130             if (x >= getScrollLeftMax() - contentView.snappedRightInset() - slop) {
1131                 x = getScrollLeftMax();
1132             }
1133             textArea.setScrollLeft(x);
1134         }
1135     }
1136 
1137     private void updatePrefViewportWidth() {
1138         int columnCount = getSkinnable().getPrefColumnCount();
1139         scrollPane.setPrefViewportWidth(columnCount * characterWidth + contentView.snappedLeftInset() + contentView.snappedRightInset());
1140         scrollPane.setMinViewportWidth(characterWidth + contentView.snappedLeftInset() + contentView.snappedRightInset());
1141     }
1142 
1143     private void updatePrefViewportHeight() {
1144         int rowCount = getSkinnable().getPrefRowCount();
1145         scrollPane.setPrefViewportHeight(rowCount * lineHeight + contentView.snappedTopInset() + contentView.snappedBottomInset());
1146         scrollPane.setMinViewportHeight(lineHeight + contentView.snappedTopInset() + contentView.snappedBottomInset());
1147     }
1148 
1149     private void updateFontMetrics() {
1150         Text firstParagraph = (Text)paragraphNodes.getChildren().get(0);
1151         lineHeight = Utils.getLineHeight(getSkinnable().getFont(), firstParagraph.getBoundsType());
1152         characterWidth = fontMetrics.get().getCharWidth('W');
1153     }
1154 
1155     private double getTextTranslateX() {
1156         return contentView.snappedLeftInset();
1157     }
1158 
1159     private double getTextTranslateY() {
1160         return contentView.snappedTopInset();
1161     }
1162 
1163     private double getTextLeft() {
1164         return 0;
1165     }
1166 
1167     private Point2D translateCaretPosition(Point2D p) {
1168         return p;
1169     }
1170 
1171     private Text getTextNode() {
1172         if (USE_MULTIPLE_NODES) {
1173             throw new IllegalArgumentException("Multiple node traversal is not yet implemented.");
1174         }
1175         return (Text)paragraphNodes.getChildren().get(0);
1176     }
1177 
1178     private void updateTextNodeCaretPos(int pos) {
1179         Text textNode = getTextNode();
1180         if (isForwardBias()) {
1181             textNode.setCaretPosition(pos);
1182         } else {
1183             textNode.setCaretPosition(pos - 1);
1184         }
1185         textNode.caretBiasProperty().set(isForwardBias());
1186     }
1187 
1188 
1189 
1190     /**************************************************************************
1191      *
1192      * Support classes
1193      *
1194      **************************************************************************/
1195 
1196     private class ContentView extends Region {
1197         {
1198             getStyleClass().add("content");
1199 
1200             addEventHandler(MouseEvent.MOUSE_PRESSED, event -> {
1201                 behavior.mousePressed(event);
1202                 event.consume();
1203             });
1204 
1205             addEventHandler(MouseEvent.MOUSE_RELEASED, event -> {
1206                 behavior.mouseReleased(event);
1207                 event.consume();
1208             });
1209 
1210             addEventHandler(MouseEvent.MOUSE_DRAGGED, event -> {
1211                 behavior.mouseDragged(event);
1212                 event.consume();
1213             });
1214         }
1215 
1216         @Override protected ObservableList<Node> getChildren() {
1217             return super.getChildren();
1218         }
1219 
1220         @Override public Orientation getContentBias() {
1221             return Orientation.HORIZONTAL;
1222         }
1223 
1224         @Override protected double computePrefWidth(double height) {
1225             if (computedPrefWidth < 0) {
1226                 double prefWidth = 0;
1227 
1228                 for (Node node : paragraphNodes.getChildren()) {
1229                     Text paragraphNode = (Text)node;
1230                     prefWidth = Math.max(prefWidth,
1231                             Utils.computeTextWidth(paragraphNode.getFont(),
1232                                     paragraphNode.getText(), 0));
1233                 }
1234 
1235                 prefWidth += snappedLeftInset() + snappedRightInset();
1236 
1237                 Bounds viewPortBounds = scrollPane.getViewportBounds();
1238                 computedPrefWidth = Math.max(prefWidth, (viewPortBounds != null) ? viewPortBounds.getWidth() : 0);
1239             }
1240             return computedPrefWidth;
1241         }
1242 
1243         @Override protected double computePrefHeight(double width) {
1244             if (width != widthForComputedPrefHeight) {
1245                 invalidateMetrics();
1246                 widthForComputedPrefHeight = width;
1247             }
1248 
1249             if (computedPrefHeight < 0) {
1250                 double wrappingWidth;
1251                 if (width == -1) {
1252                     wrappingWidth = 0;
1253                 } else {
1254                     wrappingWidth = Math.max(width - (snappedLeftInset() + snappedRightInset()), 0);
1255                 }
1256 
1257                 double prefHeight = 0;
1258 
1259                 for (Node node : paragraphNodes.getChildren()) {
1260                     Text paragraphNode = (Text)node;
1261                     prefHeight += Utils.computeTextHeight(
1262                             paragraphNode.getFont(),
1263                             paragraphNode.getText(),
1264                             wrappingWidth,
1265                             paragraphNode.getBoundsType());
1266                 }
1267 
1268                 prefHeight += snappedTopInset() + snappedBottomInset();
1269 
1270                 Bounds viewPortBounds = scrollPane.getViewportBounds();
1271                 computedPrefHeight = Math.max(prefHeight, (viewPortBounds != null) ? viewPortBounds.getHeight() : 0);
1272             }
1273             return computedPrefHeight;
1274         }
1275 
1276         @Override protected double computeMinWidth(double height) {
1277             if (computedMinWidth < 0) {
1278                 double hInsets = snappedLeftInset() + snappedRightInset();
1279                 computedMinWidth = Math.min(characterWidth + hInsets, computePrefWidth(height));
1280             }
1281             return computedMinWidth;
1282         }
1283 
1284         @Override protected double computeMinHeight(double width) {
1285             if (computedMinHeight < 0) {
1286                 double vInsets = snappedTopInset() + snappedBottomInset();
1287                 computedMinHeight = Math.min(lineHeight + vInsets, computePrefHeight(width));
1288             }
1289             return computedMinHeight;
1290         }
1291 
1292         @Override public void layoutChildren() {
1293             TextArea textArea = getSkinnable();
1294             double width = getWidth();
1295 
1296             // Lay out paragraphs
1297             final double topPadding = snappedTopInset();
1298             final double leftPadding = snappedLeftInset();
1299 
1300             double wrappingWidth = Math.max(width - (leftPadding + snappedRightInset()), 0);
1301 
1302             double y = topPadding;
1303 
1304             final List<Node> paragraphNodesChildren = paragraphNodes.getChildren();
1305 
1306             for (int i = 0; i < paragraphNodesChildren.size(); i++) {
1307                 Node node = paragraphNodesChildren.get(i);
1308                 Text paragraphNode = (Text)node;
1309                 paragraphNode.setWrappingWidth(wrappingWidth);
1310 
1311                 Bounds bounds = paragraphNode.getBoundsInLocal();
1312                 paragraphNode.setLayoutX(leftPadding);
1313                 paragraphNode.setLayoutY(y);
1314 
1315                 y += bounds.getHeight();
1316             }
1317 
1318             if (promptNode != null) {
1319                 promptNode.setLayoutX(leftPadding);
1320                 promptNode.setLayoutY(topPadding + promptNode.getBaselineOffset());
1321                 promptNode.setWrappingWidth(wrappingWidth);
1322             }
1323 
1324             // Update the selection
1325             IndexRange selection = textArea.getSelection();
1326             Bounds oldCaretBounds = caretPath.getBoundsInParent();
1327 
1328             selectionHighlightGroup.getChildren().clear();
1329 
1330             int caretPos = textArea.getCaretPosition();
1331             int anchorPos = textArea.getAnchor();
1332 
1333             if (SHOW_HANDLES) {
1334                 // Install and resize the handles for caret and anchor.
1335                 if (selection.getLength() > 0) {
1336                     selectionHandle1.resize(selectionHandle1.prefWidth(-1),
1337                             selectionHandle1.prefHeight(-1));
1338                     selectionHandle2.resize(selectionHandle2.prefWidth(-1),
1339                             selectionHandle2.prefHeight(-1));
1340                 } else {
1341                     caretHandle.resize(caretHandle.prefWidth(-1),
1342                             caretHandle.prefHeight(-1));
1343                 }
1344 
1345                 // Position the handle for the anchor. This could be handle1 or handle2.
1346                 // Do this before positioning the actual caret.
1347                 if (selection.getLength() > 0) {
1348                     int paragraphIndex = paragraphNodesChildren.size();
1349                     int paragraphOffset = textArea.getLength() + 1;
1350                     Text paragraphNode = null;
1351                     do {
1352                         paragraphNode = (Text)paragraphNodesChildren.get(--paragraphIndex);
1353                         paragraphOffset -= paragraphNode.getText().length() + 1;
1354                     } while (anchorPos < paragraphOffset);
1355 
1356                     updateTextNodeCaretPos(anchorPos - paragraphOffset);
1357                     caretPath.getElements().clear();
1358                     caretPath.getElements().addAll(paragraphNode.getCaretShape());
1359                     caretPath.setLayoutX(paragraphNode.getLayoutX());
1360                     caretPath.setLayoutY(paragraphNode.getLayoutY());
1361 
1362                     Bounds b = caretPath.getBoundsInParent();
1363                     if (caretPos < anchorPos) {
1364                         selectionHandle2.setLayoutX(b.getMinX() - selectionHandle2.getWidth() / 2);
1365                         selectionHandle2.setLayoutY(b.getMaxY() - 1);
1366                     } else {
1367                         selectionHandle1.setLayoutX(b.getMinX() - selectionHandle1.getWidth() / 2);
1368                         selectionHandle1.setLayoutY(b.getMinY() - selectionHandle1.getHeight() + 1);
1369                     }
1370                 }
1371             }
1372 
1373             {
1374                 // Position caret
1375                 int paragraphIndex = paragraphNodesChildren.size();
1376                 int paragraphOffset = textArea.getLength() + 1;
1377 
1378                 Text paragraphNode = null;
1379                 do {
1380                     paragraphNode = (Text)paragraphNodesChildren.get(--paragraphIndex);
1381                     paragraphOffset -= paragraphNode.getText().length() + 1;
1382                 } while (caretPos < paragraphOffset);
1383 
1384                 updateTextNodeCaretPos(caretPos - paragraphOffset);
1385 
1386                 caretPath.getElements().clear();
1387                 caretPath.getElements().addAll(paragraphNode.getCaretShape());
1388 
1389                 caretPath.setLayoutX(paragraphNode.getLayoutX());
1390 
1391                 // TODO: Remove this temporary workaround for RT-27533
1392                 paragraphNode.setLayoutX(2 * paragraphNode.getLayoutX() - paragraphNode.getBoundsInParent().getMinX());
1393 
1394                 caretPath.setLayoutY(paragraphNode.getLayoutY());
1395                 if (oldCaretBounds == null || !oldCaretBounds.equals(caretPath.getBoundsInParent())) {
1396                     scrollCaretToVisible();
1397                 }
1398             }
1399 
1400             // Update selection fg and bg
1401             int start = selection.getStart();
1402             int end = selection.getEnd();
1403             for (int i = 0, max = paragraphNodesChildren.size(); i < max; i++) {
1404                 Node paragraphNode = paragraphNodesChildren.get(i);
1405                 Text textNode = (Text)paragraphNode;
1406                 int paragraphLength = textNode.getText().length() + 1;
1407                 if (end > start && start < paragraphLength) {
1408                     textNode.setSelectionStart(start);
1409                     textNode.setSelectionEnd(Math.min(end, paragraphLength));
1410 
1411                     Path selectionHighlightPath = new Path();
1412                     selectionHighlightPath.setManaged(false);
1413                     selectionHighlightPath.setStroke(null);
1414                     PathElement[] selectionShape = textNode.getSelectionShape();
1415                     if (selectionShape != null) {
1416                         selectionHighlightPath.getElements().addAll(selectionShape);
1417                     }
1418                     selectionHighlightGroup.getChildren().add(selectionHighlightPath);
1419                     selectionHighlightGroup.setVisible(true);
1420                     selectionHighlightPath.setLayoutX(textNode.getLayoutX());
1421                     selectionHighlightPath.setLayoutY(textNode.getLayoutY());
1422                     updateHighlightFill();
1423                 } else {
1424                     textNode.setSelectionStart(-1);
1425                     textNode.setSelectionEnd(-1);
1426                     selectionHighlightGroup.setVisible(false);
1427                 }
1428                 start = Math.max(0, start - paragraphLength);
1429                 end   = Math.max(0, end   - paragraphLength);
1430             }
1431 
1432             if (SHOW_HANDLES) {
1433                 // Position handle for the caret. This could be handle1 or handle2 when
1434                 // a selection is active.
1435                 Bounds b = caretPath.getBoundsInParent();
1436                 if (selection.getLength() > 0) {
1437                     if (caretPos < anchorPos) {
1438                         selectionHandle1.setLayoutX(b.getMinX() - selectionHandle1.getWidth() / 2);
1439                         selectionHandle1.setLayoutY(b.getMinY() - selectionHandle1.getHeight() + 1);
1440                     } else {
1441                         selectionHandle2.setLayoutX(b.getMinX() - selectionHandle2.getWidth() / 2);
1442                         selectionHandle2.setLayoutY(b.getMaxY() - 1);
1443                     }
1444                 } else {
1445                     caretHandle.setLayoutX(b.getMinX() - caretHandle.getWidth() / 2 + 1);
1446                     caretHandle.setLayoutY(b.getMaxY());
1447                 }
1448             }
1449 
1450             if (scrollPane.getPrefViewportWidth() == 0
1451                     || scrollPane.getPrefViewportHeight() == 0) {
1452                 updatePrefViewportWidth();
1453                 updatePrefViewportHeight();
1454                 if (getParent() != null && scrollPane.getPrefViewportWidth() > 0
1455                         || scrollPane.getPrefViewportHeight() > 0) {
1456                     // Force layout of viewRect in ScrollPaneSkin
1457                     getParent().requestLayout();
1458                 }
1459             }
1460 
1461             // RT-36454: Fit to width/height only if smaller than viewport.
1462             // That is, grow to fit but don't shrink to fit.
1463             Bounds viewportBounds = scrollPane.getViewportBounds();
1464             boolean wasFitToWidth = scrollPane.isFitToWidth();
1465             boolean wasFitToHeight = scrollPane.isFitToHeight();
1466             boolean setFitToWidth = textArea.isWrapText() || computePrefWidth(-1) <= viewportBounds.getWidth();
1467             boolean setFitToHeight = computePrefHeight(width) <= viewportBounds.getHeight();
1468             if (wasFitToWidth != setFitToWidth || wasFitToHeight != setFitToHeight) {
1469                 Platform.runLater(() -> {
1470                     scrollPane.setFitToWidth(setFitToWidth);
1471                     scrollPane.setFitToHeight(setFitToHeight);
1472                 });
1473                 getParent().requestLayout();
1474             }
1475         }
1476     }
1477 }