modules/controls/src/main/java/javafx/scene/control/skin/TextFieldSkin.java
Print this page
rev 9240 : 8076423: JEP 253: Prepare JavaFX UI Controls & CSS APIs for Modularization
@@ -21,12 +21,16 @@
* 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 com.sun.javafx.scene.control.skin;
+package javafx.scene.control.skin;
+import com.sun.javafx.scene.control.behavior.BehaviorBase;
+import com.sun.javafx.scene.control.behavior.TextAreaBehavior;
+import com.sun.javafx.scene.control.behavior.TextInputControlBehavior;
+import com.sun.javafx.scene.control.skin.Utils;
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;
@@ -39,10 +43,13 @@
import javafx.geometry.Point2D;
import javafx.geometry.Rectangle2D;
import javafx.scene.AccessibleAttribute;
import javafx.scene.Group;
import javafx.scene.Node;
+import javafx.scene.control.Accordion;
+import javafx.scene.control.Button;
+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;
@@ -56,13 +63,25 @@
import com.sun.javafx.scene.control.behavior.TextFieldBehavior;
import com.sun.javafx.scene.control.behavior.PasswordFieldBehavior;
import com.sun.javafx.scene.text.HitInfo;
/**
- * Text field skin.
+ * Default skin implementation for the {@link TextField} control.
+ *
+ * @see TextField
+ * @since 9
*/
-public class TextFieldSkin extends TextInputControlSkin<TextField, TextFieldBehavior> {
+public class TextFieldSkin extends TextInputControlSkin<TextField> {
+
+ /**************************************************************************
+ *
+ * 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.
@@ -100,56 +119,57 @@
private ObservableBooleanValue usePromptText;
private DoubleProperty textTranslateX = new SimpleDoubleProperty(this, "textTranslateX");
private double caretWidth;
/**
- * Function to translate the text control's "dot" into the caret
- * position in the Text node. This is possibly only meaningful for
- * the PasswordField where the echoChar could be more than one
- * character.
- */
- protected int translateCaretPosition(int cp) { return cp; }
- protected Point2D translateCaretPosition(Point2D p) { return p; }
-
- /**
* Right edge of the text region sans padding
*/
- protected ObservableDoubleValue textRight;
+ private ObservableDoubleValue textRight;
private double pressX, pressY; // For dragging handles on embedded
// For use with PasswordField
- public static final char BULLET = '\u25cf';
+ static final char BULLET = '\u25cf';
- /**
- * Create a new TextFieldSkin.
- * @param textField not null
- */
- public TextFieldSkin(final TextField textField) {
- this(textField, (textField instanceof PasswordField)
- ? new PasswordFieldBehavior((PasswordField)textField)
- : new TextFieldBehavior(textField));
- }
- public TextFieldSkin(final TextField textField, final TextFieldBehavior behavior) {
- super(textField, behavior);
- behavior.setTextFieldSkin(this);
+ /**************************************************************************
+ *
+ * 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);
- textField.caretPositionProperty().addListener((observable, oldValue, newValue) -> {
- if (textField.getWidth() > 0) {
- updateTextNodeCaretPos(textField.getCaretPosition());
+ // 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());
+
+ control.caretPositionProperty().addListener((observable, oldValue, newValue) -> {
+ if (control.getWidth() > 0) {
+ updateTextNodeCaretPos(control.getCaretPosition());
if (!isForwardBias()) {
setForwardBias(true);
}
updateCaretOff();
}
});
forwardBiasProperty().addListener(observable -> {
- if (textField.getWidth() > 0) {
- updateTextNodeCaretPos(textField.getCaretPosition());
+ if (control.getWidth() > 0) {
+ updateTextNodeCaretPos(control.getCaretPosition());
updateCaretOff();
}
});
textRight = new DoubleBinding() {
@@ -180,109 +200,109 @@
}
// Add text
textNode.setManaged(false);
textNode.getStyleClass().add("text");
- textNode.fontProperty().bind(textField.fontProperty());
+ textNode.fontProperty().bind(control.fontProperty());
textNode.layoutXProperty().bind(textTranslateX);
textNode.textProperty().bind(new StringBinding() {
- { bind(textField.textProperty()); }
+ { bind(control.textProperty()); }
@Override protected String computeValue() {
- return maskText(textField.textProperty().getValueSafe());
+ return maskText(control.textProperty().getValueSafe());
}
});
- textNode.fillProperty().bind(textFill);
+ textNode.fillProperty().bind(textFillProperty());
textNode.impl_selectionFillProperty().bind(new ObjectBinding<Paint>() {
- { bind(highlightTextFill, textFill, textField.focusedProperty()); }
+ { bind(highlightTextFillProperty(), textFillProperty(), control.focusedProperty()); }
@Override protected Paint computeValue() {
- return textField.isFocused() ? highlightTextFill.get() : textFill.get();
+ return control.isFocused() ? highlightTextFillProperty().get() : textFillProperty().get();
}
});
// updated by listener on caretPosition to ensure order
- updateTextNodeCaretPos(textField.getCaretPosition());
- textField.selectionProperty().addListener(observable -> {
+ updateTextNodeCaretPos(control.getCaretPosition());
+ control.selectionProperty().addListener(observable -> {
updateSelection();
});
// Add selection
selectionHighlightPath.setManaged(false);
selectionHighlightPath.setStroke(null);
selectionHighlightPath.layoutXProperty().bind(textTranslateX);
- selectionHighlightPath.visibleProperty().bind(textField.anchorProperty().isNotEqualTo(textField.caretPositionProperty()).and(textField.focusedProperty()));
- selectionHighlightPath.fillProperty().bind(highlightFill);
+ selectionHighlightPath.visibleProperty().bind(control.anchorProperty().isNotEqualTo(control.caretPositionProperty()).and(control.focusedProperty()));
+ selectionHighlightPath.fillProperty().bind(highlightFillProperty());
textNode.impl_selectionShapeProperty().addListener(observable -> {
updateSelection();
});
// Add caret
caretPath.setManaged(false);
caretPath.setStrokeWidth(1);
- caretPath.fillProperty().bind(textFill);
- caretPath.strokeProperty().bind(textFill);
+ 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(caretVisible); }
+ { bind(caretVisibleProperty()); }
@Override protected double computeValue() {
- return caretVisible.get() ? 1.0 : 0.0;
+ return caretVisibleProperty().get() ? 1.0 : 0.0;
}
});
caretPath.layoutXProperty().bind(textTranslateX);
textNode.impl_caretShapeProperty().addListener(observable -> {
caretPath.getElements().setAll(textNode.impl_caretShapeProperty().get());
if (caretPath.getElements().size() == 0) {
// The caret pos is invalid.
- updateTextNodeCaretPos(textField.getCaretPosition());
+ 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.
- textField.fontProperty().addListener(observable -> {
+ control.fontProperty().addListener(observable -> {
// 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....
- textField.requestLayout();
+ control.requestLayout();
getSkinnable().requestLayout();
});
- registerChangeListener(textField.prefColumnCountProperty(), "prefColumnCount");
- if (textField.isFocused()) setCaretAnimating(true);
+ registerChangeListener(control.prefColumnCountProperty(), e -> getSkinnable().requestLayout());
+ if (control.isFocused()) setCaretAnimating(true);
- textField.alignmentProperty().addListener(observable -> {
- if (textField.getWidth() > 0) {
+ control.alignmentProperty().addListener(observable -> {
+ if (control.getWidth() > 0) {
updateTextPos();
updateCaretOff();
- textField.requestLayout();
+ control.requestLayout();
}
});
usePromptText = new BooleanBinding() {
- { bind(textField.textProperty(),
- textField.promptTextProperty(),
- promptTextFill); }
+ { bind(control.textProperty(),
+ control.promptTextProperty(),
+ promptTextFillProperty()); }
@Override protected boolean computeValue() {
- String txt = textField.getText();
- String promptTxt = textField.getPromptText();
+ String txt = control.getText();
+ String promptTxt = control.getPromptText();
return ((txt == null || txt.isEmpty()) &&
promptTxt != null && !promptTxt.isEmpty() &&
- !promptTextFill.get().equals(Color.TRANSPARENT));
+ !getPromptTextFill().equals(Color.TRANSPARENT));
}
};
- promptTextFill.addListener(observable -> {
+ promptTextFillProperty().addListener(observable -> {
updateTextPos();
});
- textField.textProperty().addListener(observable -> {
- if (!getBehavior().isEditing()) {
+ control.textProperty().addListener(observable -> {
+ if (!behavior.isEditing()) {
// Text changed, but not by user action
updateTextPos();
}
});
@@ -290,11 +310,11 @@
createPromptNode();
}
usePromptText.addListener(observable -> {
createPromptNode();
- textField.requestLayout();
+ control.requestLayout();
});
if (SHOW_HANDLES) {
selectionHandle1.setRotate(180);
@@ -309,349 +329,175 @@
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.impl_hitTestChar(translateCaretPosition(p));
+ HitInfo hit = textNode.impl_hitTestChar(p);
positionCaret(hit, false);
e.consume();
});
selectionHandle1.setOnMouseDragged(new EventHandler<MouseEvent>() {
@Override public void handle(MouseEvent e) {
- TextField textField = getSkinnable();
+ 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.impl_hitTestChar(translateCaretPosition(p));
+ HitInfo hit = textNode.impl_hitTestChar(p);
int pos = hit.getCharIndex();
- if (textField.getAnchor() < textField.getCaretPosition()) {
+ if (control.getAnchor() < control.getCaretPosition()) {
// Swap caret and anchor
- textField.selectRange(textField.getCaretPosition(), textField.getAnchor());
+ control.selectRange(control.getCaretPosition(), control.getAnchor());
}
if (pos >= 0) {
- if (pos >= textField.getAnchor() - 1) {
- hit.setCharIndex(Math.max(0, textField.getAnchor() - 1));
+ if (pos >= control.getAnchor() - 1) {
+ hit.setCharIndex(Math.max(0, control.getAnchor() - 1));
}
positionCaret(hit, true);
}
e.consume();
}
});
selectionHandle2.setOnMouseDragged(new EventHandler<MouseEvent>() {
@Override public void handle(MouseEvent e) {
- TextField textField = getSkinnable();
+ 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.impl_hitTestChar(translateCaretPosition(p));
+ HitInfo hit = textNode.impl_hitTestChar(p);
int pos = hit.getCharIndex();
- if (textField.getAnchor() > textField.getCaretPosition()) {
+ if (control.getAnchor() > control.getCaretPosition()) {
// Swap caret and anchor
- textField.selectRange(textField.getCaretPosition(), textField.getAnchor());
+ control.selectRange(control.getCaretPosition(), control.getAnchor());
}
if (pos > 0) {
- if (pos <= textField.getAnchor()) {
- hit.setCharIndex(Math.min(textField.getAnchor() + 1, textField.getLength()));
+ if (pos <= control.getAnchor()) {
+ hit.setCharIndex(Math.min(control.getAnchor() + 1, control.getLength()));
}
positionCaret(hit, true);
}
e.consume();
}
});
}
}
- private void updateTextNodeCaretPos(int pos) {
- if (pos == 0 || isForwardBias()) {
- textNode.setImpl_caretPosition(pos);
- } else {
- textNode.setImpl_caretPosition(pos - 1);
- }
- textNode.impl_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());
+ /***************************************************************************
+ * *
+ * Public API *
+ * *
+ **************************************************************************/
- promptNode.textProperty().bind(getSkinnable().promptTextProperty());
- promptNode.fillProperty().bind(promptTextFill);
- updateSelection();
- }
+ /** {@inheritDoc} */
+ @Override public void dispose() {
+ super.dispose();
- private void updateSelection() {
- TextField textField = getSkinnable();
- IndexRange newValue = textField.getSelection();
-
- if (newValue == null || newValue.getLength() == 0) {
- textNode.impl_selectionStartProperty().set(-1);
- textNode.impl_selectionEndProperty().set(-1);
- } else {
- textNode.impl_selectionStartProperty().set(newValue.getStart());
- // This intermediate value is needed to force selection shape layout.
- textNode.impl_selectionEndProperty().set(newValue.getStart());
- textNode.impl_selectionEndProperty().set(newValue.getEnd());
+ if (behavior != null) {
+ behavior.dispose();
}
-
- PathElement[] elements = textNode.impl_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);
- }
- }
- }
- }
-
- @Override protected void handleControlPropertyChanged(String propertyReference) {
- if ("prefColumnCount".equals(propertyReference)) {
- getSkinnable().requestLayout();
- } else {
- super.handleControlPropertyChanged(propertyReference);
- }
- }
-
- @Override
- protected double computePrefWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
+ /** {@inheritDoc} */
+ @Override protected double computePrefWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
TextField textField = getSkinnable();
double characterWidth = fontMetrics.get().computeStringWidth("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
/**
- * 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;
- promptNode.setLayoutX(newX);
- } else {
- newX = midPoint - textNodeWidth / 2;
- }
- // Update if there is space on the right
- if (newX + textNodeWidth <= textRight.get()) {
- textTranslateX.set(newX);
- }
- break;
-
- case RIGHT:
- newX = textRight.get() - textNodeWidth - caretWidth / 2;
- // Update if there is space on the right
- if (newX > oldX || newX > 0) {
- textTranslateX.set(newX);
- }
- if (usePromptText.get()) {
- promptNode.setLayoutX(textRight.get() - promptNode.getLayoutBounds().getWidth() -
- 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
- protected 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 + 1);
- }
- }
-
- /**
- * Use this implementation instead of the one provided on TextInputControl.
- * updateCaretOff would get called to position the caret, but the text needs
- * to be scrolled appropriately.
+ * 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 text 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
/**
- * Use this implementation instead of the one provided on TextInputControl
- * Simply calls into TextInputControl.deletePrevious/NextChar and responds appropriately
- * based on the return value.
+ * 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();
- final boolean shouldBeep = previous ?
- !getSkinnable().deletePreviousChar() :
- !getSkinnable().deleteNextChar();
-
- if (shouldBeep) {
-// beep();
- } else {
+ if (previous ? getSkinnable().deletePreviousChar() : getSkinnable().deleteNextChar()) {
scrollAfterDelete(textMaxXOld, caretMaxXOld);
}
}
- public 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();
-
- switch (getHAlignment()) {
- case RIGHT:
- if (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);
- } else {
- updateTextPos();
- }
- break;
-
- case LEFT:
- case CENTER:
- default:
- 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();
- }
-
- public HitInfo getIndex(double x, double y) {
+ // 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 TextPosInfo} object describing the index and forward bias.
+ */
+ public TextPosInfo getIndex(double x, double y) {
// adjust the event to be in the same coordinate space as the
// text content of the textInputControl
- Point2D p;
-
- p = new Point2D(x - textTranslateX.get() - snappedLeftInset(),
+ Point2D p = new Point2D(x - textTranslateX.get() - snappedLeftInset(),
y - snappedTopInset());
- return textNode.impl_hitTestChar(translateCaretPosition(p));
+ return new TextPosInfo(textNode.impl_hitTestChar(p));
}
- public void positionCaret(HitInfo hit, boolean select) {
+ // 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(TextPosInfo hit, boolean select) {
TextField textField = getSkinnable();
int pos = Utils.getHitInsertionIndex(hit, textField.textProperty().getValueSafe());
if (select) {
textField.selectPositionCaret(pos);
@@ -660,10 +506,15 @@
}
setForwardBias(hit.isLeading());
}
+ private void positionCaret(HitInfo hit, boolean select) {
+ positionCaret(new TextPosInfo(hit), select);
+ }
+
+ /** {@inheritDoc} */
@Override public Rectangle2D getCharacterBounds(int index) {
double x, y;
double width, height;
if (index == textNode.getText().length()) {
Bounds textNodeBounds = textNode.getBoundsInLocal();
@@ -690,27 +541,49 @@
return new Rectangle2D(x + textBounds.getMinX() + textTranslateX.get(),
y + textBounds.getMinY(), width, height);
}
+ /** {@inheritDoc} */
@Override protected PathElement[] getUnderlineShape(int start, int end) {
return textNode.impl_getUnderlineShape(start, end);
}
+ /** {@inheritDoc} */
@Override protected PathElement[] getRangeShape(int start, int end) {
return textNode.impl_getRangeShape(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);
}
- @Override public void nextCharacterVisually(boolean moveRight) {
+ /** {@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;
}
@@ -722,19 +595,20 @@
// 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.impl_hitTestChar(translateCaretPosition(new Point2D(hitX, hitY)));
+ HitInfo hit = textNode.impl_hitTestChar(new Point2D(hitX, hitY));
Path charShape = new Path(textNode.impl_getRangeShape(hit.getCharIndex(), hit.getCharIndex() + 1));
if ((moveRight && charShape.getLayoutBounds().getMaxX() > caretBounds.getMaxX()) ||
(!moveRight && charShape.getLayoutBounds().getMinX() < caretBounds.getMinX())) {
hit.setLeading(!hit.isLeading());
}
positionCaret(hit, 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) {
@@ -786,24 +660,26 @@
selectionHandle1.setLayoutY(b.getMinY() - selectionHandle1.getHeight() + 1);
selectionHandle2.setLayoutY(b.getMaxY() - 1);
}
}
- protected HPos getHAlignment() {
+ 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++) {
@@ -814,15 +690,238 @@
} else {
return txt;
}
}
- @Override
- protected Object queryAccessibleAttribute(AccessibleAttribute attribute, Object... parameters) {
+ /** {@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
+ *
+ **************************************************************************/
+
+ TextInputControlBehavior getBehavior() {
+ return behavior;
+ }
+
+ private void updateTextNodeCaretPos(int pos) {
+ if (pos == 0 || isForwardBias()) {
+ textNode.setImpl_caretPosition(pos);
+ } else {
+ textNode.setImpl_caretPosition(pos - 1);
+ }
+ textNode.impl_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.impl_selectionStartProperty().set(-1);
+ textNode.impl_selectionEndProperty().set(-1);
+ } else {
+ textNode.impl_selectionStartProperty().set(newValue.getStart());
+ // This intermediate value is needed to force selection shape layout.
+ textNode.impl_selectionEndProperty().set(newValue.getStart());
+ textNode.impl_selectionEndProperty().set(newValue.getEnd());
+ }
+
+ PathElement[] elements = textNode.impl_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;
+ promptNode.setLayoutX(newX);
+ } else {
+ newX = midPoint - textNodeWidth / 2;
+ }
+ // Update if there is space on the right
+ if (newX + textNodeWidth <= textRight.get()) {
+ textTranslateX.set(newX);
+ }
+ break;
+
+ case RIGHT:
+ newX = textRight.get() - textNodeWidth - caretWidth / 2;
+ // Update if there is space on the right
+ if (newX > oldX || newX > 0) {
+ textTranslateX.set(newX);
+ }
+ if (usePromptText.get()) {
+ promptNode.setLayoutX(textRight.get() - promptNode.getLayoutBounds().getWidth() -
+ 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 + 1);
+ }
+ }
+
+ 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();
+
+ switch (getHAlignment()) {
+ case RIGHT:
+ if (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);
+ } else {
+ updateTextPos();
+ }
+ break;
+
+ case LEFT:
+ case CENTER:
+ default:
+ 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();
+ }
}