modules/controls/src/main/java/javafx/scene/control/skin/TextAreaSkin.java

Print this page
rev 9240 : 8076423: JEP 253: Prepare JavaFX UI Controls & CSS APIs for Modularization


   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 }


   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             }
 750         }
 751     }
 752 
 753     private void paragraphEnd(boolean goPastInitialNewline, boolean select) {
 754         TextArea textArea = getSkinnable();
 755         String text = textArea.textProperty().getValueSafe();
 756         int pos = textArea.getCaretPosition();
 757         int len = text.length();
 758         boolean wentPastInitialNewline = false;
 759         boolean goPastTrailingNewline = isWindows();
 760 
 761         if (pos < len) {
 762             if (goPastInitialNewline && text.codePointAt(pos) == 0x0a) {
 763                 // We are at the end of a paragraph, start by moving to the
 764                 // next paragraph.
 765                 pos++;
 766                 wentPastInitialNewline = true;
 767             }
 768             if (!(goPastTrailingNewline && wentPastInitialNewline)) {
 769                 // Go to the end of this paragraph
 770                 while (pos < len && text.codePointAt(pos) != 0x0a) {
 771                     pos++;
 772                 }
 773                 if (goPastTrailingNewline && pos < len) {
 774                     // We are at the end of a paragraph, finish by moving to
 775                     // the beginning of the next paragraph (Windows behavior).
 776                     pos++;
 777                 }
 778             }
 779             if (select) {
 780                 textArea.selectPositionCaret(pos);
 781             } else {
 782                 textArea.positionCaret(pos);
 783             }
 784         }
 785     }
 786 
 787     /** {@inheritDoc} */









 788     @Override protected PathElement[] getUnderlineShape(int start, int end) {
 789         int pStart = 0;
 790         for (Node node : paragraphNodes.getChildren()) {
 791             Text p = (Text)node;
 792             int pEnd = pStart + p.textProperty().getValueSafe().length();
 793             if (pEnd >= start) {
 794                 return p.impl_getUnderlineShape(start - pStart, end - pStart);
 795             }
 796             pStart = pEnd + 1;
 797         }
 798         return null;
 799     }
 800 
 801     /** {@inheritDoc} */
 802     @Override protected PathElement[] getRangeShape(int start, int end) {
 803         int pStart = 0;
 804         for (Node node : paragraphNodes.getChildren()) {
 805             Text p = (Text)node;
 806             int pEnd = pStart + p.textProperty().getValueSafe().length();
 807             if (pEnd >= start) {
 808                 return p.impl_getRangeShape(start - pStart, end - pStart);
 809             }
 810             pStart = pEnd + 1;
 811         }
 812         return null;
 813     }
 814 
 815     /** {@inheritDoc} */
 816     @Override protected void addHighlight(List<? extends Node> nodes, int start) {
 817         int pStart = 0;
 818         Text paragraphNode = null;
 819         for (Node node : paragraphNodes.getChildren()) {
 820             Text p = (Text)node;
 821             int pEnd = pStart + p.textProperty().getValueSafe().length();
 822             if (pEnd >= start) {
 823                 paragraphNode = p;
 824                 break;
 825             }
 826             pStart = pEnd + 1;
 827         }
 828 
 829         if (paragraphNode != null) {
 830             for (Node node : nodes) {
 831                 node.setLayoutX(paragraphNode.getLayoutX());
 832                 node.setLayoutY(paragraphNode.getLayoutY());
 833             }
 834         }
 835         contentView.getChildren().addAll(nodes);
 836     }
 837 
 838     /** {@inheritDoc} */
 839     @Override protected void removeHighlight(List<? extends Node> nodes) {
 840         contentView.getChildren().removeAll(nodes);
 841     }
 842 
 843     /** {@inheritDoc} */


















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