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