I have a JavaFX TextField specialized to accept numbers, including scientific notation. It does pretty much everything I want. But, because it accepts scientific notation, it is easy for a user to enter a number beyond the range that can be represented by a double. When they do, the TextField displays "Infinity" (or "-Infinity"). When that happens the field can no longer be edited to correct the problem. The contents cannot be selected and deleted either. Tapping the "Escape" key does not return to the previous contents.
Here is an SSCCE, based closely on the answer by James_D to this question a few years ago.
import java.text.DecimalFormatSymbols;
import java.util.function.UnaryOperator;
import java.util.regex.Pattern;
import javafx.application.Application;
import javafx.beans.value.ObservableValue;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.TextField;
import javafx.scene.control.TextFormatter;
import javafx.scene.layout.VBox;
import javafx.scene.text.Font;
import javafx.stage.Stage;
import javafx.util.StringConverter;
public class NumericTextFieldDemo extends Application {
char sep = new DecimalFormatSymbols().getDecimalSeparator();
String negStarter = new StringBuilder("-").append(sep).toString();
String posStarter = new StringBuilder("+").append(sep).toString();
String patternStr = new StringBuilder()
.append("[+|-]?(([1-9][0-9]*)|0)?(\\")
.append(sep)
.append("[0-9]*)?(([e|E][+|-]?[0-9]*)?)")
.toString();
Pattern validEditingState = Pattern.compile(patternStr);
class NumericTextField extends TextField {
UnaryOperator<TextFormatter.Change> filter = c -> {
String text = c.getControlNewText();
if (validEditingState.matcher(text).matches()) {
return c;
} else {
return null;
}
};
StringConverter<Double> converter = new StringConverter<Double>() {
#Override
public Double fromString(String s) {
if (s.isEmpty() || "-".equals(s) || "+".equals(s)
|| negStarter.equals(s) || posStarter.equals(s)) {
return 0.0;
} else {
return Double.valueOf(s);
}
}
#Override
public String toString(Double d) {
return d.toString();
}
};
NumericTextField(double initValue) {
TextFormatter<Double> textFormatter = new TextFormatter<>(converter, initValue, filter);
textFormatter.valueProperty().addListener((ObservableValue<? extends Double> obs, Double oldValue, Double newValue) -> {
System.out.println("User entered value: " + newValue);
});
setTextFormatter(textFormatter);
}
NumericTextField() {
this(0.0);
}
}
#Override
public void start(Stage primaryStage) throws Exception {
NumericTextField ntf = new NumericTextField();
// Setting the font seems to be required on macOS.
ntf.setFont(new Font("Arial", 14));
VBox root = new VBox(5, ntf);
root.setAlignment(Pos.CENTER);
primaryStage.setScene(new Scene(root, 250, 150));
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
Is there any way to catch the infinities and leave the TextField in a usable state? Is there some change that could be made to the class to prevent entering such numbers in the first place?
Just use the built-in string converter for doubles:
TextFormatter<Double> tf = new TextFormatter<>(new DoubleStringConverter());
TextField ntf = new TextField();
ntf.setTextFormatter(tf);
Related
Using as a starting point the DecimalField class found on this site, I wrote the following:
import javafx.scene.control.TextField;
import java.util.regex.Pattern;
public class DecimalField extends TextField {
public Boolean rate, positive, integer;
Pattern decimalPattern;
DecimalField(Boolean rate, Boolean positive) {
this.rate = rate;
this.positive = positive;
decimalPattern = Pattern.compile ("[-+]?[0-9]*(\\.[0-9]*)?");
// decimalPattern = Pattern.compile("[-+]?(\\b[0-9]+(\\.[0-9]*)?|\\.[0-9]+)([eE][-+]?[0-9]+\\b)?");
if (rate) {
decimalPattern = Pattern.compile ("[-+]?[0-9]*(\\.[0-9]*)?[%]?");
} else if (positive) {
decimalPattern = Pattern.compile ("[1-9][0-9]*(\\.[0-9]*)?");
}
}
#Override
public void replaceText(int start, int end, String text) {
if (validate (start, text)) {
super.replaceText (start, end, text);
}
}
#Override
public void replaceSelection(String text) {
if (validate (Integer.MAX_VALUE, text)) {
super.replaceSelection (text);
}
}
private boolean validate(int start, String text) {
String currentText = (getText ().isEmpty ()) ? "" : getText ();
if (start == 0) { //to handle "-1.1" or ".1" cases
return decimalPattern.matcher (text + currentText).matches ();
} else {
return decimalPattern.matcher (currentText + text).matches ();
}
}
}
Depending on the parameters sent to the constructor, this class can be used to restrict entries to a standard decimal number, to a positive only (i.e. > 0) decimal number, or to a number followed by the per-cent character.
It seems to work fine (a small test application is provided), but I wanted to also be able to specify a number in scientific notation such as 25.56e-5
I could not write the right regex pattern. A pattern such as "[0-9.eE+-]*" would limit the entry to acceptable characters but would not enforce the number syntax! Suggestions are welcome.
Here is the test program:
import javafx.application.Application;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
public class DecimalFieldTest extends Application {
#Override
public void start(Stage primaryStage) {
// Boolean rate;
// rate = false; positive = true;
Label basicLbl = new Label("Basic Decimal");
DecimalField decimalField = new DecimalField (false, false);
Label rateLbl = new Label("Rate Decimal");
DecimalField rateDecimalField = new DecimalField (true, false);
Label positiveLbl = new Label("Positive Decimal");
DecimalField positiveDecimalField = new DecimalField (false, true);
Button clickMe = new Button ("Click Me");
clickMe.setOnAction (new EventHandler<ActionEvent> () {
#Override
public void handle(ActionEvent event) {
String s;
s = decimalField.getText ();
if (!s.isEmpty ()) getOut(s, false);
s = rateDecimalField.getText ();
if (!s.isEmpty ()) getOut(s, true);
s = positiveDecimalField.getText ();
if (!s.isEmpty ()) getOut(s, false);
}
});
decimalField.setOnAction (new EventHandler<ActionEvent> () {
#Override
public void handle(ActionEvent event) {
getOut(decimalField.getText (),false);
}
});
rateDecimalField.setOnAction (new EventHandler<ActionEvent> () {
#Override
public void handle(ActionEvent event) {
getOut(rateDecimalField.getText (),true);
}
});
positiveDecimalField.setOnAction (new EventHandler<ActionEvent> () {
#Override
public void handle(ActionEvent event) {
getOut(positiveDecimalField.getText (),false);
}
});
VBox root = new VBox (5, basicLbl, decimalField,
rateLbl, rateDecimalField, positiveLbl, positiveDecimalField, clickMe);
root.setAlignment (Pos.CENTER);
Scene scene = new Scene (root, 300, 250);
primaryStage.setScene (scene);
primaryStage.show ();
}
void getOut(String s, Boolean rate) {
// for rate : textField.getText().replaceAll("%","")
String ss = s.replaceAll ("%", "");
double value = Double.parseDouble (ss);
if (rate) {
System.out.println (String.format (ss + " <-> " + value) + "%");
} else {
System.out.println (String.format (ss + " <-> " + value));
}
}
public static void main(String[] args) {
launch (args);
}
}
You can use NumberTextField with BigDecimal and different number formats.
package control;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ObservableValue;
import javafx.event.ActionEvent;
import javafx.geometry.Pos;
import javafx.scene.control.TextField;
import java.math.BigDecimal;
import java.text.NumberFormat;
import java.text.ParseException;
/**
* Number text field
*
* <p>
* Original source code from:
* https://dzone.com/articles/javafx-numbertextfield-and
*/
public class NumberTextField extends TextField {
private final NumberFormat numberFormat;
private ObjectProperty<BigDecimal> value = new SimpleObjectProperty<>();
public NumberTextField(NumberFormat numberFormat) {
this(BigDecimal.ZERO, numberFormat, 100);
}
public NumberTextField(NumberFormat numberFormat, double width) {
this(BigDecimal.ZERO, numberFormat, width);
}
/**
* Number field with properties.
*
* #param value decimal value
* #param numberFormat number format
* #param width min, max and pref width
*/
public NumberTextField(BigDecimal value, NumberFormat numberFormat, double width) {
super();
this.numberFormat = numberFormat;
setMinWidth(width);
setMaxWidth(width);
setPrefWidth(width);
initHandlers();
setValue(value);
setAlignment(Pos.BOTTOM_RIGHT);
}
public final BigDecimal getValue() {
return value.get();
}
public final void setValue(BigDecimal value) {
this.value.set(value);
}
public ObjectProperty<BigDecimal> valueProperty() {
return value;
}
private void initHandlers() {
// try to parse when focus is lost or RETURN is hit
setOnAction((ActionEvent arg0) -> {
parseAndFormatInput();
});
focusedProperty().addListener((ObservableValue<? extends Boolean> observable,
Boolean oldValue, Boolean newValue) -> {
if (!newValue) {
parseAndFormatInput();
}
});
// Set text in field if BigDecimal property is changed from outside.
valueProperty().addListener((ObservableValue<? extends BigDecimal> observable,
BigDecimal oldValue, BigDecimal newValue) -> {
setText(numberFormat.format(newValue));
});
}
/**
* Tries to parse the user input to a number according to the provided
* NumberFormat
*/
private void parseAndFormatInput() {
try {
String input = getText();
if (input == null || input.length() == 0) {
return;
}
Number parsedNumber = numberFormat.parse(input);
BigDecimal newValue = new BigDecimal(parsedNumber.toString());
setValue(newValue);
selectAll();
} catch (ParseException ex) {
// If parsing fails keep old number
setText(numberFormat.format(value.get()));
}
}
}
And as BigDecimalTextField:
NumberTextField bigDecimalField =
new NumberTextField(new DecimalFormat("#,###,###,##0.00"));
I know about the method list.getSelectionModel().getSelectedItem(); and its index version, but heres my problem:
I have setup a List in my GUI which holds Objects of a class Person. In my GUI theres also Text Fields with the attributes of that class (Name,Street,Age etc).
What I did so far is implement a method clickList() which will fill the attribute fields with the data from the selected object in the listview. So the user can edit them from here and press another button which should then update those attributes.
I also setup so you can create a new object from the text form by doing this inside my "OK" Button inside my Controller:
ObservableList<Person> items = FXCollections.observableArrayList();
items.add(new Person(tf_vn.getText(), tf_nn.getText(),tf_strasse.getText(), tf_plz.getText(), tf_ort.getText(),genderChoice, sliderAge.getValue());
list.setItems(items);
However what im struggling with is the editing of the already existing Person. Can someone give me some pointers? I know I can get the selected object index but how do I work with it? Basicly I just need to find a way to do something like selectedObject.setAge(),selectedObject.setName() etc
I looked through all the getSelectionModel() methods but didnt find a solution, im sure there is a easy one....
Thanks in advance !
This way you can reach selectedObject's methods
ListView<Person> l = new ListView<>();
...
l.getSelectionModel().getSelectedItem().setFirstName("new name");
l.refresh();
a full example may look like;
package so;
import javafx.application.Application;
import javafx.beans.property.SimpleStringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.Insets;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.TextField;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.scene.text.Font;
import javafx.stage.Stage;
public class TableViewSample extends Application {
private TableView<Person> table = new TableView<Person>();
private final ObservableList<Person> data = FXCollections.observableArrayList(
new Person("Jacob", "Smith", "jacob.smith#example.com"),
new Person("Isabella", "Johnson", "isabella.johnson#example.com"),
new Person("Ethan", "Williams", "ethan.williams#example.com"),
new Person("Emma", "Jones", "emma.jones#example.com"),
new Person("Michael", "Brown", "michael.brown#example.com"));
final HBox hb = new HBox();
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage stage) {
Scene scene = new Scene(new Group());
stage.setTitle("Table View Sample");
stage.setWidth(450);
stage.setHeight(550);
final Label label = new Label("Address Book");
label.setFont(new Font("Arial", 20));
// table.setEditable(true);
TableColumn<Person, String> firstNameCol = new TableColumn<>("First Name");
firstNameCol.setMinWidth(100);
firstNameCol.setCellValueFactory(new PropertyValueFactory<Person, String>("firstName"));
TableColumn<Person, String> lastNameCol = new TableColumn<>("Last Name");
lastNameCol.setMinWidth(100);
lastNameCol.setCellValueFactory(new PropertyValueFactory<Person, String>("lastName"));
TableColumn<Person, String> emailCol = new TableColumn<>("Email");
emailCol.setMinWidth(200);
emailCol.setCellValueFactory(new PropertyValueFactory<Person, String>("email"));
table.setItems(data);
table.getColumns().addAll(firstNameCol, lastNameCol, emailCol);
final TextField addFirstName = new TextField();
addFirstName.setPromptText("First Name");
addFirstName.setMaxWidth(firstNameCol.getPrefWidth());
final TextField addLastName = new TextField();
addLastName.setMaxWidth(lastNameCol.getPrefWidth());
addLastName.setPromptText("Last Name");
final TextField addEmail = new TextField();
addEmail.setMaxWidth(emailCol.getPrefWidth());
addEmail.setPromptText("Email");
final Button addButton = new Button("Add");
addButton.setOnAction(new EventHandler<ActionEvent>() {
#Override
public void handle(ActionEvent e) {
if (table.getSelectionModel().getSelectedItem() != null) {
table.getSelectionModel().getSelectedItem().setFirstName(addFirstName.getText());
table.getSelectionModel().getSelectedItem().setLastName(addLastName.getText());
table.getSelectionModel().getSelectedItem().setEmail(addEmail.getText());
table.refresh();
} else {
data.add(new Person(addFirstName.getText(), addLastName.getText(), addEmail.getText()));
addFirstName.clear();
addLastName.clear();
addEmail.clear();
}
}
});
table.getSelectionModel().selectedItemProperty().addListener((e, o, n) -> {
// addFirstName.clear();
// addLastName.clear();
// addEmail.clear();
if (n != null) {
addFirstName.setText(n.getFirstName());
addLastName.setText(n.getLastName());
addEmail.setText(n.getEmail());
addButton.setText("Edit");
}
else {
addButton.setText("Add");
}
});
hb.getChildren().addAll(addFirstName, addLastName, addEmail, addButton);
hb.setSpacing(3);
final VBox vbox = new VBox();
vbox.setSpacing(5);
vbox.setPadding(new Insets(10, 0, 0, 10));
vbox.getChildren().addAll(label, table, hb);
((Group) scene.getRoot()).getChildren().addAll(vbox);
stage.setScene(scene);
stage.show();
}
public static class Person {
private final SimpleStringProperty firstName;
private final SimpleStringProperty lastName;
private final SimpleStringProperty email;
private Person(String fName, String lName, String email) {
this.firstName = new SimpleStringProperty(fName);
this.lastName = new SimpleStringProperty(lName);
this.email = new SimpleStringProperty(email);
}
public String getFirstName() {
return firstName.get();
}
public void setFirstName(String fName) {
firstName.set(fName);
}
public String getLastName() {
return lastName.get();
}
public void setLastName(String fName) {
lastName.set(fName);
}
public String getEmail() {
return email.get();
}
public void setEmail(String fName) {
email.set(fName);
}
}
}
I've got an issue with Tableview in JavaFX.
Whenever I add a new row in my Tableview my list is deleted.
I pre-make a list with 4 objects in it
When i use my button to add a row, the row is added.
The thing is when the row is added. If I had text in one of my cell, all is erased.
Here's my main program :
package application;
import vue.*;
import domaine.Reponse;
import javafx.application.Application;
import javafx.collections.ObservableList;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.TableView;
import javafx.stage.Stage;
public class Mainfx extends Application {
public static final String nomApplication = "QCM-Builder";
private TableViewReponse tableauReponse;
private Button addButton;
public static void main(String[] args) {
launch(args);
}
public void start(Stage stage){
Scene scene = new Scene(new Group());
stage.setTitle("Table View Sample");
stage.setWidth(300);
stage.setHeight(500);
tableauReponse = new TableViewReponse();
tableauReponse.setTranslateX(130);
tableauReponse.setTranslateY(300);
tableauReponse.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY);
tableauReponse.setPrefHeight(150);
addButton = new Button("Add");
addButton.setTranslateX(400);
addButton.setTranslateY(400);
((Group) scene.getRoot()).getChildren().addAll(addButton);
addButton.setOnAction(new EventHandler<ActionEvent>() {
#Override
public void handle(ActionEvent e) {
ObservableList<Reponse> list = tableauReponse.getItems();
list.add(new Reponse("",false));
tableauReponse.setItems(list);
}
});
((Group) scene.getRoot()).getChildren().addAll(tableauReponse);
stage.setScene(scene);
stage.show();
}
}
And here's my custom Tableview
package vue;
import java.io.IOException;
import java.sql.SQLException;
import domaine.Reponse;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableColumn.CellDataFeatures;
import javafx.scene.control.TableColumn.CellEditEvent;
import javafx.scene.control.TablePosition;
import javafx.scene.control.TableView;
import javafx.scene.control.cell.CheckBoxTableCell;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.scene.control.cell.TextFieldTableCell;
import javafx.scene.layout.StackPane;
import javafx.util.Callback;
public class TableViewReponse extends TableView<Reponse> {
private TableColumn<Reponse, String> reponseCol;
private TableColumn<Reponse, Boolean> singleCol;
private final ObservableList<Reponse> list =
FXCollections.observableArrayList(
new Reponse("",false),
new Reponse("",false),
new Reponse("",false),
new Reponse("",false)
);
public TableViewReponse() {
super();
this.setEditable(true);
reponseCol = new TableColumn<Reponse, String>("RĂ©ponse");
singleCol = new TableColumn<Reponse, Boolean>("Correcte ?");
// ==== FULL NAME (TEXT FIELD) ===
reponseCol.setCellValueFactory(new PropertyValueFactory<Reponse, String>("reponse"));
reponseCol.setCellFactory(TextFieldTableCell.<Reponse>forTableColumn());
reponseCol.setMinWidth(200);
// On Cell edit commit (for FullName column)
reponseCol.setOnEditCommit((CellEditEvent<Reponse, String> event) -> {
TablePosition<Reponse, String> pos = event.getTablePosition();
int row = pos.getRow();
Reponse reponse = event.getTableView().getItems().get(row);
reponse.setLibelle(event.getNewValue());
try {
System.out.println("Bisous "+ reponse.toStringAMC());
} catch (ClassNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (SQLException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
});
// ==== SINGLE? (CHECK BOX) ===
singleCol.setCellValueFactory(new Callback<CellDataFeatures<Reponse, Boolean>, ObservableValue<Boolean>>() {
#Override
public ObservableValue<Boolean> call(CellDataFeatures<Reponse, Boolean> param) {
Reponse reponse = param.getValue();
SimpleBooleanProperty booleanProp = new SimpleBooleanProperty(reponse.estJuste());
// Note: singleCol.setOnEditCommit(): Not work for
// CheckBoxTableCell.
// When "Single?" column change.
booleanProp.addListener(new ChangeListener<Boolean>() {
#Override
public void changed(ObservableValue<? extends Boolean> observable, Boolean oldValue,
Boolean newValue) {
reponse.setJuste(newValue);
}
});
return booleanProp;
}
});
singleCol.setCellFactory(new Callback<TableColumn<Reponse, Boolean>, TableCell<Reponse, Boolean>>() {
#Override
public TableCell<Reponse, Boolean> call(TableColumn<Reponse, Boolean> p) {
CheckBoxTableCell<Reponse, Boolean> cell = new CheckBoxTableCell<Reponse, Boolean>();
cell.setAlignment(Pos.CENTER);
//cell.commitEdit(true);
return cell;
}
});
this.setItems(list);
this.getColumns().addAll(reponseCol, singleCol);
StackPane root = new StackPane();
root.setPadding(new Insets(5));
}
public void ajouterReponse() {
ObservableList<Reponse> list = this.getItems();
list.add(new Reponse("", false));
this.setItems(list);
}
public String getColReponse(int i) {
return reponseCol.getTableView().getItems().get(i).getLibelle();
}
public void viderColReponse(int i) {
reponseCol.getTableView().getItems().get(i).setLibelle(null);
}
public Boolean getColSingle(int i) {
return this.singleCol.getCellData(i);
}
}
Any ideas why ?
Thanks by advance
As you didn't show your Reponse class i can only assume but i think the problem may come from the mismatch cause by
reponseCol.setCellValueFactory(new PropertyValueFactory<Reponse, String>("reponse"));
and the fact you are doing:
reponseCol.setOnEditCommit((CellEditEvent<Reponse, String> event) -> {
//Some code
reponse.setLibelle(event.getNewValue());
//the rest
});
Assuming you are using the common naming system for setter/getter your Reponse
class shoud have a libelle field that you are updating on commiting the cell.
On the other side you are telling the column to search the value to display in a property named reponse (maybe another field of your class?).
Your are updating a different value than the one your ask to display on automatic refresh, that's why the column is clear when your add a new one.
To fix it you can either do:
reponseCol.setCellValueFactory(new PropertyValueFactory<Reponse, String>("libelle"));
to display the libelle or update the reponse field on commit instead.
SpinnerValueFactory svf = new SpinnerValueFactory.DoubleSpinnerValueFactory(-500,500,1,0.5);
spiKolicina.setValueFactory(svf);
spiKolicina.setEditable(true);
UnaryOperator<TextFormatter.Change> filter = new UnaryOperator<TextFormatter.Change>() {
#Override
public TextFormatter.Change apply(TextFormatter.Change t) {
if (t.isReplaced())
if(t.getText().matches("[^0-9]"))
t.setText(t.getControlText().substring(t.getRangeStart(), t.getRangeEnd()));
if (t.isAdded()) {
if (t.getControlText().contains(".")) {
if (t.getText().matches("[^0-9]")) {
t.setText("");
}
}
else if (t.getText().matches("[^0-9.]")) {
t.setText("");
}
}
return t;
}
};
spiKolicina.getEditor().setTextFormatter(new TextFormatter<>(filter));
How to alter regex expression to allow me to enter negative double number in spinner textfield?
Also when the spinner textfield is empty and you press the up or down btn it gives nullPointException, i would like it to go to some default value
I usually find it easier to check the resulting text from a change; return the change "as-is" if it is ok, and return null otherwise. Here you want the resulting text to be an optional negative sign, zero or more digits, an options decimal separator, and zero or more digits. (Note this allows any valid floating point number, or an empty string, or a negative sign or digital separator on its own.)
So for the filter:
UnaryOperator<TextFormatter.Change> filter = new UnaryOperator<TextFormatter.Change>() {
#Override
public TextFormatter.Change apply(TextFormatter.Change t) {
String newText = t.getControlNewText() ;
if (newText.matches("-?[0-9]*\\.?[0-9]*")) {
return t ;
}
return null ;
}
};
You can also define a converter, which you can use with both the spinner itself and the text field which is the editor for the spinner. This just needs to parse the string as a double, but should handle the special cases of -, ., and -.. So:
StringConverter<Double> converter = new StringConverter<Double>() {
#Override
public String toString(Double object) {
return object.toString() ;
}
#Override
public Double fromString(String string) {
if (string.isEmpty() || ".".equals(string) || "-".equals(string) || "-.".equals(string)) {
return 0.0 ;
} else {
return new Double(string);
}
}
};
then do
svf.setConverter(converter);
spinner.getEditor().setTextFormatter(new TextFormatter<>(converter, 0.0, filter));
This converter will properly handle interpreting empty strings, etc, as 0.0, and so will avoid the exception when you try to increment or decrement when the editor is in that state.
SSCCE:
import java.util.function.UnaryOperator;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Spinner;
import javafx.scene.control.SpinnerValueFactory;
import javafx.scene.control.TextFormatter;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
import javafx.util.StringConverter;
public class SpinnerTest extends Application {
#Override
public void start(Stage primaryStage) {
SpinnerValueFactory<Double> svf = new SpinnerValueFactory.DoubleSpinnerValueFactory(-500,500,1,0.5);
Spinner<Double> spinner = new Spinner<>();
spinner.setValueFactory(svf);
spinner.setEditable(true);
UnaryOperator<TextFormatter.Change> filter = new UnaryOperator<TextFormatter.Change>() {
#Override
public TextFormatter.Change apply(TextFormatter.Change t) {
String newText = t.getControlNewText() ;
if (newText.matches("-?[0-9]*\\.?[0-9]*")) {
return t ;
}
return null ;
}
};
StringConverter<Double> converter = new StringConverter<Double>() {
#Override
public String toString(Double object) {
return object.toString() ;
}
#Override
public Double fromString(String string) {
if (string.isEmpty() || ".".equals(string) || "-".equals(string) || "-.".equals(string)) {
return 0.0 ;
} else {
return new Double(string);
}
}
};
svf.setConverter(converter);
spinner.getEditor().setTextFormatter(new TextFormatter<>(converter, 0.0, filter));
StackPane root = new StackPane(spinner);
primaryStage.setScene(new Scene(root, 180, 80));
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
I'm writing a program that creates a process using youtube-dl. That process has two InputStreams (inputStream and errorStream) of which I want to reroute each into a text area.
I've been trying to get the TextAreas to update without locking the JavaFX thread. It's working but I feel like it's terribly inefficient as it creates a large number of Task objects that only append a line. I've recreated the code I've been using below, using List<String> instead of BufferedReader to simplify the problem a bit.
When I press the button it will create two threads, one for each list, with an UpdateTask. The UpdateTask then creates a WriteTask gives it to Platform.runLater() which places it on the JavaFX thread again.
Surely there must be a better way to do this?
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import javafx.application.Platform;
import javafx.concurrent.Task;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.scene.control.Button;
import javafx.scene.control.TextArea;
import javafx.scene.control.TextField;
import javafx.scene.layout.VBox;
public class ConcurrentTest extends VBox{
TextArea output;
TextArea error;
Button start;
TextField writable;
public ConcurrentTest(){
// Init components
output = new TextArea();
output.setEditable(false);
error = new TextArea();
error.setEditable(false);
// Init button
start = new Button("Print stuff");
start.setOnAction(new EventHandler<ActionEvent>() {
#Override
public void handle(ActionEvent arg0) {
List<String> outputLines = Arrays.asList("A", "B", "C", "D");
List<String> errorLines = Arrays.asList("A", "B", "C", "D");;
Thread outputThread = new Thread(new UpdateTask<String>(output, outputLines));
outputThread.setDaemon(true);
outputThread.start();
Thread errorThread = new Thread(new UpdateTask<String>(error, errorLines));
errorThread.setDaemon(true);
errorThread.start();
}
});
writable = new TextField();
writable.setPromptText("Write while some text areas are getting updated.");
// Add components
this.getChildren().addAll(output, error, start, writable);
}
// UPDATE TASK CLASS
public class UpdateTask<V> extends Task<V>{
TextArea target;
Iterator<String> it;
public UpdateTask(TextArea target, List<String> lines){
this.target = target;
it = lines.iterator();
}
#Override
protected V call() throws Exception {
while(it.hasNext()){
Thread.sleep(1500); // Time to type something to test
Platform.runLater(new WriteTask<String>(target, it.next()));
}
return null;
}
}
// WRITE TASK CLASS
public class WriteTask<V> extends Task<V>{
TextArea target;
String line;
public WriteTask(TextArea target, String line) {
this.target = target;
this.line = line;
}
#Override
protected V call() throws Exception {
target.appendText(line + "\n");
return null;
}
}
}
For the entire program, the launcher with main:
import javafx.application.Application;
import javafx.stage.Stage;
import javafx.scene.Parent;
import javafx.scene.Scene;
public class ConcurrentTestLauncher extends Application {
#Override
public void start(Stage primaryStage) {
try {
Parent root = new ConcurrentTest();
Scene scene = new Scene(root);
primaryStage.setTitle("Concurrent FX Test");
primaryStage.setScene(scene);
primaryStage.show();
} catch(Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
launch(args);
}
}