1 /* 2 * Copyright (c) 2011, 2015, Oracle and/or its affiliates. All rights reserved. 3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4 * 5 * This code is free software; you can redistribute it and/or modify it 6 * under the terms of the GNU General Public License version 2 only, as 7 * published by the Free Software Foundation. Oracle designates this 8 * particular file as subject to the "Classpath" exception as provided 9 * by Oracle in the LICENSE file that accompanied this code. 10 * 11 * This code is distributed in the hope that it will be useful, but WITHOUT 12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 14 * version 2 for more details (a copy is included in the LICENSE file that 15 * accompanied this code). 16 * 17 * You should have received a copy of the GNU General Public License version 18 * 2 along with this work; if not, write to the Free Software Foundation, 19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 20 * 21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 22 * or visit www.oracle.com if you need additional information or have any 23 * questions. 24 */ 25 26 package javafx.scene.control.skin; 27 28 import com.sun.javafx.scene.control.behavior.BehaviorBase; 29 import com.sun.javafx.scene.control.behavior.TextAreaBehavior; 30 import com.sun.javafx.scene.control.behavior.TextInputControlBehavior; 31 import com.sun.javafx.scene.control.skin.Utils; 32 import javafx.beans.binding.BooleanBinding; 33 import javafx.beans.binding.DoubleBinding; 34 import javafx.beans.binding.ObjectBinding; 35 import javafx.beans.binding.StringBinding; 36 import javafx.beans.property.DoubleProperty; 37 import javafx.beans.property.SimpleDoubleProperty; 38 import javafx.beans.value.ObservableBooleanValue; 39 import javafx.beans.value.ObservableDoubleValue; 40 import javafx.event.EventHandler; 41 import javafx.geometry.Bounds; 42 import javafx.geometry.HPos; 43 import javafx.geometry.Point2D; 44 import javafx.geometry.Rectangle2D; 45 import javafx.scene.AccessibleAttribute; 46 import javafx.scene.Group; 47 import javafx.scene.Node; 48 import javafx.scene.control.Accordion; 49 import javafx.scene.control.Button; 50 import javafx.scene.control.Control; 51 import javafx.scene.control.IndexRange; 52 import javafx.scene.control.PasswordField; 53 import javafx.scene.control.TextField; 54 import javafx.scene.input.MouseEvent; 55 import javafx.scene.layout.Pane; 56 import javafx.scene.paint.Color; 57 import javafx.scene.paint.Paint; 58 import javafx.scene.shape.Path; 59 import javafx.scene.shape.PathElement; 60 import javafx.scene.shape.Rectangle; 61 import javafx.scene.text.Text; 62 import java.util.List; 63 import com.sun.javafx.scene.control.behavior.TextFieldBehavior; 64 import com.sun.javafx.scene.control.behavior.PasswordFieldBehavior; 65 import com.sun.javafx.scene.text.HitInfo; 66 67 /** 68 * Default skin implementation for the {@link TextField} control. 69 * 70 * @see TextField 71 * @since 9 72 */ 73 public class TextFieldSkin extends TextInputControlSkin<TextField> { 74 75 /************************************************************************** 76 * 77 * Private fields 78 * 79 **************************************************************************/ 80 81 private final TextFieldBehavior behavior; 82 83 /** 84 * This group contains the text, caret, and selection rectangle. 85 * It is clipped. The textNode, selectionHighlightPath, and 195 if (SHOW_HANDLES) { 196 handleGroup = new Group(); 197 handleGroup.setManaged(false); 198 handleGroup.getChildren().addAll(caretHandle, selectionHandle1, selectionHandle2); 199 getChildren().add(handleGroup); 200 } 201 202 // Add text 203 textNode.setManaged(false); 204 textNode.getStyleClass().add("text"); 205 textNode.fontProperty().bind(control.fontProperty()); 206 207 textNode.layoutXProperty().bind(textTranslateX); 208 textNode.textProperty().bind(new StringBinding() { 209 { bind(control.textProperty()); } 210 @Override protected String computeValue() { 211 return maskText(control.textProperty().getValueSafe()); 212 } 213 }); 214 textNode.fillProperty().bind(textFillProperty()); 215 textNode.impl_selectionFillProperty().bind(new ObjectBinding<Paint>() { 216 { bind(highlightTextFillProperty(), textFillProperty(), control.focusedProperty()); } 217 @Override protected Paint computeValue() { 218 return control.isFocused() ? highlightTextFillProperty().get() : textFillProperty().get(); 219 } 220 }); 221 // updated by listener on caretPosition to ensure order 222 updateTextNodeCaretPos(control.getCaretPosition()); 223 control.selectionProperty().addListener(observable -> { 224 updateSelection(); 225 }); 226 227 // Add selection 228 selectionHighlightPath.setManaged(false); 229 selectionHighlightPath.setStroke(null); 230 selectionHighlightPath.layoutXProperty().bind(textTranslateX); 231 selectionHighlightPath.visibleProperty().bind(control.anchorProperty().isNotEqualTo(control.caretPositionProperty()).and(control.focusedProperty())); 232 selectionHighlightPath.fillProperty().bind(highlightFillProperty()); 233 textNode.impl_selectionShapeProperty().addListener(observable -> { 234 updateSelection(); 235 }); 236 237 // Add caret 238 caretPath.setManaged(false); 239 caretPath.setStrokeWidth(1); 240 caretPath.fillProperty().bind(textFillProperty()); 241 caretPath.strokeProperty().bind(textFillProperty()); 242 243 // modifying visibility of the caret forces a layout-pass (RT-32373), so 244 // instead we modify the opacity. 245 caretPath.opacityProperty().bind(new DoubleBinding() { 246 { bind(caretVisibleProperty()); } 247 @Override protected double computeValue() { 248 return caretVisibleProperty().get() ? 1.0 : 0.0; 249 } 250 }); 251 caretPath.layoutXProperty().bind(textTranslateX); 252 textNode.impl_caretShapeProperty().addListener(observable -> { 253 caretPath.getElements().setAll(textNode.impl_caretShapeProperty().get()); 254 if (caretPath.getElements().size() == 0) { 255 // The caret pos is invalid. 256 updateTextNodeCaretPos(control.getCaretPosition()); 257 } else if (caretPath.getElements().size() == 4) { 258 // The caret is split. Ignore and keep the previous width value. 259 } else { 260 caretWidth = Math.round(caretPath.getLayoutBounds().getWidth()); 261 } 262 }); 263 264 // Be sure to get the control to request layout when the font changes, 265 // since this will affect the pref height and pref width. 266 control.fontProperty().addListener(observable -> { 267 // I do both so that any cached values for prefWidth/height are cleared. 268 // The problem is that the skin is unmanaged and so calling request layout 269 // doesn't walk up the tree all the way. I think.... 270 control.requestLayout(); 271 getSkinnable().requestLayout(); 272 }); 273 314 createPromptNode(); 315 control.requestLayout(); 316 }); 317 318 if (SHOW_HANDLES) { 319 selectionHandle1.setRotate(180); 320 321 EventHandler<MouseEvent> handlePressHandler = e -> { 322 pressX = e.getX(); 323 pressY = e.getY(); 324 e.consume(); 325 }; 326 327 caretHandle.setOnMousePressed(handlePressHandler); 328 selectionHandle1.setOnMousePressed(handlePressHandler); 329 selectionHandle2.setOnMousePressed(handlePressHandler); 330 331 caretHandle.setOnMouseDragged(e -> { 332 Point2D p = new Point2D(caretHandle.getLayoutX() + e.getX() + pressX - textNode.getLayoutX(), 333 caretHandle.getLayoutY() + e.getY() - pressY - 6); 334 HitInfo hit = textNode.impl_hitTestChar(p); 335 positionCaret(hit, false); 336 e.consume(); 337 }); 338 339 selectionHandle1.setOnMouseDragged(new EventHandler<MouseEvent>() { 340 @Override public void handle(MouseEvent e) { 341 TextField control = getSkinnable(); 342 Point2D tp = textNode.localToScene(0, 0); 343 Point2D p = new Point2D(e.getSceneX() - tp.getX() + 10/*??*/ - pressX + selectionHandle1.getWidth() / 2, 344 e.getSceneY() - tp.getY() - pressY - 6); 345 HitInfo hit = textNode.impl_hitTestChar(p); 346 int pos = hit.getCharIndex(); 347 if (control.getAnchor() < control.getCaretPosition()) { 348 // Swap caret and anchor 349 control.selectRange(control.getCaretPosition(), control.getAnchor()); 350 } 351 if (pos >= 0) { 352 if (pos >= control.getAnchor() - 1) { 353 hit.setCharIndex(Math.max(0, control.getAnchor() - 1)); 354 } 355 positionCaret(hit, true); 356 } 357 e.consume(); 358 } 359 }); 360 361 selectionHandle2.setOnMouseDragged(new EventHandler<MouseEvent>() { 362 @Override public void handle(MouseEvent e) { 363 TextField control = getSkinnable(); 364 Point2D tp = textNode.localToScene(0, 0); 365 Point2D p = new Point2D(e.getSceneX() - tp.getX() + 10/*??*/ - pressX + selectionHandle2.getWidth() / 2, 366 e.getSceneY() - tp.getY() - pressY - 6); 367 HitInfo hit = textNode.impl_hitTestChar(p); 368 int pos = hit.getCharIndex(); 369 if (control.getAnchor() > control.getCaretPosition()) { 370 // Swap caret and anchor 371 control.selectRange(control.getCaretPosition(), control.getAnchor()); 372 } 373 if (pos > 0) { 374 if (pos <= control.getAnchor()) { 375 hit.setCharIndex(Math.min(control.getAnchor() + 1, control.getLength())); 376 } 377 positionCaret(hit, true); 378 } 379 e.consume(); 380 } 381 }); 382 } 383 } 384 385 386 387 /*************************************************************************** 388 * * 389 * Public API * 390 * * 391 **************************************************************************/ 392 393 /** {@inheritDoc} */ 394 @Override public void dispose() { 395 super.dispose(); 396 397 if (behavior != null) { 461 * Call this implementation from behavior classes instead of the 462 * one provided on TextInputControl to ensure that the text 463 * scrolls as needed. 464 * 465 * @param previous whether to delete the preceding character. 466 */ 467 public void deleteChar(boolean previous) { 468 final double textMaxXOld = textNode.getBoundsInParent().getMaxX(); 469 final double caretMaxXOld = caretPath.getLayoutBounds().getMaxX() + textTranslateX.get(); 470 if (previous ? getSkinnable().deletePreviousChar() : getSkinnable().deleteNextChar()) { 471 scrollAfterDelete(textMaxXOld, caretMaxXOld); 472 } 473 } 474 475 // Public for behavior 476 /** 477 * Performs a hit test, mapping point to index in the content. 478 * 479 * @param x the x coordinate of the point. 480 * @param y the y coordinate of the point. 481 * @return a {@code TextPosInfo} object describing the index and forward bias. 482 */ 483 public TextPosInfo getIndex(double x, double y) { 484 // adjust the event to be in the same coordinate space as the 485 // text content of the textInputControl 486 Point2D p = new Point2D(x - textTranslateX.get() - snappedLeftInset(), 487 y - snappedTopInset()); 488 return new TextPosInfo(textNode.impl_hitTestChar(p)); 489 } 490 491 // Public for behavior 492 /** 493 * Moves the caret to the specified position. 494 * 495 * @param hit the new position and forward bias of the caret. 496 * @param select whether to extend selection to the new position. 497 */ 498 public void positionCaret(TextPosInfo hit, boolean select) { 499 TextField textField = getSkinnable(); 500 int pos = Utils.getHitInsertionIndex(hit, textField.textProperty().getValueSafe()); 501 502 if (select) { 503 textField.selectPositionCaret(pos); 504 } else { 505 textField.positionCaret(pos); 506 } 507 508 setForwardBias(hit.isLeading()); 509 } 510 511 private void positionCaret(HitInfo hit, boolean select) { 512 positionCaret(new TextPosInfo(hit), select); 513 } 514 515 /** {@inheritDoc} */ 516 @Override public Rectangle2D getCharacterBounds(int index) { 517 double x, y; 518 double width, height; 519 if (index == textNode.getText().length()) { 520 Bounds textNodeBounds = textNode.getBoundsInLocal(); 521 x = textNodeBounds.getMaxX(); 522 y = 0; 523 width = 0; 524 height = textNodeBounds.getMaxY(); 525 } else { 526 characterBoundingPath.getElements().clear(); 527 characterBoundingPath.getElements().addAll(textNode.impl_getRangeShape(index, index + 1)); 528 characterBoundingPath.setLayoutX(textNode.getLayoutX()); 529 characterBoundingPath.setLayoutY(textNode.getLayoutY()); 530 531 Bounds bounds = characterBoundingPath.getBoundsInLocal(); 532 533 x = bounds.getMinX(); 534 y = bounds.getMinY(); 535 // Sometimes the bounds is empty, in which case we must ignore the width/height 536 width = bounds.isEmpty() ? 0 : bounds.getWidth(); 537 height = bounds.isEmpty() ? 0 : bounds.getHeight(); 538 } 539 540 Bounds textBounds = textGroup.getBoundsInParent(); 541 542 return new Rectangle2D(x + textBounds.getMinX() + textTranslateX.get(), 543 y + textBounds.getMinY(), width, height); 544 } 545 546 /** {@inheritDoc} */ 547 @Override protected PathElement[] getUnderlineShape(int start, int end) { 548 return textNode.impl_getUnderlineShape(start, end); 549 } 550 551 /** {@inheritDoc} */ 552 @Override protected PathElement[] getRangeShape(int start, int end) { 553 return textNode.impl_getRangeShape(start, end); 554 } 555 556 /** {@inheritDoc} */ 557 @Override protected void addHighlight(List<? extends Node> nodes, int start) { 558 textGroup.getChildren().addAll(nodes); 559 } 560 561 /** {@inheritDoc} */ 562 @Override protected void removeHighlight(List<? extends Node> nodes) { 563 textGroup.getChildren().removeAll(nodes); 564 } 565 566 /** {@inheritDoc} */ 567 @Override public void moveCaret(TextUnit unit, Direction dir, boolean select) { 568 switch (unit) { 569 case CHARACTER: 570 switch (dir) { 571 case LEFT: 572 case RIGHT: 573 nextCharacterVisually(dir == Direction.RIGHT); 580 throw new IllegalArgumentException(""+unit); 581 } 582 } 583 584 private void nextCharacterVisually(boolean moveRight) { 585 if (isRTL()) { 586 // Text node is mirrored. 587 moveRight = !moveRight; 588 } 589 590 Bounds caretBounds = caretPath.getLayoutBounds(); 591 if (caretPath.getElements().size() == 4) { 592 // The caret is split 593 // TODO: Find a better way to get the primary caret position 594 // instead of depending on the internal implementation. 595 // See RT-25465. 596 caretBounds = new Path(caretPath.getElements().get(0), caretPath.getElements().get(1)).getLayoutBounds(); 597 } 598 double hitX = moveRight ? caretBounds.getMaxX() : caretBounds.getMinX(); 599 double hitY = (caretBounds.getMinY() + caretBounds.getMaxY()) / 2; 600 HitInfo hit = textNode.impl_hitTestChar(new Point2D(hitX, hitY)); 601 Path charShape = new Path(textNode.impl_getRangeShape(hit.getCharIndex(), hit.getCharIndex() + 1)); 602 if ((moveRight && charShape.getLayoutBounds().getMaxX() > caretBounds.getMaxX()) || 603 (!moveRight && charShape.getLayoutBounds().getMinX() < caretBounds.getMinX())) { 604 hit.setLeading(!hit.isLeading()); 605 } 606 positionCaret(hit, false); 607 } 608 609 /** {@inheritDoc} */ 610 @Override protected void layoutChildren(final double x, final double y, 611 final double w, final double h) { 612 super.layoutChildren(x, y, w, h); 613 614 if (textNode != null) { 615 double textY; 616 final Bounds textNodeBounds = textNode.getLayoutBounds(); 617 final double ascent = textNode.getBaselineOffset(); 618 final double descent = textNodeBounds.getHeight() - ascent; 619 620 switch (getSkinnable().getAlignment().getVpos()) { 621 case TOP: 622 textY = ascent; 623 break; 624 625 case CENTER: 626 textY = (ascent + textGroup.getHeight() - descent) / 2; 699 case OFFSET_AT_POINT: 700 return textNode.queryAccessibleAttribute(attribute, parameters); 701 default: return super.queryAccessibleAttribute(attribute, parameters); 702 } 703 } 704 705 706 707 /************************************************************************** 708 * 709 * Private implementation 710 * 711 **************************************************************************/ 712 713 TextInputControlBehavior getBehavior() { 714 return behavior; 715 } 716 717 private void updateTextNodeCaretPos(int pos) { 718 if (pos == 0 || isForwardBias()) { 719 textNode.setImpl_caretPosition(pos); 720 } else { 721 textNode.setImpl_caretPosition(pos - 1); 722 } 723 textNode.impl_caretBiasProperty().set(isForwardBias()); 724 } 725 726 private void createPromptNode() { 727 if (promptNode != null || !usePromptText.get()) return; 728 729 promptNode = new Text(); 730 textGroup.getChildren().add(0, promptNode); 731 promptNode.setManaged(false); 732 promptNode.getStyleClass().add("text"); 733 promptNode.visibleProperty().bind(usePromptText); 734 promptNode.fontProperty().bind(getSkinnable().fontProperty()); 735 736 promptNode.textProperty().bind(getSkinnable().promptTextProperty()); 737 promptNode.fillProperty().bind(promptTextFillProperty()); 738 updateSelection(); 739 } 740 741 private void updateSelection() { 742 TextField textField = getSkinnable(); 743 IndexRange newValue = textField.getSelection(); 744 745 if (newValue == null || newValue.getLength() == 0) { 746 textNode.impl_selectionStartProperty().set(-1); 747 textNode.impl_selectionEndProperty().set(-1); 748 } else { 749 textNode.impl_selectionStartProperty().set(newValue.getStart()); 750 // This intermediate value is needed to force selection shape layout. 751 textNode.impl_selectionEndProperty().set(newValue.getStart()); 752 textNode.impl_selectionEndProperty().set(newValue.getEnd()); 753 } 754 755 PathElement[] elements = textNode.impl_selectionShapeProperty().get(); 756 if (elements == null) { 757 selectionHighlightPath.getElements().clear(); 758 } else { 759 selectionHighlightPath.getElements().setAll(elements); 760 } 761 762 if (SHOW_HANDLES && newValue != null && newValue.getLength() > 0) { 763 int caretPos = textField.getCaretPosition(); 764 int anchorPos = textField.getAnchor(); 765 766 { 767 // Position the handle for the anchor. This could be handle1 or handle2. 768 // Do this before positioning the handle for the caret. 769 updateTextNodeCaretPos(anchorPos); 770 Bounds b = caretPath.getBoundsInParent(); 771 if (caretPos < anchorPos) { 772 selectionHandle2.setLayoutX(b.getMinX() - selectionHandle2.getWidth() / 2); 773 } else { 774 selectionHandle1.setLayoutX(b.getMinX() - selectionHandle1.getWidth() / 2); 775 } | 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 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 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) { 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); 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; 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 } |