blob: ba465f31a9b408c2baadf2a0ddbc834c6841221f [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to you under the Apache License,
* Version 2.0 (the "License"); you may not use this file except in
* compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.pivot.wtk;
import java.awt.Toolkit;
import java.io.IOException;
import org.apache.pivot.collections.LinkedList;
import org.apache.pivot.json.JSON;
import org.apache.pivot.util.ListenerList;
import org.apache.pivot.util.Vote;
import org.apache.pivot.wtk.validation.Validator;
/**
* A component that allows a user to enter a single line of unformatted text.
*/
public class TextInput extends Component {
/**
* Text input skin interface. Text input skins are required to implement
* this.
*/
public interface Skin {
/**
* @return The insertion point for a given location.
*
* @param x The X-position (of the mouse probably).
*/
public int getInsertionPoint(int x);
/**
* @return The bounds of the character at a given index.
*
* @param index The location to check.
*/
public Bounds getCharacterBounds(int index);
}
/**
* Translates between text and context data during data binding.
*/
public interface TextBindMapping {
/**
* Converts a value from the bind context to a text representation
* during a {@link Component#load(Object)} operation.
*
* @param value The value retrieved from the bound object.
* @return A text representation of this value for display.
*/
public String toString(Object value);
/**
* Converts a text string to a value to be stored in the bind context
* during a {@link Component#store(Object)} operation.
*
* @param text The current text from the control.
* @return A value suitable for storage in the bound object.
*/
public Object valueOf(String text);
}
private interface Edit {
public void undo();
}
private class InsertTextEdit implements Edit {
private final int index;
private final int count;
public InsertTextEdit(CharSequence text, int index) {
this.index = index;
count = text.length();
}
@Override
public void undo() {
removeText(index, count, false);
}
}
private class RemoveTextEdit implements Edit {
private final int index;
private final String text;
public RemoveTextEdit(int index, int count) {
this.index = index;
text = getText(index, index + count);
}
@Override
public void undo() {
insertText(text, index, false);
}
}
private static class TextInputListenerList extends WTKListenerList<TextInputListener> implements
TextInputListener {
@Override
public void textSizeChanged(TextInput textInput, int previousTextSize) {
for (TextInputListener listener : this) {
listener.textSizeChanged(textInput, previousTextSize);
}
}
@Override
public void maximumLengthChanged(TextInput textInput, int previousMaximumLength) {
for (TextInputListener listener : this) {
listener.maximumLengthChanged(textInput, previousMaximumLength);
}
}
@Override
public void passwordChanged(TextInput textInput) {
for (TextInputListener listener : this) {
listener.passwordChanged(textInput);
}
}
@Override
public void promptChanged(TextInput textInput, String previousPrompt) {
for (TextInputListener listener : this) {
listener.promptChanged(textInput, previousPrompt);
}
}
@Override
public void textValidatorChanged(TextInput textInput, Validator previousValidator) {
for (TextInputListener listener : this) {
listener.textValidatorChanged(textInput, previousValidator);
}
}
@Override
public void strictValidationChanged(TextInput textInput) {
for (TextInputListener listener : this) {
listener.strictValidationChanged(textInput);
}
}
@Override
public void textValidChanged(TextInput textInput) {
for (TextInputListener listener : this) {
listener.textValidChanged(textInput);
}
}
@Override
public void editableChanged(TextInput textInput) {
for (TextInputListener listener : this) {
listener.editableChanged(textInput);
}
}
}
private static class TextInputContentListenerList extends
WTKListenerList<TextInputContentListener> implements TextInputContentListener {
@Override
public Vote previewInsertText(TextInput textInput, CharSequence text, int index) {
Vote vote = Vote.APPROVE;
for (TextInputContentListener listener : this) {
vote = vote.tally(listener.previewInsertText(textInput, text, index));
}
return vote;
}
@Override
public void insertTextVetoed(TextInput textInput, Vote reason) {
for (TextInputContentListener listener : this) {
listener.insertTextVetoed(textInput, reason);
}
}
@Override
public void textInserted(TextInput textInput, int index, int count) {
for (TextInputContentListener listener : this) {
listener.textInserted(textInput, index, count);
}
}
@Override
public Vote previewRemoveText(TextInput textInput, int index, int count) {
Vote vote = Vote.APPROVE;
for (TextInputContentListener listener : this) {
vote = vote.tally(listener.previewRemoveText(textInput, index, count));
}
return vote;
}
@Override
public void removeTextVetoed(TextInput textInput, Vote reason) {
for (TextInputContentListener listener : this) {
listener.removeTextVetoed(textInput, reason);
}
}
@Override
public void textRemoved(TextInput textInput, int index, int count) {
for (TextInputContentListener listener : this) {
listener.textRemoved(textInput, index, count);
}
}
@Override
public void textChanged(TextInput textInput) {
for (TextInputContentListener listener : this) {
listener.textChanged(textInput);
}
}
}
private static class TextInputSelectionListenerList extends
WTKListenerList<TextInputSelectionListener> implements TextInputSelectionListener {
@Override
public void selectionChanged(TextInput textInput, int previousSelectionStart,
int previousSelectionLength) {
for (TextInputSelectionListener listener : this) {
listener.selectionChanged(textInput, previousSelectionStart,
previousSelectionLength);
}
}
}
private static class TextInputBindingListenerList extends
WTKListenerList<TextInputBindingListener> implements TextInputBindingListener {
@Override
public void textKeyChanged(TextInput textInput, String previousTextKey) {
for (TextInputBindingListener listener : this) {
listener.textKeyChanged(textInput, previousTextKey);
}
}
@Override
public void textBindTypeChanged(TextInput textInput, BindType previousTextBindType) {
for (TextInputBindingListener listener : this) {
listener.textBindTypeChanged(textInput, previousTextBindType);
}
}
@Override
public void textBindMappingChanged(TextInput textInput,
TextBindMapping previousTextBindMapping) {
for (TextInputBindingListener listener : this) {
listener.textBindMappingChanged(textInput, previousTextBindMapping);
}
}
}
private StringBuilder characters = new StringBuilder();
private int selectionStart = 0;
private int selectionLength = 0;
private int textSize = DEFAULT_TEXT_SIZE;
private int maximumLength = 32767;
private boolean password = false;
private String prompt = null;
private boolean editable = true;
private String textKey = null;
private BindType textBindType = BindType.BOTH;
private TextBindMapping textBindMapping = null;
private Validator validator = null;
private boolean strictValidation = false;
private boolean textValid = true;
private LinkedList<Edit> editHistory = new LinkedList<>();
private TextInputListenerList textInputListeners = new TextInputListenerList();
private TextInputContentListenerList textInputContentListeners = new TextInputContentListenerList();
private TextInputSelectionListenerList textInputSelectionListeners = new TextInputSelectionListenerList();
private TextInputBindingListenerList textInputBindingListeners = new TextInputBindingListenerList();
public static final int DEFAULT_TEXT_SIZE = 16;
private static final int MAXIMUM_EDIT_HISTORY_LENGTH = 30;
public TextInput() {
installSkin(TextInput.class);
}
@Override
protected void setSkin(org.apache.pivot.wtk.Skin skin) {
if (!(skin instanceof TextInput.Skin)) {
throw new IllegalArgumentException("Skin class must implement "
+ TextInput.Skin.class.getName());
}
super.setSkin(skin);
}
/**
* Returns the text content of the text input.
*
* @return A string containing a copy of the text input's text content.
*/
public String getText() {
return getText(0, getCharacterCount());
}
/**
* Returns a portion of the text content of the text input.
*
* @param beginIndex The starting index of the text to retrieve (inclusive).
* @param endIndex The ending index of the text (exclusive).
* @return A string containing a copy of the text area's text content.
*/
public String getText(int beginIndex, int endIndex) {
return characters.substring(beginIndex, endIndex);
}
public void setText(String text) {
if (text == null) {
throw new IllegalArgumentException();
}
if (text.length() > maximumLength) {
throw new IllegalArgumentException("Text length is greater than maximum length.");
}
characters = new StringBuilder(text);
// Update selection
int previousSelectionStart = selectionStart;
int previousSelectionLength = selectionLength;
selectionStart = text.length();
selectionLength = 0;
// Update the valid flag
boolean previousTextValid = textValid;
textValid = (validator == null) ? true : validator.isValid(text);
// Clear the edit history
editHistory.clear();
// Fire change events
textInputContentListeners.textChanged(this);
if (textValid != previousTextValid) {
textInputListeners.textValidChanged(this);
}
if (selectionStart != previousSelectionStart || selectionLength != previousSelectionLength) {
textInputSelectionListeners.selectionChanged(this, selectionStart, selectionLength);
}
}
public void insertText(CharSequence text, int index) {
insertText(text, index, true);
}
private void insertText(CharSequence text, int index, boolean addToEditHistory) {
if (text == null) {
throw new IllegalArgumentException();
}
if (characters.length() + text.length() > maximumLength) {
throw new IllegalArgumentException("Insertion of text would exceed maximum length.");
}
if (text.length() > 0) {
Vote vote = textInputContentListeners.previewInsertText(this, text, index);
if (vote == Vote.APPROVE) {
// Insert the text
characters.insert(index, text);
// Add an insert history item
if (addToEditHistory) {
addHistoryItem(new InsertTextEdit(text, index));
}
// Update selection
int previousSelectionStart = selectionStart;
int previousSelectionLength = selectionLength;
selectionStart = index + text.length();
selectionLength = 0;
// Update the valid flag
boolean previousTextValid = textValid;
textValid = (validator == null) ? true : validator.isValid(getText());
// Fire change events
textInputContentListeners.textInserted(this, index, text.length());
textInputContentListeners.textChanged(this);
if (textValid != previousTextValid) {
textInputListeners.textValidChanged(this);
}
if (selectionStart != previousSelectionStart
|| selectionLength != previousSelectionLength) {
textInputSelectionListeners.selectionChanged(this, selectionStart,
selectionLength);
}
} else {
textInputContentListeners.insertTextVetoed(this, vote);
}
}
}
public void removeText(int index, int count) {
removeText(index, count, true);
}
private void removeText(int index, int count, boolean addToEditHistory) {
if (count > 0) {
Vote vote = textInputContentListeners.previewRemoveText(this, index, count);
if (vote == Vote.APPROVE) {
// Add a remove history item
if (addToEditHistory) {
addHistoryItem(new RemoveTextEdit(index, count));
}
// Remove the text
characters.delete(index, index + count);
// Update the selection
int previousSelectionStart = selectionStart;
int previousSelectionLength = selectionLength;
selectionStart = index;
selectionLength = 0;
// Update the valid flag
boolean previousTextValid = textValid;
textValid = (validator == null) ? true : validator.isValid(getText());
// Fire change events
textInputContentListeners.textRemoved(this, index, count);
textInputContentListeners.textChanged(this);
if (textValid != previousTextValid) {
textInputListeners.textValidChanged(this);
}
if (selectionStart != previousSelectionStart
|| selectionLength != previousSelectionLength) {
textInputSelectionListeners.selectionChanged(this, selectionStart,
selectionLength);
}
} else {
textInputContentListeners.removeTextVetoed(this, vote);
}
}
}
/**
* @return A character sequence representing the text input's content.
*/
public CharSequence getCharacters() {
return characters;
}
/**
* @return The character at a given index.
*
* @param index Location of the character to retrieve.
*/
public char getCharacterAt(int index) {
return characters.charAt(index);
}
/**
* @return The number of characters in the text input.
*/
public int getCharacterCount() {
return characters.length();
}
/**
* Places any selected text on the clipboard and deletes it from the text
* input.
*/
public void cut() {
copy();
removeText(selectionStart, selectionLength);
}
/**
* Places any selected text on the clipboard.
*/
public void copy() {
// Copy selection to clipboard
String selectedText = getSelectedText();
if (selectedText.length() > 0) {
LocalManifest clipboardContent = new LocalManifest();
clipboardContent.putText(selectedText);
Clipboard.setContent(clipboardContent);
}
}
/**
* Inserts text from the clipboard into the text input.
*/
public void paste() {
Manifest clipboardContent = Clipboard.getContent();
if (clipboardContent != null && clipboardContent.containsText()) {
// Paste the string representation of the content
String text = null;
try {
text = clipboardContent.getText();
} catch (IOException exception) {
// No-op
}
if (text != null) {
if ((characters.length() + text.length()) > maximumLength) {
Toolkit.getDefaultToolkit().beep();
} else {
removeText(selectionStart, selectionLength);
insertText(text, selectionStart);
}
}
}
}
public void undo() {
int n = editHistory.getLength();
if (n > 0) {
Edit edit = editHistory.remove(n - 1, 1).get(0);
edit.undo();
}
}
private void addHistoryItem(Edit edit) {
editHistory.add(edit);
if (editHistory.getLength() > MAXIMUM_EDIT_HISTORY_LENGTH) {
editHistory.remove(0, 1);
}
}
/**
* @return The starting index of the selection.
*/
public int getSelectionStart() {
return selectionStart;
}
/**
* @return The length of the selection; may be <tt>0</tt>.
*/
public int getSelectionLength() {
return selectionLength;
}
/**
* Returns a span representing the current selection.
*
* @return A span containing the current selection. Both start and end
* points are inclusive. Returns <tt>null</tt> if the selection length is
* <tt>0</tt>.
*/
public Span getSelection() {
return (selectionLength == 0) ? null : new Span(selectionStart, selectionStart
+ selectionLength - 1);
}
/**
* Sets the selection. The sum of the selection start and length must be
* less than the length of the text input's content.
*
* @param selectionStart The starting index of the selection.
* @param selectionLength The length of the selection.
*/
public void setSelection(int selectionStart, int selectionLength) {
if (selectionLength < 0) {
throw new IllegalArgumentException("selectionLength is negative.");
}
if (selectionStart < 0 || selectionStart + selectionLength > characters.length()) {
throw new IndexOutOfBoundsException();
}
int previousSelectionStart = this.selectionStart;
int previousSelectionLength = this.selectionLength;
if (previousSelectionStart != selectionStart || previousSelectionLength != selectionLength) {
this.selectionStart = selectionStart;
this.selectionLength = selectionLength;
textInputSelectionListeners.selectionChanged(this, previousSelectionStart,
previousSelectionLength);
}
}
/**
* Sets the selection.
*
* @param selection The span (start inclusive to end inclusive).
* @see #setSelection(int, int)
* @throws IllegalArgumentException if the selection span is {@code null}.
*/
public final void setSelection(Span selection) {
if (selection == null) {
throw new IllegalArgumentException("selection is null.");
}
setSelection(Math.min(selection.start, selection.end), (int) selection.getLength());
}
/**
* Selects all text.
*/
public void selectAll() {
setSelection(0, characters.length());
}
/**
* Clears the selection.
*/
public void clearSelection() {
setSelection(0, 0);
}
/**
* Returns the currently selected text.
*
* @return A new string containing a copy of the text in the selected range.
*/
public String getSelectedText() {
return getText(selectionStart, selectionStart + selectionLength);
}
/**
* Returns the text size.
*
* @return The number of characters to display in the text input.
*/
public int getTextSize() {
return textSize;
}
/**
* Sets the text size.
*
* @param textSize The number of characters to display in the text input.
* @throws IllegalArgumentException if the size value is negative.
*/
public void setTextSize(int textSize) {
if (textSize < 0) {
throw new IllegalArgumentException("textSize is negative.");
}
int previousTextSize = this.textSize;
if (previousTextSize != textSize) {
this.textSize = textSize;
textInputListeners.textSizeChanged(this, previousTextSize);
}
}
/**
* Returns the maximum length of the text input's text content.
*
* @return The maximum length of the text input's text content.
*/
public int getMaximumLength() {
return maximumLength;
}
/**
* Sets the maximum length of the text input's text content.
*
* @param maximumLength The maximum length of the text input's text content.
* @throws IllegalArgumentException if the length value is negative.
*/
public void setMaximumLength(int maximumLength) {
if (maximumLength < 0) {
throw new IllegalArgumentException("maximumLength is negative.");
}
int previousMaximumLength = this.maximumLength;
if (previousMaximumLength != maximumLength) {
this.maximumLength = maximumLength;
// Truncate the text, if necessary (do not allow listeners to vote
// on this change)
int length = characters.length();
if (length > maximumLength) {
int count = length - maximumLength;
characters.delete(maximumLength, length);
textInputContentListeners.textRemoved(this, maximumLength, count);
textInputContentListeners.textChanged(this);
}
textInputListeners.maximumLengthChanged(this, previousMaximumLength);
}
}
/**
* Returns the password flag.
*
* @return <tt>true</tt> if this is a password text input; <tt>false</tt>,
* otherwise.
*/
public boolean isPassword() {
return password;
}
/**
* Sets or clears the password flag. If the password flag is set, the text
* input will visually mask its contents.
*
* @param password <tt>true</tt> if this is a password text input;
* <tt>false</tt>, otherwise.
*/
public void setPassword(boolean password) {
if (this.password != password) {
this.password = password;
textInputListeners.passwordChanged(this);
}
}
/**
* @return The text input's prompt.
*/
public String getPrompt() {
return prompt;
}
/**
* Sets the text input's prompt.
*
* @param prompt The prompt text, or <tt>null</tt> for no prompt.
*/
public void setPrompt(String prompt) {
String previousPrompt = this.prompt;
if (previousPrompt != prompt) {
this.prompt = prompt;
textInputListeners.promptChanged(this, previousPrompt);
}
}
/**
* Returns the text input's text key.
*
* @return The text key, or <tt>null</tt> if no text key is set.
*/
public String getTextKey() {
return textKey;
}
/**
* Sets the text input's text key.
*
* @param textKey The text key, or <tt>null</tt> to clear the binding.
*/
public void setTextKey(String textKey) {
String previousTextKey = this.textKey;
if (previousTextKey != textKey) {
this.textKey = textKey;
textInputBindingListeners.textKeyChanged(this, previousTextKey);
}
}
public BindType getTextBindType() {
return textBindType;
}
public void setTextBindType(BindType textBindType) {
if (textBindType == null) {
throw new IllegalArgumentException();
}
BindType previousTextBindType = this.textBindType;
if (previousTextBindType != textBindType) {
this.textBindType = textBindType;
textInputBindingListeners.textBindTypeChanged(this, previousTextBindType);
}
}
public TextBindMapping getTextBindMapping() {
return textBindMapping;
}
public void setTextBindMapping(TextBindMapping textBindMapping) {
TextBindMapping previousTextBindMapping = this.textBindMapping;
if (previousTextBindMapping != textBindMapping) {
this.textBindMapping = textBindMapping;
textInputBindingListeners.textBindMappingChanged(this, previousTextBindMapping);
}
}
@Override
public void load(Object context) {
if (textKey != null && JSON.containsKey(context, textKey) && textBindType != BindType.STORE) {
Object value = JSON.get(context, textKey);
if (textBindMapping == null) {
value = (value == null) ? "" : value.toString();
} else {
value = textBindMapping.toString(value);
}
setText((String) value);
}
}
@Override
public void store(Object context) {
if (textKey != null && textBindType != BindType.LOAD) {
String text = getText();
JSON.put(context, textKey,
(textBindMapping == null) ? text : textBindMapping.valueOf(text));
}
}
@Override
public void clear() {
if (textKey != null) {
setText("");
}
}
public int getInsertionPoint(int x) {
TextInput.Skin textInputSkin = (TextInput.Skin) getSkin();
return textInputSkin.getInsertionPoint(x);
}
public Bounds getCharacterBounds(int index) {
TextInput.Skin textInputSkin = (TextInput.Skin) getSkin();
return textInputSkin.getCharacterBounds(index);
}
/**
* @return The validator associated with this text input.
*/
public Validator getValidator() {
return validator;
}
/**
* Sets the validator associated with this text input.
*
* @param validator The validator to use, or <tt>null</tt> to use no
* validator.
*/
public void setValidator(Validator validator) {
Validator previousValidator = this.validator;
if (validator != previousValidator) {
this.validator = validator;
// Store previous text valid flag
boolean previousTextValid = textValid;
// Update the text valid flag
textValid = (validator == null) ? true : validator.isValid(getText());
textInputListeners.textValidatorChanged(this, previousValidator);
// Fire additional events as needed
if (textValid != previousTextValid) {
textInputListeners.textValidChanged(this);
}
}
}
/**
* @return The text input's strict validation flag.
*/
public boolean isStrictValidation() {
return strictValidation;
}
/**
* Sets the text input's strict validation flag. When enabled, only valid
* text will be accepted by the text input.
*
* @param strictValidation The new flag setting.
*/
public void setStrictValidation(boolean strictValidation) {
if (this.strictValidation != strictValidation) {
this.strictValidation = strictValidation;
textInputListeners.strictValidationChanged(this);
}
}
/**
* Reports whether this text input's text is currently valid as defined by
* its validator.
*
* @return <tt>true</tt> if the text is valid or no validator is installed;
* <tt>false</tt>, otherwise.
*/
public boolean isTextValid() {
return textValid;
}
/**
* @return The text area's editable flag.
*/
public boolean isEditable() {
return editable;
}
/**
* Sets the text area's editable flag.
*
* @param editable The new flag setting.
*/
public void setEditable(boolean editable) {
if (this.editable != editable) {
if (!editable) {
if (isFocused()) {
clearFocus();
}
}
this.editable = editable;
textInputListeners.editableChanged(this);
}
}
/**
* @return The text input listener list.
*/
public ListenerList<TextInputListener> getTextInputListeners() {
return textInputListeners;
}
/**
* @return The text input content listener list.
*/
public ListenerList<TextInputContentListener> getTextInputContentListeners() {
return textInputContentListeners;
}
/**
* @return The text input selection listener list.
*/
public ListenerList<TextInputSelectionListener> getTextInputSelectionListeners() {
return textInputSelectionListeners;
}
/**
* @return The text input binding listener list.
*/
public ListenerList<TextInputBindingListener> getTextInputBindingListeners() {
return textInputBindingListeners;
}
}