modules/controls/src/main/java/javafx/scene/control/skin/TextAreaSkin.java
Print this page
rev 9240 : 8076423: JEP 253: Prepare JavaFX UI Controls & CSS APIs for Modularization
@@ -21,13 +21,15 @@
* 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.skin.Utils;
import com.sun.javafx.scene.text.HitInfo;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.application.Platform;
import javafx.beans.binding.BooleanBinding;
@@ -46,10 +48,13 @@
import javafx.geometry.VPos;
import javafx.geometry.VerticalDirection;
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.ScrollPane;
import javafx.scene.control.TextArea;
import javafx.scene.input.MouseEvent;
import javafx.scene.input.ScrollEvent;
@@ -60,466 +65,223 @@
import javafx.scene.text.Text;
import javafx.util.Duration;
import java.util.List;
+import static com.sun.javafx.PlatformUtil.isMac;
+import static com.sun.javafx.PlatformUtil.isWindows;
+
/**
- * Text area skin.
+ * Default skin implementation for the {@link TextArea} control.
+ *
+ * @see TextArea
+ * @since 9
*/
-public class TextAreaSkin extends TextInputControlSkin<TextArea, TextAreaBehavior> {
+public class TextAreaSkin extends TextInputControlSkin<TextArea> {
+
+ /**************************************************************************
+ *
+ * Static fields
+ *
+ **************************************************************************/
+
+ /** A shared helper object, used only by downLines(). */
+ private static final Path tmpCaretPath = new Path();
+
+
+
+ /**************************************************************************
+ *
+ * Private fields
+ *
+ **************************************************************************/
final private TextArea textArea;
// *** NOTE: Multiple node mode is not yet fully implemented *** //
private final boolean USE_MULTIPLE_NODES = false;
+ private final TextAreaBehavior behavior;
+
private double computedMinWidth = Double.NEGATIVE_INFINITY;
private double computedMinHeight = Double.NEGATIVE_INFINITY;
private double computedPrefWidth = Double.NEGATIVE_INFINITY;
private double computedPrefHeight = Double.NEGATIVE_INFINITY;
private double widthForComputedPrefHeight = Double.NEGATIVE_INFINITY;
private double characterWidth;
private double lineHeight;
- @Override protected void invalidateMetrics() {
- computedMinWidth = Double.NEGATIVE_INFINITY;
- computedMinHeight = Double.NEGATIVE_INFINITY;
- computedPrefWidth = Double.NEGATIVE_INFINITY;
- computedPrefHeight = Double.NEGATIVE_INFINITY;
- }
+ private ContentView contentView = new ContentView();
+ private Group paragraphNodes = new Group();
- private class ContentView extends Region {
- {
- getStyleClass().add("content");
+ private Text promptNode;
+ private ObservableBooleanValue usePromptText;
- addEventHandler(MouseEvent.MOUSE_PRESSED, event -> {
- getBehavior().mousePressed(event);
- event.consume();
- });
+ private ObservableIntegerValue caretPosition;
+ private Group selectionHighlightGroup = new Group();
- addEventHandler(MouseEvent.MOUSE_RELEASED, event -> {
- getBehavior().mouseReleased(event);
- event.consume();
- });
+ private ScrollPane scrollPane;
+ private Bounds oldViewportBounds;
- addEventHandler(MouseEvent.MOUSE_DRAGGED, event -> {
- getBehavior().mouseDragged(event);
- event.consume();
- });
- }
+ private VerticalDirection scrollDirection = null;
- @Override protected ObservableList<Node> getChildren() {
- return super.getChildren();
- }
+ private Path characterBoundingPath = new Path();
- @Override public Orientation getContentBias() {
- return Orientation.HORIZONTAL;
+ private Timeline scrollSelectionTimeline = new Timeline();
+ private EventHandler<ActionEvent> scrollSelectionHandler = event -> {
+ switch (scrollDirection) {
+ case UP: {
+ // TODO Get previous offset
+ break;
}
- @Override protected double computePrefWidth(double height) {
- if (computedPrefWidth < 0) {
- double prefWidth = 0;
-
- for (Node node : paragraphNodes.getChildren()) {
- Text paragraphNode = (Text)node;
- prefWidth = Math.max(prefWidth,
- Utils.computeTextWidth(paragraphNode.getFont(),
- paragraphNode.getText(), 0));
+ case DOWN: {
+ // TODO Get next offset
+ break;
+ }
}
+ };
- prefWidth += snappedLeftInset() + snappedRightInset();
+ private double pressX, pressY; // For dragging handles on embedded
+ private boolean handlePressed;
- Bounds viewPortBounds = scrollPane.getViewportBounds();
- computedPrefWidth = Math.max(prefWidth, (viewPortBounds != null) ? viewPortBounds.getWidth() : 0);
- }
- return computedPrefWidth;
- }
+ /**
+ * Remembers horizontal position when traversing up / down.
+ */
+ double targetCaretX = -1;
- @Override
- protected double computePrefHeight(double width) {
- if (width != widthForComputedPrefHeight) {
- invalidateMetrics();
- widthForComputedPrefHeight = width;
- }
- if (computedPrefHeight < 0) {
- double wrappingWidth;
- if (width == -1) {
- wrappingWidth = 0;
- } else {
- wrappingWidth = Math.max(width - (snappedLeftInset() + snappedRightInset()), 0);
- }
- double prefHeight = 0;
+ /**************************************************************************
+ *
+ * Constructors
+ *
+ **************************************************************************/
- for (Node node : paragraphNodes.getChildren()) {
- Text paragraphNode = (Text)node;
- prefHeight += Utils.computeTextHeight(
- paragraphNode.getFont(),
- paragraphNode.getText(),
- wrappingWidth,
- paragraphNode.getBoundsType());
- }
+ /**
+ * Creates a new TextAreaSkin 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 TextAreaSkin(final TextArea control) {
+ super(control);
- prefHeight += snappedTopInset() + snappedBottomInset();
+ // install default input map for the text area control
+ this.behavior = new TextAreaBehavior(control);
+ this.behavior.setTextAreaSkin(this);
+// control.setInputMap(behavior.getInputMap());
- Bounds viewPortBounds = scrollPane.getViewportBounds();
- computedPrefHeight = Math.max(prefHeight, (viewPortBounds != null) ? viewPortBounds.getHeight() : 0);
- }
- return computedPrefHeight;
- }
+ this.textArea = control;
- @Override protected double computeMinWidth(double height) {
- if (computedMinWidth < 0) {
- double hInsets = snappedLeftInset() + snappedRightInset();
- computedMinWidth = Math.min(characterWidth + hInsets, computePrefWidth(height));
+ caretPosition = new IntegerBinding() {
+ { bind(control.caretPositionProperty()); }
+ @Override protected int computeValue() {
+ return control.getCaretPosition();
}
- return computedMinWidth;
+ };
+ caretPosition.addListener((observable, oldValue, newValue) -> {
+ targetCaretX = -1;
+ if (newValue.intValue() > oldValue.intValue()) {
+ setForwardBias(true);
}
+ });
- @Override protected double computeMinHeight(double width) {
- if (computedMinHeight < 0) {
- double vInsets = snappedTopInset() + snappedBottomInset();
- computedMinHeight = Math.min(lineHeight + vInsets, computePrefHeight(width));
- }
- return computedMinHeight;
+ forwardBiasProperty().addListener(observable -> {
+ if (control.getWidth() > 0) {
+ updateTextNodeCaretPos(control.getCaretPosition());
}
+ });
- @Override
- public void layoutChildren() {
- TextArea textArea = getSkinnable();
- double width = getWidth();
-
- // Lay out paragraphs
- final double topPadding = snappedTopInset();
- final double leftPadding = snappedLeftInset();
-
- double wrappingWidth = Math.max(width - (leftPadding + snappedRightInset()), 0);
+// setManaged(false);
- double y = topPadding;
+ // Initialize content
+ scrollPane = new ScrollPane();
+ scrollPane.setFitToWidth(control.isWrapText());
+ scrollPane.setContent(contentView);
+ getChildren().add(scrollPane);
- final List<Node> paragraphNodesChildren = paragraphNodes.getChildren();
+ getSkinnable().addEventFilter(ScrollEvent.ANY, event -> {
+ if (event.isDirect() && handlePressed) {
+ event.consume();
+ }
+ });
- for (int i = 0; i < paragraphNodesChildren.size(); i++) {
- Node node = paragraphNodesChildren.get(i);
- Text paragraphNode = (Text)node;
- paragraphNode.setWrappingWidth(wrappingWidth);
+ // Add selection
+ selectionHighlightGroup.setManaged(false);
+ selectionHighlightGroup.setVisible(false);
+ contentView.getChildren().add(selectionHighlightGroup);
- Bounds bounds = paragraphNode.getBoundsInLocal();
- paragraphNode.setLayoutX(leftPadding);
- paragraphNode.setLayoutY(y);
+ // Add content view
+ paragraphNodes.setManaged(false);
+ contentView.getChildren().add(paragraphNodes);
- y += bounds.getHeight();
+ // 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;
}
+ });
+ contentView.getChildren().add(caretPath);
- if (promptNode != null) {
- promptNode.setLayoutX(leftPadding);
- promptNode.setLayoutY(topPadding + promptNode.getBaselineOffset());
- promptNode.setWrappingWidth(wrappingWidth);
+ if (SHOW_HANDLES) {
+ contentView.getChildren().addAll(caretHandle, selectionHandle1, selectionHandle2);
}
- // Update the selection
- IndexRange selection = textArea.getSelection();
- Bounds oldCaretBounds = caretPath.getBoundsInParent();
+ scrollPane.hvalueProperty().addListener((observable, oldValue, newValue) -> {
+ getSkinnable().setScrollLeft(newValue.doubleValue() * getScrollLeftMax());
+ });
- selectionHighlightGroup.getChildren().clear();
+ scrollPane.vvalueProperty().addListener((observable, oldValue, newValue) -> {
+ getSkinnable().setScrollTop(newValue.doubleValue() * getScrollTopMax());
+ });
- int caretPos = textArea.getCaretPosition();
- int anchorPos = textArea.getAnchor();
+ // Initialize the scroll selection timeline
+ scrollSelectionTimeline.setCycleCount(Timeline.INDEFINITE);
+ List<KeyFrame> scrollSelectionFrames = scrollSelectionTimeline.getKeyFrames();
+ scrollSelectionFrames.clear();
+ scrollSelectionFrames.add(new KeyFrame(Duration.millis(350), scrollSelectionHandler));
- if (SHOW_HANDLES) {
- // Install and resize the handles for caret and anchor.
- if (selection.getLength() > 0) {
- selectionHandle1.resize(selectionHandle1.prefWidth(-1),
- selectionHandle1.prefHeight(-1));
- selectionHandle2.resize(selectionHandle2.prefWidth(-1),
- selectionHandle2.prefHeight(-1));
- } else {
- caretHandle.resize(caretHandle.prefWidth(-1),
- caretHandle.prefHeight(-1));
+ // Add initial text content
+ for (int i = 0, n = USE_MULTIPLE_NODES ? control.getParagraphs().size() : 1; i < n; i++) {
+ CharSequence paragraph = (n == 1) ? control.textProperty().getValueSafe() : control.getParagraphs().get(i);
+ addParagraphNode(i, paragraph.toString());
}
- // Position the handle for the anchor. This could be handle1 or handle2.
- // Do this before positioning the actual caret.
- if (selection.getLength() > 0) {
- int paragraphIndex = paragraphNodesChildren.size();
- int paragraphOffset = textArea.getLength() + 1;
- Text paragraphNode = null;
- do {
- paragraphNode = (Text)paragraphNodesChildren.get(--paragraphIndex);
- paragraphOffset -= paragraphNode.getText().length() + 1;
- } while (anchorPos < paragraphOffset);
-
- updateTextNodeCaretPos(anchorPos - paragraphOffset);
- caretPath.getElements().clear();
- caretPath.getElements().addAll(paragraphNode.getImpl_caretShape());
- caretPath.setLayoutX(paragraphNode.getLayoutX());
- caretPath.setLayoutY(paragraphNode.getLayoutY());
+ control.selectionProperty().addListener((observable, oldValue, newValue) -> {
+ // TODO Why do we need two calls here?
+ control.requestLayout();
+ contentView.requestLayout();
+ });
- Bounds b = caretPath.getBoundsInParent();
- if (caretPos < anchorPos) {
- selectionHandle2.setLayoutX(b.getMinX() - selectionHandle2.getWidth() / 2);
- selectionHandle2.setLayoutY(b.getMaxY() - 1);
- } else {
- selectionHandle1.setLayoutX(b.getMinX() - selectionHandle1.getWidth() / 2);
- selectionHandle1.setLayoutY(b.getMinY() - selectionHandle1.getHeight() + 1);
- }
- }
- }
+ control.wrapTextProperty().addListener((observable, oldValue, newValue) -> {
+ invalidateMetrics();
+ scrollPane.setFitToWidth(newValue);
+ });
- {
- // Position caret
- int paragraphIndex = paragraphNodesChildren.size();
- int paragraphOffset = textArea.getLength() + 1;
+ control.prefColumnCountProperty().addListener((observable, oldValue, newValue) -> {
+ invalidateMetrics();
+ updatePrefViewportWidth();
+ });
- Text paragraphNode = null;
- do {
- paragraphNode = (Text)paragraphNodesChildren.get(--paragraphIndex);
- paragraphOffset -= paragraphNode.getText().length() + 1;
- } while (caretPos < paragraphOffset);
+ control.prefRowCountProperty().addListener((observable, oldValue, newValue) -> {
+ invalidateMetrics();
+ updatePrefViewportHeight();
+ });
- updateTextNodeCaretPos(caretPos - paragraphOffset);
-
- caretPath.getElements().clear();
- caretPath.getElements().addAll(paragraphNode.getImpl_caretShape());
-
- caretPath.setLayoutX(paragraphNode.getLayoutX());
-
- // TODO: Remove this temporary workaround for RT-27533
- paragraphNode.setLayoutX(2 * paragraphNode.getLayoutX() - paragraphNode.getBoundsInParent().getMinX());
-
- caretPath.setLayoutY(paragraphNode.getLayoutY());
- if (oldCaretBounds == null || !oldCaretBounds.equals(caretPath.getBoundsInParent())) {
- scrollCaretToVisible();
- }
- }
-
- // Update selection fg and bg
- int start = selection.getStart();
- int end = selection.getEnd();
- for (int i = 0, max = paragraphNodesChildren.size(); i < max; i++) {
- Node paragraphNode = paragraphNodesChildren.get(i);
- Text textNode = (Text)paragraphNode;
- int paragraphLength = textNode.getText().length() + 1;
- if (end > start && start < paragraphLength) {
- textNode.setImpl_selectionStart(start);
- textNode.setImpl_selectionEnd(Math.min(end, paragraphLength));
-
- Path selectionHighlightPath = new Path();
- selectionHighlightPath.setManaged(false);
- selectionHighlightPath.setStroke(null);
- PathElement[] selectionShape = textNode.getImpl_selectionShape();
- if (selectionShape != null) {
- selectionHighlightPath.getElements().addAll(selectionShape);
- }
- selectionHighlightGroup.getChildren().add(selectionHighlightPath);
- selectionHighlightGroup.setVisible(true);
- selectionHighlightPath.setLayoutX(textNode.getLayoutX());
- selectionHighlightPath.setLayoutY(textNode.getLayoutY());
- updateHighlightFill();
- } else {
- textNode.setImpl_selectionStart(-1);
- textNode.setImpl_selectionEnd(-1);
- selectionHighlightGroup.setVisible(false);
- }
- start = Math.max(0, start - paragraphLength);
- end = Math.max(0, end - paragraphLength);
- }
-
- if (SHOW_HANDLES) {
- // Position handle for the caret. This could be handle1 or handle2 when
- // a selection is active.
- Bounds b = caretPath.getBoundsInParent();
- if (selection.getLength() > 0) {
- if (caretPos < anchorPos) {
- selectionHandle1.setLayoutX(b.getMinX() - selectionHandle1.getWidth() / 2);
- selectionHandle1.setLayoutY(b.getMinY() - selectionHandle1.getHeight() + 1);
- } else {
- selectionHandle2.setLayoutX(b.getMinX() - selectionHandle2.getWidth() / 2);
- selectionHandle2.setLayoutY(b.getMaxY() - 1);
- }
- } else {
- caretHandle.setLayoutX(b.getMinX() - caretHandle.getWidth() / 2 + 1);
- caretHandle.setLayoutY(b.getMaxY());
- }
- }
-
- if (scrollPane.getPrefViewportWidth() == 0
- || scrollPane.getPrefViewportHeight() == 0) {
- updatePrefViewportWidth();
- updatePrefViewportHeight();
- if (getParent() != null && scrollPane.getPrefViewportWidth() > 0
- || scrollPane.getPrefViewportHeight() > 0) {
- // Force layout of viewRect in ScrollPaneSkin
- getParent().requestLayout();
- }
- }
-
- // RT-36454: Fit to width/height only if smaller than viewport.
- // That is, grow to fit but don't shrink to fit.
- Bounds viewportBounds = scrollPane.getViewportBounds();
- boolean wasFitToWidth = scrollPane.isFitToWidth();
- boolean wasFitToHeight = scrollPane.isFitToHeight();
- boolean setFitToWidth = textArea.isWrapText() || computePrefWidth(-1) <= viewportBounds.getWidth();
- boolean setFitToHeight = computePrefHeight(width) <= viewportBounds.getHeight();
- if (wasFitToWidth != setFitToWidth || wasFitToHeight != setFitToHeight) {
- Platform.runLater(() -> {
- scrollPane.setFitToWidth(setFitToWidth);
- scrollPane.setFitToHeight(setFitToHeight);
- });
- getParent().requestLayout();
- }
- }
- }
-
- private ContentView contentView = new ContentView();
- private Group paragraphNodes = new Group();
-
- private Text promptNode;
- private ObservableBooleanValue usePromptText;
-
- private ObservableIntegerValue caretPosition;
- private Group selectionHighlightGroup = new Group();
-
- private ScrollPane scrollPane;
- private Bounds oldViewportBounds;
-
- private VerticalDirection scrollDirection = null;
-
- private Path characterBoundingPath = new Path();
-
- private Timeline scrollSelectionTimeline = new Timeline();
- private EventHandler<ActionEvent> scrollSelectionHandler = event -> {
- switch (scrollDirection) {
- case UP: {
- // TODO Get previous offset
- break;
- }
-
- case DOWN: {
- // TODO Get next offset
- break;
- }
- }
- };
-
- public static final int SCROLL_RATE = 30;
-
- private double pressX, pressY; // For dragging handles on embedded
- private boolean handlePressed;
-
- public TextAreaSkin(final TextArea textArea) {
- super(textArea, new TextAreaBehavior(textArea));
- getBehavior().setTextAreaSkin(this);
- this.textArea = textArea;
-
- caretPosition = new IntegerBinding() {
- { bind(textArea.caretPositionProperty()); }
- @Override protected int computeValue() {
- return textArea.getCaretPosition();
- }
- };
- caretPosition.addListener((observable, oldValue, newValue) -> {
- targetCaretX = -1;
- if (newValue.intValue() > oldValue.intValue()) {
- setForwardBias(true);
- }
- });
-
- forwardBiasProperty().addListener(observable -> {
- if (textArea.getWidth() > 0) {
- updateTextNodeCaretPos(textArea.getCaretPosition());
- }
- });
-
-// setManaged(false);
-
- // Initialize content
- scrollPane = new ScrollPane();
- scrollPane.setFitToWidth(textArea.isWrapText());
- scrollPane.setContent(contentView);
- getChildren().add(scrollPane);
-
- getSkinnable().addEventFilter(ScrollEvent.ANY, event -> {
- if (event.isDirect() && handlePressed) {
- event.consume();
- }
- });
-
- // Add selection
- selectionHighlightGroup.setManaged(false);
- selectionHighlightGroup.setVisible(false);
- contentView.getChildren().add(selectionHighlightGroup);
-
- // Add content view
- paragraphNodes.setManaged(false);
- contentView.getChildren().add(paragraphNodes);
-
- // Add caret
- caretPath.setManaged(false);
- caretPath.setStrokeWidth(1);
- caretPath.fillProperty().bind(textFill);
- caretPath.strokeProperty().bind(textFill);
- // modifying visibility of the caret forces a layout-pass (RT-32373), so
- // instead we modify the opacity.
- caretPath.opacityProperty().bind(new DoubleBinding() {
- { bind(caretVisible); }
- @Override protected double computeValue() {
- return caretVisible.get() ? 1.0 : 0.0;
- }
- });
- contentView.getChildren().add(caretPath);
-
- if (SHOW_HANDLES) {
- contentView.getChildren().addAll(caretHandle, selectionHandle1, selectionHandle2);
- }
-
- scrollPane.hvalueProperty().addListener((observable, oldValue, newValue) -> {
- getSkinnable().setScrollLeft(newValue.doubleValue() * getScrollLeftMax());
- });
-
- scrollPane.vvalueProperty().addListener((observable, oldValue, newValue) -> {
- getSkinnable().setScrollTop(newValue.doubleValue() * getScrollTopMax());
- });
-
- // Initialize the scroll selection timeline
- scrollSelectionTimeline.setCycleCount(Timeline.INDEFINITE);
- List<KeyFrame> scrollSelectionFrames = scrollSelectionTimeline.getKeyFrames();
- scrollSelectionFrames.clear();
- scrollSelectionFrames.add(new KeyFrame(Duration.millis(350), scrollSelectionHandler));
-
- // Add initial text content
- for (int i = 0, n = USE_MULTIPLE_NODES ? textArea.getParagraphs().size() : 1; i < n; i++) {
- CharSequence paragraph = (n == 1) ? textArea.textProperty().getValueSafe() : textArea.getParagraphs().get(i);
- addParagraphNode(i, paragraph.toString());
- }
-
- textArea.selectionProperty().addListener((observable, oldValue, newValue) -> {
- // TODO Why do we need two calls here?
- textArea.requestLayout();
- contentView.requestLayout();
- });
-
- textArea.wrapTextProperty().addListener((observable, oldValue, newValue) -> {
- invalidateMetrics();
- scrollPane.setFitToWidth(newValue);
- });
-
- textArea.prefColumnCountProperty().addListener((observable, oldValue, newValue) -> {
- invalidateMetrics();
- updatePrefViewportWidth();
- });
-
- textArea.prefRowCountProperty().addListener((observable, oldValue, newValue) -> {
- invalidateMetrics();
- updatePrefViewportHeight();
- });
-
- updateFontMetrics();
- fontMetrics.addListener(valueModel -> {
- updateFontMetrics();
- });
+ updateFontMetrics();
+ fontMetrics.addListener(valueModel -> {
+ updateFontMetrics();
+ });
contentView.paddingProperty().addListener(valueModel -> {
updatePrefViewportWidth();
updatePrefViewportHeight();
});
@@ -539,24 +301,24 @@
contentView.requestLayout();
}
}
});
- textArea.scrollTopProperty().addListener((observable, oldValue, newValue) -> {
+ control.scrollTopProperty().addListener((observable, oldValue, newValue) -> {
double vValue = (newValue.doubleValue() < getScrollTopMax())
? (newValue.doubleValue() / getScrollTopMax()) : 1.0;
scrollPane.setVvalue(vValue);
});
- textArea.scrollLeftProperty().addListener((observable, oldValue, newValue) -> {
+ control.scrollLeftProperty().addListener((observable, oldValue, newValue) -> {
double hValue = (newValue.doubleValue() < getScrollLeftMax())
? (newValue.doubleValue() / getScrollLeftMax()) : 1.0;
scrollPane.setHvalue(hValue);
});
if (USE_MULTIPLE_NODES) {
- textArea.getParagraphs().addListener((ListChangeListener.Change<? extends CharSequence> change) -> {
+ control.getParagraphs().addListener((ListChangeListener.Change<? extends CharSequence> change) -> {
while (change.next()) {
int from = change.getFrom();
int to = change.getTo();
List<? extends CharSequence> removed = change.getRemoved();
if (from < to) {
@@ -579,22 +341,22 @@
paragraphNodes.getChildren().subList(from, from + removed.size()).clear();
}
}
});
} else {
- textArea.textProperty().addListener(observable -> {
+ control.textProperty().addListener(observable -> {
invalidateMetrics();
- ((Text)paragraphNodes.getChildren().get(0)).setText(textArea.textProperty().getValueSafe());
+ ((Text)paragraphNodes.getChildren().get(0)).setText(control.textProperty().getValueSafe());
contentView.requestLayout();
});
}
usePromptText = new BooleanBinding() {
- { bind(textArea.textProperty(), textArea.promptTextProperty()); }
+ { bind(control.textProperty(), control.promptTextProperty()); }
@Override protected boolean computeValue() {
- String txt = textArea.getText();
- String promptTxt = textArea.getPromptText();
+ String txt = control.getText();
+ String promptTxt = control.getPromptText();
return ((txt == null || txt.isEmpty()) &&
promptTxt != null && !promptTxt.isEmpty());
}
};
@@ -602,17 +364,17 @@
createPromptNode();
}
usePromptText.addListener(observable -> {
createPromptNode();
- textArea.requestLayout();
+ control.requestLayout();
});
updateHighlightFill();
updatePrefViewportWidth();
updatePrefViewportHeight();
- if (textArea.isFocused()) setCaretAnimating(true);
+ if (control.isFocused()) setCaretAnimating(true);
if (SHOW_HANDLES) {
selectionHandle1.setRotate(180);
EventHandler<MouseEvent> handlePressHandler = e -> {
@@ -648,585 +410,329 @@
if (element instanceof MoveTo && ((MoveTo)element).getY() > e.getY() - getTextTranslateY()) {
hit.setCharIndex(pos - 1);
}
textNode.setImpl_caretPosition(oldPos);
}
- positionCaret(hit, false, false);
+ positionCaret(hit, false);
e.consume();
});
- selectionHandle1.setOnMouseDragged(new EventHandler<MouseEvent>() {
- @Override public void handle(MouseEvent e) {
- TextArea textArea = getSkinnable();
+ selectionHandle1.setOnMouseDragged(e -> {
+ TextArea control1 = getSkinnable();
Text textNode = getTextNode();
Point2D tp = textNode.localToScene(0, 0);
Point2D p = new Point2D(e.getSceneX() - tp.getX() + 10/*??*/ - pressX + selectionHandle1.getWidth() / 2,
e.getSceneY() - tp.getY() - pressY + selectionHandle1.getHeight() + 5);
HitInfo hit = textNode.impl_hitTestChar(translateCaretPosition(p));
int pos = hit.getCharIndex();
- if (textArea.getAnchor() < textArea.getCaretPosition()) {
+ if (control1.getAnchor() < control1.getCaretPosition()) {
// Swap caret and anchor
- textArea.selectRange(textArea.getCaretPosition(), textArea.getAnchor());
+ control1.selectRange(control1.getCaretPosition(), control1.getAnchor());
}
if (pos > 0) {
- if (pos >= textArea.getAnchor()) {
- pos = textArea.getAnchor();
+ if (pos >= control1.getAnchor()) {
+ pos = control1.getAnchor();
}
int oldPos = textNode.getImpl_caretPosition();
textNode.setImpl_caretPosition(pos);
PathElement element = textNode.getImpl_caretShape()[0];
if (element instanceof MoveTo && ((MoveTo)element).getY() > e.getY() - getTextTranslateY()) {
hit.setCharIndex(pos - 1);
}
textNode.setImpl_caretPosition(oldPos);
}
- positionCaret(hit, true, false);
+ positionCaret(hit, true);
e.consume();
- }
});
- selectionHandle2.setOnMouseDragged(new EventHandler<MouseEvent>() {
- @Override public void handle(MouseEvent e) {
- TextArea textArea = getSkinnable();
+ selectionHandle2.setOnMouseDragged(e -> {
+ TextArea control1 = getSkinnable();
Text textNode = getTextNode();
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));
int pos = hit.getCharIndex();
- if (textArea.getAnchor() > textArea.getCaretPosition()) {
+ if (control1.getAnchor() > control1.getCaretPosition()) {
// Swap caret and anchor
- textArea.selectRange(textArea.getCaretPosition(), textArea.getAnchor());
+ control1.selectRange(control1.getCaretPosition(), control1.getAnchor());
}
if (pos > 0) {
- if (pos <= textArea.getAnchor() + 1) {
- pos = Math.min(textArea.getAnchor() + 2, textArea.getLength());
+ if (pos <= control1.getAnchor() + 1) {
+ pos = Math.min(control1.getAnchor() + 2, control1.getLength());
}
int oldPos = textNode.getImpl_caretPosition();
textNode.setImpl_caretPosition(pos);
PathElement element = textNode.getImpl_caretShape()[0];
if (element instanceof MoveTo && ((MoveTo)element).getY() > e.getY() - getTextTranslateY()) {
hit.setCharIndex(pos - 1);
}
textNode.setImpl_caretPosition(oldPos);
- positionCaret(hit, true, false);
+ positionCaret(hit, true);
}
e.consume();
- }
});
}
}
- @Override
- protected void layoutChildren(double contentX, double contentY, double contentWidth, double contentHeight) {
- scrollPane.resizeRelocate(contentX, contentY, contentWidth, contentHeight);
- }
- private void createPromptNode() {
- if (promptNode == null && usePromptText.get()) {
- promptNode = new Text();
- contentView.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(promptTextFill);
- }
- }
- private void addParagraphNode(int i, String string) {
- final TextArea textArea = getSkinnable();
- Text paragraphNode = new Text(string);
- paragraphNode.setTextOrigin(VPos.TOP);
- paragraphNode.setManaged(false);
- paragraphNode.getStyleClass().add("text");
- paragraphNode.boundsTypeProperty().addListener((observable, oldValue, newValue) -> {
- invalidateMetrics();
- updateFontMetrics();
- });
- paragraphNodes.getChildren().add(i, paragraphNode);
+ /***************************************************************************
+ * *
+ * Public API *
+ * *
+ **************************************************************************/
- paragraphNode.fontProperty().bind(textArea.fontProperty());
- paragraphNode.fillProperty().bind(textFill);
- paragraphNode.impl_selectionFillProperty().bind(highlightTextFill);
+ /** {@inheritDoc} */
+ @Override protected void invalidateMetrics() {
+ computedMinWidth = Double.NEGATIVE_INFINITY;
+ computedMinHeight = Double.NEGATIVE_INFINITY;
+ computedPrefWidth = Double.NEGATIVE_INFINITY;
+ computedPrefHeight = Double.NEGATIVE_INFINITY;
}
- @Override
- public void dispose() {
- // TODO Unregister listeners on text editor, paragraph list
- throw new UnsupportedOperationException();
+ /** {@inheritDoc} */
+ @Override protected void layoutChildren(double contentX, double contentY, double contentWidth, double contentHeight) {
+ scrollPane.resizeRelocate(contentX, contentY, contentWidth, contentHeight);
}
- @Override
- public double computeBaselineOffset(double topInset, double rightInset, double bottomInset, double leftInset) {
- Text firstParagraph = (Text) paragraphNodes.getChildren().get(0);
- return Utils.getAscent(getSkinnable().getFont(),firstParagraph.getBoundsType())
- + contentView.snappedTopInset() + textArea.snappedTopInset();
+ /** {@inheritDoc} */
+ @Override protected void updateHighlightFill() {
+ for (Node node : selectionHighlightGroup.getChildren()) {
+ Path selectionHighlightPath = (Path)node;
+ selectionHighlightPath.setFill(highlightFillProperty().get());
+ }
}
- @Override
- public char getCharacter(int index) {
- int n = paragraphNodes.getChildren().size();
-
- int paragraphIndex = 0;
- int offset = index;
-
- String paragraph = null;
- while (paragraphIndex < n) {
- Text paragraphNode = (Text)paragraphNodes.getChildren().get(paragraphIndex);
- paragraph = paragraphNode.getText();
- int count = paragraph.length() + 1;
+ // 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
+ Text textNode = getTextNode();
+ Point2D p = new Point2D(x - textNode.getLayoutX(), y - getTextTranslateY());
+ HitInfo hit = textNode.impl_hitTestChar(translateCaretPosition(p));
+ int pos = hit.getCharIndex();
+ if (pos > 0) {
+ int oldPos = textNode.getImpl_caretPosition();
+ textNode.setImpl_caretPosition(pos);
+ PathElement element = textNode.getImpl_caretShape()[0];
+ if (element instanceof MoveTo && ((MoveTo)element).getY() > y - getTextTranslateY()) {
+ hit.setCharIndex(pos - 1);
+ }
+ textNode.setImpl_caretPosition(oldPos);
+ }
+ return new TextPosInfo(hit);
+ };
- if (offset < count) {
+ /** {@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;
- offset -= count;
- paragraphIndex++;
+ case LINE:
+ switch (dir) {
+ case UP:
+ previousLine(select);
+ break;
+ case DOWN:
+ nextLine(select);
+ break;
+ case BEGINNING:
+ lineStart(select, select && isMac());
+ break;
+ case END:
+ lineEnd(select, select && isMac());
+ break;
+ default:
+ throw new IllegalArgumentException(""+dir);
}
+ break;
- return offset == paragraph.length() ? '\n' : paragraph.charAt(offset);
+ case PAGE:
+ switch (dir) {
+ case UP:
+ previousPage(select);
+ break;
+ case DOWN:
+ nextPage(select);
+ break;
+ default:
+ throw new IllegalArgumentException(""+dir);
}
+ break;
- @Override
- public int getInsertionPoint(double x, double y) {
- TextArea textArea = getSkinnable();
-
- int n = paragraphNodes.getChildren().size();
- int index = -1;
-
- if (n > 0) {
- if (y < contentView.snappedTopInset()) {
- // Select the character at x in the first row
- Text paragraphNode = (Text)paragraphNodes.getChildren().get(0);
- index = getNextInsertionPoint(paragraphNode, x, -1, VerticalDirection.DOWN);
- } else if (y > contentView.snappedTopInset() + contentView.getHeight()) {
- // Select the character at x in the last row
- int lastParagraphIndex = n - 1;
- Text lastParagraphView = (Text)paragraphNodes.getChildren().get(lastParagraphIndex);
-
- index = getNextInsertionPoint(lastParagraphView, x, -1, VerticalDirection.UP)
- + (textArea.getLength() - lastParagraphView.getText().length());
- } else {
- // Select the character at x in the row at y
- int paragraphOffset = 0;
- for (int i = 0; i < n; i++) {
- Text paragraphNode = (Text)paragraphNodes.getChildren().get(i);
-
- Bounds bounds = paragraphNode.getBoundsInLocal();
- double paragraphViewY = paragraphNode.getLayoutY() + bounds.getMinY();
- if (y >= paragraphViewY
- && y < paragraphViewY + paragraphNode.getBoundsInLocal().getHeight()) {
- index = getInsertionPoint(paragraphNode,
- x - paragraphNode.getLayoutX(),
- y - paragraphNode.getLayoutY()) + paragraphOffset;
+ case PARAGRAPH:
+ switch (dir) {
+ case UP:
+ paragraphStart(true, select);
+ break;
+ case DOWN:
+ paragraphEnd(true, select);
break;
+ case BEGINNING:
+ paragraphStart(false, select);
+ break;
+ case END:
+ paragraphEnd(false, select);
+ break;
+ default:
+ throw new IllegalArgumentException(""+dir);
}
+ break;
- paragraphOffset += paragraphNode.getText().length() + 1;
- }
+ default:
+ throw new IllegalArgumentException(""+unit);
}
}
- return index;
+ private void nextCharacterVisually(boolean moveRight) {
+ if (isRTL()) {
+ // Text node is mirrored.
+ moveRight = !moveRight;
}
- public void positionCaret(HitInfo hit, boolean select, boolean extendSelection) {
- int pos = Utils.getHitInsertionIndex(hit, getSkinnable().getText());
- boolean isNewLine =
- (pos > 0 &&
- pos <= getSkinnable().getLength() &&
- getSkinnable().getText().codePointAt(pos-1) == 0x0a);
-
- // special handling for a new line
- if (!hit.isLeading() && isNewLine) {
- hit.setLeading(true);
- pos -= 1;
+ Text textNode = getTextNode();
+ 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();
}
-
- if (select) {
- if (extendSelection) {
- getSkinnable().extendSelection(pos);
+ double hitX = moveRight ? caretBounds.getMaxX() : caretBounds.getMinX();
+ double hitY = (caretBounds.getMinY() + caretBounds.getMaxY()) / 2;
+ 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);
} else {
- getSkinnable().selectPositionCaret(pos);
- }
+ // We're at beginning or end of line. Try moving up / down.
+ int dot = textArea.getCaretPosition();
+ targetCaretX = moveRight ? 0 : Double.MAX_VALUE;
+ // TODO: Use Bidi sniffing instead of assuming right means forward here?
+ downLines(moveRight ? 1 : -1, false, false);
+ targetCaretX = -1;
+ if (dot == textArea.getCaretPosition()) {
+ if (moveRight) {
+ textArea.forward();
} else {
- getSkinnable().positionCaret(pos);
- }
-
- setForwardBias(hit.isLeading());
+ textArea.backward();
}
-
- private double getScrollTopMax() {
- return Math.max(0, contentView.getHeight() - scrollPane.getViewportBounds().getHeight());
}
-
- private double getScrollLeftMax() {
- return Math.max(0, contentView.getWidth() - scrollPane.getViewportBounds().getWidth());
}
-
- private int getInsertionPoint(Text paragraphNode, double x, double y) {
- HitInfo hitInfo = paragraphNode.impl_hitTestChar(new Point2D(x, y));
- return Utils.getHitInsertionIndex(hitInfo, paragraphNode.getText());
}
- public int getNextInsertionPoint(double x, int from, VerticalDirection scrollDirection) {
- // TODO
- return 0;
- }
+ private void downLines(int nLines, boolean select, boolean extendSelection) {
+ Text textNode = getTextNode();
+ Bounds caretBounds = caretPath.getLayoutBounds();
- private int getNextInsertionPoint(Text paragraphNode, double x, int from,
- VerticalDirection scrollDirection) {
- // TODO
- return 0;
+ // The middle y coordinate of the the line we want to go to.
+ double targetLineMidY = (caretBounds.getMinY() + caretBounds.getMaxY()) / 2 + nLines * lineHeight;
+ if (targetLineMidY < 0) {
+ targetLineMidY = 0;
}
- @Override
- public Rectangle2D getCharacterBounds(int index) {
- TextArea textArea = getSkinnable();
-
- int paragraphIndex = paragraphNodes.getChildren().size();
- int paragraphOffset = textArea.getLength() + 1;
+ // The target x for the caret. This may have been set during a
+ // previous call.
+ double x = (targetCaretX >= 0) ? targetCaretX : (caretBounds.getMaxX());
- Text paragraphNode = null;
- do {
- paragraphNode = (Text)paragraphNodes.getChildren().get(--paragraphIndex);
- paragraphOffset -= paragraphNode.getText().length() + 1;
- } while (index < paragraphOffset);
+ // Find a text position for the target x,y.
+ HitInfo hit = textNode.impl_hitTestChar(translateCaretPosition(new Point2D(x, targetLineMidY)));
+ int pos = hit.getCharIndex();
- int characterIndex = index - paragraphOffset;
- boolean terminator = false;
+ // Save the old pos temporarily while testing the new one.
+ int oldPos = textNode.getImpl_caretPosition();
+ boolean oldBias = textNode.isImpl_caretBias();
+ textNode.setImpl_caretBias(hit.isLeading());
+ textNode.setImpl_caretPosition(pos);
+ tmpCaretPath.getElements().clear();
+ tmpCaretPath.getElements().addAll(textNode.getImpl_caretShape());
+ tmpCaretPath.setLayoutX(textNode.getLayoutX());
+ tmpCaretPath.setLayoutY(textNode.getLayoutY());
+ Bounds tmpCaretBounds = tmpCaretPath.getLayoutBounds();
+ // The y for the middle of the row we found.
+ double foundLineMidY = (tmpCaretBounds.getMinY() + tmpCaretBounds.getMaxY()) / 2;
+ textNode.setImpl_caretBias(oldBias);
+ textNode.setImpl_caretPosition(oldPos);
- if (characterIndex == paragraphNode.getText().length()) {
- characterIndex--;
- terminator = true;
+ if (pos > 0) {
+ if (nLines > 0 && foundLineMidY > targetLineMidY) {
+ // We went too far and ended up after a newline.
+ hit.setCharIndex(pos - 1);
}
- characterBoundingPath.getElements().clear();
- characterBoundingPath.getElements().addAll(paragraphNode.impl_getRangeShape(characterIndex, characterIndex + 1));
- characterBoundingPath.setLayoutX(paragraphNode.getLayoutX());
- characterBoundingPath.setLayoutY(paragraphNode.getLayoutY());
+ if (pos >= textArea.getLength() && getCharacter(pos - 1) == '\n') {
+ // Special case for newline at end of text.
+ hit.setLeading(true);
+ }
+ }
- Bounds bounds = characterBoundingPath.getBoundsInLocal();
+ // Test if the found line is in the correct direction and move
+ // the caret.
+ if (nLines == 0 ||
+ (nLines > 0 && foundLineMidY > caretBounds.getMaxY()) ||
+ (nLines < 0 && foundLineMidY < caretBounds.getMinY())) {
- double x = bounds.getMinX() + paragraphNode.getLayoutX() - textArea.getScrollLeft();
- double y = bounds.getMinY() + paragraphNode.getLayoutY() - textArea.getScrollTop();
+ positionCaret(hit, select, extendSelection);
+ targetCaretX = x;
+ }
+ }
- // Sometimes the bounds is empty, in which case we must ignore the width/height
- double width = bounds.isEmpty() ? 0 : bounds.getWidth();
- double height = bounds.isEmpty() ? 0 : bounds.getHeight();
+ private void previousLine(boolean select) {
+ downLines(-1, select, false);
+ }
- if (terminator) {
- x += width;
- width = 0;
+ private void nextLine(boolean select) {
+ downLines(1, select, false);
}
- return new Rectangle2D(x, y, width, height);
+ private void previousPage(boolean select) {
+ downLines(-(int)(scrollPane.getViewportBounds().getHeight() / lineHeight),
+ select, false);
}
- @Override public void scrollCharacterToVisible(final int index) {
- // TODO We queue a callback because when characters are added or
- // removed the bounds are not immediately updated; is this really
- // necessary?
+ private void nextPage(boolean select) {
+ downLines((int)(scrollPane.getViewportBounds().getHeight() / lineHeight),
+ select, false);
+ }
- Platform.runLater(() -> {
- if (getSkinnable().getLength() == 0) {
- return;
+ private void lineStart(boolean select, boolean extendSelection) {
+ targetCaretX = 0;
+ downLines(0, select, extendSelection);
+ targetCaretX = -1;
}
- Rectangle2D characterBounds = getCharacterBounds(index);
- scrollBoundsToVisible(characterBounds);
- });
+
+ private void lineEnd(boolean select, boolean extendSelection) {
+ targetCaretX = Double.MAX_VALUE;
+ downLines(0, select, extendSelection);
+ targetCaretX = -1;
}
- private void scrollCaretToVisible() {
+
+ private void paragraphStart(boolean previousIfAtStart, boolean select) {
TextArea textArea = getSkinnable();
- Bounds bounds = caretPath.getLayoutBounds();
- double x = bounds.getMinX() - textArea.getScrollLeft();
- double y = bounds.getMinY() - textArea.getScrollTop();
- double w = bounds.getWidth();
- double h = bounds.getHeight();
-
- if (SHOW_HANDLES) {
- if (caretHandle.isVisible()) {
- h += caretHandle.getHeight();
- } else if (selectionHandle1.isVisible() && selectionHandle2.isVisible()) {
- x -= selectionHandle1.getWidth() / 2;
- y -= selectionHandle1.getHeight();
- w += selectionHandle1.getWidth() / 2 + selectionHandle2.getWidth() / 2;
- h += selectionHandle1.getHeight() + selectionHandle2.getHeight();
- }
- }
-
- if (w > 0 && h > 0) {
- scrollBoundsToVisible(new Rectangle2D(x, y, w, h));
- }
- }
-
- private void scrollBoundsToVisible(Rectangle2D bounds) {
- TextArea textArea = getSkinnable();
- Bounds viewportBounds = scrollPane.getViewportBounds();
-
- double viewportWidth = viewportBounds.getWidth();
- double viewportHeight = viewportBounds.getHeight();
- double scrollTop = textArea.getScrollTop();
- double scrollLeft = textArea.getScrollLeft();
- double slop = 6.0;
-
- if (bounds.getMinY() < 0) {
- double y = scrollTop + bounds.getMinY();
- if (y <= contentView.snappedTopInset()) {
- y = 0;
- }
- textArea.setScrollTop(y);
- } else if (contentView.snappedTopInset() + bounds.getMaxY() > viewportHeight) {
- double y = scrollTop + contentView.snappedTopInset() + bounds.getMaxY() - viewportHeight;
- if (y >= getScrollTopMax() - contentView.snappedBottomInset()) {
- y = getScrollTopMax();
- }
- textArea.setScrollTop(y);
- }
-
-
- if (bounds.getMinX() < 0) {
- double x = scrollLeft + bounds.getMinX() - slop;
- if (x <= contentView.snappedLeftInset() + slop) {
- x = 0;
- }
- textArea.setScrollLeft(x);
- } else if (contentView.snappedLeftInset() + bounds.getMaxX() > viewportWidth) {
- double x = scrollLeft + contentView.snappedLeftInset() + bounds.getMaxX() - viewportWidth + slop;
- if (x >= getScrollLeftMax() - contentView.snappedRightInset() - slop) {
- x = getScrollLeftMax();
- }
- textArea.setScrollLeft(x);
- }
- }
-
- private void updatePrefViewportWidth() {
- int columnCount = getSkinnable().getPrefColumnCount();
- scrollPane.setPrefViewportWidth(columnCount * characterWidth + contentView.snappedLeftInset() + contentView.snappedRightInset());
- scrollPane.setMinViewportWidth(characterWidth + contentView.snappedLeftInset() + contentView.snappedRightInset());
- }
-
- private void updatePrefViewportHeight() {
- int rowCount = getSkinnable().getPrefRowCount();
- scrollPane.setPrefViewportHeight(rowCount * lineHeight + contentView.snappedTopInset() + contentView.snappedBottomInset());
- scrollPane.setMinViewportHeight(lineHeight + contentView.snappedTopInset() + contentView.snappedBottomInset());
- }
-
- private void updateFontMetrics() {
- Text firstParagraph = (Text)paragraphNodes.getChildren().get(0);
- lineHeight = Utils.getLineHeight(getSkinnable().getFont(),firstParagraph.getBoundsType());
- characterWidth = fontMetrics.get().computeStringWidth("W");
- }
-
- @Override
- protected void updateHighlightFill() {
- for (Node node : selectionHighlightGroup.getChildren()) {
- Path selectionHighlightPath = (Path)node;
- selectionHighlightPath.setFill(highlightFill.get());
- }
- }
-
-// protected void handleMouseReleasedEvent(MouseEvent event) {
-// // super.handleMouseReleasedEvent(event);
-
-// // Stop the scroll selection timer
-// scrollSelectionTimeline.stop();
-// scrollDirection = null;
-
-// // Select all if the user double-clicked
-// if (event.getButton() == MouseButton.PRIMARY
-// && event.getClickCount() == 3) {
-// // TODO Select the current row
-// }
-// }
-
- // Callbacks from Behavior class
-
- private double getTextTranslateX() {
- return contentView.snappedLeftInset();
- }
-
- private double getTextTranslateY() {
- return contentView.snappedTopInset();
- }
-
- private double getTextLeft() {
- return 0;
- }
-
- private Point2D translateCaretPosition(Point2D p) {
- return p;
- }
-
- private Text getTextNode() {
- if (USE_MULTIPLE_NODES) {
- throw new IllegalArgumentException("Multiple node traversal is not yet implemented.");
- }
- return (Text)paragraphNodes.getChildren().get(0);
- }
-
- public HitInfo getIndex(double x, double y) {
- // adjust the event to be in the same coordinate space as the
- // text content of the textInputControl
- Text textNode = getTextNode();
- Point2D p = new Point2D(x - textNode.getLayoutX(), y - getTextTranslateY());
- HitInfo hit = textNode.impl_hitTestChar(translateCaretPosition(p));
- int pos = hit.getCharIndex();
- if (pos > 0) {
- int oldPos = textNode.getImpl_caretPosition();
- textNode.setImpl_caretPosition(pos);
- PathElement element = textNode.getImpl_caretShape()[0];
- if (element instanceof MoveTo && ((MoveTo)element).getY() > y - getTextTranslateY()) {
- hit.setCharIndex(pos - 1);
- }
- textNode.setImpl_caretPosition(oldPos);
- }
- return hit;
- };
-
- /**
- * Remembers horizontal position when traversing up / down.
- */
- double targetCaretX = -1;
-
- @Override public void nextCharacterVisually(boolean moveRight) {
- if (isRTL()) {
- // Text node is mirrored.
- moveRight = !moveRight;
- }
-
- Text textNode = getTextNode();
- 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.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, false);
- } else {
- // We're at beginning or end of line. Try moving up / down.
- int dot = textArea.getCaretPosition();
- targetCaretX = moveRight ? 0 : Double.MAX_VALUE;
- // TODO: Use Bidi sniffing instead of assuming right means forward here?
- downLines(moveRight ? 1 : -1, false, false);
- targetCaretX = -1;
- if (dot == textArea.getCaretPosition()) {
- if (moveRight) {
- textArea.forward();
- } else {
- textArea.backward();
- }
- }
- }
- }
-
- /** A shared helper object, used only by downLines(). */
- private static final Path tmpCaretPath = new Path();
-
- protected void downLines(int nLines, boolean select, boolean extendSelection) {
- Text textNode = getTextNode();
- Bounds caretBounds = caretPath.getLayoutBounds();
-
- // The middle y coordinate of the the line we want to go to.
- double targetLineMidY = (caretBounds.getMinY() + caretBounds.getMaxY()) / 2 + nLines * lineHeight;
- if (targetLineMidY < 0) {
- targetLineMidY = 0;
- }
-
- // The target x for the caret. This may have been set during a
- // previous call.
- double x = (targetCaretX >= 0) ? targetCaretX : (caretBounds.getMaxX());
-
- // Find a text position for the target x,y.
- HitInfo hit = textNode.impl_hitTestChar(translateCaretPosition(new Point2D(x, targetLineMidY)));
- int pos = hit.getCharIndex();
-
- // Save the old pos temporarily while testing the new one.
- int oldPos = textNode.getImpl_caretPosition();
- boolean oldBias = textNode.isImpl_caretBias();
- textNode.setImpl_caretBias(hit.isLeading());
- textNode.setImpl_caretPosition(pos);
- tmpCaretPath.getElements().clear();
- tmpCaretPath.getElements().addAll(textNode.getImpl_caretShape());
- tmpCaretPath.setLayoutX(textNode.getLayoutX());
- tmpCaretPath.setLayoutY(textNode.getLayoutY());
- Bounds tmpCaretBounds = tmpCaretPath.getLayoutBounds();
- // The y for the middle of the row we found.
- double foundLineMidY = (tmpCaretBounds.getMinY() + tmpCaretBounds.getMaxY()) / 2;
- textNode.setImpl_caretBias(oldBias);
- textNode.setImpl_caretPosition(oldPos);
-
- if (pos > 0) {
- if (nLines > 0 && foundLineMidY > targetLineMidY) {
- // We went too far and ended up after a newline.
- hit.setCharIndex(pos - 1);
- }
-
- if (pos >= textArea.getLength() && getCharacter(pos - 1) == '\n') {
- // Special case for newline at end of text.
- hit.setLeading(true);
- }
- }
-
- // Test if the found line is in the correct direction and move
- // the caret.
- if (nLines == 0 ||
- (nLines > 0 && foundLineMidY > caretBounds.getMaxY()) ||
- (nLines < 0 && foundLineMidY < caretBounds.getMinY())) {
-
- positionCaret(hit, select, extendSelection);
- targetCaretX = x;
- }
- }
-
- public void previousLine(boolean select) {
- downLines(-1, select, false);
- }
-
- public void nextLine(boolean select) {
- downLines(1, select, false);
- }
-
- public void previousPage(boolean select) {
- downLines(-(int)(scrollPane.getViewportBounds().getHeight() / lineHeight),
- select, false);
- }
-
- public void nextPage(boolean select) {
- downLines((int)(scrollPane.getViewportBounds().getHeight() / lineHeight),
- select, false);
- }
-
- public void lineStart(boolean select, boolean extendSelection) {
- targetCaretX = 0;
- downLines(0, select, extendSelection);
- targetCaretX = -1;
- }
-
- public void lineEnd(boolean select, boolean extendSelection) {
- targetCaretX = Double.MAX_VALUE;
- downLines(0, select, extendSelection);
- targetCaretX = -1;
- }
-
-
- public void paragraphStart(boolean previousIfAtStart, boolean select) {
- TextArea textArea = getSkinnable();
- String text = textArea.textProperty().getValueSafe();
- int pos = textArea.getCaretPosition();
+ String text = textArea.textProperty().getValueSafe();
+ int pos = textArea.getCaretPosition();
if (pos > 0) {
if (previousIfAtStart && text.codePointAt(pos-1) == 0x0a) {
// We are at the beginning of a paragraph.
// Back up to the previous paragraph.
@@ -1242,16 +748,17 @@
textArea.positionCaret(pos);
}
}
}
- public void paragraphEnd(boolean goPastInitialNewline, boolean goPastTrailingNewline, boolean select) {
+ private void paragraphEnd(boolean goPastInitialNewline, boolean select) {
TextArea textArea = getSkinnable();
String text = textArea.textProperty().getValueSafe();
int pos = textArea.getCaretPosition();
int len = text.length();
boolean wentPastInitialNewline = false;
+ boolean goPastTrailingNewline = isWindows();
if (pos < len) {
if (goPastInitialNewline && text.codePointAt(pos) == 0x0a) {
// We are at the end of a paragraph, start by moving to the
// next paragraph.
@@ -1275,20 +782,11 @@
textArea.positionCaret(pos);
}
}
}
- private void updateTextNodeCaretPos(int pos) {
- Text textNode = getTextNode();
- if (isForwardBias()) {
- textNode.setImpl_caretPosition(pos);
- } else {
- textNode.setImpl_caretPosition(pos - 1);
- }
- textNode.impl_caretBiasProperty().set(isForwardBias());
- }
-
+ /** {@inheritDoc} */
@Override protected PathElement[] getUnderlineShape(int start, int end) {
int pStart = 0;
for (Node node : paragraphNodes.getChildren()) {
Text p = (Text)node;
int pEnd = pStart + p.textProperty().getValueSafe().length();
@@ -1298,10 +796,11 @@
pStart = pEnd + 1;
}
return null;
}
+ /** {@inheritDoc} */
@Override protected PathElement[] getRangeShape(int start, int end) {
int pStart = 0;
for (Node node : paragraphNodes.getChildren()) {
Text p = (Text)node;
int pEnd = pStart + p.textProperty().getValueSafe().length();
@@ -1311,10 +810,11 @@
pStart = pEnd + 1;
}
return null;
}
+ /** {@inheritDoc} */
@Override protected void addHighlight(List<? extends Node> nodes, int start) {
int pStart = 0;
Text paragraphNode = null;
for (Node node : paragraphNodes.getChildren()) {
Text p = (Text)node;
@@ -1333,49 +833,36 @@
}
}
contentView.getChildren().addAll(nodes);
}
+ /** {@inheritDoc} */
@Override protected void removeHighlight(List<? extends Node> nodes) {
contentView.getChildren().removeAll(nodes);
}
- /**
- * Use this implementation instead of the one provided on TextInputControl
- * Simply calls into TextInputControl.deletePrevious/NextChar and responds appropriately
- * based on the return value.
- */
- 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 {
-// scrollAfterDelete(textMaxXOld, caretMaxXOld);
- }
- }
-
+ /** {@inheritDoc} */
@Override public Point2D getMenuPosition() {
contentView.layoutChildren();
Point2D p = super.getMenuPosition();
if (p != null) {
p = new Point2D(Math.max(0, p.getX() - contentView.snappedLeftInset() - getSkinnable().getScrollLeft()),
Math.max(0, p.getY() - contentView.snappedTopInset() - getSkinnable().getScrollTop()));
}
return p;
}
+ // Public for FXVKSkin
+ /**
+ * @return the {@code Bounds} of the caret shape, relative to the {@code TextArea}.
+ */
public Bounds getCaretBounds() {
return getSkinnable().sceneToLocal(caretPath.localToScene(caretPath.getBoundsInLocal()));
}
- @Override
- protected Object queryAccessibleAttribute(AccessibleAttribute attribute, Object... parameters) {
+ /** {@inheritDoc} */
+ @Override protected Object queryAccessibleAttribute(AccessibleAttribute attribute, Object... parameters) {
switch (attribute) {
case LINE_FOR_OFFSET:
case LINE_START:
case LINE_END:
case BOUNDS_FOR_RANGE:
@@ -1383,6 +870,660 @@
Text text = getTextNode();
return text.queryAccessibleAttribute(attribute, parameters);
default: return super.queryAccessibleAttribute(attribute, parameters);
}
}
+
+ /** {@inheritDoc} */
+ @Override public void dispose() {
+ super.dispose();
+
+ if (behavior != null) {
+ behavior.dispose();
+ }
+
+ // TODO Unregister listeners on text editor, paragraph list
+ throw new UnsupportedOperationException();
+ }
+
+ /** {@inheritDoc} */
+ @Override public double computeBaselineOffset(double topInset, double rightInset, double bottomInset, double leftInset) {
+ Text firstParagraph = (Text) paragraphNodes.getChildren().get(0);
+ return Utils.getAscent(getSkinnable().getFont(), firstParagraph.getBoundsType())
+ + contentView.snappedTopInset() + textArea.snappedTopInset();
+ }
+
+ private char getCharacter(int index) {
+ int n = paragraphNodes.getChildren().size();
+
+ int paragraphIndex = 0;
+ int offset = index;
+
+ String paragraph = null;
+ while (paragraphIndex < n) {
+ Text paragraphNode = (Text)paragraphNodes.getChildren().get(paragraphIndex);
+ paragraph = paragraphNode.getText();
+ int count = paragraph.length() + 1;
+
+ if (offset < count) {
+ break;
+ }
+
+ offset -= count;
+ paragraphIndex++;
+ }
+
+ return offset == paragraph.length() ? '\n' : paragraph.charAt(offset);
+ }
+
+ /** {@inheritDoc} */
+ @Override protected int getInsertionPoint(double x, double y) {
+ TextArea textArea = getSkinnable();
+
+ int n = paragraphNodes.getChildren().size();
+ int index = -1;
+
+ if (n > 0) {
+ if (y < contentView.snappedTopInset()) {
+ // Select the character at x in the first row
+ Text paragraphNode = (Text)paragraphNodes.getChildren().get(0);
+ index = getNextInsertionPoint(paragraphNode, x, -1, VerticalDirection.DOWN);
+ } else if (y > contentView.snappedTopInset() + contentView.getHeight()) {
+ // Select the character at x in the last row
+ int lastParagraphIndex = n - 1;
+ Text lastParagraphView = (Text)paragraphNodes.getChildren().get(lastParagraphIndex);
+
+ index = getNextInsertionPoint(lastParagraphView, x, -1, VerticalDirection.UP)
+ + (textArea.getLength() - lastParagraphView.getText().length());
+ } else {
+ // Select the character at x in the row at y
+ int paragraphOffset = 0;
+ for (int i = 0; i < n; i++) {
+ Text paragraphNode = (Text)paragraphNodes.getChildren().get(i);
+
+ Bounds bounds = paragraphNode.getBoundsInLocal();
+ double paragraphViewY = paragraphNode.getLayoutY() + bounds.getMinY();
+ if (y >= paragraphViewY
+ && y < paragraphViewY + paragraphNode.getBoundsInLocal().getHeight()) {
+ index = getInsertionPoint(paragraphNode,
+ x - paragraphNode.getLayoutX(),
+ y - paragraphNode.getLayoutY()) + paragraphOffset;
+ break;
+ }
+
+ paragraphOffset += paragraphNode.getText().length() + 1;
+ }
+ }
+ }
+
+ return index;
+ }
+
+ // 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) {
+ positionCaret(hit, select, false);
+ }
+
+ private void positionCaret(TextPosInfo hit, boolean select, boolean extendSelection) {
+ int pos = Utils.getHitInsertionIndex(hit, getSkinnable().getText());
+ boolean isNewLine =
+ (pos > 0 &&
+ pos <= getSkinnable().getLength() &&
+ getSkinnable().getText().codePointAt(pos-1) == 0x0a);
+
+ // special handling for a new line
+ if (!hit.isLeading() && isNewLine) {
+ hit.setLeading(true);
+ pos -= 1;
+ }
+
+ if (select) {
+ if (extendSelection) {
+ getSkinnable().extendSelection(pos);
+ } else {
+ getSkinnable().selectPositionCaret(pos);
+ }
+ } else {
+ getSkinnable().positionCaret(pos);
+ }
+
+ setForwardBias(hit.isLeading());
+ }
+
+ private void positionCaret(HitInfo hit, boolean select) {
+ positionCaret(new TextPosInfo(hit), select);
+ }
+
+ private void positionCaret(HitInfo hit, boolean select, boolean extendSelection) {
+ positionCaret(new TextPosInfo(hit), select, extendSelection);
+ }
+
+ /** {@inheritDoc} */
+ @Override public Rectangle2D getCharacterBounds(int index) {
+ TextArea textArea = getSkinnable();
+
+ int paragraphIndex = paragraphNodes.getChildren().size();
+ int paragraphOffset = textArea.getLength() + 1;
+
+ Text paragraphNode = null;
+ do {
+ paragraphNode = (Text)paragraphNodes.getChildren().get(--paragraphIndex);
+ paragraphOffset -= paragraphNode.getText().length() + 1;
+ } while (index < paragraphOffset);
+
+ int characterIndex = index - paragraphOffset;
+ boolean terminator = false;
+
+ if (characterIndex == paragraphNode.getText().length()) {
+ characterIndex--;
+ terminator = true;
+ }
+
+ characterBoundingPath.getElements().clear();
+ characterBoundingPath.getElements().addAll(paragraphNode.impl_getRangeShape(characterIndex, characterIndex + 1));
+ characterBoundingPath.setLayoutX(paragraphNode.getLayoutX());
+ characterBoundingPath.setLayoutY(paragraphNode.getLayoutY());
+
+ Bounds bounds = characterBoundingPath.getBoundsInLocal();
+
+ double x = bounds.getMinX() + paragraphNode.getLayoutX() - textArea.getScrollLeft();
+ double y = bounds.getMinY() + paragraphNode.getLayoutY() - textArea.getScrollTop();
+
+ // Sometimes the bounds is empty, in which case we must ignore the width/height
+ double width = bounds.isEmpty() ? 0 : bounds.getWidth();
+ double height = bounds.isEmpty() ? 0 : bounds.getHeight();
+
+ if (terminator) {
+ x += width;
+ width = 0;
+ }
+
+ return new Rectangle2D(x, y, width, height);
+ }
+
+ /** {@inheritDoc} */
+ @Override protected void scrollCharacterToVisible(final int index) {
+ // TODO We queue a callback because when characters are added or
+ // removed the bounds are not immediately updated; is this really
+ // necessary?
+
+ Platform.runLater(() -> {
+ if (getSkinnable().getLength() == 0) {
+ return;
+ }
+ Rectangle2D characterBounds = getCharacterBounds(index);
+ scrollBoundsToVisible(characterBounds);
+ });
+ }
+
+
+
+ /**************************************************************************
+ *
+ * Private implementation
+ *
+ **************************************************************************/
+
+ TextAreaBehavior getBehavior() {
+ return behavior;
+ }
+
+ private void createPromptNode() {
+ if (promptNode == null && usePromptText.get()) {
+ promptNode = new Text();
+ contentView.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());
+ }
+ }
+
+ private void addParagraphNode(int i, String string) {
+ final TextArea textArea = getSkinnable();
+ Text paragraphNode = new Text(string);
+ paragraphNode.setTextOrigin(VPos.TOP);
+ paragraphNode.setManaged(false);
+ paragraphNode.getStyleClass().add("text");
+ paragraphNode.boundsTypeProperty().addListener((observable, oldValue, newValue) -> {
+ invalidateMetrics();
+ updateFontMetrics();
+ });
+ paragraphNodes.getChildren().add(i, paragraphNode);
+
+ paragraphNode.fontProperty().bind(textArea.fontProperty());
+ paragraphNode.fillProperty().bind(textFillProperty());
+ paragraphNode.impl_selectionFillProperty().bind(highlightTextFillProperty());
+ }
+
+ private double getScrollTopMax() {
+ return Math.max(0, contentView.getHeight() - scrollPane.getViewportBounds().getHeight());
+ }
+
+ private double getScrollLeftMax() {
+ return Math.max(0, contentView.getWidth() - scrollPane.getViewportBounds().getWidth());
+ }
+
+ private int getInsertionPoint(Text paragraphNode, double x, double y) {
+ TextPosInfo hitInfo = new TextPosInfo(paragraphNode.impl_hitTestChar(new Point2D(x, y)));
+ return Utils.getHitInsertionIndex(hitInfo, paragraphNode.getText());
+ }
+
+ private int getNextInsertionPoint(Text paragraphNode, double x, int from,
+ VerticalDirection scrollDirection) {
+ // TODO
+ return 0;
+ }
+
+ private void scrollCaretToVisible() {
+ TextArea textArea = getSkinnable();
+ Bounds bounds = caretPath.getLayoutBounds();
+ double x = bounds.getMinX() - textArea.getScrollLeft();
+ double y = bounds.getMinY() - textArea.getScrollTop();
+ double w = bounds.getWidth();
+ double h = bounds.getHeight();
+
+ if (SHOW_HANDLES) {
+ if (caretHandle.isVisible()) {
+ h += caretHandle.getHeight();
+ } else if (selectionHandle1.isVisible() && selectionHandle2.isVisible()) {
+ x -= selectionHandle1.getWidth() / 2;
+ y -= selectionHandle1.getHeight();
+ w += selectionHandle1.getWidth() / 2 + selectionHandle2.getWidth() / 2;
+ h += selectionHandle1.getHeight() + selectionHandle2.getHeight();
+ }
+ }
+
+ if (w > 0 && h > 0) {
+ scrollBoundsToVisible(new Rectangle2D(x, y, w, h));
+ }
+ }
+
+ private void scrollBoundsToVisible(Rectangle2D bounds) {
+ TextArea textArea = getSkinnable();
+ Bounds viewportBounds = scrollPane.getViewportBounds();
+
+ double viewportWidth = viewportBounds.getWidth();
+ double viewportHeight = viewportBounds.getHeight();
+ double scrollTop = textArea.getScrollTop();
+ double scrollLeft = textArea.getScrollLeft();
+ double slop = 6.0;
+
+ if (bounds.getMinY() < 0) {
+ double y = scrollTop + bounds.getMinY();
+ if (y <= contentView.snappedTopInset()) {
+ y = 0;
+ }
+ textArea.setScrollTop(y);
+ } else if (contentView.snappedTopInset() + bounds.getMaxY() > viewportHeight) {
+ double y = scrollTop + contentView.snappedTopInset() + bounds.getMaxY() - viewportHeight;
+ if (y >= getScrollTopMax() - contentView.snappedBottomInset()) {
+ y = getScrollTopMax();
+ }
+ textArea.setScrollTop(y);
+ }
+
+
+ if (bounds.getMinX() < 0) {
+ double x = scrollLeft + bounds.getMinX() - slop;
+ if (x <= contentView.snappedLeftInset() + slop) {
+ x = 0;
+ }
+ textArea.setScrollLeft(x);
+ } else if (contentView.snappedLeftInset() + bounds.getMaxX() > viewportWidth) {
+ double x = scrollLeft + contentView.snappedLeftInset() + bounds.getMaxX() - viewportWidth + slop;
+ if (x >= getScrollLeftMax() - contentView.snappedRightInset() - slop) {
+ x = getScrollLeftMax();
+ }
+ textArea.setScrollLeft(x);
+ }
+ }
+
+ private void updatePrefViewportWidth() {
+ int columnCount = getSkinnable().getPrefColumnCount();
+ scrollPane.setPrefViewportWidth(columnCount * characterWidth + contentView.snappedLeftInset() + contentView.snappedRightInset());
+ scrollPane.setMinViewportWidth(characterWidth + contentView.snappedLeftInset() + contentView.snappedRightInset());
+ }
+
+ private void updatePrefViewportHeight() {
+ int rowCount = getSkinnable().getPrefRowCount();
+ scrollPane.setPrefViewportHeight(rowCount * lineHeight + contentView.snappedTopInset() + contentView.snappedBottomInset());
+ scrollPane.setMinViewportHeight(lineHeight + contentView.snappedTopInset() + contentView.snappedBottomInset());
+ }
+
+ private void updateFontMetrics() {
+ Text firstParagraph = (Text)paragraphNodes.getChildren().get(0);
+ lineHeight = Utils.getLineHeight(getSkinnable().getFont(), firstParagraph.getBoundsType());
+ characterWidth = fontMetrics.get().computeStringWidth("W");
+ }
+
+ private double getTextTranslateX() {
+ return contentView.snappedLeftInset();
+ }
+
+ private double getTextTranslateY() {
+ return contentView.snappedTopInset();
+ }
+
+ private double getTextLeft() {
+ return 0;
+ }
+
+ private Point2D translateCaretPosition(Point2D p) {
+ return p;
+ }
+
+ private Text getTextNode() {
+ if (USE_MULTIPLE_NODES) {
+ throw new IllegalArgumentException("Multiple node traversal is not yet implemented.");
+ }
+ return (Text)paragraphNodes.getChildren().get(0);
+ }
+
+ private void updateTextNodeCaretPos(int pos) {
+ Text textNode = getTextNode();
+ if (isForwardBias()) {
+ textNode.setImpl_caretPosition(pos);
+ } else {
+ textNode.setImpl_caretPosition(pos - 1);
+ }
+ textNode.impl_caretBiasProperty().set(isForwardBias());
+ }
+
+
+
+ /**************************************************************************
+ *
+ * Support classes
+ *
+ **************************************************************************/
+
+ private class ContentView extends Region {
+ {
+ getStyleClass().add("content");
+
+ addEventHandler(MouseEvent.MOUSE_PRESSED, event -> {
+ behavior.mousePressed(event);
+ event.consume();
+ });
+
+ addEventHandler(MouseEvent.MOUSE_RELEASED, event -> {
+ behavior.mouseReleased(event);
+ event.consume();
+ });
+
+ addEventHandler(MouseEvent.MOUSE_DRAGGED, event -> {
+ behavior.mouseDragged(event);
+ event.consume();
+ });
+ }
+
+ @Override protected ObservableList<Node> getChildren() {
+ return super.getChildren();
+ }
+
+ @Override public Orientation getContentBias() {
+ return Orientation.HORIZONTAL;
+ }
+
+ @Override protected double computePrefWidth(double height) {
+ if (computedPrefWidth < 0) {
+ double prefWidth = 0;
+
+ for (Node node : paragraphNodes.getChildren()) {
+ Text paragraphNode = (Text)node;
+ prefWidth = Math.max(prefWidth,
+ Utils.computeTextWidth(paragraphNode.getFont(),
+ paragraphNode.getText(), 0));
+ }
+
+ prefWidth += snappedLeftInset() + snappedRightInset();
+
+ Bounds viewPortBounds = scrollPane.getViewportBounds();
+ computedPrefWidth = Math.max(prefWidth, (viewPortBounds != null) ? viewPortBounds.getWidth() : 0);
+ }
+ return computedPrefWidth;
+ }
+
+ @Override protected double computePrefHeight(double width) {
+ if (width != widthForComputedPrefHeight) {
+ invalidateMetrics();
+ widthForComputedPrefHeight = width;
+ }
+
+ if (computedPrefHeight < 0) {
+ double wrappingWidth;
+ if (width == -1) {
+ wrappingWidth = 0;
+ } else {
+ wrappingWidth = Math.max(width - (snappedLeftInset() + snappedRightInset()), 0);
+ }
+
+ double prefHeight = 0;
+
+ for (Node node : paragraphNodes.getChildren()) {
+ Text paragraphNode = (Text)node;
+ prefHeight += Utils.computeTextHeight(
+ paragraphNode.getFont(),
+ paragraphNode.getText(),
+ wrappingWidth,
+ paragraphNode.getBoundsType());
+ }
+
+ prefHeight += snappedTopInset() + snappedBottomInset();
+
+ Bounds viewPortBounds = scrollPane.getViewportBounds();
+ computedPrefHeight = Math.max(prefHeight, (viewPortBounds != null) ? viewPortBounds.getHeight() : 0);
+ }
+ return computedPrefHeight;
+ }
+
+ @Override protected double computeMinWidth(double height) {
+ if (computedMinWidth < 0) {
+ double hInsets = snappedLeftInset() + snappedRightInset();
+ computedMinWidth = Math.min(characterWidth + hInsets, computePrefWidth(height));
+ }
+ return computedMinWidth;
+ }
+
+ @Override protected double computeMinHeight(double width) {
+ if (computedMinHeight < 0) {
+ double vInsets = snappedTopInset() + snappedBottomInset();
+ computedMinHeight = Math.min(lineHeight + vInsets, computePrefHeight(width));
+ }
+ return computedMinHeight;
+ }
+
+ @Override public void layoutChildren() {
+ TextArea textArea = getSkinnable();
+ double width = getWidth();
+
+ // Lay out paragraphs
+ final double topPadding = snappedTopInset();
+ final double leftPadding = snappedLeftInset();
+
+ double wrappingWidth = Math.max(width - (leftPadding + snappedRightInset()), 0);
+
+ double y = topPadding;
+
+ final List<Node> paragraphNodesChildren = paragraphNodes.getChildren();
+
+ for (int i = 0; i < paragraphNodesChildren.size(); i++) {
+ Node node = paragraphNodesChildren.get(i);
+ Text paragraphNode = (Text)node;
+ paragraphNode.setWrappingWidth(wrappingWidth);
+
+ Bounds bounds = paragraphNode.getBoundsInLocal();
+ paragraphNode.setLayoutX(leftPadding);
+ paragraphNode.setLayoutY(y);
+
+ y += bounds.getHeight();
+ }
+
+ if (promptNode != null) {
+ promptNode.setLayoutX(leftPadding);
+ promptNode.setLayoutY(topPadding + promptNode.getBaselineOffset());
+ promptNode.setWrappingWidth(wrappingWidth);
+ }
+
+ // Update the selection
+ IndexRange selection = textArea.getSelection();
+ Bounds oldCaretBounds = caretPath.getBoundsInParent();
+
+ selectionHighlightGroup.getChildren().clear();
+
+ int caretPos = textArea.getCaretPosition();
+ int anchorPos = textArea.getAnchor();
+
+ if (SHOW_HANDLES) {
+ // Install and resize the handles for caret and anchor.
+ if (selection.getLength() > 0) {
+ selectionHandle1.resize(selectionHandle1.prefWidth(-1),
+ selectionHandle1.prefHeight(-1));
+ selectionHandle2.resize(selectionHandle2.prefWidth(-1),
+ selectionHandle2.prefHeight(-1));
+ } else {
+ caretHandle.resize(caretHandle.prefWidth(-1),
+ caretHandle.prefHeight(-1));
+ }
+
+ // Position the handle for the anchor. This could be handle1 or handle2.
+ // Do this before positioning the actual caret.
+ if (selection.getLength() > 0) {
+ int paragraphIndex = paragraphNodesChildren.size();
+ int paragraphOffset = textArea.getLength() + 1;
+ Text paragraphNode = null;
+ do {
+ paragraphNode = (Text)paragraphNodesChildren.get(--paragraphIndex);
+ paragraphOffset -= paragraphNode.getText().length() + 1;
+ } while (anchorPos < paragraphOffset);
+
+ updateTextNodeCaretPos(anchorPos - paragraphOffset);
+ caretPath.getElements().clear();
+ caretPath.getElements().addAll(paragraphNode.getImpl_caretShape());
+ caretPath.setLayoutX(paragraphNode.getLayoutX());
+ caretPath.setLayoutY(paragraphNode.getLayoutY());
+
+ Bounds b = caretPath.getBoundsInParent();
+ if (caretPos < anchorPos) {
+ selectionHandle2.setLayoutX(b.getMinX() - selectionHandle2.getWidth() / 2);
+ selectionHandle2.setLayoutY(b.getMaxY() - 1);
+ } else {
+ selectionHandle1.setLayoutX(b.getMinX() - selectionHandle1.getWidth() / 2);
+ selectionHandle1.setLayoutY(b.getMinY() - selectionHandle1.getHeight() + 1);
+ }
+ }
+ }
+
+ {
+ // Position caret
+ int paragraphIndex = paragraphNodesChildren.size();
+ int paragraphOffset = textArea.getLength() + 1;
+
+ Text paragraphNode = null;
+ do {
+ paragraphNode = (Text)paragraphNodesChildren.get(--paragraphIndex);
+ paragraphOffset -= paragraphNode.getText().length() + 1;
+ } while (caretPos < paragraphOffset);
+
+ updateTextNodeCaretPos(caretPos - paragraphOffset);
+
+ caretPath.getElements().clear();
+ caretPath.getElements().addAll(paragraphNode.getImpl_caretShape());
+
+ caretPath.setLayoutX(paragraphNode.getLayoutX());
+
+ // TODO: Remove this temporary workaround for RT-27533
+ paragraphNode.setLayoutX(2 * paragraphNode.getLayoutX() - paragraphNode.getBoundsInParent().getMinX());
+
+ caretPath.setLayoutY(paragraphNode.getLayoutY());
+ if (oldCaretBounds == null || !oldCaretBounds.equals(caretPath.getBoundsInParent())) {
+ scrollCaretToVisible();
+ }
+ }
+
+ // Update selection fg and bg
+ int start = selection.getStart();
+ int end = selection.getEnd();
+ for (int i = 0, max = paragraphNodesChildren.size(); i < max; i++) {
+ Node paragraphNode = paragraphNodesChildren.get(i);
+ Text textNode = (Text)paragraphNode;
+ int paragraphLength = textNode.getText().length() + 1;
+ if (end > start && start < paragraphLength) {
+ textNode.setImpl_selectionStart(start);
+ textNode.setImpl_selectionEnd(Math.min(end, paragraphLength));
+
+ Path selectionHighlightPath = new Path();
+ selectionHighlightPath.setManaged(false);
+ selectionHighlightPath.setStroke(null);
+ PathElement[] selectionShape = textNode.getImpl_selectionShape();
+ if (selectionShape != null) {
+ selectionHighlightPath.getElements().addAll(selectionShape);
+ }
+ selectionHighlightGroup.getChildren().add(selectionHighlightPath);
+ selectionHighlightGroup.setVisible(true);
+ selectionHighlightPath.setLayoutX(textNode.getLayoutX());
+ selectionHighlightPath.setLayoutY(textNode.getLayoutY());
+ updateHighlightFill();
+ } else {
+ textNode.setImpl_selectionStart(-1);
+ textNode.setImpl_selectionEnd(-1);
+ selectionHighlightGroup.setVisible(false);
+ }
+ start = Math.max(0, start - paragraphLength);
+ end = Math.max(0, end - paragraphLength);
+ }
+
+ if (SHOW_HANDLES) {
+ // Position handle for the caret. This could be handle1 or handle2 when
+ // a selection is active.
+ Bounds b = caretPath.getBoundsInParent();
+ if (selection.getLength() > 0) {
+ if (caretPos < anchorPos) {
+ selectionHandle1.setLayoutX(b.getMinX() - selectionHandle1.getWidth() / 2);
+ selectionHandle1.setLayoutY(b.getMinY() - selectionHandle1.getHeight() + 1);
+ } else {
+ selectionHandle2.setLayoutX(b.getMinX() - selectionHandle2.getWidth() / 2);
+ selectionHandle2.setLayoutY(b.getMaxY() - 1);
+ }
+ } else {
+ caretHandle.setLayoutX(b.getMinX() - caretHandle.getWidth() / 2 + 1);
+ caretHandle.setLayoutY(b.getMaxY());
+ }
+ }
+
+ if (scrollPane.getPrefViewportWidth() == 0
+ || scrollPane.getPrefViewportHeight() == 0) {
+ updatePrefViewportWidth();
+ updatePrefViewportHeight();
+ if (getParent() != null && scrollPane.getPrefViewportWidth() > 0
+ || scrollPane.getPrefViewportHeight() > 0) {
+ // Force layout of viewRect in ScrollPaneSkin
+ getParent().requestLayout();
+ }
+ }
+
+ // RT-36454: Fit to width/height only if smaller than viewport.
+ // That is, grow to fit but don't shrink to fit.
+ Bounds viewportBounds = scrollPane.getViewportBounds();
+ boolean wasFitToWidth = scrollPane.isFitToWidth();
+ boolean wasFitToHeight = scrollPane.isFitToHeight();
+ boolean setFitToWidth = textArea.isWrapText() || computePrefWidth(-1) <= viewportBounds.getWidth();
+ boolean setFitToHeight = computePrefHeight(width) <= viewportBounds.getHeight();
+ if (wasFitToWidth != setFitToWidth || wasFitToHeight != setFitToHeight) {
+ Platform.runLater(() -> {
+ scrollPane.setFitToWidth(setFitToWidth);
+ scrollPane.setFitToHeight(setFitToHeight);
+ });
+ getParent().requestLayout();
+ }
+ }
+ }
}