Bind CheckBoxTableCell to BooleanBinding - javafx

I want to bind a CheckBox in a TableViewCell to a BooleanBinding. The following sample consists of a TableView with a column name and isEffectiveRequired. The checkbox in the column is bound to the Expression:
isRequired.or(name.isEqualTo("X"))
So an item is "effectivly required" when the item in the row is required OR the name is an X, then the expression should be true.
Unfortunately the CheckBox does not reflect the change. For debugging I added a textfield, showing the nameProperty, requiredProperty and the computed effectiveRequiredProperty.
Interestingly when returning just the isRequiredProperty instead of the binding the checkbox works.
public ObservableBooleanValue effectiveRequiredProperty() {
// Bindings with this work:
// return isRequired;
// with this not
return isRequired.or(name.isEqualTo(SPECIAL_STRING));
}
So what is the difference between a Property and a ObservableValue in regard to a CheckBox?
public class TableCellCBBinding extends Application {
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage primaryStage) throws Exception {
init(primaryStage);
primaryStage.show();
}
private void init(Stage primaryStage) {
primaryStage.setScene(new Scene(buildContent()));
}
private Parent buildContent() {
TableView<ViewModel> tableView = new TableView<>();
tableView.setItems(sampleEntries());
tableView.setEditable(true);
tableView.getColumns().add(buildRequiredColumn());
tableView.getColumns().add(buildNameColumn());
// Add a Textfield to show the values for the first item
// As soon as the name is set to "X", the effectiveRequiredProperty should evaluate to true and the CheckBox should reflect this but it does not
TextField text = new TextField();
ViewModel firstItem = tableView.getItems().get(0);
text.textProperty()
.bind(Bindings.format("%s | %s | %s", firstItem.nameProperty(), firstItem.isRequiredProperty(), firstItem.effectiveRequiredProperty()));
return new HBox(text, tableView);
}
private TableColumn<ViewModel, String> buildNameColumn() {
TableColumn<ViewModel, String> nameColumn = new TableColumn<>("Name");
nameColumn.setCellValueFactory(new PropertyValueFactory<>("name"));
nameColumn.setCellFactory(TextFieldTableCell.forTableColumn());
nameColumn.setEditable(true);
return nameColumn;
}
private TableColumn<ViewModel, Boolean> buildRequiredColumn() {
TableColumn<ViewModel, Boolean> requiredColumn = new TableColumn<>("isEffectiveRequired");
requiredColumn.setMinWidth(50);
// This is should bind my BindingExpression from to ViewModel to the CheckBox
requiredColumn.setCellValueFactory( p -> p.getValue().effectiveRequiredProperty());
requiredColumn.setCellFactory( CheckBoxTableCell.forTableColumn(requiredColumn));
return requiredColumn;
}
private ObservableList<ViewModel> sampleEntries() {
return FXCollections.observableArrayList(
new ViewModel(false, "A"),
new ViewModel(true, "B"),
new ViewModel(false, "C"),
new ViewModel(true, "D"),
new ViewModel(false, "E"));
}
public static class ViewModel {
public static final String SPECIAL_STRING = "X";
private final StringProperty name;
private final BooleanProperty isRequired;
public ViewModel(boolean isRequired, String name) {
this.name = new SimpleStringProperty(this, "name", name);
this.isRequired = new SimpleBooleanProperty(this, "isRequired", isRequired);
this.name.addListener((observable, oldValue, newValue) -> System.out.println(newValue));
}
public StringProperty nameProperty() {return name;}
public final String getName(){return name.get();}
public final void setName(String value){
name.set(value);}
public boolean isRequired() {
return isRequired.get();
}
public BooleanProperty isRequiredProperty() {
return isRequired;
}
public void setRequired(final boolean required) {
this.isRequired.set(required);
}
public ObservableBooleanValue effectiveRequiredProperty() {
// Bindings with this work:
// return isRequired;
// with this not
return isRequired.or(name.isEqualTo(SPECIAL_STRING));
}
}
}
When typing an X into the name the checkbox in the row should be checked.
When typing an X into the name the checkbox in the row is not checked. It's never checked like it is not bound at all.

CheckBoxXXCells don't live up to their doc when it comes to binding their selected state, f.i. (citing here just for signature, even if not set explicitely):
public final Callback <Integer,​ObservableValue<Boolean>> getSelectedStateCallback()
Returns the Callback that is bound to by the CheckBox shown on screen.
clearly talks about an ObservableValue, so we would expect that it at least shows the selection state.
Actually, the implementation does exactly nothing if it's not a property, the relevant part from its updateItem:
StringConverter<T> c = getConverter();
if (showLabel) {
setText(c.toString(item));
}
setGraphic(checkBox);
if (booleanProperty instanceof BooleanProperty) {
checkBox.selectedProperty().unbindBidirectional((BooleanProperty)booleanProperty);
}
ObservableValue<?> obsValue = getSelectedProperty();
if (obsValue instanceof BooleanProperty) {
booleanProperty = (ObservableValue<Boolean>) obsValue;
checkBox.selectedProperty().bindBidirectional((BooleanProperty)booleanProperty);
}
checkBox.disableProperty().bind(Bindings.not(
getTableView().editableProperty().and(
getTableColumn().editableProperty()).and(
editableProperty())
));
To work around, use a custom cell that updates the selected state in its updateItem. With the added quirk that we need to disable the check's firing to really keep the visuals in sync with backing state:
requiredColumn.setCellFactory(cc -> {
TableCell<ViewModel, Boolean> cell = new TableCell<>() {
CheckBox check = new CheckBox() {
#Override
public void fire() {
// do nothing - visualizing read-only property
// could do better, like actually changing the table's
// selection
}
};
{
getStyleClass().add("check-box-table-cell");
check.setOnAction(e -> {
e.consume();
});
}
#Override
protected void updateItem(Boolean item, boolean empty) {
super.updateItem(item, empty);
if (empty || item == null) {
setText(null);
setGraphic(null);
} else {
check.setSelected(item);
setGraphic(check);
}
}
};
return cell;
});

Related

How to make TextFieldTableCell conditional on model property?

I have a TableView with an editable TextFieldTableCell that I want to restrict to be available based on a BooleanProperty of my model object.
For example, textField.disableProperty().bind(item.editableProperty().not())
Currently, I have the basic implementation from the Oracle docs:
colComment.setCellFactory(TextFieldTableCell.forTableColumn());
colComment.setOnEditCommit(event -> event.getTableView().getItems().get(
event.getTablePosition().getRow()).setComment(
event.getNewValue())
);
This obviously does not allow much flexibility. The desire is to check the item's editableProperty and if it is true, display the TextFieldTableCell and bind it to the item's commentProperty.
If that property is false, the cell should simply display the value of the commentProperty.
I have not worked with editable TableViews in the past so I am a bit lost.
I have tried to hack out a workaround with manually setting the graphic myself, but that just does nothing with the cell:
colComment.setCellFactory(cell -> new TableCell<LogEntry, String>() {
final TextField txtComment = new TextField();
#Override
protected void updateItem(String item, boolean empty) {
super.updateItem(item, empty);
if (item == null || empty) {
setGraphic(null);
} else {
LogEntry logEntry = (LogEntry) getTableRow().getItem();
if (logEntry.isEditable()) {
txtComment.textProperty().bindBidirectional(logEntry.commentProperty());
setGraphic(txtComment);
} else {
setText(item);
}
}
}
});
The basic approach is to disallow cell's editing based on a condition. TextFieldTableCell has no direct support for such, but can be extended just as any other type of cell. Options are
override startEdit to do nothing if the condition is not met
bind the cell's editability property to a condition of the rowItem
The most simple is the first (the latter is a bit more involved, due to requiring updates when parent TableRow and its item changes). A quick example (all boiler-plate except the cell ;):
public class TableCellConditionalEditable extends Application {
/**
* Cell with custom condition to prevent editing.
*/
public static class ConditionalEditableCell extends TextFieldTableCell<ConditionalWritable, String> {
public ConditionalEditableCell() {
super(new DefaultStringConverter());
}
/**
* Overridden to do nothing if rowItem-related condition not met.
*/
#Override
public void startEdit() {
if (!isConditionalEditable()) return;
super.startEdit();
}
private boolean isConditionalEditable() {
if (getTableRow() == null || getTableRow().getItem() == null || isEmpty()) return false;
return getTableRow().getItem().writableProperty().get();
}
}
private Parent createContent() {
TableView<ConditionalWritable> table = new TableView<>(ConditionalWritable.conditionalWritables());
TableColumn<ConditionalWritable, String> text = new TableColumn<>("Text");
text.setCellValueFactory(cc -> cc.getValue().textProperty());
TableColumn<ConditionalWritable, Boolean> writable = new TableColumn<>("Writable");
writable.setCellValueFactory(cc -> cc.getValue().writableProperty());
table.getColumns().addAll(text, writable);
table.setEditable(true);
text.setCellFactory(cc -> new ConditionalEditableCell());
BorderPane content = new BorderPane(table);
return content;
}
#Override
public void start(Stage stage) throws Exception {
stage.setScene(new Scene(createContent()));
stage.show();
}
public static void main(String[] args) {
launch(args);
}
public static class ConditionalWritable {
private SimpleStringProperty text;
private SimpleBooleanProperty writable;
public ConditionalWritable(String text, boolean writable) {
this.text = new SimpleStringProperty(text);
this.writable = new SimpleBooleanProperty(writable);
}
public StringProperty textProperty() {
return text;
}
public BooleanProperty writableProperty() {
return writable;
}
public static ObservableList<ConditionalWritable> conditionalWritables() {
return FXCollections.observableArrayList(
new ConditionalWritable("some data", false),
new ConditionalWritable("other data", true),
new ConditionalWritable("nothing important", true)
);
}
}
}

