/*
* Copyright 2001-2013 Stephen Colebourne
*
* Licensed 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.joda.beans.ui.swing.component;
import java.awt.Toolkit;
import java.awt.event.FocusEvent;
import java.util.Objects;
import javax.swing.JTextField;
import javax.swing.text.AbstractDocument;
import javax.swing.text.AttributeSet;
import javax.swing.text.BadLocationException;
import javax.swing.text.Document;
import javax.swing.text.DocumentFilter;
import org.joda.beans.ui.swing.SwingUISettings;
/**
* A text field that can validate itself.
* <p>
* This {@code JTextField} has additional functionality that allows it to
* validate entry as you type and on exit.
* Implementations should override one of the {@code process} or {@code validate} methods.
* <p>
* The aim is to provide something simpler than {@code JFormattedTextField}.
*/
public class JValidatedTextField extends JTextField {
/** Serialization version. */
private static final long serialVersionUID = 1L;
/**
* The validator to use, may be null.
*/
private final JTextFieldValidator validator;
/**
* The filter.
*/
private final ValidationDocumentFilter filter;
/**
* Creates an instance.
*/
public JValidatedTextField() {
this(0, null);
}
/**
* Creates an instance.
* <p>
* This uses a {@code PlainDocument} which must not be changed.
*
* @param columns the number of columns to use as the preferred width, zero for natural behavior
*/
public JValidatedTextField(int columns) {
this(columns, null);
}
/**
* Creates an instance.
* <p>
* This uses a {@code PlainDocument} which must not be changed.
*
* @param validator the validator to use, not null
*/
public JValidatedTextField(JTextFieldValidator validator) {
this(0, validator);
}
/**
* Creates an instance.
* <p>
* This uses a {@code PlainDocument} which must not be changed.
*
* @param columns the number of columns to use as the preferred width, zero for natural behavior
* @param validator the validator to use, not null
*/
public JValidatedTextField(int columns, JTextFieldValidator validator) {
super(columns);
this.validator = validator;
this.filter = new ValidationDocumentFilter(this);
AbstractDocument doc = (AbstractDocument) getDocument();
doc.setDocumentFilter(filter);
setUI(new ErrorBackgroundTextUI(getUI()));
}
//-------------------------------------------------------------------------
// override to fix text on exit
@Override
protected void processFocusEvent(FocusEvent e) {
if (e.isTemporary() == false && e.getID() == FocusEvent.FOCUS_LOST) {
String text = Objects.toString(getText(), "");
String fixed = handleExit(text);
if (Objects.equals(text, fixed) == false) {
AbstractDocument doc = (AbstractDocument) getDocument();
doc.setDocumentFilter(null);
setText(fixed);
doc.setDocumentFilter(filter);
}
}
super.processFocusEvent(e);
}
//-------------------------------------------------------------------------
/**
* Gets the error status.
*
* @return the error status, not null
*/
public ErrorStatus getErrorStatus() {
return SwingUtils.getErrorStatus(this);
}
/**
* Sets the error status.
*
* @param errorStatus the error status, not null
*/
public void setErrorStatus(ErrorStatus errorStatus) {
SwingUtils.setErrorStatus(this, errorStatus);
}
//-------------------------------------------------------------------------
/**
* Validates a proposed edit.
* <p>
* This is intended to be used to block entry of certain characters into the field.
* For example, blocking letters in a numeric field.
* <p>
* This is run on the EDT and must be fast and thread-safe.
* Implementations must not access methods on the document.
*
* @param text the whole text of the field to validate, not null
* @return true if the text is a valid value during editing
*/
protected boolean validatedChange(String text) {
return (validator != null ? validator.validateChange(text) : true);
}
/**
* Checks the current status of the text.
* <p>
* This validates that the text is a complete value.
* The input will never be empty during editing but can be empty on exit.
* <p>
* This is run on the EDT and must be fast and thread-safe.
* Implementations must not access methods on the document.
*
* @param text the whole text of the field to validate, not null
* @param onExit true if exiting, false if editing
* @return the error status, not null
*/
protected ErrorStatus validatedStatus(String text, boolean onExit) {
return (validator != null ? validator.checkStatus(text, onExit) : ErrorStatus.VALID);
}
/**
* Handles exit from the field.
* <p>
* This is used to adjust and correct the input value.
* For example, an implementation could change the text to upper case.
* <p>
* There is no support for blocking exit of the field.
* Implementations should either fix the value or accept that it stays invalid.
* This method is invoked whether or not the text is invalid.
* <p>
* This is run on the EDT and must be fast and thread-safe.
* Implementations must not access methods on the document.
*
* @param text the whole text of the field to validate, not null
* @return true if the text is a valid value for exit
*/
protected String validatedExit(String text) {
return (validator != null ? validator.onExit(text, getErrorStatus()) : text);
}
/**
* Handles a change to the text value of the field.
* <p>
* This is used to color the background if the text is invalid.
* Implementations could override this behavior.
* <p>
* This is run on the EDT and must be fast and thread-safe.
* Implementations must not access methods on the document.
* Implementations should override {@link #validatedStatus(String)} not this method.
*
* @param text the whole text of the field to validate, not null
*/
protected void handleChange(String text) {
ErrorStatus status = (text.isEmpty() ? ErrorStatus.VALID : validatedStatus(text, false));
updateErrorStatus(status, false);
}
protected void updateErrorStatus(ErrorStatus status, boolean repaint) {
if (status.equals(getErrorStatus()) == false) {
setErrorStatus(status);
String errorText = (status.isError() ? getErrorText(status) : null);
setToolTipText(errorText);
if (repaint) {
repaint();
}
}
}
/**
* Gets the error text.
*
* @return the error text, empty if valid, not null
*/
private String getErrorText(ErrorStatus status) {
if (status.isError()) {
return SwingUISettings.INSTANCE.lookupResourceUI(status.getErrorKey(), status.getErrorInfo());
}
return "";
}
/**
* Handles exit from the field.
* <p>
* This is used to adjust and correct the input value.
* For example, an implementation could change the text to upper case.
* <p>
* There is no support for blocking exit of the field.
* Implementations should either fix the value or accept that it stays invalid.
* This method is invoked whether or not the text is invalid.
* <p>
* This is run on the EDT and must be fast and thread-safe.
* Implementations must not access methods on the document.
* Implementations should override {@link #validatedExit(String)} not this method.
*
* @param text the whole text of the field to validate, not null
* @return true if the text is a valid value for exit
*/
protected String handleExit(String text) {
ErrorStatus status = validatedStatus(text, true);
updateErrorStatus(status, true);
String resultText = validatedExit(text);
if (Objects.equals(text, resultText) == false) {
status = validatedStatus(resultText, true);
updateErrorStatus(status, true);
}
return resultText;
}
//-------------------------------------------------------------------------
/**
* A filter used to validate text document models during editing.
* <p>
* This {@code DocumentFilter} can be used with a {@code JTextField} to
* validate entry as you type.
*/
static class ValidationDocumentFilter extends DocumentFilter {
/**
* The validator.
*/
private final JValidatedTextField textField;
/**
* Creates an instance.
*
* @param textField the text field to use, not null
*/
ValidationDocumentFilter(JValidatedTextField textField) {
this.textField = Objects.requireNonNull(textField, "textField");
}
//-------------------------------------------------------------------------
@Override
public void insertString(FilterBypass fb, int offset, String inserted, AttributeSet attr) throws BadLocationException {
Document doc = fb.getDocument();
StringBuilder sb = new StringBuilder();
sb.append(doc.getText(0, doc.getLength()));
sb.insert(offset, inserted);
if (textField.validatedChange(sb.toString())) {
fb.insertString(offset, inserted, attr);
textField.handleChange(getText(fb));
} else {
Toolkit.getDefaultToolkit().beep();
}
}
@Override
public void replace(FilterBypass fb, int offset, int length, String text, AttributeSet attrs) throws BadLocationException {
Document doc = fb.getDocument();
StringBuilder sb = new StringBuilder();
sb.append(doc.getText(0, doc.getLength()));
sb.replace(offset, offset + length, text);
if (textField.validatedChange(sb.toString())) {
fb.replace(offset, length, text, attrs);
textField.handleChange(getText(fb));
} else {
Toolkit.getDefaultToolkit().beep();
}
}
@Override
public void remove(FilterBypass fb, int offset, int length) throws BadLocationException {
Document doc = fb.getDocument();
StringBuilder sb = new StringBuilder();
sb.append(doc.getText(0, doc.getLength()));
sb.delete(offset, offset + length);
if (textField.validatedChange(sb.toString())) {
fb.remove(offset, length);
textField.handleChange(getText(fb));
} else {
Toolkit.getDefaultToolkit().beep();
}
}
private String getText(FilterBypass fb) {
try {
return fb.getDocument().getText(0, fb.getDocument().getLength());
} catch (BadLocationException ex) {
return "";
}
}
}
}