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 **************************************************************************/ 86 87 /** A shared helper object, used only by downLines(). */ 88 private static final Path tmpCaretPath = new Path(); 89 90 91 92 /************************************************************************** 93 * 94 * Private fields 95 * 96 **************************************************************************/ 97 98 final private TextArea textArea; 99 100 // *** NOTE: Multiple node mode is not yet fully implemented *** // 101 private final boolean USE_MULTIPLE_NODES = false; 102 103 private final TextAreaBehavior behavior; 104 105 private double computedMinWidth = Double.NEGATIVE_INFINITY; 106 private double computedMinHeight = Double.NEGATIVE_INFINITY; 107 private double computedPrefWidth = Double.NEGATIVE_INFINITY; 108 private double computedPrefHeight = Double.NEGATIVE_INFINITY; 109 private double widthForComputedPrefHeight = Double.NEGATIVE_INFINITY; 110 private double characterWidth; 111 private double lineHeight; 112 113 private ContentView contentView = new ContentView(); 114 private Group paragraphNodes = new Group(); 115 116 private Text promptNode; 117 private ObservableBooleanValue usePromptText; 118 119 private ObservableIntegerValue caretPosition; 120 private Group selectionHighlightGroup = new Group(); 121 122 private ScrollPane scrollPane; 123 private Bounds oldViewportBounds; 124 125 private VerticalDirection scrollDirection = null; 126 127 private Path characterBoundingPath = new Path(); 128 129 private Timeline scrollSelectionTimeline = new Timeline(); 130 private EventHandler<ActionEvent> scrollSelectionHandler = event -> { 131 switch (scrollDirection) { 132 case UP: { 133 // TODO Get previous offset 134 break; 135 } 136 137 case DOWN: { 138 // TODO Get next offset 139 break; 140 } 141 } 142 }; 143 144 private double pressX, pressY; // For dragging handles on embedded 145 private boolean handlePressed; 146 147 /** 148 * Remembers horizontal position when traversing up / down. 149 */ 150 double targetCaretX = -1; 151 152 153 154 /************************************************************************** 155 * 156 * Constructors 157 * 158 **************************************************************************/ 159 160 /** 161 * Creates a new TextAreaSkin instance, installing the necessary child 162 * nodes into the Control {@link Control#getChildren() children} list, as 163 * well as the necessary input mappings for handling key, mouse, etc events. 164 * 165 * @param control The control that this skin should be installed onto. 166 */ 167 public TextAreaSkin(final TextArea control) { 168 super(control); 169 170 // install default input map for the text area control 171 this.behavior = new TextAreaBehavior(control); 172 this.behavior.setTextAreaSkin(this); 173 // control.setInputMap(behavior.getInputMap()); 174 175 this.textArea = control; 176 177 caretPosition = new IntegerBinding() { 178 { bind(control.caretPositionProperty()); } 179 @Override protected int computeValue() { 180 return control.getCaretPosition(); 181 } 182 }; 183 caretPosition.addListener((observable, oldValue, newValue) -> { 184 targetCaretX = -1; 185 if (newValue.intValue() > oldValue.intValue()) { 186 setForwardBias(true); 187 } 188 }); 189 190 forwardBiasProperty().addListener(observable -> { 191 if (control.getWidth() > 0) { 192 updateTextNodeCaretPos(control.getCaretPosition()); 193 } 194 }); 195 196 // setManaged(false); 197 198 // Initialize content 199 scrollPane = new ScrollPane(); 200 scrollPane.setFitToWidth(control.isWrapText()); 201 scrollPane.setContent(contentView); 202 getChildren().add(scrollPane); 203 204 getSkinnable().addEventFilter(ScrollEvent.ANY, event -> { 205 if (event.isDirect() && handlePressed) { 206 event.consume(); 207 } 208 }); 209 210 // Add selection 211 selectionHighlightGroup.setManaged(false); 212 selectionHighlightGroup.setVisible(false); 213 contentView.getChildren().add(selectionHighlightGroup); 214 215 // Add content view 216 paragraphNodes.setManaged(false); 217 contentView.getChildren().add(paragraphNodes); 218 219 // Add caret 220 caretPath.setManaged(false); 221 caretPath.setStrokeWidth(1); 222 caretPath.fillProperty().bind(textFillProperty()); 223 caretPath.strokeProperty().bind(textFillProperty()); 224 // modifying visibility of the caret forces a layout-pass (RT-32373), so 225 // instead we modify the opacity. 226 caretPath.opacityProperty().bind(new DoubleBinding() { 227 { bind(caretVisibleProperty()); } 228 @Override protected double computeValue() { 229 return caretVisibleProperty().get() ? 1.0 : 0.0; 230 } 231 }); 232 contentView.getChildren().add(caretPath); 233 234 if (SHOW_HANDLES) { 235 contentView.getChildren().addAll(caretHandle, selectionHandle1, selectionHandle2); 236 } 237 238 scrollPane.hvalueProperty().addListener((observable, oldValue, newValue) -> { 239 getSkinnable().setScrollLeft(newValue.doubleValue() * getScrollLeftMax()); 240 }); 241 242 scrollPane.vvalueProperty().addListener((observable, oldValue, newValue) -> { 243 getSkinnable().setScrollTop(newValue.doubleValue() * getScrollTopMax()); 244 }); 245 246 // Initialize the scroll selection timeline 247 scrollSelectionTimeline.setCycleCount(Timeline.INDEFINITE); 248 List<KeyFrame> scrollSelectionFrames = scrollSelectionTimeline.getKeyFrames(); 249 scrollSelectionFrames.clear(); 250 scrollSelectionFrames.add(new KeyFrame(Duration.millis(350), scrollSelectionHandler)); 251 252 // Add initial text content 253 for (int i = 0, n = USE_MULTIPLE_NODES ? control.getParagraphs().size() : 1; i < n; i++) { 254 CharSequence paragraph = (n == 1) ? control.textProperty().getValueSafe() : control.getParagraphs().get(i); 255 addParagraphNode(i, paragraph.toString()); 256 } 257 258 control.selectionProperty().addListener((observable, oldValue, newValue) -> { 259 // TODO Why do we need two calls here? 260 control.requestLayout(); 261 contentView.requestLayout(); 262 }); 263 264 control.wrapTextProperty().addListener((observable, oldValue, newValue) -> { 265 invalidateMetrics(); 266 scrollPane.setFitToWidth(newValue); 267 }); 268 269 control.prefColumnCountProperty().addListener((observable, oldValue, newValue) -> { 270 invalidateMetrics(); 271 updatePrefViewportWidth(); 272 }); 273 274 control.prefRowCountProperty().addListener((observable, oldValue, newValue) -> { 275 invalidateMetrics(); 276 updatePrefViewportHeight(); 277 }); 278 279 updateFontMetrics(); 280 fontMetrics.addListener(valueModel -> { 281 updateFontMetrics(); 282 }); 283 284 contentView.paddingProperty().addListener(valueModel -> { 285 updatePrefViewportWidth(); 286 updatePrefViewportHeight(); 287 }); 288 289 scrollPane.viewportBoundsProperty().addListener(valueModel -> { 290 if (scrollPane.getViewportBounds() != null) { 291 // ScrollPane creates a new Bounds instance for each 292 // layout pass, so we need to check if the width/height 293 // have really changed to avoid infinite layout requests. 294 Bounds newViewportBounds = scrollPane.getViewportBounds(); 295 if (oldViewportBounds == null || 296 oldViewportBounds.getWidth() != newViewportBounds.getWidth() || 297 oldViewportBounds.getHeight() != newViewportBounds.getHeight()) { 298 299 invalidateMetrics(); 300 oldViewportBounds = newViewportBounds; 301 contentView.requestLayout(); 302 } 303 } 304 }); 305 306 control.scrollTopProperty().addListener((observable, oldValue, newValue) -> { 307 double vValue = (newValue.doubleValue() < getScrollTopMax()) 308 ? (newValue.doubleValue() / getScrollTopMax()) : 1.0; 309 scrollPane.setVvalue(vValue); 310 }); 311 312 control.scrollLeftProperty().addListener((observable, oldValue, newValue) -> { 313 double hValue = (newValue.doubleValue() < getScrollLeftMax()) 314 ? (newValue.doubleValue() / getScrollLeftMax()) : 1.0; 315 scrollPane.setHvalue(hValue); 316 }); 317 318 if (USE_MULTIPLE_NODES) { 319 control.getParagraphs().addListener((ListChangeListener.Change<? extends CharSequence> change) -> { 320 while (change.next()) { 321 int from = change.getFrom(); 322 int to = change.getTo(); 323 List<? extends CharSequence> removed = change.getRemoved(); 324 if (from < to) { 325 326 if (removed.isEmpty()) { 327 // This is an add 328 for (int i = from, n = to; i < n; i++) { 329 addParagraphNode(i, change.getList().get(i).toString()); 330 } 331 } else { 332 // This is an update 333 for (int i = from, n = to; i < n; i++) { 334 Node node = paragraphNodes.getChildren().get(i); 335 Text paragraphNode = (Text) node; 336 paragraphNode.setText(change.getList().get(i).toString()); 337 } 338 } 339 } else { 340 // This is a remove 341 paragraphNodes.getChildren().subList(from, from + removed.size()).clear(); 342 } 343 } 344 }); 345 } else { 346 control.textProperty().addListener(observable -> { 347 invalidateMetrics(); 348 ((Text)paragraphNodes.getChildren().get(0)).setText(control.textProperty().getValueSafe()); 349 contentView.requestLayout(); 350 }); 351 } 352 353 usePromptText = new BooleanBinding() { 354 { bind(control.textProperty(), control.promptTextProperty()); } 355 @Override protected boolean computeValue() { 356 String txt = control.getText(); 357 String promptTxt = control.getPromptText(); 358 return ((txt == null || txt.isEmpty()) && 359 promptTxt != null && !promptTxt.isEmpty()); 360 } 361 }; 362 363 if (usePromptText.get()) { 364 createPromptNode(); 365 } 366 367 usePromptText.addListener(observable -> { 368 createPromptNode(); 369 control.requestLayout(); 370 }); 371 372 updateHighlightFill(); 373 updatePrefViewportWidth(); 374 updatePrefViewportHeight(); 375 if (control.isFocused()) setCaretAnimating(true); 376 377 if (SHOW_HANDLES) { 378 selectionHandle1.setRotate(180); 379 380 EventHandler<MouseEvent> handlePressHandler = e -> { 381 pressX = e.getX(); 382 pressY = e.getY(); 383 handlePressed = true; 384 e.consume(); 385 }; 386 387 EventHandler<MouseEvent> handleReleaseHandler = event -> { 388 handlePressed = false; 389 }; 390 391 caretHandle.setOnMousePressed(handlePressHandler); 392 selectionHandle1.setOnMousePressed(handlePressHandler); 393 selectionHandle2.setOnMousePressed(handlePressHandler); 394 395 caretHandle.setOnMouseReleased(handleReleaseHandler); 396 selectionHandle1.setOnMouseReleased(handleReleaseHandler); 397 selectionHandle2.setOnMouseReleased(handleReleaseHandler); 398 399 caretHandle.setOnMouseDragged(e -> { 400 Text textNode = getTextNode(); 401 Point2D tp = textNode.localToScene(0, 0); 402 Point2D p = new Point2D(e.getSceneX() - tp.getX() + 10/*??*/ - pressX + caretHandle.getWidth() / 2, 403 e.getSceneY() - tp.getY() - pressY - 6); 404 HitInfo hit = textNode.impl_hitTestChar(translateCaretPosition(p)); 405 int pos = hit.getCharIndex(); 406 if (pos > 0) { 407 int oldPos = textNode.getImpl_caretPosition(); 408 textNode.setImpl_caretPosition(pos); 409 PathElement element = textNode.getImpl_caretShape()[0]; 410 if (element instanceof MoveTo && ((MoveTo)element).getY() > e.getY() - getTextTranslateY()) { 411 hit.setCharIndex(pos - 1); 412 } 413 textNode.setImpl_caretPosition(oldPos); 414 } 415 positionCaret(hit, false); 416 e.consume(); 417 }); 418 419 selectionHandle1.setOnMouseDragged(e -> { 420 TextArea control1 = getSkinnable(); 421 Text textNode = getTextNode(); 422 Point2D tp = textNode.localToScene(0, 0); 423 Point2D p = new Point2D(e.getSceneX() - tp.getX() + 10/*??*/ - pressX + selectionHandle1.getWidth() / 2, 424 e.getSceneY() - tp.getY() - pressY + selectionHandle1.getHeight() + 5); 425 HitInfo hit = textNode.impl_hitTestChar(translateCaretPosition(p)); 426 int pos = hit.getCharIndex(); 427 if (control1.getAnchor() < control1.getCaretPosition()) { 428 // Swap caret and anchor 429 control1.selectRange(control1.getCaretPosition(), control1.getAnchor()); 430 } 431 if (pos > 0) { 432 if (pos >= control1.getAnchor()) { 433 pos = control1.getAnchor(); 434 } 435 int oldPos = textNode.getImpl_caretPosition(); 436 textNode.setImpl_caretPosition(pos); 437 PathElement element = textNode.getImpl_caretShape()[0]; 438 if (element instanceof MoveTo && ((MoveTo)element).getY() > e.getY() - getTextTranslateY()) { 439 hit.setCharIndex(pos - 1); 440 } 441 textNode.setImpl_caretPosition(oldPos); 442 } 443 positionCaret(hit, true); 444 e.consume(); 445 }); 446 447 selectionHandle2.setOnMouseDragged(e -> { 448 TextArea control1 = getSkinnable(); 449 Text textNode = getTextNode(); 450 Point2D tp = textNode.localToScene(0, 0); 451 Point2D p = new Point2D(e.getSceneX() - tp.getX() + 10/*??*/ - pressX + selectionHandle2.getWidth() / 2, 452 e.getSceneY() - tp.getY() - pressY - 6); 453 HitInfo hit = textNode.impl_hitTestChar(translateCaretPosition(p)); 454 int pos = hit.getCharIndex(); 455 if (control1.getAnchor() > control1.getCaretPosition()) { 456 // Swap caret and anchor 457 control1.selectRange(control1.getCaretPosition(), control1.getAnchor()); 458 } 459 if (pos > 0) { 460 if (pos <= control1.getAnchor() + 1) { 461 pos = Math.min(control1.getAnchor() + 2, control1.getLength()); 462 } 463 int oldPos = textNode.getImpl_caretPosition(); 464 textNode.setImpl_caretPosition(pos); 465 PathElement element = textNode.getImpl_caretShape()[0]; 466 if (element instanceof MoveTo && ((MoveTo)element).getY() > e.getY() - getTextTranslateY()) { 467 hit.setCharIndex(pos - 1); 468 } 469 textNode.setImpl_caretPosition(oldPos); 470 positionCaret(hit, true); 471 } 472 e.consume(); 473 }); 474 } 475 } 476 477 478 479 /*************************************************************************** 480 * * 481 * Public API * 482 * * 483 **************************************************************************/ 484 485 /** {@inheritDoc} */ 486 @Override protected void invalidateMetrics() { 487 computedMinWidth = Double.NEGATIVE_INFINITY; 488 computedMinHeight = Double.NEGATIVE_INFINITY; 489 computedPrefWidth = Double.NEGATIVE_INFINITY; 490 computedPrefHeight = Double.NEGATIVE_INFINITY; 491 } 492 493 /** {@inheritDoc} */ 494 @Override protected void layoutChildren(double contentX, double contentY, double contentWidth, double contentHeight) { 495 scrollPane.resizeRelocate(contentX, contentY, contentWidth, contentHeight); 496 } 497 498 /** {@inheritDoc} */ 499 @Override protected void updateHighlightFill() { 500 for (Node node : selectionHighlightGroup.getChildren()) { 501 Path selectionHighlightPath = (Path)node; 502 selectionHighlightPath.setFill(highlightFillProperty().get()); 503 } 504 } 505 506 // Public for behavior 507 /** 508 * Performs a hit test, mapping point to index in the content. 509 * 510 * @param x the x coordinate of the point. 511 * @param y the y coordinate of the point. 512 * @return a {@code TextPosInfo} object describing the index and forward bias. 513 */ 514 public TextPosInfo getIndex(double x, double y) { 515 // adjust the event to be in the same coordinate space as the 516 // text content of the textInputControl 517 Text textNode = getTextNode(); 518 Point2D p = new Point2D(x - textNode.getLayoutX(), y - getTextTranslateY()); 519 HitInfo hit = textNode.impl_hitTestChar(translateCaretPosition(p)); 520 int pos = hit.getCharIndex(); 521 if (pos > 0) { 522 int oldPos = textNode.getImpl_caretPosition(); 523 textNode.setImpl_caretPosition(pos); 524 PathElement element = textNode.getImpl_caretShape()[0]; 525 if (element instanceof MoveTo && ((MoveTo)element).getY() > y - getTextTranslateY()) { 526 hit.setCharIndex(pos - 1); 527 } 528 textNode.setImpl_caretPosition(oldPos); 529 } 530 return new TextPosInfo(hit); 531 }; 532 533 /** {@inheritDoc} */ 534 @Override public void moveCaret(TextUnit unit, Direction dir, boolean select) { 535 switch (unit) { 536 case CHARACTER: 537 switch (dir) { 538 case LEFT: 539 case RIGHT: 540 nextCharacterVisually(dir == Direction.RIGHT); 541 break; 542 default: 543 throw new IllegalArgumentException(""+dir); 544 } 545 break; 546 547 case LINE: 548 switch (dir) { 549 case UP: 550 previousLine(select); 551 break; 552 case DOWN: 553 nextLine(select); 554 break; 555 case BEGINNING: 556 lineStart(select, select && isMac()); 557 break; 558 case END: 559 lineEnd(select, select && isMac()); 560 break; 561 default: 562 throw new IllegalArgumentException(""+dir); 563 } 564 break; 565 566 case PAGE: 567 switch (dir) { 568 case UP: 569 previousPage(select); 570 break; 571 case DOWN: 572 nextPage(select); 573 break; 574 default: 575 throw new IllegalArgumentException(""+dir); 576 } 577 break; 578 579 case PARAGRAPH: 580 switch (dir) { 581 case UP: 582 paragraphStart(true, select); 583 break; 584 case DOWN: 585 paragraphEnd(true, select); 586 break; 587 case BEGINNING: 588 paragraphStart(false, select); 589 break; 590 case END: 591 paragraphEnd(false, select); 592 break; 593 default: 594 throw new IllegalArgumentException(""+dir); 595 } 596 break; 597 598 default: 599 throw new IllegalArgumentException(""+unit); 600 } 601 } 602 603 private void nextCharacterVisually(boolean moveRight) { 604 if (isRTL()) { 605 // Text node is mirrored. 606 moveRight = !moveRight; 607 } 608 609 Text textNode = getTextNode(); 610 Bounds caretBounds = caretPath.getLayoutBounds(); 611 if (caretPath.getElements().size() == 4) { 612 // The caret is split 613 // TODO: Find a better way to get the primary caret position 614 // instead of depending on the internal implementation. 615 // See RT-25465. 616 caretBounds = new Path(caretPath.getElements().get(0), caretPath.getElements().get(1)).getLayoutBounds(); 617 } 618 double hitX = moveRight ? caretBounds.getMaxX() : caretBounds.getMinX(); 619 double hitY = (caretBounds.getMinY() + caretBounds.getMaxY()) / 2; 620 HitInfo hit = textNode.impl_hitTestChar(new Point2D(hitX, hitY)); 621 Path charShape = new Path(textNode.impl_getRangeShape(hit.getCharIndex(), hit.getCharIndex() + 1)); 622 if ((moveRight && charShape.getLayoutBounds().getMaxX() > caretBounds.getMaxX()) || 623 (!moveRight && charShape.getLayoutBounds().getMinX() < caretBounds.getMinX())) { 624 hit.setLeading(!hit.isLeading()); 625 positionCaret(hit, false); 626 } else { 627 // We're at beginning or end of line. Try moving up / down. 628 int dot = textArea.getCaretPosition(); 629 targetCaretX = moveRight ? 0 : Double.MAX_VALUE; 630 // TODO: Use Bidi sniffing instead of assuming right means forward here? 631 downLines(moveRight ? 1 : -1, false, false); 632 targetCaretX = -1; 633 if (dot == textArea.getCaretPosition()) { 634 if (moveRight) { 635 textArea.forward(); 636 } else { 637 textArea.backward(); 638 } 639 } 640 } 641 } 642 643 private void downLines(int nLines, boolean select, boolean extendSelection) { 644 Text textNode = getTextNode(); 645 Bounds caretBounds = caretPath.getLayoutBounds(); 646 647 // The middle y coordinate of the the line we want to go to. 648 double targetLineMidY = (caretBounds.getMinY() + caretBounds.getMaxY()) / 2 + nLines * lineHeight; 649 if (targetLineMidY < 0) { 650 targetLineMidY = 0; 651 } 652 653 // The target x for the caret. This may have been set during a 654 // previous call. 655 double x = (targetCaretX >= 0) ? targetCaretX : (caretBounds.getMaxX()); 656 657 // Find a text position for the target x,y. 658 HitInfo hit = textNode.impl_hitTestChar(translateCaretPosition(new Point2D(x, targetLineMidY))); 659 int pos = hit.getCharIndex(); 660 661 // Save the old pos temporarily while testing the new one. 662 int oldPos = textNode.getImpl_caretPosition(); 663 boolean oldBias = textNode.isImpl_caretBias(); 664 textNode.setImpl_caretBias(hit.isLeading()); 665 textNode.setImpl_caretPosition(pos); 666 tmpCaretPath.getElements().clear(); 667 tmpCaretPath.getElements().addAll(textNode.getImpl_caretShape()); 668 tmpCaretPath.setLayoutX(textNode.getLayoutX()); 669 tmpCaretPath.setLayoutY(textNode.getLayoutY()); 670 Bounds tmpCaretBounds = tmpCaretPath.getLayoutBounds(); 671 // The y for the middle of the row we found. 672 double foundLineMidY = (tmpCaretBounds.getMinY() + tmpCaretBounds.getMaxY()) / 2; 673 textNode.setImpl_caretBias(oldBias); 674 textNode.setImpl_caretPosition(oldPos); 675 676 if (pos > 0) { 677 if (nLines > 0 && foundLineMidY > targetLineMidY) { 678 // We went too far and ended up after a newline. 679 hit.setCharIndex(pos - 1); 680 } 681 682 if (pos >= textArea.getLength() && getCharacter(pos - 1) == '\n') { 683 // Special case for newline at end of text. 684 hit.setLeading(true); 685 } 686 } 687 688 // Test if the found line is in the correct direction and move 689 // the caret. 690 if (nLines == 0 || 691 (nLines > 0 && foundLineMidY > caretBounds.getMaxY()) || 692 (nLines < 0 && foundLineMidY < caretBounds.getMinY())) { 693 694 positionCaret(hit, select, extendSelection); 695 targetCaretX = x; 696 } 697 } 698 699 private void previousLine(boolean select) { 700 downLines(-1, select, false); 701 } 702 703 private void nextLine(boolean select) { 704 downLines(1, select, false); 705 } 706 707 private void previousPage(boolean select) { 708 downLines(-(int)(scrollPane.getViewportBounds().getHeight() / lineHeight), 709 select, false); 710 } 711 712 private void nextPage(boolean select) { 713 downLines((int)(scrollPane.getViewportBounds().getHeight() / lineHeight), 714 select, false); 715 } 716 717 private void lineStart(boolean select, boolean extendSelection) { 718 targetCaretX = 0; 719 downLines(0, select, extendSelection); 720 targetCaretX = -1; 721 } 722 723 private void lineEnd(boolean select, boolean extendSelection) { 724 targetCaretX = Double.MAX_VALUE; 725 downLines(0, select, extendSelection); 726 targetCaretX = -1; 727 } 728 729 730 private void paragraphStart(boolean previousIfAtStart, boolean select) { 731 TextArea textArea = getSkinnable(); 732 String text = textArea.textProperty().getValueSafe(); 733 int pos = textArea.getCaretPosition(); 734 735 if (pos > 0) { 736 if (previousIfAtStart && text.codePointAt(pos-1) == 0x0a) { 737 // We are at the beginning of a paragraph. 738 // Back up to the previous paragraph. 739 pos--; 740 } 741 // Back up to the beginning of this paragraph 742 while (pos > 0 && text.codePointAt(pos-1) != 0x0a) { 743 pos--; 744 } 745 if (select) { 746 textArea.selectPositionCaret(pos); 747 } else { 748 textArea.positionCaret(pos); 749 setForwardBias(true); 750 } 751 } 752 } 753 754 private void paragraphEnd(boolean goPastInitialNewline, boolean select) { 755 TextArea textArea = getSkinnable(); 756 String text = textArea.textProperty().getValueSafe(); 757 int pos = textArea.getCaretPosition(); 758 int len = text.length(); 759 boolean wentPastInitialNewline = false; 760 boolean goPastTrailingNewline = isWindows(); 761 762 if (pos < len) { 763 if (goPastInitialNewline && text.codePointAt(pos) == 0x0a) { 764 // We are at the end of a paragraph, start by moving to the 765 // next paragraph. 766 pos++; 767 wentPastInitialNewline = true; 768 } 769 if (!(goPastTrailingNewline && wentPastInitialNewline)) { 770 // Go to the end of this paragraph 771 while (pos < len && text.codePointAt(pos) != 0x0a) { 772 pos++; 773 } 774 if (goPastTrailingNewline && pos < len) { 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 830 if (paragraphNode != null) { 831 for (Node node : nodes) { 832 node.setLayoutX(paragraphNode.getLayoutX()); 833 node.setLayoutY(paragraphNode.getLayoutY()); 834 } 835 } 836 contentView.getChildren().addAll(nodes); 837 } 838 839 /** {@inheritDoc} */ 840 @Override protected void removeHighlight(List<? extends Node> nodes) { 841 contentView.getChildren().removeAll(nodes); 842 } 843 844 /** {@inheritDoc} */ 845 @Override public Point2D getMenuPosition() { 846 contentView.layoutChildren(); 847 Point2D p = super.getMenuPosition(); 848 if (p != null) { 849 p = new Point2D(Math.max(0, p.getX() - contentView.snappedLeftInset() - getSkinnable().getScrollLeft()), 850 Math.max(0, p.getY() - contentView.snappedTopInset() - getSkinnable().getScrollTop())); 851 } 852 return p; 853 } 854 855 // Public for FXVKSkin 856 /** 857 * @return the {@code Bounds} of the caret shape, relative to the {@code TextArea}. 858 */ 859 public Bounds getCaretBounds() { 860 return getSkinnable().sceneToLocal(caretPath.localToScene(caretPath.getBoundsInLocal())); 861 } 862 863 /** {@inheritDoc} */ 864 @Override protected Object queryAccessibleAttribute(AccessibleAttribute attribute, Object... parameters) { 865 switch (attribute) { 866 case LINE_FOR_OFFSET: 867 case LINE_START: 868 case LINE_END: 869 case BOUNDS_FOR_RANGE: 870 case OFFSET_AT_POINT: 871 Text text = getTextNode(); 872 return text.queryAccessibleAttribute(attribute, parameters); 873 default: return super.queryAccessibleAttribute(attribute, parameters); 874 } 875 } 876 877 /** {@inheritDoc} */ 878 @Override public void dispose() { 879 super.dispose(); 880 881 if (behavior != null) { 882 behavior.dispose(); 883 } 884 885 // TODO Unregister listeners on text editor, paragraph list 886 throw new UnsupportedOperationException(); 887 } 888 889 /** {@inheritDoc} */ 890 @Override public double computeBaselineOffset(double topInset, double rightInset, double bottomInset, double leftInset) { 891 Text firstParagraph = (Text) paragraphNodes.getChildren().get(0); 892 return Utils.getAscent(getSkinnable().getFont(), firstParagraph.getBoundsType()) 893 + contentView.snappedTopInset() + textArea.snappedTopInset(); 894 } 895 896 private char getCharacter(int index) { 897 int n = paragraphNodes.getChildren().size(); 898 899 int paragraphIndex = 0; 900 int offset = index; 901 902 String paragraph = null; 903 while (paragraphIndex < n) { 904 Text paragraphNode = (Text)paragraphNodes.getChildren().get(paragraphIndex); 905 paragraph = paragraphNode.getText(); 906 int count = paragraph.length() + 1; 907 908 if (offset < count) { 909 break; 910 } 911 912 offset -= count; 913 paragraphIndex++; 914 } 915 916 return offset == paragraph.length() ? '\n' : paragraph.charAt(offset); 917 } 918 919 /** {@inheritDoc} */ 920 @Override protected int getInsertionPoint(double x, double y) { 921 TextArea textArea = getSkinnable(); 922 923 int n = paragraphNodes.getChildren().size(); 924 int index = -1; 925 926 if (n > 0) { 927 if (y < contentView.snappedTopInset()) { 928 // Select the character at x in the first row 929 Text paragraphNode = (Text)paragraphNodes.getChildren().get(0); 930 index = getNextInsertionPoint(paragraphNode, x, -1, VerticalDirection.DOWN); 931 } else if (y > contentView.snappedTopInset() + contentView.getHeight()) { 932 // Select the character at x in the last row 933 int lastParagraphIndex = n - 1; 934 Text lastParagraphView = (Text)paragraphNodes.getChildren().get(lastParagraphIndex); 935 936 index = getNextInsertionPoint(lastParagraphView, x, -1, VerticalDirection.UP) 937 + (textArea.getLength() - lastParagraphView.getText().length()); 938 } else { 939 // Select the character at x in the row at y 940 int paragraphOffset = 0; 941 for (int i = 0; i < n; i++) { 942 Text paragraphNode = (Text)paragraphNodes.getChildren().get(i); 943 944 Bounds bounds = paragraphNode.getBoundsInLocal(); 945 double paragraphViewY = paragraphNode.getLayoutY() + bounds.getMinY(); 946 if (y >= paragraphViewY 947 && y < paragraphViewY + paragraphNode.getBoundsInLocal().getHeight()) { 948 index = getInsertionPoint(paragraphNode, 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 1050 /** {@inheritDoc} */ 1051 @Override protected void scrollCharacterToVisible(final int index) { 1052 // TODO We queue a callback because when characters are added or 1053 // removed the bounds are not immediately updated; is this really 1054 // necessary? 1055 1056 Platform.runLater(() -> { 1057 if (getSkinnable().getLength() == 0) { 1058 return; 1059 } 1060 Rectangle2D characterBounds = getCharacterBounds(index); 1061 scrollBoundsToVisible(characterBounds); 1062 }); 1063 } 1064 1065 1066 1067 /************************************************************************** 1068 * 1069 * Private implementation 1070 * 1071 **************************************************************************/ 1072 1073 TextAreaBehavior getBehavior() { 1074 return behavior; 1075 } 1076 1077 private void createPromptNode() { 1078 if (promptNode == null && usePromptText.get()) { 1079 promptNode = new Text(); 1080 contentView.getChildren().add(0, promptNode); 1081 promptNode.setManaged(false); 1082 promptNode.getStyleClass().add("text"); 1083 promptNode.visibleProperty().bind(usePromptText); 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()) { 1138 x -= selectionHandle1.getWidth() / 2; 1139 y -= selectionHandle1.getHeight(); 1140 w += selectionHandle1.getWidth() / 2 + selectionHandle2.getWidth() / 2; 1141 h += selectionHandle1.getHeight() + selectionHandle2.getHeight(); 1142 } 1143 } 1144 1145 if (w > 0 && h > 0) { 1146 scrollBoundsToVisible(new Rectangle2D(x, y, w, h)); 1147 } 1148 } 1149 1150 private void scrollBoundsToVisible(Rectangle2D bounds) { 1151 TextArea textArea = getSkinnable(); 1152 Bounds viewportBounds = scrollPane.getViewportBounds(); 1153 1154 double viewportWidth = viewportBounds.getWidth(); 1155 double viewportHeight = viewportBounds.getHeight(); 1156 double scrollTop = textArea.getScrollTop(); 1157 double scrollLeft = textArea.getScrollLeft(); 1158 double slop = 6.0; 1159 1160 if (bounds.getMinY() < 0) { 1161 double y = scrollTop + bounds.getMinY(); 1162 if (y <= contentView.snappedTopInset()) { 1163 y = 0; 1164 } 1165 textArea.setScrollTop(y); 1166 } else if (contentView.snappedTopInset() + bounds.getMaxY() > viewportHeight) { 1167 double y = scrollTop + contentView.snappedTopInset() + bounds.getMaxY() - viewportHeight; 1168 if (y >= getScrollTopMax() - contentView.snappedBottomInset()) { 1169 y = getScrollTopMax(); 1170 } 1171 textArea.setScrollTop(y); 1172 } 1173 1174 1175 if (bounds.getMinX() < 0) { 1176 double x = scrollLeft + bounds.getMinX() - slop; 1177 if (x <= contentView.snappedLeftInset() + slop) { 1178 x = 0; 1179 } 1180 textArea.setScrollLeft(x); 1181 } else if (contentView.snappedLeftInset() + bounds.getMaxX() > viewportWidth) { 1182 double x = scrollLeft + contentView.snappedLeftInset() + bounds.getMaxX() - viewportWidth + slop; 1183 if (x >= getScrollLeftMax() - contentView.snappedRightInset() - slop) { 1184 x = getScrollLeftMax(); 1185 } 1186 textArea.setScrollLeft(x); 1187 } 1188 } 1189 1190 private void updatePrefViewportWidth() { 1191 int columnCount = getSkinnable().getPrefColumnCount(); 1192 scrollPane.setPrefViewportWidth(columnCount * characterWidth + contentView.snappedLeftInset() + contentView.snappedRightInset()); 1193 scrollPane.setMinViewportWidth(characterWidth + contentView.snappedLeftInset() + contentView.snappedRightInset()); 1194 } 1195 1196 private void updatePrefViewportHeight() { 1197 int rowCount = getSkinnable().getPrefRowCount(); 1198 scrollPane.setPrefViewportHeight(rowCount * lineHeight + contentView.snappedTopInset() + contentView.snappedBottomInset()); 1199 scrollPane.setMinViewportHeight(lineHeight + contentView.snappedTopInset() + contentView.snappedBottomInset()); 1200 } 1201 1202 private void updateFontMetrics() { 1203 Text firstParagraph = (Text)paragraphNodes.getChildren().get(0); 1204 lineHeight = Utils.getLineHeight(getSkinnable().getFont(), firstParagraph.getBoundsType()); 1205 characterWidth = fontMetrics.get().computeStringWidth("W"); 1206 } 1207 1208 private double getTextTranslateX() { 1209 return contentView.snappedLeftInset(); 1210 } 1211 1212 private double getTextTranslateY() { 1213 return contentView.snappedTopInset(); 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 -> { 1259 behavior.mouseReleased(event); 1260 event.consume(); 1261 }); 1262 1263 addEventHandler(MouseEvent.MOUSE_DRAGGED, event -> { 1264 behavior.mouseDragged(event); 1265 event.consume(); 1266 }); 1267 } 1268 1269 @Override protected ObservableList<Node> getChildren() { 1270 return super.getChildren(); 1271 } 1272 1273 @Override public Orientation getContentBias() { 1274 return Orientation.HORIZONTAL; 1275 } 1276 1277 @Override protected double computePrefWidth(double height) { 1278 if (computedPrefWidth < 0) { 1279 double prefWidth = 0; 1280 1281 for (Node node : paragraphNodes.getChildren()) { 1282 Text paragraphNode = (Text)node; 1283 prefWidth = Math.max(prefWidth, 1284 Utils.computeTextWidth(paragraphNode.getFont(), 1285 paragraphNode.getText(), 0)); 1286 } 1287 1288 prefWidth += snappedLeftInset() + snappedRightInset(); 1289 1290 Bounds viewPortBounds = scrollPane.getViewportBounds(); 1291 computedPrefWidth = Math.max(prefWidth, (viewPortBounds != null) ? viewPortBounds.getWidth() : 0); 1292 } 1293 return computedPrefWidth; 1294 } 1295 1296 @Override protected double computePrefHeight(double width) { 1297 if (width != widthForComputedPrefHeight) { 1298 invalidateMetrics(); 1299 widthForComputedPrefHeight = width; 1300 } 1301 1302 if (computedPrefHeight < 0) { 1303 double wrappingWidth; 1304 if (width == -1) { 1305 wrappingWidth = 0; 1306 } else { 1307 wrappingWidth = Math.max(width - (snappedLeftInset() + snappedRightInset()), 0); 1308 } 1309 1310 double prefHeight = 0; 1311 1312 for (Node node : paragraphNodes.getChildren()) { 1313 Text paragraphNode = (Text)node; 1314 prefHeight += Utils.computeTextHeight( 1315 paragraphNode.getFont(), 1316 paragraphNode.getText(), 1317 wrappingWidth, 1318 paragraphNode.getBoundsType()); 1319 } 1320 1321 prefHeight += snappedTopInset() + snappedBottomInset(); 1322 1323 Bounds viewPortBounds = scrollPane.getViewportBounds(); 1324 computedPrefHeight = Math.max(prefHeight, (viewPortBounds != null) ? viewPortBounds.getHeight() : 0); 1325 } 1326 return computedPrefHeight; 1327 } 1328 1329 @Override protected double computeMinWidth(double height) { 1330 if (computedMinWidth < 0) { 1331 double hInsets = snappedLeftInset() + snappedRightInset(); 1332 computedMinWidth = Math.min(characterWidth + hInsets, computePrefWidth(height)); 1333 } 1334 return computedMinWidth; 1335 } 1336 1337 @Override protected double computeMinHeight(double width) { 1338 if (computedMinHeight < 0) { 1339 double vInsets = snappedTopInset() + snappedBottomInset(); 1340 computedMinHeight = Math.min(lineHeight + vInsets, computePrefHeight(width)); 1341 } 1342 return computedMinHeight; 1343 } 1344 1345 @Override public void layoutChildren() { 1346 TextArea textArea = getSkinnable(); 1347 double width = getWidth(); 1348 1349 // Lay out paragraphs 1350 final double topPadding = snappedTopInset(); 1351 final double leftPadding = snappedLeftInset(); 1352 1353 double wrappingWidth = Math.max(width - (leftPadding + snappedRightInset()), 0); 1354 1355 double y = topPadding; 1356 1357 final List<Node> paragraphNodesChildren = paragraphNodes.getChildren(); 1358 1359 for (int i = 0; i < paragraphNodesChildren.size(); i++) { 1360 Node node = paragraphNodesChildren.get(i); 1361 Text paragraphNode = (Text)node; 1362 paragraphNode.setWrappingWidth(wrappingWidth); 1363 1364 Bounds bounds = paragraphNode.getBoundsInLocal(); 1365 paragraphNode.setLayoutX(leftPadding); 1366 paragraphNode.setLayoutY(y); 1367 1368 y += bounds.getHeight(); 1369 } 1370 1371 if (promptNode != null) { 1372 promptNode.setLayoutX(leftPadding); 1373 promptNode.setLayoutY(topPadding + promptNode.getBaselineOffset()); 1374 promptNode.setWrappingWidth(wrappingWidth); 1375 } 1376 1377 // Update the selection 1378 IndexRange selection = textArea.getSelection(); 1379 Bounds oldCaretBounds = caretPath.getBoundsInParent(); 1380 1381 selectionHighlightGroup.getChildren().clear(); 1382 1383 int caretPos = textArea.getCaretPosition(); 1384 int anchorPos = textArea.getAnchor(); 1385 1386 if (SHOW_HANDLES) { 1387 // Install and resize the handles for caret and anchor. 1388 if (selection.getLength() > 0) { 1389 selectionHandle1.resize(selectionHandle1.prefWidth(-1), 1390 selectionHandle1.prefHeight(-1)); 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); 1499 caretHandle.setLayoutY(b.getMaxY()); 1500 } 1501 } 1502 1503 if (scrollPane.getPrefViewportWidth() == 0 1504 || scrollPane.getPrefViewportHeight() == 0) { 1505 updatePrefViewportWidth(); 1506 updatePrefViewportHeight(); 1507 if (getParent() != null && scrollPane.getPrefViewportWidth() > 0 1508 || scrollPane.getPrefViewportHeight() > 0) { 1509 // Force layout of viewRect in ScrollPaneSkin 1510 getParent().requestLayout(); 1511 } 1512 } 1513 1514 // RT-36454: Fit to width/height only if smaller than viewport. 1515 // That is, grow to fit but don't shrink to fit. 1516 Bounds viewportBounds = scrollPane.getViewportBounds(); 1517 boolean wasFitToWidth = scrollPane.isFitToWidth(); 1518 boolean wasFitToHeight = scrollPane.isFitToHeight(); 1519 boolean setFitToWidth = textArea.isWrapText() || computePrefWidth(-1) <= viewportBounds.getWidth(); 1520 boolean setFitToHeight = computePrefHeight(width) <= viewportBounds.getHeight(); 1521 if (wasFitToWidth != setFitToWidth || wasFitToHeight != setFitToHeight) { 1522 Platform.runLater(() -> { 1523 scrollPane.setFitToWidth(setFitToWidth); 1524 scrollPane.setFitToHeight(setFitToHeight); 1525 }); 1526 getParent().requestLayout(); 1527 } 1528 } 1529 } 1530 }