JavaFX TableView Cell color change depending on text value

I have a JavaFX desktop app with a TableView. I populate the data using a POJO named Orders which ultimately comes from a Firebird SQL database.
Image of what I have now
What I am looking to do is change the background fill color of each cell in the first column 'Status' depending on the text value. So if the text value is 'READY' then green, 'STARTED' will be yellow and 'DONE' will be gray.
Image of what I would like
Here is the code portion I use to populate the TableView:
`
#FXML private TableView<Orders> tblOrders;
#FXML private TableColumn<Orders, Integer> clmStatus;
#FXML private TableColumn<Orders, String> clmStartDateTime;
#FXML private TableColumn<Orders, String> clmShopOrder;
#FXML private TableColumn<Orders, String> clmRotation;
#FXML private TableColumn<Orders, String> clmGMIECode;
#FXML private TableColumn<Orders, String> clmSAPCode;
#FXML private TableColumn<Orders, Integer> clmLineName;
#FXML private TableColumn<Orders, Integer> clmOrderProductionNr;
private ObservableList<Orders> list;
public void initialize(URL location, ResourceBundle resources) {
populateTable();
}
private void populateTable() {
log.appLog("Populating table\r\n");
clmStatus.setCellValueFactory(new PropertyValueFactory<>("status"));
clmStartDateTime.setCellValueFactory(new PropertyValueFactory<>
("startDateTime"));
clmShopOrder.setCellValueFactory(new PropertyValueFactory<>("extra1"));
clmRotation.setCellValueFactory(new
PropertyValueFactory<("batchLotNr"));
clmGMIECode.setCellValueFactory(new PropertyValueFactory<>("wareNr"));
clmSAPCode.setCellValueFactory(new PropertyValueFactory<>
("serviceDescription"));
clmLineName.setCellValueFactory(new PropertyValueFactory<>
("productionLineNr"));
clmOrderProductionNr.setCellValueFactory(new PropertyValueFactory<>
("orderProductionNr"));
tblOrders.setItems(list);
}
`
Code sample of my Orders POJO:
`
public class Orders {
private final SimpleStringProperty status;
private final SimpleStringProperty startDateTime;
private final SimpleStringProperty extra1;
private final SimpleStringProperty batchLotNr;
private final SimpleStringProperty wareNr;
private final SimpleStringProperty serviceDescription;
private final SimpleStringProperty productionLineNr;
private final SimpleIntegerProperty orderProductionNr;
Orders(String status, String startDateTime, String extra1, String batchLotNr, String wareNr, String serviceDescription, String productionLineNr, int orderProductionNr) {
this.status = new SimpleStringProperty(status);
this.startDateTime = new SimpleStringProperty(startDateTime);
this.extra1 = new SimpleStringProperty(extra1);
this.batchLotNr = new SimpleStringProperty(batchLotNr);
this.wareNr = new SimpleStringProperty(wareNr);
this.serviceDescription = new SimpleStringProperty(serviceDescription);
this.productionLineNr = new SimpleStringProperty(productionLineNr);
this.orderProductionNr = new SimpleIntegerProperty((orderProductionNr));
}
public String getStatus() {
return status.get();
}
public String getStartDateTime() {return startDateTime.get(); }
public String getExtra1() {
return extra1.get();
}
public String getBatchLotNr() {
return batchLotNr.get();
}
public String getWareNr() {
return wareNr.get();
}
public String getServiceDescription() {
return serviceDescription.get();
}
public String getProductionLineNr() {
return productionLineNr.get();
}
int getOrderProductionNr() {return orderProductionNr.get();}
}
`
I have tried using a callback but I have never used callbacks before and don't properly understand how I can fit my needs into a callback. Any help will be important to my learning. Thanks SO.
You have to define a custom TableCell for your status column like this:
public class ColoredStatusTableCell extends TableCell<TableRow, Status> {
#Override
protected void updateItem(Status item, boolean empty) {
super.updateItem(item, empty);
if (empty || getTableRow() == null) {
setText(null);
setGraphic(null);
} else {
TableRow row = (TableRow) getTableRow().getItem();
setText(item.toString());
setStyle("-fx-background-color: " + row.getColorAsString());
// If the statis is changing dynamic you have to add the following:
row.statusProperty()
.addListener((observable, oldValue, newValue) ->
setStyle("-fx-background-color: " + row.getColorAsString()));
}
}
}
Where TableRow:
public class TableRow {
private ObjectProperty<Status> status;
private Map<Status, Color> statusColor;
public TableRow(Status status, Map<Status, Color> statusColor) {
this.status = new SimpleObjectProperty<>(status);
this.statusColor = statusColor;
}
public Status getStatus() {
return status.get();
}
public ObjectProperty<Status> statusProperty() {
return status;
}
public Color getStatusColor() {
return statusColor.get(status.get());
}
public String getColorAsString() {
return String.format("#%02X%02X%02X",
(int) (getStatusColor().getRed() * 255),
(int) (getStatusColor().getGreen() * 255),
(int) (getStatusColor().getBlue() * 255));
}
}
Status:
public enum Status {
READY, STARTED, DONE
}
and the controller:
public class TestController {
#FXML
private TableView<TableRow> table;
#FXML
private TableColumn<TableRow, Status> column;
private ObservableList<TableRow> data = FXCollections.observableArrayList();
#FXML
public void initialize() {
column.setCellValueFactory(data -> data.getValue().statusProperty());
column.setCellFactory(factory -> new ColoredStatusTableCell());
Map<Status, Color> statusColor = new HashMap<>();
statusColor.put(Status.READY, Color.GREEN);
statusColor.put(Status.STARTED, Color.YELLOW);
statusColor.put(Status.DONE, Color.GRAY);
TableRow ready = new TableRow(Status.READY, statusColor);
TableRow started = new TableRow(Status.STARTED, statusColor);
TableRow done = new TableRow(Status.DONE, statusColor);
data.addAll(ready, started, done);
table.setItems(data);
}
}
I chose to set the status as an enum because it is easier to handle it,
then I have used a map to each status-color combination, then in the cell you can set its background color to the matched color of the status.
If you want of course instead of Color.YELLOW and so on you can use a custom Color.rgb(red,green,blue)
I finally found the solution without having to use any extra classes, just a callback in my controller class with the help of this SO link:
StackOverFlow Link
`
private void populateTable() {
log.appLog("Populating table\r\n");
//clmStatus.setCellValueFactory(new PropertyValueFactory<>("status"));
clmStatus.setCellFactory(new Callback<TableColumn<Orders, String>,
TableCell<Orders, String>>()
{
#Override
public TableCell<Orders, String> call(
TableColumn<Orders, String> param) {
return new TableCell<Orders, String>() {
#Override
protected void updateItem(String item, boolean empty) {
if (!empty) {
int currentIndex = indexProperty()
.getValue() < 0 ? 0
: indexProperty().getValue();
String clmStatus = param
.getTableView().getItems()
.get(currentIndex).getStatus();
if (clmStatus.equals("READY")) {
setTextFill(Color.WHITE);
setStyle("-fx-font-weight: bold");
setStyle("-fx-background-color: green");
setText(clmStatus);
} else if (clmStatus.equals("STARTED")){
setTextFill(Color.BLACK);
setStyle("-fx-font-weight: bold");
setStyle("-fx-background-color: yellow");
setText(clmStatus);
} else if (clmStatus.equals("DONE")){
setTextFill(Color.BLACK);
setStyle("-fx-font-weight: bold");
setStyle("-fx-background-color: gray");
setText(clmStatus);
} else {
setTextFill(Color.WHITE);
setStyle("-fx-font-weight: bold");
setStyle("-fx-background-color: red");
setText(clmStatus);
}
}
}
};
}
});
clmStartDateTime.setCellValueFactory(new PropertyValueFactory<>("startDateTime"));
clmShopOrder.setCellValueFactory(new PropertyValueFactory<>("extra1"));
clmRotation.setCellValueFactory(new PropertyValueFactory<>("batchLotNr"));
clmGMIECode.setCellValueFactory(new PropertyValueFactory<>("wareNr"));
clmSAPCode.setCellValueFactory(new PropertyValueFactory<>("serviceDescription"));
clmLineName.setCellValueFactory(new PropertyValueFactory<>("productionLineNr"));
clmOrderProductionNr.setCellValueFactory(new PropertyValueFactory<>("orderProductionNr"));
tblOrders.setItems(list);
}
`
I don't have badge to comment, but wanted to add some details.
I wanted to format color of cell based on the boolean value which i have in my data set. I have reviewed this question and similar one provided already here:
Stackoverflow link - style based on another cell in row
What was missing in both for me is reseting style when there is no value as kleopatra mentioned.
This works for me:
public class TableCellColored extends TableCell<DimensionDtoFxBean, DimValVoFxBean> {
private static final String DEFAULT_STYLE_CLASS = "table-cell";
public TableCellColored() {
super();
}
#Override
protected void updateItem(DimValVoFxBean item, boolean empty) {
super.updateItem(item, empty);
if (empty || item == null) {
setText("");
resetStyle();
return;
}
setText(Optional.ofNullable(item.getValue()).map(BigDecimal::toString).orElse(""));
Boolean conversionFlag = Optional.ofNullable(item.getConversionFlag()).orElse(true);
updateStyle(conversionFlag);
item.conversionFlagProperty()
.addListener((observable, oldValue, newValue) -> updateStyle(newValue));
}
private void updateStyle(Boolean conversionFlag) {
if (!conversionFlag) {
setStyle("-fx-background-color: red");
} else {
resetStyle();
}
}
private void resetStyle() {
setStyle("");
getStyleClass().addAll(TableCellColored.DEFAULT_STYLE_CLASS);
}
}
Since I have value object with value and boolean flag I can do it i seperate class and don't have add lambda in controller.
Deafult styling of cell is transparent so if we use style to change color, we have to reset it when there is no value.
Since direct styling has bigger priority than class it overrides default styling from css classes.
To be on the safe side I also apply DEFAULT_STYLE_CLASS. Value taken from TableCell class.
Without listener and styles reset I red was staying in table during scrolling. After few scrolls all cells where red. So listener and styles reset is the must have for me.

