Editable TableView in JavaFX, numeric input only
I am trying to create an editable TableView in JavaFX, displaying various values stored in a custom InventoryLocation class. Some of these values are strings, while others are different numeric data types (short, int, double), and some of the strings have certain required formats associated with them. I use something like the following block of code to define each column of a table using SortStringConverter () or similar to take a text input and convert it to the target data type:
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. In the example above, they should not be able to enter any non-numeric characters. Elsewhere in my application, in simple TextFields, I use something like this to enforce Regex matching on 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 the editable TableView? Currently, the first block of code allows the user to write whatever value they like and throws a number exception if the string cannot be converted to short. I would like the code to prevent them from entering invalid values in the first place.
source to share
You need to create a custom table - 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 formatting that will prevent 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 this on your column:
numberColumn.setCellFactory(col -> new EditableBigDecimalTableCell<MyDTO>());
source to share