javafx.scene.control.skin.TextInputControlSkin Maven / Gradle / Ivy
/*
* Copyright (c) 2011, 2024, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package javafx.scene.control.skin;
import java.lang.ref.WeakReference;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import com.sun.javafx.PlatformUtil;
import com.sun.javafx.scene.control.Properties;
import com.sun.javafx.scene.control.behavior.TextInputControlBehavior;
import com.sun.javafx.scene.control.skin.FXVK;
import com.sun.javafx.scene.input.ExtendedInputMethodRequests;
import com.sun.javafx.tk.FontMetrics;
import com.sun.javafx.tk.Toolkit;
import static com.sun.javafx.PlatformUtil.*;
import javafx.animation.Animation.Status;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.application.ConditionalFeature;
import javafx.application.Platform;
import javafx.beans.binding.BooleanBinding;
import javafx.beans.binding.ObjectBinding;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.value.ObservableBooleanValue;
import javafx.beans.value.ObservableObjectValue;
import javafx.collections.ObservableList;
import javafx.css.CssMetaData;
import javafx.css.Styleable;
import javafx.css.StyleableBooleanProperty;
import javafx.css.StyleableObjectProperty;
import javafx.css.StyleableProperty;
import javafx.css.converter.BooleanConverter;
import javafx.css.converter.PaintConverter;
import javafx.event.EventHandler;
import javafx.geometry.NodeOrientation;
import javafx.geometry.Point2D;
import javafx.geometry.Rectangle2D;
import javafx.scene.AccessibleAction;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.IndexRange;
import javafx.scene.control.SkinBase;
import javafx.scene.control.TextInputControl;
import javafx.scene.input.InputMethodEvent;
import javafx.scene.input.InputMethodHighlight;
import javafx.scene.input.InputMethodRequests;
import javafx.scene.input.InputMethodTextRun;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.scene.paint.Paint;
import javafx.scene.shape.ClosePath;
import javafx.scene.shape.HLineTo;
import javafx.scene.shape.Line;
import javafx.scene.shape.LineTo;
import javafx.scene.shape.MoveTo;
import javafx.scene.shape.Path;
import javafx.scene.shape.PathElement;
import javafx.scene.shape.Shape;
import javafx.scene.shape.VLineTo;
import javafx.stage.Window;
import javafx.util.Duration;
/**
* Abstract base class for text input skins.
*
* @param the type of the text input control
* @since 9
* @see TextFieldSkin
* @see TextAreaSkin
*/
public abstract class TextInputControlSkin extends SkinBase {
/* ************************************************************************
*
* Static fields / blocks
*
**************************************************************************/
/**
* Unit names for caret movement.
*
* @see #moveCaret(TextUnit, Direction, boolean)
*/
public static enum TextUnit {
/** Character unit */
CHARACTER,
/** Word unit */
WORD,
/** Line unit */
LINE,
/** Paragraph unit */
PARAGRAPH,
/** Page unit */
PAGE
}
/**
* Direction names for caret movement.
*
* @see #moveCaret(TextUnit, Direction, boolean)
*/
public static enum Direction {
/** Left Direction */
LEFT,
/** Right Direction */
RIGHT,
/** Up Direction */
UP,
/** Down Direction */
DOWN,
/** Beginning */
BEGINNING,
/** End */
END
}
static boolean preload = false;
static {
@SuppressWarnings("removal")
var dummy = AccessController.doPrivileged((PrivilegedAction) () -> {
String s = System.getProperty("com.sun.javafx.virtualKeyboard.preload");
if (s != null) {
if (s.equalsIgnoreCase("PRERENDER")) {
preload = true;
}
}
return null;
});
}
/**
* Specifies whether we ought to show handles. We should do it on touch platforms
*/
static final boolean SHOW_HANDLES = Properties.IS_TOUCH_SUPPORTED;
private final static boolean IS_FXVK_SUPPORTED = Platform.isSupported(ConditionalFeature.VIRTUAL_KEYBOARD);
/* ************************************************************************
*
* Private fields
*
**************************************************************************/
final ObservableObjectValue fontMetrics;
private ObservableBooleanValue caretVisible;
private CaretBlinking caretBlinking = new CaretBlinking(blinkProperty());
/**
* A path, provided by the textNode, which represents the caret.
* I assume this has to be updated whenever the caretPosition
* changes. Perhaps more frequently (including text changes),
* but I'm not sure.
*/
final Path caretPath = new Path();
StackPane caretHandle = null;
StackPane selectionHandle1 = null;
StackPane selectionHandle2 = null;
// Start/Length of the text under input method composition
private int imstart;
private int imlength;
// Holds concrete attributes for the composition runs
private List imattrs = new java.util.ArrayList<>();
private final EventHandler inputMethodTextChangedHandler = this::handleInputMethodEvent;
private InputMethodRequests inputMethodRequests;
/* ************************************************************************
*
* Constructors
*
**************************************************************************/
/**
* Creates a new instance of TextInputControlSkin, although note that this
* instance does not handle any behavior / input mappings - this needs to be
* handled appropriately by subclasses.
*
* @param control The control that this skin should be installed onto.
*/
public TextInputControlSkin(final T control) {
super(control);
fontMetrics = new ObjectBinding<>() {
{ bind(control.fontProperty()); }
@Override protected FontMetrics computeValue() {
invalidateMetrics();
return Toolkit.getToolkit().getFontLoader().getFontMetrics(control.getFont());
}
};
/**
* The caret is visible when the text box is focused AND when the selection
* is empty. If the selection is non empty or the text box is not focused
* then we don't want to show the caret. Also, we show the caret while
* performing some operations such as most key strokes. In that case we
* simply toggle its opacity.
*
*/
caretVisible = new BooleanBinding() {
{ bind(control.focusedProperty(), control.anchorProperty(), control.caretPositionProperty(),
control.disabledProperty(), control.editableProperty(), displayCaret, blinkProperty());}
@Override protected boolean computeValue() {
// RT-10682: On Windows, we show the caret during selection, but on others we hide it
return !blinkProperty().get() && displayCaret.get() && control.isFocused() &&
(isWindows() || (control.getCaretPosition() == control.getAnchor())) &&
!control.isDisabled() &&
control.isEditable();
}
};
if (SHOW_HANDLES) {
caretHandle = new StackPane();
selectionHandle1 = new StackPane();
selectionHandle2 = new StackPane();
caretHandle.setManaged(false);
selectionHandle1.setManaged(false);
selectionHandle2.setManaged(false);
if (PlatformUtil.isIOS()) {
caretHandle.setVisible(false);
} else {
caretHandle.visibleProperty().bind(new BooleanBinding() {
{
bind(control.focusedProperty(), control.anchorProperty(),
control.caretPositionProperty(), control.disabledProperty(),
control.editableProperty(), control.lengthProperty(), displayCaret);
}
@Override
protected boolean computeValue() {
return (displayCaret.get() && control.isFocused() &&
control.getCaretPosition() == control.getAnchor() &&
!control.isDisabled() && control.isEditable() &&
control.getLength() > 0);
}
});
}
selectionHandle1.visibleProperty().bind(new BooleanBinding() {
{ bind(control.focusedProperty(), control.anchorProperty(), control.caretPositionProperty(),
control.disabledProperty(), displayCaret);}
@Override protected boolean computeValue() {
return (displayCaret.get() && control.isFocused() &&
control.getCaretPosition() != control.getAnchor() &&
!control.isDisabled());
}
});
selectionHandle2.visibleProperty().bind(new BooleanBinding() {
{ bind(control.focusedProperty(), control.anchorProperty(), control.caretPositionProperty(),
control.disabledProperty(), displayCaret);}
@Override protected boolean computeValue() {
return (displayCaret.get() && control.isFocused() &&
control.getCaretPosition() != control.getAnchor() &&
!control.isDisabled());
}
});
caretHandle.getStyleClass().setAll("caret-handle");
selectionHandle1.getStyleClass().setAll("selection-handle");
selectionHandle2.getStyleClass().setAll("selection-handle");
selectionHandle1.setId("selection-handle-1");
selectionHandle2.setId("selection-handle-2");
}
if (IS_FXVK_SUPPORTED) {
if (preload) {
Scene scene = control.getScene();
if (scene != null) {
Window window = scene.getWindow();
if (window != null) {
FXVK.init(control);
}
}
}
registerInvalidationListener(control.focusedProperty(), observable -> {
if (FXVK.useFXVK()) {
Scene scene = getSkinnable().getScene();
if (control.isEditable() && control.isFocused()) {
FXVK.attach(control);
} else if (scene == null ||
scene.getWindow() == null ||
!scene.getWindow().isFocused() ||
!(scene.getFocusOwner() instanceof TextInputControl &&
((TextInputControl)scene.getFocusOwner()).isEditable())) {
FXVK.detach();
}
}
});
}
}
@Override
public void install() {
super.install();
TextInputControl control = getSkinnable();
// IMPORTANT: both setOnInputMethodTextChanged() and setInputMethodRequests() are required for IME to work
if (control.getOnInputMethodTextChanged() == null) {
control.setOnInputMethodTextChanged(inputMethodTextChangedHandler);
}
if (control.getInputMethodRequests() == null) {
inputMethodRequests = new ExtendedInputMethodRequests() {
@Override public Point2D getTextLocation(int offset) {
Scene scene = control.getScene();
Window window = scene != null ? scene.getWindow() : null;
if (window == null) {
return new Point2D(0, 0);
}
// Don't use imstart here because it isn't initialized yet.
Rectangle2D characterBounds = getCharacterBounds(control.getSelection().getStart() + offset);
Point2D p = control.localToScene(characterBounds.getMinX(), characterBounds.getMaxY());
Point2D location = new Point2D(window.getX() + scene.getX() + p.getX(),
window.getY() + scene.getY() + p.getY());
return location;
}
@Override public int getLocationOffset(int x, int y) {
return getInsertionPoint(x, y);
}
@Override public void cancelLatestCommittedText() {
// TODO
}
@Override public String getSelectedText() {
IndexRange selection = control.getSelection();
return control.getText(selection.getStart(), selection.getEnd());
}
@Override public int getInsertPositionOffset() {
int caretPosition = control.getCaretPosition();
if (caretPosition < imstart) {
return caretPosition;
} else if (caretPosition < imstart + imlength) {
return imstart;
} else {
return caretPosition - imlength;
}
}
@Override public String getCommittedText(int begin, int end) {
if (begin < imstart) {
if (end <= imstart) {
return control.getText(begin, end);
} else {
return control.getText(begin, imstart) + control.getText(imstart + imlength, end + imlength);
}
} else {
return control.getText(begin + imlength, end + imlength);
}
}
@Override public int getCommittedTextLength() {
return control.getText().length() - imlength;
}
};
control.setInputMethodRequests(inputMethodRequests);
}
}
@Override
public void dispose() {
if (getSkinnable() == null) {
return;
}
if (getSkinnable().getInputMethodRequests() == inputMethodRequests) {
getSkinnable().setInputMethodRequests(null);
}
if (getSkinnable().getOnInputMethodTextChanged() == inputMethodTextChangedHandler) {
getSkinnable().setOnInputMethodTextChanged(null);
}
super.dispose();
}
/* ************************************************************************
*
* Properties
*
**************************************************************************/
// --- blink
private BooleanProperty blink;
private final void setBlink(boolean value) {
blinkProperty().set(value);
}
private final boolean isBlink() {
return blinkProperty().get();
}
private final BooleanProperty blinkProperty() {
if (blink == null) {
blink = new SimpleBooleanProperty(this, "blink", true);
}
return blink;
}
// --- text fill
/**
* The fill to use for the text under normal conditions
*/
private final ObjectProperty textFill = new StyleableObjectProperty(Color.BLACK) {
@Override protected void invalidated() {
updateTextFill();
}
@Override public Object getBean() {
return TextInputControlSkin.this;
}
@Override public String getName() {
return "textFill";
}
@Override public CssMetaData getCssMetaData() {
return StyleableProperties.TEXT_FILL;
}
};
/**
* The fill {@code Paint} used for the foreground text color.
* @param value the text fill
*/
protected final void setTextFill(Paint value) {
textFill.set(value);
}
protected final Paint getTextFill() {
return textFill.get();
}
protected final ObjectProperty textFillProperty() {
return textFill;
}
/**
* The fill {@code Paint} used for the foreground of prompt text.
*/
private final ObjectProperty promptTextFill = new StyleableObjectProperty(Color.GRAY) {
@Override public Object getBean() {
return TextInputControlSkin.this;
}
@Override public String getName() {
return "promptTextFill";
}
@Override public CssMetaData getCssMetaData() {
return StyleableProperties.PROMPT_TEXT_FILL;
}
};
/**
* The fill {@code Paint} used for the foreground prompt text color.
* @param value the prompt text fill
*/
protected final void setPromptTextFill(Paint value) {
promptTextFill.set(value);
}
protected final Paint getPromptTextFill() {
return promptTextFill.get();
}
protected final ObjectProperty promptTextFillProperty() {
return promptTextFill;
}
// --- hightlight fill
/**
* The fill to use for the text when highlighted.
*/
private final ObjectProperty highlightFill = new StyleableObjectProperty(Color.DODGERBLUE) {
@Override protected void invalidated() {
updateHighlightFill();
}
@Override public Object getBean() {
return TextInputControlSkin.this;
}
@Override public String getName() {
return "highlightFill";
}
@Override public CssMetaData getCssMetaData() {
return StyleableProperties.HIGHLIGHT_FILL;
}
};
/**
* The fill {@code Paint} used for the background of selected text.
* @param value the highlight fill
*/
protected final void setHighlightFill(Paint value) {
highlightFill.set(value);
}
protected final Paint getHighlightFill() {
return highlightFill.get();
}
protected final ObjectProperty highlightFillProperty() {
return highlightFill;
}
/**
* The fill {@code Paint} used for the foreground of selected text.
*/
private final ObjectProperty highlightTextFill = new StyleableObjectProperty(Color.WHITE) {
@Override protected void invalidated() {
updateHighlightTextFill();
}
@Override public Object getBean() {
return TextInputControlSkin.this;
}
@Override public String getName() {
return "highlightTextFill";
}
@Override public CssMetaData getCssMetaData() {
return StyleableProperties.HIGHLIGHT_TEXT_FILL;
}
};
/**
* The fill {@code Paint} used for the foreground of selected text.
* @param value the highlight text fill
*/
protected final void setHighlightTextFill(Paint value) {
highlightTextFill.set(value);
}
protected final Paint getHighlightTextFill() {
return highlightTextFill.get();
}
protected final ObjectProperty highlightTextFillProperty() {
return highlightTextFill;
}
// --- display caret
private final BooleanProperty displayCaret = new StyleableBooleanProperty(true) {
@Override public Object getBean() {
return TextInputControlSkin.this;
}
@Override public String getName() {
return "displayCaret";
}
@Override public CssMetaData getCssMetaData() {
return StyleableProperties.DISPLAY_CARET;
}
};
private final void setDisplayCaret(boolean value) {
displayCaret.set(value);
}
private final boolean isDisplayCaret() {
return displayCaret.get();
}
private final BooleanProperty displayCaretProperty() {
return displayCaret;
}
/**
* Caret bias in the content. true means a bias towards forward character
* (true=leading/false=trailing)
*/
private BooleanProperty forwardBias = new SimpleBooleanProperty(this, "forwardBias", true);
protected final BooleanProperty forwardBiasProperty() {
return forwardBias;
}
// Public for behavior
public final void setForwardBias(boolean isLeading) {
forwardBias.set(isLeading);
}
protected final boolean isForwardBias() {
return forwardBias.get();
}
/* ************************************************************************
*
* Abstract API
*
**************************************************************************/
/**
* Gets the path elements describing the shape of the underline for the given range.
* @param start the start
* @param end the end
* @return the path elements describing the shape of the underline for the given range
*/
protected abstract PathElement[] getUnderlineShape(int start, int end);
/** Gets the path elements describing the bounding rectangles for the given range of text.
* @param start the start
* @param end the end
* @return the path elements describing the bounding rectangles for the given range of text
*/
protected abstract PathElement[] getRangeShape(int start, int end);
/**
* Adds highlight for composed text from Input Method.
* @param nodes the list of nodes
* @param start the start
*/
protected abstract void addHighlight(List extends Node> nodes, int start);
/**
* Removes highlight for composed text from Input Method.
* @param nodes the list of nodes
*/
protected abstract void removeHighlight(List extends Node> nodes);
// Public for behavior
/**
* Moves the caret by one of the given text unit, in the given
* direction. Note that only certain combinations are valid,
* depending on the implementing subclass.
*
* @param unit the unit of text to move by.
* @param dir the direction of movement.
* @param select whether to extends the selection to the new posititon.
*/
public abstract void moveCaret(TextUnit unit, Direction dir, boolean select);
/* ************************************************************************
*
* Public API
*
**************************************************************************/
// Public for behavior
/**
* Returns the position to be used for a context menu, based on the location
* of the caret handle or selection handles. This is supported only on touch
* displays and does not use the location of the mouse.
* @return the position to be used for this context menu
*/
public Point2D getMenuPosition() {
if (SHOW_HANDLES) {
if (caretHandle.isVisible()) {
return new Point2D(caretHandle.getLayoutX() + caretHandle.getWidth() / 2,
caretHandle.getLayoutY());
} else if (selectionHandle1.isVisible() && selectionHandle2.isVisible()) {
return new Point2D((selectionHandle1.getLayoutX() + selectionHandle1.getWidth() / 2 +
selectionHandle2.getLayoutX() + selectionHandle2.getWidth() / 2) / 2,
selectionHandle2.getLayoutY() + selectionHandle2.getHeight() / 2);
} else {
return null;
}
} else {
throw new UnsupportedOperationException();
}
}
// For use with PasswordField in TextFieldSkin
/**
* This method may be overridden by subclasses to replace the displayed
* characters without affecting the actual text content. This is used to
* display bullet characters in PasswordField.
*
* @param txt the content that may need to be masked.
* @return the replacement string. This may just be the input string, or may be a string of replacement characters with the same length as the input string.
*/
protected String maskText(String txt) {
return txt;
}
/**
* Returns the insertion point for a given location.
*
* @param x the x location
* @param y the y location
* @return the insertion point for a given location
*/
protected int getInsertionPoint(double x, double y) { return 0; }
/**
* Returns the bounds of the character at a given index.
*
* @param index the index
* @return the bounds of the character at a given index
*/
public Rectangle2D getCharacterBounds(int index) { return null; }
/**
* Ensures that the character at a given index is visible.
*
* @param index the index
*/
protected void scrollCharacterToVisible(int index) {}
/**
* Invalidates cached min and pref sizes for the TextInputControl.
*/
protected void invalidateMetrics() {
}
/**
* Called when textFill property changes.
*/
protected void updateTextFill() {}
/**
* Called when highlightFill property changes.
*/
protected void updateHighlightFill() {}
/**
* Called when highlightTextFill property changes.
*/
protected void updateHighlightTextFill() {}
/**
* Handles an input method event.
* @param event the {@code InputMethodEvent} to be handled
*/
protected void handleInputMethodEvent(InputMethodEvent event) {
final TextInputControl textInput = getSkinnable();
if (textInput.isEditable() && !textInput.textProperty().isBound() && !textInput.isDisabled()) {
// remove previous input method text (if any) or selected text
if (imlength != 0) {
removeHighlight(imattrs);
imattrs.clear();
textInput.selectRange(imstart, imstart + imlength);
}
// Insert committed text
if (event.getCommitted().length() != 0) {
String committed = event.getCommitted();
textInput.replaceText(textInput.getSelection(), committed);
}
// Replace composed text
imstart = textInput.getSelection().getStart();
StringBuilder composed = new StringBuilder();
for (InputMethodTextRun run : event.getComposed()) {
composed.append(run.getText());
}
textInput.replaceText(textInput.getSelection(), composed.toString());
imlength = composed.length();
if (imlength != 0) {
int pos = imstart;
for (InputMethodTextRun run : event.getComposed()) {
int endPos = pos + run.getText().length();
createInputMethodAttributes(run.getHighlight(), pos, endPos);
pos = endPos;
}
addHighlight(imattrs, imstart);
// Set caret position in composed text
int caretPos = event.getCaretPosition();
if (caretPos >= 0 && caretPos < imlength) {
textInput.selectRange(imstart + caretPos, imstart + caretPos);
}
}
}
}
// Public for behavior
/**
* Starts or stops caret blinking. The behavior classes use this to temporarily
* pause blinking while user is typing or otherwise moving the caret.
*
* @param value whether caret should be blinking.
*/
public void setCaretAnimating(boolean value) {
if (value) {
caretBlinking.start();
} else {
caretBlinking.stop();
blinkProperty().set(true);
}
}
/* ************************************************************************
*
* Private implementation
*
**************************************************************************/
TextInputControlBehavior getBehavior() {
return null;
}
ObservableBooleanValue caretVisibleProperty() {
return caretVisible;
}
// for testing only!
boolean isCaretBlinking() {
return caretBlinking.caretTimeline.getStatus() == Status.RUNNING;
}
boolean isRTL() {
return (getSkinnable().getEffectiveNodeOrientation() == NodeOrientation.RIGHT_TO_LEFT);
}
private void createInputMethodAttributes(InputMethodHighlight highlight, int start, int end) {
double minX = 0f;
double maxX = 0f;
double minY = 0f;
double maxY = 0f;
PathElement elements[] = getUnderlineShape(start, end);
for (int i = 0; i < elements.length; i++) {
PathElement pe = elements[i];
if (pe instanceof MoveTo) {
minX = maxX = ((MoveTo)pe).getX();
minY = maxY = ((MoveTo)pe).getY();
} else if (pe instanceof LineTo) {
minX = (minX < ((LineTo)pe).getX() ? minX : ((LineTo)pe).getX());
maxX = (maxX > ((LineTo)pe).getX() ? maxX : ((LineTo)pe).getX());
minY = (minY < ((LineTo)pe).getY() ? minY : ((LineTo)pe).getY());
maxY = (maxY > ((LineTo)pe).getY() ? maxY : ((LineTo)pe).getY());
} else if (pe instanceof HLineTo) {
minX = (minX < ((HLineTo)pe).getX() ? minX : ((HLineTo)pe).getX());
maxX = (maxX > ((HLineTo)pe).getX() ? maxX : ((HLineTo)pe).getX());
} else if (pe instanceof VLineTo) {
minY = (minY < ((VLineTo)pe).getY() ? minY : ((VLineTo)pe).getY());
maxY = (maxY > ((VLineTo)pe).getY() ? maxY : ((VLineTo)pe).getY());
}
// Don't assume that shapes are ended with ClosePath.
if (pe instanceof ClosePath ||
i == elements.length - 1 ||
(i < elements.length - 1 && elements[i+1] instanceof MoveTo)) {
// Now, create the attribute.
Shape attr = null;
if (highlight == InputMethodHighlight.SELECTED_RAW) {
// blue background
attr = new Path();
((Path)attr).getElements().addAll(getRangeShape(start, end));
attr.setFill(Color.BLUE);
attr.setOpacity(0.3f);
} else if (highlight == InputMethodHighlight.UNSELECTED_RAW) {
// dash underline.
attr = new Line(minX + 2, maxY + 1, maxX - 2, maxY + 1);
attr.setStroke(textFill.get());
attr.setStrokeWidth(maxY - minY);
ObservableList dashArray = attr.getStrokeDashArray();
dashArray.add(Double.valueOf(2f));
dashArray.add(Double.valueOf(2f));
} else if (highlight == InputMethodHighlight.SELECTED_CONVERTED) {
// thick underline.
attr = new Line(minX + 2, maxY + 1, maxX - 2, maxY + 1);
attr.setStroke(textFill.get());
attr.setStrokeWidth((maxY - minY) * 3);
} else if (highlight == InputMethodHighlight.UNSELECTED_CONVERTED) {
// single underline.
attr = new Line(minX + 2, maxY + 1, maxX - 2, maxY + 1);
attr.setStroke(textFill.get());
attr.setStrokeWidth(maxY - minY);
}
if (attr != null) {
attr.setManaged(false);
imattrs.add(attr);
}
}
}
}
/* ************************************************************************
*
* Support classes
*
**************************************************************************/
private static final class CaretBlinking {
private final Timeline caretTimeline;
private final WeakReference blinkPropertyRef;
public CaretBlinking(final BooleanProperty blinkProperty) {
blinkPropertyRef = new WeakReference<>(blinkProperty);
caretTimeline = new Timeline();
caretTimeline.setCycleCount(Timeline.INDEFINITE);
caretTimeline.getKeyFrames().addAll(
new KeyFrame(Duration.ZERO, e -> setBlink(false)),
new KeyFrame(Duration.seconds(.5), e -> setBlink(true)),
new KeyFrame(Duration.seconds(1)));
}
public void start() {
caretTimeline.play();
}
public void stop() {
caretTimeline.stop();
}
private void setBlink(final boolean value) {
final BooleanProperty blinkProperty = blinkPropertyRef.get();
if (blinkProperty == null) {
caretTimeline.stop();
return;
}
blinkProperty.set(value);
}
}
private static class StyleableProperties {
private static final CssMetaData TEXT_FILL =
new CssMetaData<>("-fx-text-fill",
PaintConverter.getInstance(), Color.BLACK) {
@Override public boolean isSettable(TextInputControl n) {
final TextInputControlSkin> skin = (TextInputControlSkin>) n.getSkin();
return skin.textFill == null || !skin.textFill.isBound();
}
@Override @SuppressWarnings("unchecked")
public StyleableProperty getStyleableProperty(TextInputControl n) {
final TextInputControlSkin> skin = (TextInputControlSkin>) n.getSkin();
return (StyleableProperty)skin.textFill;
}
};
private static final CssMetaData PROMPT_TEXT_FILL =
new CssMetaData<>("-fx-prompt-text-fill",
PaintConverter.getInstance(), Color.GRAY) {
@Override public boolean isSettable(TextInputControl n) {
final TextInputControlSkin> skin = (TextInputControlSkin>) n.getSkin();
return skin.promptTextFill == null || !skin.promptTextFill.isBound();
}
@Override @SuppressWarnings("unchecked")
public StyleableProperty getStyleableProperty(TextInputControl n) {
final TextInputControlSkin> skin = (TextInputControlSkin>) n.getSkin();
return (StyleableProperty)skin.promptTextFill;
}
};
private static final CssMetaData HIGHLIGHT_FILL =
new CssMetaData<>("-fx-highlight-fill",
PaintConverter.getInstance(), Color.DODGERBLUE) {
@Override public boolean isSettable(TextInputControl n) {
final TextInputControlSkin> skin = (TextInputControlSkin>) n.getSkin();
return skin.highlightFill == null || !skin.highlightFill.isBound();
}
@Override @SuppressWarnings("unchecked")
public StyleableProperty getStyleableProperty(TextInputControl n) {
final TextInputControlSkin> skin = (TextInputControlSkin>) n.getSkin();
return (StyleableProperty)skin.highlightFill;
}
};
private static final CssMetaData HIGHLIGHT_TEXT_FILL =
new CssMetaData<>("-fx-highlight-text-fill",
PaintConverter.getInstance(), Color.WHITE) {
@Override public boolean isSettable(TextInputControl n) {
final TextInputControlSkin> skin = (TextInputControlSkin>) n.getSkin();
return skin.highlightTextFill == null || !skin.highlightTextFill.isBound();
}
@Override @SuppressWarnings("unchecked")
public StyleableProperty getStyleableProperty(TextInputControl n) {
final TextInputControlSkin> skin = (TextInputControlSkin>) n.getSkin();
return (StyleableProperty)skin.highlightTextFill;
}
};
private static final CssMetaData DISPLAY_CARET =
new CssMetaData<>("-fx-display-caret",
BooleanConverter.getInstance(), Boolean.TRUE) {
@Override public boolean isSettable(TextInputControl n) {
final TextInputControlSkin> skin = (TextInputControlSkin>) n.getSkin();
return skin.displayCaret == null || !skin.displayCaret.isBound();
}
@Override @SuppressWarnings("unchecked")
public StyleableProperty getStyleableProperty(TextInputControl n) {
final TextInputControlSkin> skin = (TextInputControlSkin>) n.getSkin();
return (StyleableProperty)skin.displayCaret;
}
};
private static final List> STYLEABLES;
static {
List> styleables =
new ArrayList<>(SkinBase.getClassCssMetaData());
styleables.add(TEXT_FILL);
styleables.add(PROMPT_TEXT_FILL);
styleables.add(HIGHLIGHT_FILL);
styleables.add(HIGHLIGHT_TEXT_FILL);
styleables.add(DISPLAY_CARET);
STYLEABLES = Collections.unmodifiableList(styleables);
}
}
/**
* Returns the CssMetaData associated with this class, which may include the
* CssMetaData of its superclasses.
* @return the CssMetaData associated with this class, which may include the
* CssMetaData of its superclasses
*/
public static List> getClassCssMetaData() {
return StyleableProperties.STYLEABLES;
}
/**
* {@inheritDoc}
*/
@Override public List> getCssMetaData() {
return getClassCssMetaData();
}
@Override protected void executeAccessibleAction(AccessibleAction action, Object... parameters) {
switch (action) {
case SHOW_TEXT_RANGE: {
Integer start = (Integer)parameters[0];
Integer end = (Integer)parameters[1];
if (start != null && end != null) {
scrollCharacterToVisible(end);
scrollCharacterToVisible(start);
scrollCharacterToVisible(end);
}
break;
}
default: super.executeAccessibleAction(action, parameters);
}
}
}