Can't bind JavaFX TextField to ListView value

I have a JavaFX ListView. When I click on an item in the list, I would like like two...edit controls, a ComboBox and a TextField, to be populated by the appropriate values from the model.
First, my model:
public class Recipient {
private final SimpleStringProperty type = new SimpleStringProperty();
private final SimpleStringProperty address = new SimpleStringProperty();
// property getters
}
In my controller, I have:
public class Controller implements Initializable {
#FXML
private ComboBox type;
#FXML
private TextField address;
#FXML
private ListView<Recipient> recipList;
private final ObservableList<String> types = FXCollections.observableArrayList("SMS", "Email");
private final ObservableList<Recipient> recips = FXCollections.observableArrayList(Recipient.DUMMYDATA);
private final ObjectProperty<Recipient> recipient = new SimpleObjectProperty<>();
#Override
public void initialize(URL url, ResourceBundle rb) {
type.setItems(types);
recipList.setItems(recips);
recipList.setCellFactory((ListView<Recipient> p) -> new ListCell<Recipient>() {
#Override
public void updateItem(Recipient recip, boolean empty) {
super.updateItem(recip, empty);
final int index = p.getItems().indexOf(recip);
if (index > -1) {
setText(String.format("%s - %s", recip.typeProperty().get(), recip.addressProperty().get()));
} else {
setText(null);
}
}
});
recipient.setValue(new Recipient());
recipList.setOnMouseClicked(event -> recipClicked(event));
type.valueProperty().bindBidirectional(recipient.get().typeProperty());
address.textProperty().bindBidirectional(recipient.get().addressProperty());
}
public void recipClicked(MouseEvent event) {
final MultipleSelectionModel<Recipient> get = recipList.selectionModelProperty().get();
final Recipient selectedItem = get.getSelectedItem();
recipient.setValue(selectedItem);
}
}
When I click on the list, the SimpleObjectProperty is updated as expected, but my controls do now show the data. What am I missing?
Your bindings bind to the properties belonging to the current recipient at the time the bindings are made. If the value of recipient changes, then, for example, address.textProperty will still be bound to the addressProperty() of the previous value of recipient, not the new one.
You can use a listener on recipient to bind and unbind the controls:
recipient.addListener((obs, oldRecipient, newRecipient) -> {
if (oldRecipient != null) {
type.valueProperty().unbindBidirectional(oldRecipient.typeProperty());
address.textProperty().unbindBidirectional(oldRecipient.addressProperty());
}
if (newRecipient != null) {
type.valueProperty().bindBidirectional(newRecipient.typeProperty());
address.textProperty().bindBidirectional(newRecipient.addressProperty());
}
});
As an aside, note that you should not use a mouse listener to respond to changes in selection: it will not work, for example, if the user uses the keyboard to change selection in the list view. You can replace recipList.setOnMouseClicked(...) with
recipient.bind(recipList.getSelectionModel().selectedItemProperty());
and remove recipClicked(...) entirely. (In fact, you might not need recipient at all: you can just replace it with recipList.getSelectionModel().selectedItemProperty().)
public class Controller implements Initializable {
#FXML
private ComboBox type;
#FXML
private TextField address;
#FXML
private ListView<Recipient> recipList;
private final ObservableList<String> types = FXCollections.observableArrayList("SMS", "Email");
private final ObservableList<Recipient> recips = FXCollections.observableArrayList(Recipient.DUMMYDATA);
#Override
public void initialize(URL url, ResourceBundle rb) {
type.setItems(types);
recipList.setItems(recips);
recipList.setCellFactory((ListView<Recipient> p) -> new ListCell<Recipient>() {
#Override
public void updateItem(Recipient recip, boolean empty) {
super.updateItem(recip, empty);
if (empty) {
setText(null);
} else {
setText(String.format("%s - %s", recip.typeProperty().get(), recip.addressProperty().get()));
}
}
});
recipList.getSelectionModel().selectedItemProperty().addListener((obs, oldRecipient, newRecipient) -> {
if (oldRecipient != null) {
type.valueProperty().unbindBidirectional(oldRecipient.typeProperty());
address.textProperty().unbindBidirectional(oldRecipient.addressProperty());
}
if (newRecipient != null) {
type.valueProperty().bindBidirectional(newRecipient.typeProperty());
address.textProperty().bindBidirectional(newRecipient.addressProperty());
}
});
}
}

