< prev index next >

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

Print this page


   1 /*
   2  * Copyright (c) 2011, 2015, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 
  26 package javafx.scene.control.skin;
  27 
  28 import com.sun.javafx.scene.control.behavior.BehaviorBase;
  29 import com.sun.javafx.scene.control.behavior.TextAreaBehavior;
  30 import com.sun.javafx.scene.control.skin.Utils;
  31 import com.sun.javafx.scene.text.HitInfo;
  32 import javafx.animation.KeyFrame;
  33 import javafx.animation.Timeline;
  34 import javafx.application.Platform;
  35 import javafx.beans.binding.BooleanBinding;
  36 import javafx.beans.binding.DoubleBinding;
  37 import javafx.beans.binding.IntegerBinding;
  38 import javafx.beans.value.ObservableBooleanValue;
  39 import javafx.beans.value.ObservableIntegerValue;
  40 import javafx.collections.ListChangeListener;
  41 import javafx.collections.ObservableList;
  42 import javafx.event.ActionEvent;
  43 import javafx.event.EventHandler;
  44 import javafx.geometry.Bounds;
  45 import javafx.geometry.Orientation;
  46 import javafx.geometry.Point2D;
  47 import javafx.geometry.Rectangle2D;
  48 import javafx.geometry.VPos;
  49 import javafx.geometry.VerticalDirection;
  50 import javafx.scene.AccessibleAttribute;
  51 import javafx.scene.Group;
  52 import javafx.scene.Node;
  53 import javafx.scene.control.Accordion;
  54 import javafx.scene.control.Button;
  55 import javafx.scene.control.Control;
  56 import javafx.scene.control.IndexRange;
  57 import javafx.scene.control.ScrollPane;
  58 import javafx.scene.control.TextArea;
  59 import javafx.scene.input.MouseEvent;
  60 import javafx.scene.input.ScrollEvent;
  61 import javafx.scene.layout.Region;
  62 import javafx.scene.shape.MoveTo;
  63 import javafx.scene.shape.Path;
  64 import javafx.scene.shape.PathElement;
  65 import javafx.scene.text.Text;

  66 import javafx.util.Duration;
  67 
  68 import java.util.List;
  69 
  70 import static com.sun.javafx.PlatformUtil.isMac;
  71 import static com.sun.javafx.PlatformUtil.isWindows;
  72 
  73 /**
  74  * Default skin implementation for the {@link TextArea} control.
  75  *
  76  * @see TextArea
  77  * @since 9
  78  */
  79 public class TextAreaSkin extends TextInputControlSkin<TextArea> {
  80 
  81     /**************************************************************************
  82      *
  83      * Static fields
  84      *
  85      **************************************************************************/


 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;


 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);


 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);


 775                     // We are at the end of a paragraph, finish by moving to
 776                     // the beginning of the next paragraph (Windows behavior).
 777                     pos++;
 778                 }
 779             }
 780             if (select) {
 781                 textArea.selectPositionCaret(pos);
 782             } else {
 783                 textArea.positionCaret(pos);
 784             }
 785         }
 786     }
 787 
 788     /** {@inheritDoc} */
 789     @Override protected PathElement[] getUnderlineShape(int start, int end) {
 790         int pStart = 0;
 791         for (Node node : paragraphNodes.getChildren()) {
 792             Text p = (Text)node;
 793             int pEnd = pStart + p.textProperty().getValueSafe().length();
 794             if (pEnd >= start) {
 795                 return p.impl_getUnderlineShape(start - pStart, end - pStart);
 796             }
 797             pStart = pEnd + 1;
 798         }
 799         return null;
 800     }
 801 
 802     /** {@inheritDoc} */
 803     @Override protected PathElement[] getRangeShape(int start, int end) {
 804         int pStart = 0;
 805         for (Node node : paragraphNodes.getChildren()) {
 806             Text p = (Text)node;
 807             int pEnd = pStart + p.textProperty().getValueSafe().length();
 808             if (pEnd >= start) {
 809                 return p.impl_getRangeShape(start - pStart, end - pStart);
 810             }
 811             pStart = pEnd + 1;
 812         }
 813         return null;
 814     }
 815 
 816     /** {@inheritDoc} */
 817     @Override protected void addHighlight(List<? extends Node> nodes, int start) {
 818         int pStart = 0;
 819         Text paragraphNode = null;
 820         for (Node node : paragraphNodes.getChildren()) {
 821             Text p = (Text)node;
 822             int pEnd = pStart + p.textProperty().getValueSafe().length();
 823             if (pEnd >= start) {
 824                 paragraphNode = p;
 825                 break;
 826             }
 827             pStart = pEnd + 1;
 828         }
 829 


 949                                 x - paragraphNode.getLayoutX(),
 950                                 y - paragraphNode.getLayoutY()) + paragraphOffset;
 951                         break;
 952                     }
 953 
 954                     paragraphOffset += paragraphNode.getText().length() + 1;
 955                 }
 956             }
 957         }
 958 
 959         return index;
 960     }
 961 
 962     // Public for behavior
 963     /**
 964      * Moves the caret to the specified position.
 965      *
 966      * @param hit the new position and forward bias of the caret.
 967      * @param select whether to extend selection to the new position.
 968      */
 969     public void positionCaret(TextPosInfo hit, boolean select) {
 970         positionCaret(hit, select, false);
 971     }
 972 
 973     private void positionCaret(TextPosInfo hit, boolean select, boolean extendSelection) {
 974         int pos = Utils.getHitInsertionIndex(hit, getSkinnable().getText());
 975         boolean isNewLine =
 976                 (pos > 0 &&
 977                         pos <= getSkinnable().getLength() &&
 978                         getSkinnable().getText().codePointAt(pos-1) == 0x0a);
 979 
 980         // special handling for a new line
 981         if (!hit.isLeading() && isNewLine) {
 982             hit.setLeading(true);
 983             pos -= 1;
 984         }
 985 
 986         if (select) {
 987             if (extendSelection) {
 988                 getSkinnable().extendSelection(pos);
 989             } else {
 990                 getSkinnable().selectPositionCaret(pos);
 991             }
 992         } else {
 993             getSkinnable().positionCaret(pos);
 994         }
 995 
 996         setForwardBias(hit.isLeading());
 997     }
 998 
 999     private void positionCaret(HitInfo hit, boolean select) {
1000         positionCaret(new TextPosInfo(hit), select);
1001     }
1002 
1003     private void positionCaret(HitInfo hit, boolean select, boolean extendSelection) {
1004         positionCaret(new TextPosInfo(hit), select, extendSelection);
1005     }
1006 
1007     /** {@inheritDoc} */
1008     @Override public Rectangle2D getCharacterBounds(int index) {
1009         TextArea textArea = getSkinnable();
1010 
1011         int paragraphIndex = paragraphNodes.getChildren().size();
1012         int paragraphOffset = textArea.getLength() + 1;
1013 
1014         Text paragraphNode = null;
1015         do {
1016             paragraphNode = (Text)paragraphNodes.getChildren().get(--paragraphIndex);
1017             paragraphOffset -= paragraphNode.getText().length() + 1;
1018         } while (index < paragraphOffset);
1019 
1020         int characterIndex = index - paragraphOffset;
1021         boolean terminator = false;
1022 
1023         if (characterIndex == paragraphNode.getText().length()) {
1024             characterIndex--;
1025             terminator = true;
1026         }
1027 
1028         characterBoundingPath.getElements().clear();
1029         characterBoundingPath.getElements().addAll(paragraphNode.impl_getRangeShape(characterIndex, characterIndex + 1));
1030         characterBoundingPath.setLayoutX(paragraphNode.getLayoutX());
1031         characterBoundingPath.setLayoutY(paragraphNode.getLayoutY());
1032 
1033         Bounds bounds = characterBoundingPath.getBoundsInLocal();
1034 
1035         double x = bounds.getMinX() + paragraphNode.getLayoutX() - textArea.getScrollLeft();
1036         double y = bounds.getMinY() + paragraphNode.getLayoutY() - textArea.getScrollTop();
1037 
1038         // Sometimes the bounds is empty, in which case we must ignore the width/height
1039         double width = bounds.isEmpty() ? 0 : bounds.getWidth();
1040         double height = bounds.isEmpty() ? 0 : bounds.getHeight();
1041 
1042         if (terminator) {
1043             x += width;
1044             width = 0;
1045         }
1046 
1047         return new Rectangle2D(x, y, width, height);
1048     }
1049 


1084             promptNode.fontProperty().bind(getSkinnable().fontProperty());
1085             promptNode.textProperty().bind(getSkinnable().promptTextProperty());
1086             promptNode.fillProperty().bind(promptTextFillProperty());
1087         }
1088     }
1089 
1090     private void addParagraphNode(int i, String string) {
1091         final TextArea textArea = getSkinnable();
1092         Text paragraphNode = new Text(string);
1093         paragraphNode.setTextOrigin(VPos.TOP);
1094         paragraphNode.setManaged(false);
1095         paragraphNode.getStyleClass().add("text");
1096         paragraphNode.boundsTypeProperty().addListener((observable, oldValue, newValue) -> {
1097             invalidateMetrics();
1098             updateFontMetrics();
1099         });
1100         paragraphNodes.getChildren().add(i, paragraphNode);
1101 
1102         paragraphNode.fontProperty().bind(textArea.fontProperty());
1103         paragraphNode.fillProperty().bind(textFillProperty());
1104         paragraphNode.impl_selectionFillProperty().bind(highlightTextFillProperty());
1105     }
1106 
1107     private double getScrollTopMax() {
1108         return Math.max(0, contentView.getHeight() - scrollPane.getViewportBounds().getHeight());
1109     }
1110 
1111     private double getScrollLeftMax() {
1112         return Math.max(0, contentView.getWidth() - scrollPane.getViewportBounds().getWidth());
1113     }
1114 
1115     private int getInsertionPoint(Text paragraphNode, double x, double y) {
1116         TextPosInfo hitInfo = new TextPosInfo(paragraphNode.impl_hitTestChar(new Point2D(x, y)));
1117         return Utils.getHitInsertionIndex(hitInfo, paragraphNode.getText());
1118     }
1119 
1120     private int getNextInsertionPoint(Text paragraphNode, double x, int from,
1121         VerticalDirection scrollDirection) {
1122         // TODO
1123         return 0;
1124     }
1125 
1126     private void scrollCaretToVisible() {
1127         TextArea textArea = getSkinnable();
1128         Bounds bounds = caretPath.getLayoutBounds();
1129         double x = bounds.getMinX() - textArea.getScrollLeft();
1130         double y = bounds.getMinY() - textArea.getScrollTop();
1131         double w = bounds.getWidth();
1132         double h = bounds.getHeight();
1133 
1134         if (SHOW_HANDLES) {
1135             if (caretHandle.isVisible()) {
1136                 h += caretHandle.getHeight();
1137             } else if (selectionHandle1.isVisible() && selectionHandle2.isVisible()) {


1214     }
1215 
1216     private double getTextLeft() {
1217         return 0;
1218     }
1219 
1220     private Point2D translateCaretPosition(Point2D p) {
1221         return p;
1222     }
1223 
1224     private Text getTextNode() {
1225         if (USE_MULTIPLE_NODES) {
1226             throw new IllegalArgumentException("Multiple node traversal is not yet implemented.");
1227         }
1228         return (Text)paragraphNodes.getChildren().get(0);
1229     }
1230 
1231     private void updateTextNodeCaretPos(int pos) {
1232         Text textNode = getTextNode();
1233         if (isForwardBias()) {
1234             textNode.setImpl_caretPosition(pos);
1235         } else {
1236             textNode.setImpl_caretPosition(pos - 1);
1237         }
1238         textNode.impl_caretBiasProperty().set(isForwardBias());
1239     }
1240 
1241 
1242 
1243     /**************************************************************************
1244      *
1245      * Support classes
1246      *
1247      **************************************************************************/
1248 
1249     private class ContentView extends Region {
1250         {
1251             getStyleClass().add("content");
1252 
1253             addEventHandler(MouseEvent.MOUSE_PRESSED, event -> {
1254                 behavior.mousePressed(event);
1255                 event.consume();
1256             });
1257 
1258             addEventHandler(MouseEvent.MOUSE_RELEASED, event -> {


1391                     selectionHandle2.resize(selectionHandle2.prefWidth(-1),
1392                             selectionHandle2.prefHeight(-1));
1393                 } else {
1394                     caretHandle.resize(caretHandle.prefWidth(-1),
1395                             caretHandle.prefHeight(-1));
1396                 }
1397 
1398                 // Position the handle for the anchor. This could be handle1 or handle2.
1399                 // Do this before positioning the actual caret.
1400                 if (selection.getLength() > 0) {
1401                     int paragraphIndex = paragraphNodesChildren.size();
1402                     int paragraphOffset = textArea.getLength() + 1;
1403                     Text paragraphNode = null;
1404                     do {
1405                         paragraphNode = (Text)paragraphNodesChildren.get(--paragraphIndex);
1406                         paragraphOffset -= paragraphNode.getText().length() + 1;
1407                     } while (anchorPos < paragraphOffset);
1408 
1409                     updateTextNodeCaretPos(anchorPos - paragraphOffset);
1410                     caretPath.getElements().clear();
1411                     caretPath.getElements().addAll(paragraphNode.getImpl_caretShape());
1412                     caretPath.setLayoutX(paragraphNode.getLayoutX());
1413                     caretPath.setLayoutY(paragraphNode.getLayoutY());
1414 
1415                     Bounds b = caretPath.getBoundsInParent();
1416                     if (caretPos < anchorPos) {
1417                         selectionHandle2.setLayoutX(b.getMinX() - selectionHandle2.getWidth() / 2);
1418                         selectionHandle2.setLayoutY(b.getMaxY() - 1);
1419                     } else {
1420                         selectionHandle1.setLayoutX(b.getMinX() - selectionHandle1.getWidth() / 2);
1421                         selectionHandle1.setLayoutY(b.getMinY() - selectionHandle1.getHeight() + 1);
1422                     }
1423                 }
1424             }
1425 
1426             {
1427                 // Position caret
1428                 int paragraphIndex = paragraphNodesChildren.size();
1429                 int paragraphOffset = textArea.getLength() + 1;
1430 
1431                 Text paragraphNode = null;
1432                 do {
1433                     paragraphNode = (Text)paragraphNodesChildren.get(--paragraphIndex);
1434                     paragraphOffset -= paragraphNode.getText().length() + 1;
1435                 } while (caretPos < paragraphOffset);
1436 
1437                 updateTextNodeCaretPos(caretPos - paragraphOffset);
1438 
1439                 caretPath.getElements().clear();
1440                 caretPath.getElements().addAll(paragraphNode.getImpl_caretShape());
1441 
1442                 caretPath.setLayoutX(paragraphNode.getLayoutX());
1443 
1444                 // TODO: Remove this temporary workaround for RT-27533
1445                 paragraphNode.setLayoutX(2 * paragraphNode.getLayoutX() - paragraphNode.getBoundsInParent().getMinX());
1446 
1447                 caretPath.setLayoutY(paragraphNode.getLayoutY());
1448                 if (oldCaretBounds == null || !oldCaretBounds.equals(caretPath.getBoundsInParent())) {
1449                     scrollCaretToVisible();
1450                 }
1451             }
1452 
1453             // Update selection fg and bg
1454             int start = selection.getStart();
1455             int end = selection.getEnd();
1456             for (int i = 0, max = paragraphNodesChildren.size(); i < max; i++) {
1457                 Node paragraphNode = paragraphNodesChildren.get(i);
1458                 Text textNode = (Text)paragraphNode;
1459                 int paragraphLength = textNode.getText().length() + 1;
1460                 if (end > start && start < paragraphLength) {
1461                     textNode.setImpl_selectionStart(start);
1462                     textNode.setImpl_selectionEnd(Math.min(end, paragraphLength));
1463 
1464                     Path selectionHighlightPath = new Path();
1465                     selectionHighlightPath.setManaged(false);
1466                     selectionHighlightPath.setStroke(null);
1467                     PathElement[] selectionShape = textNode.getImpl_selectionShape();
1468                     if (selectionShape != null) {
1469                         selectionHighlightPath.getElements().addAll(selectionShape);
1470                     }
1471                     selectionHighlightGroup.getChildren().add(selectionHighlightPath);
1472                     selectionHighlightGroup.setVisible(true);
1473                     selectionHighlightPath.setLayoutX(textNode.getLayoutX());
1474                     selectionHighlightPath.setLayoutY(textNode.getLayoutY());
1475                     updateHighlightFill();
1476                 } else {
1477                     textNode.setImpl_selectionStart(-1);
1478                     textNode.setImpl_selectionEnd(-1);
1479                     selectionHighlightGroup.setVisible(false);
1480                 }
1481                 start = Math.max(0, start - paragraphLength);
1482                 end   = Math.max(0, end   - paragraphLength);
1483             }
1484 
1485             if (SHOW_HANDLES) {
1486                 // Position handle for the caret. This could be handle1 or handle2 when
1487                 // a selection is active.
1488                 Bounds b = caretPath.getBoundsInParent();
1489                 if (selection.getLength() > 0) {
1490                     if (caretPos < anchorPos) {
1491                         selectionHandle1.setLayoutX(b.getMinX() - selectionHandle1.getWidth() / 2);
1492                         selectionHandle1.setLayoutY(b.getMinY() - selectionHandle1.getHeight() + 1);
1493                     } else {
1494                         selectionHandle2.setLayoutX(b.getMinX() - selectionHandle2.getWidth() / 2);
1495                         selectionHandle2.setLayoutY(b.getMaxY() - 1);
1496                     }
1497                 } else {
1498                     caretHandle.setLayoutX(b.getMinX() - caretHandle.getWidth() / 2 + 1);


   1 /*
   2  * Copyright (c) 2011, 2016, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 
  26 package javafx.scene.control.skin;
  27 
  28 import com.sun.javafx.scene.control.behavior.BehaviorBase;
  29 import com.sun.javafx.scene.control.behavior.TextAreaBehavior;
  30 import com.sun.javafx.scene.control.skin.Utils;

  31 import javafx.animation.KeyFrame;
  32 import javafx.animation.Timeline;
  33 import javafx.application.Platform;
  34 import javafx.beans.binding.BooleanBinding;
  35 import javafx.beans.binding.DoubleBinding;
  36 import javafx.beans.binding.IntegerBinding;
  37 import javafx.beans.value.ObservableBooleanValue;
  38 import javafx.beans.value.ObservableIntegerValue;
  39 import javafx.collections.ListChangeListener;
  40 import javafx.collections.ObservableList;
  41 import javafx.event.ActionEvent;
  42 import javafx.event.EventHandler;
  43 import javafx.geometry.Bounds;
  44 import javafx.geometry.Orientation;
  45 import javafx.geometry.Point2D;
  46 import javafx.geometry.Rectangle2D;
  47 import javafx.geometry.VPos;
  48 import javafx.geometry.VerticalDirection;
  49 import javafx.scene.AccessibleAttribute;
  50 import javafx.scene.Group;
  51 import javafx.scene.Node;
  52 import javafx.scene.control.Accordion;
  53 import javafx.scene.control.Button;
  54 import javafx.scene.control.Control;
  55 import javafx.scene.control.IndexRange;
  56 import javafx.scene.control.ScrollPane;
  57 import javafx.scene.control.TextArea;
  58 import javafx.scene.input.MouseEvent;
  59 import javafx.scene.input.ScrollEvent;
  60 import javafx.scene.layout.Region;
  61 import javafx.scene.shape.MoveTo;
  62 import javafx.scene.shape.Path;
  63 import javafx.scene.shape.PathElement;
  64 import javafx.scene.text.Text;
  65 import javafx.scene.text.HitInfo;
  66 import javafx.util.Duration;
  67 
  68 import java.util.List;
  69 
  70 import static com.sun.javafx.PlatformUtil.isMac;
  71 import static com.sun.javafx.PlatformUtil.isWindows;
  72 
  73 /**
  74  * Default skin implementation for the {@link TextArea} control.
  75  *
  76  * @see TextArea
  77  * @since 9
  78  */
  79 public class TextAreaSkin extends TextInputControlSkin<TextArea> {
  80 
  81     /**************************************************************************
  82      *
  83      * Static fields
  84      *
  85      **************************************************************************/


 382                 pressY = e.getY();
 383                 handlePressed = true;
 384                 e.consume();
 385             };
 386 
 387             EventHandler<MouseEvent> handleReleaseHandler = event -> {
 388                 handlePressed = false;
 389             };
 390 
 391             caretHandle.setOnMousePressed(handlePressHandler);
 392             selectionHandle1.setOnMousePressed(handlePressHandler);
 393             selectionHandle2.setOnMousePressed(handlePressHandler);
 394 
 395             caretHandle.setOnMouseReleased(handleReleaseHandler);
 396             selectionHandle1.setOnMouseReleased(handleReleaseHandler);
 397             selectionHandle2.setOnMouseReleased(handleReleaseHandler);
 398 
 399             caretHandle.setOnMouseDragged(e -> {
 400                 Text textNode = getTextNode();
 401                 Point2D tp = textNode.localToScene(0, 0);
 402                 Point2D p = new Point2D(e.getSceneX() - tp.getX() - pressX + caretHandle.getWidth() / 2,
 403                                         e.getSceneY() - tp.getY() - pressY - 6);
 404                 HitInfo hit = textNode.hitTest(translateCaretPosition(p));










 405                 positionCaret(hit, false);
 406                 e.consume();
 407             });
 408 
 409             selectionHandle1.setOnMouseDragged(e -> {
 410                 TextArea control1 = getSkinnable();
 411                 Text textNode = getTextNode();
 412                 Point2D tp = textNode.localToScene(0, 0);
 413                 Point2D p = new Point2D(e.getSceneX() - tp.getX() - pressX + selectionHandle1.getWidth() / 2,
 414                                         e.getSceneY() - tp.getY() - pressY + selectionHandle1.getHeight() + 5);
 415                 HitInfo hit = textNode.hitTest(translateCaretPosition(p));

 416                 if (control1.getAnchor() < control1.getCaretPosition()) {
 417                     // Swap caret and anchor
 418                     control1.selectRange(control1.getCaretPosition(), control1.getAnchor());
 419                 }
 420                 int pos = hit.getCharIndex();
 421                 if (pos > 0) {
 422                     if (pos >= control1.getAnchor()) {
 423                         pos = control1.getAnchor();
 424                     }







 425                 }
 426                 positionCaret(hit, true);
 427                 e.consume();
 428             });
 429 
 430             selectionHandle2.setOnMouseDragged(e -> {
 431                 TextArea control1 = getSkinnable();
 432                 Text textNode = getTextNode();
 433                 Point2D tp = textNode.localToScene(0, 0);
 434                 Point2D p = new Point2D(e.getSceneX() - tp.getX() - pressX + selectionHandle2.getWidth() / 2,
 435                                         e.getSceneY() - tp.getY() - pressY - 6);
 436                 HitInfo hit = textNode.hitTest(translateCaretPosition(p));

 437                 if (control1.getAnchor() > control1.getCaretPosition()) {
 438                     // Swap caret and anchor
 439                     control1.selectRange(control1.getCaretPosition(), control1.getAnchor());
 440                 }
 441                 int pos = hit.getCharIndex();
 442                 if (pos > 0) {
 443                     if (pos <= control1.getAnchor() + 1) {
 444                         pos = Math.min(control1.getAnchor() + 2, control1.getLength());
 445                     }







 446                     positionCaret(hit, true);
 447                 }
 448                 e.consume();
 449             });
 450         }
 451     }
 452 
 453 
 454 
 455     /***************************************************************************
 456      *                                                                         *
 457      * Public API                                                              *
 458      *                                                                         *
 459      **************************************************************************/
 460 
 461     /** {@inheritDoc} */
 462     @Override protected void invalidateMetrics() {
 463         computedMinWidth = Double.NEGATIVE_INFINITY;
 464         computedMinHeight = Double.NEGATIVE_INFINITY;
 465         computedPrefWidth = Double.NEGATIVE_INFINITY;


 468 
 469     /** {@inheritDoc} */
 470     @Override protected void layoutChildren(double contentX, double contentY, double contentWidth, double contentHeight) {
 471         scrollPane.resizeRelocate(contentX, contentY, contentWidth, contentHeight);
 472     }
 473 
 474     /** {@inheritDoc} */
 475     @Override protected void updateHighlightFill() {
 476         for (Node node : selectionHighlightGroup.getChildren()) {
 477             Path selectionHighlightPath = (Path)node;
 478             selectionHighlightPath.setFill(highlightFillProperty().get());
 479         }
 480     }
 481 
 482     // Public for behavior
 483     /**
 484      * Performs a hit test, mapping point to index in the content.
 485      *
 486      * @param x the x coordinate of the point.
 487      * @param y the y coordinate of the point.
 488      * @return a {@code HitInfo} object describing the index and forward bias.
 489      */
 490     public HitInfo getIndex(double x, double y) {
 491         // adjust the event to be in the same coordinate space as the
 492         // text content of the textInputControl
 493         Text textNode = getTextNode();
 494         Point2D p = new Point2D(x - textNode.getLayoutX(), y - getTextTranslateY());
 495         HitInfo hit = textNode.hitTest(translateCaretPosition(p));
 496         return hit;










 497     };
 498 
 499     /** {@inheritDoc} */
 500     @Override public void moveCaret(TextUnit unit, Direction dir, boolean select) {
 501         switch (unit) {
 502             case CHARACTER:
 503                 switch (dir) {
 504                     case LEFT:
 505                     case RIGHT:
 506                         nextCharacterVisually(dir == Direction.RIGHT);
 507                         break;
 508                     default:
 509                         throw new IllegalArgumentException(""+dir);
 510                 }
 511                 break;
 512 
 513             case LINE:
 514                 switch (dir) {
 515                     case UP:
 516                         previousLine(select);


 566         }
 567     }
 568 
 569     private void nextCharacterVisually(boolean moveRight) {
 570         if (isRTL()) {
 571             // Text node is mirrored.
 572             moveRight = !moveRight;
 573         }
 574 
 575         Text textNode = getTextNode();
 576         Bounds caretBounds = caretPath.getLayoutBounds();
 577         if (caretPath.getElements().size() == 4) {
 578             // The caret is split
 579             // TODO: Find a better way to get the primary caret position
 580             // instead of depending on the internal implementation.
 581             // See RT-25465.
 582             caretBounds = new Path(caretPath.getElements().get(0), caretPath.getElements().get(1)).getLayoutBounds();
 583         }
 584         double hitX = moveRight ? caretBounds.getMaxX() : caretBounds.getMinX();
 585         double hitY = (caretBounds.getMinY() + caretBounds.getMaxY()) / 2;
 586         HitInfo hit = textNode.hitTest(new Point2D(hitX, hitY));
 587         boolean leading = hit.isLeading();
 588         Path charShape = new Path(textNode.rangeShape(hit.getCharIndex(), hit.getCharIndex() + 1));
 589         if ((moveRight && charShape.getLayoutBounds().getMaxX() > caretBounds.getMaxX()) ||
 590                 (!moveRight && charShape.getLayoutBounds().getMinX() < caretBounds.getMinX())) {
 591             leading = !leading;
 592             positionCaret(hit.getInsertionIndex(), leading, false, false);
 593         } else {
 594             // We're at beginning or end of line. Try moving up / down.
 595             int dot = textArea.getCaretPosition();
 596             targetCaretX = moveRight ? 0 : Double.MAX_VALUE;
 597             // TODO: Use Bidi sniffing instead of assuming right means forward here?
 598             downLines(moveRight ? 1 : -1, false, false);
 599             targetCaretX = -1;
 600             if (dot == textArea.getCaretPosition()) {
 601                 if (moveRight) {
 602                     textArea.forward();
 603                 } else {
 604                     textArea.backward();
 605                 }
 606             }
 607         }
 608     }
 609 
 610     private void downLines(int nLines, boolean select, boolean extendSelection) {
 611         Text textNode = getTextNode();
 612         Bounds caretBounds = caretPath.getLayoutBounds();
 613 
 614         // The middle y coordinate of the the line we want to go to.
 615         double targetLineMidY = (caretBounds.getMinY() + caretBounds.getMaxY()) / 2 + nLines * lineHeight;
 616         if (targetLineMidY < 0) {
 617             targetLineMidY = 0;
 618         }
 619 
 620         // The target x for the caret. This may have been set during a
 621         // previous call.
 622         double x = (targetCaretX >= 0) ? targetCaretX : (caretBounds.getMaxX());
 623 
 624         // Find a text position for the target x,y.
 625         HitInfo hit = textNode.hitTest(translateCaretPosition(new Point2D(x, targetLineMidY)));
 626         int pos = hit.getCharIndex();
 627 
 628         // Save the old pos temporarily while testing the new one.
 629         int oldPos = textNode.getCaretPosition();
 630         boolean oldBias = textNode.isCaretBias();
 631         textNode.setCaretBias(hit.isLeading());
 632         textNode.setCaretPosition(pos);
 633         tmpCaretPath.getElements().clear();
 634         tmpCaretPath.getElements().addAll(textNode.getCaretShape());
 635         tmpCaretPath.setLayoutX(textNode.getLayoutX());
 636         tmpCaretPath.setLayoutY(textNode.getLayoutY());
 637         Bounds tmpCaretBounds = tmpCaretPath.getLayoutBounds();
 638         // The y for the middle of the row we found.
 639         double foundLineMidY = (tmpCaretBounds.getMinY() + tmpCaretBounds.getMaxY()) / 2;
 640         textNode.setCaretBias(oldBias);
 641         textNode.setCaretPosition(oldPos);












 642 
 643         // Test if the found line is in the correct direction and move
 644         // the caret.
 645         if (nLines == 0 ||
 646                 (nLines > 0 && foundLineMidY > caretBounds.getMaxY()) ||
 647                 (nLines < 0 && foundLineMidY < caretBounds.getMinY())) {
 648 
 649             positionCaret(hit.getInsertionIndex(), hit.isLeading(), select, extendSelection);
 650             targetCaretX = x;
 651         }
 652     }
 653 
 654     private void previousLine(boolean select) {
 655         downLines(-1, select, false);
 656     }
 657 
 658     private void nextLine(boolean select) {
 659         downLines(1, select, false);
 660     }
 661 
 662     private void previousPage(boolean select) {
 663         downLines(-(int)(scrollPane.getViewportBounds().getHeight() / lineHeight),
 664                 select, false);
 665     }
 666 
 667     private void nextPage(boolean select) {
 668         downLines((int)(scrollPane.getViewportBounds().getHeight() / lineHeight),
 669                 select, false);


 730                     // We are at the end of a paragraph, finish by moving to
 731                     // the beginning of the next paragraph (Windows behavior).
 732                     pos++;
 733                 }
 734             }
 735             if (select) {
 736                 textArea.selectPositionCaret(pos);
 737             } else {
 738                 textArea.positionCaret(pos);
 739             }
 740         }
 741     }
 742 
 743     /** {@inheritDoc} */
 744     @Override protected PathElement[] getUnderlineShape(int start, int end) {
 745         int pStart = 0;
 746         for (Node node : paragraphNodes.getChildren()) {
 747             Text p = (Text)node;
 748             int pEnd = pStart + p.textProperty().getValueSafe().length();
 749             if (pEnd >= start) {
 750                 return p.underlineShape(start - pStart, end - pStart);
 751             }
 752             pStart = pEnd + 1;
 753         }
 754         return null;
 755     }
 756 
 757     /** {@inheritDoc} */
 758     @Override protected PathElement[] getRangeShape(int start, int end) {
 759         int pStart = 0;
 760         for (Node node : paragraphNodes.getChildren()) {
 761             Text p = (Text)node;
 762             int pEnd = pStart + p.textProperty().getValueSafe().length();
 763             if (pEnd >= start) {
 764                 return p.rangeShape(start - pStart, end - pStart);
 765             }
 766             pStart = pEnd + 1;
 767         }
 768         return null;
 769     }
 770 
 771     /** {@inheritDoc} */
 772     @Override protected void addHighlight(List<? extends Node> nodes, int start) {
 773         int pStart = 0;
 774         Text paragraphNode = null;
 775         for (Node node : paragraphNodes.getChildren()) {
 776             Text p = (Text)node;
 777             int pEnd = pStart + p.textProperty().getValueSafe().length();
 778             if (pEnd >= start) {
 779                 paragraphNode = p;
 780                 break;
 781             }
 782             pStart = pEnd + 1;
 783         }
 784 


 904                                 x - paragraphNode.getLayoutX(),
 905                                 y - paragraphNode.getLayoutY()) + paragraphOffset;
 906                         break;
 907                     }
 908 
 909                     paragraphOffset += paragraphNode.getText().length() + 1;
 910                 }
 911             }
 912         }
 913 
 914         return index;
 915     }
 916 
 917     // Public for behavior
 918     /**
 919      * Moves the caret to the specified position.
 920      *
 921      * @param hit the new position and forward bias of the caret.
 922      * @param select whether to extend selection to the new position.
 923      */
 924     public void positionCaret(HitInfo hit, boolean select) {
 925         positionCaret(hit.getInsertionIndex(), hit.isLeading(), select, false);
 926     }
 927 
 928     private void positionCaret(int pos, boolean leading, boolean select, boolean extendSelection) {

 929         boolean isNewLine =
 930                 (pos > 0 &&
 931                         pos <= getSkinnable().getLength() &&
 932                         getSkinnable().getText().codePointAt(pos-1) == 0x0a);
 933 
 934         // special handling for a new line
 935         if (!leading && isNewLine) {
 936             leading = true;
 937             pos -= 1;
 938         }
 939 
 940         if (select) {
 941             if (extendSelection) {
 942                 getSkinnable().extendSelection(pos);
 943             } else {
 944                 getSkinnable().selectPositionCaret(pos);
 945             }
 946         } else {
 947             getSkinnable().positionCaret(pos);
 948         }
 949 
 950         setForwardBias(leading);








 951     }
 952 
 953     /** {@inheritDoc} */
 954     @Override public Rectangle2D getCharacterBounds(int index) {
 955         TextArea textArea = getSkinnable();
 956 
 957         int paragraphIndex = paragraphNodes.getChildren().size();
 958         int paragraphOffset = textArea.getLength() + 1;
 959 
 960         Text paragraphNode = null;
 961         do {
 962             paragraphNode = (Text)paragraphNodes.getChildren().get(--paragraphIndex);
 963             paragraphOffset -= paragraphNode.getText().length() + 1;
 964         } while (index < paragraphOffset);
 965 
 966         int characterIndex = index - paragraphOffset;
 967         boolean terminator = false;
 968 
 969         if (characterIndex == paragraphNode.getText().length()) {
 970             characterIndex--;
 971             terminator = true;
 972         }
 973 
 974         characterBoundingPath.getElements().clear();
 975         characterBoundingPath.getElements().addAll(paragraphNode.rangeShape(characterIndex, characterIndex + 1));
 976         characterBoundingPath.setLayoutX(paragraphNode.getLayoutX());
 977         characterBoundingPath.setLayoutY(paragraphNode.getLayoutY());
 978 
 979         Bounds bounds = characterBoundingPath.getBoundsInLocal();
 980 
 981         double x = bounds.getMinX() + paragraphNode.getLayoutX() - textArea.getScrollLeft();
 982         double y = bounds.getMinY() + paragraphNode.getLayoutY() - textArea.getScrollTop();
 983 
 984         // Sometimes the bounds is empty, in which case we must ignore the width/height
 985         double width = bounds.isEmpty() ? 0 : bounds.getWidth();
 986         double height = bounds.isEmpty() ? 0 : bounds.getHeight();
 987 
 988         if (terminator) {
 989             x += width;
 990             width = 0;
 991         }
 992 
 993         return new Rectangle2D(x, y, width, height);
 994     }
 995 


