Reputation: 55
i'm trying to make custom table cell with a DatePicker. and having Filtered list in the table that filters the contents depending on radio button selection. when i select a date for a cell everything works fine.
Screenshot 1
but when trying to filter and fire the predicate, the date looks going down to the bottom of the table. and when re-selecting the old radio button, it appears in the correct row.
Screenshot 2
any Ideas?
my controller :
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.collections.transformation.FilteredList;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.RadioButton;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.cell.ComboBoxTableCell;
import javafx.scene.control.cell.TextFieldTableCell;
import javafx.util.Callback;
import javafx.util.converter.IntegerStringConverter;
import java.net.URL;
import java.time.LocalDate;
import java.util.ResourceBundle;
public class Controller implements Initializable {
@FXML
private TableColumn<SetterGetter, String> colStatus;
@FXML
private TableColumn<SetterGetter, Integer> colCode;
@FXML
private TableColumn<SetterGetter, LocalDate> colDueDate;
@FXML
private TableColumn<SetterGetter, String> colName;
@FXML
private RadioButton rdAll;
@FXML
private RadioButton rdDelayed;
@FXML
private RadioButton rdDone;
@FXML
private TableView<SetterGetter> tableTasks;
private RadioButton selectedRadioButton;
ObservableList<SetterGetter> mainTaskList = FXCollections.observableArrayList();
FilteredList<SetterGetter> tableTaskList = new FilteredList<>(mainTaskList, p -> true);
@Override
public void initialize(URL url, ResourceBundle resourceBundle) {
selectedRadioButton = rdAll;
colCode.setOnEditCommit(new EventHandler<TableColumn.CellEditEvent<SetterGetter, Integer>>() {
@Override
public void handle(TableColumn.CellEditEvent<SetterGetter, Integer> event) {
SetterGetter row = event.getRowValue();
row.setId(event.getNewValue());
}
});
colName.setOnEditCommit(new EventHandler<TableColumn.CellEditEvent<SetterGetter, String>>() {
@Override
public void handle(TableColumn.CellEditEvent<SetterGetter, String> event) {
SetterGetter row = event.getRowValue();
row.setName(event.getNewValue());
}
});
colStatus.setOnEditCommit(new EventHandler<TableColumn.CellEditEvent<SetterGetter, String>>() {
@Override
public void handle(TableColumn.CellEditEvent<SetterGetter, String> event) {
SetterGetter row = event.getRowValue();
row.setStatus(event.getNewValue().equals("منجز") ? 0 : 1);
}
});
colCode.setCellValueFactory(b -> new SimpleIntegerProperty(b.getValue().getId()).asObject());
colDueDate.setCellValueFactory(b -> b.getValue().getDate());
colName.setCellValueFactory(b -> new SimpleStringProperty(b.getValue().getName()));
colStatus.setCellValueFactory(b -> new SimpleStringProperty(
b.getValue().getStatus() == 0 ? "منجز"
: "لم ينجز بعد")
);
colCode.setCellFactory(TextFieldTableCell.forTableColumn(new IntegerStringConverter()));
colName.setCellFactory(TextFieldTableCell.forTableColumn());
colStatus.setCellFactory(ComboBoxTableCell.forTableColumn(FXCollections.observableArrayList(
"منجز",
"لم ينجز بعد")
)
);
colDueDate.setCellFactory(new Callback<TableColumn<SetterGetter, LocalDate>, TableCell<SetterGetter, LocalDate>>() {
@Override
public TableCell<SetterGetter, LocalDate> call(TableColumn<SetterGetter, LocalDate> setterGetterStringTableColumn) {
return new DatePickerTableCell();
}
});
mainTaskList.addAll(
new SetterGetter(0, null, null, 1),
new SetterGetter(1, null, null, 1)
);
tableTasks.setItems(tableTaskList);
}
@FXML
void addCheck(ActionEvent event) {
mainTaskList.add(new SetterGetter(0,
null,
null,
0)
);
}
@FXML
void selectRadioButton(ActionEvent event) {
if (event.getSource() != selectedRadioButton) {
RadioButton newRadio = (RadioButton) event.getSource();
newRadio.setStyle("-fx-font-family:arial;-fx-font-size:14;-fx-font-weight:bold;-fx-border-color:red;-fx-border-radius:20;");
selectedRadioButton.setStyle("-fx-font-family:arial;-fx-font-size:14;-fx-font-weight:bold;");
selectedRadioButton = newRadio;
}
firePredicate();
}
private void firePredicate() {
tableTaskList.setPredicate(p -> {
if (selectedRadioButton.equals(rdDone) && p.getStatus() != 0)
return false;
else if (selectedRadioButton.equals(rdDelayed) && p.getStatus() != 1)
return false;
else return true;
});
}
}
DatePickerTableCell class:
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.scene.control.DatePicker;
import javafx.scene.control.TableCell;
import java.time.LocalDate;
public class DatePickerTableCell extends TableCell<SetterGetter, LocalDate> {
private final DatePicker datePicker = new DatePicker();
public DatePickerTableCell() {
super();
}
@Override
public void startEdit() {
super.startEdit();
setGraphic(datePicker);
setText(null);
datePicker.setOnAction(new EventHandler<ActionEvent>() {
@Override
public void handle(ActionEvent actionEvent) {
commitEdit(datePicker.getValue());
}
});
}
@Override
public void commitEdit(LocalDate s) {
super.commitEdit(s);
setText(s.toString());
setGraphic(null);
setItem(s);
}
@Override
public void cancelEdit() {
super.cancelEdit();
setText(datePicker.getValue() == null ? null : datePicker.getValue().toString());
setGraphic(null);
}
}
SetterGetter class:
import javafx.beans.property.ObjectProperty;
import java.time.LocalDate;
public class SetterGetter {
int id;
String name;
ObjectProperty<LocalDate> date;
int status;
public SetterGetter(int id, String name, ObjectProperty<LocalDate> date, int status) {
this.id = id;
this.name = name;
this.date = date;
this.status = status;
}
public int getStatus() {
return status;
}
public void setStatus(int status) {
this.status = status;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public ObjectProperty<LocalDate> getDate() {
return date;
}
public void setDate(ObjectProperty<LocalDate> date) {
this.date = date;
}
}
Main class :
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;
public class Main extends Application {
@Override
public void start(Stage primaryStage) throws Exception{
Parent root = FXMLLoader.load(getClass().getResource("sample.fxml"));
primaryStage.setScene(new Scene(root, 565, 551));
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
fxml file:
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.geometry.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<?import javafx.scene.text.*?>
<AnchorPane maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="551.0" prefWidth="565.0" xmlns="http://javafx.com/javafx/11.0.1" xmlns:fx="http://javafx.com/fxml/1" fx:controller="sample.Controller">
<children>
<TableView fx:id="tableTasks" editable="true" layoutX="16.0" layoutY="163.0" nodeOrientation="LEFT_TO_RIGHT" prefHeight="400.0" prefWidth="554.0" style="-fx-font-family: arial; -fx-font-size: 15;" AnchorPane.bottomAnchor="10.0" AnchorPane.leftAnchor="10.0" AnchorPane.rightAnchor="10.0">
<columns>
<TableColumn fx:id="colDueDate" prefWidth="112" style="-fx-font-family: arial; -fx-font-size: 15;" text="التاريخ" />
<TableColumn fx:id="colStatus" prefWidth="112" style="-fx-font-family: arial; -fx-font-size: 15;" text="الحالة" />
<TableColumn fx:id="colName" prefWidth="112" style="-fx-font-family: arial; -fx-font-size: 15;" text="الأسم" />
<TableColumn fx:id="colCode" prefWidth="112" style="-fx-font-family: arial; -fx-font-size: 15;" text="الكود" />
</columns>
<columnResizePolicy>
<TableView fx:constant="CONSTRAINED_RESIZE_POLICY" />
</columnResizePolicy>
</TableView>
<HBox alignment="CENTER" layoutX="160.0" layoutY="27.0" spacing="15.0">
<children>
<RadioButton fx:id="rdDelayed" mnemonicParsing="false" onAction="#selectRadioButton" style="-fx-font-family: arial; -fx-font-size: 14; -fx-font-weight: bold;" text="لم ينجز بعد">
<padding>
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
</padding>
<toggleGroup>
<ToggleGroup fx:id="radioGroup" />
</toggleGroup>
</RadioButton>
<RadioButton fx:id="rdDone" mnemonicParsing="false" onAction="#selectRadioButton" style="-fx-font-family: arial; -fx-font-size: 14; -fx-font-weight: bold;" text="منجز" toggleGroup="$radioGroup">
<padding>
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
</padding>
</RadioButton>
<RadioButton fx:id="rdAll" mnemonicParsing="false" onAction="#selectRadioButton" selected="true" style="-fx-font-family: arial; -fx-font-size: 14; -fx-font-weight: bold; -fx-border-color: red; -fx-border-radius: 20 20 20 20;" text="الكل" toggleGroup="$radioGroup">
<padding>
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
</padding>
</RadioButton>
</children>
</HBox>
<Button layoutX="473.0" layoutY="31.0" mnemonicParsing="false" onAction="#addCheck" prefHeight="34.0" prefWidth="77.0" style="-fx-font-family: arial; -fx-font-size: 18;" text="إضافة">
<font>
<Font name="Arial" size="17.0" />
</font>
</Button>
</children>
</AnchorPane>
Upvotes: 2
Views: 214
Reputation: 51525
The base problem in the question is the visual artefact when hiding a data row that contains a value in the date column. The reason for that artefact is the missing override of updateItem(S, boolean)
in the custom cell implementation which leads to incorrect cell state when that cell is re-used (as already noted in comments and Sai's answer). The solution is to implement the method.
Hammering in: updateItem is specified in Cell and none of the direct descendants of IndexedCell - that is neither ListCell nor Tree-/TableCell nor TreeCell - override it to do anything useful. Which implies that each and every custom cell implementation extending those has to do it itself. The api doc the updateItem method can be overridden to allow for complete customisation of the cell is not quite correct.
Problem might be: how to override it correctly? Its purpose is to update the visual state of the cell based on the item. There are two basic rules:
Example:
protected void updateItem(T item, boolean empty) {
super.updateItem(item, empty);
if (empty || item == null) {
setText(null);
setGraphic(null);
} else {
setText(item.toString());
setGraphic(getGraphic(item));
}
}
Note: this is slightly different from the example in the api doc in actually setting an item-related graphic (without, resetting the graphic for the empty state doesn't make sense ;)
Next problem might be editing: typically, a cell meant for editing has an input control (f.i. a TextField) only while in editing mode. This implies showing/hiding those when toggling editing state. The responsibility for doing so resides in the edit lifecylce methods (start-/cancel-/commitEdit).
All applied to a custom DatePickerTableCell below:
Caveat: this strict separation of concerns (editing dependent visuals vs item dependent visuals) is possible since fx17 which fixes a bunch of bugs with cell editing state after cell re-use (see the release notes, f.i. JDK-8264127 for ListCell, JDK-8265206 for Tree-/TableCell, JDK-8265210 for TreeCell and related)
An alternative custom DatePickerTableCell:
public static class DatePickerTableCellEx1<S> extends TableCell<S, LocalDate> {
private final DatePicker datePicker = new DatePicker();
public DatePickerTableCellEx1() {
datePicker.setOnAction(new EventHandler<ActionEvent>() {
@Override
public void handle(ActionEvent actionEvent) {
commitEdit(datePicker.getValue());
}
});
setGraphic(datePicker);
setContentDisplay(ContentDisplay.TEXT_ONLY);
}
@Override
protected void updateItem(LocalDate item, boolean empty) {
super.updateItem(item, empty);
if (empty || item == null) {
setText(null);
datePicker.setValue(null);
} else {
setText(datePicker.getConverter().toString(item));
datePicker.setValue(item);
}
}
@Override
public void startEdit() {
super.startEdit();
if (!isEditing()) return;
setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
}
@Override
public void commitEdit(LocalDate s) {
super.commitEdit(s);
setContentDisplay(ContentDisplay.TEXT_ONLY);
}
@Override
public void cancelEdit() {
super.cancelEdit();
setContentDisplay(ContentDisplay.TEXT_ONLY);
}
}
(Mostly unrelated) note on api design around Properties:
Typically, property valued fields should be not null and immutable
// the field
Property<Music> music;
// API
Property<Music> gimmeTheMusic() {
// usage
Property<Music> music = radio.gimmeTheMusic();
assertNotNull(music);
music.addListener(... // turn radio down/up);
// __DO NOT__: it will break the usage
void change(Property<Music> music) {
this.music = music;
}
With that in place, TableColumn's default commit handler will work as expected and there's no need to install a custom commit handler.
Upvotes: 1
Reputation: 9959
There are multiple things missing in your demo. Among that, one is already mentioned by @jewelsea in his comment.
Issue#1:
I noticed that you have not added a commit event for the date column. You need to persist the value(date) into the row object to get correct results when filtering.
Your code in the initialize method will be something like this:
colDueDate.setOnEditCommit(new EventHandler<TableColumn.CellEditEvent<SetterGetter, LocalDate>>() {
@Override
public void handle(TableColumn.CellEditEvent<SetterGetter, LocalDate> event) {
SetterGetter row = event.getRowValue();
if(row.date==null){
row.setDate(new SimpleObjectProperty<>());
}
row.date.set(event.getNewValue());
}
});
Issue#2:
As mentioned by @jewelsea, you need to override the updateItem() method of the TableCell when defining a custom cell. The code in the DatePickerTableCell will be like:
@Override
protected void updateItem(LocalDate item, boolean empty) {
super.updateItem(item, empty);
if (isEditing()) {
setText(null);
setGraphic(datePicker);
} else {
setGraphic(null);
if (item != null) {
setText(item.toString());
} else {
setText(null);
}
}
}
The missing if-else condition in the updateItem is the main cause for your actual issue of the cell value to be in inappropriate place.
A VirtualFlow reuses the cells and can place it any where. It is our duty to ensure that it renders correctly when the updateItem is called. If the if-else condition is missing, you can see cell being used with wrong value.
Issue#3: Not exactly an issue ;)
Use proper naming conventions for the setter/getter of observable properties in the SetterGetter class.
public LocalDate getDate() {
return date.get();
}
public void setDate(LocalDate date) {
this.date.set(date);
}
public ObjectProperty<LocalDate> dateProperty() {
return date;
}
Upvotes: 4