1 /*
   2  * Copyright (c) 2011, 2015, 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 com.sun.javafx.scene.text.HitInfo;
  32 import javafx.animation.KeyFrame;
  33 import javafx.animation.Timeline;
  34 import javafx.application.Platform;
  35 import javafx.beans.binding.BooleanBinding;
  36 import javafx.beans.binding.DoubleBinding;
  37 import javafx.beans.binding.IntegerBinding;
  38 import javafx.beans.value.ObservableBooleanValue;
  39 import javafx.beans.value.ObservableIntegerValue;
  40 import javafx.collections.ListChangeListener;
  41 import javafx.collections.ObservableList;
  42 import javafx.event.ActionEvent;
  43 import javafx.event.EventHandler;
  44 import javafx.geometry.Bounds;
  45 import javafx.geometry.Orientation;
  46 import javafx.geometry.Point2D;
  47 import javafx.geometry.Rectangle2D;
  48 import javafx.geometry.VPos;
  49 import javafx.geometry.VerticalDirection;
  50 import javafx.scene.AccessibleAttribute;
  51 import javafx.scene.Group;
  52 import javafx.scene.Node;
  53 import javafx.scene.control.Accordion;
  54 import javafx.scene.control.Button;
  55 import javafx.scene.control.Control;
  56 import javafx.scene.control.IndexRange;
  57 import javafx.scene.control.ScrollPane;
  58 import javafx.scene.control.TextArea;
  59 import javafx.scene.input.MouseEvent;
  60 import javafx.scene.input.ScrollEvent;
  61 import javafx.scene.layout.Region;
  62 import javafx.scene.shape.MoveTo;
  63 import javafx.scene.shape.Path;
  64 import javafx.scene.shape.PathElement;
  65 import javafx.scene.text.Text;
  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() + 10/*??*/ - pressX + caretHandle.getWidth() / 2,
 403                                         e.getSceneY() - tp.getY() - pressY - 6);
 404                 HitInfo hit = textNode.impl_hitTestChar(translateCaretPosition(p));
 405                 int pos = hit.getCharIndex();
 406                 if (pos > 0) {
 407                     int oldPos = textNode.getImpl_caretPosition();
 408                     textNode.setImpl_caretPosition(pos);
 409                     PathElement element = textNode.getImpl_caretShape()[0];
 410                     if (element instanceof MoveTo && ((MoveTo)element).getY() > e.getY() - getTextTranslateY()) {
 411                         hit.setCharIndex(pos - 1);
 412                     }
 413                     textNode.setImpl_caretPosition(oldPos);
 414                 }
 415                 positionCaret(hit, false);
 416                 e.consume();
 417             });
 418 
 419             selectionHandle1.setOnMouseDragged(e -> {
 420                 TextArea control1 = getSkinnable();
 421                 Text textNode = getTextNode();
 422                 Point2D tp = textNode.localToScene(0, 0);
 423                 Point2D p = new Point2D(e.getSceneX() - tp.getX() + 10/*??*/ - pressX + selectionHandle1.getWidth() / 2,
 424                                         e.getSceneY() - tp.getY() - pressY + selectionHandle1.getHeight() + 5);
 425                 HitInfo hit = textNode.impl_hitTestChar(translateCaretPosition(p));
 426                 int pos = hit.getCharIndex();
 427                 if (control1.getAnchor() < control1.getCaretPosition()) {
 428                     // Swap caret and anchor
 429                     control1.selectRange(control1.getCaretPosition(), control1.getAnchor());
 430                 }
 431                 if (pos > 0) {
 432                     if (pos >= control1.getAnchor()) {
 433                         pos = control1.getAnchor();
 434                     }
 435                     int oldPos = textNode.getImpl_caretPosition();
 436                     textNode.setImpl_caretPosition(pos);
 437                     PathElement element = textNode.getImpl_caretShape()[0];
 438                     if (element instanceof MoveTo && ((MoveTo)element).getY() > e.getY() - getTextTranslateY()) {
 439                         hit.setCharIndex(pos - 1);
 440                     }
 441                     textNode.setImpl_caretPosition(oldPos);
 442                 }
 443                 positionCaret(hit, true);
 444                 e.consume();
 445             });
 446 
 447             selectionHandle2.setOnMouseDragged(e -> {
 448                 TextArea control1 = getSkinnable();
 449                 Text textNode = getTextNode();
 450                 Point2D tp = textNode.localToScene(0, 0);
 451                 Point2D p = new Point2D(e.getSceneX() - tp.getX() + 10/*??*/ - pressX + selectionHandle2.getWidth() / 2,
 452                                         e.getSceneY() - tp.getY() - pressY - 6);
 453                 HitInfo hit = textNode.impl_hitTestChar(translateCaretPosition(p));
 454                 int pos = hit.getCharIndex();
 455                 if (control1.getAnchor() > control1.getCaretPosition()) {
 456                     // Swap caret and anchor
 457                     control1.selectRange(control1.getCaretPosition(), control1.getAnchor());
 458                 }
 459                 if (pos > 0) {
 460                     if (pos <= control1.getAnchor() + 1) {
 461                         pos = Math.min(control1.getAnchor() + 2, control1.getLength());
 462                     }
 463                     int oldPos = textNode.getImpl_caretPosition();
 464                     textNode.setImpl_caretPosition(pos);
 465                     PathElement element = textNode.getImpl_caretShape()[0];
 466                     if (element instanceof MoveTo && ((MoveTo)element).getY() > e.getY() - getTextTranslateY()) {
 467                         hit.setCharIndex(pos - 1);
 468                     }
 469                     textNode.setImpl_caretPosition(oldPos);
 470                     positionCaret(hit, true);
 471                 }
 472                 e.consume();
 473             });
 474         }
 475     }
 476 
 477 
 478 
 479     /***************************************************************************
 480      *                                                                         *
 481      * Public API                                                              *
 482      *                                                                         *
 483      **************************************************************************/
 484 
 485     /** {@inheritDoc} */
 486     @Override protected void invalidateMetrics() {
 487         computedMinWidth = Double.NEGATIVE_INFINITY;
 488         computedMinHeight = Double.NEGATIVE_INFINITY;
 489         computedPrefWidth = Double.NEGATIVE_INFINITY;
 490         computedPrefHeight = Double.NEGATIVE_INFINITY;
 491     }
 492 
 493     /** {@inheritDoc} */
 494     @Override protected void layoutChildren(double contentX, double contentY, double contentWidth, double contentHeight) {
 495         scrollPane.resizeRelocate(contentX, contentY, contentWidth, contentHeight);
 496     }
 497 
 498     /** {@inheritDoc} */
 499     @Override protected void updateHighlightFill() {
 500         for (Node node : selectionHighlightGroup.getChildren()) {
 501             Path selectionHighlightPath = (Path)node;
 502             selectionHighlightPath.setFill(highlightFillProperty().get());
 503         }
 504     }
 505 
 506     // Public for behavior
 507     /**
 508      * Performs a hit test, mapping point to index in the content.
 509      *
 510      * @param x the x coordinate of the point.
 511      * @param y the y coordinate of the point.
 512      * @return a {@code TextPosInfo} object describing the index and forward bias.
 513      */
 514     public TextPosInfo getIndex(double x, double y) {
 515         // adjust the event to be in the same coordinate space as the
 516         // text content of the textInputControl
 517         Text textNode = getTextNode();
 518         Point2D p = new Point2D(x - textNode.getLayoutX(), y - getTextTranslateY());
 519         HitInfo hit = textNode.impl_hitTestChar(translateCaretPosition(p));
 520         int pos = hit.getCharIndex();
 521         if (pos > 0) {
 522             int oldPos = textNode.getImpl_caretPosition();
 523             textNode.setImpl_caretPosition(pos);
 524             PathElement element = textNode.getImpl_caretShape()[0];
 525             if (element instanceof MoveTo && ((MoveTo)element).getY() > y - getTextTranslateY()) {
 526                 hit.setCharIndex(pos - 1);
 527             }
 528             textNode.setImpl_caretPosition(oldPos);
 529         }
 530         return new TextPosInfo(hit);
 531     };
 532 
 533     /** {@inheritDoc} */
 534     @Override public void moveCaret(TextUnit unit, Direction dir, boolean select) {
 535         switch (unit) {
 536             case CHARACTER:
 537                 switch (dir) {
 538                     case LEFT:
 539                     case RIGHT:
 540                         nextCharacterVisually(dir == Direction.RIGHT);
 541                         break;
 542                     default:
 543                         throw new IllegalArgumentException(""+dir);
 544                 }
 545                 break;
 546 
 547             case LINE:
 548                 switch (dir) {
 549                     case UP:
 550                         previousLine(select);
 551                         break;
 552                     case DOWN:
 553                         nextLine(select);
 554                         break;
 555                     case BEGINNING:
 556                         lineStart(select, select && isMac());
 557                         break;
 558                     case END:
 559                         lineEnd(select, select && isMac());
 560                         break;
 561                     default:
 562                         throw new IllegalArgumentException(""+dir);
 563                 }
 564                 break;
 565 
 566             case PAGE:
 567                 switch (dir) {
 568                     case UP:
 569                         previousPage(select);
 570                         break;
 571                     case DOWN:
 572                         nextPage(select);
 573                         break;
 574                     default:
 575                         throw new IllegalArgumentException(""+dir);
 576                 }
 577                 break;
 578 
 579             case PARAGRAPH:
 580                 switch (dir) {
 581                     case UP:
 582                         paragraphStart(true, select);
 583                         break;
 584                     case DOWN:
 585                         paragraphEnd(true, select);
 586                         break;
 587                     case BEGINNING:
 588                         paragraphStart(false, select);
 589                         break;
 590                     case END:
 591                         paragraphEnd(false, select);
 592                         break;
 593                     default:
 594                         throw new IllegalArgumentException(""+dir);
 595                 }
 596                 break;
 597 
 598             default:
 599                 throw new IllegalArgumentException(""+unit);
 600         }
 601     }
 602 
 603     private void nextCharacterVisually(boolean moveRight) {
 604         if (isRTL()) {
 605             // Text node is mirrored.
 606             moveRight = !moveRight;
 607         }
 608 
 609         Text textNode = getTextNode();
 610         Bounds caretBounds = caretPath.getLayoutBounds();
 611         if (caretPath.getElements().size() == 4) {
 612             // The caret is split
 613             // TODO: Find a better way to get the primary caret position
 614             // instead of depending on the internal implementation.
 615             // See RT-25465.
 616             caretBounds = new Path(caretPath.getElements().get(0), caretPath.getElements().get(1)).getLayoutBounds();
 617         }
 618         double hitX = moveRight ? caretBounds.getMaxX() : caretBounds.getMinX();
 619         double hitY = (caretBounds.getMinY() + caretBounds.getMaxY()) / 2;
 620         HitInfo hit = textNode.impl_hitTestChar(new Point2D(hitX, hitY));
 621         Path charShape = new Path(textNode.impl_getRangeShape(hit.getCharIndex(), hit.getCharIndex() + 1));
 622         if ((moveRight && charShape.getLayoutBounds().getMaxX() > caretBounds.getMaxX()) ||
 623                 (!moveRight && charShape.getLayoutBounds().getMinX() < caretBounds.getMinX())) {
 624             hit.setLeading(!hit.isLeading());
 625             positionCaret(hit, false);
 626         } else {
 627             // We're at beginning or end of line. Try moving up / down.
 628             int dot = textArea.getCaretPosition();
 629             targetCaretX = moveRight ? 0 : Double.MAX_VALUE;
 630             // TODO: Use Bidi sniffing instead of assuming right means forward here?
 631             downLines(moveRight ? 1 : -1, false, false);
 632             targetCaretX = -1;
 633             if (dot == textArea.getCaretPosition()) {
 634                 if (moveRight) {
 635                     textArea.forward();
 636                 } else {
 637                     textArea.backward();
 638                 }
 639             }
 640         }
 641     }
 642 
 643     private void downLines(int nLines, boolean select, boolean extendSelection) {
 644         Text textNode = getTextNode();
 645         Bounds caretBounds = caretPath.getLayoutBounds();
 646 
 647         // The middle y coordinate of the the line we want to go to.
 648         double targetLineMidY = (caretBounds.getMinY() + caretBounds.getMaxY()) / 2 + nLines * lineHeight;
 649         if (targetLineMidY < 0) {
 650             targetLineMidY = 0;
 651         }
 652 
 653         // The target x for the caret. This may have been set during a
 654         // previous call.
 655         double x = (targetCaretX >= 0) ? targetCaretX : (caretBounds.getMaxX());
 656 
 657         // Find a text position for the target x,y.
 658         HitInfo hit = textNode.impl_hitTestChar(translateCaretPosition(new Point2D(x, targetLineMidY)));
 659         int pos = hit.getCharIndex();
 660 
 661         // Save the old pos temporarily while testing the new one.
 662         int oldPos = textNode.getImpl_caretPosition();
 663         boolean oldBias = textNode.isImpl_caretBias();
 664         textNode.setImpl_caretBias(hit.isLeading());
 665         textNode.setImpl_caretPosition(pos);
 666         tmpCaretPath.getElements().clear();
 667         tmpCaretPath.getElements().addAll(textNode.getImpl_caretShape());
 668         tmpCaretPath.setLayoutX(textNode.getLayoutX());
 669         tmpCaretPath.setLayoutY(textNode.getLayoutY());
 670         Bounds tmpCaretBounds = tmpCaretPath.getLayoutBounds();
 671         // The y for the middle of the row we found.
 672         double foundLineMidY = (tmpCaretBounds.getMinY() + tmpCaretBounds.getMaxY()) / 2;
 673         textNode.setImpl_caretBias(oldBias);
 674         textNode.setImpl_caretPosition(oldPos);
 675 
 676         if (pos > 0) {
 677             if (nLines > 0 && foundLineMidY > targetLineMidY) {
 678                 // We went too far and ended up after a newline.
 679                 hit.setCharIndex(pos - 1);
 680             }
 681 
 682             if (pos >= textArea.getLength() && getCharacter(pos - 1) == '\n') {
 683                 // Special case for newline at end of text.
 684                 hit.setLeading(true);
 685             }
 686         }
 687 
 688         // Test if the found line is in the correct direction and move
 689         // the caret.
 690         if (nLines == 0 ||
 691                 (nLines > 0 && foundLineMidY > caretBounds.getMaxY()) ||
 692                 (nLines < 0 && foundLineMidY < caretBounds.getMinY())) {
 693 
 694             positionCaret(hit, select, extendSelection);
 695             targetCaretX = x;
 696         }
 697     }
 698 
 699     private void previousLine(boolean select) {
 700         downLines(-1, select, false);
 701     }
 702 
 703     private void nextLine(boolean select) {
 704         downLines(1, select, false);
 705     }
 706 
 707     private void previousPage(boolean select) {
 708         downLines(-(int)(scrollPane.getViewportBounds().getHeight() / lineHeight),
 709                 select, false);
 710     }
 711 
 712     private void nextPage(boolean select) {
 713         downLines((int)(scrollPane.getViewportBounds().getHeight() / lineHeight),
 714                 select, false);
 715     }
 716 
 717     private void lineStart(boolean select, boolean extendSelection) {
 718         targetCaretX = 0;
 719         downLines(0, select, extendSelection);
 720         targetCaretX = -1;
 721     }
 722 
 723     private void lineEnd(boolean select, boolean extendSelection) {
 724         targetCaretX = Double.MAX_VALUE;
 725         downLines(0, select, extendSelection);
 726         targetCaretX = -1;
 727     }
 728 
 729 
 730     private void paragraphStart(boolean previousIfAtStart, boolean select) {
 731         TextArea textArea = getSkinnable();
 732         String text = textArea.textProperty().getValueSafe();
 733         int pos = textArea.getCaretPosition();
 734 
 735         if (pos > 0) {
 736             if (previousIfAtStart && text.codePointAt(pos-1) == 0x0a) {
 737                 // We are at the beginning of a paragraph.
 738                 // Back up to the previous paragraph.
 739                 pos--;
 740             }
 741             // Back up to the beginning of this paragraph
 742             while (pos > 0 && text.codePointAt(pos-1) != 0x0a) {
 743                 pos--;
 744             }
 745             if (select) {
 746                 textArea.selectPositionCaret(pos);
 747             } else {
 748                 textArea.positionCaret(pos);
 749                 setForwardBias(true);
 750             }
 751         }
 752     }
 753 
 754     private void paragraphEnd(boolean goPastInitialNewline, boolean select) {
 755         TextArea textArea = getSkinnable();
 756         String text = textArea.textProperty().getValueSafe();
 757         int pos = textArea.getCaretPosition();
 758         int len = text.length();
 759         boolean wentPastInitialNewline = false;
 760         boolean goPastTrailingNewline = isWindows();
 761 
 762         if (pos < len) {
 763             if (goPastInitialNewline && text.codePointAt(pos) == 0x0a) {
 764                 // We are at the end of a paragraph, start by moving to the
 765                 // next paragraph.
 766                 pos++;
 767                 wentPastInitialNewline = true;
 768             }
 769             if (!(goPastTrailingNewline && wentPastInitialNewline)) {
 770                 // Go to the end of this paragraph
 771                 while (pos < len && text.codePointAt(pos) != 0x0a) {
 772                     pos++;
 773                 }
 774                 if (goPastTrailingNewline && pos < len) {
 775                     // We are at the end of a paragraph, finish by moving to
 776                     // the beginning of the next paragraph (Windows behavior).
 777                     pos++;
 778                 }
 779             }
 780             if (select) {
 781                 textArea.selectPositionCaret(pos);
 782             } else {
 783                 textArea.positionCaret(pos);
 784             }
 785         }
 786     }
 787 
 788     /** {@inheritDoc} */
 789     @Override protected PathElement[] getUnderlineShape(int start, int end) {
 790         int pStart = 0;
 791         for (Node node : paragraphNodes.getChildren()) {
 792             Text p = (Text)node;
 793             int pEnd = pStart + p.textProperty().getValueSafe().length();
 794             if (pEnd >= start) {
 795                 return p.impl_getUnderlineShape(start - pStart, end - pStart);
 796             }
 797             pStart = pEnd + 1;
 798         }
 799         return null;
 800     }
 801 
 802     /** {@inheritDoc} */
 803     @Override protected PathElement[] getRangeShape(int start, int end) {
 804         int pStart = 0;
 805         for (Node node : paragraphNodes.getChildren()) {
 806             Text p = (Text)node;
 807             int pEnd = pStart + p.textProperty().getValueSafe().length();
 808             if (pEnd >= start) {
 809                 return p.impl_getRangeShape(start - pStart, end - pStart);
 810             }
 811             pStart = pEnd + 1;
 812         }
 813         return null;
 814     }
 815 
 816     /** {@inheritDoc} */
 817     @Override protected void addHighlight(List<? extends Node> nodes, int start) {
 818         int pStart = 0;
 819         Text paragraphNode = null;
 820         for (Node node : paragraphNodes.getChildren()) {
 821             Text p = (Text)node;
 822             int pEnd = pStart + p.textProperty().getValueSafe().length();
 823             if (pEnd >= start) {
 824                 paragraphNode = p;
 825                 break;
 826             }
 827             pStart = pEnd + 1;
 828         }
 829 
 830         if (paragraphNode != null) {
 831             for (Node node : nodes) {
 832                 node.setLayoutX(paragraphNode.getLayoutX());
 833                 node.setLayoutY(paragraphNode.getLayoutY());
 834             }
 835         }
 836         contentView.getChildren().addAll(nodes);
 837     }
 838 
 839     /** {@inheritDoc} */
 840     @Override protected void removeHighlight(List<? extends Node> nodes) {
 841         contentView.getChildren().removeAll(nodes);
 842     }
 843 
 844     /** {@inheritDoc} */
 845     @Override public Point2D getMenuPosition() {
 846         contentView.layoutChildren();
 847         Point2D p = super.getMenuPosition();
 848         if (p != null) {
 849             p = new Point2D(Math.max(0, p.getX() - contentView.snappedLeftInset() - getSkinnable().getScrollLeft()),
 850                     Math.max(0, p.getY() - contentView.snappedTopInset() - getSkinnable().getScrollTop()));
 851         }
 852         return p;
 853     }
 854 
 855     // Public for FXVKSkin
 856     /**
 857      * @return the {@code Bounds} of the caret shape, relative to the {@code TextArea}.
 858      */
 859     public Bounds getCaretBounds() {
 860         return getSkinnable().sceneToLocal(caretPath.localToScene(caretPath.getBoundsInLocal()));
 861     }
 862 
 863     /** {@inheritDoc} */
 864     @Override protected Object queryAccessibleAttribute(AccessibleAttribute attribute, Object... parameters) {
 865         switch (attribute) {
 866             case LINE_FOR_OFFSET:
 867             case LINE_START:
 868             case LINE_END:
 869             case BOUNDS_FOR_RANGE:
 870             case OFFSET_AT_POINT:
 871                 Text text = getTextNode();
 872                 return text.queryAccessibleAttribute(attribute, parameters);
 873             default: return super.queryAccessibleAttribute(attribute, parameters);
 874         }
 875     }
 876 
 877     /** {@inheritDoc} */
 878     @Override public void dispose() {
 879         super.dispose();
 880 
 881         if (behavior != null) {
 882             behavior.dispose();
 883         }
 884 
 885         // TODO Unregister listeners on text editor, paragraph list
 886         throw new UnsupportedOperationException();
 887     }
 888 
 889     /** {@inheritDoc} */
 890     @Override public double computeBaselineOffset(double topInset, double rightInset, double bottomInset, double leftInset) {
 891         Text firstParagraph = (Text) paragraphNodes.getChildren().get(0);
 892         return Utils.getAscent(getSkinnable().getFont(), firstParagraph.getBoundsType())
 893                 + contentView.snappedTopInset() + textArea.snappedTopInset();
 894     }
 895 
 896     private char getCharacter(int index) {
 897         int n = paragraphNodes.getChildren().size();
 898 
 899         int paragraphIndex = 0;
 900         int offset = index;
 901 
 902         String paragraph = null;
 903         while (paragraphIndex < n) {
 904             Text paragraphNode = (Text)paragraphNodes.getChildren().get(paragraphIndex);
 905             paragraph = paragraphNode.getText();
 906             int count = paragraph.length() + 1;
 907 
 908             if (offset < count) {
 909                 break;
 910             }
 911 
 912             offset -= count;
 913             paragraphIndex++;
 914         }
 915 
 916         return offset == paragraph.length() ? '\n' : paragraph.charAt(offset);
 917     }
 918 
 919     /** {@inheritDoc} */
 920     @Override protected int getInsertionPoint(double x, double y) {
 921         TextArea textArea = getSkinnable();
 922 
 923         int n = paragraphNodes.getChildren().size();
 924         int index = -1;
 925 
 926         if (n > 0) {
 927             if (y < contentView.snappedTopInset()) {
 928                 // Select the character at x in the first row
 929                 Text paragraphNode = (Text)paragraphNodes.getChildren().get(0);
 930                 index = getNextInsertionPoint(paragraphNode, x, -1, VerticalDirection.DOWN);
 931             } else if (y > contentView.snappedTopInset() + contentView.getHeight()) {
 932                 // Select the character at x in the last row
 933                 int lastParagraphIndex = n - 1;
 934                 Text lastParagraphView = (Text)paragraphNodes.getChildren().get(lastParagraphIndex);
 935 
 936                 index = getNextInsertionPoint(lastParagraphView, x, -1, VerticalDirection.UP)
 937                         + (textArea.getLength() - lastParagraphView.getText().length());
 938             } else {
 939                 // Select the character at x in the row at y
 940                 int paragraphOffset = 0;
 941                 for (int i = 0; i < n; i++) {
 942                     Text paragraphNode = (Text)paragraphNodes.getChildren().get(i);
 943 
 944                     Bounds bounds = paragraphNode.getBoundsInLocal();
 945                     double paragraphViewY = paragraphNode.getLayoutY() + bounds.getMinY();
 946                     if (y >= paragraphViewY
 947                             && y < paragraphViewY + paragraphNode.getBoundsInLocal().getHeight()) {
 948                         index = getInsertionPoint(paragraphNode,
 949                                 x - paragraphNode.getLayoutX(),
 950                                 y - paragraphNode.getLayoutY()) + paragraphOffset;
 951                         break;
 952                     }
 953 
 954                     paragraphOffset += paragraphNode.getText().length() + 1;
 955                 }
 956             }
 957         }
 958 
 959         return index;
 960     }
 961 
 962     // Public for behavior
 963     /**
 964      * Moves the caret to the specified position.
 965      *
 966      * @param hit the new position and forward bias of the caret.
 967      * @param select whether to extend selection to the new position.
 968      */
 969     public void positionCaret(TextPosInfo hit, boolean select) {
 970         positionCaret(hit, select, false);
 971     }
 972 
 973     private void positionCaret(TextPosInfo hit, boolean select, boolean extendSelection) {
 974         int pos = Utils.getHitInsertionIndex(hit, getSkinnable().getText());
 975         boolean isNewLine =
 976                 (pos > 0 &&
 977                         pos <= getSkinnable().getLength() &&
 978                         getSkinnable().getText().codePointAt(pos-1) == 0x0a);
 979 
 980         // special handling for a new line
 981         if (!hit.isLeading() && isNewLine) {
 982             hit.setLeading(true);
 983             pos -= 1;
 984         }
 985 
 986         if (select) {
 987             if (extendSelection) {
 988                 getSkinnable().extendSelection(pos);
 989             } else {
 990                 getSkinnable().selectPositionCaret(pos);
 991             }
 992         } else {
 993             getSkinnable().positionCaret(pos);
 994         }
 995 
 996         setForwardBias(hit.isLeading());
 997     }
 998 
 999     private void positionCaret(HitInfo hit, boolean select) {
1000         positionCaret(new TextPosInfo(hit), select);
1001     }
1002 
1003     private void positionCaret(HitInfo hit, boolean select, boolean extendSelection) {
1004         positionCaret(new TextPosInfo(hit), select, extendSelection);
1005     }
1006 
1007     /** {@inheritDoc} */
1008     @Override public Rectangle2D getCharacterBounds(int index) {
1009         TextArea textArea = getSkinnable();
1010 
1011         int paragraphIndex = paragraphNodes.getChildren().size();
1012         int paragraphOffset = textArea.getLength() + 1;
1013 
1014         Text paragraphNode = null;
1015         do {
1016             paragraphNode = (Text)paragraphNodes.getChildren().get(--paragraphIndex);
1017             paragraphOffset -= paragraphNode.getText().length() + 1;
1018         } while (index < paragraphOffset);
1019 
1020         int characterIndex = index - paragraphOffset;
1021         boolean terminator = false;
1022 
1023         if (characterIndex == paragraphNode.getText().length()) {
1024             characterIndex--;
1025             terminator = true;
1026         }
1027 
1028         characterBoundingPath.getElements().clear();
1029         characterBoundingPath.getElements().addAll(paragraphNode.impl_getRangeShape(characterIndex, characterIndex + 1));
1030         characterBoundingPath.setLayoutX(paragraphNode.getLayoutX());
1031         characterBoundingPath.setLayoutY(paragraphNode.getLayoutY());
1032 
1033         Bounds bounds = characterBoundingPath.getBoundsInLocal();
1034 
1035         double x = bounds.getMinX() + paragraphNode.getLayoutX() - textArea.getScrollLeft();
1036         double y = bounds.getMinY() + paragraphNode.getLayoutY() - textArea.getScrollTop();
1037 
1038         // Sometimes the bounds is empty, in which case we must ignore the width/height
1039         double width = bounds.isEmpty() ? 0 : bounds.getWidth();
1040         double height = bounds.isEmpty() ? 0 : bounds.getHeight();
1041 
1042         if (terminator) {
1043             x += width;
1044             width = 0;
1045         }
1046 
1047         return new Rectangle2D(x, y, width, height);
1048     }
1049 
1050     /** {@inheritDoc} */
1051     @Override protected void scrollCharacterToVisible(final int index) {
1052         // TODO We queue a callback because when characters are added or
1053         // removed the bounds are not immediately updated; is this really
1054         // necessary?
1055 
1056         Platform.runLater(() -> {
1057             if (getSkinnable().getLength() == 0) {
1058                 return;
1059             }
1060             Rectangle2D characterBounds = getCharacterBounds(index);
1061             scrollBoundsToVisible(characterBounds);
1062         });
1063     }
1064 
1065 
1066 
1067     /**************************************************************************
1068      *
1069      * Private implementation
1070      *
1071      **************************************************************************/
1072 
1073     TextAreaBehavior getBehavior() {
1074         return behavior;
1075     }
1076 
1077     private void createPromptNode() {
1078         if (promptNode == null && usePromptText.get()) {
1079             promptNode = new Text();
1080             contentView.getChildren().add(0, promptNode);
1081             promptNode.setManaged(false);
1082             promptNode.getStyleClass().add("text");
1083             promptNode.visibleProperty().bind(usePromptText);
1084             promptNode.fontProperty().bind(getSkinnable().fontProperty());
1085             promptNode.textProperty().bind(getSkinnable().promptTextProperty());
1086             promptNode.fillProperty().bind(promptTextFillProperty());
1087         }
1088     }
1089 
1090     private void addParagraphNode(int i, String string) {
1091         final TextArea textArea = getSkinnable();
1092         Text paragraphNode = new Text(string);
1093         paragraphNode.setTextOrigin(VPos.TOP);
1094         paragraphNode.setManaged(false);
1095         paragraphNode.getStyleClass().add("text");
1096         paragraphNode.boundsTypeProperty().addListener((observable, oldValue, newValue) -> {
1097             invalidateMetrics();
1098             updateFontMetrics();
1099         });
1100         paragraphNodes.getChildren().add(i, paragraphNode);
1101 
1102         paragraphNode.fontProperty().bind(textArea.fontProperty());
1103         paragraphNode.fillProperty().bind(textFillProperty());
1104         paragraphNode.impl_selectionFillProperty().bind(highlightTextFillProperty());
1105     }
1106 
1107     private double getScrollTopMax() {
1108         return Math.max(0, contentView.getHeight() - scrollPane.getViewportBounds().getHeight());
1109     }
1110 
1111     private double getScrollLeftMax() {
1112         return Math.max(0, contentView.getWidth() - scrollPane.getViewportBounds().getWidth());
1113     }
1114 
1115     private int getInsertionPoint(Text paragraphNode, double x, double y) {
1116         TextPosInfo hitInfo = new TextPosInfo(paragraphNode.impl_hitTestChar(new Point2D(x, y)));
1117         return Utils.getHitInsertionIndex(hitInfo, paragraphNode.getText());
1118     }
1119 
1120     private int getNextInsertionPoint(Text paragraphNode, double x, int from,
1121         VerticalDirection scrollDirection) {
1122         // TODO
1123         return 0;
1124     }
1125 
1126     private void scrollCaretToVisible() {
1127         TextArea textArea = getSkinnable();
1128         Bounds bounds = caretPath.getLayoutBounds();
1129         double x = bounds.getMinX() - textArea.getScrollLeft();
1130         double y = bounds.getMinY() - textArea.getScrollTop();
1131         double w = bounds.getWidth();
1132         double h = bounds.getHeight();
1133 
1134         if (SHOW_HANDLES) {
1135             if (caretHandle.isVisible()) {
1136                 h += caretHandle.getHeight();
1137             } else if (selectionHandle1.isVisible() && selectionHandle2.isVisible()) {
1138                 x -= selectionHandle1.getWidth() / 2;
1139                 y -= selectionHandle1.getHeight();
1140                 w += selectionHandle1.getWidth() / 2 + selectionHandle2.getWidth() / 2;
1141                 h += selectionHandle1.getHeight() + selectionHandle2.getHeight();
1142             }
1143         }
1144 
1145         if (w > 0 && h > 0) {
1146             scrollBoundsToVisible(new Rectangle2D(x, y, w, h));
1147         }
1148     }
1149 
1150     private void scrollBoundsToVisible(Rectangle2D bounds) {
1151         TextArea textArea = getSkinnable();
1152         Bounds viewportBounds = scrollPane.getViewportBounds();
1153 
1154         double viewportWidth = viewportBounds.getWidth();
1155         double viewportHeight = viewportBounds.getHeight();
1156         double scrollTop = textArea.getScrollTop();
1157         double scrollLeft = textArea.getScrollLeft();
1158         double slop = 6.0;
1159 
1160         if (bounds.getMinY() < 0) {
1161             double y = scrollTop + bounds.getMinY();
1162             if (y <= contentView.snappedTopInset()) {
1163                 y = 0;
1164             }
1165             textArea.setScrollTop(y);
1166         } else if (contentView.snappedTopInset() + bounds.getMaxY() > viewportHeight) {
1167             double y = scrollTop + contentView.snappedTopInset() + bounds.getMaxY() - viewportHeight;
1168             if (y >= getScrollTopMax() - contentView.snappedBottomInset()) {
1169                 y = getScrollTopMax();
1170             }
1171             textArea.setScrollTop(y);
1172         }
1173 
1174 
1175         if (bounds.getMinX() < 0) {
1176             double x = scrollLeft + bounds.getMinX() - slop;
1177             if (x <= contentView.snappedLeftInset() + slop) {
1178                 x = 0;
1179             }
1180             textArea.setScrollLeft(x);
1181         } else if (contentView.snappedLeftInset() + bounds.getMaxX() > viewportWidth) {
1182             double x = scrollLeft + contentView.snappedLeftInset() + bounds.getMaxX() - viewportWidth + slop;
1183             if (x >= getScrollLeftMax() - contentView.snappedRightInset() - slop) {
1184                 x = getScrollLeftMax();
1185             }
1186             textArea.setScrollLeft(x);
1187         }
1188     }
1189 
1190     private void updatePrefViewportWidth() {
1191         int columnCount = getSkinnable().getPrefColumnCount();
1192         scrollPane.setPrefViewportWidth(columnCount * characterWidth + contentView.snappedLeftInset() + contentView.snappedRightInset());
1193         scrollPane.setMinViewportWidth(characterWidth + contentView.snappedLeftInset() + contentView.snappedRightInset());
1194     }
1195 
1196     private void updatePrefViewportHeight() {
1197         int rowCount = getSkinnable().getPrefRowCount();
1198         scrollPane.setPrefViewportHeight(rowCount * lineHeight + contentView.snappedTopInset() + contentView.snappedBottomInset());
1199         scrollPane.setMinViewportHeight(lineHeight + contentView.snappedTopInset() + contentView.snappedBottomInset());
1200     }
1201 
1202     private void updateFontMetrics() {
1203         Text firstParagraph = (Text)paragraphNodes.getChildren().get(0);
1204         lineHeight = Utils.getLineHeight(getSkinnable().getFont(), firstParagraph.getBoundsType());
1205         characterWidth = fontMetrics.get().computeStringWidth("W");
1206     }
1207 
1208     private double getTextTranslateX() {
1209         return contentView.snappedLeftInset();
1210     }
1211 
1212     private double getTextTranslateY() {
1213         return contentView.snappedTopInset();
1214     }
1215 
1216     private double getTextLeft() {
1217         return 0;
1218     }
1219 
1220     private Point2D translateCaretPosition(Point2D p) {
1221         return p;
1222     }
1223 
1224     private Text getTextNode() {
1225         if (USE_MULTIPLE_NODES) {
1226             throw new IllegalArgumentException("Multiple node traversal is not yet implemented.");
1227         }
1228         return (Text)paragraphNodes.getChildren().get(0);
1229     }
1230 
1231     private void updateTextNodeCaretPos(int pos) {
1232         Text textNode = getTextNode();
1233         if (isForwardBias()) {
1234             textNode.setImpl_caretPosition(pos);
1235         } else {
1236             textNode.setImpl_caretPosition(pos - 1);
1237         }
1238         textNode.impl_caretBiasProperty().set(isForwardBias());
1239     }
1240 
1241 
1242 
1243     /**************************************************************************
1244      *
1245      * Support classes
1246      *
1247      **************************************************************************/
1248 
1249     private class ContentView extends Region {
1250         {
1251             getStyleClass().add("content");
1252 
1253             addEventHandler(MouseEvent.MOUSE_PRESSED, event -> {
1254                 behavior.mousePressed(event);
1255                 event.consume();
1256             });
1257 
1258             addEventHandler(MouseEvent.MOUSE_RELEASED, event -> {
1259                 behavior.mouseReleased(event);
1260                 event.consume();
1261             });
1262 
1263             addEventHandler(MouseEvent.MOUSE_DRAGGED, event -> {
1264                 behavior.mouseDragged(event);
1265                 event.consume();
1266             });
1267         }
1268 
1269         @Override protected ObservableList<Node> getChildren() {
1270             return super.getChildren();
1271         }
1272 
1273         @Override public Orientation getContentBias() {
1274             return Orientation.HORIZONTAL;
1275         }
1276 
1277         @Override protected double computePrefWidth(double height) {
1278             if (computedPrefWidth < 0) {
1279                 double prefWidth = 0;
1280 
1281                 for (Node node : paragraphNodes.getChildren()) {
1282                     Text paragraphNode = (Text)node;
1283                     prefWidth = Math.max(prefWidth,
1284                             Utils.computeTextWidth(paragraphNode.getFont(),
1285                                     paragraphNode.getText(), 0));
1286                 }
1287 
1288                 prefWidth += snappedLeftInset() + snappedRightInset();
1289 
1290                 Bounds viewPortBounds = scrollPane.getViewportBounds();
1291                 computedPrefWidth = Math.max(prefWidth, (viewPortBounds != null) ? viewPortBounds.getWidth() : 0);
1292             }
1293             return computedPrefWidth;
1294         }
1295 
1296         @Override protected double computePrefHeight(double width) {
1297             if (width != widthForComputedPrefHeight) {
1298                 invalidateMetrics();
1299                 widthForComputedPrefHeight = width;
1300             }
1301 
1302             if (computedPrefHeight < 0) {
1303                 double wrappingWidth;
1304                 if (width == -1) {
1305                     wrappingWidth = 0;
1306                 } else {
1307                     wrappingWidth = Math.max(width - (snappedLeftInset() + snappedRightInset()), 0);
1308                 }
1309 
1310                 double prefHeight = 0;
1311 
1312                 for (Node node : paragraphNodes.getChildren()) {
1313                     Text paragraphNode = (Text)node;
1314                     prefHeight += Utils.computeTextHeight(
1315                             paragraphNode.getFont(),
1316                             paragraphNode.getText(),
1317                             wrappingWidth,
1318                             paragraphNode.getBoundsType());
1319                 }
1320 
1321                 prefHeight += snappedTopInset() + snappedBottomInset();
1322 
1323                 Bounds viewPortBounds = scrollPane.getViewportBounds();
1324                 computedPrefHeight = Math.max(prefHeight, (viewPortBounds != null) ? viewPortBounds.getHeight() : 0);
1325             }
1326             return computedPrefHeight;
1327         }
1328 
1329         @Override protected double computeMinWidth(double height) {
1330             if (computedMinWidth < 0) {
1331                 double hInsets = snappedLeftInset() + snappedRightInset();
1332                 computedMinWidth = Math.min(characterWidth + hInsets, computePrefWidth(height));
1333             }
1334             return computedMinWidth;
1335         }
1336 
1337         @Override protected double computeMinHeight(double width) {
1338             if (computedMinHeight < 0) {
1339                 double vInsets = snappedTopInset() + snappedBottomInset();
1340                 computedMinHeight = Math.min(lineHeight + vInsets, computePrefHeight(width));
1341             }
1342             return computedMinHeight;
1343         }
1344 
1345         @Override public void layoutChildren() {
1346             TextArea textArea = getSkinnable();
1347             double width = getWidth();
1348 
1349             // Lay out paragraphs
1350             final double topPadding = snappedTopInset();
1351             final double leftPadding = snappedLeftInset();
1352 
1353             double wrappingWidth = Math.max(width - (leftPadding + snappedRightInset()), 0);
1354 
1355             double y = topPadding;
1356 
1357             final List<Node> paragraphNodesChildren = paragraphNodes.getChildren();
1358 
1359             for (int i = 0; i < paragraphNodesChildren.size(); i++) {
1360                 Node node = paragraphNodesChildren.get(i);
1361                 Text paragraphNode = (Text)node;
1362                 paragraphNode.setWrappingWidth(wrappingWidth);
1363 
1364                 Bounds bounds = paragraphNode.getBoundsInLocal();
1365                 paragraphNode.setLayoutX(leftPadding);
1366                 paragraphNode.setLayoutY(y);
1367 
1368                 y += bounds.getHeight();
1369             }
1370 
1371             if (promptNode != null) {
1372                 promptNode.setLayoutX(leftPadding);
1373                 promptNode.setLayoutY(topPadding + promptNode.getBaselineOffset());
1374                 promptNode.setWrappingWidth(wrappingWidth);
1375             }
1376 
1377             // Update the selection
1378             IndexRange selection = textArea.getSelection();
1379             Bounds oldCaretBounds = caretPath.getBoundsInParent();
1380 
1381             selectionHighlightGroup.getChildren().clear();
1382 
1383             int caretPos = textArea.getCaretPosition();
1384             int anchorPos = textArea.getAnchor();
1385 
1386             if (SHOW_HANDLES) {
1387                 // Install and resize the handles for caret and anchor.
1388                 if (selection.getLength() > 0) {
1389                     selectionHandle1.resize(selectionHandle1.prefWidth(-1),
1390                             selectionHandle1.prefHeight(-1));
1391                     selectionHandle2.resize(selectionHandle2.prefWidth(-1),
1392                             selectionHandle2.prefHeight(-1));
1393                 } else {
1394                     caretHandle.resize(caretHandle.prefWidth(-1),
1395                             caretHandle.prefHeight(-1));
1396                 }
1397 
1398                 // Position the handle for the anchor. This could be handle1 or handle2.
1399                 // Do this before positioning the actual caret.
1400                 if (selection.getLength() > 0) {
1401                     int paragraphIndex = paragraphNodesChildren.size();
1402                     int paragraphOffset = textArea.getLength() + 1;
1403                     Text paragraphNode = null;
1404                     do {
1405                         paragraphNode = (Text)paragraphNodesChildren.get(--paragraphIndex);
1406                         paragraphOffset -= paragraphNode.getText().length() + 1;
1407                     } while (anchorPos < paragraphOffset);
1408 
1409                     updateTextNodeCaretPos(anchorPos - paragraphOffset);
1410                     caretPath.getElements().clear();
1411                     caretPath.getElements().addAll(paragraphNode.getImpl_caretShape());
1412                     caretPath.setLayoutX(paragraphNode.getLayoutX());
1413                     caretPath.setLayoutY(paragraphNode.getLayoutY());
1414 
1415                     Bounds b = caretPath.getBoundsInParent();
1416                     if (caretPos < anchorPos) {
1417                         selectionHandle2.setLayoutX(b.getMinX() - selectionHandle2.getWidth() / 2);
1418                         selectionHandle2.setLayoutY(b.getMaxY() - 1);
1419                     } else {
1420                         selectionHandle1.setLayoutX(b.getMinX() - selectionHandle1.getWidth() / 2);
1421                         selectionHandle1.setLayoutY(b.getMinY() - selectionHandle1.getHeight() + 1);
1422                     }
1423                 }
1424             }
1425 
1426             {
1427                 // Position caret
1428                 int paragraphIndex = paragraphNodesChildren.size();
1429                 int paragraphOffset = textArea.getLength() + 1;
1430 
1431                 Text paragraphNode = null;
1432                 do {
1433                     paragraphNode = (Text)paragraphNodesChildren.get(--paragraphIndex);
1434                     paragraphOffset -= paragraphNode.getText().length() + 1;
1435                 } while (caretPos < paragraphOffset);
1436 
1437                 updateTextNodeCaretPos(caretPos - paragraphOffset);
1438 
1439                 caretPath.getElements().clear();
1440                 caretPath.getElements().addAll(paragraphNode.getImpl_caretShape());
1441 
1442                 caretPath.setLayoutX(paragraphNode.getLayoutX());
1443 
1444                 // TODO: Remove this temporary workaround for RT-27533
1445                 paragraphNode.setLayoutX(2 * paragraphNode.getLayoutX() - paragraphNode.getBoundsInParent().getMinX());
1446 
1447                 caretPath.setLayoutY(paragraphNode.getLayoutY());
1448                 if (oldCaretBounds == null || !oldCaretBounds.equals(caretPath.getBoundsInParent())) {
1449                     scrollCaretToVisible();
1450                 }
1451             }
1452 
1453             // Update selection fg and bg
1454             int start = selection.getStart();
1455             int end = selection.getEnd();
1456             for (int i = 0, max = paragraphNodesChildren.size(); i < max; i++) {
1457                 Node paragraphNode = paragraphNodesChildren.get(i);
1458                 Text textNode = (Text)paragraphNode;
1459                 int paragraphLength = textNode.getText().length() + 1;
1460                 if (end > start && start < paragraphLength) {
1461                     textNode.setImpl_selectionStart(start);
1462                     textNode.setImpl_selectionEnd(Math.min(end, paragraphLength));
1463 
1464                     Path selectionHighlightPath = new Path();
1465                     selectionHighlightPath.setManaged(false);
1466                     selectionHighlightPath.setStroke(null);
1467                     PathElement[] selectionShape = textNode.getImpl_selectionShape();
1468                     if (selectionShape != null) {
1469                         selectionHighlightPath.getElements().addAll(selectionShape);
1470                     }
1471                     selectionHighlightGroup.getChildren().add(selectionHighlightPath);
1472                     selectionHighlightGroup.setVisible(true);
1473                     selectionHighlightPath.setLayoutX(textNode.getLayoutX());
1474                     selectionHighlightPath.setLayoutY(textNode.getLayoutY());
1475                     updateHighlightFill();
1476                 } else {
1477                     textNode.setImpl_selectionStart(-1);
1478                     textNode.setImpl_selectionEnd(-1);
1479                     selectionHighlightGroup.setVisible(false);
1480                 }
1481                 start = Math.max(0, start - paragraphLength);
1482                 end   = Math.max(0, end   - paragraphLength);
1483             }
1484 
1485             if (SHOW_HANDLES) {
1486                 // Position handle for the caret. This could be handle1 or handle2 when
1487                 // a selection is active.
1488                 Bounds b = caretPath.getBoundsInParent();
1489                 if (selection.getLength() > 0) {
1490                     if (caretPos < anchorPos) {
1491                         selectionHandle1.setLayoutX(b.getMinX() - selectionHandle1.getWidth() / 2);
1492                         selectionHandle1.setLayoutY(b.getMinY() - selectionHandle1.getHeight() + 1);
1493                     } else {
1494                         selectionHandle2.setLayoutX(b.getMinX() - selectionHandle2.getWidth() / 2);
1495                         selectionHandle2.setLayoutY(b.getMaxY() - 1);
1496                     }
1497                 } else {
1498                     caretHandle.setLayoutX(b.getMinX() - caretHandle.getWidth() / 2 + 1);
1499                     caretHandle.setLayoutY(b.getMaxY());
1500                 }
1501             }
1502 
1503             if (scrollPane.getPrefViewportWidth() == 0
1504                     || scrollPane.getPrefViewportHeight() == 0) {
1505                 updatePrefViewportWidth();
1506                 updatePrefViewportHeight();
1507                 if (getParent() != null && scrollPane.getPrefViewportWidth() > 0
1508                         || scrollPane.getPrefViewportHeight() > 0) {
1509                     // Force layout of viewRect in ScrollPaneSkin
1510                     getParent().requestLayout();
1511                 }
1512             }
1513 
1514             // RT-36454: Fit to width/height only if smaller than viewport.
1515             // That is, grow to fit but don't shrink to fit.
1516             Bounds viewportBounds = scrollPane.getViewportBounds();
1517             boolean wasFitToWidth = scrollPane.isFitToWidth();
1518             boolean wasFitToHeight = scrollPane.isFitToHeight();
1519             boolean setFitToWidth = textArea.isWrapText() || computePrefWidth(-1) <= viewportBounds.getWidth();
1520             boolean setFitToHeight = computePrefHeight(width) <= viewportBounds.getHeight();
1521             if (wasFitToWidth != setFitToWidth || wasFitToHeight != setFitToHeight) {
1522                 Platform.runLater(() -> {
1523                     scrollPane.setFitToWidth(setFitToWidth);
1524                     scrollPane.setFitToHeight(setFitToHeight);
1525                 });
1526                 getParent().requestLayout();
1527             }
1528         }
1529     }
1530 }