1030             promptNode.fontProperty().bind(getSkinnable().fontProperty());
1031             promptNode.textProperty().bind(getSkinnable().promptTextProperty());
1032             promptNode.fillProperty().bind(promptTextFillProperty());
1033         }
1034     }
1035 
1036     private void addParagraphNode(int i, String string) {
1037         final TextArea textArea = getSkinnable();
1038         Text paragraphNode = new Text(string);
1039         paragraphNode.setTextOrigin(VPos.TOP);
1040         paragraphNode.setManaged(false);
1041         paragraphNode.getStyleClass().add("text");
1042         paragraphNode.boundsTypeProperty().addListener((observable, oldValue, newValue) -> {
1043             invalidateMetrics();
1044             updateFontMetrics();
1045         });
1046         paragraphNodes.getChildren().add(i, paragraphNode);
1047 
1048         paragraphNode.fontProperty().bind(textArea.fontProperty());
1049         paragraphNode.fillProperty().bind(textFillProperty());
1050         paragraphNode.selectionFillProperty().bind(highlightTextFillProperty());
1051     }
1052 
1053     private double getScrollTopMax() {
1054         return Math.max(0, contentView.getHeight() - scrollPane.getViewportBounds().getHeight());
1055     }
1056 
1057     private double getScrollLeftMax() {
1058         return Math.max(0, contentView.getWidth() - scrollPane.getViewportBounds().getWidth());
1059     }
1060 
1061     private int getInsertionPoint(Text paragraphNode, double x, double y) {
1062         HitInfo hitInfo = paragraphNode.hitTest(new Point2D(x, y));
1063         return hitInfo.getInsertionIndex();
1064     }
1065 
1066     private int getNextInsertionPoint(Text paragraphNode, double x, int from,
1067         VerticalDirection scrollDirection) {
1068         // TODO
1069         return 0;
1070     }
1071 
1072     private void scrollCaretToVisible() {
1073         TextArea textArea = getSkinnable();
1074         Bounds bounds = caretPath.getLayoutBounds();
1075         double x = bounds.getMinX() - textArea.getScrollLeft();
1076         double y = bounds.getMinY() - textArea.getScrollTop();
1077         double w = bounds.getWidth();
1078         double h = bounds.getHeight();
1079 
1080         if (SHOW_HANDLES) {
1081             if (caretHandle.isVisible()) {
1082                 h += caretHandle.getHeight();
1083             } else if (selectionHandle1.isVisible() && selectionHandle2.isVisible()) {


1160     }
1161 
1162     private double getTextLeft() {
1163         return 0;
1164     }
1165 
1166     private Point2D translateCaretPosition(Point2D p) {
1167         return p;
1168     }
1169 
1170     private Text getTextNode() {
1171         if (USE_MULTIPLE_NODES) {
1172             throw new IllegalArgumentException("Multiple node traversal is not yet implemented.");
1173         }
1174         return (Text)paragraphNodes.getChildren().get(0);
1175     }
1176 
1177     private void updateTextNodeCaretPos(int pos) {
1178         Text textNode = getTextNode();
1179         if (isForwardBias()) {
1180             textNode.setCaretPosition(pos);
1181         } else {
1182             textNode.setCaretPosition(pos - 1);
1183         }
1184         textNode.caretBiasProperty().set(isForwardBias());
1185     }
1186 
1187 
1188 
1189     /**************************************************************************
1190      *
1191      * Support classes
1192      *
1193      **************************************************************************/
1194 
1195     private class ContentView extends Region {
1196         {
1197             getStyleClass().add("content");
1198 
1199             addEventHandler(MouseEvent.MOUSE_PRESSED, event -> {
1200                 behavior.mousePressed(event);
1201                 event.consume();
1202             });
1203 
1204             addEventHandler(MouseEvent.MOUSE_RELEASED, event -> {


1337                     selectionHandle2.resize(selectionHandle2.prefWidth(-1),
1338                             selectionHandle2.prefHeight(-1));
1339                 } else {
1340                     caretHandle.resize(caretHandle.prefWidth(-1),
1341                             caretHandle.prefHeight(-1));
1342                 }
1343 
1344                 // Position the handle for the anchor. This could be handle1 or handle2.
1345                 // Do this before positioning the actual caret.
1346                 if (selection.getLength() > 0) {
1347                     int paragraphIndex = paragraphNodesChildren.size();
1348                     int paragraphOffset = textArea.getLength() + 1;
1349                     Text paragraphNode = null;
1350                     do {
1351                         paragraphNode = (Text)paragraphNodesChildren.get(--paragraphIndex);
1352                         paragraphOffset -= paragraphNode.getText().length() + 1;
1353                     } while (anchorPos < paragraphOffset);
1354 
1355                     updateTextNodeCaretPos(anchorPos - paragraphOffset);
1356                     caretPath.getElements().clear();
1357                     caretPath.getElements().addAll(paragraphNode.getCaretShape());
1358                     caretPath.setLayoutX(paragraphNode.getLayoutX());
1359                     caretPath.setLayoutY(paragraphNode.getLayoutY());
1360 
1361                     Bounds b = caretPath.getBoundsInParent();
1362                     if (caretPos < anchorPos) {
1363                         selectionHandle2.setLayoutX(b.getMinX() - selectionHandle2.getWidth() / 2);
1364                         selectionHandle2.setLayoutY(b.getMaxY() - 1);
1365                     } else {
1366                         selectionHandle1.setLayoutX(b.getMinX() - selectionHandle1.getWidth() / 2);
1367                         selectionHandle1.setLayoutY(b.getMinY() - selectionHandle1.getHeight() + 1);
1368                     }
1369                 }
1370             }
1371 
1372             {
1373                 // Position caret
1374                 int paragraphIndex = paragraphNodesChildren.size();
1375                 int paragraphOffset = textArea.getLength() + 1;
1376 
1377                 Text paragraphNode = null;
1378                 do {
1379                     paragraphNode = (Text)paragraphNodesChildren.get(--paragraphIndex);
1380                     paragraphOffset -= paragraphNode.getText().length() + 1;
1381                 } while (caretPos < paragraphOffset);
1382 
1383                 updateTextNodeCaretPos(caretPos - paragraphOffset);
1384 
1385                 caretPath.getElements().clear();
1386                 caretPath.getElements().addAll(paragraphNode.getCaretShape());
1387 
1388                 caretPath.setLayoutX(paragraphNode.getLayoutX());
1389 
1390                 // TODO: Remove this temporary workaround for RT-27533
1391                 paragraphNode.setLayoutX(2 * paragraphNode.getLayoutX() - paragraphNode.getBoundsInParent().getMinX());
1392 
1393                 caretPath.setLayoutY(paragraphNode.getLayoutY());
1394                 if (oldCaretBounds == null || !oldCaretBounds.equals(caretPath.getBoundsInParent())) {
1395                     scrollCaretToVisible();
1396                 }
1397             }
1398 
1399             // Update selection fg and bg
1400             int start = selection.getStart();
1401             int end = selection.getEnd();
1402             for (int i = 0, max = paragraphNodesChildren.size(); i < max; i++) {
1403                 Node paragraphNode = paragraphNodesChildren.get(i);
1404                 Text textNode = (Text)paragraphNode;
1405                 int paragraphLength = textNode.getText().length() + 1;
1406                 if (end > start && start < paragraphLength) {
1407                     textNode.setSelectionStart(start);
1408                     textNode.setSelectionEnd(Math.min(end, paragraphLength));
1409 
1410                     Path selectionHighlightPath = new Path();
1411                     selectionHighlightPath.setManaged(false);
1412                     selectionHighlightPath.setStroke(null);
1413                     PathElement[] selectionShape = textNode.getSelectionShape();
1414                     if (selectionShape != null) {
1415                         selectionHighlightPath.getElements().addAll(selectionShape);
1416                     }
1417                     selectionHighlightGroup.getChildren().add(selectionHighlightPath);
1418                     selectionHighlightGroup.setVisible(true);
1419                     selectionHighlightPath.setLayoutX(textNode.getLayoutX());
1420                     selectionHighlightPath.setLayoutY(textNode.getLayoutY());
1421                     updateHighlightFill();
1422                 } else {
1423                     textNode.setSelectionStart(-1);
1424                     textNode.setSelectionEnd(-1);
1425                     selectionHighlightGroup.setVisible(false);
1426                 }
1427                 start = Math.max(0, start - paragraphLength);
1428                 end   = Math.max(0, end   - paragraphLength);
1429             }
1430 
1431             if (SHOW_HANDLES) {
1432                 // Position handle for the caret. This could be handle1 or handle2 when
1433                 // a selection is active.
1434                 Bounds b = caretPath.getBoundsInParent();
1435                 if (selection.getLength() > 0) {
1436                     if (caretPos < anchorPos) {
1437                         selectionHandle1.setLayoutX(b.getMinX() - selectionHandle1.getWidth() / 2);
1438                         selectionHandle1.setLayoutY(b.getMinY() - selectionHandle1.getHeight() + 1);
1439                     } else {
1440                         selectionHandle2.setLayoutX(b.getMinX() - selectionHandle2.getWidth() / 2);
1441                         selectionHandle2.setLayoutY(b.getMaxY() - 1);
1442                     }
1443                 } else {
1444                     caretHandle.setLayoutX(b.getMinX() - caretHandle.getWidth() / 2 + 1);


< prev index next >