Updating current row when clicked upon in TableView JavaFX

So I have code that works if the user selects a different row than the one currently selected
table.getSelectionModel().selectedItemProperty().addListener(
(observable, oldValue, newValue) -> {
if (newValue == null) {
updateDetails(oldValue);
return;
}
updateDetails(newValue);
});
}
However, I want this to work if the user clicks on the same value as well - basically, there's a part of the code that modifies an image shown but that image doesn't update itself unless I click on another row then go back to the row I was previously on. I would like to be able to update the row I'm on simply by clicking on it (which would call updateDetails) but can't seem to figure this out...
Create a custom rowFactory and add a mouse listener to it.
Example
This displays the old value property of the last item clicked and the new item clicked as text of the Label.
TableView<Item> tv = new TableView<>(FXCollections.observableArrayList(new Item("foo"), new Item("bar"), new Item("42")));
Label label = new Label();
TableColumn<Item, String> valueColumn = new TableColumn<>("value");
valueColumn.setCellValueFactory(d -> d.getValue().valueProperty());
tv.getColumns().add(valueColumn);
EventHandler<MouseEvent> eventHandler = new EventHandler<MouseEvent>() {
private Item lastItem;
#Override
public void handle(MouseEvent event) {
if (event.getButton() == MouseButton.PRIMARY) {
TableRow<Item> source = (TableRow<Item>) event.getSource();
if (!source.isEmpty()) {
label.setText(MessageFormat.format("old: {0}; new: {1}", lastItem == null ? null : lastItem.getValue(), (lastItem = source.getItem()).getValue()));
}
}
}
};
tv.setRowFactory(t -> {
TableRow<Item> row = new TableRow();
row.setOnMouseClicked(eventHandler);
return row;
});
public class Item {
public Item() {
}
public Item(String value) {
this.value.set(value);
}
private final StringProperty value = new SimpleStringProperty();
public String getValue() {
return value.get();
}
public void setValue(String val) {
value.set(val);
}
public StringProperty valueProperty() {
return value;
}
}

