1 /* 2 * Copyright (c) 2011, 2016, Oracle and/or its affiliates. All rights reserved. 3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4 * 5 * This code is free software; you can redistribute it and/or modify it 6 * under the terms of the GNU General Public License version 2 only, as 7 * published by the Free Software Foundation. Oracle designates this 8 * particular file as subject to the "Classpath" exception as provided 9 * by Oracle in the LICENSE file that accompanied this code. 10 * 11 * This code is distributed in the hope that it will be useful, but WITHOUT 12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 14 * version 2 for more details (a copy is included in the LICENSE file that 15 * accompanied this code). 16 * 17 * You should have received a copy of the GNU General Public License version 18 * 2 along with this work; if not, write to the Free Software Foundation, 19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 20 * 21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 22 * or visit www.oracle.com if you need additional information or have any 23 * questions. 24 */ 25 26 package javafx.scene.control.skin; 27 28 import com.sun.javafx.scene.control.behavior.BehaviorBase; 29 import com.sun.javafx.scene.control.behavior.TextAreaBehavior; 30 import com.sun.javafx.scene.control.behavior.TextInputControlBehavior; 31 import javafx.beans.binding.BooleanBinding; 32 import javafx.beans.binding.DoubleBinding; 33 import javafx.beans.binding.ObjectBinding; 34 import javafx.beans.binding.StringBinding; 35 import javafx.beans.property.DoubleProperty; 36 import javafx.beans.property.SimpleDoubleProperty; 37 import javafx.beans.value.ObservableBooleanValue; 38 import javafx.beans.value.ObservableDoubleValue; 39 import javafx.event.EventHandler; 40 import javafx.geometry.Bounds; 41 import javafx.geometry.HPos; 42 import javafx.geometry.Point2D; 43 import javafx.geometry.Rectangle2D; 44 import javafx.scene.AccessibleAttribute; 45 import javafx.scene.Group; 46 import javafx.scene.Node; 47 import javafx.scene.control.Accordion; 48 import javafx.scene.control.Button; 49 import javafx.scene.control.Control; 50 import javafx.scene.control.IndexRange; 51 import javafx.scene.control.PasswordField; 52 import javafx.scene.control.TextField; 53 import javafx.scene.input.MouseEvent; 54 import javafx.scene.layout.Pane; 55 import javafx.scene.paint.Color; 56 import javafx.scene.paint.Paint; 57 import javafx.scene.shape.Path; 58 import javafx.scene.shape.PathElement; 59 import javafx.scene.shape.Rectangle; 60 import javafx.scene.text.Text; 61 import javafx.scene.text.HitInfo; 62 import java.util.List; 63 import com.sun.javafx.scene.control.behavior.TextFieldBehavior; 64 import com.sun.javafx.scene.control.behavior.PasswordFieldBehavior; 65 66 /** 67 * Default skin implementation for the {@link TextField} control. 68 * 69 * @see TextField 70 * @since 9 71 */ 72 public class TextFieldSkin extends TextInputControlSkin<TextField> { 73 74 /************************************************************************** 75 * 76 * Private fields 77 * 78 **************************************************************************/ 79 80 private final TextFieldBehavior behavior; 81 82 /** 83 * This group contains the text, caret, and selection rectangle. 84 * It is clipped. The textNode, selectionHighlightPath, and 85 * caret are each translated individually when horizontal 86 * translation is needed to keep the caretPosition visible. 87 */ 88 private Pane textGroup = new Pane(); 89 private Group handleGroup; 90 91 /** 92 * The clip, applied to the textGroup. This makes sure that any 93 * text / selection wandering off the text box is clipped 94 */ 95 private Rectangle clip = new Rectangle(); 96 /** 97 * The node actually displaying the text. Note that it has the 98 * ability to render both the normal fill as well as the highlight 99 * fill, to perform hit testing, fetching of the selection 100 * highlight, and other such duties. 101 */ 102 private Text textNode = new Text(); 103 /** 104 * 105 * The node used for showing the prompt text. 106 */ 107 private Text promptNode; 108 /** 109 * A path, provided by the textNode, which represents the area 110 * which is selected. The path elements which make up the 111 * selection must be updated whenever the selection changes. We 112 * don't need to keep track of text changes because those will 113 * force the selection to be updated. 114 */ 115 private Path selectionHighlightPath = new Path(); 116 117 private Path characterBoundingPath = new Path(); 118 private ObservableBooleanValue usePromptText; 119 private DoubleProperty textTranslateX = new SimpleDoubleProperty(this, "textTranslateX"); 120 private double caretWidth; 121 122 /** 123 * Right edge of the text region sans padding 124 */ 125 private ObservableDoubleValue textRight; 126 127 private double pressX, pressY; // For dragging handles on embedded 128 129 // For use with PasswordField 130 static final char BULLET = '\u25cf'; 131 132 133 134 /************************************************************************** 135 * 136 * Constructors 137 * 138 **************************************************************************/ 139 140 /** 141 * Creates a new TextFieldSkin instance, installing the necessary child 142 * nodes into the Control {@link Control#getChildren() children} list, as 143 * well as the necessary input mappings for handling key, mouse, etc events. 144 * 145 * @param control The control that this skin should be installed onto. 146 */ 147 public TextFieldSkin(final TextField control) { 148 super(control); 149 150 // install default input map for the text field control 151 this.behavior = (control instanceof PasswordField) 152 ? new PasswordFieldBehavior((PasswordField)control) 153 : new TextFieldBehavior(control); 154 this.behavior.setTextFieldSkin(this); 155 // control.setInputMap(behavior.getInputMap()); 156 157 control.caretPositionProperty().addListener((observable, oldValue, newValue) -> { 158 if (control.getWidth() > 0) { 159 updateTextNodeCaretPos(control.getCaretPosition()); 160 if (!isForwardBias()) { 161 setForwardBias(true); 162 } 163 updateCaretOff(); 164 } 165 }); 166 167 forwardBiasProperty().addListener(observable -> { 168 if (control.getWidth() > 0) { 169 updateTextNodeCaretPos(control.getCaretPosition()); 170 updateCaretOff(); 171 } 172 }); 173 174 textRight = new DoubleBinding() { 175 { bind(textGroup.widthProperty()); } 176 @Override protected double computeValue() { 177 return textGroup.getWidth(); 178 } 179 }; 180 181 // Once this was crucial for performance, not sure now. 182 clip.setSmooth(false); 183 clip.setX(0); 184 clip.widthProperty().bind(textGroup.widthProperty()); 185 clip.heightProperty().bind(textGroup.heightProperty()); 186 187 // Add content 188 textGroup.setClip(clip); 189 // Hack to defeat the fact that otherwise when the caret blinks the parent group 190 // bounds are completely invalidated and therefore the dirty region is much 191 // larger than necessary. 192 textGroup.getChildren().addAll(selectionHighlightPath, textNode, new Group(caretPath)); 193 getChildren().add(textGroup); 194 if (SHOW_HANDLES) { 195 handleGroup = new Group(); 196 handleGroup.setManaged(false); 197 handleGroup.getChildren().addAll(caretHandle, selectionHandle1, selectionHandle2); 198 getChildren().add(handleGroup); 199 } 200 201 // Add text 202 textNode.setManaged(false); 203 textNode.getStyleClass().add("text"); 204 textNode.fontProperty().bind(control.fontProperty()); 205 206 textNode.layoutXProperty().bind(textTranslateX); 207 textNode.textProperty().bind(new StringBinding() { 208 { bind(control.textProperty()); } 209 @Override protected String computeValue() { 210 return maskText(control.textProperty().getValueSafe()); 211 } 212 }); 213 textNode.fillProperty().bind(textFillProperty()); 214 textNode.selectionFillProperty().bind(new ObjectBinding<Paint>() { 215 { bind(highlightTextFillProperty(), textFillProperty(), control.focusedProperty()); } 216 @Override protected Paint computeValue() { 217 return control.isFocused() ? highlightTextFillProperty().get() : textFillProperty().get(); 218 } 219 }); 220 // updated by listener on caretPosition to ensure order 221 updateTextNodeCaretPos(control.getCaretPosition()); 222 control.selectionProperty().addListener(observable -> { 223 updateSelection(); 224 }); 225 226 // Add selection 227 selectionHighlightPath.setManaged(false); 228 selectionHighlightPath.setStroke(null); 229 selectionHighlightPath.layoutXProperty().bind(textTranslateX); 230 selectionHighlightPath.visibleProperty().bind(control.anchorProperty().isNotEqualTo(control.caretPositionProperty()).and(control.focusedProperty())); 231 selectionHighlightPath.fillProperty().bind(highlightFillProperty()); 232 textNode.selectionShapeProperty().addListener(observable -> { 233 updateSelection(); 234 }); 235 236 // Add caret 237 caretPath.setManaged(false); 238 caretPath.setStrokeWidth(1); 239 caretPath.fillProperty().bind(textFillProperty()); 240 caretPath.strokeProperty().bind(textFillProperty()); 241 242 // modifying visibility of the caret forces a layout-pass (RT-32373), so 243 // instead we modify the opacity. 244 caretPath.opacityProperty().bind(new DoubleBinding() { 245 { bind(caretVisibleProperty()); } 246 @Override protected double computeValue() { 247 return caretVisibleProperty().get() ? 1.0 : 0.0; 248 } 249 }); 250 caretPath.layoutXProperty().bind(textTranslateX); 251 textNode.caretShapeProperty().addListener(observable -> { 252 caretPath.getElements().setAll(textNode.caretShapeProperty().get()); 253 if (caretPath.getElements().size() == 0) { 254 // The caret pos is invalid. 255 updateTextNodeCaretPos(control.getCaretPosition()); 256 } else if (caretPath.getElements().size() == 4) { 257 // The caret is split. Ignore and keep the previous width value. 258 } else { 259 caretWidth = Math.round(caretPath.getLayoutBounds().getWidth()); 260 } 261 }); 262 263 // Be sure to get the control to request layout when the font changes, 264 // since this will affect the pref height and pref width. 265 control.fontProperty().addListener(observable -> { 266 // I do both so that any cached values for prefWidth/height are cleared. 267 // The problem is that the skin is unmanaged and so calling request layout 268 // doesn't walk up the tree all the way. I think.... 269 control.requestLayout(); 270 getSkinnable().requestLayout(); 271 }); 272 273 registerChangeListener(control.prefColumnCountProperty(), e -> getSkinnable().requestLayout()); 274 if (control.isFocused()) setCaretAnimating(true); 275 276 control.alignmentProperty().addListener(observable -> { 277 if (control.getWidth() > 0) { 278 updateTextPos(); 279 updateCaretOff(); 280 control.requestLayout(); 281 } 282 }); 283 284 usePromptText = new BooleanBinding() { 285 { bind(control.textProperty(), 286 control.promptTextProperty(), 287 promptTextFillProperty()); } 288 @Override protected boolean computeValue() { 289 String txt = control.getText(); 290 String promptTxt = control.getPromptText(); 291 return ((txt == null || txt.isEmpty()) && 292 promptTxt != null && !promptTxt.isEmpty() && 293 !getPromptTextFill().equals(Color.TRANSPARENT)); 294 } 295 }; 296 297 promptTextFillProperty().addListener(observable -> { 298 updateTextPos(); 299 }); 300 301 control.textProperty().addListener(observable -> { 302 if (!behavior.isEditing()) { 303 // Text changed, but not by user action 304 updateTextPos(); 305 } 306 }); 307 308 if (usePromptText.get()) { 309 createPromptNode(); 310 } 311 312 usePromptText.addListener(observable -> { 313 createPromptNode(); 314 control.requestLayout(); 315 }); 316 317 if (SHOW_HANDLES) { 318 selectionHandle1.setRotate(180); 319 320 EventHandler<MouseEvent> handlePressHandler = e -> { 321 pressX = e.getX(); 322 pressY = e.getY(); 323 e.consume(); 324 }; 325 326 caretHandle.setOnMousePressed(handlePressHandler); 327 selectionHandle1.setOnMousePressed(handlePressHandler); 328 selectionHandle2.setOnMousePressed(handlePressHandler); 329 330 caretHandle.setOnMouseDragged(e -> { 331 Point2D p = new Point2D(caretHandle.getLayoutX() + e.getX() + pressX - textNode.getLayoutX(), 332 caretHandle.getLayoutY() + e.getY() - pressY - 6); 333 HitInfo hit = textNode.hitTest(p); 334 positionCaret(hit, false); 335 e.consume(); 336 }); 337 338 selectionHandle1.setOnMouseDragged(new EventHandler<MouseEvent>() { 339 @Override public void handle(MouseEvent e) { 340 TextField control = getSkinnable(); 341 Point2D tp = textNode.localToScene(0, 0); 342 Point2D p = new Point2D(e.getSceneX() - tp.getX() + 10/*??*/ - pressX + selectionHandle1.getWidth() / 2, 343 e.getSceneY() - tp.getY() - pressY - 6); 344 HitInfo hit = textNode.hitTest(p); 345 if (control.getAnchor() < control.getCaretPosition()) { 346 // Swap caret and anchor 347 control.selectRange(control.getCaretPosition(), control.getAnchor()); 348 } 349 int pos = hit.getInsertionIndex(); 350 if (pos >= 0) { 351 if (pos >= control.getAnchor() - 1) { 352 pos = Math.max(0, control.getAnchor() - 1); 353 } 354 positionCaret(pos, hit.isLeading(), true); 355 } 356 e.consume(); 357 } 358 }); 359 360 selectionHandle2.setOnMouseDragged(new EventHandler<MouseEvent>() { 361 @Override public void handle(MouseEvent e) { 362 TextField control = getSkinnable(); 363 Point2D tp = textNode.localToScene(0, 0); 364 Point2D p = new Point2D(e.getSceneX() - tp.getX() + 10/*??*/ - pressX + selectionHandle2.getWidth() / 2, 365 e.getSceneY() - tp.getY() - pressY - 6); 366 HitInfo hit = textNode.hitTest(p); 367 if (control.getAnchor() > control.getCaretPosition()) { 368 // Swap caret and anchor 369 control.selectRange(control.getCaretPosition(), control.getAnchor()); 370 } 371 int pos = hit.getInsertionIndex(); 372 if (pos > 0) { 373 if (pos <= control.getAnchor()) { 374 pos = Math.min(control.getAnchor() + 1, control.getLength()); 375 } 376 positionCaret(pos, hit.isLeading(), true); 377 } 378 e.consume(); 379 } 380 }); 381 } 382 } 383 384 385 386 /*************************************************************************** 387 * * 388 * Public API * 389 * * 390 **************************************************************************/ 391 392 /** {@inheritDoc} */ 393 @Override public void dispose() { 394 super.dispose(); 395 396 if (behavior != null) { 397 behavior.dispose(); 398 } 399 } 400 401 /** {@inheritDoc} */ 402 @Override protected double computePrefWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) { 403 TextField textField = getSkinnable(); 404 405 double characterWidth = fontMetrics.get().getCharWidth('W'); 406 407 int columnCount = textField.getPrefColumnCount(); 408 409 return columnCount * characterWidth + leftInset + rightInset; 410 } 411 412 /** {@inheritDoc} */ 413 @Override protected double computeMinHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) { 414 return computePrefHeight(width, topInset, rightInset, bottomInset, leftInset); 415 } 416 417 /** {@inheritDoc} */ 418 @Override protected double computePrefHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) { 419 return topInset + textNode.getLayoutBounds().getHeight() + bottomInset; 420 } 421 422 /** {@inheritDoc} */ 423 @Override protected double computeMaxHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) { 424 return getSkinnable().prefHeight(width); 425 } 426 427 /** {@inheritDoc} */ 428 @Override public double computeBaselineOffset(double topInset, double rightInset, double bottomInset, double leftInset) { 429 return topInset + textNode.getBaselineOffset(); 430 } 431 432 // Public for behavior 433 /** 434 * Replaces a range of characters with the given text. 435 * 436 * Call this implementation from behavior classes instead of the 437 * one provided on TextInputControl to ensure that the text 438 * scrolls as needed. 439 * 440 * @param start The starting index in the range, inclusive. This must be >= 0 and < the end. 441 * @param end The ending index in the range, exclusive. This is one-past the last character to 442 * delete (consistent with the String manipulation methods). This must be > the start, 443 * and <= the length of the text. 444 * @param text The text that is to replace the range. This must not be null. 445 * @see TextField#replaceText(int, int, String) 446 */ 447 public void replaceText(int start, int end, String txt) { 448 final double textMaxXOld = textNode.getBoundsInParent().getMaxX(); 449 final double caretMaxXOld = caretPath.getLayoutBounds().getMaxX() + textTranslateX.get(); 450 getSkinnable().replaceText(start, end, txt); 451 scrollAfterDelete(textMaxXOld, caretMaxXOld); 452 } 453 454 // Public for behavior 455 /** 456 * Deletes the character that follows or precedes the current 457 * caret position from the text if there is no selection, or 458 * deletes the selection if there is one. 459 * 460 * Call this implementation from behavior classes instead of the 461 * one provided on TextInputControl to ensure that the text 462 * scrolls as needed. 463 * 464 * @param previous whether to delete the preceding character. 465 */ 466 public void deleteChar(boolean previous) { 467 final double textMaxXOld = textNode.getBoundsInParent().getMaxX(); 468 final double caretMaxXOld = caretPath.getLayoutBounds().getMaxX() + textTranslateX.get(); 469 if (previous ? getSkinnable().deletePreviousChar() : getSkinnable().deleteNextChar()) { 470 scrollAfterDelete(textMaxXOld, caretMaxXOld); 471 } 472 } 473 474 // Public for behavior 475 /** 476 * Performs a hit test, mapping point to index in the content. 477 * 478 * @param x the x coordinate of the point. 479 * @param y the y coordinate of the point. 480 * @return a {@code HitInfo} object describing the index and forward bias. 481 */ 482 public HitInfo getIndex(double x, double y) { 483 // adjust the event to be in the same coordinate space as the 484 // text content of the textInputControl 485 Point2D p = new Point2D(x - textTranslateX.get() - snappedLeftInset(), 486 y - snappedTopInset()); 487 return textNode.hitTest(p); 488 } 489 490 // Public for behavior 491 /** 492 * Moves the caret to the specified position. 493 * 494 * @param hit the new position and forward bias of the caret. 495 * @param select whether to extend selection to the new position. 496 */ 497 public void positionCaret(HitInfo hit, boolean select) { 498 positionCaret(hit.getInsertionIndex(), hit.isLeading(), select); 499 } 500 501 private void positionCaret(int pos, boolean leading, boolean select) { 502 TextField textField = getSkinnable(); 503 if (select) { 504 textField.selectPositionCaret(pos); 505 } else { 506 textField.positionCaret(pos); 507 } 508 setForwardBias(leading); 509 } 510 511 /** {@inheritDoc} */ 512 @Override public Rectangle2D getCharacterBounds(int index) { 513 double x, y; 514 double width, height; 515 if (index == textNode.getText().length()) { 516 Bounds textNodeBounds = textNode.getBoundsInLocal(); 517 x = textNodeBounds.getMaxX(); 518 y = 0; 519 width = 0; 520 height = textNodeBounds.getMaxY(); 521 } else { 522 characterBoundingPath.getElements().clear(); 523 characterBoundingPath.getElements().addAll(textNode.rangeShape(index, index + 1)); 524 characterBoundingPath.setLayoutX(textNode.getLayoutX()); 525 characterBoundingPath.setLayoutY(textNode.getLayoutY()); 526 527 Bounds bounds = characterBoundingPath.getBoundsInLocal(); 528 529 x = bounds.getMinX(); 530 y = bounds.getMinY(); 531 // Sometimes the bounds is empty, in which case we must ignore the width/height 532 width = bounds.isEmpty() ? 0 : bounds.getWidth(); 533 height = bounds.isEmpty() ? 0 : bounds.getHeight(); 534 } 535 536 Bounds textBounds = textGroup.getBoundsInParent(); 537 538 return new Rectangle2D(x + textBounds.getMinX() + textTranslateX.get(), 539 y + textBounds.getMinY(), width, height); 540 } 541 542 /** {@inheritDoc} */ 543 @Override protected PathElement[] getUnderlineShape(int start, int end) { 544 return textNode.underlineShape(start, end); 545 } 546 547 /** {@inheritDoc} */ 548 @Override protected PathElement[] getRangeShape(int start, int end) { 549 return textNode.rangeShape(start, end); 550 } 551 552 /** {@inheritDoc} */ 553 @Override protected void addHighlight(List<? extends Node> nodes, int start) { 554 textGroup.getChildren().addAll(nodes); 555 } 556 557 /** {@inheritDoc} */ 558 @Override protected void removeHighlight(List<? extends Node> nodes) { 559 textGroup.getChildren().removeAll(nodes); 560 } 561 562 /** {@inheritDoc} */ 563 @Override public void moveCaret(TextUnit unit, Direction dir, boolean select) { 564 switch (unit) { 565 case CHARACTER: 566 switch (dir) { 567 case LEFT: 568 case RIGHT: 569 nextCharacterVisually(dir == Direction.RIGHT); 570 break; 571 default: 572 throw new IllegalArgumentException(""+dir); 573 } 574 break; 575 default: 576 throw new IllegalArgumentException(""+unit); 577 } 578 } 579 580 private void nextCharacterVisually(boolean moveRight) { 581 if (isRTL()) { 582 // Text node is mirrored. 583 moveRight = !moveRight; 584 } 585 586 Bounds caretBounds = caretPath.getLayoutBounds(); 587 if (caretPath.getElements().size() == 4) { 588 // The caret is split 589 // TODO: Find a better way to get the primary caret position 590 // instead of depending on the internal implementation. 591 // See RT-25465. 592 caretBounds = new Path(caretPath.getElements().get(0), caretPath.getElements().get(1)).getLayoutBounds(); 593 } 594 double hitX = moveRight ? caretBounds.getMaxX() : caretBounds.getMinX(); 595 double hitY = (caretBounds.getMinY() + caretBounds.getMaxY()) / 2; 596 HitInfo hit = textNode.hitTest(new Point2D(hitX, hitY)); 597 boolean leading = hit.isLeading(); 598 Path charShape = new Path(textNode.rangeShape(hit.getCharIndex(), hit.getCharIndex() + 1)); 599 if ((moveRight && charShape.getLayoutBounds().getMaxX() > caretBounds.getMaxX()) || 600 (!moveRight && charShape.getLayoutBounds().getMinX() < caretBounds.getMinX())) { 601 leading = !leading; 602 } 603 positionCaret(hit.getInsertionIndex(), leading, false); 604 } 605 606 /** {@inheritDoc} */ 607 @Override protected void layoutChildren(final double x, final double y, 608 final double w, final double h) { 609 super.layoutChildren(x, y, w, h); 610 611 if (textNode != null) { 612 double textY; 613 final Bounds textNodeBounds = textNode.getLayoutBounds(); 614 final double ascent = textNode.getBaselineOffset(); 615 final double descent = textNodeBounds.getHeight() - ascent; 616 617 switch (getSkinnable().getAlignment().getVpos()) { 618 case TOP: 619 textY = ascent; 620 break; 621 622 case CENTER: 623 textY = (ascent + textGroup.getHeight() - descent) / 2; 624 break; 625 626 case BOTTOM: 627 default: 628 textY = textGroup.getHeight() - descent; 629 } 630 textNode.setY(textY); 631 if (promptNode != null) { 632 promptNode.setY(textY); 633 } 634 635 if (getSkinnable().getWidth() > 0) { 636 updateTextPos(); 637 updateCaretOff(); 638 } 639 } 640 641 if (SHOW_HANDLES) { 642 handleGroup.setLayoutX(x + caretWidth / 2); 643 handleGroup.setLayoutY(y); 644 645 // Resize handles for caret and anchor. 646 // IndexRange selection = textField.getSelection(); 647 selectionHandle1.resize(selectionHandle1.prefWidth(-1), 648 selectionHandle1.prefHeight(-1)); 649 selectionHandle2.resize(selectionHandle2.prefWidth(-1), 650 selectionHandle2.prefHeight(-1)); 651 caretHandle.resize(caretHandle.prefWidth(-1), 652 caretHandle.prefHeight(-1)); 653 654 Bounds b = caretPath.getBoundsInParent(); 655 caretHandle.setLayoutY(b.getMaxY() - 1); 656 //selectionHandle1.setLayoutY(b.getMaxY() - 1); 657 selectionHandle1.setLayoutY(b.getMinY() - selectionHandle1.getHeight() + 1); 658 selectionHandle2.setLayoutY(b.getMaxY() - 1); 659 } 660 } 661 662 private HPos getHAlignment() { 663 HPos hPos = getSkinnable().getAlignment().getHpos(); 664 return hPos; 665 } 666 667 /** {@inheritDoc} */ 668 @Override public Point2D getMenuPosition() { 669 Point2D p = super.getMenuPosition(); 670 if (p != null) { 671 p = new Point2D(Math.max(0, p.getX() - textNode.getLayoutX() - snappedLeftInset() + textTranslateX.get()), 672 Math.max(0, p.getY() - textNode.getLayoutY() - snappedTopInset())); 673 } 674 return p; 675 } 676 677 /** {@inheritDoc} */ 678 @Override protected String maskText(String txt) { 679 if (getSkinnable() instanceof PasswordField) { 680 int n = txt.length(); 681 StringBuilder passwordBuilder = new StringBuilder(n); 682 for (int i = 0; i < n; i++) { 683 passwordBuilder.append(BULLET); 684 } 685 686 return passwordBuilder.toString(); 687 } else { 688 return txt; 689 } 690 } 691 692 /** {@inheritDoc} */ 693 @Override protected Object queryAccessibleAttribute(AccessibleAttribute attribute, Object... parameters) { 694 switch (attribute) { 695 case BOUNDS_FOR_RANGE: 696 case OFFSET_AT_POINT: 697 return textNode.queryAccessibleAttribute(attribute, parameters); 698 default: return super.queryAccessibleAttribute(attribute, parameters); 699 } 700 } 701 702 703 704 /************************************************************************** 705 * 706 * Private implementation 707 * 708 **************************************************************************/ 709 710 TextInputControlBehavior getBehavior() { 711 return behavior; 712 } 713 714 private void updateTextNodeCaretPos(int pos) { 715 if (pos == 0 || isForwardBias()) { 716 textNode.setCaretPosition(pos); 717 } else { 718 textNode.setCaretPosition(pos - 1); 719 } 720 textNode.caretBiasProperty().set(isForwardBias()); 721 } 722 723 private void createPromptNode() { 724 if (promptNode != null || !usePromptText.get()) return; 725 726 promptNode = new Text(); 727 textGroup.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 733 promptNode.textProperty().bind(getSkinnable().promptTextProperty()); 734 promptNode.fillProperty().bind(promptTextFillProperty()); 735 updateSelection(); 736 } 737 738 private void updateSelection() { 739 TextField textField = getSkinnable(); 740 IndexRange newValue = textField.getSelection(); 741 742 if (newValue == null || newValue.getLength() == 0) { 743 textNode.selectionStartProperty().set(-1); 744 textNode.selectionEndProperty().set(-1); 745 } else { 746 textNode.selectionStartProperty().set(newValue.getStart()); 747 // This intermediate value is needed to force selection shape layout. 748 textNode.selectionEndProperty().set(newValue.getStart()); 749 textNode.selectionEndProperty().set(newValue.getEnd()); 750 } 751 752 PathElement[] elements = textNode.selectionShapeProperty().get(); 753 if (elements == null) { 754 selectionHighlightPath.getElements().clear(); 755 } else { 756 selectionHighlightPath.getElements().setAll(elements); 757 } 758 759 if (SHOW_HANDLES && newValue != null && newValue.getLength() > 0) { 760 int caretPos = textField.getCaretPosition(); 761 int anchorPos = textField.getAnchor(); 762 763 { 764 // Position the handle for the anchor. This could be handle1 or handle2. 765 // Do this before positioning the handle for the caret. 766 updateTextNodeCaretPos(anchorPos); 767 Bounds b = caretPath.getBoundsInParent(); 768 if (caretPos < anchorPos) { 769 selectionHandle2.setLayoutX(b.getMinX() - selectionHandle2.getWidth() / 2); 770 } else { 771 selectionHandle1.setLayoutX(b.getMinX() - selectionHandle1.getWidth() / 2); 772 } 773 } 774 775 { 776 // Position handle for the caret. This could be handle1 or handle2. 777 updateTextNodeCaretPos(caretPos); 778 Bounds b = caretPath.getBoundsInParent(); 779 if (caretPos < anchorPos) { 780 selectionHandle1.setLayoutX(b.getMinX() - selectionHandle1.getWidth() / 2); 781 } else { 782 selectionHandle2.setLayoutX(b.getMinX() - selectionHandle2.getWidth() / 2); 783 } 784 } 785 } 786 } 787 788 /** 789 * Updates the textTranslateX value for the Text node position. This is 790 * done for general layout, but care is taken to avoid resetting the 791 * position when there's a need to scroll the text due to caret movement, 792 * or when editing text that overflows on either side. 793 */ 794 private void updateTextPos() { 795 double oldX = textTranslateX.get(); 796 double newX; 797 double textNodeWidth = textNode.getLayoutBounds().getWidth(); 798 799 switch (getHAlignment()) { 800 case CENTER: 801 double midPoint = textRight.get() / 2; 802 if (usePromptText.get()) { 803 // If a prompt is shown (which implies that the text is 804 // empty), then we align the Text node so that the caret will 805 // appear at the left of the centered prompt. 806 newX = midPoint - promptNode.getLayoutBounds().getWidth() / 2; 807 promptNode.setLayoutX(newX); 808 } else { 809 newX = midPoint - textNodeWidth / 2; 810 } 811 // Update if there is space on the right 812 if (newX + textNodeWidth <= textRight.get()) { 813 textTranslateX.set(newX); 814 } 815 break; 816 817 case RIGHT: 818 newX = textRight.get() - textNodeWidth - caretWidth / 2; 819 // Update if there is space on the right 820 if (newX > oldX || newX > 0) { 821 textTranslateX.set(newX); 822 } 823 if (usePromptText.get()) { 824 promptNode.setLayoutX(textRight.get() - promptNode.getLayoutBounds().getWidth() - 825 caretWidth / 2); 826 } 827 break; 828 829 case LEFT: 830 default: 831 newX = caretWidth / 2; 832 // Update if there is space on either side. 833 if (newX < oldX || newX + textNodeWidth <= textRight.get()) { 834 textTranslateX.set(newX); 835 } 836 if (usePromptText.get()) { 837 promptNode.layoutXProperty().set(newX); 838 } 839 } 840 } 841 842 // should be called when the padding changes, or the text box width, or 843 // the dot moves 844 private void updateCaretOff() { 845 double delta = 0.0; 846 double caretX = caretPath.getLayoutBounds().getMinX() + textTranslateX.get(); 847 // If the caret position is less than or equal to the left edge of the 848 // clip then the caret will be clipped. We want the caret to end up 849 // being positioned one pixel right of the clip's left edge. The same 850 // applies on the right edge (but going the other direction of course). 851 if (caretX < 0) { 852 // I'll end up with a negative number 853 delta = caretX; 854 } else if (caretX > (textRight.get() - caretWidth)) { 855 // I'll end up with a positive number 856 delta = caretX - (textRight.get() - caretWidth); 857 } 858 859 // If delta is negative, then translate in the negative direction 860 // to cause the text to scroll to the right. Vice-versa for positive. 861 switch (getHAlignment()) { 862 case CENTER: 863 textTranslateX.set(textTranslateX.get() - delta); 864 break; 865 866 case RIGHT: 867 textTranslateX.set(Math.max(textTranslateX.get() - delta, 868 textRight.get() - textNode.getLayoutBounds().getWidth() - 869 caretWidth / 2)); 870 break; 871 872 case LEFT: 873 default: 874 textTranslateX.set(Math.min(textTranslateX.get() - delta, 875 caretWidth / 2)); 876 } 877 if (SHOW_HANDLES) { 878 caretHandle.setLayoutX(caretX - caretHandle.getWidth() / 2); 879 } 880 } 881 882 private void scrollAfterDelete(double textMaxXOld, double caretMaxXOld) { 883 final Bounds textLayoutBounds = textNode.getLayoutBounds(); 884 final Bounds textBounds = textNode.localToParent(textLayoutBounds); 885 final Bounds clipBounds = clip.getBoundsInParent(); 886 final Bounds caretBounds = caretPath.getLayoutBounds(); 887 888 switch (getHAlignment()) { 889 case RIGHT: 890 if (textBounds.getMaxX() > clipBounds.getMaxX()) { 891 double delta = caretMaxXOld - caretBounds.getMaxX() - textTranslateX.get(); 892 if (textBounds.getMaxX() + delta < clipBounds.getMaxX()) { 893 if (textMaxXOld <= clipBounds.getMaxX()) { 894 delta = textMaxXOld - textBounds.getMaxX(); 895 } else { 896 delta = clipBounds.getMaxX() - textBounds.getMaxX(); 897 } 898 } 899 textTranslateX.set(textTranslateX.get() + delta); 900 } else { 901 updateTextPos(); 902 } 903 break; 904 905 case LEFT: 906 case CENTER: 907 default: 908 if (textBounds.getMinX() < clipBounds.getMinX() + caretWidth / 2 && 909 textBounds.getMaxX() <= clipBounds.getMaxX()) { 910 double delta = caretMaxXOld - caretBounds.getMaxX() - textTranslateX.get(); 911 if (textBounds.getMaxX() + delta < clipBounds.getMaxX()) { 912 if (textMaxXOld <= clipBounds.getMaxX()) { 913 delta = textMaxXOld - textBounds.getMaxX(); 914 } else { 915 delta = clipBounds.getMaxX() - textBounds.getMaxX(); 916 } 917 } 918 textTranslateX.set(textTranslateX.get() + delta); 919 } 920 } 921 922 updateCaretOff(); 923 } 924 }