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,33 ****
* 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;
import com.sun.javafx.scene.control.behavior.TextAreaBehavior;
import com.sun.javafx.scene.text.HitInfo;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.application.Platform;
import javafx.beans.binding.BooleanBinding;
--- 21,35 ----
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
! package javafx.scene.control.skin;
+ import 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,55 ****
--- 48,60 ----
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,525 ****
import javafx.scene.text.Text;
import javafx.util.Duration;
import java.util.List;
/**
! * Text area skin.
*/
! public class TextAreaSkin extends TextInputControlSkin<TextArea, TextAreaBehavior> {
final private TextArea textArea;
// *** NOTE: Multiple node mode is not yet fully implemented *** //
private final boolean USE_MULTIPLE_NODES = false;
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 class ContentView extends Region {
! {
! getStyleClass().add("content");
! addEventHandler(MouseEvent.MOUSE_PRESSED, event -> {
! getBehavior().mousePressed(event);
! event.consume();
! });
! addEventHandler(MouseEvent.MOUSE_RELEASED, event -> {
! getBehavior().mouseReleased(event);
! event.consume();
! });
! addEventHandler(MouseEvent.MOUSE_DRAGGED, event -> {
! getBehavior().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();
! }
! }
! }
!
! 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();
! });
contentView.paddingProperty().addListener(valueModel -> {
updatePrefViewportWidth();
updatePrefViewportHeight();
});
--- 65,287 ----
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;
+
/**
! * Default skin implementation for the {@link TextArea} control.
! *
! * @see TextArea
! * @since 9
*/
! 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;
! 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;
! }
}
+ };
! private double pressX, pressY; // For dragging handles on embedded
! private boolean handlePressed;
! /**
! * Remembers horizontal position when traversing up / down.
! */
! double targetCaretX = -1;
! /**************************************************************************
! *
! * Constructors
! *
! **************************************************************************/
! /**
! * 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);
! // install default input map for the text area control
! this.behavior = new TextAreaBehavior(control);
! this.behavior.setTextAreaSkin(this);
! // control.setInputMap(behavior.getInputMap());
! this.textArea = control;
! caretPosition = new IntegerBinding() {
! { bind(control.caretPositionProperty()); }
! @Override protected int computeValue() {
! return control.getCaretPosition();
}
! };
! caretPosition.addListener((observable, oldValue, newValue) -> {
! targetCaretX = -1;
! if (newValue.intValue() > oldValue.intValue()) {
! setForwardBias(true);
}
+ });
! forwardBiasProperty().addListener(observable -> {
! if (control.getWidth() > 0) {
! updateTextNodeCaretPos(control.getCaretPosition());
}
+ });
! // setManaged(false);
! // Initialize content
! scrollPane = new ScrollPane();
! scrollPane.setFitToWidth(control.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(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 (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 ? control.getParagraphs().size() : 1; i < n; i++) {
! CharSequence paragraph = (n == 1) ? control.textProperty().getValueSafe() : control.getParagraphs().get(i);
! addParagraphNode(i, paragraph.toString());
}
! control.selectionProperty().addListener((observable, oldValue, newValue) -> {
! // TODO Why do we need two calls here?
! control.requestLayout();
! contentView.requestLayout();
! });
! control.wrapTextProperty().addListener((observable, oldValue, newValue) -> {
! invalidateMetrics();
! scrollPane.setFitToWidth(newValue);
! });
! control.prefColumnCountProperty().addListener((observable, oldValue, newValue) -> {
! invalidateMetrics();
! updatePrefViewportWidth();
! });
! control.prefRowCountProperty().addListener((observable, oldValue, newValue) -> {
! invalidateMetrics();
! updatePrefViewportHeight();
! });
! updateFontMetrics();
! fontMetrics.addListener(valueModel -> {
! updateFontMetrics();
! });
contentView.paddingProperty().addListener(valueModel -> {
updatePrefViewportWidth();
updatePrefViewportHeight();
});
*** 539,562 ****
contentView.requestLayout();
}
}
});
! textArea.scrollTopProperty().addListener((observable, oldValue, newValue) -> {
double vValue = (newValue.doubleValue() < getScrollTopMax())
? (newValue.doubleValue() / getScrollTopMax()) : 1.0;
scrollPane.setVvalue(vValue);
});
! textArea.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) -> {
while (change.next()) {
int from = change.getFrom();
int to = change.getTo();
List<? extends CharSequence> removed = change.getRemoved();
if (from < to) {
--- 301,324 ----
contentView.requestLayout();
}
}
});
! control.scrollTopProperty().addListener((observable, oldValue, newValue) -> {
double vValue = (newValue.doubleValue() < getScrollTopMax())
? (newValue.doubleValue() / getScrollTopMax()) : 1.0;
scrollPane.setVvalue(vValue);
});
! control.scrollLeftProperty().addListener((observable, oldValue, newValue) -> {
double hValue = (newValue.doubleValue() < getScrollLeftMax())
? (newValue.doubleValue() / getScrollLeftMax()) : 1.0;
scrollPane.setHvalue(hValue);
});
if (USE_MULTIPLE_NODES) {
! 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,600 ****
paragraphNodes.getChildren().subList(from, from + removed.size()).clear();
}
}
});
} else {
! textArea.textProperty().addListener(observable -> {
invalidateMetrics();
! ((Text)paragraphNodes.getChildren().get(0)).setText(textArea.textProperty().getValueSafe());
contentView.requestLayout();
});
}
usePromptText = new BooleanBinding() {
! { bind(textArea.textProperty(), textArea.promptTextProperty()); }
@Override protected boolean computeValue() {
! String txt = textArea.getText();
! String promptTxt = textArea.getPromptText();
return ((txt == null || txt.isEmpty()) &&
promptTxt != null && !promptTxt.isEmpty());
}
};
--- 341,362 ----
paragraphNodes.getChildren().subList(from, from + removed.size()).clear();
}
}
});
} else {
! control.textProperty().addListener(observable -> {
invalidateMetrics();
! ((Text)paragraphNodes.getChildren().get(0)).setText(control.textProperty().getValueSafe());
contentView.requestLayout();
});
}
usePromptText = new BooleanBinding() {
! { bind(control.textProperty(), control.promptTextProperty()); }
@Override protected boolean computeValue() {
! String txt = control.getText();
! String promptTxt = control.getPromptText();
return ((txt == null || txt.isEmpty()) &&
promptTxt != null && !promptTxt.isEmpty());
}
};
*** 602,618 ****
createPromptNode();
}
usePromptText.addListener(observable -> {
createPromptNode();
! textArea.requestLayout();
});
updateHighlightFill();
updatePrefViewportWidth();
updatePrefViewportHeight();
! if (textArea.isFocused()) setCaretAnimating(true);
if (SHOW_HANDLES) {
selectionHandle1.setRotate(180);
EventHandler<MouseEvent> handlePressHandler = e -> {
--- 364,380 ----
createPromptNode();
}
usePromptText.addListener(observable -> {
createPromptNode();
! control.requestLayout();
});
updateHighlightFill();
updatePrefViewportWidth();
updatePrefViewportHeight();
! if (control.isFocused()) setCaretAnimating(true);
if (SHOW_HANDLES) {
selectionHandle1.setRotate(180);
EventHandler<MouseEvent> handlePressHandler = e -> {
*** 648,1232 ****
if (element instanceof MoveTo && ((MoveTo)element).getY() > e.getY() - getTextTranslateY()) {
hit.setCharIndex(pos - 1);
}
textNode.setImpl_caretPosition(oldPos);
}
! positionCaret(hit, false, false);
e.consume();
});
! selectionHandle1.setOnMouseDragged(new EventHandler<MouseEvent>() {
! @Override public void handle(MouseEvent e) {
! TextArea textArea = 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()) {
// Swap caret and anchor
! textArea.selectRange(textArea.getCaretPosition(), textArea.getAnchor());
}
if (pos > 0) {
! if (pos >= textArea.getAnchor()) {
! pos = textArea.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);
e.consume();
- }
});
! selectionHandle2.setOnMouseDragged(new EventHandler<MouseEvent>() {
! @Override public void handle(MouseEvent e) {
! TextArea textArea = 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()) {
// Swap caret and anchor
! textArea.selectRange(textArea.getCaretPosition(), textArea.getAnchor());
}
if (pos > 0) {
! if (pos <= textArea.getAnchor() + 1) {
! pos = Math.min(textArea.getAnchor() + 2, textArea.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);
}
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);
! paragraphNode.fontProperty().bind(textArea.fontProperty());
! paragraphNode.fillProperty().bind(textFill);
! paragraphNode.impl_selectionFillProperty().bind(highlightTextFill);
}
! @Override
! public void dispose() {
! // TODO Unregister listeners on text editor, paragraph list
! throw new UnsupportedOperationException();
}
! @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();
}
! @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;
! if (offset < count) {
break;
}
! offset -= count;
! paragraphIndex++;
}
! return offset == paragraph.length() ? '\n' : paragraph.charAt(offset);
}
! @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;
break;
}
! paragraphOffset += paragraphNode.getText().length() + 1;
! }
}
}
! return index;
}
! 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;
}
!
! if (select) {
! if (extendSelection) {
! getSkinnable().extendSelection(pos);
} else {
! getSkinnable().selectPositionCaret(pos);
! }
} else {
! getSkinnable().positionCaret(pos);
! }
!
! setForwardBias(hit.isLeading());
}
-
- 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 int getNextInsertionPoint(Text paragraphNode, double x, int from,
! VerticalDirection scrollDirection) {
! // TODO
! return 0;
}
! @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);
}
! @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?
! Platform.runLater(() -> {
! if (getSkinnable().getLength() == 0) {
! return;
}
! Rectangle2D characterBounds = getCharacterBounds(index);
! scrollBoundsToVisible(characterBounds);
! });
}
! 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");
! }
!
! @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();
if (pos > 0) {
if (previousIfAtStart && text.codePointAt(pos-1) == 0x0a) {
// We are at the beginning of a paragraph.
// Back up to the previous paragraph.
--- 410,738 ----
if (element instanceof MoveTo && ((MoveTo)element).getY() > e.getY() - getTextTranslateY()) {
hit.setCharIndex(pos - 1);
}
textNode.setImpl_caretPosition(oldPos);
}
! positionCaret(hit, false);
e.consume();
});
! 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 (control1.getAnchor() < control1.getCaretPosition()) {
// Swap caret and anchor
! control1.selectRange(control1.getCaretPosition(), control1.getAnchor());
}
if (pos > 0) {
! 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);
e.consume();
});
! 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 (control1.getAnchor() > control1.getCaretPosition()) {
// Swap caret and anchor
! control1.selectRange(control1.getCaretPosition(), control1.getAnchor());
}
if (pos > 0) {
! 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);
}
e.consume();
});
}
}
! /***************************************************************************
! * *
! * Public API *
! * *
! **************************************************************************/
! /** {@inheritDoc} */
! @Override protected void invalidateMetrics() {
! computedMinWidth = Double.NEGATIVE_INFINITY;
! computedMinHeight = Double.NEGATIVE_INFINITY;
! computedPrefWidth = Double.NEGATIVE_INFINITY;
! computedPrefHeight = Double.NEGATIVE_INFINITY;
}
! /** {@inheritDoc} */
! @Override protected void layoutChildren(double contentX, double contentY, double contentWidth, double contentHeight) {
! scrollPane.resizeRelocate(contentX, contentY, contentWidth, contentHeight);
}
! /** {@inheritDoc} */
! @Override protected void updateHighlightFill() {
! for (Node node : selectionHighlightGroup.getChildren()) {
! Path selectionHighlightPath = (Path)node;
! selectionHighlightPath.setFill(highlightFillProperty().get());
! }
}
! // 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);
! };
! /** {@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;
! 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;
! case PAGE:
! switch (dir) {
! case UP:
! previousPage(select);
! break;
! case DOWN:
! nextPage(select);
! break;
! default:
! throw new IllegalArgumentException(""+dir);
}
+ break;
! 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;
! default:
! throw new IllegalArgumentException(""+unit);
}
}
! private 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);
} 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();
}
}
}
}
! private 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;
! }
! }
! private void previousLine(boolean select) {
! downLines(-1, select, false);
! }
! private void nextLine(boolean select) {
! downLines(1, select, false);
}
! private void previousPage(boolean select) {
! downLines(-(int)(scrollPane.getViewportBounds().getHeight() / lineHeight),
! select, false);
}
! private void nextPage(boolean select) {
! downLines((int)(scrollPane.getViewportBounds().getHeight() / lineHeight),
! select, false);
! }
! private void lineStart(boolean select, boolean extendSelection) {
! targetCaretX = 0;
! downLines(0, select, extendSelection);
! targetCaretX = -1;
}
!
! private void lineEnd(boolean select, boolean extendSelection) {
! targetCaretX = Double.MAX_VALUE;
! downLines(0, select, extendSelection);
! targetCaretX = -1;
}
!
! private void paragraphStart(boolean previousIfAtStart, boolean select) {
TextArea textArea = getSkinnable();
! 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,1257 ****
textArea.positionCaret(pos);
}
}
}
! public void paragraphEnd(boolean goPastInitialNewline, boolean goPastTrailingNewline, boolean select) {
TextArea textArea = getSkinnable();
String text = textArea.textProperty().getValueSafe();
int pos = textArea.getCaretPosition();
int len = text.length();
boolean wentPastInitialNewline = false;
if (pos < len) {
if (goPastInitialNewline && text.codePointAt(pos) == 0x0a) {
// We are at the end of a paragraph, start by moving to the
// next paragraph.
--- 748,764 ----
textArea.positionCaret(pos);
}
}
}
! 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,1294 ****
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());
! }
!
@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();
--- 782,792 ----
textArea.positionCaret(pos);
}
}
}
! /** {@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,1307 ****
--- 796,806 ----
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,1320 ****
--- 810,820 ----
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,1381 ****
}
}
contentView.getChildren().addAll(nodes);
}
@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);
! }
! }
!
@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 Bounds getCaretBounds() {
return getSkinnable().sceneToLocal(caretPath.localToScene(caretPath.getBoundsInLocal()));
}
! @Override
! protected Object queryAccessibleAttribute(AccessibleAttribute attribute, Object... parameters) {
switch (attribute) {
case LINE_FOR_OFFSET:
case LINE_START:
case LINE_END:
case BOUNDS_FOR_RANGE:
--- 833,868 ----
}
}
contentView.getChildren().addAll(nodes);
}
+ /** {@inheritDoc} */
@Override protected void removeHighlight(List<? extends Node> nodes) {
contentView.getChildren().removeAll(nodes);
}
! /** {@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()));
}
! /** {@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,1388 ****
--- 870,1529 ----
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();
+ }
+ }
+ }
}