1 /* 2 * Copyright (c) 2011, 2017, 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 **************************************************************************/ 86 87 /** A shared helper object, used only by downLines(). */ 88 private static final Path tmpCaretPath = new Path(); 89 90 91 92 /************************************************************************** 93 * 94 * Private fields 95 * 96 **************************************************************************/ 97 98 final private TextArea textArea; 99 100 // *** NOTE: Multiple node mode is not yet fully implemented *** // 101 private static final boolean USE_MULTIPLE_NODES = false; 102 103 private final TextAreaBehavior behavior; 104 105 private double computedMinWidth = Double.NEGATIVE_INFINITY; 106 private double computedMinHeight = Double.NEGATIVE_INFINITY; 107 private double computedPrefWidth = Double.NEGATIVE_INFINITY; 108 private double computedPrefHeight = Double.NEGATIVE_INFINITY; 109 private double widthForComputedPrefHeight = Double.NEGATIVE_INFINITY; 110 private double characterWidth; 111 private double lineHeight; 112 113 private ContentView contentView = new ContentView(); 114 private Group paragraphNodes = new Group(); 115 116 private Text promptNode; 117 private ObservableBooleanValue usePromptText; 118 119 private ObservableIntegerValue caretPosition; 120 private Group selectionHighlightGroup = new Group(); 121 122 private ScrollPane scrollPane; 123 private Bounds oldViewportBounds; 124 125 private VerticalDirection scrollDirection = null; 126 127 private Path characterBoundingPath = new Path(); 128 129 private Timeline scrollSelectionTimeline = new Timeline(); 130 private EventHandler<ActionEvent> scrollSelectionHandler = event -> { 131 switch (scrollDirection) { 132 case UP: { 133 // TODO Get previous offset 134 break; 135 } 136 137 case DOWN: { 138 // TODO Get next offset 139 break; 140 } 141 } 142 }; 143 144 private double pressX, pressY; // For dragging handles on embedded 145 private boolean handlePressed; 146 147 /** 148 * Remembers horizontal position when traversing up / down. 149 */ 150 double targetCaretX = -1; 151 152 153 154 /************************************************************************** 155 * 156 * Constructors 157 * 158 **************************************************************************/ 159 160 /** 161 * Creates a new TextAreaSkin instance, installing the necessary child 162 * nodes into the Control {@link Control#getChildren() children} list, as 163 * well as the necessary input mappings for handling key, mouse, etc events. 164 * 165 * @param control The control that this skin should be installed onto. 166 */ 167 public TextAreaSkin(final TextArea control) { 168 super(control); 169 170 // install default input map for the text area control 171 this.behavior = new TextAreaBehavior(control); 172 this.behavior.setTextAreaSkin(this); 173 // control.setInputMap(behavior.getInputMap()); 174 175 this.textArea = control; 176 177 caretPosition = new IntegerBinding() { 178 { bind(control.caretPositionProperty()); } 179 @Override protected int computeValue() { 180 return control.getCaretPosition(); 181 } 182 }; 183 caretPosition.addListener((observable, oldValue, newValue) -> { 184 targetCaretX = -1; 185 if (control.getWidth() > 0) { 186 setForwardBias(true); 187 } 188 }); 189 190 forwardBiasProperty().addListener(observable -> { 191 if (control.getWidth() > 0) { 192 updateTextNodeCaretPos(control.getCaretPosition()); 193 } 194 }); 195 196 // setManaged(false); 197 198 // Initialize content 199 scrollPane = new ScrollPane(); 200 scrollPane.setFitToWidth(control.isWrapText()); 201 scrollPane.setContent(contentView); 202 getChildren().add(scrollPane); 203 204 getSkinnable().addEventFilter(ScrollEvent.ANY, event -> { 205 if (event.isDirect() && handlePressed) { 206 event.consume(); 207 } 208 }); 209 210 // Add selection 211 selectionHighlightGroup.setManaged(false); 212 selectionHighlightGroup.setVisible(false); 213 contentView.getChildren().add(selectionHighlightGroup); 214 215 // Add content view 216 paragraphNodes.setManaged(false); 217 contentView.getChildren().add(paragraphNodes); 218 219 // Add caret 220 caretPath.setManaged(false); 221 caretPath.setStrokeWidth(1); 222 caretPath.fillProperty().bind(textFillProperty()); 223 caretPath.strokeProperty().bind(textFillProperty()); 224 // modifying visibility of the caret forces a layout-pass (RT-32373), so 225 // instead we modify the opacity. 226 caretPath.opacityProperty().bind(new DoubleBinding() { 227 { bind(caretVisibleProperty()); } 228 @Override protected double computeValue() { 229 return caretVisibleProperty().get() ? 1.0 : 0.0; 230 } 231 }); 232 contentView.getChildren().add(caretPath); 233 234 if (SHOW_HANDLES) { 235 contentView.getChildren().addAll(caretHandle, selectionHandle1, selectionHandle2); 236 } 237 238 scrollPane.hvalueProperty().addListener((observable, oldValue, newValue) -> { 239 getSkinnable().setScrollLeft(newValue.doubleValue() * getScrollLeftMax()); 240 }); 241 242 scrollPane.vvalueProperty().addListener((observable, oldValue, newValue) -> { 243 getSkinnable().setScrollTop(newValue.doubleValue() * getScrollTopMax()); 244 }); 245 246 // Initialize the scroll selection timeline 247 scrollSelectionTimeline.setCycleCount(Timeline.INDEFINITE); 248 List<KeyFrame> scrollSelectionFrames = scrollSelectionTimeline.getKeyFrames(); 249 scrollSelectionFrames.clear(); 250 scrollSelectionFrames.add(new KeyFrame(Duration.millis(350), scrollSelectionHandler)); 251 252 // Add initial text content 253 for (int i = 0, n = USE_MULTIPLE_NODES ? control.getParagraphs().size() : 1; i < n; i++) { 254 CharSequence paragraph = (n == 1) ? control.textProperty().getValueSafe() : control.getParagraphs().get(i); 255 addParagraphNode(i, paragraph.toString()); 256 } 257 258 control.selectionProperty().addListener((observable, oldValue, newValue) -> { 259 // TODO Why do we need two calls here? 260 control.requestLayout(); 261 contentView.requestLayout(); 262 }); 263 264 control.wrapTextProperty().addListener((observable, oldValue, newValue) -> { 265 invalidateMetrics(); 266 scrollPane.setFitToWidth(newValue); 267 }); 268 269 control.prefColumnCountProperty().addListener((observable, oldValue, newValue) -> { 270 invalidateMetrics(); 271 updatePrefViewportWidth(); 272 }); 273 274 control.prefRowCountProperty().addListener((observable, oldValue, newValue) -> { 275 invalidateMetrics(); 276 updatePrefViewportHeight(); 277 }); 278 279 updateFontMetrics(); 280 fontMetrics.addListener(valueModel -> { 281 updateFontMetrics(); 282 }); 283 284 contentView.paddingProperty().addListener(valueModel -> { 285 updatePrefViewportWidth(); 286 updatePrefViewportHeight(); 287 }); 288 289 scrollPane.viewportBoundsProperty().addListener(valueModel -> { 290 if (scrollPane.getViewportBounds() != null) { 291 // ScrollPane creates a new Bounds instance for each 292 // layout pass, so we need to check if the width/height 293 // have really changed to avoid infinite layout requests. 294 Bounds newViewportBounds = scrollPane.getViewportBounds(); 295 if (oldViewportBounds == null || 296 oldViewportBounds.getWidth() != newViewportBounds.getWidth() || 297 oldViewportBounds.getHeight() != newViewportBounds.getHeight()) { 298 299 invalidateMetrics(); 300 oldViewportBounds = newViewportBounds; 301 contentView.requestLayout(); 302 } 303 } 304 }); 305 306 control.scrollTopProperty().addListener((observable, oldValue, newValue) -> { 307 double vValue = (newValue.doubleValue() < getScrollTopMax()) 308 ? (newValue.doubleValue() / getScrollTopMax()) : 1.0; 309 scrollPane.setVvalue(vValue); 310 }); 311 312 control.scrollLeftProperty().addListener((observable, oldValue, newValue) -> { 313 double hValue = (newValue.doubleValue() < getScrollLeftMax()) 314 ? (newValue.doubleValue() / getScrollLeftMax()) : 1.0; 315 scrollPane.setHvalue(hValue); 316 }); 317 318 if (USE_MULTIPLE_NODES) { 319 control.getParagraphs().addListener((ListChangeListener.Change<? extends CharSequence> change) -> { 320 while (change.next()) { 321 int from = change.getFrom(); 322 int to = change.getTo(); 323 List<? extends CharSequence> removed = change.getRemoved(); 324 if (from < to) { 325 326 if (removed.isEmpty()) { 327 // This is an add 328 for (int i = from, n = to; i < n; i++) { 329 addParagraphNode(i, change.getList().get(i).toString()); 330 } 331 } else { 332 // This is an update 333 for (int i = from, n = to; i < n; i++) { 334 Node node = paragraphNodes.getChildren().get(i); 335 Text paragraphNode = (Text) node; 336 paragraphNode.setText(change.getList().get(i).toString()); 337 } 338 } 339 } else { 340 // This is a remove 341 paragraphNodes.getChildren().subList(from, from + removed.size()).clear(); 342 } 343 } 344 }); 345 } else { 346 control.textProperty().addListener(observable -> { 347 invalidateMetrics(); 348 ((Text)paragraphNodes.getChildren().get(0)).setText(control.textProperty().getValueSafe()); 349 contentView.requestLayout(); 350 }); 351 } 352 353 usePromptText = new BooleanBinding() { 354 { bind(control.textProperty(), control.promptTextProperty()); } 355 @Override protected boolean computeValue() { 356 String txt = control.getText(); 357 String promptTxt = control.getPromptText(); 358 return ((txt == null || txt.isEmpty()) && 359 promptTxt != null && !promptTxt.isEmpty()); 360 } 361 }; 362 363 if (usePromptText.get()) { 364 createPromptNode(); 365 } 366 367 usePromptText.addListener(observable -> { 368 createPromptNode(); 369 control.requestLayout(); 370 }); 371 372 updateHighlightFill(); 373 updatePrefViewportWidth(); 374 updatePrefViewportHeight(); 375 if (control.isFocused()) setCaretAnimating(true); 376 377 if (SHOW_HANDLES) { 378 selectionHandle1.setRotate(180); 379 380 EventHandler<MouseEvent> handlePressHandler = e -> { 381 pressX = e.getX(); 382 pressY = e.getY(); 383 handlePressed = true; 384 e.consume(); 385 }; 386 387 EventHandler<MouseEvent> handleReleaseHandler = event -> { 388 handlePressed = false; 389 }; 390 391 caretHandle.setOnMousePressed(handlePressHandler); 392 selectionHandle1.setOnMousePressed(handlePressHandler); 393 selectionHandle2.setOnMousePressed(handlePressHandler); 394 395 caretHandle.setOnMouseReleased(handleReleaseHandler); 396 selectionHandle1.setOnMouseReleased(handleReleaseHandler); 397 selectionHandle2.setOnMouseReleased(handleReleaseHandler); 398 399 caretHandle.setOnMouseDragged(e -> { 400 Text textNode = getTextNode(); 401 Point2D tp = textNode.localToScene(0, 0); 402 Point2D p = new Point2D(e.getSceneX() - tp.getX() - 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; 466 computedPrefHeight = Double.NEGATIVE_INFINITY; 467 } 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); 517 break; 518 case DOWN: 519 nextLine(select); 520 break; 521 case BEGINNING: 522 lineStart(select, select && isMac()); 523 break; 524 case END: 525 lineEnd(select, select && isMac()); 526 break; 527 default: 528 throw new IllegalArgumentException(""+dir); 529 } 530 break; 531 532 case PAGE: 533 switch (dir) { 534 case UP: 535 previousPage(select); 536 break; 537 case DOWN: 538 nextPage(select); 539 break; 540 default: 541 throw new IllegalArgumentException(""+dir); 542 } 543 break; 544 545 case PARAGRAPH: 546 switch (dir) { 547 case UP: 548 paragraphStart(true, select); 549 break; 550 case DOWN: 551 paragraphEnd(true, select); 552 break; 553 case BEGINNING: 554 paragraphStart(false, select); 555 break; 556 case END: 557 paragraphEnd(false, select); 558 break; 559 default: 560 throw new IllegalArgumentException(""+dir); 561 } 562 break; 563 564 default: 565 throw new IllegalArgumentException(""+unit); 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); 670 } 671 672 private void lineStart(boolean select, boolean extendSelection) { 673 targetCaretX = 0; 674 downLines(0, select, extendSelection); 675 targetCaretX = -1; 676 } 677 678 private void lineEnd(boolean select, boolean extendSelection) { 679 targetCaretX = Double.MAX_VALUE; 680 downLines(0, select, extendSelection); 681 targetCaretX = -1; 682 } 683 684 685 private void paragraphStart(boolean previousIfAtStart, boolean select) { 686 TextArea textArea = getSkinnable(); 687 String text = textArea.textProperty().getValueSafe(); 688 int pos = textArea.getCaretPosition(); 689 690 if (pos > 0) { 691 if (previousIfAtStart && text.codePointAt(pos-1) == 0x0a) { 692 // We are at the beginning of a paragraph. 693 // Back up to the previous paragraph. 694 pos--; 695 } 696 // Back up to the beginning of this paragraph 697 while (pos > 0 && text.codePointAt(pos-1) != 0x0a) { 698 pos--; 699 } 700 if (select) { 701 textArea.selectPositionCaret(pos); 702 } else { 703 textArea.positionCaret(pos); 704 setForwardBias(true); 705 } 706 } 707 } 708 709 private void paragraphEnd(boolean goPastInitialNewline, boolean select) { 710 TextArea textArea = getSkinnable(); 711 String text = textArea.textProperty().getValueSafe(); 712 int pos = textArea.getCaretPosition(); 713 int len = text.length(); 714 boolean wentPastInitialNewline = false; 715 boolean goPastTrailingNewline = isWindows(); 716 717 if (pos < len) { 718 if (goPastInitialNewline && text.codePointAt(pos) == 0x0a) { 719 // We are at the end of a paragraph, start by moving to the 720 // next paragraph. 721 pos++; 722 wentPastInitialNewline = true; 723 } 724 if (!(goPastTrailingNewline && wentPastInitialNewline)) { 725 // Go to the end of this paragraph 726 while (pos < len && text.codePointAt(pos) != 0x0a) { 727 pos++; 728 } 729 if (goPastTrailingNewline && pos < len) { 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 785 if (paragraphNode != null) { 786 for (Node node : nodes) { 787 node.setLayoutX(paragraphNode.getLayoutX()); 788 node.setLayoutY(paragraphNode.getLayoutY()); 789 } 790 } 791 contentView.getChildren().addAll(nodes); 792 } 793 794 /** {@inheritDoc} */ 795 @Override protected void removeHighlight(List<? extends Node> nodes) { 796 contentView.getChildren().removeAll(nodes); 797 } 798 799 /** {@inheritDoc} */ 800 @Override public Point2D getMenuPosition() { 801 contentView.layoutChildren(); 802 Point2D p = super.getMenuPosition(); 803 if (p != null) { 804 p = new Point2D(Math.max(0, p.getX() - contentView.snappedLeftInset() - getSkinnable().getScrollLeft()), 805 Math.max(0, p.getY() - contentView.snappedTopInset() - getSkinnable().getScrollTop())); 806 } 807 return p; 808 } 809 810 // Public for FXVKSkin 811 /** 812 * Gets the {@code Bounds} of the caret of the skinned {@code TextArea}. 813 * @return the {@code Bounds} of the caret shape, relative to the {@code TextArea}. 814 */ 815 public Bounds getCaretBounds() { 816 return getSkinnable().sceneToLocal(caretPath.localToScene(caretPath.getBoundsInLocal())); 817 } 818 819 /** {@inheritDoc} */ 820 @Override protected Object queryAccessibleAttribute(AccessibleAttribute attribute, Object... parameters) { 821 switch (attribute) { 822 case LINE_FOR_OFFSET: 823 case LINE_START: 824 case LINE_END: 825 case BOUNDS_FOR_RANGE: 826 case OFFSET_AT_POINT: 827 Text text = getTextNode(); 828 return text.queryAccessibleAttribute(attribute, parameters); 829 default: return super.queryAccessibleAttribute(attribute, parameters); 830 } 831 } 832 833 /** {@inheritDoc} */ 834 @Override public void dispose() { 835 super.dispose(); 836 837 if (behavior != null) { 838 behavior.dispose(); 839 } 840 841 // TODO Unregister listeners on text editor, paragraph list 842 throw new UnsupportedOperationException(); 843 } 844 845 /** {@inheritDoc} */ 846 @Override public double computeBaselineOffset(double topInset, double rightInset, double bottomInset, double leftInset) { 847 Text firstParagraph = (Text) paragraphNodes.getChildren().get(0); 848 return Utils.getAscent(getSkinnable().getFont(), firstParagraph.getBoundsType()) 849 + contentView.snappedTopInset() + textArea.snappedTopInset(); 850 } 851 852 private char getCharacter(int index) { 853 int n = paragraphNodes.getChildren().size(); 854 855 int paragraphIndex = 0; 856 int offset = index; 857 858 String paragraph = null; 859 while (paragraphIndex < n) { 860 Text paragraphNode = (Text)paragraphNodes.getChildren().get(paragraphIndex); 861 paragraph = paragraphNode.getText(); 862 int count = paragraph.length() + 1; 863 864 if (offset < count) { 865 break; 866 } 867 868 offset -= count; 869 paragraphIndex++; 870 } 871 872 return offset == paragraph.length() ? '\n' : paragraph.charAt(offset); 873 } 874 875 /** {@inheritDoc} */ 876 @Override protected int getInsertionPoint(double x, double y) { 877 TextArea textArea = getSkinnable(); 878 879 int n = paragraphNodes.getChildren().size(); 880 int index = -1; 881 882 if (n > 0) { 883 if (y < contentView.snappedTopInset()) { 884 // Select the character at x in the first row 885 Text paragraphNode = (Text)paragraphNodes.getChildren().get(0); 886 index = getNextInsertionPoint(paragraphNode, x, -1, VerticalDirection.DOWN); 887 } else if (y > contentView.snappedTopInset() + contentView.getHeight()) { 888 // Select the character at x in the last row 889 int lastParagraphIndex = n - 1; 890 Text lastParagraphView = (Text)paragraphNodes.getChildren().get(lastParagraphIndex); 891 892 index = getNextInsertionPoint(lastParagraphView, x, -1, VerticalDirection.UP) 893 + (textArea.getLength() - lastParagraphView.getText().length()); 894 } else { 895 // Select the character at x in the row at y 896 int paragraphOffset = 0; 897 for (int i = 0; i < n; i++) { 898 Text paragraphNode = (Text)paragraphNodes.getChildren().get(i); 899 900 Bounds bounds = paragraphNode.getBoundsInLocal(); 901 double paragraphViewY = paragraphNode.getLayoutY() + bounds.getMinY(); 902 if (y >= paragraphViewY 903 && y < paragraphViewY + paragraphNode.getBoundsInLocal().getHeight()) { 904 index = getInsertionPoint(paragraphNode, 905 x - paragraphNode.getLayoutX(), 906 y - paragraphNode.getLayoutY()) + paragraphOffset; 907 break; 908 } 909 910 paragraphOffset += paragraphNode.getText().length() + 1; 911 } 912 } 913 } 914 915 return index; 916 } 917 918 // Public for behavior 919 /** 920 * Moves the caret to the specified position. 921 * 922 * @param hit the new position and forward bias of the caret. 923 * @param select whether to extend selection to the new position. 924 */ 925 public void positionCaret(HitInfo hit, boolean select) { 926 positionCaret(hit.getInsertionIndex(), hit.isLeading(), select, false); 927 } 928 929 private void positionCaret(int pos, boolean leading, boolean select, boolean extendSelection) { 930 boolean isNewLine = 931 (pos > 0 && 932 pos <= getSkinnable().getLength() && 933 getSkinnable().getText().codePointAt(pos-1) == 0x0a); 934 935 // special handling for a new line 936 if (!leading && isNewLine) { 937 leading = true; 938 pos -= 1; 939 } 940 941 if (select) { 942 if (extendSelection) { 943 getSkinnable().extendSelection(pos); 944 } else { 945 getSkinnable().selectPositionCaret(pos); 946 } 947 } else { 948 getSkinnable().positionCaret(pos); 949 } 950 951 setForwardBias(leading); 952 } 953 954 /** {@inheritDoc} */ 955 @Override public Rectangle2D getCharacterBounds(int index) { 956 TextArea textArea = getSkinnable(); 957 958 int paragraphIndex = paragraphNodes.getChildren().size(); 959 int paragraphOffset = textArea.getLength() + 1; 960 961 Text paragraphNode = null; 962 do { 963 paragraphNode = (Text)paragraphNodes.getChildren().get(--paragraphIndex); 964 paragraphOffset -= paragraphNode.getText().length() + 1; 965 } while (index < paragraphOffset); 966 967 int characterIndex = index - paragraphOffset; 968 boolean terminator = false; 969 970 if (characterIndex == paragraphNode.getText().length()) { 971 characterIndex--; 972 terminator = true; 973 } 974 975 characterBoundingPath.getElements().clear(); 976 characterBoundingPath.getElements().addAll(paragraphNode.rangeShape(characterIndex, characterIndex + 1)); 977 characterBoundingPath.setLayoutX(paragraphNode.getLayoutX()); 978 characterBoundingPath.setLayoutY(paragraphNode.getLayoutY()); 979 980 Bounds bounds = characterBoundingPath.getBoundsInLocal(); 981 982 double x = bounds.getMinX() + paragraphNode.getLayoutX() - textArea.getScrollLeft(); 983 double y = bounds.getMinY() + paragraphNode.getLayoutY() - textArea.getScrollTop(); 984 985 // Sometimes the bounds is empty, in which case we must ignore the width/height 986 double width = bounds.isEmpty() ? 0 : bounds.getWidth(); 987 double height = bounds.isEmpty() ? 0 : bounds.getHeight(); 988 989 if (terminator) { 990 x += width; 991 width = 0; 992 } 993 994 return new Rectangle2D(x, y, width, height); 995 } 996 997 /** {@inheritDoc} */ 998 @Override protected void scrollCharacterToVisible(final int index) { 999 // TODO We queue a callback because when characters are added or 1000 // removed the bounds are not immediately updated; is this really 1001 // necessary? 1002 1003 Platform.runLater(() -> { 1004 if (getSkinnable().getLength() == 0) { 1005 return; 1006 } 1007 Rectangle2D characterBounds = getCharacterBounds(index); 1008 scrollBoundsToVisible(characterBounds); 1009 }); 1010 } 1011 1012 1013 1014 /************************************************************************** 1015 * 1016 * Private implementation 1017 * 1018 **************************************************************************/ 1019 1020 TextAreaBehavior getBehavior() { 1021 return behavior; 1022 } 1023 1024 private void createPromptNode() { 1025 if (promptNode == null && usePromptText.get()) { 1026 promptNode = new Text(); 1027 contentView.getChildren().add(0, promptNode); 1028 promptNode.setManaged(false); 1029 promptNode.getStyleClass().add("text"); 1030 promptNode.visibleProperty().bind(usePromptText); 1031 promptNode.fontProperty().bind(getSkinnable().fontProperty()); 1032 promptNode.textProperty().bind(getSkinnable().promptTextProperty()); 1033 promptNode.fillProperty().bind(promptTextFillProperty()); 1034 } 1035 } 1036 1037 private void addParagraphNode(int i, String string) { 1038 final TextArea textArea = getSkinnable(); 1039 Text paragraphNode = new Text(string); 1040 paragraphNode.setTextOrigin(VPos.TOP); 1041 paragraphNode.setManaged(false); 1042 paragraphNode.getStyleClass().add("text"); 1043 paragraphNode.boundsTypeProperty().addListener((observable, oldValue, newValue) -> { 1044 invalidateMetrics(); 1045 updateFontMetrics(); 1046 }); 1047 paragraphNodes.getChildren().add(i, paragraphNode); 1048 1049 paragraphNode.fontProperty().bind(textArea.fontProperty()); 1050 paragraphNode.fillProperty().bind(textFillProperty()); 1051 paragraphNode.selectionFillProperty().bind(highlightTextFillProperty()); 1052 } 1053 1054 private double getScrollTopMax() { 1055 return Math.max(0, contentView.getHeight() - scrollPane.getViewportBounds().getHeight()); 1056 } 1057 1058 private double getScrollLeftMax() { 1059 return Math.max(0, contentView.getWidth() - scrollPane.getViewportBounds().getWidth()); 1060 } 1061 1062 private int getInsertionPoint(Text paragraphNode, double x, double y) { 1063 HitInfo hitInfo = paragraphNode.hitTest(new Point2D(x, y)); 1064 return hitInfo.getInsertionIndex(); 1065 } 1066 1067 private int getNextInsertionPoint(Text paragraphNode, double x, int from, 1068 VerticalDirection scrollDirection) { 1069 // TODO 1070 return 0; 1071 } 1072 1073 private void scrollCaretToVisible() { 1074 TextArea textArea = getSkinnable(); 1075 Bounds bounds = caretPath.getLayoutBounds(); 1076 double x = bounds.getMinX() - textArea.getScrollLeft(); 1077 double y = bounds.getMinY() - textArea.getScrollTop(); 1078 double w = bounds.getWidth(); 1079 double h = bounds.getHeight(); 1080 1081 if (SHOW_HANDLES) { 1082 if (caretHandle.isVisible()) { 1083 h += caretHandle.getHeight(); 1084 } else if (selectionHandle1.isVisible() && selectionHandle2.isVisible()) { 1085 x -= selectionHandle1.getWidth() / 2; 1086 y -= selectionHandle1.getHeight(); 1087 w += selectionHandle1.getWidth() / 2 + selectionHandle2.getWidth() / 2; 1088 h += selectionHandle1.getHeight() + selectionHandle2.getHeight(); 1089 } 1090 } 1091 1092 if (w > 0 && h > 0) { 1093 scrollBoundsToVisible(new Rectangle2D(x, y, w, h)); 1094 } 1095 } 1096 1097 private void scrollBoundsToVisible(Rectangle2D bounds) { 1098 TextArea textArea = getSkinnable(); 1099 Bounds viewportBounds = scrollPane.getViewportBounds(); 1100 1101 double viewportWidth = viewportBounds.getWidth(); 1102 double viewportHeight = viewportBounds.getHeight(); 1103 double scrollTop = textArea.getScrollTop(); 1104 double scrollLeft = textArea.getScrollLeft(); 1105 double slop = 6.0; 1106 1107 if (bounds.getMinY() < 0) { 1108 double y = scrollTop + bounds.getMinY(); 1109 if (y <= contentView.snappedTopInset()) { 1110 y = 0; 1111 } 1112 textArea.setScrollTop(y); 1113 } else if (contentView.snappedTopInset() + bounds.getMaxY() > viewportHeight) { 1114 double y = scrollTop + contentView.snappedTopInset() + bounds.getMaxY() - viewportHeight; 1115 if (y >= getScrollTopMax() - contentView.snappedBottomInset()) { 1116 y = getScrollTopMax(); 1117 } 1118 textArea.setScrollTop(y); 1119 } 1120 1121 1122 if (bounds.getMinX() < 0) { 1123 double x = scrollLeft + bounds.getMinX() - slop; 1124 if (x <= contentView.snappedLeftInset() + slop) { 1125 x = 0; 1126 } 1127 textArea.setScrollLeft(x); 1128 } else if (contentView.snappedLeftInset() + bounds.getMaxX() > viewportWidth) { 1129 double x = scrollLeft + contentView.snappedLeftInset() + bounds.getMaxX() - viewportWidth + slop; 1130 if (x >= getScrollLeftMax() - contentView.snappedRightInset() - slop) { 1131 x = getScrollLeftMax(); 1132 } 1133 textArea.setScrollLeft(x); 1134 } 1135 } 1136 1137 private void updatePrefViewportWidth() { 1138 int columnCount = getSkinnable().getPrefColumnCount(); 1139 scrollPane.setPrefViewportWidth(columnCount * characterWidth + contentView.snappedLeftInset() + contentView.snappedRightInset()); 1140 scrollPane.setMinViewportWidth(characterWidth + contentView.snappedLeftInset() + contentView.snappedRightInset()); 1141 } 1142 1143 private void updatePrefViewportHeight() { 1144 int rowCount = getSkinnable().getPrefRowCount(); 1145 scrollPane.setPrefViewportHeight(rowCount * lineHeight + contentView.snappedTopInset() + contentView.snappedBottomInset()); 1146 scrollPane.setMinViewportHeight(lineHeight + contentView.snappedTopInset() + contentView.snappedBottomInset()); 1147 } 1148 1149 private void updateFontMetrics() { 1150 Text firstParagraph = (Text)paragraphNodes.getChildren().get(0); 1151 lineHeight = Utils.getLineHeight(getSkinnable().getFont(), firstParagraph.getBoundsType()); 1152 characterWidth = fontMetrics.get().getCharWidth('W'); 1153 } 1154 1155 private double getTextTranslateX() { 1156 return contentView.snappedLeftInset(); 1157 } 1158 1159 private double getTextTranslateY() { 1160 return contentView.snappedTopInset(); 1161 } 1162 1163 private double getTextLeft() { 1164 return 0; 1165 } 1166 1167 private Point2D translateCaretPosition(Point2D p) { 1168 return p; 1169 } 1170 1171 private Text getTextNode() { 1172 if (USE_MULTIPLE_NODES) { 1173 throw new IllegalArgumentException("Multiple node traversal is not yet implemented."); 1174 } 1175 return (Text)paragraphNodes.getChildren().get(0); 1176 } 1177 1178 private void updateTextNodeCaretPos(int pos) { 1179 Text textNode = getTextNode(); 1180 if (isForwardBias()) { 1181 textNode.setCaretPosition(pos); 1182 } else { 1183 textNode.setCaretPosition(pos - 1); 1184 } 1185 textNode.caretBiasProperty().set(isForwardBias()); 1186 } 1187 1188 1189 1190 /************************************************************************** 1191 * 1192 * Support classes 1193 * 1194 **************************************************************************/ 1195 1196 private class ContentView extends Region { 1197 { 1198 getStyleClass().add("content"); 1199 1200 addEventHandler(MouseEvent.MOUSE_PRESSED, event -> { 1201 behavior.mousePressed(event); 1202 event.consume(); 1203 }); 1204 1205 addEventHandler(MouseEvent.MOUSE_RELEASED, event -> { 1206 behavior.mouseReleased(event); 1207 event.consume(); 1208 }); 1209 1210 addEventHandler(MouseEvent.MOUSE_DRAGGED, event -> { 1211 behavior.mouseDragged(event); 1212 event.consume(); 1213 }); 1214 } 1215 1216 @Override protected ObservableList<Node> getChildren() { 1217 return super.getChildren(); 1218 } 1219 1220 @Override public Orientation getContentBias() { 1221 return Orientation.HORIZONTAL; 1222 } 1223 1224 @Override protected double computePrefWidth(double height) { 1225 if (computedPrefWidth < 0) { 1226 double prefWidth = 0; 1227 1228 for (Node node : paragraphNodes.getChildren()) { 1229 Text paragraphNode = (Text)node; 1230 prefWidth = Math.max(prefWidth, 1231 Utils.computeTextWidth(paragraphNode.getFont(), 1232 paragraphNode.getText(), 0)); 1233 } 1234 1235 prefWidth += snappedLeftInset() + snappedRightInset(); 1236 1237 Bounds viewPortBounds = scrollPane.getViewportBounds(); 1238 computedPrefWidth = Math.max(prefWidth, (viewPortBounds != null) ? viewPortBounds.getWidth() : 0); 1239 } 1240 return computedPrefWidth; 1241 } 1242 1243 @Override protected double computePrefHeight(double width) { 1244 if (width != widthForComputedPrefHeight) { 1245 invalidateMetrics(); 1246 widthForComputedPrefHeight = width; 1247 } 1248 1249 if (computedPrefHeight < 0) { 1250 double wrappingWidth; 1251 if (width == -1) { 1252 wrappingWidth = 0; 1253 } else { 1254 wrappingWidth = Math.max(width - (snappedLeftInset() + snappedRightInset()), 0); 1255 } 1256 1257 double prefHeight = 0; 1258 1259 for (Node node : paragraphNodes.getChildren()) { 1260 Text paragraphNode = (Text)node; 1261 prefHeight += Utils.computeTextHeight( 1262 paragraphNode.getFont(), 1263 paragraphNode.getText(), 1264 wrappingWidth, 1265 paragraphNode.getBoundsType()); 1266 } 1267 1268 prefHeight += snappedTopInset() + snappedBottomInset(); 1269 1270 Bounds viewPortBounds = scrollPane.getViewportBounds(); 1271 computedPrefHeight = Math.max(prefHeight, (viewPortBounds != null) ? viewPortBounds.getHeight() : 0); 1272 } 1273 return computedPrefHeight; 1274 } 1275 1276 @Override protected double computeMinWidth(double height) { 1277 if (computedMinWidth < 0) { 1278 double hInsets = snappedLeftInset() + snappedRightInset(); 1279 computedMinWidth = Math.min(characterWidth + hInsets, computePrefWidth(height)); 1280 } 1281 return computedMinWidth; 1282 } 1283 1284 @Override protected double computeMinHeight(double width) { 1285 if (computedMinHeight < 0) { 1286 double vInsets = snappedTopInset() + snappedBottomInset(); 1287 computedMinHeight = Math.min(lineHeight + vInsets, computePrefHeight(width)); 1288 } 1289 return computedMinHeight; 1290 } 1291 1292 @Override public void layoutChildren() { 1293 TextArea textArea = getSkinnable(); 1294 double width = getWidth(); 1295 1296 // Lay out paragraphs 1297 final double topPadding = snappedTopInset(); 1298 final double leftPadding = snappedLeftInset(); 1299 1300 double wrappingWidth = Math.max(width - (leftPadding + snappedRightInset()), 0); 1301 1302 double y = topPadding; 1303 1304 final List<Node> paragraphNodesChildren = paragraphNodes.getChildren(); 1305 1306 for (int i = 0; i < paragraphNodesChildren.size(); i++) { 1307 Node node = paragraphNodesChildren.get(i); 1308 Text paragraphNode = (Text)node; 1309 paragraphNode.setWrappingWidth(wrappingWidth); 1310 1311 Bounds bounds = paragraphNode.getBoundsInLocal(); 1312 paragraphNode.setLayoutX(leftPadding); 1313 paragraphNode.setLayoutY(y); 1314 1315 y += bounds.getHeight(); 1316 } 1317 1318 if (promptNode != null) { 1319 promptNode.setLayoutX(leftPadding); 1320 promptNode.setLayoutY(topPadding + promptNode.getBaselineOffset()); 1321 promptNode.setWrappingWidth(wrappingWidth); 1322 } 1323 1324 // Update the selection 1325 IndexRange selection = textArea.getSelection(); 1326 Bounds oldCaretBounds = caretPath.getBoundsInParent(); 1327 1328 selectionHighlightGroup.getChildren().clear(); 1329 1330 int caretPos = textArea.getCaretPosition(); 1331 int anchorPos = textArea.getAnchor(); 1332 1333 if (SHOW_HANDLES) { 1334 // Install and resize the handles for caret and anchor. 1335 if (selection.getLength() > 0) { 1336 selectionHandle1.resize(selectionHandle1.prefWidth(-1), 1337 selectionHandle1.prefHeight(-1)); 1338 selectionHandle2.resize(selectionHandle2.prefWidth(-1), 1339 selectionHandle2.prefHeight(-1)); 1340 } else { 1341 caretHandle.resize(caretHandle.prefWidth(-1), 1342 caretHandle.prefHeight(-1)); 1343 } 1344 1345 // Position the handle for the anchor. This could be handle1 or handle2. 1346 // Do this before positioning the actual caret. 1347 if (selection.getLength() > 0) { 1348 int paragraphIndex = paragraphNodesChildren.size(); 1349 int paragraphOffset = textArea.getLength() + 1; 1350 Text paragraphNode = null; 1351 do { 1352 paragraphNode = (Text)paragraphNodesChildren.get(--paragraphIndex); 1353 paragraphOffset -= paragraphNode.getText().length() + 1; 1354 } while (anchorPos < paragraphOffset); 1355 1356 updateTextNodeCaretPos(anchorPos - paragraphOffset); 1357 caretPath.getElements().clear(); 1358 caretPath.getElements().addAll(paragraphNode.getCaretShape()); 1359 caretPath.setLayoutX(paragraphNode.getLayoutX()); 1360 caretPath.setLayoutY(paragraphNode.getLayoutY()); 1361 1362 Bounds b = caretPath.getBoundsInParent(); 1363 if (caretPos < anchorPos) { 1364 selectionHandle2.setLayoutX(b.getMinX() - selectionHandle2.getWidth() / 2); 1365 selectionHandle2.setLayoutY(b.getMaxY() - 1); 1366 } else { 1367 selectionHandle1.setLayoutX(b.getMinX() - selectionHandle1.getWidth() / 2); 1368 selectionHandle1.setLayoutY(b.getMinY() - selectionHandle1.getHeight() + 1); 1369 } 1370 } 1371 } 1372 1373 { 1374 // Position caret 1375 int paragraphIndex = paragraphNodesChildren.size(); 1376 int paragraphOffset = textArea.getLength() + 1; 1377 1378 Text paragraphNode = null; 1379 do { 1380 paragraphNode = (Text)paragraphNodesChildren.get(--paragraphIndex); 1381 paragraphOffset -= paragraphNode.getText().length() + 1; 1382 } while (caretPos < paragraphOffset); 1383 1384 updateTextNodeCaretPos(caretPos - paragraphOffset); 1385 1386 caretPath.getElements().clear(); 1387 caretPath.getElements().addAll(paragraphNode.getCaretShape()); 1388 1389 caretPath.setLayoutX(paragraphNode.getLayoutX()); 1390 1391 // TODO: Remove this temporary workaround for RT-27533 1392 paragraphNode.setLayoutX(2 * paragraphNode.getLayoutX() - paragraphNode.getBoundsInParent().getMinX()); 1393 1394 caretPath.setLayoutY(paragraphNode.getLayoutY()); 1395 if (oldCaretBounds == null || !oldCaretBounds.equals(caretPath.getBoundsInParent())) { 1396 scrollCaretToVisible(); 1397 } 1398 } 1399 1400 // Update selection fg and bg 1401 int start = selection.getStart(); 1402 int end = selection.getEnd(); 1403 for (int i = 0, max = paragraphNodesChildren.size(); i < max; i++) { 1404 Node paragraphNode = paragraphNodesChildren.get(i); 1405 Text textNode = (Text)paragraphNode; 1406 int paragraphLength = textNode.getText().length() + 1; 1407 if (end > start && start < paragraphLength) { 1408 textNode.setSelectionStart(start); 1409 textNode.setSelectionEnd(Math.min(end, paragraphLength)); 1410 1411 Path selectionHighlightPath = new Path(); 1412 selectionHighlightPath.setManaged(false); 1413 selectionHighlightPath.setStroke(null); 1414 PathElement[] selectionShape = textNode.getSelectionShape(); 1415 if (selectionShape != null) { 1416 selectionHighlightPath.getElements().addAll(selectionShape); 1417 } 1418 selectionHighlightGroup.getChildren().add(selectionHighlightPath); 1419 selectionHighlightGroup.setVisible(true); 1420 selectionHighlightPath.setLayoutX(textNode.getLayoutX()); 1421 selectionHighlightPath.setLayoutY(textNode.getLayoutY()); 1422 updateHighlightFill(); 1423 } else { 1424 textNode.setSelectionStart(-1); 1425 textNode.setSelectionEnd(-1); 1426 selectionHighlightGroup.setVisible(false); 1427 } 1428 start = Math.max(0, start - paragraphLength); 1429 end = Math.max(0, end - paragraphLength); 1430 } 1431 1432 if (SHOW_HANDLES) { 1433 // Position handle for the caret. This could be handle1 or handle2 when 1434 // a selection is active. 1435 Bounds b = caretPath.getBoundsInParent(); 1436 if (selection.getLength() > 0) { 1437 if (caretPos < anchorPos) { 1438 selectionHandle1.setLayoutX(b.getMinX() - selectionHandle1.getWidth() / 2); 1439 selectionHandle1.setLayoutY(b.getMinY() - selectionHandle1.getHeight() + 1); 1440 } else { 1441 selectionHandle2.setLayoutX(b.getMinX() - selectionHandle2.getWidth() / 2); 1442 selectionHandle2.setLayoutY(b.getMaxY() - 1); 1443 } 1444 } else { 1445 caretHandle.setLayoutX(b.getMinX() - caretHandle.getWidth() / 2 + 1); 1446 caretHandle.setLayoutY(b.getMaxY()); 1447 } 1448 } 1449 1450 if (scrollPane.getPrefViewportWidth() == 0 1451 || scrollPane.getPrefViewportHeight() == 0) { 1452 updatePrefViewportWidth(); 1453 updatePrefViewportHeight(); 1454 if (getParent() != null && scrollPane.getPrefViewportWidth() > 0 1455 || scrollPane.getPrefViewportHeight() > 0) { 1456 // Force layout of viewRect in ScrollPaneSkin 1457 getParent().requestLayout(); 1458 } 1459 } 1460 1461 // RT-36454: Fit to width/height only if smaller than viewport. 1462 // That is, grow to fit but don't shrink to fit. 1463 Bounds viewportBounds = scrollPane.getViewportBounds(); 1464 boolean wasFitToWidth = scrollPane.isFitToWidth(); 1465 boolean wasFitToHeight = scrollPane.isFitToHeight(); 1466 boolean setFitToWidth = textArea.isWrapText() || computePrefWidth(-1) <= viewportBounds.getWidth(); 1467 boolean setFitToHeight = computePrefHeight(width) <= viewportBounds.getHeight(); 1468 if (wasFitToWidth != setFitToWidth || wasFitToHeight != setFitToHeight) { 1469 Platform.runLater(() -> { 1470 scrollPane.setFitToWidth(setFitToWidth); 1471 scrollPane.setFitToHeight(setFitToHeight); 1472 }); 1473 getParent().requestLayout(); 1474 } 1475 } 1476 } 1477 }