Would be good to have a way of creating TableViews where the content is always editable (without having to double click on a cell) in an easy way.... and... where it is easy to update other cells in that same row
After a few experiments, we've achieved that, but through a pretty loop the loop sort of thing by extending TableRow awith one InputField (or editable control) for each column and binding/unbinding in itemProperty change event, an abstract CellFactory used by anonymous inner classes.... one teaspoon of grated ginger and a bit salt...
being something that one would think is a quite common use case, I was wondering if there is either an easy way of doing it or if the JavaFX api can add something to make it easier
This is what it has taken us
-------------------------------------------------------------------------------------------------------------------------------------------------
Abstract cell factory....
-------------------------------------------------------------------------------------------------------------------------------------------------
public abstract class ControlTableCellFactory<R extends TableRow<M>, M, T> implements Callback<TableColumn<M, T>, TableCell<M, T>> {
@Override
public TableCell<M, T> call(TableColumn<M, T> column) {
return new ControlTableCell<R, M, T>() {
@Override
public Node getGraphic(R row) {
return ControlTableCellFactory.this.getGraphic(row);
}
};
}
public abstract Node getGraphic(R row);
-------------------------------------------------------------------------------------------------------------------------------------------------
Which gets use like this....
-------------------------------------------------------------------------------------------------------------------------------------------------
taxColumn.setCellFactory(
new ControlTableCellFactory<PurchaseOrderLineTableRow, PurchaseOrderLineModel, BigDecimal>() {
@Override
public Node getGraphic(PurchaseOrderLineTableRow row) {
return row.getTax();
}
});
-------------------------------------------------------------------------------------------------------------------------------------------------
Table row...
-------------------------------------------------------------------------------------------------------------------------------------------------
/*
* Copyright © - 2013 Crane Technical Services Pty Ltd. All rights reserved.
*/
package com.anahata.jobtracking.ui.jfx.purchaseorder;
import com.anahata.jobtracking.domain.model.product.GenericProduct;
import com.anahata.jobtracking.domain.model.product.SpecificProduct;
import com.anahata.jobtracking.domain.model.purchaseorder.PurchaseOrderLineType;
import com.anahata.jobtracking.ui.jfx.product.ProductDataFormatter;
import com.anahata.util.jfx.scene.control.AutoCompleteTextField;
import com.anahata.util.jfx.scene.control.AutoCompleteTextField.DataProvider;
import com.anahata.util.jfx.scene.control.AutoCompleteTextField.Mode;
import com.anahata.util.jfx.scene.control.DisplayableCellFactory;
import com.anahata.util.jfx.scene.control.MoneyField;
import java.math.BigDecimal;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.BooleanBinding;
import javafx.beans.property.Property;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.event.EventHandler;
import javafx.scene.control.ComboBox;
import javafx.scene.control.TableRow;
import javafx.scene.control.TextField;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.util.converter.IntegerStringConverter;
import lombok.Getter;
/**
* TableView row for a Purchase Order line. Contains all editable components.
*
* @author Pablo Rodriguez Pina <pablo@anahata-it.com.au>
*/
public class PurchaseOrderLineTableRow extends TableRow<PurchaseOrderLineModel> {
@Getter
private ComboBox<PurchaseOrderLineType> type = new ComboBox<>();
@Getter
private TextField qty = new TextField();
@Getter
private AutoCompleteTextField product = new AutoCompleteTextField();
@Getter
private MoneyField grossPrice = new MoneyField();
@Getter
private ComboBox<BigDecimal> tax;
private PurchaseOrderController purchaseOrderController;
/**
* Required args constructor.
*
* @param controller the parent controller.
* @param productDataProvider data provider for product lookups.
* @param taxCombo a preconfigured tax combo
*/
public PurchaseOrderLineTableRow(PurchaseOrderController controller, DataProvider productDataProvider,
ComboBox<BigDecimal> taxCombo) {
DisplayableCellFactory.setComboBoxCellFactory(type);
purchaseOrderController = controller;
tax = taxCombo;
product.modeProperty().bind(Bindings.when(type.valueProperty().isEqualTo(PurchaseOrderLineType.PRODUCTS)).then(
AutoCompleteTextField.Mode.TYPED_ONLY).otherwise(Mode.STRING_ONLY));
product.dataProviderProperty().bind(
Bindings.when(type.valueProperty().isEqualTo(PurchaseOrderLineType.PRODUCTS)).then(productDataProvider).otherwise(
(DataProvider)null));
product.setFilterItems(false);
product.setDataFormatter(new ProductDataFormatter(true));
grossPrice.setCurrencyVisible(false);
grossPrice.disableProperty().bind(product.valueProperty().isNull().or(product.valueProperty().isEqualTo("")));
tax.disableProperty().bind(product.valueProperty().isNull());
//Select the row when any of the editable components gains focus
//todo change grossPrice.focusedProperty to controlsFocusedProperty
BooleanBinding focused = product.textFieldFocusedProperty().or(grossPrice.controlsFocusedProperty()).or(
qty.focusedProperty()).or(
tax.focusedProperty());
focused.addListener(new ChangeListener<Boolean>() {
@Override
public void changed(
ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean newValue) {
if (newValue) {
getTableView().getSelectionModel().clearSelection();
getTableView().getSelectionModel().select(getItem());
}
}
});
//Automatic line addition
grossPrice.getAmount().addEventHandler(KeyEvent.KEY_PRESSED, new EventHandler<KeyEvent>() {
@Override
public void handle(KeyEvent event) {
if (event.getCode() == KeyCode.TAB) {
if (!purchaseOrderController.isGstVisible()) {
if (getTableView().getItems().indexOf(getItem()) == getTableView().getItems().size() - 1) {
purchaseOrderController.addLines(1);
}
}
}
}
});
tax.addEventHandler(KeyEvent.KEY_PRESSED, new EventHandler<KeyEvent>() {
@Override
public void handle(KeyEvent event) {
if (event.getCode() == KeyCode.TAB) {
if (getTableView().getItems().indexOf(getItem()) == getTableView().getItems().size() - 1) {
purchaseOrderController.addLines(1);
}
}
}
});
product.valueProperty().addListener(new ChangeListener() {
@Override
public void changed(ObservableValue observable, Object oldValue, Object newValue) {
checkStyle();
}
});
disableProperty().addListener(new ChangeListener<Boolean>() {
@Override
public void changed(
ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean newValue) {
checkStyle();
}
});
super.itemProperty().addListener(new ChangeListener<PurchaseOrderLineModel>() {
@Override
public void changed(ObservableValue<? extends PurchaseOrderLineModel> observable,
PurchaseOrderLineModel oldValue, final PurchaseOrderLineModel newValue) {
if (oldValue != null) {
type.valueProperty().unbindBidirectional((Property)oldValue.typeProperty());
qty.textProperty().unbindBidirectional((Property)oldValue.qtyProperty());
product.valueProperty().unbindBidirectional(oldValue.productProperty());
grossPrice.valueProperty().unbindBidirectional(oldValue.priceProperty());
grossPrice.amountTextProperty().removeListener(oldValue.getChangeListener());
tax.valueProperty().unbindBidirectional(oldValue.taxProperty());
disableProperty().unbind();
}
if (newValue != null) {
disableProperty().bind(newValue.activeProperty().not());
type.getSelectionModel().clearSelection();
type.getItems().clear();
type.getItems().setAll(newValue.getDelegate().getValidLineTypes());
type.valueProperty().bindBidirectional((Property)newValue.typeProperty());
type.getSelectionModel().select(newValue.typeProperty().getValue());
qty.textProperty().bindBidirectional((Property)newValue.qtyProperty(), new IntegerStringConverter());
product.valueProperty().bindBidirectional(newValue.productProperty());
grossPrice.valueProperty().bindBidirectional(newValue.priceProperty());
grossPrice.amountTextProperty().addListener(newValue.getChangeListener());
tax.valueProperty().bindBidirectional(newValue.taxProperty());
if (!newValue.isFocusRequested()) {
Platform.runLater(new Runnable() {
@Override
public void run() {
product.requestFocus();
newValue.setFocusRequested(true);
}
});
}
}
}
});
}
private void checkStyle() {
getStyleClass().remove("noproduct");
getStyleClass().remove("specificproduct");
getStyleClass().remove("genericproduct");
getStyleClass().remove("deleted");
Object productValue = this.product.getValue();
if (isDisabled()) {
getStyleClass().add("deleted");
} else if (productValue == null || productValue instanceof String) {
getStyleClass().add("noproduct");
} else if (productValue instanceof SpecificProduct) {
getStyleClass().add("specificproduct");
} else if (productValue instanceof GenericProduct) {
getStyleClass().add("genericproduct");
}
}
}
After a few experiments, we've achieved that, but through a pretty loop the loop sort of thing by extending TableRow awith one InputField (or editable control) for each column and binding/unbinding in itemProperty change event, an abstract CellFactory used by anonymous inner classes.... one teaspoon of grated ginger and a bit salt...
being something that one would think is a quite common use case, I was wondering if there is either an easy way of doing it or if the JavaFX api can add something to make it easier
This is what it has taken us
-------------------------------------------------------------------------------------------------------------------------------------------------
Abstract cell factory....
-------------------------------------------------------------------------------------------------------------------------------------------------
public abstract class ControlTableCellFactory<R extends TableRow<M>, M, T> implements Callback<TableColumn<M, T>, TableCell<M, T>> {
@Override
public TableCell<M, T> call(TableColumn<M, T> column) {
return new ControlTableCell<R, M, T>() {
@Override
public Node getGraphic(R row) {
return ControlTableCellFactory.this.getGraphic(row);
}
};
}
public abstract Node getGraphic(R row);
-------------------------------------------------------------------------------------------------------------------------------------------------
Which gets use like this....
-------------------------------------------------------------------------------------------------------------------------------------------------
taxColumn.setCellFactory(
new ControlTableCellFactory<PurchaseOrderLineTableRow, PurchaseOrderLineModel, BigDecimal>() {
@Override
public Node getGraphic(PurchaseOrderLineTableRow row) {
return row.getTax();
}
});
-------------------------------------------------------------------------------------------------------------------------------------------------
Table row...
-------------------------------------------------------------------------------------------------------------------------------------------------
/*
* Copyright © - 2013 Crane Technical Services Pty Ltd. All rights reserved.
*/
package com.anahata.jobtracking.ui.jfx.purchaseorder;
import com.anahata.jobtracking.domain.model.product.GenericProduct;
import com.anahata.jobtracking.domain.model.product.SpecificProduct;
import com.anahata.jobtracking.domain.model.purchaseorder.PurchaseOrderLineType;
import com.anahata.jobtracking.ui.jfx.product.ProductDataFormatter;
import com.anahata.util.jfx.scene.control.AutoCompleteTextField;
import com.anahata.util.jfx.scene.control.AutoCompleteTextField.DataProvider;
import com.anahata.util.jfx.scene.control.AutoCompleteTextField.Mode;
import com.anahata.util.jfx.scene.control.DisplayableCellFactory;
import com.anahata.util.jfx.scene.control.MoneyField;
import java.math.BigDecimal;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.BooleanBinding;
import javafx.beans.property.Property;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.event.EventHandler;
import javafx.scene.control.ComboBox;
import javafx.scene.control.TableRow;
import javafx.scene.control.TextField;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.util.converter.IntegerStringConverter;
import lombok.Getter;
/**
* TableView row for a Purchase Order line. Contains all editable components.
*
* @author Pablo Rodriguez Pina <pablo@anahata-it.com.au>
*/
public class PurchaseOrderLineTableRow extends TableRow<PurchaseOrderLineModel> {
@Getter
private ComboBox<PurchaseOrderLineType> type = new ComboBox<>();
@Getter
private TextField qty = new TextField();
@Getter
private AutoCompleteTextField product = new AutoCompleteTextField();
@Getter
private MoneyField grossPrice = new MoneyField();
@Getter
private ComboBox<BigDecimal> tax;
private PurchaseOrderController purchaseOrderController;
/**
* Required args constructor.
*
* @param controller the parent controller.
* @param productDataProvider data provider for product lookups.
* @param taxCombo a preconfigured tax combo
*/
public PurchaseOrderLineTableRow(PurchaseOrderController controller, DataProvider productDataProvider,
ComboBox<BigDecimal> taxCombo) {
DisplayableCellFactory.setComboBoxCellFactory(type);
purchaseOrderController = controller;
tax = taxCombo;
product.modeProperty().bind(Bindings.when(type.valueProperty().isEqualTo(PurchaseOrderLineType.PRODUCTS)).then(
AutoCompleteTextField.Mode.TYPED_ONLY).otherwise(Mode.STRING_ONLY));
product.dataProviderProperty().bind(
Bindings.when(type.valueProperty().isEqualTo(PurchaseOrderLineType.PRODUCTS)).then(productDataProvider).otherwise(
(DataProvider)null));
product.setFilterItems(false);
product.setDataFormatter(new ProductDataFormatter(true));
grossPrice.setCurrencyVisible(false);
grossPrice.disableProperty().bind(product.valueProperty().isNull().or(product.valueProperty().isEqualTo("")));
tax.disableProperty().bind(product.valueProperty().isNull());
//Select the row when any of the editable components gains focus
//todo change grossPrice.focusedProperty to controlsFocusedProperty
BooleanBinding focused = product.textFieldFocusedProperty().or(grossPrice.controlsFocusedProperty()).or(
qty.focusedProperty()).or(
tax.focusedProperty());
focused.addListener(new ChangeListener<Boolean>() {
@Override
public void changed(
ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean newValue) {
if (newValue) {
getTableView().getSelectionModel().clearSelection();
getTableView().getSelectionModel().select(getItem());
}
}
});
//Automatic line addition
grossPrice.getAmount().addEventHandler(KeyEvent.KEY_PRESSED, new EventHandler<KeyEvent>() {
@Override
public void handle(KeyEvent event) {
if (event.getCode() == KeyCode.TAB) {
if (!purchaseOrderController.isGstVisible()) {
if (getTableView().getItems().indexOf(getItem()) == getTableView().getItems().size() - 1) {
purchaseOrderController.addLines(1);
}
}
}
}
});
tax.addEventHandler(KeyEvent.KEY_PRESSED, new EventHandler<KeyEvent>() {
@Override
public void handle(KeyEvent event) {
if (event.getCode() == KeyCode.TAB) {
if (getTableView().getItems().indexOf(getItem()) == getTableView().getItems().size() - 1) {
purchaseOrderController.addLines(1);
}
}
}
});
product.valueProperty().addListener(new ChangeListener() {
@Override
public void changed(ObservableValue observable, Object oldValue, Object newValue) {
checkStyle();
}
});
disableProperty().addListener(new ChangeListener<Boolean>() {
@Override
public void changed(
ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean newValue) {
checkStyle();
}
});
super.itemProperty().addListener(new ChangeListener<PurchaseOrderLineModel>() {
@Override
public void changed(ObservableValue<? extends PurchaseOrderLineModel> observable,
PurchaseOrderLineModel oldValue, final PurchaseOrderLineModel newValue) {
if (oldValue != null) {
type.valueProperty().unbindBidirectional((Property)oldValue.typeProperty());
qty.textProperty().unbindBidirectional((Property)oldValue.qtyProperty());
product.valueProperty().unbindBidirectional(oldValue.productProperty());
grossPrice.valueProperty().unbindBidirectional(oldValue.priceProperty());
grossPrice.amountTextProperty().removeListener(oldValue.getChangeListener());
tax.valueProperty().unbindBidirectional(oldValue.taxProperty());
disableProperty().unbind();
}
if (newValue != null) {
disableProperty().bind(newValue.activeProperty().not());
type.getSelectionModel().clearSelection();
type.getItems().clear();
type.getItems().setAll(newValue.getDelegate().getValidLineTypes());
type.valueProperty().bindBidirectional((Property)newValue.typeProperty());
type.getSelectionModel().select(newValue.typeProperty().getValue());
qty.textProperty().bindBidirectional((Property)newValue.qtyProperty(), new IntegerStringConverter());
product.valueProperty().bindBidirectional(newValue.productProperty());
grossPrice.valueProperty().bindBidirectional(newValue.priceProperty());
grossPrice.amountTextProperty().addListener(newValue.getChangeListener());
tax.valueProperty().bindBidirectional(newValue.taxProperty());
if (!newValue.isFocusRequested()) {
Platform.runLater(new Runnable() {
@Override
public void run() {
product.requestFocus();
newValue.setFocusRequested(true);
}
});
}
}
}
});
}
private void checkStyle() {
getStyleClass().remove("noproduct");
getStyleClass().remove("specificproduct");
getStyleClass().remove("genericproduct");
getStyleClass().remove("deleted");
Object productValue = this.product.getValue();
if (isDisabled()) {
getStyleClass().add("deleted");
} else if (productValue == null || productValue instanceof String) {
getStyleClass().add("noproduct");
} else if (productValue instanceof SpecificProduct) {
getStyleClass().add("specificproduct");
} else if (productValue instanceof GenericProduct) {
getStyleClass().add("genericproduct");
}
}
}