Reputation: 4239
I have the Trade
object class with a
public class Trade {
private DoubleProperty price;
private ReadOnlyBooleanWrapper caution;
public Trade(double price){
this.price = new SimpleDoubleProperty(price);
this.caution = new ReadOnlyBooleanWrapper();
this.caution.bind(this.volume.greaterThan(0));
}
public double getPrice(){
return this.price.get();
}
public DoubleProperty priceProperty(){
return this.price;
}
public void setPrice(double price){
this.price.set(price);
}
}
In my Controller class, I have the following TableView
and TableColumn
Problem is two-fold:
double
. But the EditingDoubleCell code below only return String. How can I make it return double and all the String
s the user typed in will be ignored?Price
column (talking about the same price cell) will change its color to blue when the caution
property is true and to red when the caution
property is false?public class EditingDoubleCell extends TableCell<Trade,String>{
private TextField textField;
public EditingDoubleCell() {
}
@Override
public void startEdit() {
if (!isEmpty()) {
super.startEdit();
createTextField();
setText(null);
setGraphic(textField);
textField.requestFocus();
//textField.selectAll();
}
}
@Override
public void cancelEdit() {
super.cancelEdit();
setText((String) getItem());
setGraphic(null);
}
@Override
public void updateItem(String item, boolean empty) {
super.updateItem(item, empty);
if (empty) {
setText(null);
setGraphic(null);
} else {
if (isEditing()) {
if (textField != null) {
textField.setText(getString());
}
setText(null);
setGraphic(textField);
} else {
setText(getString());
setGraphic(null);
}
}
}
private String getString() {
return getItem() == null ? "" : getItem().toString();
}
private void createTextField(){
Locale locale = new Locale("en", "UK");
String pattern = "###,###.###";
DecimalFormat df = (DecimalFormat) NumberFormat.getNumberInstance(locale);
df.applyPattern(pattern);
//String format = df.format(123456789.123);
//System.out.println(format);
//NumberFormat nf = NumberFormat.getIntegerInstance();
textField = new TextField();
// add filter to allow for typing only integer
textField.setTextFormatter( new TextFormatter<>( c ->
{
if (c.getControlNewText().isEmpty()) {
return c;
}
ParsePosition parsePosition = new ParsePosition( 0 );
Object object = df.parse( c.getControlNewText(), parsePosition );
if ( object == null || parsePosition.getIndex() < c.getControlNewText().length() )
{
return null;
}
else
{
return c;
}
} ) );
textField.setText( getString() );
textField.setMinWidth( this.getWidth() - this.getGraphicTextGap() * 2 );
// commit on Enter
textField.setOnAction( new EventHandler<ActionEvent>()
{
@Override
public void handle( ActionEvent event )
{
commitEdit( textField.getText() );
}
} );
textField.focusedProperty().addListener( new ChangeListener<Boolean>()
{
@Override
public void changed( ObservableValue<? extends Boolean> arg0,
Boolean arg1, Boolean arg2 )
{
if ( !arg2 )
{
commitEdit( textField.getText() );
}
}
} );
}
}
Upvotes: 10
Views: 3355
Reputation: 209684
For the first part of the problem, you should create your TextFormatter
as a TextFormatter<Double>
. This makes the valueProperty
of the TextFormatter
into a Property<Double>
, so you can commit your edits by calling getValue()
on the formatter. You need to specify a StringConverter<Double>
so that it knows how to go from text to a Double
, and vice-versa. So this looks like:
StringConverter<Double> converter = new StringConverter<Double>() {
@Override
public String toString(Double number) {
return df.format(number);
}
@Override
public Double fromString(String string) {
try {
double value = df.parse(string).doubleValue() ;
return value;
} catch (ParseException e) {
e.printStackTrace();
return 0.0 ;
}
}
};
textFormatter = new TextFormatter<>(converter, 0.0, c -> {
if (partialInputPattern.matcher(c.getControlNewText()).matches()) {
return c ;
} else {
return null ;
}
}) ;
I changed the filter here, because your filter was only matching a "complete" input. Since the filter is applied to every individual edit, you must allow "partial" input, such as "100,"
. The filter you had would not allow this (for example). The filter in the version here uses a regular expression: you can tinker with this to get it right but I use
Pattern partialInputPattern = Pattern.compile(""[-+]?[,0-9]*(\\.[0-9]*)?");
which is pretty lenient with what it allows.
Now, instead of committing the edit directly when the user hits enter, just commit the edit when the value of the text formatter changes:
// commit on Enter
textFormatter.valueProperty().addListener((obs, oldValue, newValue) -> {
commitEdit(newValue);
});
The whole cell class now looks like
public static class EditingDoubleCell extends TableCell<Trade,Double>{
private TextField textField;
private TextFormatter<Double> textFormatter ;
private DecimalFormat df ;
public EditingDoubleCell(String...styleClasses) {
Locale locale = new Locale("en", "UK");
String pattern = "###,###.###";
df = (DecimalFormat) NumberFormat.getNumberInstance(locale);
df.applyPattern(pattern);
getStyleClass().addAll(styleClasses);
}
@Override
public void startEdit() {
if (!isEmpty()) {
super.startEdit();
createTextField();
setText(null);
setGraphic(textField);
textField.requestFocus();
}
}
@Override
public void cancelEdit() {
super.cancelEdit();
setText(df.format(getItem()));
setGraphic(null);
}
@Override
public void updateItem(Double item, boolean empty) {
super.updateItem(item, empty);
if (empty) {
setText(null);
setGraphic(null);
} else {
if (isEditing()) {
if (textField != null) {
textField.setText(getString());
}
setText(null);
setGraphic(textField);
} else {
setText(getString());
setGraphic(null);
}
}
}
private String getString() {
return getItem() == null ? "" : df.format(getItem());
}
private void createTextField(){
textField = new TextField();
StringConverter<Double> converter = new StringConverter<Double>() {
@Override
public String toString(Double number) {
return df.format(number);
}
@Override
public Double fromString(String string) {
try {
double value = df.parse(string).doubleValue() ;
return value;
} catch (ParseException e) {
e.printStackTrace();
return 0.0 ;
}
}
};
textFormatter = new TextFormatter<>(converter, 0.0, c ->
{
if (c.getControlNewText().isEmpty()) {
return c;
}
ParsePosition parsePosition = new ParsePosition( 0 );
Object object = df.parse( c.getControlNewText(), parsePosition );
if ( object == null || parsePosition.getIndex() < c.getControlNewText().length() )
{
return null;
}
else
{
return c;
}
} ) ;
// add filter to allow for typing only integer
textField.setTextFormatter( textFormatter);
textField.setText( getString() );
textField.setMinWidth( this.getWidth() - this.getGraphicTextGap() * 2 );
// commit on Enter
textFormatter.valueProperty().addListener((obs, oldValue, newValue) -> {
commitEdit(newValue);
});
}
}
(I added the constructor parameter so it will work with the solution to your second question.)
The second part is answered elsewhere, but I would just create a rowFactory
for your table that sets a CSS pseudoclass based on the state of the caution property:
PseudoClass caution = PseudoClass.getPseudoClass("caution");
table.setRowFactory(tv -> {
TableRow<Trade> row = new TableRow<>();
ChangeListener<Boolean> cautionListener = (obs, wasCaution, isNowCaution) ->
row.pseudoClassStateChanged(caution, isNowCaution);
row.itemProperty().addListener((obs, oldTrade, newTrade) -> {
if (oldTrade != null) {
oldTrade.cautionProperty().removeListener(cautionListener);
}
if (newTrade == null) {
row.pseudoClassStateChanged(caution, false);
} else {
row.pseudoClassStateChanged(caution, newTrade.isCaution());
newTrade.cautionProperty().addListener(cautionListener);
}
});
return row ;
});
Then just set a style class on the cell you want the style to change on (e.g. add the style class "price-cell"
to the EditingDoubleCell
you defined). Then you can just use a CSS stylesheet to change the style as you need, e.g.
.table-row-cell .price-cell {
-fx-text-fill: red ;
}
.table-row-cell:caution .price-cell {
-fx-text-fill: blue ;
}
will make the text red for price cells in rows that do not have caution
set, and make it blue in rows that do.
Here is the complete SSCCE:
import java.text.DecimalFormat;
import java.text.NumberFormat;
import java.text.ParseException;
import java.text.ParsePosition;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Random;
import java.util.function.Function;
import java.util.regex.Pattern;
import javafx.application.Application;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.ReadOnlyBooleanProperty;
import javafx.beans.property.ReadOnlyBooleanWrapper;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.css.PseudoClass;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableRow;
import javafx.scene.control.TableView;
import javafx.scene.control.TextField;
import javafx.scene.control.TextFormatter;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
import javafx.util.StringConverter;
public class TradeTable extends Application {
private final Random rng = new Random();
@Override
public void start(Stage primaryStage) {
TableView<Trade> table = new TableView<>();
table.setEditable(true);
TableColumn<Trade, Integer> volumeCol = column("Volume", trade -> trade.volumeProperty().asObject());
TableColumn<Trade, Double> priceCol = column("Price", trade -> trade.priceProperty().asObject());
priceCol.setCellFactory(col -> new EditingDoubleCell("price-cell"));
table.getColumns().add(volumeCol);
table.getColumns().add(priceCol);
PseudoClass caution = PseudoClass.getPseudoClass("caution");
table.setRowFactory(tv -> {
TableRow<Trade> row = new TableRow<>();
ChangeListener<Boolean> cautionListener = (obs, wasCaution, isNowCaution) ->
row.pseudoClassStateChanged(caution, isNowCaution);
row.itemProperty().addListener((obs, oldTrade, newTrade) -> {
if (oldTrade != null) {
oldTrade.cautionProperty().removeListener(cautionListener);
}
if (newTrade == null) {
row.pseudoClassStateChanged(caution, false);
} else {
row.pseudoClassStateChanged(caution, newTrade.isCaution());
newTrade.cautionProperty().addListener(cautionListener);
}
});
return row ;
});
table.getItems().addAll(createRandomData());
Button button = new Button("Change Data");
button.setOnAction(e -> table.getItems().forEach(trade -> {
if (rng.nextDouble() < 0.5) {
trade.setVolume(0);
} else {
trade.setVolume(rng.nextInt(10000));
}
trade.setPrice(rng.nextDouble() * 1000);
}));
BorderPane.setAlignment(button, Pos.CENTER);
BorderPane.setMargin(button, new Insets(10));
BorderPane root = new BorderPane(table, null, null, button, null);
Scene scene = new Scene(root, 600, 600);
scene.getStylesheets().add("trade-table.css");
primaryStage.setScene(scene);
primaryStage.show();
}
private List<Trade> createRandomData() {
List<Trade> trades = new ArrayList<>(50);
for (int i = 0 ; i < 50; i++) {
int volume = rng.nextDouble() < 0.5 ? 0 : rng.nextInt(10000) ;
double price = rng.nextDouble() * 10000 ;
trades.add(new Trade(price, volume));
}
return trades ;
}
private static <S,T> TableColumn<S,T> column(String title, Function<S, ObservableValue<T>> property) {
TableColumn<S,T> col = new TableColumn<>(title);
col.setCellValueFactory(cellData -> property.apply(cellData.getValue()));
return col ;
}
public static class Trade {
private DoubleProperty price;
private IntegerProperty volume ;
private ReadOnlyBooleanWrapper caution;
public Trade(double price, int volume){
this.price = new SimpleDoubleProperty(price);
this.volume = new SimpleIntegerProperty(volume);
this.caution = new ReadOnlyBooleanWrapper();
this.caution.bind(this.volume.greaterThan(0));
}
public double getPrice(){
return this.price.get();
}
public DoubleProperty priceProperty(){
return this.price;
}
public void setPrice(double price){
this.price.set(price);
}
public final IntegerProperty volumeProperty() {
return this.volume;
}
public final int getVolume() {
return this.volumeProperty().get();
}
public final void setVolume(final int volume) {
this.volumeProperty().set(volume);
}
public final ReadOnlyBooleanProperty cautionProperty() {
return this.caution.getReadOnlyProperty();
}
public final boolean isCaution() {
return this.cautionProperty().get();
}
}
public static class EditingDoubleCell extends TableCell<Trade,Double>{
private TextField textField;
private TextFormatter<Double> textFormatter ;
private Pattern partialInputPattern = Pattern.compile(
"[-+]?[,0-9]*(\\.[0-9]*)?");
private DecimalFormat df ;
public EditingDoubleCell(String...styleClasses) {
Locale locale = new Locale("en", "UK");
String pattern = "###,###.###";
df = (DecimalFormat) NumberFormat.getNumberInstance(locale);
df.applyPattern(pattern);
getStyleClass().addAll(styleClasses);
}
@Override
public void startEdit() {
if (!isEmpty()) {
super.startEdit();
createTextField();
setText(null);
setGraphic(textField);
textField.requestFocus();
}
}
@Override
public void cancelEdit() {
super.cancelEdit();
setText(df.format(getItem()));
setGraphic(null);
}
@Override
public void updateItem(Double item, boolean empty) {
super.updateItem(item, empty);
if (empty) {
setText(null);
setGraphic(null);
} else {
if (isEditing()) {
if (textField != null) {
textField.setText(getString());
}
setText(null);
setGraphic(textField);
} else {
setText(getString());
setGraphic(null);
}
}
}
private String getString() {
return getItem() == null ? "" : df.format(getItem());
}
private void createTextField(){
textField = new TextField();
StringConverter<Double> converter = new StringConverter<Double>() {
@Override
public String toString(Double number) {
return df.format(number);
}
@Override
public Double fromString(String string) {
try {
double value = df.parse(string).doubleValue() ;
return value;
} catch (ParseException e) {
e.printStackTrace();
return 0.0 ;
}
}
};
textFormatter = new TextFormatter<>(converter, 0.0, c -> {
if (partialInputPattern.matcher(c.getControlNewText()).matches()) {
return c ;
} else {
return null ;
}
}) ;
// add filter to allow for typing only integer
textField.setTextFormatter( textFormatter);
textField.setText( getString() );
textField.setMinWidth( this.getWidth() - this.getGraphicTextGap() * 2 );
// commit on Enter
textFormatter.valueProperty().addListener((obs, oldValue, newValue) -> {
commitEdit(newValue);
});
}
}
public static void main(String[] args) {
launch(args);
}
}
With the CSS code above in trade-table.css.
Upvotes: 9
Reputation: 2961
the first part of the question: You can try the following class (It worked for me):
public class EditingDoubleCell extends TableCell<Trade, Double> {
private TextField textField;
public EditingDoubleCell() {
textField = new TextField();
textField.setOnAction(e -> commitEdit(Double.valueOf(textField.getText())));
}
@Override
public void startEdit() {
if (!isEmpty()) {
super.startEdit();
setText(null);
setGraphic(textField);
textField.requestFocus();
}
}
@Override
public void cancelEdit() {
super.cancelEdit();
setText(getString());
setGraphic(null);
}
@Override
public void commitEdit(Double newValue) {
super.commitEdit(newValue);
}
@Override
public void updateItem(Double item, boolean empty) {
super.updateItem(item, empty);
if (empty) {
setText(null);
setGraphic(null);
} else {
Locale locale = new Locale("en", "UK");
String pattern = "###,###.###";
DecimalFormat df = (DecimalFormat) NumberFormat.getNumberInstance(locale);
df.applyPattern(pattern);
String s = df.format(getItem());
setText(s);
setGraphic(null);
// set font of Price cell to a color
TableRow<Trade> row = getTableRow();
if (row.getItem().getCaution()) {
setStyle("-fx-background-color:blue;");
} else {
setStyle("-fx-background-color: red;");
}
}
}
private String getString() {
return getItem() == null ? "" : getItem().toString();
}
}
the second part of the question: Just call setcellfactory(...)
for caution column and you have to override the method updateItem(...)
:
cautionCol.setCellFactory(column -> new TableCell<Trade, Boolean>() {
@Override
protected void updateItem(Boolean item, boolean empty) {
super.updateItem(item, empty);
if (item == null || empty) {
setText(null);
} else {
setText(String.valueOf(item));
//TableRow<Trade> row = getTableRow();
if (item) {
setStyle("-fx-background-color:blue;");
} else {
setStyle("-fx-background-color: red;");
}
}
}
});
Upvotes: 6
Reputation: 21
I had similar problem, I did as follows:
SimpleDoubleProperty price = new SimpleDoubleProperty();
price.setValue(Double.parseDouble(EditingDoubleCell().getString()));
ObservableValue<Double> g = price.asObject();
return g;
This method anticipates that you can parse your String into double. Works for me, tell me if it helped :)
Upvotes: 2