Custom Java-fx cellfactory messes up the setCellValueFactory

After messing around with Netbeans and Scenebuilder for a while I'm stuck at a problem I can't quite understand. I use a custom cellfactory to bind a doubleclick event to the cells in my tableview. But when I set the cellfactory and a cellValueFactory only the custom cellFactory has an effect.
I'm trying to populate a tableview with data from a number of objects and bind a double click event to the cells of the first column. Populating is not the problem, I just used
idNumber.setCellValueFactory(new PropertyValueFactory<LiveStock, String>("idNumber"));
status.setCellValueFactory(new PropertyValueFactory<LiveStock, String>("status"));
I then googled around to figure out how to bind a doubleclick event to the cells of the table and found javafx, TableView: detect a doubleclick on a cell
amongst others...
I defined a custom cellFactory like this:
Callback<TableColumn<LiveStock, String>, TableCell<LiveStock, String>> cellFactory =
new Callback<TableColumn<LiveStock, String>, TableCell<LiveStock, String>>() {
#Override
public TableCell call(TableColumn p) {
TableCell cell = new TableCell<LiveStock, String>() {};
cell.addEventFilter(MouseEvent.MOUSE_CLICKED, new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent event) {
if (event.getClickCount() == 2) {
System.out.println("double clicked!");
TableCell c = (TableCell) event.getSource();
System.out.println("Livestock ID: " + c.getId());
}
}
});
return cell;
}
I removed the update and toString methods just to see if they where the reason I ran in to problems.
So I tried
idNumber.setCellFactory(cellFactory);
idNumber.setCellValueFactory(new PropertyValueFactory<LiveStock, String>("idNumber"));
This results in my cells beeing empty, but having the double click binding
any ideas?
My LiveStock class looks like this:
package projekt1.fx;
import javafx.beans.property.SimpleStringProperty;
public class LiveStock {
private final int idNumber;
private final SimpleStringProperty ownerID;
private SimpleStringProperty status;
private double lat;
private double longd;
public LiveStock(int idNumber, String ownerID) {
this.idNumber = idNumber;
this.ownerID = new SimpleStringProperty(ownerID);
this.status = new SimpleStringProperty("ok");
}
public int getIdNumber() {
return this.idNumber;
}
// public void setIdNumber(int number) {
// this.idNumber = number;
// }
public String getOwnerID(){
return ownerID.get();
}
public void setOwnerID(String id){
ownerID.set(id);
}
public String getStatus(){
return status.get();
}
public void setStatus(String st){
status.set(st);
}
}
The cellfactory now looks like this:
Callback<TableColumn<LiveStock, String>, TableCell<LiveStock, String>> cellFactory =
new Callback<TableColumn<LiveStock, String>, TableCell<LiveStock, String>>() {
#Override
public TableCell call(TableColumn p) {
TableCell cell = new TableCell<LiveStock, String>() {
#Override
public void updateItem(String item, boolean empty) {
super.updateItem(item, empty);
// setText("HELLO WORLD!");
setText(empty ? null : getString());
}
};
cell.addEventFilter(MouseEvent.MOUSE_CLICKED, new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent event) {
if (event.getClickCount() == 2) {
System.out.println("double clicked!");
TableCell c = (TableCell) event.getSource();
System.out.println("Livestock ID: " + c.getId());
togglePopup(null);
}
}
});
return cell;
}
};
Documentation of Cell API says:
Because by far the most common use case for cells is to show text to a
user, this use case is specially optimized for within Cell. This is
done by Cell extending from Labeled. This means that subclasses of
Cell need only set the text property, rather than create a separate
Label and set that within the Cell. ...
The current source code of Cell constructor sets the text to null:
public Cell() {
setText(null);
...
}
The subclass IndexedCell and sub-subclass TableCell, both of them don't set the text of Labeled.
The text is set by default cell factory of TableColumn in source code.
public static final Callback<TableColumn<?,?>, TableCell<?,?>> DEFAULT_CELL_FACTORY = new Callback<TableColumn<?,?>, TableCell<?,?>>() {
#Override public TableCell<?,?> call(TableColumn<?,?> param) {
return new TableCell() {
#Override protected void updateItem(Object item, boolean empty) {
if (item == getItem()) return;
super.updateItem(item, empty);
if (item == null) {
super.setText(null);
super.setGraphic(null);
} else if (item instanceof Node) {
super.setText(null);
super.setGraphic((Node)item);
} else {
super.setText(item.toString());
super.setGraphic(null);
}
}
};
}
};
However by defining your own cell factory that creates new TableCell but does not set the text in its overriden updateItem() method, will be resulting an empty (=null) column cell text. So yes the reason of the problem was removing updateItem method, which calls setText(...) internally.
EDIT:
Specify the generic types explicitly for TableColumns as,
TableColumn<LiveStock, Integer> idNumber = new TableColumn<LiveStock, Integer>("ID No");
This will avoid type mismatches or wrong type castings.
Then the cell factory callback for your use case will be
Callback<TableColumn<LiveStock, Integer>, TableCell<LiveStock, Integer>> cellFactory =
new Callback<TableColumn<LiveStock, Integer>, TableCell<LiveStock, Integer>>() {
public TableCell<LiveStock, Integer> call(TableColumn<LiveStock, Integer> p) {
TableCell<LiveStock, Integer> cell = new TableCell<LiveStock, Integer>() {
#Override
public void updateItem(Integer item, boolean empty) {
super.updateItem(item, empty);
setText((item == null || empty) ? null : item.toString());
setGraphic(null);
}
};
cell.addEventFilter(MouseEvent.MOUSE_CLICKED, new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent event) {
if (event.getClickCount() > 1) {
System.out.println("double clicked!");
TableCell c = (TableCell) event.getSource();
System.out.println("Cell text: " + c.getText());
}
}
});
return cell;
}
};
What is changed?
The type of idNumber in LiveStock is int. By defining new TableColumn<LiveStock, Integer> we say this is a column from LiveStock row for its attribute idNumber which has a type int, but the generic types must be a reference type, it cannot be TableCell<LiveStock, int> so we define TableCell<LiveStock, Integer>. The thumb of rule: row item class's attribute type should match the second generic type parameter of TableColumn and due to this the parameter of TableCell also.
getString method is defined in the referenced answer link mentioned by you. But it is just a user defined method, not mandatory to use it.

Resources