احمد ربايعة
احمد ربايعة

Reputation: 55

problem when Filtering Table with custom DatePicker TableCell JavaFX

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

screenshot

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

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

Answers (2)

kleopatra
kleopatra

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:

  • always call super before doing custom stuff
  • whatever is set if there is an item must be reversed if there is none

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:

  • a DatePicker is the input control for editing and is set as the graphic
  • editing is committed when receiving an action from the picker: note that the handler is set once in the constructor
  • updateItem is implemented to configure both input control and text, independent of editing state
  • the showing/hiding is controlled exclusively in the edit lifecycle methods by setting the cell's contentDisplay as needed; note that we back out off startEdit if super didn't switch into editing mode

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

Sai Dandem
Sai Dandem

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

Related Questions