Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

javafx: How can I make TableCell Edit return double instead of string and the font changes color based on a condition?

Tags:

java

javafx

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:

  1. The price property and price column only accepts double. But the EditingDoubleCell code below only return String. How can I make it return double and all the Strings the user typed in will be ignored?
  2. The second function I would like to have is that: the font within the cell of 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() );
                }
            }
        } );

    }
}
like image 314
mynameisJEFF Avatar asked Aug 24 '15 17:08

mynameisJEFF


2 Answers

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.

like image 170
James_D Avatar answered Nov 15 '22 20:11

James_D


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;");
                }

            }
        }

    });
like image 28
Kachna Avatar answered Nov 15 '22 20:11

Kachna