javafx.scene.control.TextInputControl 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;
import com.sun.javafx.scene.control.FormatterAccessor;
import javafx.beans.DefaultProperty;
import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
import javafx.beans.binding.IntegerBinding;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ObjectPropertyBase;
import javafx.beans.property.ReadOnlyBooleanProperty;
import javafx.beans.property.ReadOnlyBooleanWrapper;
import javafx.beans.property.ReadOnlyIntegerProperty;
import javafx.beans.property.ReadOnlyIntegerWrapper;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.ReadOnlyObjectWrapper;
import javafx.beans.property.ReadOnlyStringProperty;
import javafx.beans.property.ReadOnlyStringWrapper;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableStringValue;
import javafx.beans.value.ObservableValue;
import javafx.beans.value.WritableValue;
import javafx.css.CssMetaData;
import javafx.css.FontCssMetaData;
import javafx.css.PseudoClass;
import javafx.css.StyleOrigin;
import javafx.css.Styleable;
import javafx.css.StyleableObjectProperty;
import javafx.css.StyleableProperty;
import javafx.scene.AccessibleAction;
import javafx.scene.AccessibleAttribute;
import javafx.scene.input.Clipboard;
import javafx.scene.input.ClipboardContent;
import javafx.scene.text.Font;
import java.text.BreakIterator;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import com.sun.javafx.util.Utils;
import com.sun.javafx.binding.ExpressionHelper;
import com.sun.javafx.scene.NodeHelper;
import javafx.util.StringConverter;
/**
* Abstract base class for text input controls.
* @since JavaFX 2.0
*/
@DefaultProperty("text")
public abstract class TextInputControl extends Control {
/**
* Interface representing a text input's content. Since it is an ObservableStringValue,
* you can also bind to, or observe the content.
* @since JavaFX 2.0
*/
protected interface Content extends ObservableStringValue {
/**
* Retrieves a subset of the content.
*
* @param start the start
* @param end the end
* @return a subset of the content
*/
public String get(int start, int end);
/**
* Inserts a sequence of characters into the content.
*
* @param index the index
* @param text the text string
* @param notifyListeners the notify listener flag
* @since JavaFX 2.1
*/
public void insert(int index, String text, boolean notifyListeners);
/**
* Removes a sequence of characters from the content.
*
* @param start the start
* @param end the end
* @param notifyListeners the notify listener flag
* @since JavaFX 2.1
*/
public void delete(int start, int end, boolean notifyListeners);
/**
* Returns the number of characters represented by the content.
* @return the number of characters
*/
public int length();
}
/**
* Package private base implementation of Content.
*/
abstract static class ContentBase implements Content {
private ExpressionHelper helper;
@Override
public void addListener(ChangeListener super String> changeListener) {
helper = ExpressionHelper.addListener(helper, this, changeListener);
}
@Override
public void removeListener(ChangeListener super String> changeListener) {
helper = ExpressionHelper.removeListener(helper, changeListener);
}
@Override
public void addListener(InvalidationListener listener) {
helper = ExpressionHelper.addListener(helper, this, listener);
}
@Override
public void removeListener(InvalidationListener listener) {
helper = ExpressionHelper.removeListener(helper, listener);
}
protected final void fireValueChangedEvent() {
ExpressionHelper.fireValueChangedEvent(helper);
}
}
private boolean blockSelectedTextUpdate;
/* *************************************************************************
* *
* Constructors *
* *
**************************************************************************/
/**
* Creates a new TextInputControl. The content is an immutable property and
* must be specified (as non-null) at the time of construction.
*
* @param content a non-null implementation of Content.
*/
protected TextInputControl(final Content content) {
this.content = content;
// Add a listener so that whenever the Content is changed, we notify
// listeners of the text property that it is invalid.
content.addListener(observable -> {
if (content.length() > 0) {
text.textIsNull = false;
}
text.controlContentHasChanged();
});
// Bind the length to be based on the length of the text property
length.bind(new IntegerBinding() {
{ bind(text); }
@Override protected int computeValue() {
String txt = text.get();
return txt == null ? 0 : txt.length();
}
});
// Bind the selected text to be based on the selection and text properties
selection.addListener((ob, o, n) -> updateSelectedText());
text.addListener((ob, o, n) -> updateSelectedText());
focusedProperty().addListener((ob, o, n) -> {
if (n) {
if (getTextFormatter() != null) {
updateText(getTextFormatter());
}
} else {
commitValue();
}
});
// Specify the default style class
getStyleClass().add("text-input");
}
private void updateSelectedText() {
if (!blockSelectedTextUpdate) {
String txt = text.get();
IndexRange sel = selection.get();
if (txt == null || sel == null) {
selectedText.set("");
} else {
int start = sel.getStart();
int end = sel.getEnd();
int length = txt.length();
if (end > start + length) {
end = length;
}
if (start > length - 1) {
start = end = 0;
}
selectedText.set(txt.substring(start, end));
}
}
}
/* *************************************************************************
* *
* Properties *
* *
**************************************************************************/
/**
* The default font to use for text in the TextInputControl. If the TextInputControl's text is
* rich text then this font may or may not be used depending on the font
* information embedded in the rich text, but in any case where a default
* font is required, this font will be used.
* @return the font property
* @since JavaFX 8.0
*/
public final ObjectProperty fontProperty() {
if (font == null) {
font = new StyleableObjectProperty(Font.getDefault()) {
private boolean fontSetByCss = false;
@Override
public void applyStyle(StyleOrigin newOrigin, Font value) {
//
// RT-20727 - if CSS is setting the font, then make sure invalidate doesn't call NodeHelper.reapplyCSS
//
try {
// super.applyStyle calls set which might throw if value is bound.
// Have to make sure fontSetByCss is reset.
fontSetByCss = true;
super.applyStyle(newOrigin, value);
} catch(Exception e) {
throw e;
} finally {
fontSetByCss = false;
}
}
@Override
public void set(Font value) {
final Font oldValue = get();
if (value == null ? oldValue == null : value.equals(oldValue)) {
return;
}
super.set(value);
}
@Override
protected void invalidated() {
// RT-20727 - if font is changed by calling setFont, then
// css might need to be reapplied since font size affects
// calculated values for styles with relative values
if(fontSetByCss == false) {
NodeHelper.reapplyCSS(TextInputControl.this);
}
}
@Override
public CssMetaData getCssMetaData() {
return StyleableProperties.FONT;
}
@Override
public Object getBean() {
return TextInputControl.this;
}
@Override
public String getName() {
return "font";
}
};
}
return font;
}
private ObjectProperty font;
public final void setFont(Font value) { fontProperty().setValue(value); }
public final Font getFont() { return font == null ? Font.getDefault() : font.getValue(); }
/**
* The prompt text to display in the {@code TextInputControl}. If set to null or an empty string, no
* prompt text is displayed.
*
* @defaultValue An empty String
* @since JavaFX 2.2
*/
private StringProperty promptText = new SimpleStringProperty(this, "promptText", "") {
@Override protected void invalidated() {
// Strip out newlines
String txt = get();
if (txt != null && txt.contains("\n")) {
txt = txt.replace("\n", "");
set(txt);
}
}
};
public final StringProperty promptTextProperty() { return promptText; }
public final String getPromptText() { return promptText.get(); }
public final void setPromptText(String value) { promptText.set(value); }
/**
* The property contains currently attached {@link TextFormatter}.
* Since the value is part of the {@code Formatter}, changing the TextFormatter will update the text based on the new textFormatter.
*
* @defaultValue null
* @since JavaFX 8u40
*/
private final ObjectProperty> textFormatter = new ObjectPropertyBase<>() {
private TextFormatter> oldFormatter = null;
@Override
public Object getBean() {
return TextInputControl.this;
}
@Override
public String getName() {
return "textFormatter";
}
@Override
protected void invalidated() {
final TextFormatter> formatter = get();
try {
if (formatter != null) {
try {
formatter.bindToControl(f -> updateText(f));
} catch (IllegalStateException e) {
if (isBound()) {
unbind();
}
set(null);
throw e;
}
if (!isFocused()) {
updateText(get());
}
}
if (oldFormatter != null) {
oldFormatter.unbindFromControl();
}
} finally {
oldFormatter = formatter;
}
}
};
public final ObjectProperty> textFormatterProperty() { return textFormatter; }
public final TextFormatter> getTextFormatter() { return textFormatter.get(); }
public final void setTextFormatter(TextFormatter> value) { textFormatter.set(value); }
private final Content content;
/**
* Returns the text input's content model.
* @return the text input's content model
*/
protected final Content getContent() {
return content;
}
/**
* The textual content of this TextInputControl.
*/
private TextProperty text = new TextProperty();
public final String getText() { return text.get(); }
public final void setText(String value) { text.set(value); }
public final StringProperty textProperty() { return text; }
/**
* The number of characters in the text input.
*/
private ReadOnlyIntegerWrapper length = new ReadOnlyIntegerWrapper(this, "length");
public final int getLength() { return length.get(); }
public final ReadOnlyIntegerProperty lengthProperty() { return length.getReadOnlyProperty(); }
/**
* Indicates whether this TextInputControl can be edited by the user.
*/
private BooleanProperty editable = new SimpleBooleanProperty(this, "editable", true) {
@Override protected void invalidated() {
pseudoClassStateChanged(PSEUDO_CLASS_READONLY, ! get());
}
};
public final boolean isEditable() { return editable.getValue(); }
public final void setEditable(boolean value) { editable.setValue(value); }
public final BooleanProperty editableProperty() { return editable; }
/**
* The current selection.
*/
private ReadOnlyObjectWrapper selection = new ReadOnlyObjectWrapper<>(this, "selection", new IndexRange(0, 0));
public final IndexRange getSelection() { return selection.getValue(); }
public final ReadOnlyObjectProperty selectionProperty() { return selection.getReadOnlyProperty(); }
/**
* Defines the characters in the TextInputControl which are selected
*/
private ReadOnlyStringWrapper selectedText = new ReadOnlyStringWrapper(this, "selectedText");
public final String getSelectedText() { return selectedText.get(); }
public final ReadOnlyStringProperty selectedTextProperty() { return selectedText.getReadOnlyProperty(); }
/**
* The anchor
of the text selection.
* The anchor
and caretPosition
make up the selection
* range. Selection must always be specified in terms of begin <= end, but
* anchor
may be less than, equal to, or greater than the
* caretPosition
. Depending on how the user selects text,
* the anchor might represent the lower or upper bound of the selection.
*/
private ReadOnlyIntegerWrapper anchor = new ReadOnlyIntegerWrapper(this, "anchor", 0);
public final int getAnchor() { return anchor.get(); }
public final ReadOnlyIntegerProperty anchorProperty() { return anchor.getReadOnlyProperty(); }
/**
* The current position of the caret within the text.
* The anchor
and caretPosition
make up the selection
* range. Selection must always be specified in terms of begin <= end, but
* anchor
may be less than, equal to, or greater than the
* caretPosition
. Depending on how the user selects text,
* the caretPosition might represent the lower or upper bound of the selection.
*/
private ReadOnlyIntegerWrapper caretPosition = new ReadOnlyIntegerWrapper(this, "caretPosition", 0);
public final int getCaretPosition() { return caretPosition.get(); }
public final ReadOnlyIntegerProperty caretPositionProperty() { return caretPosition.getReadOnlyProperty(); }
private UndoRedoChange undoChangeHead = new UndoRedoChange();
private UndoRedoChange undoChange = undoChangeHead;
private boolean createNewUndoRecord = false;
/**
* The property describes if it's currently possible to undo the latest change of the content that was done.
* @defaultValue false
* @since JavaFX 8u40
*/
private final ReadOnlyBooleanWrapper undoable = new ReadOnlyBooleanWrapper(this, "undoable", false);
public final boolean isUndoable() { return undoable.get(); }
public final ReadOnlyBooleanProperty undoableProperty() { return undoable.getReadOnlyProperty(); }
/**
* The property describes if it's currently possible to redo the latest change of the content that was undone.
* @defaultValue false
* @since JavaFX 8u40
*/
private final ReadOnlyBooleanWrapper redoable = new ReadOnlyBooleanWrapper(this, "redoable", false);
public final boolean isRedoable() { return redoable.get(); }
public final ReadOnlyBooleanProperty redoableProperty() { return redoable.getReadOnlyProperty(); }
/* *************************************************************************
* *
* Methods *
* *
**************************************************************************/
/**
* Returns a subset of the text input's content.
*
* @param start must be a value between 0 and end - 1.
* @param end must be less than or equal to the length
* @return the subset of the text input's content
*/
public String getText(int start, int end) {
if (start > end) {
throw new IllegalArgumentException("The start must be <= the end");
}
if (start < 0
|| end > getLength()) {
throw new IndexOutOfBoundsException();
}
return getContent().get(start, end);
}
/**
* Appends a sequence of characters to the content.
*
* @param text a non null String
*/
public void appendText(String text) {
insertText(getLength(), text);
}
/**
* Inserts a sequence of characters into the content.
*
* @param index The location to insert the text.
* @param text The text to insert.
*/
public void insertText(int index, String text) {
replaceText(index, index, text);
}
/**
* Removes a range of characters from the content.
*
* @param range The range of text to delete. The range object must not be null.
*
* @see #deleteText(int, int)
*/
public void deleteText(IndexRange range) {
replaceText(range, "");
}
/**
* Removes a range of characters from the content.
*
* @param start The starting index in the range, inclusive. This must be >= 0 and < the end.
* @param end The ending index in the range, exclusive. This is one-past the last character to
* delete (consistent with the String manipulation methods). This must be > the start,
* and <= the length of the text.
*/
public void deleteText(int start, int end) {
replaceText(start, end, "");
}
/**
* Replaces a range of characters with the given text.
*
* @param range The range of text to replace. The range object must not be null.
* @param text The text that is to replace the range. This must not be null.
*
* @see #replaceText(int, int, String)
*/
public void replaceText(IndexRange range, String text) {
final int start = range.getStart();
final int end = start + range.getLength();
replaceText(start, end, text);
}
/**
* Replaces a range of characters with the given text.
*
* @param start The starting index in the range, inclusive. This must be >= 0 and < the end.
* @param end The ending index in the range, exclusive. This is one-past the last character to
* delete (consistent with the String manipulation methods). This must be > the start,
* and <= the length of the text.
* @param text The text that is to replace the range. This must not be null.
*/
public void replaceText(final int start, final int end, final String text) {
if (start > end) {
throw new IllegalArgumentException();
}
if (text == null) {
throw new NullPointerException();
}
if (start < 0
|| end > getLength()) {
throw new IndexOutOfBoundsException();
}
if (!this.text.isBound()) {
final int oldLength = getLength();
TextFormatter> formatter = getTextFormatter();
TextFormatter.Change change;
if (formatter != null && formatter.getFilter() != null) {
change = new TextFormatter.Change(this, getFormatterAccessor(), start, end, text);
change = formatter.getFilter().apply(change);
if (change == null) {
return;
}
} else {
change = new TextFormatter.Change(this, getFormatterAccessor(), start, end, filterInput(text));
}
// Update the content
updateContent(change, oldLength == 0);
}
}
private void updateContent(TextFormatter.Change change, boolean forceNewUndoRecord) {
final boolean nonEmptySelection = getSelection().getLength() > 0;
String oldText = getText(change.start, change.end);
int adjustmentAmount = replaceText(change.start, change.end, change.text, change.getAnchor(), change.getCaretPosition());
String newText = getText(change.start, change.start + change.text.length() - adjustmentAmount);
if (newText.equals(oldText)) {
// Undo record not required as there is no change in the text.
return;
}
/*
* A new undo record is created, if
* 1. createNewUndoRecord is true, currently it is set to true for paste operation
* 2. Text is selected and a character is typed
* 3. This is the first operation to be added to undo record
* 4. forceNewUndoRecord is true, currently it is set to true if there is no text present
* 5. Space character is typed
* 6. 2500 milliseconds are elapsed since the undo record was created
* 7. Cursor position is changed and a character is typed
* 8. A range of text is replaced programmatically using replaceText()
* Otherwise, the last undo record is updated or discarded.
*/
int endOfUndoChange = undoChange == undoChangeHead ? -1 : undoChange.start + undoChange.newText.length();
boolean isNewSpaceChar = false;
if (newText.equals(" ")) {
if (!UndoRedoChange.isSpaceCharSequence()) {
isNewSpaceChar = true;
UndoRedoChange.setSpaceCharSequence(true);
}
} else {
UndoRedoChange.setSpaceCharSequence(false);
}
if (createNewUndoRecord || nonEmptySelection || endOfUndoChange == -1 || forceNewUndoRecord ||
isNewSpaceChar || UndoRedoChange.hasChangeDurationElapsed() ||
(endOfUndoChange != change.start && endOfUndoChange != change.end) || change.end - change.start > 0) {
undoChange = undoChange.add(change.start, oldText, newText);
} else if (change.start != change.end && change.text.isEmpty()) {
// I know I am deleting, and am located at the end of the range of the current undo record
if (undoChange.newText.length() > 0) {
undoChange.newText = undoChange.newText.substring(0, change.start - undoChange.start);
if (undoChange.newText.isEmpty()) {
// throw away this undo change record
undoChange = undoChange.discard();
}
} else {
if (change.start == endOfUndoChange) {
undoChange.oldText += oldText;
} else { // end == endOfUndoChange
undoChange.oldText = oldText + undoChange.oldText;
undoChange.start--;
}
}
} else {
// I know I am adding, and am located at the end of the range of the current undo record
undoChange.newText += newText;
}
updateUndoRedoState();
}
/**
* Transfers the currently selected range in the text to the clipboard,
* removing the current selection.
*/
public void cut() {
copy();
IndexRange selection = getSelection();
deleteText(selection.getStart(), selection.getEnd());
}
/**
* Transfers the currently selected range in the text to the clipboard,
* leaving the current selection.
*/
public void copy() {
final String selectedText = getSelectedText();
if (selectedText.length() > 0) {
final ClipboardContent content = new ClipboardContent();
content.putString(selectedText);
Clipboard.getSystemClipboard().setContent(content);
}
}
/**
* Transfers the contents in the clipboard into this text,
* replacing the current selection. If there is no selection, the contents
* in the clipboard is inserted at the current caret position.
*/
public void paste() {
final Clipboard clipboard = Clipboard.getSystemClipboard();
if (clipboard.hasString()) {
final String text = clipboard.getString();
if (text != null) {
createNewUndoRecord = true;
try {
replaceSelection(text);
} finally {
createNewUndoRecord = false;
}
}
}
}
/**
* Moves the selection backward one char in the text. This may have the
* effect of deselecting, depending on the location of the anchor relative
* to the caretPosition. This function effectively just moves the caretPosition.
*/
public void selectBackward() {
if (getCaretPosition() > 0 && getLength() > 0) {
// because the anchor stays put, by moving the caret to the left
// we ensure that a selection is registered and that it is correct
if (charIterator == null) {
charIterator = BreakIterator.getCharacterInstance();
}
charIterator.setText(getText());
selectRange(getAnchor(), charIterator.preceding(getCaretPosition()));
}
}
/**
* Moves the selection forward one char in the text. This may have the
* effect of deselecting, depending on the location of the anchor relative
* to the caretPosition. This function effectively just moves the caret forward.
*/
public void selectForward() {
final int textLength = getLength();
if (textLength > 0 && getCaretPosition() < textLength) {
if (charIterator == null) {
charIterator = BreakIterator.getCharacterInstance();
}
charIterator.setText(getText());
selectRange(getAnchor(), charIterator.following(getCaretPosition()));
}
}
/**
* The break iterator instances for navigation over words and complex characters.
*/
private BreakIterator charIterator;
private BreakIterator wordIterator;
/**
* Moves the caret to the beginning of previous word. This function
* also has the effect of clearing the selection.
*/
public void previousWord() {
previousWord(false);
}
/**
* Moves the caret to the beginning of next word. This function
* also has the effect of clearing the selection.
*/
public void nextWord() {
nextWord(false);
}
/**
* Moves the caret to the end of the next word. This function
* also has the effect of clearing the selection.
*/
public void endOfNextWord() {
endOfNextWord(false);
}
/**
* Moves the caret to the beginning of previous word. This does not cause
* the selection to be cleared. Rather, the anchor stays put and the caretPosition is
* moved to the beginning of previous word.
*/
public void selectPreviousWord() {
previousWord(true);
}
/**
* Moves the caret to the beginning of next word. This does not cause
* the selection to be cleared. Rather, the anchor stays put and the caretPosition is
* moved to the beginning of next word.
*/
public void selectNextWord() {
nextWord(true);
}
/**
* Moves the caret to the end of the next word. This does not cause
* the selection to be cleared.
*/
public void selectEndOfNextWord() {
endOfNextWord(true);
}
private void previousWord(boolean select) {
final int textLength = getLength();
final String text = getText();
if (textLength <= 0) {
return;
}
if (wordIterator == null) {
wordIterator = BreakIterator.getWordInstance();
}
wordIterator.setText(text);
int pos = wordIterator.preceding(Utils.clamp(0, getCaretPosition(), textLength));
// Skip the non-word region, then move/select to the beginning of the word.
while (pos != BreakIterator.DONE && !isLetterOrDigit(text, pos)) {
pos = wordIterator.preceding(Utils.clamp(0, pos, textLength));
}
// move/select
selectRange(select ? getAnchor() : pos, pos);
}
private void nextWord(boolean select) {
final int textLength = getLength();
final String text = getText();
if (textLength <= 0) {
return;
}
if (wordIterator == null) {
wordIterator = BreakIterator.getWordInstance();
}
wordIterator.setText(text);
int last = wordIterator.following(Utils.clamp(0, getCaretPosition(), textLength-1));
int current = wordIterator.next();
// Skip whitespace characters to the beginning of next word, but
// stop at newline. Then move the caret or select a range.
while (current != BreakIterator.DONE) {
for (int p=last; p<=current; p++) {
char ch = text.charAt(Utils.clamp(0, p, textLength-1));
// Avoid using Character.isSpaceChar() and Character.isWhitespace(),
// because they include LINE_SEPARATOR, PARAGRAPH_SEPARATOR, etc.
if (ch != ' ' && ch != '\t') {
if (select) {
selectRange(getAnchor(), p);
} else {
selectRange(p, p);
}
return;
}
}
last = current;
current = wordIterator.next();
}
// move/select to the end
if (select) {
selectRange(getAnchor(), textLength);
} else {
end();
}
}
private void endOfNextWord(boolean select) {
final int textLength = getLength();
final String text = getText();
if (textLength <= 0) {
return;
}
if (wordIterator == null) {
wordIterator = BreakIterator.getWordInstance();
}
wordIterator.setText(text);
int last = wordIterator.following(Utils.clamp(0, getCaretPosition(), textLength));
int current = wordIterator.next();
// skip the non-word region, then move/select to the end of the word.
while (current != BreakIterator.DONE) {
for (int p=last; p<=current; p++) {
if (!Character.isLetterOrDigit(text.charAt(Utils.clamp(0, p, textLength-1)))) {
if (select) {
selectRange(getAnchor(), p);
} else {
selectRange(p, p);
}
return;
}
}
last = current;
current = wordIterator.next();
}
// move/select to the end
if (select) {
selectRange(getAnchor(), textLength);
} else {
end();
}
}
/**
* Selects all text in the text input.
*/
public void selectAll() {
selectRange(0, getLength());
}
/**
* Moves the caret to before the first char of the text. This function
* also has the effect of clearing the selection.
*/
public void home() {
// user wants to go to start
selectRange(0, 0);
}
/**
* Moves the caret to after the last char of the text. This function
* also has the effect of clearing the selection.
*/
public void end() {
// user wants to go to end
final int textLength = getLength();
if (textLength > 0) {
selectRange(textLength, textLength);
}
}
/**
* Moves the caret to before the first char of text. This does not cause
* the selection to be cleared. Rather, the anchor stays put and the
* caretPosition is moved to before the first char.
*/
public void selectHome() {
selectRange(getAnchor(), 0);
}
/**
* Moves the caret to after the last char of text. This does not cause
* the selection to be cleared. Rather, the anchor stays put and the
* caretPosition is moved to after the last char.
*/
public void selectEnd() {
final int textLength = getLength();
if (textLength > 0) selectRange(getAnchor(), textLength);
}
/**
* Deletes the character that precedes the current caret position from the
* text if there is no selection, or deletes the selection if there is one.
* This function returns true if the deletion succeeded, false otherwise.
* @return true if the deletion succeeded, false otherwise
*/
public boolean deletePreviousChar() {
boolean failed = true;
if (isEditable() && !isDisabled()) {
final String text = getText();
final int dot = getCaretPosition();
final int mark = getAnchor();
if (dot != mark) {
// there is a selection of text to remove
replaceSelection("");
failed = false;
} else if (dot > 0) {
// The caret is not at the beginning, so remove some characters.
// Typically you'd only be removing a single character, but
// in some cases you must remove two depending on the unicode
// characters
// Note: Do not use charIterator here, because we do want to
// break up clusters when deleting backwards.
int p = Character.offsetByCodePoints(text, dot, -1);
deleteText(p, dot);
failed = false;
}
}
return !failed;
}
/**
* Deletes the character that follows the current caret position from the
* text if there is no selection, or deletes the selection if there is one.
* This function returns true if the deletion succeeded, false otherwise.
* @return true if the deletion succeeded, false otherwise
*/
public boolean deleteNextChar() {
boolean failed = true;
if (isEditable() && !isDisabled()) {
final int textLength = getLength();
final String text = getText();
final int dot = getCaretPosition();
final int mark = getAnchor();
if (dot != mark) {
// there is a selection of text to remove
replaceSelection("");
failed = false;
} else if (textLength > 0 && dot < textLength) {
// The caret is not at the end, so remove some characters.
// Typically you'd only be removing a single character, but
// in some cases you must remove two depending on the unicode
// characters
if (charIterator == null) {
charIterator = BreakIterator.getCharacterInstance();
}
charIterator.setText(text);
int p = charIterator.following(dot);
deleteText(dot, p);
failed = false;
}
}
return !failed;
}
/**
* Moves the caret position forward. If there is no selection, then the
* caret position is moved one character forward. If there is a selection,
* then the caret position is moved to the end of the selection and
* the selection cleared.
*/
public void forward() {
// user has moved caret to the right
final int textLength = getLength();
final int dot = getCaretPosition();
final int mark = getAnchor();
if (dot != mark) {
int pos = Math.max(dot, mark);
selectRange(pos, pos);
} else if (dot < textLength && textLength > 0) {
if (charIterator == null) {
charIterator = BreakIterator.getCharacterInstance();
}
charIterator.setText(getText());
int pos = charIterator.following(dot);
selectRange(pos, pos);
}
deselect();
}
/**
* Moves the caret position backward. If there is no selection, then the
* caret position is moved one character backward. If there is a selection,
* then the caret position is moved to the beginning of the selection and
* the selection cleared.
*
* Note: This function is intended to be used by experts, primarily
* by those implementing new Skins or Behaviors. It is not common
* for developers or designers to access this function directly.
*/
public void backward() {
// user has moved caret to the left
final int textLength = getLength();
final int dot = getCaretPosition();
final int mark = getAnchor();
if (dot != mark) {
int pos = Math.min(dot, mark);
selectRange(pos, pos);
} else if (dot > 0 && textLength > 0) {
if (charIterator == null) {
charIterator = BreakIterator.getCharacterInstance();
}
charIterator.setText(getText());
int pos = charIterator.preceding(dot);
selectRange(pos, pos);
}
deselect();
}
/**
* Positions the caret to the position indicated by {@code pos}. This
* function will also clear the selection.
* @param pos the position
*/
public void positionCaret(int pos) {
final int p = Utils.clamp(0, pos, getLength());
selectRange(p, p);
}
/**
* Positions the caret to the position indicated by {@code pos} and extends
* the selection, if there is one. If there is no selection, then a
* selection is formed where the anchor is at the current caret position
* and the caretPosition is moved to pos.
* @param pos the position
*/
public void selectPositionCaret(int pos) {
selectRange(getAnchor(), Utils.clamp(0, pos, getLength()));
}
/**
* Positions the anchor and caretPosition explicitly.
* @param anchor the anchor
* @param caretPosition the caretPosition
*/
public void selectRange(int anchor, int caretPosition) {
caretPosition = Utils.clamp(0, caretPosition, getLength());
anchor = Utils.clamp(0, anchor, getLength());
TextFormatter.Change change = new TextFormatter.Change(this, getFormatterAccessor(), anchor, caretPosition);
TextFormatter> formatter = getTextFormatter();
if (formatter != null && formatter.getFilter() != null) {
change = formatter.getFilter().apply(change);
if (change == null) {
return;
}
}
updateContent(change, false);
}
private void doSelectRange(int anchor, int caretPosition) {
this.caretPosition.set(Utils.clamp(0, caretPosition, getLength()));
this.anchor.set(Utils.clamp(0, anchor, getLength()));
this.selection.set(IndexRange.normalize(getAnchor(), getCaretPosition()));
notifyAccessibleAttributeChanged(AccessibleAttribute.SELECTION_START);
}
/**
* This function will extend the selection to include the specified pos.
* This is different from selectPositionCaret in that it does not simply
* move the caret. Rather, it will reposition the caret and anchor as necessary
* to ensure that pos becomes the new caret and the far other end of the
* selection becomes the anchor.
* @param pos the position
*/
public void extendSelection(int pos) {
final int p = Utils.clamp(0, pos, getLength());
final int dot = getCaretPosition();
final int mark = getAnchor();
int start = Math.min(dot, mark);
int end = Math.max(dot, mark);
if (p < start) {
selectRange(end, p);
} else {
selectRange(start, p);
}
}
/**
* Clears the text.
*/
public void clear() {
deselect();
if (!text.isBound()) {
setText("");
}
}
/**
* Clears the selection.
*/
public void deselect() {
// set the anchor equal to the caret position, which clears the selection
// while also preserving the caret position
selectRange(getCaretPosition(), getCaretPosition());
}
/**
* Replaces the selection with the given replacement String. If there is
* no selection, then the replacement text is simply inserted at the current
* caret position. If there was a selection, then the selection is cleared
* and the given replacement text inserted.
* @param replacement the replacement string
*/
public void replaceSelection(String replacement) {
replaceText(getSelection(), replacement);
}
/**
* If possible, undoes the last modification. If {@link #isUndoable()} returns
* false, then calling this method has no effect.
* @since JavaFX 8u40
*/
public final void undo() {
if (isUndoable()) {
// Apply reverse change here
final int start = undoChange.start;
final String newText = undoChange.newText;
final String oldText = undoChange.oldText;
blockSelectedTextUpdate = true;
try {
if (newText != null) {
getContent().delete(start, start + newText.length(), oldText.isEmpty());
}
if (oldText != null) {
getContent().insert(start, oldText, true);
doSelectRange(start, start + oldText.length());
} else {
doSelectRange(start, start + newText.length());
}
undoChange = undoChange.prev;
} finally {
blockSelectedTextUpdate = false;
updateSelectedText();
}
}
updateUndoRedoState();
}
/**
* If possible, redoes the last undone modification. If {@link #isRedoable()} returns
* false, then calling this method has no effect.
* @since JavaFX 8u40
*/
public final void redo() {
if (isRedoable()) {
// Apply change here
undoChange = undoChange.next;
final int start = undoChange.start;
final String newText = undoChange.newText;
final String oldText = undoChange.oldText;
blockSelectedTextUpdate = true;
try {
if (oldText != null) {
getContent().delete(start, start + oldText.length(), newText.isEmpty());
}
if (newText != null) {
getContent().insert(start, newText, true);
doSelectRange(start + newText.length(), start + newText.length());
} else {
doSelectRange(start, start);
}
} finally {
blockSelectedTextUpdate = false;
updateSelectedText();
}
}
updateUndoRedoState();
// else beep ?
}
// Used by TextArea, although there are probably other better ways of
// doing this.
void textUpdated() { }
private void resetUndoRedoState() {
undoChange = undoChangeHead;
undoChange.next = null;
updateUndoRedoState();
}
private void updateUndoRedoState() {
undoable.set(undoChange != undoChangeHead);
redoable.set(undoChange.next != null);
}
private boolean filterAndSet(String value) {
// Send the new value through the textFormatter, if one exists.
TextFormatter> formatter = getTextFormatter();
int length = content.length();
if (formatter != null && formatter.getFilter() != null && !text.isBound()) {
TextFormatter.Change change = new TextFormatter.Change(
TextInputControl.this, getFormatterAccessor(), 0, length, value, 0, 0);
change = formatter.getFilter().apply(change);
if (change == null) {
return false;
}
replaceText(change.start, change.end, change.text, change.getAnchor(), change.getCaretPosition());
} else {
replaceText(0, length, value, 0, 0);
}
return true;
}
/**
* This is what is ultimately called by every code path that will update
* the content (except for undo / redo). The input into this method has
* already run through the textFormatter where appropriate.
*
* @param start The start index into the existing text which
* will be replaced by the new value
* @param end The end index into the existing text which will
* be replaced by the new value. As with
* String.replace this is a lastIndex+1 value
* @param value The new text value
* @param anchor The new selection anchor after the change is made
* @param caretPosition The new selection caretPosition after the change
* is made.
* @return The amount of adjustment made to the end / anchor / caretPosition to
* accommodate for subsequent filtering (such as the filtering of
* new lines by the TextField)
*/
private int replaceText(int start, int end, String value, int anchor, int caretPosition) {
// RT-16566: Need to take into account stripping of chars into the
// final anchor & caret position
blockSelectedTextUpdate = true;
try {
int length = getLength();
int adjustmentAmount = 0;
if (end != start) {
getContent().delete(start, end, value.isEmpty());
length -= (end - start);
}
if (value != null) {
getContent().insert(start, value, true);
adjustmentAmount = value.length() - (getLength() - length);
anchor -= adjustmentAmount;
caretPosition -= adjustmentAmount;
}
doSelectRange(anchor, caretPosition);
return adjustmentAmount;
} finally {
blockSelectedTextUpdate = false;
updateSelectedText();
}
}
private void updateText(TextFormatter formatter) {
T value = formatter.getValue();
StringConverter converter = formatter.getValueConverter();
if (converter != null) {
String text = converter.toString(value);
if (text == null) text = "";
replaceText(0, getLength(), text, text.length(), text.length());
}
}
/**
* Commit the current text and convert it to a value.
* @since JavaFX 8u40
*/
public final void commitValue() {
if (getTextFormatter() != null) {
getTextFormatter().updateValue(getText());
}
}
/**
* If the field is currently being edited, this call will set text to the last commited value.
* @since JavaFX 8u40
*/
public final void cancelEdit() {
if (getTextFormatter() != null) {
updateText(getTextFormatter());
}
}
private FormatterAccessor accessor;
private FormatterAccessor getFormatterAccessor() {
if (accessor == null) {
accessor = new TextInputControlFromatterAccessor();
}
return accessor;
}
String filterInput(String text) {
// This method should be overridden by child classes.
// It is overridden in TextField and TextArea as needed.
return text;
}
/**
* A little utility method for stripping out unwanted characters.
*
* @param txt
* @param stripNewlines
* @param stripTabs
* @return The string after having the unwanted characters stripped out.
*/
static String filterInput(String txt, boolean stripNewlines, boolean stripTabs) {
// Most of the time, when text is inserted, there are no illegal
// characters. So we'll do a "cheap" check for illegal characters.
// If we find one, we'll do a longer replace algorithm. In the
// case of illegal characters, this may at worst be an O(2n) solution.
// Strip out any characters that are outside the printed range
if (containsInvalidCharacters(txt, stripNewlines, stripTabs)) {
StringBuilder s = new StringBuilder(txt.length());
for (int i=0; i observable = null;
// Added to the observable when bound
private InvalidationListener listener = null;
// Used for event handling
private ExpressionHelper helper = null;
// The developer my set the Text property to null. Although
// the Content must be given an empty String, we must still
// treat the value as though it were null, so that a subsequent
// getText() will return null.
private boolean textIsNull = false;
@Override public String get() {
// Since we force eager binding and content is always up to date,
// we just need to get it from content and not through the binding
return textIsNull ? null : content.get();
}
@Override public void set(String value) {
if (isBound()) {
throw new java.lang.RuntimeException("A bound value cannot be set.");
}
doSet(value);
markInvalid();
}
/**
* Called whenever the content on the control has changed (as determined
* by a listener on the content).
*/
private void controlContentHasChanged() {
markInvalid();
notifyAccessibleAttributeChanged(AccessibleAttribute.TEXT);
}
@Override public void bind(ObservableValue extends String> observable) {
if (observable == null) {
throw new NullPointerException("Cannot bind to null");
}
if (!observable.equals(this.observable)) {
unbind();
this.observable = observable;
if (listener == null) {
listener = new Listener();
}
this.observable.addListener(listener);
markInvalid();
doSet(observable.getValue());
}
}
@Override public void unbind() {
if (observable != null) {
doSet(observable.getValue());
observable.removeListener(listener);
observable = null;
}
}
@Override public boolean isBound() {
return observable != null;
}
@Override public void addListener(InvalidationListener listener) {
helper = ExpressionHelper.addListener(helper, this, listener);
}
@Override public void removeListener(InvalidationListener listener) {
helper = ExpressionHelper.removeListener(helper, listener);
}
@Override public void addListener(ChangeListener super String> listener) {
helper = ExpressionHelper.addListener(helper, this, listener);
}
@Override public void removeListener(ChangeListener super String> listener) {
helper = ExpressionHelper.removeListener(helper, listener);
}
@Override public Object getBean() {
return TextInputControl.this;
}
@Override public String getName() {
return "text";
}
private void fireValueChangedEvent() {
ExpressionHelper.fireValueChangedEvent(helper);
}
private void markInvalid() {
fireValueChangedEvent();
}
/**
* doSet is called whenever the setText() method was called directly
* on the TextInputControl, or when the text property was bound,
* unbound, or reacted to a binding invalidation. It is *not* called
* when modifications to the content happened indirectly, such as
* through the replaceText / replaceSelection methods.
*
* @param value The new value
*/
private void doSet(String value) {
// Guard against the null value.
textIsNull = value == null;
if (value == null) value = "";
if (!filterAndSet(value)) return;
if (getTextFormatter() != null) {
getTextFormatter().updateValue(getText());
}
textUpdated();
// If the programmer has directly manipulated the text property
// or has it bound up, then we will clear out any modifications
// from the undo manager as we must suppose that the control is
// being reused, for example, between forms.
resetUndoRedoState();
}
private class Listener implements InvalidationListener {
@Override
public void invalidated(Observable valueModel) {
// We now need to force it to be eagerly recomputed
// because we need to push these changes to the
// content model. Because changing the model ends
// up calling invalidate and markInvalid, the
// listeners will all be notified.
doSet(observable.getValue());
}
}
}
/**
* Used to form a linked-list of Undo / Redo changes. Each UndoRedoChange
* records the old and new text, and the start index. It also has
* the links to the previous and next Changes in the chain. There
* are two special UndoRedoChange objects in this chain representing the
* head and the tail so we can have beforeFirst and afterLast
* behavior as necessary.
*/
static class UndoRedoChange {
static long prevRecordTime;
static final long CHANGE_DURATION = 2500; // milliseconds
static boolean spaceCharSequence = false;
int start;
String oldText;
String newText;
UndoRedoChange prev;
UndoRedoChange next;
UndoRedoChange() { }
public UndoRedoChange add(int start, String oldText, String newText) {
UndoRedoChange c = new UndoRedoChange();
c.start = start;
c.oldText = oldText;
c.newText = newText;
c.prev = this;
next = c;
prevRecordTime = System.currentTimeMillis();
return c;
}
static boolean hasChangeDurationElapsed() {
return (System.currentTimeMillis() - prevRecordTime > CHANGE_DURATION) ;
}
static void setSpaceCharSequence(boolean value) {
spaceCharSequence = value;
}
static boolean isSpaceCharSequence() {
return spaceCharSequence;
}
public UndoRedoChange discard() {
prev.next = next;
return prev;
}
// Handy to use when debugging, just put it in undo or redo
// method or replaceText to see what is happening to the undo
// history as it occurs.
void debugPrint() {
UndoRedoChange c = this;
System.out.print("[");
while (c != null) {
System.out.print(c.toString());
if (c.next != null) System.out.print(", ");
c = c.next;
}
System.out.println("]");
}
@Override public String toString() {
if (oldText == null && newText == null) {
return "head";
}
if (oldText.isEmpty() && !newText.isEmpty()) {
return "added '" + newText + "' at index " + start;
} else if (!oldText.isEmpty() && !newText.isEmpty()) {
return "replaced '" + oldText + "' with '" + newText + "' at index " + start;
} else {
return "deleted '" + oldText + "' at index " + start;
}
}
}
/* *************************************************************************
* *
* Stylesheet Handling *
* *
**************************************************************************/
private static final PseudoClass PSEUDO_CLASS_READONLY
= PseudoClass.getPseudoClass("readonly");
private static class StyleableProperties {
private static final FontCssMetaData FONT =
new FontCssMetaData<>("-fx-font", Font.getDefault()) {
@Override
public boolean isSettable(TextInputControl n) {
return n.font == null || !n.font.isBound();
}
@Override
public StyleableProperty getStyleableProperty(TextInputControl n) {
return (StyleableProperty)(WritableValue)n.fontProperty();
}
};
private static final List> STYLEABLES;
static {
final List> styleables =
new ArrayList<>(Control.getClassCssMetaData());
styleables.add(FONT);
STYLEABLES = Collections.unmodifiableList(styleables);
}
}
/**
* Gets the {@code CssMetaData} associated with this class, which may include the
* {@code CssMetaData} of its superclasses.
* @return the {@code CssMetaData}
* @since JavaFX 8.0
*/
public static List> getClassCssMetaData() {
return StyleableProperties.STYLEABLES;
}
/**
* {@inheritDoc}
* @since JavaFX 8.0
*/
@Override
public List> getControlCssMetaData() {
return getClassCssMetaData();
}
/* *************************************************************************
* *
* Accessibility handling *
* *
**************************************************************************/
/** {@inheritDoc} */
@Override
public Object queryAccessibleAttribute(AccessibleAttribute attribute, Object... parameters) {
switch (attribute) {
case TEXT: {
String accText = getAccessibleText();
if (accText != null && !accText.isEmpty()) return accText;
String text = getText();
if (text == null || text.isEmpty()) {
text = getPromptText();
}
return text;
}
case EDITABLE: return isEditable();
case SELECTION_START: return getSelection().getStart();
case SELECTION_END: return getSelection().getEnd();
case CARET_OFFSET: return getCaretPosition();
case FONT: return getFont();
default: return super.queryAccessibleAttribute(attribute, parameters);
}
}
/** {@inheritDoc} */
@Override
public void executeAccessibleAction(AccessibleAction action, Object... parameters) {
switch (action) {
case SET_TEXT: {
String value = (String) parameters[0];
if (value != null) setText(value);
break;
}
case SET_TEXT_SELECTION: {
Integer start = (Integer) parameters[0];
Integer end = (Integer) parameters[1];
if (start != null && end != null) {
selectRange(start, end);
}
break;
}
default: super.executeAccessibleAction(action, parameters);
}
}
private class TextInputControlFromatterAccessor implements FormatterAccessor {
@Override
public int getTextLength() {
return TextInputControl.this.getLength();
}
@Override
public String getText(int begin, int end) {
return TextInputControl.this.getText(begin, end);
}
@Override
public int getCaret() {
return TextInputControl.this.getCaretPosition();
}
@Override
public int getAnchor() {
return TextInputControl.this.getAnchor();
}
}
private static boolean isLetterOrDigit(String text, int ix) {
if (ix < 0) {
// should not happen
return false;
} else if (ix >= text.length()) {
return false;
}
// ignore the case when 'c' is a high surrogate without the low surrogate
int c = Character.codePointAt(text, ix);
return Character.isLetterOrDigit(c);
}
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy