Reputation: 35
I am trying to create an editable TableView in JavaFX, displaying various values stored in a custom class InventoryLocation. Some of these values are strings, while others are various numeric data types (short, int, double), and some of the strings have specific required formats associated with them. I am using something like the following block of code to define each table column, using SortStringConverter() or similar to take the text input and convert it to the target datatype:
TableColumn<InventoryLocation,Short> CabinetColumn = new TableColumn<>("Cabinet");
CabinetColumn.setMinWidth(50);
CabinetColumn.setCellValueFactory(new PropertyValueFactory<>("Cabinet"));
CabinetColumn.setCellFactory(TextFieldTableCell.forTableColumn(new ShortStringConverter()));
However, I would like to prevent the user from entering any invalid data to begin with. For the above example, they should not be able to type any non-numeric characters. Elsewhere in my application, in simple TextFields, I am using something like this to enforce Regex matching on the user input:
quantity.textProperty().addListener(new ChangeListener<String>() {
@Override
public void changed(ObservableValue<? extends String> observable, String oldValue, String newValue) {
if (!newValue.matches("\\d*")) {
quantity.setText(newValue.replaceAll("[^\\d]", ""));
}
}
});
How can I apply something similar to the text entry used by an editable TableView? Currently, the first code block will allow the user to write any value they like, and throws a number format exception if the string cannot be converted to a short. I would like the code to prevent them from entering invalid values in the first place.
Upvotes: 3
Views: 2596
Reputation: 4209
You need to create a custom tablecell - for example:
public class EditableBigDecimalTableCell<T> extends TableCell<T, BigDecimal> {
private TextField textField;
private int minDecimals, maxDecimals;
/**
* This is the default - we will use this as 2 decimal places
*/
public EditableBigDecimalTableCell () {
minDecimals = 2;
maxDecimals = 2;
}
/**
* Used when the cell needs to have a different behavior than 2 decimals
*/
public EditableBigDecimalTableCell (int min, int max) {
minDecimals = min;
maxDecimals = max;
}
@Override
public void startEdit() {
if(editableProperty().get()){
if (!isEmpty()) {
super.startEdit();
createTextField();
setText(null);
setGraphic(textField);
textField.requestFocus();
}
}
}
@Override
public void cancelEdit() {
super.cancelEdit();
setText(getItem() != null ? getItem().toPlainString() : null);
setGraphic(null);
}
@Override
public void updateItem(BigDecimal item, boolean empty) {
super.updateItem(item, empty);
if (empty) {
setText(null);
setGraphic(null);
} else {
if (isEditing()) {
if (textField != null) {
textField.setText(getString());
textField.selectAll();
}
setText(null);
setGraphic(textField);
} else {
setText(getString());
setGraphic(null);
}
}
}
private void createTextField() {
textField = new TextField();
textField.setTextFormatter(new DecimalTextFormatter(minDecimals, maxDecimals));
textField.setText(getString());
textField.setOnAction(evt -> {
if(textField.getText() != null && !textField.getText().isEmpty()){
NumberStringConverter nsc = new NumberStringConverter();
Number n = nsc.fromString(textField.getText());
commitEdit(BigDecimal.valueOf(n.doubleValue()));
}
});
textField.setMinWidth(this.getWidth() - this.getGraphicTextGap() * 2);
textField.setOnKeyPressed((ke) -> {
if (ke.getCode().equals(KeyCode.ESCAPE)) {
cancelEdit();
}
});
textField.setAlignment(Pos.CENTER_RIGHT);
this.setAlignment(Pos.CENTER_RIGHT);
}
private String getString() {
NumberFormat nf = NumberFormat.getNumberInstance();
nf.setMinimumFractionDigits(minDecimals);
nf.setMaximumFractionDigits(maxDecimals);
return getItem() == null ? "" : nf.format(getItem());
}
@Override
public void commitEdit(BigDecimal item) {
if (isEditing()) {
super.commitEdit(item);
} else {
final TableView<T> table = getTableView();
if (table != null) {
TablePosition<T, BigDecimal> position = new TablePosition<T, BigDecimal>(getTableView(),
getTableRow().getIndex(), getTableColumn());
CellEditEvent<T, BigDecimal> editEvent = new CellEditEvent<T, BigDecimal>(table, position,
TableColumn.editCommitEvent(), item);
Event.fireEvent(getTableColumn(), editEvent);
}
updateItem(item, false);
if (table != null) {
table.edit(-1, null);
}
}
}
}
Using a formatter which will prevent the values you don't want.
public class DecimalTextFormatter extends TextFormatter<Number> {
private static DecimalFormat format = new DecimalFormat( "#.0;-#.0" );
public DecimalTextFormatter(int minDecimals, int maxDecimals) {
super(
new StringConverter<Number>() {
@Override
public String toString(Number object) {
if(object == null){
return "";
}
String format = "0.";
for (int i = 0; i < maxDecimals; i++) {
if(i < minDecimals ) {
format = format + "0" ;
}else {
format = format + "#" ;
}
}
format = format + ";-" + format;
DecimalFormat df = new DecimalFormat(format);
String formatted = df.format(object);
return formatted;
}
@Override
public Number fromString(String string){
try {
return format.parse(string);
} catch (ParseException e) {
return null;
}
}
},
0,
new UnaryOperator<TextFormatter.Change>() {
@Override
public TextFormatter.Change apply(TextFormatter.Change change) {
if ( change.getControlNewText().isEmpty() )
{
return change;
}
ParsePosition parsePosition = new ParsePosition( 0 );
Object object = format.parse( change.getControlNewText(), parsePosition );
if(change.getControlNewText().equals("-")){
return change;
}
if(change.getCaretPosition() == 1){
if(change.getControlNewText().equals(".")){
return change;
}
}
if ( object == null || parsePosition.getIndex() < change.getControlNewText().length() )
{
return null;
}
else
{
int decPos = change.getControlNewText().indexOf(".");
if(decPos > 0){
int numberOfDecimals = change.getControlNewText().substring(decPos+1).length();
if(numberOfDecimals > maxDecimals){
return null;
}
}
return change;
}
}
}
);
}
}
Then use that on your column:
numberColumn.setCellFactory(col -> new EditableBigDecimalTableCell<MyDTO>());
Upvotes: 7