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