1 /* 2 * Copyright (c) 2011, 2014, 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 javafx.beans.binding.BooleanBinding; 29 import javafx.beans.binding.DoubleBinding; 30 import javafx.beans.binding.ObjectBinding; 31 import javafx.beans.binding.StringBinding; 32 import javafx.beans.property.DoubleProperty; 33 import javafx.beans.property.SimpleDoubleProperty; 34 import javafx.beans.value.ObservableBooleanValue; 35 import javafx.beans.value.ObservableDoubleValue; 36 import javafx.event.EventHandler; 37 import javafx.geometry.Bounds; 38 import javafx.geometry.HPos; 39 import javafx.geometry.Point2D; 40 import javafx.geometry.Rectangle2D; 41 import javafx.scene.Group; 42 import javafx.scene.Node; 43 import javafx.scene.accessibility.Attribute; 44 import javafx.scene.control.IndexRange; 45 import javafx.scene.control.PasswordField; 46 import javafx.scene.control.TextField; 47 import javafx.scene.input.MouseEvent; 48 import javafx.scene.layout.Pane; 49 import javafx.scene.paint.Color; 50 import javafx.scene.paint.Paint; 51 import javafx.scene.shape.Path; 52 import javafx.scene.shape.PathElement; 53 import javafx.scene.shape.Rectangle; 54 import javafx.scene.text.Text; 55 import java.util.List; 56 import com.sun.javafx.scene.control.behavior.TextFieldBehavior; 57 import com.sun.javafx.scene.control.behavior.PasswordFieldBehavior; 58 import com.sun.javafx.scene.text.HitInfo; 59 60 /** 61 * Text field skin. 62 */ 63 public class TextFieldSkin extends TextInputControlSkin<TextField, TextFieldBehavior> { 64 /** 65 * This group contains the text, caret, and selection rectangle. 66 * It is clipped. The textNode, selectionHighlightPath, and 67 * caret are each translated individually when horizontal 68 * translation is needed to keep the caretPosition visible. 69 */ 70 private Pane textGroup = new Pane(); 71 private Group handleGroup; 72 73 /** 74 * The clip, applied to the textGroup. This makes sure that any 75 * text / selection wandering off the text box is clipped 76 */ 77 private Rectangle clip = new Rectangle(); 78 /** 79 * The node actually displaying the text. Note that it has the 80 * ability to render both the normal fill as well as the highlight 81 * fill, to perform hit testing, fetching of the selection 82 * highlight, and other such duties. 83 */ 84 private Text textNode = new Text(); 85 /** 86 * 87 * The node used for showing the prompt text. 88 */ 89 private Text promptNode; 90 /** 91 * A path, provided by the textNode, which represents the area 92 * which is selected. The path elements which make up the 93 * selection must be updated whenever the selection changes. We 94 * don't need to keep track of text changes because those will 95 * force the selection to be updated. 96 */ 97 private Path selectionHighlightPath = new Path(); 98 99 private Path characterBoundingPath = new Path(); 100 private ObservableBooleanValue usePromptText; 101 private DoubleProperty textTranslateX = new SimpleDoubleProperty(this, "textTranslateX"); 102 private double caretWidth; 103 104 /** 105 * Function to translate the text control's "dot" into the caret 106 * position in the Text node. This is possibly only meaningful for 107 * the PasswordField where the echoChar could be more than one 108 * character. 109 */ 110 protected int translateCaretPosition(int cp) { return cp; } 111 protected Point2D translateCaretPosition(Point2D p) { return p; } 112 113 /** 114 * Right edge of the text region sans padding 115 */ 116 protected ObservableDoubleValue textRight; 117 118 private double pressX, pressY; // For dragging handles on embedded 119 120 // For use with PasswordField 121 public static final char BULLET = '\u2022'; 122 123 /** 124 * Create a new TextFieldSkin. 125 * @param textField not null 126 */ 127 public TextFieldSkin(final TextField textField) { 128 this(textField, (textField instanceof PasswordField) 129 ? new PasswordFieldBehavior((PasswordField)textField) 130 : new TextFieldBehavior(textField)); 131 } 132 133 public TextFieldSkin(final TextField textField, final TextFieldBehavior behavior) { 134 super(textField, behavior); 135 behavior.setTextFieldSkin(this); 136 137 138 textField.caretPositionProperty().addListener((observable, oldValue, newValue) -> { 139 if (textField.getWidth() > 0) { 140 updateTextNodeCaretPos(textField.getCaretPosition()); 141 if (!isForwardBias()) { 142 setForwardBias(true); 143 } 144 updateCaretOff(); 145 } 146 }); 147 148 forwardBiasProperty().addListener(observable -> { 149 if (textField.getWidth() > 0) { 150 updateTextNodeCaretPos(textField.getCaretPosition()); 151 updateCaretOff(); 152 } 153 }); 154 155 textRight = new DoubleBinding() { 156 { bind(textGroup.widthProperty()); } 157 @Override protected double computeValue() { 158 return textGroup.getWidth(); 159 } 160 }; 161 162 // Once this was crucial for performance, not sure now. 163 clip.setSmooth(false); 164 clip.setX(0); 165 clip.widthProperty().bind(textGroup.widthProperty()); 166 clip.heightProperty().bind(textGroup.heightProperty()); 167 168 // Add content 169 textGroup.setClip(clip); 170 // Hack to defeat the fact that otherwise when the caret blinks the parent group 171 // bounds are completely invalidated and therefore the dirty region is much 172 // larger than necessary. 173 textGroup.getChildren().addAll(selectionHighlightPath, textNode, new Group(caretPath)); 174 getChildren().add(textGroup); 175 if (SHOW_HANDLES) { 176 handleGroup = new Group(); 177 handleGroup.setManaged(false); 178 handleGroup.getChildren().addAll(caretHandle, selectionHandle1, selectionHandle2); 179 getChildren().add(handleGroup); 180 } 181 182 // Add text 183 textNode.setManaged(false); 184 textNode.getStyleClass().add("text"); 185 textNode.fontProperty().bind(textField.fontProperty()); 186 187 textNode.layoutXProperty().bind(textTranslateX); 188 textNode.textProperty().bind(new StringBinding() { 189 { bind(textField.textProperty()); } 190 @Override protected String computeValue() { 191 return maskText(textField.textProperty().getValueSafe()); 192 } 193 }); 194 textNode.fillProperty().bind(textFill); 195 textNode.impl_selectionFillProperty().bind(new ObjectBinding<Paint>() { 196 { bind(highlightTextFill, textFill, textField.focusedProperty()); } 197 @Override protected Paint computeValue() { 198 return textField.isFocused() ? highlightTextFill.get() : textFill.get(); 199 } 200 }); 201 // updated by listener on caretPosition to ensure order 202 updateTextNodeCaretPos(textField.getCaretPosition()); 203 textField.selectionProperty().addListener(observable -> { 204 updateSelection(); 205 }); 206 207 // Add selection 208 selectionHighlightPath.setManaged(false); 209 selectionHighlightPath.setStroke(null); 210 selectionHighlightPath.layoutXProperty().bind(textTranslateX); 211 selectionHighlightPath.visibleProperty().bind(textField.anchorProperty().isNotEqualTo(textField.caretPositionProperty()).and(textField.focusedProperty())); 212 selectionHighlightPath.fillProperty().bind(highlightFill); 213 textNode.impl_selectionShapeProperty().addListener(observable -> { 214 updateSelection(); 215 }); 216 217 // Add caret 218 caretPath.setManaged(false); 219 caretPath.setStrokeWidth(1); 220 caretPath.fillProperty().bind(textFill); 221 caretPath.strokeProperty().bind(textFill); 222 223 // modifying visibility of the caret forces a layout-pass (RT-32373), so 224 // instead we modify the opacity. 225 caretPath.opacityProperty().bind(new DoubleBinding() { 226 { bind(caretVisible); } 227 @Override protected double computeValue() { 228 return caretVisible.get() ? 1.0 : 0.0; 229 } 230 }); 231 caretPath.layoutXProperty().bind(textTranslateX); 232 textNode.impl_caretShapeProperty().addListener(observable -> { 233 caretPath.getElements().setAll(textNode.impl_caretShapeProperty().get()); 234 if (caretPath.getElements().size() == 0) { 235 // The caret pos is invalid. 236 updateTextNodeCaretPos(textField.getCaretPosition()); 237 } else if (caretPath.getElements().size() == 4) { 238 // The caret is split. Ignore and keep the previous width value. 239 } else { 240 caretWidth = Math.round(caretPath.getLayoutBounds().getWidth()); 241 } 242 }); 243 244 // Be sure to get the control to request layout when the font changes, 245 // since this will affect the pref height and pref width. 246 textField.fontProperty().addListener(observable -> { 247 // I do both so that any cached values for prefWidth/height are cleared. 248 // The problem is that the skin is unmanaged and so calling request layout 249 // doesn't walk up the tree all the way. I think.... 250 textField.requestLayout(); 251 getSkinnable().requestLayout(); 252 }); 253 254 registerChangeListener(textField.prefColumnCountProperty(), "prefColumnCount"); 255 if (textField.isFocused()) setCaretAnimating(true); 256 257 textField.alignmentProperty().addListener(observable -> { 258 if (textField.getWidth() > 0) { 259 updateTextPos(); 260 updateCaretOff(); 261 textField.requestLayout(); 262 } 263 }); 264 265 usePromptText = new BooleanBinding() { 266 { bind(textField.textProperty(), 267 textField.promptTextProperty(), 268 promptTextFill); } 269 @Override protected boolean computeValue() { 270 String txt = textField.getText(); 271 String promptTxt = textField.getPromptText(); 272 return ((txt == null || txt.isEmpty()) && 273 promptTxt != null && !promptTxt.isEmpty() && 274 !promptTextFill.get().equals(Color.TRANSPARENT)); 275 } 276 }; 277 278 promptTextFill.addListener(observable -> { 279 updateTextPos(); 280 }); 281 282 textField.textProperty().addListener(observable -> { 283 if (!getBehavior().isEditing()) { 284 // Text changed, but not by user action 285 updateTextPos(); 286 } 287 }); 288 289 if (usePromptText.get()) { 290 createPromptNode(); 291 } 292 293 usePromptText.addListener(observable -> { 294 createPromptNode(); 295 textField.requestLayout(); 296 }); 297 298 if (SHOW_HANDLES) { 299 selectionHandle1.setRotate(180); 300 301 EventHandler<MouseEvent> handlePressHandler = e -> { 302 pressX = e.getX(); 303 pressY = e.getY(); 304 e.consume(); 305 }; 306 307 caretHandle.setOnMousePressed(handlePressHandler); 308 selectionHandle1.setOnMousePressed(handlePressHandler); 309 selectionHandle2.setOnMousePressed(handlePressHandler); 310 311 caretHandle.setOnMouseDragged(e -> { 312 Point2D p = new Point2D(caretHandle.getLayoutX() + e.getX() + pressX - textNode.getLayoutX(), 313 caretHandle.getLayoutY() + e.getY() - pressY - 6); 314 HitInfo hit = textNode.impl_hitTestChar(translateCaretPosition(p)); 315 positionCaret(hit, false); 316 e.consume(); 317 }); 318 319 selectionHandle1.setOnMouseDragged(new EventHandler<MouseEvent>() { 320 @Override public void handle(MouseEvent e) { 321 TextField textField = getSkinnable(); 322 Point2D tp = textNode.localToScene(0, 0); 323 Point2D p = new Point2D(e.getSceneX() - tp.getX() + 10/*??*/ - pressX + selectionHandle1.getWidth() / 2, 324 e.getSceneY() - tp.getY() - pressY - 6); 325 HitInfo hit = textNode.impl_hitTestChar(translateCaretPosition(p)); 326 int pos = hit.getCharIndex(); 327 if (textField.getAnchor() < textField.getCaretPosition()) { 328 // Swap caret and anchor 329 textField.selectRange(textField.getCaretPosition(), textField.getAnchor()); 330 } 331 if (pos >= 0) { 332 if (pos >= textField.getAnchor() - 1) { 333 hit.setCharIndex(Math.max(0, textField.getAnchor() - 1)); 334 } 335 positionCaret(hit, true); 336 } 337 e.consume(); 338 } 339 }); 340 341 selectionHandle2.setOnMouseDragged(new EventHandler<MouseEvent>() { 342 @Override public void handle(MouseEvent e) { 343 TextField textField = getSkinnable(); 344 Point2D tp = textNode.localToScene(0, 0); 345 Point2D p = new Point2D(e.getSceneX() - tp.getX() + 10/*??*/ - pressX + selectionHandle2.getWidth() / 2, 346 e.getSceneY() - tp.getY() - pressY - 6); 347 HitInfo hit = textNode.impl_hitTestChar(translateCaretPosition(p)); 348 int pos = hit.getCharIndex(); 349 if (textField.getAnchor() > textField.getCaretPosition()) { 350 // Swap caret and anchor 351 textField.selectRange(textField.getCaretPosition(), textField.getAnchor()); 352 } 353 if (pos > 0) { 354 if (pos <= textField.getAnchor()) { 355 hit.setCharIndex(Math.min(textField.getAnchor() + 1, textField.getLength())); 356 } 357 positionCaret(hit, true); 358 } 359 e.consume(); 360 } 361 }); 362 } 363 } 364 365 private void updateTextNodeCaretPos(int pos) { 366 if (pos == 0 || isForwardBias()) { 367 textNode.setImpl_caretPosition(pos); 368 } else { 369 textNode.setImpl_caretPosition(pos - 1); 370 } 371 textNode.impl_caretBiasProperty().set(isForwardBias()); 372 } 373 374 private void createPromptNode() { 375 if (promptNode != null || !usePromptText.get()) return; 376 377 promptNode = new Text(); 378 textGroup.getChildren().add(0, promptNode); 379 promptNode.setManaged(false); 380 promptNode.getStyleClass().add("text"); 381 promptNode.visibleProperty().bind(usePromptText); 382 promptNode.fontProperty().bind(getSkinnable().fontProperty()); 383 384 promptNode.textProperty().bind(getSkinnable().promptTextProperty()); 385 promptNode.fillProperty().bind(promptTextFill); 386 updateSelection(); 387 } 388 389 private void updateSelection() { 390 TextField textField = getSkinnable(); 391 IndexRange newValue = textField.getSelection(); 392 393 if (newValue == null || newValue.getLength() == 0) { 394 textNode.impl_selectionStartProperty().set(-1); 395 textNode.impl_selectionEndProperty().set(-1); 396 } else { 397 textNode.impl_selectionStartProperty().set(newValue.getStart()); 398 // This intermediate value is needed to force selection shape layout. 399 textNode.impl_selectionEndProperty().set(newValue.getStart()); 400 textNode.impl_selectionEndProperty().set(newValue.getEnd()); 401 } 402 403 PathElement[] elements = textNode.impl_selectionShapeProperty().get(); 404 if (elements == null) { 405 selectionHighlightPath.getElements().clear(); 406 } else { 407 selectionHighlightPath.getElements().setAll(elements); 408 } 409 410 if (SHOW_HANDLES && newValue != null && newValue.getLength() > 0) { 411 int caretPos = textField.getCaretPosition(); 412 int anchorPos = textField.getAnchor(); 413 414 { 415 // Position the handle for the anchor. This could be handle1 or handle2. 416 // Do this before positioning the handle for the caret. 417 updateTextNodeCaretPos(anchorPos); 418 Bounds b = caretPath.getBoundsInParent(); 419 if (caretPos < anchorPos) { 420 selectionHandle2.setLayoutX(b.getMinX() - selectionHandle2.getWidth() / 2); 421 } else { 422 selectionHandle1.setLayoutX(b.getMinX() - selectionHandle1.getWidth() / 2); 423 } 424 } 425 426 { 427 // Position handle for the caret. This could be handle1 or handle2. 428 updateTextNodeCaretPos(caretPos); 429 Bounds b = caretPath.getBoundsInParent(); 430 if (caretPos < anchorPos) { 431 selectionHandle1.setLayoutX(b.getMinX() - selectionHandle1.getWidth() / 2); 432 } else { 433 selectionHandle2.setLayoutX(b.getMinX() - selectionHandle2.getWidth() / 2); 434 } 435 } 436 } 437 } 438 439 @Override protected void handleControlPropertyChanged(String propertyReference) { 440 if ("prefColumnCount".equals(propertyReference)) { 441 getSkinnable().requestLayout(); 442 } else { 443 super.handleControlPropertyChanged(propertyReference); 444 } 445 } 446 447 @Override 448 protected double computePrefWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) { 449 TextField textField = getSkinnable(); 450 451 double characterWidth = fontMetrics.get().computeStringWidth("W"); 452 453 int columnCount = textField.getPrefColumnCount(); 454 455 return columnCount * characterWidth + leftInset + rightInset; 456 } 457 458 @Override protected double computeMinHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) { 459 return computePrefHeight(width, topInset, rightInset, bottomInset, leftInset); 460 } 461 462 @Override protected double computePrefHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) { 463 return topInset + textNode.getLayoutBounds().getHeight() + bottomInset; 464 } 465 466 @Override protected double computeMaxHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) { 467 return getSkinnable().prefHeight(width); 468 } 469 470 @Override public double computeBaselineOffset(double topInset, double rightInset, double bottomInset, double leftInset) { 471 return topInset + textNode.getBaselineOffset(); 472 } 473 474 /** 475 * Updates the textTranslateX value for the Text node position. This is 476 * done for general layout, but care is taken to avoid resetting the 477 * position when there's a need to scroll the text due to caret movement, 478 * or when editing text that overflows on either side. 479 */ 480 private void updateTextPos() { 481 double oldX = textTranslateX.get(); 482 double newX; 483 double textNodeWidth = textNode.getLayoutBounds().getWidth(); 484 485 switch (getHAlignment()) { 486 case CENTER: 487 double midPoint = textRight.get() / 2; 488 if (usePromptText.get()) { 489 // If a prompt is shown (which implies that the text is 490 // empty), then we align the Text node so that the caret will 491 // appear at the left of the centered prompt. 492 newX = midPoint - promptNode.getLayoutBounds().getWidth() / 2; 493 promptNode.setLayoutX(newX); 494 } else { 495 newX = midPoint - textNodeWidth / 2; 496 } 497 // Update if there is space on the right 498 if (newX + textNodeWidth <= textRight.get()) { 499 textTranslateX.set(newX); 500 } 501 break; 502 503 case RIGHT: 504 newX = textRight.get() - textNodeWidth - caretWidth / 2; 505 // Update if there is space on the right 506 if (newX > oldX || newX > 0) { 507 textTranslateX.set(newX); 508 } 509 if (usePromptText.get()) { 510 promptNode.setLayoutX(textRight.get() - promptNode.getLayoutBounds().getWidth() - 511 caretWidth / 2); 512 } 513 break; 514 515 case LEFT: 516 default: 517 newX = caretWidth / 2; 518 // Update if there is space on either side. 519 if (newX < oldX || newX + textNodeWidth <= textRight.get()) { 520 textTranslateX.set(newX); 521 } 522 if (usePromptText.get()) { 523 promptNode.layoutXProperty().set(newX); 524 } 525 } 526 } 527 528 // should be called when the padding changes, or the text box width, or 529 // the dot moves 530 protected void updateCaretOff() { 531 double delta = 0.0; 532 double caretX = caretPath.getLayoutBounds().getMinX() + textTranslateX.get(); 533 // If the caret position is less than or equal to the left edge of the 534 // clip then the caret will be clipped. We want the caret to end up 535 // being positioned one pixel right of the clip's left edge. The same 536 // applies on the right edge (but going the other direction of course). 537 if (caretX < 0) { 538 // I'll end up with a negative number 539 delta = caretX; 540 } else if (caretX > (textRight.get() - caretWidth)) { 541 // I'll end up with a positive number 542 delta = caretX - (textRight.get() - caretWidth); 543 } 544 545 // If delta is negative, then translate in the negative direction 546 // to cause the text to scroll to the right. Vice-versa for positive. 547 switch (getHAlignment()) { 548 case CENTER: 549 textTranslateX.set(textTranslateX.get() - delta); 550 break; 551 552 case RIGHT: 553 textTranslateX.set(Math.max(textTranslateX.get() - delta, 554 textRight.get() - textNode.getLayoutBounds().getWidth() - 555 caretWidth / 2)); 556 break; 557 558 case LEFT: 559 default: 560 textTranslateX.set(Math.min(textTranslateX.get() - delta, 561 caretWidth / 2)); 562 } 563 if (SHOW_HANDLES) { 564 caretHandle.setLayoutX(caretX - caretHandle.getWidth() / 2 + 1); 565 } 566 } 567 568 /** 569 * Use this implementation instead of the one provided on TextInputControl. 570 * updateCaretOff would get called to position the caret, but the text needs 571 * to be scrolled appropriately. 572 */ 573 public void replaceText(int start, int end, String txt) { 574 final double textMaxXOld = textNode.getBoundsInParent().getMaxX(); 575 final double caretMaxXOld = caretPath.getLayoutBounds().getMaxX() + textTranslateX.get(); 576 getSkinnable().replaceText(start, end, txt); 577 scrollAfterDelete(textMaxXOld, caretMaxXOld); 578 } 579 580 /** 581 * Use this implementation instead of the one provided on TextInputControl 582 * Simply calls into TextInputControl.deletePrevious/NextChar and responds appropriately 583 * based on the return value. 584 */ 585 public void deleteChar(boolean previous) { 586 final double textMaxXOld = textNode.getBoundsInParent().getMaxX(); 587 final double caretMaxXOld = caretPath.getLayoutBounds().getMaxX() + textTranslateX.get(); 588 final boolean shouldBeep = previous ? 589 !getSkinnable().deletePreviousChar() : 590 !getSkinnable().deleteNextChar(); 591 592 if (shouldBeep) { 593 // beep(); 594 } else { 595 scrollAfterDelete(textMaxXOld, caretMaxXOld); 596 } 597 } 598 599 public void scrollAfterDelete(double textMaxXOld, double caretMaxXOld) { 600 final Bounds textLayoutBounds = textNode.getLayoutBounds(); 601 final Bounds textBounds = textNode.localToParent(textLayoutBounds); 602 final Bounds clipBounds = clip.getBoundsInParent(); 603 final Bounds caretBounds = caretPath.getLayoutBounds(); 604 605 switch (getHAlignment()) { 606 case RIGHT: 607 if (textBounds.getMaxX() > clipBounds.getMaxX()) { 608 double delta = caretMaxXOld - caretBounds.getMaxX() - textTranslateX.get(); 609 if (textBounds.getMaxX() + delta < clipBounds.getMaxX()) { 610 if (textMaxXOld <= clipBounds.getMaxX()) { 611 delta = textMaxXOld - textBounds.getMaxX(); 612 } else { 613 delta = clipBounds.getMaxX() - textBounds.getMaxX(); 614 } 615 } 616 textTranslateX.set(textTranslateX.get() + delta); 617 } else { 618 updateTextPos(); 619 } 620 break; 621 622 case LEFT: 623 case CENTER: 624 default: 625 if (textBounds.getMinX() < clipBounds.getMinX() + caretWidth / 2 && 626 textBounds.getMaxX() <= clipBounds.getMaxX()) { 627 double delta = caretMaxXOld - caretBounds.getMaxX() - textTranslateX.get(); 628 if (textBounds.getMaxX() + delta < clipBounds.getMaxX()) { 629 if (textMaxXOld <= clipBounds.getMaxX()) { 630 delta = textMaxXOld - textBounds.getMaxX(); 631 } else { 632 delta = clipBounds.getMaxX() - textBounds.getMaxX(); 633 } 634 } 635 textTranslateX.set(textTranslateX.get() + delta); 636 } 637 } 638 639 updateCaretOff(); 640 } 641 642 public HitInfo getIndex(double x, double y) { 643 // adjust the event to be in the same coordinate space as the 644 // text content of the textInputControl 645 Point2D p; 646 647 p = new Point2D(x - textTranslateX.get() - snappedLeftInset(), 648 y - snappedTopInset()); 649 return textNode.impl_hitTestChar(translateCaretPosition(p)); 650 } 651 652 public void positionCaret(HitInfo hit, boolean select) { 653 TextField textField = getSkinnable(); 654 int pos = Utils.getHitInsertionIndex(hit, textField.textProperty().getValueSafe()); 655 656 if (select) { 657 textField.selectPositionCaret(pos); 658 } else { 659 textField.positionCaret(pos); 660 } 661 662 setForwardBias(hit.isLeading()); 663 } 664 665 @Override public Rectangle2D getCharacterBounds(int index) { 666 double x, y; 667 double width, height; 668 if (index == textNode.getText().length()) { 669 Bounds textNodeBounds = textNode.getBoundsInLocal(); 670 x = textNodeBounds.getMaxX(); 671 y = 0; 672 width = 0; 673 height = textNodeBounds.getMaxY(); 674 } else { 675 characterBoundingPath.getElements().clear(); 676 characterBoundingPath.getElements().addAll(textNode.impl_getRangeShape(index, index + 1)); 677 characterBoundingPath.setLayoutX(textNode.getLayoutX()); 678 characterBoundingPath.setLayoutY(textNode.getLayoutY()); 679 680 Bounds bounds = characterBoundingPath.getBoundsInLocal(); 681 682 x = bounds.getMinX(); 683 y = bounds.getMinY(); 684 // Sometimes the bounds is empty, in which case we must ignore the width/height 685 width = bounds.isEmpty() ? 0 : bounds.getWidth(); 686 height = bounds.isEmpty() ? 0 : bounds.getHeight(); 687 } 688 689 Bounds textBounds = textGroup.getBoundsInParent(); 690 691 return new Rectangle2D(x + textBounds.getMinX() + textTranslateX.get(), 692 y + textBounds.getMinY(), width, height); 693 } 694 695 @Override protected PathElement[] getUnderlineShape(int start, int end) { 696 return textNode.impl_getUnderlineShape(start, end); 697 } 698 699 @Override protected PathElement[] getRangeShape(int start, int end) { 700 return textNode.impl_getRangeShape(start, end); 701 } 702 703 @Override protected void addHighlight(List<? extends Node> nodes, int start) { 704 textGroup.getChildren().addAll(nodes); 705 } 706 707 @Override protected void removeHighlight(List<? extends Node> nodes) { 708 textGroup.getChildren().removeAll(nodes); 709 } 710 711 @Override public void nextCharacterVisually(boolean moveRight) { 712 if (isRTL()) { 713 // Text node is mirrored. 714 moveRight = !moveRight; 715 } 716 717 Bounds caretBounds = caretPath.getLayoutBounds(); 718 if (caretPath.getElements().size() == 4) { 719 // The caret is split 720 // TODO: Find a better way to get the primary caret position 721 // instead of depending on the internal implementation. 722 // See RT-25465. 723 caretBounds = new Path(caretPath.getElements().get(0), caretPath.getElements().get(1)).getLayoutBounds(); 724 } 725 double hitX = moveRight ? caretBounds.getMaxX() : caretBounds.getMinX(); 726 double hitY = (caretBounds.getMinY() + caretBounds.getMaxY()) / 2; 727 HitInfo hit = textNode.impl_hitTestChar(translateCaretPosition(new Point2D(hitX, hitY))); 728 Path charShape = new Path(textNode.impl_getRangeShape(hit.getCharIndex(), hit.getCharIndex() + 1)); 729 if ((moveRight && charShape.getLayoutBounds().getMaxX() > caretBounds.getMaxX()) || 730 (!moveRight && charShape.getLayoutBounds().getMinX() < caretBounds.getMinX())) { 731 hit.setLeading(!hit.isLeading()); 732 } 733 positionCaret(hit, false); 734 } 735 736 @Override protected void layoutChildren(final double x, final double y, 737 final double w, final double h) { 738 super.layoutChildren(x, y, w, h); 739 740 if (textNode != null) { 741 double textY; 742 final Bounds textNodeBounds = textNode.getLayoutBounds(); 743 final double ascent = textNode.getBaselineOffset(); 744 final double descent = textNodeBounds.getHeight() - ascent; 745 746 switch (getSkinnable().getAlignment().getVpos()) { 747 case TOP: 748 textY = ascent; 749 break; 750 751 case CENTER: 752 textY = (ascent + textGroup.getHeight() - descent) / 2; 753 break; 754 755 case BOTTOM: 756 default: 757 textY = textGroup.getHeight() - descent; 758 } 759 textNode.setY(textY); 760 if (promptNode != null) { 761 promptNode.setY(textY); 762 } 763 764 if (getSkinnable().getWidth() > 0) { 765 updateTextPos(); 766 updateCaretOff(); 767 } 768 } 769 770 if (SHOW_HANDLES) { 771 handleGroup.setLayoutX(x + textTranslateX.get()); 772 handleGroup.setLayoutY(y); 773 774 // Resize handles for caret and anchor. 775 // IndexRange selection = textField.getSelection(); 776 selectionHandle1.resize(selectionHandle1.prefWidth(-1), 777 selectionHandle1.prefHeight(-1)); 778 selectionHandle2.resize(selectionHandle2.prefWidth(-1), 779 selectionHandle2.prefHeight(-1)); 780 caretHandle.resize(caretHandle.prefWidth(-1), 781 caretHandle.prefHeight(-1)); 782 783 Bounds b = caretPath.getBoundsInParent(); 784 caretHandle.setLayoutY(b.getMaxY() - 1); 785 //selectionHandle1.setLayoutY(b.getMaxY() - 1); 786 selectionHandle1.setLayoutY(b.getMinY() - selectionHandle1.getHeight() + 1); 787 selectionHandle2.setLayoutY(b.getMaxY() - 1); 788 } 789 } 790 791 protected HPos getHAlignment() { 792 HPos hPos = getSkinnable().getAlignment().getHpos(); 793 return hPos; 794 } 795 796 @Override public Point2D getMenuPosition() { 797 Point2D p = super.getMenuPosition(); 798 if (p != null) { 799 p = new Point2D(Math.max(0, p.getX() - textNode.getLayoutX() - snappedLeftInset() + textTranslateX.get()), 800 Math.max(0, p.getY() - textNode.getLayoutY() - snappedTopInset())); 801 } 802 return p; 803 } 804 805 @Override protected String maskText(String txt) { 806 if (getSkinnable() instanceof PasswordField) { 807 int n = txt.length(); 808 StringBuilder passwordBuilder = new StringBuilder(n); 809 for (int i = 0; i < n; i++) { 810 passwordBuilder.append(BULLET); 811 } 812 813 return passwordBuilder.toString(); 814 } else { 815 return txt; 816 } 817 } 818 819 @Override 820 protected Object accGetAttribute(Attribute attribute, Object... parameters) { 821 switch (attribute) { 822 case BOUNDS_FOR_RANGE: 823 case OFFSET_AT_POINT: 824 return textNode.accGetAttribute(attribute, parameters); 825 default: return super.accGetAttribute(attribute, parameters); 826 } 827 } 828 }