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 com.sun.javafx.scene.control.skin; 27 28 import com.sun.javafx.scene.control.behavior.TextAreaBehavior; 29 import com.sun.javafx.scene.text.HitInfo; 30 import javafx.animation.KeyFrame; 31 import javafx.animation.Timeline; 32 import javafx.application.Platform; 33 import javafx.beans.binding.BooleanBinding; 34 import javafx.beans.binding.DoubleBinding; 35 import javafx.beans.binding.IntegerBinding; 36 import javafx.beans.value.ObservableBooleanValue; 37 import javafx.beans.value.ObservableIntegerValue; 38 import javafx.collections.ListChangeListener; 39 import javafx.collections.ObservableList; 40 import javafx.event.ActionEvent; 41 import javafx.event.EventHandler; 42 import javafx.geometry.Bounds; 43 import javafx.geometry.Orientation; 44 import javafx.geometry.Point2D; 45 import javafx.geometry.Rectangle2D; 46 import javafx.geometry.VPos; 47 import javafx.geometry.VerticalDirection; 48 import javafx.scene.AccessibleAttribute; 49 import javafx.scene.Group; 50 import javafx.scene.Node; 51 import javafx.scene.control.IndexRange; 52 import javafx.scene.control.ScrollPane; 53 import javafx.scene.control.TextArea; 54 import javafx.scene.input.MouseEvent; 55 import javafx.scene.input.ScrollEvent; 56 import javafx.scene.layout.Region; 57 import javafx.scene.shape.MoveTo; 58 import javafx.scene.shape.Path; 59 import javafx.scene.shape.PathElement; 60 import javafx.scene.text.Text; 61 import javafx.util.Duration; 62 63 import java.util.List; 64 65 /** 66 * Text area skin. 67 */ 68 public class TextAreaSkin extends TextInputControlSkin<TextArea, TextAreaBehavior> { 69 70 final private TextArea textArea; 71 72 // *** NOTE: Multiple node mode is not yet fully implemented *** // 73 private final boolean USE_MULTIPLE_NODES = false; 74 75 private double computedMinWidth = Double.NEGATIVE_INFINITY; 76 private double computedMinHeight = Double.NEGATIVE_INFINITY; 77 private double computedPrefWidth = Double.NEGATIVE_INFINITY; 78 private double computedPrefHeight = Double.NEGATIVE_INFINITY; 79 private double widthForComputedPrefHeight = Double.NEGATIVE_INFINITY; 80 private double characterWidth; 81 private double lineHeight; 82 83 @Override protected void invalidateMetrics() { 84 computedMinWidth = Double.NEGATIVE_INFINITY; 85 computedMinHeight = Double.NEGATIVE_INFINITY; 86 computedPrefWidth = Double.NEGATIVE_INFINITY; 87 computedPrefHeight = Double.NEGATIVE_INFINITY; 88 } 89 90 private class ContentView extends Region { 91 { 92 getStyleClass().add("content"); 93 94 addEventHandler(MouseEvent.MOUSE_PRESSED, event -> { 95 getBehavior().mousePressed(event); 96 event.consume(); 97 }); 98 99 addEventHandler(MouseEvent.MOUSE_RELEASED, event -> { 100 getBehavior().mouseReleased(event); 101 event.consume(); 102 }); 103 104 addEventHandler(MouseEvent.MOUSE_DRAGGED, event -> { 105 getBehavior().mouseDragged(event); 106 event.consume(); 107 }); 108 } 109 110 @Override protected ObservableList<Node> getChildren() { 111 return super.getChildren(); 112 } 113 114 @Override public Orientation getContentBias() { 115 return Orientation.HORIZONTAL; 116 } 117 118 @Override protected double computePrefWidth(double height) { 119 if (computedPrefWidth < 0) { 120 double prefWidth = 0; 121 122 for (Node node : paragraphNodes.getChildren()) { 123 Text paragraphNode = (Text)node; 124 prefWidth = Math.max(prefWidth, 125 Utils.computeTextWidth(paragraphNode.getFont(), 126 paragraphNode.getText(), 0)); 127 } 128 129 prefWidth += snappedLeftInset() + snappedRightInset(); 130 131 Bounds viewPortBounds = scrollPane.getViewportBounds(); 132 computedPrefWidth = Math.max(prefWidth, (viewPortBounds != null) ? viewPortBounds.getWidth() : 0); 133 } 134 return computedPrefWidth; 135 } 136 137 @Override 138 protected double computePrefHeight(double width) { 139 if (width != widthForComputedPrefHeight) { 140 invalidateMetrics(); 141 widthForComputedPrefHeight = width; 142 } 143 144 if (computedPrefHeight < 0) { 145 double wrappingWidth; 146 if (width == -1) { 147 wrappingWidth = 0; 148 } else { 149 wrappingWidth = Math.max(width - (snappedLeftInset() + snappedRightInset()), 0); 150 } 151 152 double prefHeight = 0; 153 154 for (Node node : paragraphNodes.getChildren()) { 155 Text paragraphNode = (Text)node; 156 prefHeight += Utils.computeTextHeight( 157 paragraphNode.getFont(), 158 paragraphNode.getText(), 159 wrappingWidth, 160 paragraphNode.getBoundsType()); 161 } 162 163 prefHeight += snappedTopInset() + snappedBottomInset(); 164 165 Bounds viewPortBounds = scrollPane.getViewportBounds(); 166 computedPrefHeight = Math.max(prefHeight, (viewPortBounds != null) ? viewPortBounds.getHeight() : 0); 167 } 168 return computedPrefHeight; 169 } 170 171 @Override protected double computeMinWidth(double height) { 172 if (computedMinWidth < 0) { 173 double hInsets = snappedLeftInset() + snappedRightInset(); 174 computedMinWidth = Math.min(characterWidth + hInsets, computePrefWidth(height)); 175 } 176 return computedMinWidth; 177 } 178 179 @Override protected double computeMinHeight(double width) { 180 if (computedMinHeight < 0) { 181 double vInsets = snappedTopInset() + snappedBottomInset(); 182 computedMinHeight = Math.min(lineHeight + vInsets, computePrefHeight(width)); 183 } 184 return computedMinHeight; 185 } 186 187 @Override 188 public void layoutChildren() { 189 TextArea textArea = getSkinnable(); 190 double width = getWidth(); 191 192 // Lay out paragraphs 193 final double topPadding = snappedTopInset(); 194 final double leftPadding = snappedLeftInset(); 195 196 double wrappingWidth = Math.max(width - (leftPadding + snappedRightInset()), 0); 197 198 double y = topPadding; 199 200 final List<Node> paragraphNodesChildren = paragraphNodes.getChildren(); 201 202 for (int i = 0; i < paragraphNodesChildren.size(); i++) { 203 Node node = paragraphNodesChildren.get(i); 204 Text paragraphNode = (Text)node; 205 paragraphNode.setWrappingWidth(wrappingWidth); 206 207 Bounds bounds = paragraphNode.getBoundsInLocal(); 208 paragraphNode.setLayoutX(leftPadding); 209 paragraphNode.setLayoutY(y); 210 211 y += bounds.getHeight(); 212 } 213 214 if (promptNode != null) { 215 promptNode.setLayoutX(leftPadding); 216 promptNode.setLayoutY(topPadding + promptNode.getBaselineOffset()); 217 promptNode.setWrappingWidth(wrappingWidth); 218 } 219 220 // Update the selection 221 IndexRange selection = textArea.getSelection(); 222 Bounds oldCaretBounds = caretPath.getBoundsInParent(); 223 224 selectionHighlightGroup.getChildren().clear(); 225 226 int caretPos = textArea.getCaretPosition(); 227 int anchorPos = textArea.getAnchor(); 228 229 if (SHOW_HANDLES) { 230 // Install and resize the handles for caret and anchor. 231 if (selection.getLength() > 0) { 232 selectionHandle1.resize(selectionHandle1.prefWidth(-1), 233 selectionHandle1.prefHeight(-1)); 234 selectionHandle2.resize(selectionHandle2.prefWidth(-1), 235 selectionHandle2.prefHeight(-1)); 236 } else { 237 caretHandle.resize(caretHandle.prefWidth(-1), 238 caretHandle.prefHeight(-1)); 239 } 240 241 // Position the handle for the anchor. This could be handle1 or handle2. 242 // Do this before positioning the actual caret. 243 if (selection.getLength() > 0) { 244 int paragraphIndex = paragraphNodesChildren.size(); 245 int paragraphOffset = textArea.getLength() + 1; 246 Text paragraphNode = null; 247 do { 248 paragraphNode = (Text)paragraphNodesChildren.get(--paragraphIndex); 249 paragraphOffset -= paragraphNode.getText().length() + 1; 250 } while (anchorPos < paragraphOffset); 251 252 updateTextNodeCaretPos(anchorPos - paragraphOffset); 253 caretPath.getElements().clear(); 254 caretPath.getElements().addAll(paragraphNode.getImpl_caretShape()); 255 caretPath.setLayoutX(paragraphNode.getLayoutX()); 256 caretPath.setLayoutY(paragraphNode.getLayoutY()); 257 258 Bounds b = caretPath.getBoundsInParent(); 259 if (caretPos < anchorPos) { 260 selectionHandle2.setLayoutX(b.getMinX() - selectionHandle2.getWidth() / 2); 261 selectionHandle2.setLayoutY(b.getMaxY() - 1); 262 } else { 263 selectionHandle1.setLayoutX(b.getMinX() - selectionHandle1.getWidth() / 2); 264 selectionHandle1.setLayoutY(b.getMinY() - selectionHandle1.getHeight() + 1); 265 } 266 } 267 } 268 269 { 270 // Position caret 271 int paragraphIndex = paragraphNodesChildren.size(); 272 int paragraphOffset = textArea.getLength() + 1; 273 274 Text paragraphNode = null; 275 do { 276 paragraphNode = (Text)paragraphNodesChildren.get(--paragraphIndex); 277 paragraphOffset -= paragraphNode.getText().length() + 1; 278 } while (caretPos < paragraphOffset); 279 280 updateTextNodeCaretPos(caretPos - paragraphOffset); 281 282 caretPath.getElements().clear(); 283 caretPath.getElements().addAll(paragraphNode.getImpl_caretShape()); 284 285 caretPath.setLayoutX(paragraphNode.getLayoutX()); 286 287 // TODO: Remove this temporary workaround for RT-27533 288 paragraphNode.setLayoutX(2 * paragraphNode.getLayoutX() - paragraphNode.getBoundsInParent().getMinX()); 289 290 caretPath.setLayoutY(paragraphNode.getLayoutY()); 291 if (oldCaretBounds == null || !oldCaretBounds.equals(caretPath.getBoundsInParent())) { 292 scrollCaretToVisible(); 293 } 294 } 295 296 // Update selection fg and bg 297 int start = selection.getStart(); 298 int end = selection.getEnd(); 299 for (int i = 0, max = paragraphNodesChildren.size(); i < max; i++) { 300 Node paragraphNode = paragraphNodesChildren.get(i); 301 Text textNode = (Text)paragraphNode; 302 int paragraphLength = textNode.getText().length() + 1; 303 if (end > start && start < paragraphLength) { 304 textNode.setImpl_selectionStart(start); 305 textNode.setImpl_selectionEnd(Math.min(end, paragraphLength)); 306 307 Path selectionHighlightPath = new Path(); 308 selectionHighlightPath.setManaged(false); 309 selectionHighlightPath.setStroke(null); 310 PathElement[] selectionShape = textNode.getImpl_selectionShape(); 311 if (selectionShape != null) { 312 selectionHighlightPath.getElements().addAll(selectionShape); 313 } 314 selectionHighlightGroup.getChildren().add(selectionHighlightPath); 315 selectionHighlightGroup.setVisible(true); 316 selectionHighlightPath.setLayoutX(textNode.getLayoutX()); 317 selectionHighlightPath.setLayoutY(textNode.getLayoutY()); 318 updateHighlightFill(); 319 } else { 320 textNode.setImpl_selectionStart(-1); 321 textNode.setImpl_selectionEnd(-1); 322 selectionHighlightGroup.setVisible(false); 323 } 324 start = Math.max(0, start - paragraphLength); 325 end = Math.max(0, end - paragraphLength); 326 } 327 328 if (SHOW_HANDLES) { 329 // Position handle for the caret. This could be handle1 or handle2 when 330 // a selection is active. 331 Bounds b = caretPath.getBoundsInParent(); 332 if (selection.getLength() > 0) { 333 if (caretPos < anchorPos) { 334 selectionHandle1.setLayoutX(b.getMinX() - selectionHandle1.getWidth() / 2); 335 selectionHandle1.setLayoutY(b.getMinY() - selectionHandle1.getHeight() + 1); 336 } else { 337 selectionHandle2.setLayoutX(b.getMinX() - selectionHandle2.getWidth() / 2); 338 selectionHandle2.setLayoutY(b.getMaxY() - 1); 339 } 340 } else { 341 caretHandle.setLayoutX(b.getMinX() - caretHandle.getWidth() / 2 + 1); 342 caretHandle.setLayoutY(b.getMaxY()); 343 } 344 } 345 346 if (scrollPane.getPrefViewportWidth() == 0 347 || scrollPane.getPrefViewportHeight() == 0) { 348 updatePrefViewportWidth(); 349 updatePrefViewportHeight(); 350 if (getParent() != null && scrollPane.getPrefViewportWidth() > 0 351 || scrollPane.getPrefViewportHeight() > 0) { 352 // Force layout of viewRect in ScrollPaneSkin 353 getParent().requestLayout(); 354 } 355 } 356 357 // RT-36454: Fit to width/height only if smaller than viewport. 358 // That is, grow to fit but don't shrink to fit. 359 Bounds viewportBounds = scrollPane.getViewportBounds(); 360 boolean wasFitToWidth = scrollPane.isFitToWidth(); 361 boolean wasFitToHeight = scrollPane.isFitToHeight(); 362 boolean setFitToWidth = textArea.isWrapText() || computePrefWidth(-1) <= viewportBounds.getWidth(); 363 boolean setFitToHeight = computePrefHeight(width) <= viewportBounds.getHeight(); 364 if (wasFitToWidth != setFitToWidth || wasFitToHeight != setFitToHeight) { 365 Platform.runLater(() -> { 366 scrollPane.setFitToWidth(setFitToWidth); 367 scrollPane.setFitToHeight(setFitToHeight); 368 }); 369 getParent().requestLayout(); 370 } 371 } 372 } 373 374 private ContentView contentView = new ContentView(); 375 private Group paragraphNodes = new Group(); 376 377 private Text promptNode; 378 private ObservableBooleanValue usePromptText; 379 380 private ObservableIntegerValue caretPosition; 381 private Group selectionHighlightGroup = new Group(); 382 383 private ScrollPane scrollPane; 384 private Bounds oldViewportBounds; 385 386 private VerticalDirection scrollDirection = null; 387 388 private Path characterBoundingPath = new Path(); 389 390 private Timeline scrollSelectionTimeline = new Timeline(); 391 private EventHandler<ActionEvent> scrollSelectionHandler = event -> { 392 switch (scrollDirection) { 393 case UP: { 394 // TODO Get previous offset 395 break; 396 } 397 398 case DOWN: { 399 // TODO Get next offset 400 break; 401 } 402 } 403 }; 404 405 public static final int SCROLL_RATE = 30; 406 407 private double pressX, pressY; // For dragging handles on embedded 408 private boolean handlePressed; 409 410 public TextAreaSkin(final TextArea textArea) { 411 super(textArea, new TextAreaBehavior(textArea)); 412 getBehavior().setTextAreaSkin(this); 413 this.textArea = textArea; 414 415 caretPosition = new IntegerBinding() { 416 { bind(textArea.caretPositionProperty()); } 417 @Override protected int computeValue() { 418 return textArea.getCaretPosition(); 419 } 420 }; 421 caretPosition.addListener((observable, oldValue, newValue) -> { 422 targetCaretX = -1; 423 if (newValue.intValue() > oldValue.intValue()) { 424 setForwardBias(true); 425 } 426 }); 427 428 forwardBiasProperty().addListener(observable -> { 429 if (textArea.getWidth() > 0) { 430 updateTextNodeCaretPos(textArea.getCaretPosition()); 431 } 432 }); 433 434 // setManaged(false); 435 436 // Initialize content 437 scrollPane = new ScrollPane(); 438 scrollPane.setFitToWidth(textArea.isWrapText()); 439 scrollPane.setContent(contentView); 440 getChildren().add(scrollPane); 441 442 getSkinnable().addEventFilter(ScrollEvent.ANY, event -> { 443 if (event.isDirect() && handlePressed) { 444 event.consume(); 445 } 446 }); 447 448 // Add selection 449 selectionHighlightGroup.setManaged(false); 450 selectionHighlightGroup.setVisible(false); 451 contentView.getChildren().add(selectionHighlightGroup); 452 453 // Add content view 454 paragraphNodes.setManaged(false); 455 contentView.getChildren().add(paragraphNodes); 456 457 // Add caret 458 caretPath.setManaged(false); 459 caretPath.setStrokeWidth(1); 460 caretPath.fillProperty().bind(textFill); 461 caretPath.strokeProperty().bind(textFill); 462 // modifying visibility of the caret forces a layout-pass (RT-32373), so 463 // instead we modify the opacity. 464 caretPath.opacityProperty().bind(new DoubleBinding() { 465 { bind(caretVisible); } 466 @Override protected double computeValue() { 467 return caretVisible.get() ? 1.0 : 0.0; 468 } 469 }); 470 contentView.getChildren().add(caretPath); 471 472 if (SHOW_HANDLES) { 473 contentView.getChildren().addAll(caretHandle, selectionHandle1, selectionHandle2); 474 } 475 476 scrollPane.hvalueProperty().addListener((observable, oldValue, newValue) -> { 477 getSkinnable().setScrollLeft(newValue.doubleValue() * getScrollLeftMax()); 478 }); 479 480 scrollPane.vvalueProperty().addListener((observable, oldValue, newValue) -> { 481 getSkinnable().setScrollTop(newValue.doubleValue() * getScrollTopMax()); 482 }); 483 484 // Initialize the scroll selection timeline 485 scrollSelectionTimeline.setCycleCount(Timeline.INDEFINITE); 486 List<KeyFrame> scrollSelectionFrames = scrollSelectionTimeline.getKeyFrames(); 487 scrollSelectionFrames.clear(); 488 scrollSelectionFrames.add(new KeyFrame(Duration.millis(350), scrollSelectionHandler)); 489 490 // Add initial text content 491 for (int i = 0, n = USE_MULTIPLE_NODES ? textArea.getParagraphs().size() : 1; i < n; i++) { 492 CharSequence paragraph = (n == 1) ? textArea.textProperty().getValueSafe() : textArea.getParagraphs().get(i); 493 addParagraphNode(i, paragraph.toString()); 494 } 495 496 textArea.selectionProperty().addListener((observable, oldValue, newValue) -> { 497 // TODO Why do we need two calls here? 498 textArea.requestLayout(); 499 contentView.requestLayout(); 500 }); 501 502 textArea.wrapTextProperty().addListener((observable, oldValue, newValue) -> { 503 invalidateMetrics(); 504 scrollPane.setFitToWidth(newValue); 505 }); 506 507 textArea.prefColumnCountProperty().addListener((observable, oldValue, newValue) -> { 508 invalidateMetrics(); 509 updatePrefViewportWidth(); 510 }); 511 512 textArea.prefRowCountProperty().addListener((observable, oldValue, newValue) -> { 513 invalidateMetrics(); 514 updatePrefViewportHeight(); 515 }); 516 517 updateFontMetrics(); 518 fontMetrics.addListener(valueModel -> { 519 updateFontMetrics(); 520 }); 521 522 contentView.paddingProperty().addListener(valueModel -> { 523 updatePrefViewportWidth(); 524 updatePrefViewportHeight(); 525 }); 526 527 scrollPane.viewportBoundsProperty().addListener(valueModel -> { 528 if (scrollPane.getViewportBounds() != null) { 529 // ScrollPane creates a new Bounds instance for each 530 // layout pass, so we need to check if the width/height 531 // have really changed to avoid infinite layout requests. 532 Bounds newViewportBounds = scrollPane.getViewportBounds(); 533 if (oldViewportBounds == null || 534 oldViewportBounds.getWidth() != newViewportBounds.getWidth() || 535 oldViewportBounds.getHeight() != newViewportBounds.getHeight()) { 536 537 invalidateMetrics(); 538 oldViewportBounds = newViewportBounds; 539 contentView.requestLayout(); 540 } 541 } 542 }); 543 544 textArea.scrollTopProperty().addListener((observable, oldValue, newValue) -> { 545 double vValue = (newValue.doubleValue() < getScrollTopMax()) 546 ? (newValue.doubleValue() / getScrollTopMax()) : 1.0; 547 scrollPane.setVvalue(vValue); 548 }); 549 550 textArea.scrollLeftProperty().addListener((observable, oldValue, newValue) -> { 551 double hValue = (newValue.doubleValue() < getScrollLeftMax()) 552 ? (newValue.doubleValue() / getScrollLeftMax()) : 1.0; 553 scrollPane.setHvalue(hValue); 554 }); 555 556 if (USE_MULTIPLE_NODES) { 557 textArea.getParagraphs().addListener((ListChangeListener.Change<? extends CharSequence> change) -> { 558 while (change.next()) { 559 int from = change.getFrom(); 560 int to = change.getTo(); 561 List<? extends CharSequence> removed = change.getRemoved(); 562 if (from < to) { 563 564 if (removed.isEmpty()) { 565 // This is an add 566 for (int i = from, n = to; i < n; i++) { 567 addParagraphNode(i, change.getList().get(i).toString()); 568 } 569 } else { 570 // This is an update 571 for (int i = from, n = to; i < n; i++) { 572 Node node = paragraphNodes.getChildren().get(i); 573 Text paragraphNode = (Text) node; 574 paragraphNode.setText(change.getList().get(i).toString()); 575 } 576 } 577 } else { 578 // This is a remove 579 paragraphNodes.getChildren().subList(from, from + removed.size()).clear(); 580 } 581 } 582 }); 583 } else { 584 textArea.textProperty().addListener(observable -> { 585 invalidateMetrics(); 586 ((Text)paragraphNodes.getChildren().get(0)).setText(textArea.textProperty().getValueSafe()); 587 contentView.requestLayout(); 588 }); 589 } 590 591 usePromptText = new BooleanBinding() { 592 { bind(textArea.textProperty(), textArea.promptTextProperty()); } 593 @Override protected boolean computeValue() { 594 String txt = textArea.getText(); 595 String promptTxt = textArea.getPromptText(); 596 return ((txt == null || txt.isEmpty()) && 597 promptTxt != null && !promptTxt.isEmpty()); 598 } 599 }; 600 601 if (usePromptText.get()) { 602 createPromptNode(); 603 } 604 605 usePromptText.addListener(observable -> { 606 createPromptNode(); 607 textArea.requestLayout(); 608 }); 609 610 updateHighlightFill(); 611 updatePrefViewportWidth(); 612 updatePrefViewportHeight(); 613 if (textArea.isFocused()) setCaretAnimating(true); 614 615 if (SHOW_HANDLES) { 616 selectionHandle1.setRotate(180); 617 618 EventHandler<MouseEvent> handlePressHandler = e -> { 619 pressX = e.getX(); 620 pressY = e.getY(); 621 handlePressed = true; 622 e.consume(); 623 }; 624 625 EventHandler<MouseEvent> handleReleaseHandler = event -> { 626 handlePressed = false; 627 }; 628 629 caretHandle.setOnMousePressed(handlePressHandler); 630 selectionHandle1.setOnMousePressed(handlePressHandler); 631 selectionHandle2.setOnMousePressed(handlePressHandler); 632 633 caretHandle.setOnMouseReleased(handleReleaseHandler); 634 selectionHandle1.setOnMouseReleased(handleReleaseHandler); 635 selectionHandle2.setOnMouseReleased(handleReleaseHandler); 636 637 caretHandle.setOnMouseDragged(e -> { 638 Text textNode = getTextNode(); 639 Point2D tp = textNode.localToScene(0, 0); 640 Point2D p = new Point2D(e.getSceneX() - tp.getX() + 10/*??*/ - pressX + caretHandle.getWidth() / 2, 641 e.getSceneY() - tp.getY() - pressY - 6); 642 HitInfo hit = textNode.impl_hitTestChar(translateCaretPosition(p)); 643 int pos = hit.getCharIndex(); 644 if (pos > 0) { 645 int oldPos = textNode.getImpl_caretPosition(); 646 textNode.setImpl_caretPosition(pos); 647 PathElement element = textNode.getImpl_caretShape()[0]; 648 if (element instanceof MoveTo && ((MoveTo)element).getY() > e.getY() - getTextTranslateY()) { 649 hit.setCharIndex(pos - 1); 650 } 651 textNode.setImpl_caretPosition(oldPos); 652 } 653 positionCaret(hit, false, false); 654 e.consume(); 655 }); 656 657 selectionHandle1.setOnMouseDragged(new EventHandler<MouseEvent>() { 658 @Override public void handle(MouseEvent e) { 659 TextArea textArea = getSkinnable(); 660 Text textNode = getTextNode(); 661 Point2D tp = textNode.localToScene(0, 0); 662 Point2D p = new Point2D(e.getSceneX() - tp.getX() + 10/*??*/ - pressX + selectionHandle1.getWidth() / 2, 663 e.getSceneY() - tp.getY() - pressY + selectionHandle1.getHeight() + 5); 664 HitInfo hit = textNode.impl_hitTestChar(translateCaretPosition(p)); 665 int pos = hit.getCharIndex(); 666 if (textArea.getAnchor() < textArea.getCaretPosition()) { 667 // Swap caret and anchor 668 textArea.selectRange(textArea.getCaretPosition(), textArea.getAnchor()); 669 } 670 if (pos > 0) { 671 if (pos >= textArea.getAnchor()) { 672 pos = textArea.getAnchor(); 673 } 674 int oldPos = textNode.getImpl_caretPosition(); 675 textNode.setImpl_caretPosition(pos); 676 PathElement element = textNode.getImpl_caretShape()[0]; 677 if (element instanceof MoveTo && ((MoveTo)element).getY() > e.getY() - getTextTranslateY()) { 678 hit.setCharIndex(pos - 1); 679 } 680 textNode.setImpl_caretPosition(oldPos); 681 } 682 positionCaret(hit, true, false); 683 e.consume(); 684 } 685 }); 686 687 selectionHandle2.setOnMouseDragged(new EventHandler<MouseEvent>() { 688 @Override public void handle(MouseEvent e) { 689 TextArea textArea = getSkinnable(); 690 Text textNode = getTextNode(); 691 Point2D tp = textNode.localToScene(0, 0); 692 Point2D p = new Point2D(e.getSceneX() - tp.getX() + 10/*??*/ - pressX + selectionHandle2.getWidth() / 2, 693 e.getSceneY() - tp.getY() - pressY - 6); 694 HitInfo hit = textNode.impl_hitTestChar(translateCaretPosition(p)); 695 int pos = hit.getCharIndex(); 696 if (textArea.getAnchor() > textArea.getCaretPosition()) { 697 // Swap caret and anchor 698 textArea.selectRange(textArea.getCaretPosition(), textArea.getAnchor()); 699 } 700 if (pos > 0) { 701 if (pos <= textArea.getAnchor() + 1) { 702 pos = Math.min(textArea.getAnchor() + 2, textArea.getLength()); 703 } 704 int oldPos = textNode.getImpl_caretPosition(); 705 textNode.setImpl_caretPosition(pos); 706 PathElement element = textNode.getImpl_caretShape()[0]; 707 if (element instanceof MoveTo && ((MoveTo)element).getY() > e.getY() - getTextTranslateY()) { 708 hit.setCharIndex(pos - 1); 709 } 710 textNode.setImpl_caretPosition(oldPos); 711 positionCaret(hit, true, false); 712 } 713 e.consume(); 714 } 715 }); 716 } 717 } 718 719 @Override 720 protected void layoutChildren(double contentX, double contentY, double contentWidth, double contentHeight) { 721 scrollPane.resizeRelocate(contentX, contentY, contentWidth, contentHeight); 722 } 723 724 private void createPromptNode() { 725 if (promptNode == null && usePromptText.get()) { 726 promptNode = new Text(); 727 contentView.getChildren().add(0, promptNode); 728 promptNode.setManaged(false); 729 promptNode.getStyleClass().add("text"); 730 promptNode.visibleProperty().bind(usePromptText); 731 promptNode.fontProperty().bind(getSkinnable().fontProperty()); 732 promptNode.textProperty().bind(getSkinnable().promptTextProperty()); 733 promptNode.fillProperty().bind(promptTextFill); 734 } 735 } 736 737 private void addParagraphNode(int i, String string) { 738 final TextArea textArea = getSkinnable(); 739 Text paragraphNode = new Text(string); 740 paragraphNode.setTextOrigin(VPos.TOP); 741 paragraphNode.setManaged(false); 742 paragraphNode.getStyleClass().add("text"); 743 paragraphNode.boundsTypeProperty().addListener((observable, oldValue, newValue) -> { 744 invalidateMetrics(); 745 updateFontMetrics(); 746 }); 747 paragraphNodes.getChildren().add(i, paragraphNode); 748 749 paragraphNode.fontProperty().bind(textArea.fontProperty()); 750 paragraphNode.fillProperty().bind(textFill); 751 paragraphNode.impl_selectionFillProperty().bind(highlightTextFill); 752 } 753 754 @Override 755 public void dispose() { 756 // TODO Unregister listeners on text editor, paragraph list 757 throw new UnsupportedOperationException(); 758 } 759 760 @Override 761 public double computeBaselineOffset(double topInset, double rightInset, double bottomInset, double leftInset) { 762 Text firstParagraph = (Text) paragraphNodes.getChildren().get(0); 763 return Utils.getAscent(getSkinnable().getFont(),firstParagraph.getBoundsType()) 764 + contentView.snappedTopInset() + textArea.snappedTopInset(); 765 } 766 767 @Override 768 public char getCharacter(int index) { 769 int n = paragraphNodes.getChildren().size(); 770 771 int paragraphIndex = 0; 772 int offset = index; 773 774 String paragraph = null; 775 while (paragraphIndex < n) { 776 Text paragraphNode = (Text)paragraphNodes.getChildren().get(paragraphIndex); 777 paragraph = paragraphNode.getText(); 778 int count = paragraph.length() + 1; 779 780 if (offset < count) { 781 break; 782 } 783 784 offset -= count; 785 paragraphIndex++; 786 } 787 788 return offset == paragraph.length() ? '\n' : paragraph.charAt(offset); 789 } 790 791 @Override 792 public int getInsertionPoint(double x, double y) { 793 TextArea textArea = getSkinnable(); 794 795 int n = paragraphNodes.getChildren().size(); 796 int index = -1; 797 798 if (n > 0) { 799 if (y < contentView.snappedTopInset()) { 800 // Select the character at x in the first row 801 Text paragraphNode = (Text)paragraphNodes.getChildren().get(0); 802 index = getNextInsertionPoint(paragraphNode, x, -1, VerticalDirection.DOWN); 803 } else if (y > contentView.snappedTopInset() + contentView.getHeight()) { 804 // Select the character at x in the last row 805 int lastParagraphIndex = n - 1; 806 Text lastParagraphView = (Text)paragraphNodes.getChildren().get(lastParagraphIndex); 807 808 index = getNextInsertionPoint(lastParagraphView, x, -1, VerticalDirection.UP) 809 + (textArea.getLength() - lastParagraphView.getText().length()); 810 } else { 811 // Select the character at x in the row at y 812 int paragraphOffset = 0; 813 for (int i = 0; i < n; i++) { 814 Text paragraphNode = (Text)paragraphNodes.getChildren().get(i); 815 816 Bounds bounds = paragraphNode.getBoundsInLocal(); 817 double paragraphViewY = paragraphNode.getLayoutY() + bounds.getMinY(); 818 if (y >= paragraphViewY 819 && y < paragraphViewY + paragraphNode.getBoundsInLocal().getHeight()) { 820 index = getInsertionPoint(paragraphNode, 821 x - paragraphNode.getLayoutX(), 822 y - paragraphNode.getLayoutY()) + paragraphOffset; 823 break; 824 } 825 826 paragraphOffset += paragraphNode.getText().length() + 1; 827 } 828 } 829 } 830 831 return index; 832 } 833 834 public void positionCaret(HitInfo hit, boolean select, boolean extendSelection) { 835 int pos = Utils.getHitInsertionIndex(hit, getSkinnable().getText()); 836 boolean isNewLine = 837 (pos > 0 && 838 pos <= getSkinnable().getLength() && 839 getSkinnable().getText().codePointAt(pos-1) == 0x0a); 840 841 // special handling for a new line 842 if (!hit.isLeading() && isNewLine) { 843 hit.setLeading(true); 844 pos -= 1; 845 } 846 847 if (select) { 848 if (extendSelection) { 849 getSkinnable().extendSelection(pos); 850 } else { 851 getSkinnable().selectPositionCaret(pos); 852 } 853 } else { 854 getSkinnable().positionCaret(pos); 855 } 856 857 setForwardBias(hit.isLeading()); 858 } 859 860 private double getScrollTopMax() { 861 return Math.max(0, contentView.getHeight() - scrollPane.getViewportBounds().getHeight()); 862 } 863 864 private double getScrollLeftMax() { 865 return Math.max(0, contentView.getWidth() - scrollPane.getViewportBounds().getWidth()); 866 } 867 868 private int getInsertionPoint(Text paragraphNode, double x, double y) { 869 HitInfo hitInfo = paragraphNode.impl_hitTestChar(new Point2D(x, y)); 870 return Utils.getHitInsertionIndex(hitInfo, paragraphNode.getText()); 871 } 872 873 public int getNextInsertionPoint(double x, int from, VerticalDirection scrollDirection) { 874 // TODO 875 return 0; 876 } 877 878 private int getNextInsertionPoint(Text paragraphNode, double x, int from, 879 VerticalDirection scrollDirection) { 880 // TODO 881 return 0; 882 } 883 884 @Override 885 public Rectangle2D getCharacterBounds(int index) { 886 TextArea textArea = getSkinnable(); 887 888 int paragraphIndex = paragraphNodes.getChildren().size(); 889 int paragraphOffset = textArea.getLength() + 1; 890 891 Text paragraphNode = null; 892 do { 893 paragraphNode = (Text)paragraphNodes.getChildren().get(--paragraphIndex); 894 paragraphOffset -= paragraphNode.getText().length() + 1; 895 } while (index < paragraphOffset); 896 897 int characterIndex = index - paragraphOffset; 898 boolean terminator = false; 899 900 if (characterIndex == paragraphNode.getText().length()) { 901 characterIndex--; 902 terminator = true; 903 } 904 905 characterBoundingPath.getElements().clear(); 906 characterBoundingPath.getElements().addAll(paragraphNode.impl_getRangeShape(characterIndex, characterIndex + 1)); 907 characterBoundingPath.setLayoutX(paragraphNode.getLayoutX()); 908 characterBoundingPath.setLayoutY(paragraphNode.getLayoutY()); 909 910 Bounds bounds = characterBoundingPath.getBoundsInLocal(); 911 912 double x = bounds.getMinX() + paragraphNode.getLayoutX() - textArea.getScrollLeft(); 913 double y = bounds.getMinY() + paragraphNode.getLayoutY() - textArea.getScrollTop(); 914 915 // Sometimes the bounds is empty, in which case we must ignore the width/height 916 double width = bounds.isEmpty() ? 0 : bounds.getWidth(); 917 double height = bounds.isEmpty() ? 0 : bounds.getHeight(); 918 919 if (terminator) { 920 x += width; 921 width = 0; 922 } 923 924 return new Rectangle2D(x, y, width, height); 925 } 926 927 @Override public void scrollCharacterToVisible(final int index) { 928 // TODO We queue a callback because when characters are added or 929 // removed the bounds are not immediately updated; is this really 930 // necessary? 931 932 Platform.runLater(() -> { 933 if (getSkinnable().getLength() == 0) { 934 return; 935 } 936 Rectangle2D characterBounds = getCharacterBounds(index); 937 scrollBoundsToVisible(characterBounds); 938 }); 939 } 940 941 private void scrollCaretToVisible() { 942 TextArea textArea = getSkinnable(); 943 Bounds bounds = caretPath.getLayoutBounds(); 944 double x = bounds.getMinX() - textArea.getScrollLeft(); 945 double y = bounds.getMinY() - textArea.getScrollTop(); 946 double w = bounds.getWidth(); 947 double h = bounds.getHeight(); 948 949 if (SHOW_HANDLES) { 950 if (caretHandle.isVisible()) { 951 h += caretHandle.getHeight(); 952 } else if (selectionHandle1.isVisible() && selectionHandle2.isVisible()) { 953 x -= selectionHandle1.getWidth() / 2; 954 y -= selectionHandle1.getHeight(); 955 w += selectionHandle1.getWidth() / 2 + selectionHandle2.getWidth() / 2; 956 h += selectionHandle1.getHeight() + selectionHandle2.getHeight(); 957 } 958 } 959 960 if (w > 0 && h > 0) { 961 scrollBoundsToVisible(new Rectangle2D(x, y, w, h)); 962 } 963 } 964 965 private void scrollBoundsToVisible(Rectangle2D bounds) { 966 TextArea textArea = getSkinnable(); 967 Bounds viewportBounds = scrollPane.getViewportBounds(); 968 969 double viewportWidth = viewportBounds.getWidth(); 970 double viewportHeight = viewportBounds.getHeight(); 971 double scrollTop = textArea.getScrollTop(); 972 double scrollLeft = textArea.getScrollLeft(); 973 double slop = 6.0; 974 975 if (bounds.getMinY() < 0) { 976 double y = scrollTop + bounds.getMinY(); 977 if (y <= contentView.snappedTopInset()) { 978 y = 0; 979 } 980 textArea.setScrollTop(y); 981 } else if (contentView.snappedTopInset() + bounds.getMaxY() > viewportHeight) { 982 double y = scrollTop + contentView.snappedTopInset() + bounds.getMaxY() - viewportHeight; 983 if (y >= getScrollTopMax() - contentView.snappedBottomInset()) { 984 y = getScrollTopMax(); 985 } 986 textArea.setScrollTop(y); 987 } 988 989 990 if (bounds.getMinX() < 0) { 991 double x = scrollLeft + bounds.getMinX() - slop; 992 if (x <= contentView.snappedLeftInset() + slop) { 993 x = 0; 994 } 995 textArea.setScrollLeft(x); 996 } else if (contentView.snappedLeftInset() + bounds.getMaxX() > viewportWidth) { 997 double x = scrollLeft + contentView.snappedLeftInset() + bounds.getMaxX() - viewportWidth + slop; 998 if (x >= getScrollLeftMax() - contentView.snappedRightInset() - slop) { 999 x = getScrollLeftMax(); 1000 } 1001 textArea.setScrollLeft(x); 1002 } 1003 } 1004 1005 private void updatePrefViewportWidth() { 1006 int columnCount = getSkinnable().getPrefColumnCount(); 1007 scrollPane.setPrefViewportWidth(columnCount * characterWidth + contentView.snappedLeftInset() + contentView.snappedRightInset()); 1008 scrollPane.setMinViewportWidth(characterWidth + contentView.snappedLeftInset() + contentView.snappedRightInset()); 1009 } 1010 1011 private void updatePrefViewportHeight() { 1012 int rowCount = getSkinnable().getPrefRowCount(); 1013 scrollPane.setPrefViewportHeight(rowCount * lineHeight + contentView.snappedTopInset() + contentView.snappedBottomInset()); 1014 scrollPane.setMinViewportHeight(lineHeight + contentView.snappedTopInset() + contentView.snappedBottomInset()); 1015 } 1016 1017 private void updateFontMetrics() { 1018 Text firstParagraph = (Text)paragraphNodes.getChildren().get(0); 1019 lineHeight = Utils.getLineHeight(getSkinnable().getFont(),firstParagraph.getBoundsType()); 1020 characterWidth = fontMetrics.get().computeStringWidth("W"); 1021 } 1022 1023 @Override 1024 protected void updateHighlightFill() { 1025 for (Node node : selectionHighlightGroup.getChildren()) { 1026 Path selectionHighlightPath = (Path)node; 1027 selectionHighlightPath.setFill(highlightFill.get()); 1028 } 1029 } 1030 1031 // protected void handleMouseReleasedEvent(MouseEvent event) { 1032 // // super.handleMouseReleasedEvent(event); 1033 1034 // // Stop the scroll selection timer 1035 // scrollSelectionTimeline.stop(); 1036 // scrollDirection = null; 1037 1038 // // Select all if the user double-clicked 1039 // if (event.getButton() == MouseButton.PRIMARY 1040 // && event.getClickCount() == 3) { 1041 // // TODO Select the current row 1042 // } 1043 // } 1044 1045 // Callbacks from Behavior class 1046 1047 private double getTextTranslateX() { 1048 return contentView.snappedLeftInset(); 1049 } 1050 1051 private double getTextTranslateY() { 1052 return contentView.snappedTopInset(); 1053 } 1054 1055 private double getTextLeft() { 1056 return 0; 1057 } 1058 1059 private Point2D translateCaretPosition(Point2D p) { 1060 return p; 1061 } 1062 1063 private Text getTextNode() { 1064 if (USE_MULTIPLE_NODES) { 1065 throw new IllegalArgumentException("Multiple node traversal is not yet implemented."); 1066 } 1067 return (Text)paragraphNodes.getChildren().get(0); 1068 } 1069 1070 public HitInfo getIndex(double x, double y) { 1071 // adjust the event to be in the same coordinate space as the 1072 // text content of the textInputControl 1073 Text textNode = getTextNode(); 1074 Point2D p = new Point2D(x - textNode.getLayoutX(), y - getTextTranslateY()); 1075 HitInfo hit = textNode.impl_hitTestChar(translateCaretPosition(p)); 1076 int pos = hit.getCharIndex(); 1077 if (pos > 0) { 1078 int oldPos = textNode.getImpl_caretPosition(); 1079 textNode.setImpl_caretPosition(pos); 1080 PathElement element = textNode.getImpl_caretShape()[0]; 1081 if (element instanceof MoveTo && ((MoveTo)element).getY() > y - getTextTranslateY()) { 1082 hit.setCharIndex(pos - 1); 1083 } 1084 textNode.setImpl_caretPosition(oldPos); 1085 } 1086 return hit; 1087 }; 1088 1089 /** 1090 * Remembers horizontal position when traversing up / down. 1091 */ 1092 double targetCaretX = -1; 1093 1094 @Override public void nextCharacterVisually(boolean moveRight) { 1095 if (isRTL()) { 1096 // Text node is mirrored. 1097 moveRight = !moveRight; 1098 } 1099 1100 Text textNode = getTextNode(); 1101 Bounds caretBounds = caretPath.getLayoutBounds(); 1102 if (caretPath.getElements().size() == 4) { 1103 // The caret is split 1104 // TODO: Find a better way to get the primary caret position 1105 // instead of depending on the internal implementation. 1106 // See RT-25465. 1107 caretBounds = new Path(caretPath.getElements().get(0), caretPath.getElements().get(1)).getLayoutBounds(); 1108 } 1109 double hitX = moveRight ? caretBounds.getMaxX() : caretBounds.getMinX(); 1110 double hitY = (caretBounds.getMinY() + caretBounds.getMaxY()) / 2; 1111 HitInfo hit = textNode.impl_hitTestChar(new Point2D(hitX, hitY)); 1112 Path charShape = new Path(textNode.impl_getRangeShape(hit.getCharIndex(), hit.getCharIndex() + 1)); 1113 if ((moveRight && charShape.getLayoutBounds().getMaxX() > caretBounds.getMaxX()) || 1114 (!moveRight && charShape.getLayoutBounds().getMinX() < caretBounds.getMinX())) { 1115 hit.setLeading(!hit.isLeading()); 1116 positionCaret(hit, false, false); 1117 } else { 1118 // We're at beginning or end of line. Try moving up / down. 1119 int dot = textArea.getCaretPosition(); 1120 targetCaretX = moveRight ? 0 : Double.MAX_VALUE; 1121 // TODO: Use Bidi sniffing instead of assuming right means forward here? 1122 downLines(moveRight ? 1 : -1, false, false); 1123 targetCaretX = -1; 1124 if (dot == textArea.getCaretPosition()) { 1125 if (moveRight) { 1126 textArea.forward(); 1127 } else { 1128 textArea.backward(); 1129 } 1130 } 1131 } 1132 } 1133 1134 /** A shared helper object, used only by downLines(). */ 1135 private static final Path tmpCaretPath = new Path(); 1136 1137 protected void downLines(int nLines, boolean select, boolean extendSelection) { 1138 Text textNode = getTextNode(); 1139 Bounds caretBounds = caretPath.getLayoutBounds(); 1140 1141 // The middle y coordinate of the the line we want to go to. 1142 double targetLineMidY = (caretBounds.getMinY() + caretBounds.getMaxY()) / 2 + nLines * lineHeight; 1143 if (targetLineMidY < 0) { 1144 targetLineMidY = 0; 1145 } 1146 1147 // The target x for the caret. This may have been set during a 1148 // previous call. 1149 double x = (targetCaretX >= 0) ? targetCaretX : (caretBounds.getMaxX()); 1150 1151 // Find a text position for the target x,y. 1152 HitInfo hit = textNode.impl_hitTestChar(translateCaretPosition(new Point2D(x, targetLineMidY))); 1153 int pos = hit.getCharIndex(); 1154 1155 // Save the old pos temporarily while testing the new one. 1156 int oldPos = textNode.getImpl_caretPosition(); 1157 boolean oldBias = textNode.isImpl_caretBias(); 1158 textNode.setImpl_caretBias(hit.isLeading()); 1159 textNode.setImpl_caretPosition(pos); 1160 tmpCaretPath.getElements().clear(); 1161 tmpCaretPath.getElements().addAll(textNode.getImpl_caretShape()); 1162 tmpCaretPath.setLayoutX(textNode.getLayoutX()); 1163 tmpCaretPath.setLayoutY(textNode.getLayoutY()); 1164 Bounds tmpCaretBounds = tmpCaretPath.getLayoutBounds(); 1165 // The y for the middle of the row we found. 1166 double foundLineMidY = (tmpCaretBounds.getMinY() + tmpCaretBounds.getMaxY()) / 2; 1167 textNode.setImpl_caretBias(oldBias); 1168 textNode.setImpl_caretPosition(oldPos); 1169 1170 if (pos > 0) { 1171 if (nLines > 0 && foundLineMidY > targetLineMidY) { 1172 // We went too far and ended up after a newline. 1173 hit.setCharIndex(pos - 1); 1174 } 1175 1176 if (pos >= textArea.getLength() && getCharacter(pos - 1) == '\n') { 1177 // Special case for newline at end of text. 1178 hit.setLeading(true); 1179 } 1180 } 1181 1182 // Test if the found line is in the correct direction and move 1183 // the caret. 1184 if (nLines == 0 || 1185 (nLines > 0 && foundLineMidY > caretBounds.getMaxY()) || 1186 (nLines < 0 && foundLineMidY < caretBounds.getMinY())) { 1187 1188 positionCaret(hit, select, extendSelection); 1189 targetCaretX = x; 1190 } 1191 } 1192 1193 public void previousLine(boolean select) { 1194 downLines(-1, select, false); 1195 } 1196 1197 public void nextLine(boolean select) { 1198 downLines(1, select, false); 1199 } 1200 1201 public void previousPage(boolean select) { 1202 downLines(-(int)(scrollPane.getViewportBounds().getHeight() / lineHeight), 1203 select, false); 1204 } 1205 1206 public void nextPage(boolean select) { 1207 downLines((int)(scrollPane.getViewportBounds().getHeight() / lineHeight), 1208 select, false); 1209 } 1210 1211 public void lineStart(boolean select, boolean extendSelection) { 1212 targetCaretX = 0; 1213 downLines(0, select, extendSelection); 1214 targetCaretX = -1; 1215 } 1216 1217 public void lineEnd(boolean select, boolean extendSelection) { 1218 targetCaretX = Double.MAX_VALUE; 1219 downLines(0, select, extendSelection); 1220 targetCaretX = -1; 1221 } 1222 1223 1224 public void paragraphStart(boolean previousIfAtStart, boolean select) { 1225 TextArea textArea = getSkinnable(); 1226 String text = textArea.textProperty().getValueSafe(); 1227 int pos = textArea.getCaretPosition(); 1228 1229 if (pos > 0) { 1230 if (previousIfAtStart && text.codePointAt(pos-1) == 0x0a) { 1231 // We are at the beginning of a paragraph. 1232 // Back up to the previous paragraph. 1233 pos--; 1234 } 1235 // Back up to the beginning of this paragraph 1236 while (pos > 0 && text.codePointAt(pos-1) != 0x0a) { 1237 pos--; 1238 } 1239 if (select) { 1240 textArea.selectPositionCaret(pos); 1241 } else { 1242 textArea.positionCaret(pos); 1243 } 1244 } 1245 } 1246 1247 public void paragraphEnd(boolean goPastInitialNewline, boolean goPastTrailingNewline, boolean select) { 1248 TextArea textArea = getSkinnable(); 1249 String text = textArea.textProperty().getValueSafe(); 1250 int pos = textArea.getCaretPosition(); 1251 int len = text.length(); 1252 boolean wentPastInitialNewline = false; 1253 1254 if (pos < len) { 1255 if (goPastInitialNewline && text.codePointAt(pos) == 0x0a) { 1256 // We are at the end of a paragraph, start by moving to the 1257 // next paragraph. 1258 pos++; 1259 wentPastInitialNewline = true; 1260 } 1261 if (!(goPastTrailingNewline && wentPastInitialNewline)) { 1262 // Go to the end of this paragraph 1263 while (pos < len && text.codePointAt(pos) != 0x0a) { 1264 pos++; 1265 } 1266 if (goPastTrailingNewline && pos < len) { 1267 // We are at the end of a paragraph, finish by moving to 1268 // the beginning of the next paragraph (Windows behavior). 1269 pos++; 1270 } 1271 } 1272 if (select) { 1273 textArea.selectPositionCaret(pos); 1274 } else { 1275 textArea.positionCaret(pos); 1276 } 1277 } 1278 } 1279 1280 private void updateTextNodeCaretPos(int pos) { 1281 Text textNode = getTextNode(); 1282 if (isForwardBias()) { 1283 textNode.setImpl_caretPosition(pos); 1284 } else { 1285 textNode.setImpl_caretPosition(pos - 1); 1286 } 1287 textNode.impl_caretBiasProperty().set(isForwardBias()); 1288 } 1289 1290 @Override protected PathElement[] getUnderlineShape(int start, int end) { 1291 int pStart = 0; 1292 for (Node node : paragraphNodes.getChildren()) { 1293 Text p = (Text)node; 1294 int pEnd = pStart + p.textProperty().getValueSafe().length(); 1295 if (pEnd >= start) { 1296 return p.impl_getUnderlineShape(start - pStart, end - pStart); 1297 } 1298 pStart = pEnd + 1; 1299 } 1300 return null; 1301 } 1302 1303 @Override protected PathElement[] getRangeShape(int start, int end) { 1304 int pStart = 0; 1305 for (Node node : paragraphNodes.getChildren()) { 1306 Text p = (Text)node; 1307 int pEnd = pStart + p.textProperty().getValueSafe().length(); 1308 if (pEnd >= start) { 1309 return p.impl_getRangeShape(start - pStart, end - pStart); 1310 } 1311 pStart = pEnd + 1; 1312 } 1313 return null; 1314 } 1315 1316 @Override protected void addHighlight(List<? extends Node> nodes, int start) { 1317 int pStart = 0; 1318 Text paragraphNode = null; 1319 for (Node node : paragraphNodes.getChildren()) { 1320 Text p = (Text)node; 1321 int pEnd = pStart + p.textProperty().getValueSafe().length(); 1322 if (pEnd >= start) { 1323 paragraphNode = p; 1324 break; 1325 } 1326 pStart = pEnd + 1; 1327 } 1328 1329 if (paragraphNode != null) { 1330 for (Node node : nodes) { 1331 node.setLayoutX(paragraphNode.getLayoutX()); 1332 node.setLayoutY(paragraphNode.getLayoutY()); 1333 } 1334 } 1335 contentView.getChildren().addAll(nodes); 1336 } 1337 1338 @Override protected void removeHighlight(List<? extends Node> nodes) { 1339 contentView.getChildren().removeAll(nodes); 1340 } 1341 1342 /** 1343 * Use this implementation instead of the one provided on TextInputControl 1344 * Simply calls into TextInputControl.deletePrevious/NextChar and responds appropriately 1345 * based on the return value. 1346 */ 1347 public void deleteChar(boolean previous) { 1348 // final double textMaxXOld = textNode.getBoundsInParent().getMaxX(); 1349 // final double caretMaxXOld = caretPath.getLayoutBounds().getMaxX() + textTranslateX.get(); 1350 final boolean shouldBeep = previous ? 1351 !getSkinnable().deletePreviousChar() : 1352 !getSkinnable().deleteNextChar(); 1353 1354 if (shouldBeep) { 1355 // beep(); 1356 } else { 1357 // scrollAfterDelete(textMaxXOld, caretMaxXOld); 1358 } 1359 } 1360 1361 @Override public Point2D getMenuPosition() { 1362 contentView.layoutChildren(); 1363 Point2D p = super.getMenuPosition(); 1364 if (p != null) { 1365 p = new Point2D(Math.max(0, p.getX() - contentView.snappedLeftInset() - getSkinnable().getScrollLeft()), 1366 Math.max(0, p.getY() - contentView.snappedTopInset() - getSkinnable().getScrollTop())); 1367 } 1368 return p; 1369 } 1370 1371 public Bounds getCaretBounds() { 1372 return getSkinnable().sceneToLocal(caretPath.localToScene(caretPath.getBoundsInLocal())); 1373 } 1374 1375 @Override 1376 protected Object queryAccessibleAttribute(AccessibleAttribute attribute, Object... parameters) { 1377 switch (attribute) { 1378 case LINE_FOR_OFFSET: 1379 case LINE_START: 1380 case LINE_END: 1381 case BOUNDS_FOR_RANGE: 1382 case OFFSET_AT_POINT: 1383 Text text = getTextNode(); 1384 return text.queryAccessibleAttribute(attribute, parameters); 1385 default: return super.queryAccessibleAttribute(attribute, parameters); 1386 } 1387 } 1388 }