javafx.scene.control.skin.TextFieldSkin Maven / Gradle / Ivy
/*
* Copyright (c) 2011, 2023, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package javafx.scene.control.skin;
import java.util.List;
import com.sun.javafx.scene.control.behavior.PasswordFieldBehavior;
import com.sun.javafx.scene.control.behavior.TextFieldBehavior;
import com.sun.javafx.scene.control.behavior.TextInputControlBehavior;
import javafx.beans.binding.BooleanBinding;
import javafx.beans.binding.DoubleBinding;
import javafx.beans.binding.ObjectBinding;
import javafx.beans.binding.StringBinding;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.value.ObservableBooleanValue;
import javafx.beans.value.ObservableDoubleValue;
import javafx.event.EventHandler;
import javafx.geometry.Bounds;
import javafx.geometry.HPos;
import javafx.geometry.Point2D;
import javafx.geometry.Rectangle2D;
import javafx.scene.AccessibleAttribute;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.control.Control;
import javafx.scene.control.IndexRange;
import javafx.scene.control.PasswordField;
import javafx.scene.control.TextField;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.paint.Paint;
import javafx.scene.shape.Path;
import javafx.scene.shape.PathElement;
import javafx.scene.shape.Rectangle;
import javafx.scene.text.HitInfo;
import javafx.scene.text.Text;
/**
* Default skin implementation for the {@link TextField} control.
*
* @see TextField
* @since 9
*/
public class TextFieldSkin extends TextInputControlSkin {
/* ************************************************************************
*
* Private fields
*
**************************************************************************/
private final TextFieldBehavior behavior;
/**
* This group contains the text, caret, and selection rectangle.
* It is clipped. The textNode, selectionHighlightPath, and
* caret are each translated individually when horizontal
* translation is needed to keep the caretPosition visible.
*/
private Pane textGroup = new Pane();
private Group handleGroup;
/**
* The clip, applied to the textGroup. This makes sure that any
* text / selection wandering off the text box is clipped
*/
private Rectangle clip = new Rectangle();
/**
* The node actually displaying the text. Note that it has the
* ability to render both the normal fill as well as the highlight
* fill, to perform hit testing, fetching of the selection
* highlight, and other such duties.
*/
private Text textNode = new Text();
/**
*
* The node used for showing the prompt text.
*/
private Text promptNode;
/**
* A path, provided by the textNode, which represents the area
* which is selected. The path elements which make up the
* selection must be updated whenever the selection changes. We
* don't need to keep track of text changes because those will
* force the selection to be updated.
*/
private Path selectionHighlightPath = new Path();
private Path characterBoundingPath = new Path();
private ObservableBooleanValue usePromptText;
private DoubleProperty textTranslateX = new SimpleDoubleProperty(this, "textTranslateX");
private double caretWidth;
/**
* Right edge of the text region sans padding
*/
private ObservableDoubleValue textRight;
private double pressX, pressY; // For dragging handles on embedded
// For use with PasswordField
static final char BULLET = '\u25cf';
/* ************************************************************************
*
* Constructors
*
**************************************************************************/
/**
* Creates a new TextFieldSkin instance, installing the necessary child
* nodes into the Control {@link Control#getChildren() children} list, as
* well as the necessary input mappings for handling key, mouse, etc events.
*
* @param control The control that this skin should be installed onto.
*/
public TextFieldSkin(final TextField control) {
super(control);
// install default input map for the text field control
this.behavior = (control instanceof PasswordField)
? new PasswordFieldBehavior((PasswordField)control)
: new TextFieldBehavior(control);
this.behavior.setTextFieldSkin(this);
// control.setInputMap(behavior.getInputMap());
registerChangeListener(control.caretPositionProperty(), e -> {
if (control.getWidth() > 0) {
updateTextNodeCaretPos(control.getCaretPosition());
if (!isForwardBias()) {
setForwardBias(true);
}
updateCaretOff();
}
});
forwardBiasProperty().addListener(observable -> {
if (control.getWidth() > 0) {
updateTextNodeCaretPos(control.getCaretPosition());
updateCaretOff();
}
});
textRight = new DoubleBinding() {
{ bind(textGroup.widthProperty()); }
@Override protected double computeValue() {
return textGroup.getWidth();
}
};
// Once this was crucial for performance, not sure now.
clip.setSmooth(false);
clip.setX(0);
clip.widthProperty().bind(textGroup.widthProperty());
clip.heightProperty().bind(textGroup.heightProperty());
// Add content
textGroup.setClip(clip);
// Hack to defeat the fact that otherwise when the caret blinks the parent group
// bounds are completely invalidated and therefore the dirty region is much
// larger than necessary.
textGroup.getChildren().addAll(selectionHighlightPath, textNode, new Group(caretPath));
getChildren().add(textGroup);
if (SHOW_HANDLES) {
handleGroup = new Group();
handleGroup.setManaged(false);
handleGroup.getChildren().addAll(caretHandle, selectionHandle1, selectionHandle2);
getChildren().add(handleGroup);
}
// Add text
textNode.setManaged(false);
textNode.getStyleClass().add("text");
textNode.fontProperty().bind(control.fontProperty());
textNode.layoutXProperty().bind(textTranslateX);
textNode.textProperty().bind(new StringBinding() {
{ bind(control.textProperty()); }
@Override protected String computeValue() {
return maskText(control.textProperty().getValueSafe());
}
});
textNode.fillProperty().bind(textFillProperty());
textNode.selectionFillProperty().bind(new ObjectBinding() {
{ bind(highlightTextFillProperty(), textFillProperty(), control.focusedProperty()); }
@Override protected Paint computeValue() {
return control.isFocused() ? highlightTextFillProperty().get() : textFillProperty().get();
}
});
// updated by listener on caretPosition to ensure order
updateTextNodeCaretPos(control.getCaretPosition());
registerInvalidationListener(control.selectionProperty(), e -> updateSelection());
// Add selection
selectionHighlightPath.setManaged(false);
selectionHighlightPath.setStroke(null);
selectionHighlightPath.layoutXProperty().bind(textTranslateX);
selectionHighlightPath.visibleProperty().bind(control.anchorProperty().isNotEqualTo(control.caretPositionProperty()).and(control.focusedProperty()));
selectionHighlightPath.fillProperty().bind(highlightFillProperty());
registerInvalidationListener(textNode.selectionShapeProperty(), e -> updateSelection());
// Add caret
caretPath.setManaged(false);
caretPath.setStrokeWidth(1);
caretPath.fillProperty().bind(textFillProperty());
caretPath.strokeProperty().bind(textFillProperty());
// modifying visibility of the caret forces a layout-pass (RT-32373), so
// instead we modify the opacity.
caretPath.opacityProperty().bind(new DoubleBinding() {
{ bind(caretVisibleProperty()); }
@Override protected double computeValue() {
return caretVisibleProperty().get() ? 1.0 : 0.0;
}
});
caretPath.layoutXProperty().bind(textTranslateX);
textNode.caretShapeProperty().addListener(observable -> {
caretPath.getElements().setAll(textNode.caretShapeProperty().get());
if (caretPath.getElements().size() == 0) {
// The caret pos is invalid.
updateTextNodeCaretPos(control.getCaretPosition());
} else if (caretPath.getElements().size() == 4) {
// The caret is split. Ignore and keep the previous width value.
} else {
caretWidth = Math.round(caretPath.getLayoutBounds().getWidth());
}
});
// Be sure to get the control to request layout when the font changes,
// since this will affect the pref height and pref width.
registerInvalidationListener(control.fontProperty(), e -> {
// I do both so that any cached values for prefWidth/height are cleared.
// The problem is that the skin is unmanaged and so calling request layout
// doesn't walk up the tree all the way. I think....
control.requestLayout();
getSkinnable().requestLayout();
});
registerChangeListener(control.prefColumnCountProperty(), e -> getSkinnable().requestLayout());
if (control.isFocused()) setCaretAnimating(true);
registerInvalidationListener(control.alignmentProperty(), e -> {
if (control.getWidth() > 0) {
updateTextPos();
updateCaretOff();
control.requestLayout();
}
});
usePromptText = new BooleanBinding() {
{ bind(control.textProperty(),
control.promptTextProperty(),
promptTextFillProperty()); }
@Override protected boolean computeValue() {
String txt = control.getText();
String promptTxt = control.getPromptText();
return ((txt == null || txt.isEmpty()) &&
promptTxt != null && !promptTxt.isEmpty() &&
!getPromptTextFill().equals(Color.TRANSPARENT));
}
};
promptTextFillProperty().addListener(observable -> {
updateTextPos();
});
registerInvalidationListener(control.textProperty(), e -> {
if (!behavior.isEditing()) {
// Text changed, but not by user action
updateTextPos();
}
});
if (usePromptText.get()) {
createPromptNode();
}
registerInvalidationListener(usePromptText, e -> {
createPromptNode();
control.requestLayout();
});
if (SHOW_HANDLES) {
selectionHandle1.setRotate(180);
EventHandler handlePressHandler = e -> {
pressX = e.getX();
pressY = e.getY();
e.consume();
};
caretHandle.setOnMousePressed(handlePressHandler);
selectionHandle1.setOnMousePressed(handlePressHandler);
selectionHandle2.setOnMousePressed(handlePressHandler);
caretHandle.setOnMouseDragged(e -> {
Point2D p = new Point2D(caretHandle.getLayoutX() + e.getX() + pressX - textNode.getLayoutX(),
caretHandle.getLayoutY() + e.getY() - pressY - 6);
HitInfo hit = textNode.hitTest(p);
positionCaret(hit, false);
e.consume();
});
selectionHandle1.setOnMouseDragged(new EventHandler() {
@Override public void handle(MouseEvent e) {
TextField control = getSkinnable();
Point2D tp = textNode.localToScene(0, 0);
Point2D p = new Point2D(e.getSceneX() - tp.getX() + 10/*??*/ - pressX + selectionHandle1.getWidth() / 2,
e.getSceneY() - tp.getY() - pressY - 6);
HitInfo hit = textNode.hitTest(p);
if (control.getAnchor() < control.getCaretPosition()) {
// Swap caret and anchor
control.selectRange(control.getCaretPosition(), control.getAnchor());
}
int pos = hit.getInsertionIndex();
if (pos >= 0) {
if (pos >= control.getAnchor() - 1) {
pos = Math.max(0, control.getAnchor() - 1);
}
positionCaret(pos, hit.isLeading(), true);
}
e.consume();
}
});
selectionHandle2.setOnMouseDragged(new EventHandler() {
@Override public void handle(MouseEvent e) {
TextField control = getSkinnable();
Point2D tp = textNode.localToScene(0, 0);
Point2D p = new Point2D(e.getSceneX() - tp.getX() + 10/*??*/ - pressX + selectionHandle2.getWidth() / 2,
e.getSceneY() - tp.getY() - pressY - 6);
HitInfo hit = textNode.hitTest(p);
if (control.getAnchor() > control.getCaretPosition()) {
// Swap caret and anchor
control.selectRange(control.getCaretPosition(), control.getAnchor());
}
int pos = hit.getInsertionIndex();
if (pos > 0) {
if (pos <= control.getAnchor()) {
pos = Math.min(control.getAnchor() + 1, control.getLength());
}
positionCaret(pos, hit.isLeading(), true);
}
e.consume();
}
});
}
}
/* *************************************************************************
* *
* Public API *
* *
**************************************************************************/
/** {@inheritDoc} */
@Override public void dispose() {
if (getSkinnable() == null) return;
getChildren().removeAll(textGroup, handleGroup);
super.dispose();
if (behavior != null) {
behavior.dispose();
}
}
/** {@inheritDoc} */
@Override protected double computePrefWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
TextField textField = getSkinnable();
double characterWidth = fontMetrics.get().getCharWidth('W');
int columnCount = textField.getPrefColumnCount();
return columnCount * characterWidth + leftInset + rightInset;
}
/** {@inheritDoc} */
@Override protected double computeMinHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
return computePrefHeight(width, topInset, rightInset, bottomInset, leftInset);
}
/** {@inheritDoc} */
@Override protected double computePrefHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
return topInset + textNode.getLayoutBounds().getHeight() + bottomInset;
}
/** {@inheritDoc} */
@Override protected double computeMaxHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
return getSkinnable().prefHeight(width);
}
/** {@inheritDoc} */
@Override public double computeBaselineOffset(double topInset, double rightInset, double bottomInset, double leftInset) {
return topInset + textNode.getBaselineOffset();
}
// Public for behavior
/**
* Replaces a range of characters with the given text.
*
* Call this implementation from behavior classes instead of the
* one provided on TextInputControl to ensure that the text
* scrolls as needed.
*
* @param start The starting index in the range, inclusive. This must be >= 0 and < the end.
* @param end The ending index in the range, exclusive. This is one-past the last character to
* delete (consistent with the String manipulation methods). This must be > the start,
* and <= the length of the text.
* @param txt The text that is to replace the range. This must not be null.
* @see TextField#replaceText(int, int, String)
*/
public void replaceText(int start, int end, String txt) {
final double textMaxXOld = textNode.getBoundsInParent().getMaxX();
final double caretMaxXOld = caretPath.getLayoutBounds().getMaxX() + textTranslateX.get();
getSkinnable().replaceText(start, end, txt);
scrollAfterDelete(textMaxXOld, caretMaxXOld);
}
// Public for behavior
/**
* Deletes the character that follows or precedes the current
* caret position from the text if there is no selection, or
* deletes the selection if there is one.
*
* Call this implementation from behavior classes instead of the
* one provided on TextInputControl to ensure that the text
* scrolls as needed.
*
* @param previous whether to delete the preceding character.
*/
public void deleteChar(boolean previous) {
final double textMaxXOld = textNode.getBoundsInParent().getMaxX();
final double caretMaxXOld = caretPath.getLayoutBounds().getMaxX() + textTranslateX.get();
if (previous ? getSkinnable().deletePreviousChar() : getSkinnable().deleteNextChar()) {
scrollAfterDelete(textMaxXOld, caretMaxXOld);
}
}
// Public for behavior
/**
* Performs a hit test, mapping point to index in the content.
*
* @param x the x coordinate of the point.
* @param y the y coordinate of the point.
* @return a {@code HitInfo} object describing the index and forward bias.
*/
public HitInfo getIndex(double x, double y) {
// adjust the event to be in the same coordinate space as the
// text content of the textInputControl
Point2D p = new Point2D(x - textTranslateX.get() - snappedLeftInset(),
y - snappedTopInset());
return textNode.hitTest(p);
}
// Public for behavior
/**
* Moves the caret to the specified position.
*
* @param hit the new position and forward bias of the caret.
* @param select whether to extend selection to the new position.
*/
public void positionCaret(HitInfo hit, boolean select) {
positionCaret(hit.getInsertionIndex(), hit.isLeading(), select);
}
private void positionCaret(int pos, boolean leading, boolean select) {
TextField textField = getSkinnable();
if (select) {
textField.selectPositionCaret(pos);
} else {
textField.positionCaret(pos);
}
setForwardBias(leading);
}
/** {@inheritDoc} */
@Override public Rectangle2D getCharacterBounds(int index) {
double x, y;
double width, height;
if (index == textNode.getText().length()) {
Bounds textNodeBounds = textNode.getBoundsInLocal();
x = textNodeBounds.getMaxX();
y = 0;
width = 0;
height = textNodeBounds.getMaxY();
} else {
characterBoundingPath.getElements().clear();
characterBoundingPath.getElements().addAll(textNode.rangeShape(index, index + 1));
characterBoundingPath.setLayoutX(textNode.getLayoutX());
characterBoundingPath.setLayoutY(textNode.getLayoutY());
Bounds bounds = characterBoundingPath.getBoundsInLocal();
x = bounds.getMinX();
y = bounds.getMinY();
// Sometimes the bounds is empty, in which case we must ignore the width/height
width = bounds.isEmpty() ? 0 : bounds.getWidth();
height = bounds.isEmpty() ? 0 : bounds.getHeight();
}
Bounds textBounds = textGroup.getBoundsInParent();
return new Rectangle2D(x + textBounds.getMinX() + textTranslateX.get(),
y + textBounds.getMinY(), width, height);
}
/** {@inheritDoc} */
@Override protected PathElement[] getUnderlineShape(int start, int end) {
return textNode.underlineShape(start, end);
}
/** {@inheritDoc} */
@Override protected PathElement[] getRangeShape(int start, int end) {
return textNode.rangeShape(start, end);
}
/** {@inheritDoc} */
@Override protected void addHighlight(List extends Node> nodes, int start) {
textGroup.getChildren().addAll(nodes);
}
/** {@inheritDoc} */
@Override protected void removeHighlight(List extends Node> nodes) {
textGroup.getChildren().removeAll(nodes);
}
/** {@inheritDoc} */
@Override public void moveCaret(TextUnit unit, Direction dir, boolean select) {
switch (unit) {
case CHARACTER:
switch (dir) {
case LEFT:
case RIGHT:
nextCharacterVisually(dir == Direction.RIGHT);
break;
default:
throw new IllegalArgumentException(""+dir);
}
break;
default:
throw new IllegalArgumentException(""+unit);
}
}
private void nextCharacterVisually(boolean moveRight) {
if (isRTL()) {
// Text node is mirrored.
moveRight = !moveRight;
}
Bounds caretBounds = caretPath.getLayoutBounds();
if (caretPath.getElements().size() == 4) {
// The caret is split
// TODO: Find a better way to get the primary caret position
// instead of depending on the internal implementation.
// See RT-25465.
caretBounds = new Path(caretPath.getElements().get(0), caretPath.getElements().get(1)).getLayoutBounds();
}
double hitX = moveRight ? caretBounds.getMaxX() : caretBounds.getMinX();
double hitY = (caretBounds.getMinY() + caretBounds.getMaxY()) / 2;
HitInfo hit = textNode.hitTest(new Point2D(hitX, hitY));
boolean leading = hit.isLeading();
Path charShape = new Path(textNode.rangeShape(hit.getCharIndex(), hit.getCharIndex() + 1));
if ((moveRight && charShape.getLayoutBounds().getMaxX() > caretBounds.getMaxX()) ||
(!moveRight && charShape.getLayoutBounds().getMinX() < caretBounds.getMinX())) {
leading = !leading;
}
positionCaret(hit.getInsertionIndex(), leading, false);
}
/** {@inheritDoc} */
@Override protected void layoutChildren(final double x, final double y,
final double w, final double h) {
super.layoutChildren(x, y, w, h);
if (textNode != null) {
double textY;
final Bounds textNodeBounds = textNode.getLayoutBounds();
final double ascent = textNode.getBaselineOffset();
final double descent = textNodeBounds.getHeight() - ascent;
switch (getSkinnable().getAlignment().getVpos()) {
case TOP:
textY = ascent;
break;
case CENTER:
textY = (ascent + textGroup.getHeight() - descent) / 2;
break;
case BOTTOM:
default:
textY = textGroup.getHeight() - descent;
}
textNode.setY(textY);
if (promptNode != null) {
promptNode.setY(textY);
}
if (getSkinnable().getWidth() > 0) {
updateTextPos();
updateCaretOff();
}
}
if (SHOW_HANDLES) {
handleGroup.setLayoutX(x + caretWidth / 2);
handleGroup.setLayoutY(y);
// Resize handles for caret and anchor.
// IndexRange selection = textField.getSelection();
selectionHandle1.resize(selectionHandle1.prefWidth(-1),
selectionHandle1.prefHeight(-1));
selectionHandle2.resize(selectionHandle2.prefWidth(-1),
selectionHandle2.prefHeight(-1));
caretHandle.resize(caretHandle.prefWidth(-1),
caretHandle.prefHeight(-1));
Bounds b = caretPath.getBoundsInParent();
caretHandle.setLayoutY(b.getMaxY() - 1);
//selectionHandle1.setLayoutY(b.getMaxY() - 1);
selectionHandle1.setLayoutY(b.getMinY() - selectionHandle1.getHeight() + 1);
selectionHandle2.setLayoutY(b.getMaxY() - 1);
}
}
private HPos getHAlignment() {
HPos hPos = getSkinnable().getAlignment().getHpos();
return hPos;
}
/** {@inheritDoc} */
@Override public Point2D getMenuPosition() {
Point2D p = super.getMenuPosition();
if (p != null) {
p = new Point2D(Math.max(0, p.getX() - textNode.getLayoutX() - snappedLeftInset() + textTranslateX.get()),
Math.max(0, p.getY() - textNode.getLayoutY() - snappedTopInset()));
}
return p;
}
/** {@inheritDoc} */
@Override protected String maskText(String txt) {
if (getSkinnable() instanceof PasswordField) {
int n = txt.length();
StringBuilder passwordBuilder = new StringBuilder(n);
for (int i = 0; i < n; i++) {
passwordBuilder.append(BULLET);
}
return passwordBuilder.toString();
} else {
return txt;
}
}
/** {@inheritDoc} */
@Override protected Object queryAccessibleAttribute(AccessibleAttribute attribute, Object... parameters) {
switch (attribute) {
case BOUNDS_FOR_RANGE:
case OFFSET_AT_POINT:
return textNode.queryAccessibleAttribute(attribute, parameters);
default: return super.queryAccessibleAttribute(attribute, parameters);
}
}
/* ************************************************************************
*
* Private implementation
*
**************************************************************************/
@Override
TextInputControlBehavior getBehavior() {
return behavior;
}
private void updateTextNodeCaretPos(int pos) {
if (pos == 0 || isForwardBias()) {
textNode.setCaretPosition(pos);
} else {
textNode.setCaretPosition(pos - 1);
}
textNode.caretBiasProperty().set(isForwardBias());
}
private void createPromptNode() {
if (promptNode != null || !usePromptText.get()) return;
promptNode = new Text();
textGroup.getChildren().add(0, promptNode);
promptNode.setManaged(false);
promptNode.getStyleClass().add("text");
promptNode.visibleProperty().bind(usePromptText);
promptNode.fontProperty().bind(getSkinnable().fontProperty());
promptNode.textProperty().bind(getSkinnable().promptTextProperty());
promptNode.fillProperty().bind(promptTextFillProperty());
updateSelection();
}
private void updateSelection() {
TextField textField = getSkinnable();
IndexRange newValue = textField.getSelection();
if (newValue == null || newValue.getLength() == 0) {
textNode.selectionStartProperty().set(-1);
textNode.selectionEndProperty().set(-1);
} else {
textNode.selectionStartProperty().set(newValue.getStart());
// This intermediate value is needed to force selection shape layout.
textNode.selectionEndProperty().set(newValue.getStart());
textNode.selectionEndProperty().set(newValue.getEnd());
}
PathElement[] elements = textNode.selectionShapeProperty().get();
if (elements == null) {
selectionHighlightPath.getElements().clear();
} else {
selectionHighlightPath.getElements().setAll(elements);
}
if (SHOW_HANDLES && newValue != null && newValue.getLength() > 0) {
int caretPos = textField.getCaretPosition();
int anchorPos = textField.getAnchor();
{
// Position the handle for the anchor. This could be handle1 or handle2.
// Do this before positioning the handle for the caret.
updateTextNodeCaretPos(anchorPos);
Bounds b = caretPath.getBoundsInParent();
if (caretPos < anchorPos) {
selectionHandle2.setLayoutX(b.getMinX() - selectionHandle2.getWidth() / 2);
} else {
selectionHandle1.setLayoutX(b.getMinX() - selectionHandle1.getWidth() / 2);
}
}
{
// Position handle for the caret. This could be handle1 or handle2.
updateTextNodeCaretPos(caretPos);
Bounds b = caretPath.getBoundsInParent();
if (caretPos < anchorPos) {
selectionHandle1.setLayoutX(b.getMinX() - selectionHandle1.getWidth() / 2);
} else {
selectionHandle2.setLayoutX(b.getMinX() - selectionHandle2.getWidth() / 2);
}
}
}
}
/**
* Updates the textTranslateX value for the Text node position. This is
* done for general layout, but care is taken to avoid resetting the
* position when there's a need to scroll the text due to caret movement,
* or when editing text that overflows on either side.
*/
private void updateTextPos() {
double oldX = textTranslateX.get();
double newX;
double textNodeWidth = textNode.getLayoutBounds().getWidth();
switch (getHAlignment()) {
case CENTER:
double midPoint = textRight.get() / 2;
if (usePromptText.get()) {
// If a prompt is shown (which implies that the text is
// empty), then we align the Text node so that the caret will
// appear at the left of the centered prompt.
newX = midPoint - promptNode.getLayoutBounds().getWidth() / 2;
if (newX > 0) {
promptNode.setLayoutX(newX);
} else {
// Align to left when prompt text length is more than text field width
promptNode.setLayoutX(caretWidth / 2);
}
} else {
newX = midPoint - textNodeWidth / 2;
// Update if there is space on the right
if (newX + textNodeWidth <= textRight.get() - caretWidth / 2) {
textTranslateX.set(newX);
} else if (newX < 0 && oldX > caretWidth / 2) {
textTranslateX.set(caretWidth / 2);
}
}
break;
case RIGHT:
if (usePromptText.get()) {
newX = textRight.get() - promptNode.getLayoutBounds().getWidth() - caretWidth / 2;
if (newX > 0) {
promptNode.setLayoutX(newX);
} else {
// Align to left when prompt text length is more than text field width
promptNode.setLayoutX(caretWidth / 2);
}
} else {
newX = textRight.get() - textNodeWidth - caretWidth / 2;
// Update if there is space on the right
if (newX > oldX || newX > 0) {
textTranslateX.set(newX);
} else if (newX < 0 && oldX > caretWidth / 2) {
textTranslateX.set(caretWidth / 2);
}
}
break;
case LEFT:
default:
newX = caretWidth / 2;
// Update if there is space on either side.
if (newX < oldX || newX + textNodeWidth <= textRight.get()) {
textTranslateX.set(newX);
}
if (usePromptText.get()) {
promptNode.layoutXProperty().set(newX);
}
}
}
// should be called when the padding changes, or the text box width, or
// the dot moves
private void updateCaretOff() {
double delta = 0.0;
double caretX = caretPath.getLayoutBounds().getMinX() + textTranslateX.get();
// If the caret position is less than or equal to the left edge of the
// clip then the caret will be clipped. We want the caret to end up
// being positioned one pixel right of the clip's left edge. The same
// applies on the right edge (but going the other direction of course).
if (caretX < 0) {
// I'll end up with a negative number
delta = caretX;
} else if (caretX > (textRight.get() - caretWidth)) {
// I'll end up with a positive number
delta = caretX - (textRight.get() - caretWidth);
}
// If delta is negative, then translate in the negative direction
// to cause the text to scroll to the right. Vice-versa for positive.
switch (getHAlignment()) {
case CENTER:
textTranslateX.set(textTranslateX.get() - delta);
break;
case RIGHT:
textTranslateX.set(Math.max(textTranslateX.get() - delta,
textRight.get() - textNode.getLayoutBounds().getWidth() -
caretWidth / 2));
break;
case LEFT:
default:
textTranslateX.set(Math.min(textTranslateX.get() - delta,
caretWidth / 2));
}
if (SHOW_HANDLES) {
caretHandle.setLayoutX(caretX - caretHandle.getWidth() / 2);
}
}
private void scrollAfterDelete(double textMaxXOld, double caretMaxXOld) {
final Bounds textLayoutBounds = textNode.getLayoutBounds();
final Bounds textBounds = textNode.localToParent(textLayoutBounds);
final Bounds clipBounds = clip.getBoundsInParent();
final Bounds caretBounds = caretPath.getLayoutBounds();
if (textBounds.getMinX() < clipBounds.getMinX() + caretWidth / 2 &&
textBounds.getMaxX() <= clipBounds.getMaxX()) {
double delta = caretMaxXOld - caretBounds.getMaxX() - textTranslateX.get();
if (textBounds.getMaxX() + delta < clipBounds.getMaxX()) {
if (textMaxXOld <= clipBounds.getMaxX()) {
delta = textMaxXOld - textBounds.getMaxX();
} else {
delta = clipBounds.getMaxX() - textBounds.getMaxX();
}
}
textTranslateX.set(textTranslateX.get() + delta);
}
updateCaretOff();
}
// for testing only!
Text getTextNode() {
return textNode;
}
// for testing only!
Text getPromptNode() {
return promptNode;
}
// for testing only!
double getTextTranslateX() {
return textTranslateX.get();
}
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy