Search code examples
javajavafxbindingcurrencytreetableview

How to provide Editing of a currency (LongProperty) within a JavaFX editable TreeTableView?


How do I bind a BO currency (LongProperty) to a Javafx editable TreeTableView? Using Databinding, TextFormatter and other javaFX Stuff.

For a normal TextField I found this solution: Editiing Currency with a TextField

Bo:

import java.util.Random;

import javafx.beans.property.LongProperty;
import javafx.beans.property.SimpleLongProperty;

public class SimpleBo {
        //a simple LongProperty to store the currency without fractional digits (56,81 € would be 5681)
        private LongProperty currencyLong = new SimpleLongProperty();
        public SimpleBo() {
            setCurrencyLong(new Random().nextLong());
        }
        public final LongProperty currencyLongProperty() {
            return this.currencyLong;
        }
        public final long getCurrencyLong() {
            return this.currencyLongProperty().get();
        }
        public final void setCurrencyLong(final long currencyLong) {
            this.currencyLongProperty().set(currencyLong);
        }
}

Application:

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.TreeItem;
import javafx.scene.control.TreeTableColumn;
import javafx.scene.control.TreeTableView;
import javafx.scene.control.cell.TextFieldTreeTableCell;
import javafx.scene.control.cell.TreeItemPropertyValueFactory;
import javafx.stage.Stage;

public class BindingExample extends Application {

    public static void main(String[] args) {
        launch(args);
    }

    @Override
    public void start(Stage primaryStage) throws Exception {
        Scene scene = new Scene(createTreeTableView());
        primaryStage.setScene(scene);
        primaryStage.show();
    }

    private static TreeTableView<SimpleBo> createTreeTableView() {
        TreeTableView<SimpleBo> treeTableView = new TreeTableView<>();

        // Create column (Data type of Long).
        TreeTableColumn<SimpleBo, Number> currencyColumn = new TreeTableColumn<>("Currency");

        //Bind Values
        currencyColumn.setCellValueFactory(new TreeItemPropertyValueFactory<>("currencyLong"));

        //Set Cell Factory
        currencyColumn.setCellFactory( param -> new TextFieldTreeTableCell<>());

        // Add columns to TreeTable.
        treeTableView.getColumns().add(currencyColumn);

        SimpleBo firstBo = new SimpleBo();
        SimpleBo secondBo = new SimpleBo();
        SimpleBo thirdBo = new SimpleBo();

        // Root Item
        TreeItem<SimpleBo> itemRoot = new TreeItem<>(null);
        TreeItem<SimpleBo> itemFirst = new TreeItem<>(firstBo);
        TreeItem<SimpleBo> itemSecond = new TreeItem<>(secondBo);
        TreeItem<SimpleBo> itemThird = new TreeItem<>(thirdBo);

        itemRoot.getChildren().addAll(itemFirst, itemSecond, itemThird);

        // Set root Item for Tree
        treeTableView.setRoot(itemRoot);
        treeTableView.setShowRoot(false);
        treeTableView.setEditable(true);

        return treeTableView;
    }
}

The goal should be:

  • Bo with a LongProperty (currency Value in cents)
  • editable TreeTable, in the Users known format (optinal leading minus, thousands separator, decimal separator, currency symbol, and no other chars possible)
  • BiDirectionalBinding between Bo and TreeTableColumn.

Solution

  • A Solution would by a "CurrencyTextFieldTreeTableCell" with a custom Design.

    use the MyNumberStringConverter and Util Class (from this Answer)

    CurrencyTextFieldTreeTableCell:

    import javafx.geometry.Insets;
    import javafx.scene.control.ContentDisplay;
    import javafx.scene.control.Label;
    import javafx.scene.control.TextField;
    import javafx.scene.control.TextFormatter;
    import javafx.scene.control.TreeTableCell;
    import javafx.scene.control.TreeTableColumn;
    import javafx.scene.input.KeyCode;
    import javafx.scene.layout.HBox;
    import javafx.util.Callback;
    
    public class CurrencyTextFieldTreeTableCell extends TreeTableCell<SimpleBo, Number> {
    
        public static Callback<TreeTableColumn<SimpleBo, Number>, TreeTableCell<SimpleBo, Number>> forTreeTableColumn() {
            return forTreeTableColumn(new MyNumberStringConverter());
        }
    
        public static Callback<TreeTableColumn<SimpleBo, Number>, TreeTableCell<SimpleBo, Number>> forTreeTableColumn(
                final MyNumberStringConverter converter) {
            return list -> new CurrencyTextFieldTreeTableCell(converter);
        }
    
        /***************************************************************************
         * * Fields * *
         **************************************************************************/
    
        private HBox hBox = new HBox();
        private Label currencyLabel = new Label("€");
        private TextField textField = new TextField("" + 0l);
    
    
        /***************************************************************************
         * * Constructors * *
         **************************************************************************/
    
        public CurrencyTextFieldTreeTableCell() {
            this(new MyNumberStringConverter());
        }
    
        private CurrencyTextFieldTreeTableCell(MyNumberStringConverter converter) {
            this.getStyleClass().add("currency-text-field-tree-table-cell");
            this.converter = converter;
            setupTextField();
            setupHBox();
            setStyle("-fx-alignment: CENTER-RIGHT;");
        }
    
        /***************************************************************************
         * * Properties * *
         **************************************************************************/
    
        private MyNumberStringConverter converter = new MyNumberStringConverter();
    
        /** {@inheritDoc} */
        @Override
        public void startEdit() {
            if (!isEditable() || !getTreeTableView().isEditable() || !getTableColumn().isEditable()) {
                return;
            }
            super.startEdit();
    
            if (isEditing()) {
                this.setText(null);
    
                if (hBox != null) {
                    this.setGraphic(hBox);
                } else {
                    this.setGraphic(textField);
                }
                if (textField != null) {
                    textField.setText(getItemText());
                    textField.selectAll();
                    // requesting focus so that key input can immediately go into the
                    // TextField (see RT-28132)
                    textField.requestFocus();
                }
            }
        }
    
        /** {@inheritDoc} */
        @Override
        public void cancelEdit() {
            super.cancelEdit();
            this.setText(getItemText());
            this.setGraphic(currencyLabel);
            contentDisplayProperty().setValue(ContentDisplay.RIGHT);
        }
    
        /** {@inheritDoc} */
        @Override
        public void updateItem(Number item, boolean empty) {
            super.updateItem(item, empty);
            if (isEmpty()) {
                setText(null);
                setGraphic(null);
            } else {
                if (isEditing()) {
                    if (textField != null) {
                        textField.setText(getItemText());
                    }
                    setText(null);
                    setGraphic(hBox);
                } else {
                    setText(getItemText());
                    setGraphic(currencyLabel);
                    contentDisplayProperty().setValue(ContentDisplay.RIGHT);
                }
            }
        }
    
        private void setupTextField() {
            TextFormatter<Number> textFormatter = new TextFormatter<>(Util.createFilter());
            this.textField.setTextFormatter(textFormatter);
            // Use onAction here rather than onKeyReleased (with check for Enter),
            // as otherwise we encounter RT-34685
            this.textField.setOnAction(event -> {
                if (converter == null) {
                    throw new IllegalStateException("Attempting to convert text input into Object, but provided "
                            + "StringConverter is null. Be sure to set a StringConverter " + "in your cell factory.");
                }
                commitEdit(converter.fromString(textField.getText()).longValue());
                event.consume();
            });
            this.textField.setOnKeyReleased(t -> {
                if (t.getCode() == KeyCode.ESCAPE) {
                    cancelEdit();
                    t.consume();
                }
            });
        }
    
        private void setupHBox() {
            this.hBox.getChildren().add(this.textField);
            this.hBox.getChildren().add(new Label(" €"));
            this.hBox.setPadding(new Insets(this.hBox.getPadding().getTop() + 9.0D, this.hBox.getPadding().getRight(), this.hBox.getPadding().getBottom(), this.hBox.getPadding().getLeft()));
        }
    
        private String getItemText() {
            if(converter == null) {
                return getItem() == null ? "" : getItem().toString();
            } else {
                return converter.toString(getItem());
            }
        }
    }
    

    And change the setCellFactory to this:

        NumberFormat nFormat = NumberFormat.getInstance();
        nFormat.setMinimumIntegerDigits(1);
        nFormat.setMinimumFractionDigits(2);
        nFormat.setMaximumFractionDigits(2);
        MyNumberStringConverter numberStringConverter = new MyNumberStringConverter(nFormat);
        // Set Cell Factory
        currencyColumn.setCellFactory(
                param -> CurrencyTextFieldTreeTableCell.forTreeTableColumn(numberStringConverter).call(param));