< prev index next >

modules/controls/src/main/java/javafx/scene/control/skin/TextFieldSkin.java

Print this page


   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                 }


< prev index next >