I'm needing different editable controls in a JavaFX TreeTableCell based on an enum attribute value of the row object.
In different cases I need a DatePicker, a TextField, a CheckBox, a ComboBox, or a simple non editable Text field.
I've extended TreeTableCell and overridden the updateItem to handle different cases but that is getting very cumbersome.
Is it possible to create a custom CellFactory Callback to return different subclassed TreeTableCells based on attributes of the row object? How might I go about doing this?
public class MyCellFactory implements Callback<TreeTableColumn<MyField,String>,TreeTableCell<MyField,String>> {
#Override
public TreeTableCell<MyField, String> call(TreeTableColumn<MyField, String> param) {
return new MyCell();
}
}
public class MyCell extends TreeTableCell<MyField, String> {
private TextField textField;
private DatePicker datePicker;
private CheckBox checkBox;
private Text text;
private ComboBox<String> comboBox;
public MyCell() {
super();
}
#Override
public void updateItem(String item, boolean empty) {
super.updateItem(item, empty);
if (empty || getTreeTableRow() == null) {
setText(null);
setGraphic(null);
} else {
MyField myField = (MyField) getTreeTableRow().getItem();
if (isEditing()) {
if (myField.getFieldType().equals(MyFieldType.CheckBox)) {
if (checkBox != null) {
checkBox.setSelected(getBoolean());
}
setText(null);
setGraphic(checkBox);
} else if (myField.getFieldType().equals(MyFieldType.Date)) {
if (datePicker != null) {
datePicker.setValue(getDate());
}
setText(null);
setGraphic(datePicker);
} else {
if (textField != null) {
textField.setText(getString());
}
setText(null);
setGraphic(textField);
}
//...
}
//...
}
I've implemented an SSCCE version of James_D's method but am having trouble understanding how to commit and update changes to the different cells. I'll post the corrected version Once I find a solution
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.CheckBox;
import javafx.scene.control.ComboBox;
import javafx.scene.control.Control;
import javafx.scene.control.TextField;
import javafx.scene.control.TreeItem;
import javafx.scene.control.TreeTableCell;
import javafx.scene.control.TreeTableColumn;
import javafx.scene.control.TreeTableView;
import javafx.scene.control.cell.TreeItemPropertyValueFactory;
import javafx.scene.layout.HBox;
import javafx.stage.Stage;
import javafx.util.Callback;
public class SampleApp extends Application {
public static void main(String[] args) {
Application.launch(args);
}
#SuppressWarnings("unchecked")
#Override
public void start(Stage primaryStage) throws Exception {
TreeItem<MyField> fooFields = new TreeItem<MyField>(new MyField("Foo", "Foo", null, false, null));
TreeItem<MyField> fooText = new TreeItem<MyField>(new MyField("fooText", "fooText", "text", true, null));
TreeItem<MyField> fooCheck = new TreeItem<MyField>(new MyField("fooCheck", "fooCheck", "check", true, null));
List<String> fooCombos = Arrays.asList("foo Combo 1", "foo Combo 2");
TreeItem<MyField> fooCombo = new TreeItem<MyField>(
new MyField("fooCombo", "foo Combo", "combo", true, fooCombos));
fooFields.getChildren().addAll(fooText, fooCheck, fooCombo);
TreeItem<MyField> barFields = new TreeItem<MyField>(new MyField("Bar", "Bar", null, false, null));
TreeItem<MyField> barText = new TreeItem<MyField>(new MyField("barText", "barText", "text", true, null));
TreeItem<MyField> barCheck = new TreeItem<MyField>(new MyField("barCheck", "barCheck", "check", true, null));
List<String> barCombos = Arrays.asList("bar Combo 1", "bar Combo 2");
TreeItem<MyField> barCombo = new TreeItem<MyField>(
new MyField("barCombo", "bar Combo", "combo", true, barCombos));
barFields.getChildren().addAll(barText, barCheck, barCombo);
TreeItem<MyField> hiddenRoot = new TreeItem<MyField>(new MyField("hidden", "hidden", null, false, null));
hiddenRoot.getChildren().addAll(fooFields, barFields);
TreeTableView<MyField> treeTable = new TreeTableView<>(hiddenRoot);
treeTable.setEditable(true);
treeTable.setPrefWidth(400);
treeTable.setShowRoot(false);
TreeTableColumn<MyField, String> nameCol = new TreeTableColumn<MyField, String>("Name");
nameCol.setPrefWidth(150);
nameCol.setCellValueFactory(new TreeItemPropertyValueFactory<MyField, String>("name"));
TreeTableColumn<MyField, String> valueCol = new TreeTableColumn<MyField, String>("Value");
valueCol.setPrefWidth(250);
valueCol.setCellValueFactory(new TreeItemPropertyValueFactory<MyField, String>("value"));
valueCol.setCellFactory(new MyFieldCellFactory());
treeTable.getColumns().addAll(nameCol, valueCol);
HBox root = new HBox(treeTable);
root.setStyle("-fx-padding: 10;" + "-fx-border-style: solid inside;" + "-fx-border-width: 2;"
+ "-fx-border-insets: 5;" + "-fx-border-radius: 5;" + "-fx-border-color: blue;");
Scene scene = new Scene(root);
primaryStage.setScene(scene);
primaryStage.setTitle("Multi Control Tree Table View");
primaryStage.show();
}
public class MyField {
private String name;
private String value;
public String fieldType;
public boolean isEditable;
public List<String> comboVals;
public MyField(String name, String value, String fieldType, boolean isEditable, List<String> comboVals) {
super();
this.name = name;
this.value = value;
this.fieldType = fieldType;
this.isEditable = isEditable;
this.comboVals = comboVals;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
public String getFieldType() {
return fieldType;
}
public void setFieldType(String fieldType) {
this.fieldType = fieldType;
}
public List<String> getComboVals() {
return comboVals;
}
public void setComboVals(List<String> comboVals) {
this.comboVals = comboVals;
}
public boolean isEditable() {
return isEditable;
}
public void setEditable(boolean isEditable) {
this.isEditable = isEditable;
}
}
public class MyFieldCellFactory
implements Callback<TreeTableColumn<MyField, String>, TreeTableCell<MyField, String>> {
#Override
public TreeTableCell<MyField, String> call(TreeTableColumn<MyField, String> param) {
return new MyFieldCell();
}
}
public class MyFieldCell extends TreeTableCell<MyField, String> {
private MyEditingControlProvider controlProvider = new MyCellEditingControlProvider();
public MyFieldCell() {
super();
}
#Override
protected void updateItem(String item, boolean empty) {
super.updateItem(item, empty);
if (empty) {
setText(null);
setGraphic(null);
} else {
MyField myField = getTreeTableRow().getItem();
setText(null);
setGraphic(controlProvider.getControl(myField));
}
}
protected void commitEdit() {
super.commitEdit(getItem());
MyField myField = getTreeTableRow().getItem();
controlProvider.updateFromControl(myField);
}
}
public interface MyEditingControlProvider {
public Control getControl(MyField field);
public void updateFromControl(MyField field);
}
public class MyCellEditingControlProvider implements MyEditingControlProvider {
private Map<String, MyEditingControlProvider> providers;
public MyCellEditingControlProvider() {
providers = new HashMap<>();
providers.put("check", new CheckProvider());
providers.put("combo", new ComboProvider());
providers.put("text", new TextProvider());
}
#Override
public Control getControl(MyField field) {
if (field == null || field.getFieldType() == null) {
return null;
} else {
return providers.get(field.getFieldType()).getControl(field);
}
}
#Override
public void updateFromControl(MyField field) {
providers.get(field.getFieldType()).updateFromControl(field);
}
}
public class CheckProvider implements MyEditingControlProvider {
private CheckBox checkBox;
#Override
public Control getControl(MyField field) {
if (checkBox == null) {
createCheckBox(field);
}
return checkBox;
}
private void createCheckBox(MyField field) {
checkBox = new CheckBox("Check");
checkBox.setSelected(getBoolean(field));
}
private Boolean getBoolean(MyField field) {
return field.getValue() == null ? false : convertYNToBoolean(field.getValue());
}
private Boolean convertYNToBoolean(String val) {
if (val != null && val.equals("Y")) {
return true;
} else {
return false;
}
}
private String convertBooleanToYN(Boolean val) {
if (val) {
return "Y";
} else {
return "N";
}
}
#Override
public void updateFromControl(MyField field) {
field.setValue(convertBooleanToYN(checkBox.isSelected()));
}
}
public class ComboProvider implements MyEditingControlProvider {
private ComboBox<String> comboBox;
#Override
public Control getControl(MyField field) {
if (comboBox == null) {
createComboBox(field);
}
return comboBox;
}
private void createComboBox(MyField field) {
comboBox = new ComboBox<String>();
comboBox.setEditable(true);
resetBox(field);
}
private void resetBox(MyField field) {
comboBox.getItems().clear();
comboBox.getItems().addAll(field.getComboVals());
}
#Override
public void updateFromControl(MyField field) {
field.setValue(comboBox.getValue());
}
}
public class TextProvider implements MyEditingControlProvider {
private TextField textField;
#Override
public Control getControl(MyField field) {
if (textField == null) {
createTextField(field);
}
return textField;
}
private void createTextField(MyField field) {
textField = new TextField(field.getValue());
}
#Override
public void updateFromControl(MyField field) {
field.setValue(textField.getText());
}
}
}
The cell that is returned by the cell factory will be reused by the TreeTableView as the user expands and collapses items in the tree, or scrolls through the data, etc. So the cell you return must be able to handle all cases, and you cannot return a cell instance that only handles specific rows.
If you want to refactor, you have to refactor the code in the updateItem(...) method, which you can do to any degree of modularity you want. A (perhaps extreme) example might be:
public interface EditingControlProvider {
public Control getControl(MyField myField);
}
with some specific implementations:
public class DatePickerProvider implements EditingControlProvider {
private DatePicker datePicker ;
#Override
public Control getControl(MyField myField) {
if (datePicker == null) {
datePicker = new DatePicker();
}
datePicker.setValue(myField.getDate());
return datePicker ;
}
}
and similarly for other controls.
Then you can do
public class CellEditingControlProvider implements EditingControlProvider {
private Map<MyFieldType, EditingControlProvider> providers ;
public CellEditingControlProvider() {
providers = new HashMap<>();
providers.put(MyFieldType.CheckBox, new CheckBoxProvider());
providers.put(MyFieldType.Date, new DatePickerProvider());
// etc...
}
#Override
public Control getControl(MyField myField) {
return providers.get(myField.getFieldType()).getControl(myField);
}
}
And now your actual cell implementation reduces to:
public class MyCell extends TreeTableCell<MyField, String> {
private EditingControlProvider controlProvider = new CellEditingControlProvider();
public MyCell() {
super();
}
#Override
public void updateItem(String item, boolean empty) {
super.updateItem(item, empty);
if (empty || getTreeTableRow() == null) {
setText(null);
setGraphic(null);
} else {
MyField myField = (MyField) getTreeTableRow().getItem();
if (isEditing()) {
setText(null);
setGraphic(controlProvider.getControl(myField));
}
//...
}
//...
}
}
If you need to implement the commitEdit(...) method in the cell, you can add a method to the interface, e.g.
public void updateFromControl(MyField myField) ;
with (I think) the obvious implementations throughout, e.g.
public class DatePickerProvider implements EditingControlProvider {
// existing code...
#Override
public void updateFromControl(MyField myField) {
myField.setDate(datePicker.getValue());
}
}
Related
I know there are many related questions about this but maybe I'm missing something because I can't get the behavior I'm expecting, to work.
#FXML
private ListView<String> guiList;
void performAction(Actions action) {
try {
Task<String> task = new Task<>() {
#Override
public String call() {
String mySelection = Context.getInstance().getSelected();
ArrayList<String> selectedList = Context.getInstance().getItemsClicked();
if (selectedList == null) {
selectedList = new ArrayList<>();
}
selectedList.add(mySelection);
Context.getInstance().setItemsClicked(selectedList);
guiList.setCellFactory(new Callback<ListView<String>, ListCell<String>>() {
#Override
public ListCell<String> call(ListView<String> param) {
ListCell<String> cell = new ListCell<String>() {
#Override
protected void updateItem(String item, boolean empty) {
super.updateItem(item, empty);
if(item != null && item.matches(mySelection)) {
setText(mySelection + " [" + action + "]");
setFont(Font.font(Font.getDefault().getFamily(), FontWeight.BOLD, Font.getDefault().getSize()));
setStyle("-fx-text-fill: green;");
} else {
setText(item);
}
}
};
return cell;
}
});
return "";
}
};
} catch (Exception e) {
}
}
When I click in an item of guiList, the text is changed, gets bold and shows in green color but I don't understand why I need the else statement. If I don't use it, all the other items of the list disappear.
I ask this because I want to change ALL of the items I click and in the current behavior, the changes are only made in the last one clicked.
Here is on approach. Use an object that has a Boolean variable to keeps up with if the item has been selected.
KeyCode 1
lvMain.getSelectionModel().selectedItemProperty().addListener(((ov, t, t1) - > {
if (t1 != null) {
t1.setSelected(true);
}
}));
Key Code 2
lvMain.setCellFactory(lv - > new ListCell < MyItem > () {
#Override
public void updateItem(MyItem item, boolean empty) {
super.updateItem(item, empty);
if (empty) {
setText(null);
setGraphic(null);
} else {
setText(item.getText());
if (item.isSelected()) {
setTextFill(Color.RED);
}
}
}
});
Main
import javafx.application.Application;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.Scene;
import javafx.scene.control.ListCell;
import javafx.scene.control.ListView;
import javafx.scene.control.SelectionMode;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
/**
*
* #author blj0011
*/
public class App extends Application {
#Override
public void start(Stage primaryStage) {
ListView<MyItem> lvMain = new ListView();//Create ListView
lvMain.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);//Change ListView selection mode to multiple
ObservableList<MyItem> items = FXCollections.observableArrayList(new MyItem("Julia"), new MyItem("Ian"), new MyItem("Sue"), new MyItem("Matthew"), new MyItem("Hannah"));//ObseravableList that will be used to set the ListView
lvMain.setItems(items);//Set the ListView's items
lvMain.setCellFactory(lv -> new ListCell<MyItem>()
{
#Override
public void updateItem(MyItem item, boolean empty)
{
super.updateItem(item, empty);
if (empty) {
setText(null);
setGraphic(null);
}
else {
setText(item.getText());
if(item.isSelected())
{
setTextFill(Color.RED);
}
}
}
});
lvMain.getSelectionModel().selectedItemProperty().addListener(((ov, t, t1) -> {
if(t1 != null)
{
t1.setSelected(true);
}
}));
VBox vbox = new VBox();
vbox.getChildren().addAll(lvMain);
StackPane root = new StackPane();
root.getChildren().add(vbox);
Scene scene = new Scene(root, 300, 250);
primaryStage.setTitle("Hello World!");
primaryStage.setScene(scene);
primaryStage.show();
}
/**
* #param args the command line arguments
*/
public static void main(String[] args) {
launch(args);
}
}
MyItem
/**
*
* #author Sed
*/
public class MyItem {
private String text;
private boolean selected;
public MyItem(String text) {
this.text = text;
this.selected = false;
}
public boolean isSelected() {
return selected;
}
public void setSelected(boolean isSelected) {
this.selected = isSelected;
}
public String getText() {
return text;
}
public void setText(String text) {
this.text = text;
}
}
Output
I think a better solution would be to use the ListView's built in multiple selection or have your cells have a ToggleButton. When the ToggleButton is on, change the color of the text. When it is off, change the color back to it's original state.
I want to have a TableCell with a custom graphic that animates on value change, where the animation type depends on the nature of the change, so I need to know the previous value to compare to the current one.
Here's your typical custom table cell (Kotlin code):
class MyTableCell<S, T> : TableCell<S, T>() {
override fun updateItem(item: T?, empty: Boolean) {
if (empty || field == null) {
text = null
graphic = null
} else {
// need to get the old value here
}
}
I see that the super method in javafx/scene/control/TableCell.java does know the old value and uses it compare it to the current one, but the override only gets the newValue:
private void updateItem(int oldIndex) {
...
final T oldValue = getItem();
...
final T newValue = currentObservableValue == null ? null : currentObservableValue.getValue();
...
if (oldIndex == index) {
if (!isItemChanged(oldValue, newValue)) {
...
}
...
}
...
updateItem(newValue, false); // sadly, `oldValue` is not passed
I can only think of an ugly workaround, so I wonder if there is some idiomatic way to get the old cell value?
Here is a sample app:
import javafx.application.Application
import javafx.beans.property.SimpleDoubleProperty
import javafx.collections.FXCollections
import javafx.scene.Scene
import javafx.scene.control.*
import javafx.scene.control.cell.PropertyValueFactory
import javafx.stage.Stage
import kotlinx.coroutines.experimental.delay
import kotlinx.coroutines.experimental.launch
import tornadofx.*
class Foo {
val barProperty = SimpleDoubleProperty()
var bar: Double
get() = barProperty.get()
set(value) = barProperty.set(value)
}
class FooApp: Application() {
override fun start(primaryStage: Stage) {
val foos = FXCollections.observableArrayList(
Foo().apply { bar = 42.0 }
)
val table = TableView<Foo>(foos)
val barColumn = TableColumn<Foo, Double>("Bar")
barColumn.cellValueFactory = PropertyValueFactory<Foo, Double>("bar")
barColumn.setCellFactory {
FooTableCell<Foo, Double> { "%.2f".format(it) }
}
table.columns.add(barColumn)
val scene = Scene(table, 400.0, 200.0)
primaryStage.scene = scene
primaryStage.title = "Table Cell"
primaryStage.show()
launch {
while (isActive) {
delay(500)
val oldFoo = foos[0]
// Replacing the old Foo instance with a new one,
// updating the value of the `bar` field:
foos[0] = Foo().apply {
bar = oldFoo.bar - 1.0 + Math.random() * 2.0
}
// because a change to a field cannot be detected by an observable list
// and so does not propagates to the table. This won't result in
// a visible change:
// foos[0].bar = foos[0].bar - 1.0 + Math.random() * 2.0
}
}
}
}
class FooTableCell<S, T>(private val format: (T) -> String) : TableCell<S, T>() {
init {
contentDisplay = ContentDisplay.GRAPHIC_ONLY
itemProperty().addListener(ChangeListener { obs, oldItem, newItem ->
if (newItem != null && oldItem != null && newItem != oldItem) {
// This is never true.
println("!!! Old: $oldItem, New: $newItem")
} else {
println("Change listener:\nOld: $oldItem, New: $newItem\n")
}
})
}
override fun updateItem(item: T?, empty: Boolean) {
val oldItem = this.item
super.updateItem(item, empty)
if (item != null && oldItem != null && item != oldItem) {
// This is never true.
println("!!! Old: $oldItem, New: $item")
} else {
println("updateItem:\nOld: $oldItem, New: $item\n")
}
if (empty || item == null) {
graphic = null
text = null
} else if (tableRow != null) {
val cell = this
graphic = Label().apply {
textProperty().bindBidirectional(cell.textProperty())
}
text = format(item)
}
}
}
fun main(args: Array<String>) {
Application.launch(FooApp::class.java, *args)
}
The actual value of the item property is changed by the default implementation of the updateItem() method, so just get the value before calling the default implementation:
public class MyTableCell<S,T> extends TableCell<S,T> {
#Override
protected void updateItem(T item, boolean empty) {
T oldItem = getItem();
super.updateItem(item, empty) ;
// ...
}
}
Alternatively, you can just register a change listener with the itemProperty():
public class MyTableCell<S,T> extends TableCell<S,T> {
public MyTableCell() {
itemProperty().addListener((obs, oldItem, newItem) -> {
// do animation here...
});
}
#Override
protected void updateItem(T item, boolean empty) {
super.updateItem(item, empty);
// other functionality here...
}
}
Here is a SSCCE demonstrating both techniques:
import java.util.Random;
import java.util.function.Function;
import javafx.application.Application;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.value.ObservableValue;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.layout.HBox;
import javafx.stage.Stage;
public class TableCellWithChange extends Application {
public static class ChangeAwareCell<S,T> extends TableCell<S,T> {
public ChangeAwareCell() {
itemProperty().addListener((obs, oldItem, newItem) -> {
System.out.printf("In listener, value for %s changed from %s to %s%n", getTableRow().getItem(), oldItem, newItem);
});
}
#Override
protected void updateItem(T item, boolean empty) {
T oldItem = getItem();
super.updateItem(item, empty);
if (empty) {
setText(null);
} else {
setText(item.toString());
System.out.printf("Change in %s from %s to %s %n", getTableView().getItems().get(getIndex()), oldItem, item);
}
}
}
#Override
public void start(Stage primaryStage) {
TableView<Item> table = new TableView<>();
TableColumn<Item, String> itemCol = column("Item", Item::nameProperty);
table.getColumns().add(itemCol);
TableColumn<Item, Number> valueCol = column("Value", Item:: valueProperty);
table.getColumns().add(valueCol);
valueCol.setCellFactory(tc -> new ChangeAwareCell<>());
TableColumn<Item, Void> changeCol = new TableColumn<>();
changeCol.setCellFactory(tc -> new TableCell<>() {
private Button incButton = new Button("^");
private Button decButton = new Button("v");
private HBox graphic = new HBox(2, incButton, decButton);
{
incButton.setOnAction(e -> {
Item item = getTableRow().getItem();
item.setValue(item.getValue()+1);
});
decButton.setOnAction(e -> {
Item item = getTableRow().getItem();
item.setValue(item.getValue()-1);
});
}
#Override
protected void updateItem(Void item, boolean empty) {
super.updateItem(item, empty);
if (empty) {
setGraphic(null);
} else {
setGraphic(graphic);
}
}
});
table.getColumns().add(changeCol);
Random rng = new Random();
for (int i = 1 ; i <= 20 ; i++) {
table.getItems().add(new Item("Item "+i, rng.nextInt(100)));
}
Scene scene = new Scene(table);
primaryStage.setScene(scene);
primaryStage.show();
}
private <S,T> TableColumn<S,T> column(String text, Function<S, ObservableValue<T>> property) {
TableColumn<S,T> col = new TableColumn<>(text);
col.setCellValueFactory(cellData -> property.apply(cellData.getValue()));
col.setPrefWidth(150);
return col ;
}
public static class Item {
private final StringProperty name = new SimpleStringProperty();
private final IntegerProperty value = new SimpleIntegerProperty();
public Item(String name, int value) {
setName(name);
setValue(value);
}
#Override
public String toString() {
return getName();
}
public final StringProperty nameProperty() {
return this.name;
}
public final String getName() {
return this.nameProperty().get();
}
public final void setName(final String name) {
this.nameProperty().set(name);
}
public final IntegerProperty valueProperty() {
return this.value;
}
public final int getValue() {
return this.valueProperty().get();
}
public final void setValue(final int value) {
this.valueProperty().set(value);
}
}
public static void main(String[] args) {
launch(args);
}
}
Of course, these items will also change, e.g. when the user scrolls around the table, or if the cells are reused in other ways; so this might not be exactly when you want. You might want to add a listener to the appropriate property in the model instead. The simplest way to do this is probably to store a reference to the actual property from the model in the cell, and update that reference when the cell is updated:
public static class ChangeAwareCell<S,T> extends TableCell<S,T> {
private Function<S, ObservableValue<T>> property ;
private ObservableValue<T> lastObservableValue ;
private ChangeListener<T> listener = (obs, oldValue, newValue) -> valueChanged(oldValue, newValue);
public ChangeAwareCell(Function<S, ObservableValue<T>> property) {
this.property = property ;
}
private void valueChanged(T oldValue, T newValue) {
System.out.printf("Value changed from %s to %s %n", oldValue, newValue);
}
#Override
protected void updateItem(T item, boolean empty) {
super.updateItem(item, empty);
if (lastObservableValue != null) {
lastObservableValue.removeListener(listener);
}
if (empty) {
setText(null);
} else {
lastObservableValue = property.apply(getTableRow().getItem());
lastObservableValue.addListener(listener);
setText(item.toString());
}
}
}
And of course make the corresponding change:
valueCol.setCellFactory(tc -> new ChangeAwareCell<>(Item::valueProperty));
Referring to question JavaFX TableView custom cell rendering split menu button, i'm able to render split menu button in every row. I've updated my code as suggested by James_D and in the answer by Keyur Bhanderi.
The question is about get value of the row where split menu is located without selecting row before click.
Update: Added images to view output
The images below show the output, every button i click.
Updated SplitMenuCellFactory.java
package com.example.splimenubtn;
import java.util.List;
import javafx.scene.control.ContentDisplay;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.util.Callback;
public class SplitMenuCellFactory<S, T> implements Callback<TableColumn<S, T>, TableCell<S, T>> {
private List<MenuItemFactory<T>> menuItems;
public SplitMenuCellFactory() {}
public SplitMenuCellFactory(List<MenuItemFactory<T>> items) {
menuItems = items;
}
#Override
public TableCell<S, T> call(TableColumn<S, T> param) {
return new TableCell<S, T>() {
#Override
public void updateItem(T item, boolean empty) {
super.updateItem(item, empty);
if (empty) {
setGraphic(null);
setText(null);
} else {
setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
if (getTableRow() != null) {
setGraphic(new SplitMenuButtonFactory<>(menuItems, getTableRow().getIndex()).buildButton());
}
}
}
};
}
}
Update, adding missing class
SplitMenuButtonFactory.java
package com.example.splimenubtn;
import java.util.List;
import javafx.scene.control.MenuItem;
import javafx.scene.control.SplitMenuButton;
public class SplitMenuButtonFactory<T> {
private List<MenuItemFactory<T>> menuItems;
private int rIndex = 0;
public SplitMenuButtonFactory(List<MenuItemFactory<T>> items) {
menuItems = items;
}
public SplitMenuButtonFactory(List<MenuItemFactory<T>> items, int rI) {
menuItems = items;
rIndex = rI;
}
public SplitMenuButton buildButton() {
SplitMenuButton menuBtn = new SplitMenuButton();
// menuBtn.getItems().addAll(menuItems);
for (MenuItemFactory<?> mIF : menuItems) {
MenuItem btn = mIF.setRowIndex(rIndex).buildMenuItem();
if (mIF.isDefault()) {
menuBtn.setText(btn.getText());
menuBtn.setOnAction(btn.getOnAction());
}
menuBtn.getItems().add(btn);
}
return menuBtn;
}
}
MenuItemsFactory.java
package com.example.splimenubtn;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.scene.control.MenuItem;
import javafx.scene.control.TableView;
public class MenuItemFactory<S> {
private MenuItemActions itemType;
private String itemLbl;
private TableView<S> table;
private boolean defaultAction;
private int rIndex = 0;
public MenuItemFactory(MenuItemActions itemType, String itemLabel, boolean dA) {
this.itemType = itemType;
itemLbl = itemLabel;
defaultAction = dA;
}
public MenuItemFactory(MenuItemActions itemType, String itemLabel, TableView<S> t, boolean dA) {
this.itemType = itemType;
itemLbl = itemLabel;
defaultAction = dA;
table = t;
}
public MenuItemFactory<S> setDataList(TableView<S> t) {
table = t;
return this;
}
public boolean isDefault() {
return defaultAction;
}
public MenuItemFactory<S> setRowIndex(int rI) {
rIndex = rI;
return this;
}
public MenuItem buildMenuItem() {
MenuItem mI = new MenuItem();
switch (itemType) {
case DETAILS:
mI.setText(itemLbl);
mI.setOnAction(handleDetails());
break;
case EDIT:
mI.setText(itemLbl);
mI.setOnAction(handleEdit());
break;
case DELETE:
mI.setText(itemLbl);
mI.setOnAction(handleDelete());
break;
default:
break;
}
return mI;
}
private EventHandler<ActionEvent> handleDetails() {
return new EventHandler<ActionEvent>() {
#Override
public void handle(ActionEvent aE) {
System.out.println("*** DETAIL REQUEST ***");
}
};
}
private EventHandler<ActionEvent> handleEdit() {
return new EventHandler<ActionEvent>() {
#Override
public void handle(ActionEvent aE) {
System.out.println("*** EDIT REQUESTED ***");
table.getSelectionModel().select(rIndex);
System.out.println("*** " + table.getSelectionModel().getSelectedItem().toString() + " ***");
}
};
}
private EventHandler<ActionEvent> handleDelete() {
return new EventHandler<ActionEvent>() {
#Override
public void handle(ActionEvent aE) {
System.out.println("*** DELETE REQUESTED ***");
System.out.println("*** " + table.getSelectionModel().getSelectedItem().toString() + " ***");
}
};
}
}
But when i click on the button, i'm getting always last value.
How can i get the object in the row where button is?
Any help or suggestion that point me to right direction is appreciated.
Simply use the TableCell to retrieve the value in the onAction event handler (or whatever you use in the product of the SplitMenuButtonFactory you're not showing to us).
Simplified example
public static SplitMenuButton createSplitMenuButton(final TableCell cell) {
SplitMenuButton result = new SplitMenuButton();
result.setOnAction(evt -> {
TableRow row = cell.getTableRow();
System.out.println("row item: " +row.getItem());
});
return result;
}
Furthermore it's better to reuse the SplitMenuButton in the cell and updating it instead of recerating it every time the cell item changes.
#Override
public TableCell<S, T> call(TableColumn<S, T> param) {
return new TableCell<S, T>() {
private final SplitMenuButton button = createSplitMenuButton(this);
{
setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
}
#Override
public void updateItem(T item, boolean empty) {
super.updateItem(item, empty);
if (empty) {
setGraphic(null);
} else {
updateMenuButton(button, item); // placeholder for updating the button according to the new item
setGraphic(button);
}
}
};
}
I want to have a JavaFX ListView of Person objects. I want the list to display only the name and allow the name to be edited. It should also preserve the other fields in each object after committing an edit to the name. How would you do this idiomatically in JavaFX?
I have the following code, which works, but it's kind of wonky because it has a StringConverter that converts one way from Person to a String of the person's name then doesn't do the reverse conversion and instead relies on the list cell commitEdit method to take a string of the name and set it on the appropriate person.
Here's the code:
import javafx.application.Application;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.Scene;
import javafx.scene.control.ListCell;
import javafx.scene.control.ListView;
import javafx.scene.control.cell.TextFieldListCell;
import javafx.stage.Stage;
import javafx.util.Callback;
import javafx.util.StringConverter;
public class Main extends Application {
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage stage) throws Exception {
stage.setTitle("My Custom List View");
ObservableList<Person> people = FXCollections.observableArrayList(
new Person("John Doe", "123 New York"),
new Person("Jane Doe", "456 San Francisco")
);
ListView<Person> listView = new ListView();
listView.setCellFactory(new CustomCellFactory());
listView.setEditable(true);
listView.setItems(people);
Scene scene = new Scene(listView,400,300);
stage.setScene(scene);
stage.show();
}
public static class CustomCellFactory implements Callback<ListView<Person>,ListCell<Person>> {
#Override
public ListCell<Person> call(ListView param) {
TextFieldListCell<Person> cell = new TextFieldListCell() {
#Override
public void updateItem(Object item, boolean empty) {
super.updateItem(item, empty);
if (!empty && item != null) {
System.out.println("updating item: "+item.toString());
setText(((Person) item).getName());
} else {
setText(null);
}
}
#Override
public void commitEdit(Object newName) {
((Person)getItem()).setName((String)newName);
super.commitEdit(getItem());
}
};
cell.setConverter(new StringConverter() {
#Override
public String toString(Object person) {
return ((Person)person).getName();
}
#Override
public Object fromString(String string) {
return string;
}
});
return cell;
}
}
public static class Person {
private String name;
private String address;
public Person(String name, String address) {
this.name = name;
this.address = address;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
public String toString() {
return name+" at "+address;
}
}
}
TextFieldListCell is just a convenience implementation of ListCell that provides the most common form of editing for list cells (i.e. if the items in the list are Strings, or objects that have an easy conversion to and from strings). You'll often find that you need more specific editing (e.g. you'll often want to filter the text allowed in the editing text field using a TextFormatter), and in that case you just implement the ListCell yourself. I think this is a case where, on balance, it makes more sense to implement ListCell from scratch.
It seems you can force the TextFieldListCell to work for this use case, using:
listView.setCellFactory(lv -> {
TextFieldListCell<Person> cell = new TextFieldListCell<Person>();
cell.setConverter(new StringConverter<Person>() {
#Override
public String toString(Person person) {
return person.getName();
}
#Override
public Person fromString(String string) {
Person person = cell.getItem();
person.setName(string);
return person ;
}
});
return cell;
});
(Note that in your code, your updateItem() method is equivalent to the one already implemented in TextFieldListCell, so it's redundant, and the extra functionality in commitEdit(...) is now in the (typesafe) StringConverter, so there's no longer any need for a subclass.)
This just feels a little fragile, as it relies on a particular implementation of committing the new value from the text field and its interaction with the string converter, but it seems to work fine in tests.
My preference for this, however, would be to implement the ListCell directly yourself, as it gives you full control over the interaction between the text field and the editing process. This is pretty straightforward:
import javafx.application.Application;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.Scene;
import javafx.scene.control.ListCell;
import javafx.scene.control.ListView;
import javafx.scene.control.TextField;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.stage.Stage;
public class Main extends Application {
#Override
public void start(Stage primaryStage) {
ListView<Person> listView = new ListView<>();
ObservableList<Person> people = FXCollections.observableArrayList(
new Person("John Doe", "123 New York"),
new Person("Jane Doe", "456 San Francisco")
);
listView.setEditable(true);
listView.setItems(people);
listView.setCellFactory(lv -> new ListCell<Person>() {
private TextField textField = new TextField() ;
{
textField.setOnAction(e -> {
commitEdit(getItem());
});
textField.addEventFilter(KeyEvent.KEY_RELEASED, e -> {
if (e.getCode() == KeyCode.ESCAPE) {
cancelEdit();
}
});
}
#Override
protected void updateItem(Person person, boolean empty) {
super.updateItem(person, empty);
if (empty) {
setText(null);
setGraphic(null);
} else if (isEditing()) {
textField.setText(person.getName());
setText(null);
setGraphic(textField);
} else {
setText(person.getName());
setGraphic(null);
}
}
#Override
public void startEdit() {
super.startEdit();
textField.setText(getItem().getName());
setText(null);
setGraphic(textField);
textField.selectAll();
textField.requestFocus();
}
#Override
public void cancelEdit() {
super.cancelEdit();
setText(getItem().getName());
setGraphic(null);
}
#Override
public void commitEdit(Person person) {
super.commitEdit(person);
person.setName(textField.getText());
setText(textField.getText());
setGraphic(null);
}
});
// for debugging:
listView.setOnMouseClicked(e -> {
if (e.getClickCount() == 2) {
listView.getItems().forEach(p -> System.out.println(p.getName()));
}
});
Scene scene = new Scene(listView,400,300);
primaryStage.setScene(scene);
primaryStage.show();
}
public static class Person {
private String name;
private String address;
public Person(String name, String address) {
this.name = name;
this.address = address;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
public String toString() {
return name+" at "+address;
}
}
public static void main(String[] args) {
launch(args);
}
}
If you might need this kind of functionality frequently, you could easily create a reusable class:
import java.util.function.BiFunction;
import java.util.function.Function;
import javafx.scene.control.ListCell;
import javafx.scene.control.TextField;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
public class EditingListCell<T> extends ListCell<T> {
private final TextField textField ;
private final Function<T, String> propertyAccessor ;
public EditingListCell(Function<T, String> propertyAccessor, BiFunction<String, T, T> updater) {
this.propertyAccessor = propertyAccessor ;
this.textField = new TextField();
textField.setOnAction(e -> {
T newItem = updater.apply(textField.getText(), getItem());
commitEdit(newItem);
});
textField.addEventFilter(KeyEvent.KEY_RELEASED, e -> {
if (e.getCode() == KeyCode.ESCAPE) {
cancelEdit();
}
});
}
#Override
protected void updateItem(T item, boolean empty) {
super.updateItem(item, empty);
if (empty || item == null) {
setText(null);
setGraphic(null);
} else if (isEditing()) {
textField.setText(propertyAccessor.apply(item));
setText(null);
setGraphic(textField);
} else {
setText(propertyAccessor.apply(item));
setGraphic(null);
}
}
#Override
public void startEdit() {
super.startEdit();
textField.setText(propertyAccessor.apply(getItem()));
setText(null);
setGraphic(textField);
textField.selectAll();
textField.requestFocus();
}
#Override
public void cancelEdit() {
super.cancelEdit();
setText(propertyAccessor.apply(getItem()));
setGraphic(null);
}
#Override
public void commitEdit(T item) {
super.commitEdit(item);
getListView().getItems().set(getIndex(), item);
setText(propertyAccessor.apply(getItem()));
setGraphic(null);
}
}
and then you just need
listView.setCellFactory(lv -> new EditingListCell<>(
Person::getName,
(text, person) -> {
person.setName(text);
return person ;
})
);
I've set up a multiselection enabled tableview and am trying to attach a listener a checkbox inserted into a column to the selection model of the table.
checkBoxTableColumn.setCellValueFactory(
cellData -> {
CheckBox checkBox = new CheckBox();
ObjectProperty<CheckBox> sop = new SimpleObjectProperty<CheckBox>();
sop.setValue(checkBox);
sop.getValue().setText("");
sop.getValue().selectedProperty().addListener(
(obsv, oldv, newv) -> {
ArrayList<Job> tempSelectionArray = new ArrayList<>();
if(newv.booleanValue()){
tempSelectionArray.addAll(jobTableView.getSelectionModel().getSelectedItems().stream().collect(Collectors.toList()));
this.jobTableView.getSelectionModel().clearSelection();
for(Job job: tempSelectionArray){
jobTableView.getSelectionModel().select(job);
}
tempSelectionArray.clear();
}
else{
tempSelectionArray.addAll(this.jobTableView.getSelectionModel().getSelectedItems().stream().collect(Collectors.toList()));
this.jobTableView.getSelectionModel().clearSelection();
tempSelectionArray.remove(getJobTableView().getFocusModel().getFocusedItem());
for(Job job: tempSelectionArray){
this.jobTableView.getSelectionModel().select(job);
}
tempSelectionArray.clear();
}
}
);
ObservableValue<CheckBox> ov = sop;
return ov;
}
But that doesn't change the table selection.
Edited as jurge stated
checkBoxTableColumn.setCellFactory(new Callback<TableColumn<Job, Boolean>, TableCell<Job, Boolean>>() {
#Override
public TableCell<Job, Boolean> call(TableColumn<Job, Boolean> param) {
return new CheckBoxCell(jobTableView);
}
});
and checkbox cell is as
class CheckBoxCell extends TableCell<Job, Boolean>{
private CheckBox checkBox;
private TableView<Job> jobTableView;
public CheckBoxCell(TableView<Job> tableView){
this.jobTableView = tableView;
}
#Override
public void updateItem(Boolean item, boolean empty) {
super.updateItem(item, empty);
checkBox = new CheckBox();
setGraphic(checkBox);
checkBox.selectedProperty().addListener(
(obsv, oldv, newv) -> {
ArrayList<Job> tempSelectionArray = new ArrayList<>();
if(newv.booleanValue()){
tempSelectionArray.addAll(jobTableView.getSelectionModel().getSelectedItems().stream().collect(Collectors.toList()));
this.jobTableView.getSelectionModel().clearSelection();
for(Job job: tempSelectionArray){
jobTableView.getSelectionModel().select(job);
}
tempSelectionArray.clear();
}
else{
tempSelectionArray.addAll(this.jobTableView.getSelectionModel().getSelectedItems().stream().collect(Collectors.toList()));
this.jobTableView.getSelectionModel().clearSelection();
tempSelectionArray.remove(jobTableView.getFocusModel().getFocusedItem());
for(Job job: tempSelectionArray){
this.jobTableView.getSelectionModel().select(job);
}
tempSelectionArray.clear();
}
}
);
}
}
The listener wouldn't work this time.....
edit-#2
in initialize method of controller:
jobTableView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
checkBoxTableColumn.setEditable(true);
checkBoxTableColumn.setCellValueFactory(
new PropertyValueFactory<Job, Boolean>("isContextSelected")
);
checkBoxTableColumn.setCellFactory(
new Callback<TableColumn<Job, Boolean>, TableCell<Job, Boolean>>() {
#Override
public TableCell<Job, Boolean> call(TableColumn<Job, Boolean> param) {
return new CheckBoxTableCell<Job, Boolean>(){
{
setAlignment(Pos.CENTER);
}
#Override
public void updateItem(Boolean item, boolean empty){
if(!empty){
TableRow row = getTableRow();
if(row != null){
Integer rowNumber = row.getIndex();
TableView.TableViewSelectionModel sm = getTableView().getSelectionModel();
if(item){
sm.select(rowNumber);
}
else{
sm.clearSelection(rowNumber);
}
}
}
super.updateItem(item, empty);
}
};
}
}
);
the job class:
private BooleanProperty isContextSelected;
public BooleanProperty isContextSelectedProperty() {
return isContextSelected;
}
edit--
Ignore the unnecessary parts. The whole code as requested.:
The controller:
package BillControl.view;
import BillControl.Controller.PopulateView;
import BillControl.Controller.Validator;
import BillControl.MainApp;
import BillControl.model.Article;
import BillControl.model.Job;
import javafx.beans.property.*;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.fxml.FXML;
import javafx.geometry.Pos;
import javafx.scene.control.*;
import javafx.scene.control.cell.CheckBoxTableCell;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.stage.Stage;
import javafx.util.Callback;
import java.util.ArrayList;
import java.util.TreeSet;
public class ArticleJobAssignmentController {
private Article article;
private Stage jobAssignStage;
private boolean okClicked = false;
private MainApp mainApp;
ArrayList<Job> selectedJobList = new ArrayList<>();
private ObservableList<Job> masterJobList = FXCollections.observableArrayList();
private ObservableList<Job> currentJobList = FXCollections.observableArrayList();
private ObservableList<Job> articleEngagementList = FXCollections.observableArrayList();
private TreeSet rowIndices = new TreeSet();
#FXML
private Label articleNameLabel;
#FXML
private Label noOfJobsLabel;
#FXML
private Button okButton;
#FXML
private Button cancelButton;
#FXML
private Label errorLabel;
#FXML
private TableView<Job> jobTableView;
#FXML
private TableColumn<Job, Boolean> checkBoxTableColumn;
#FXML
private TableColumn<Job, String> jobNameColumn;
#FXML
private TableColumn<Job, String> clientNameColumn;
#FXML
private TableColumn<Job, Integer> noOfArticlesColumn;
#FXML
private TableColumn<Job, String> alreadyEngagedColumn;
public ArticleJobAssignmentController(){
}
public void initialize(){
errorLabel.setVisible(false);
jobTableView.setEditable(true);
jobTableView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
checkBoxTableColumn.setEditable(true);
checkBoxTableColumn.setCellValueFactory(
new PropertyValueFactory<Job, Boolean>("isContextSelected")
);
checkBoxTableColumn.setCellFactory(
new Callback<TableColumn<Job, Boolean>, TableCell<Job, Boolean>>() {
#Override
public TableCell<Job, Boolean> call(TableColumn<Job, Boolean> param) {
return new CheckBoxTableCell<Job, Boolean>(){
{
setAlignment(Pos.CENTER);
}
#Override
public void updateItem(Boolean item, boolean empty){
if(!empty){
TableRow row = getTableRow();
if(row != null){
Integer rowNumber = row.getIndex();
TableView.TableViewSelectionModel sm = getTableView().getSelectionModel();
if(item){
sm.select(rowNumber);
}
else{
sm.clearSelection(rowNumber);
}
}
}
super.updateItem(item, empty);
}
};
}
}
);
jobNameColumn.setCellValueFactory(
cellData -> cellData.getValue().nameProperty()
);
noOfArticlesColumn.setCellValueFactory(
cellData -> {
SimpleIntegerProperty sip = new SimpleIntegerProperty(cellData.getValue().numberOfArticlesProperty().getValue());
ObservableValue<Integer> ov = sip.asObject();
return ov;
}
);
alreadyEngagedColumn.setCellValueFactory(
cellData -> {
Boolean engaged = false;
for(Job job: articleEngagementList){
if(job.getNumberID().equals(cellData.getValue().getNumberID())){
engaged = true;
}
}
if(engaged){
SimpleStringProperty sbp = new SimpleStringProperty("Yes");
ObservableValue<String> ov = sbp;
return ov;
}
else {
SimpleStringProperty sbp = new SimpleStringProperty("No");
ObservableValue<String> ov = sbp;
return ov;
}
}
);
jobTableView.getSelectionModel().getSelectedItems().addListener(
new ListChangeListener<Job>() {
#Override
public void onChanged(Change<? extends Job> c) {
noOfJobsLabel.setText(String.valueOf(c.getList().size()));
}
}
);
}
public void filterMasterList(){
for(Job job : masterJobList){
if(!job.getIsCompleted()){
currentJobList.add(job);
}
}
for(Job currentJob : currentJobList){
currentJob.setIsContextSelected(false);
}
}
#FXML
public void handleOkClicked(){
if(!Validator.articleJobAssignment(this)){
for(Job job : jobTableView.getSelectionModel().getSelectedItems()){
selectedJobList.add(job);
}
okClicked = true;
jobAssignStage.close();
}
else {
errorLabel.setText("Select at least one job");
errorLabel.setVisible(true);
}
}
#FXML
public void handleCancelClicked(){
jobAssignStage.close();
}
public void setArticle(Article article) {
this.article = article;
articleNameLabel.setText(article.getName());
}
public void setJobAssignStage(Stage jobAssignStage) {
this.jobAssignStage = jobAssignStage;
}
public void setOkClicked(boolean okClicked) {
this.okClicked = okClicked;
}
public void setMainApp(MainApp mainApp) {
this.mainApp = mainApp;
setMasterJobList(mainApp.getJobObservableList());
filterMasterList();
jobTableView.setItems(currentJobList);
if(article != null){
articleEngagementList = PopulateView.articleCurrentEngagementList(articleEngagementList, article.getId(), mainApp.getClientObservableList());
}
}
public Label getArticleNameLabel() {
return articleNameLabel;
}
public Label getNoOfJobsLabel() {
return noOfJobsLabel;
}
public Button getOkButton() {
return okButton;
}
public Button getCancelButton() {
return cancelButton;
}
public TableView<Job> getJobTableView() {
return jobTableView;
}
public TableColumn<Job, String> getJobNameColumn() {
return jobNameColumn;
}
public TableColumn<Job, String> getClientNameColumn() {
return clientNameColumn;
}
public TableColumn<Job, Integer> getNoOfArticlesColumn() {
return noOfArticlesColumn;
}
public ObservableList<Job> getMasterJobList() {
return masterJobList;
}
public void setMasterJobList(ObservableList<Job> masterJobList) {
this.masterJobList = masterJobList;
}
public boolean isOkClicked() {
return okClicked;
}
public ArrayList<Job> getSelectedJobList() {
return selectedJobList;
}
}
the job class(ignore the constructors):
package BillControl.model;
import BillControl.Controller.PopulateItems;
import BillControl.GeneralUtils.DateUtil;
import javafx.beans.property.*;
import javafx.collections.ObservableList;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.TimeUnit;
public class Job {
private String numberID;
private StringProperty name;
private ObjectProperty<Client> client;
private StringProperty clientName;
private ObjectProperty<Date> startPeriod;
private ObjectProperty<Date> endPeriod;
private ObjectProperty<Date> startDate;
private ObjectProperty<Date> targetDate;
private BooleanProperty isDelayed;
private LongProperty remainingDays;
private LongProperty delayedDays;
private BooleanProperty isCompleted;
private ObjectProperty<Date> completionDate;
private LongProperty daysToComplete;
private StringProperty partner;
private IntegerProperty numberOfArticles;
private BooleanProperty isContextSelected;
private String clientID;
public Job(Client client){
this.numberID = null;
this.client = new SimpleObjectProperty<Client>(client);
this.clientName = new SimpleStringProperty(client.getName());
this.name = new SimpleStringProperty("");
this.partner = new SimpleStringProperty("");
this.startDate = new SimpleObjectProperty<Date>();
this.targetDate = new SimpleObjectProperty<Date>();
this.completionDate = new SimpleObjectProperty<Date>();
this.isCompleted = new SimpleBooleanProperty(false);
this.startPeriod = new SimpleObjectProperty<Date>();
this.endPeriod = new SimpleObjectProperty<Date>();
this.fillOthers(false);
// todo check fill others logic
}
public Job(ObservableList clientList, String numberID){
this.numberID = numberID;
// this.numberID = null;
this.name = new SimpleStringProperty("");
this.partner = new SimpleStringProperty("");
this.startDate = new SimpleObjectProperty<Date>();
this.targetDate = new SimpleObjectProperty<Date>();
this.completionDate = new SimpleObjectProperty<Date>();
this.isCompleted = new SimpleBooleanProperty(false);
this.startPeriod = new SimpleObjectProperty<Date>();
this.endPeriod = new SimpleObjectProperty<Date>();
this.client = new SimpleObjectProperty<Client>();
this.clientName = new SimpleStringProperty();
this.numberOfArticles = new SimpleIntegerProperty();
this.isContextSelected = new SimpleBooleanProperty(false);
PopulateItems.populateJob(this);
Client selectedClient = null;
for(Object clientObject : clientList){
Client queriedClient = (Client) clientObject;
String name = queriedClient.getName();
String queriedName = PopulateItems.clientID2NameHelper(this.getClientID());
if(name.equals(queriedName)){
selectedClient = (Client) clientObject;
break;
}
}
this.setClient(selectedClient);
this.setClientName(this.getClient().getName());
this.fillOthers(true);
}
public Job(){
this.numberID = null;
this.client = new SimpleObjectProperty<Client>();
this.clientName = new SimpleStringProperty("");
this.name = new SimpleStringProperty("");
this.partner = new SimpleStringProperty("");
this.startDate = new SimpleObjectProperty<Date>();
this.targetDate = new SimpleObjectProperty<Date>();
this.completionDate = new SimpleObjectProperty<Date>();
this.isCompleted = new SimpleBooleanProperty(false);
this.startPeriod = new SimpleObjectProperty<Date>();
this.endPeriod = new SimpleObjectProperty<Date>();
this.fillOthers(false);
}
public void fillOthers(Boolean filledJob){
if(filledJob){
DateFormat dateFormat = new SimpleDateFormat("dd/MM/yyyy");
try {
Date startDate = this.getStartDate();
Date completionDate = this.getCompletionDate();
Date currentDate = DateUtil.getCurrentDate();
Date targetDate = this.getTargetDate();
if (this.getIsCompleted()){
// completion days
this.daysToComplete = new SimpleLongProperty();
long duration = completionDate.getTime() - startDate.getTime();
long diffInDays = TimeUnit.MILLISECONDS.toDays(duration);
this.setDaysToComplete(diffInDays);
}
else{
this.remainingDays = new SimpleLongProperty();
this.isDelayed = new SimpleBooleanProperty();
if (targetDate.after(currentDate) && !this.getIsCompleted()){
// remaining days
long duration = targetDate.getTime() - currentDate.getTime();
long diffInDays = TimeUnit.MILLISECONDS.toDays(duration);
this.setRemainingDays(diffInDays);
this.setIsDelayed(false);
}
else if (targetDate.before(currentDate) && !this.getIsCompleted()) {
// delayed days
this.delayedDays = new SimpleLongProperty();
this.setIsDelayed(true);
long duration = currentDate.getTime() - targetDate.getTime();
long diffInDays = TimeUnit.MILLISECONDS.toDays(duration);
this.setRemainingDays(0);
this.setDelayedDays(diffInDays);
}
}
}
catch (Exception e){
e.printStackTrace();
}
}
else {
//TODO client creation form job
}
}
public String getName() {
return name.get();
}
public StringProperty nameProperty() {
return name;
}
public void setName(String name) {
this.name.set(name);
}
public Client getClient() {
return client.get();
}
public ObjectProperty<Client> clientProperty() {
return client;
}
public void setClient(Client client) {
this.client.set(client);
}
public Date getStartDate() {
return startDate.get();
}
public ObjectProperty<Date> startDateProperty() {
return startDate;
}
public void setStartDate(Date startDate) {
this.startDate.set(startDate);
}
public Date getTargetDate() {
return targetDate.get();
}
public ObjectProperty<Date> targetDateProperty() {
return targetDate;
}
public void setTargetDate(Date targetDate) {
this.targetDate.set(targetDate);
}
public boolean getIsDelayed() {
return isDelayed.get();
}
public BooleanProperty isDelayedProperty() {
return isDelayed;
}
public void setIsDelayed(boolean isDelayed) {
this.isDelayed.set(isDelayed);
}
public long getRemainingDays() {
return remainingDays.get();
}
public LongProperty remainingDaysProperty() {
return remainingDays;
}
public void setRemainingDays(long remainingDays) {
this.remainingDays.set(remainingDays);
}
public long getDelayedDays() {
return delayedDays.get();
}
public LongProperty delayedDaysProperty() {
return delayedDays;
}
public void setDelayedDays(long delayedDays) {
this.delayedDays.set(delayedDays);
}
public boolean getIsCompleted() {
return isCompleted.get();
}
public BooleanProperty isCompletedProperty() {
return isCompleted;
}
public void setIsCompleted(boolean isCompleted) {
this.isCompleted.set(isCompleted);
}
public Date getCompletionDate() {
return completionDate.get();
}
public ObjectProperty<Date> completionDateProperty() {
return completionDate;
}
public void setCompletionDate(Date completionDate) {
this.completionDate.set(completionDate);
}
public long getDaysToComplete() {
return daysToComplete.get();
}
public LongProperty daysToCompleteProperty() {
return daysToComplete;
}
public void setDaysToComplete(long daysToComplete) {
this.daysToComplete.set(daysToComplete);
}
public String getPartner() {
return partner.get();
}
public StringProperty partnerProperty() {
return partner;
}
public void setPartner(String partner) {
this.partner.set(partner);
}
public Integer getNumberOfArticles() {
return numberOfArticles.get();
}
public IntegerProperty numberOfArticlesProperty() {
return numberOfArticles;
}
public void setNumberOfArticles(int numberOfArticles) {
this.numberOfArticles.set(numberOfArticles);
}
public String getNumberID() {
return numberID;
}
public String getClientName() {
return clientName.get();
}
public StringProperty clientNameProperty() {
return clientName;
}
public void setClientName(String clientName) {
this.clientName.set(clientName);
}
public Date getStartPeriod() {
return startPeriod.get();
}
public ObjectProperty<Date> startPeriodProperty() {
return startPeriod;
}
public void setStartPeriod(Date startPeriod) {
this.startPeriod.set(startPeriod);
}
public Date getEndPeriod() {
return endPeriod.get();
}
public ObjectProperty<Date> endPeriodProperty() {
return endPeriod;
}
public void setEndPeriod(Date endPeriod) {
this.endPeriod.set(endPeriod);
}
public String getClientID() {
return clientID;
}
public void setClientID(String clientID) {
this.clientID = clientID;
}
public boolean getIsContextSelected() {
return isContextSelected.get();
}
public BooleanProperty isContextSelectedProperty() {
return isContextSelected;
}
public void setIsContextSelected(boolean isContextSelected) {
this.isContextSelected.set(isContextSelected);
}
}
Ok, so this one way to do it. You'll need a BooleanProperty in your backing model to hold the value of the check box so that the table will 'remember' if that rows check box should be selected or not if the row scrolls out of view and then back again.
TableColumn<Job,Boolean> checkCol = new TableColumn<>("Check");
checkCol.setCellValueFactory( new PropertyValueFactory<Job,Boolean>( "checkBoxValue" ) );
checkCol.setCellFactory( new Callback<TableColumn<Job,Boolean>, TableCell<Job,Boolean>>()
{
#Override
public TableCell<Job,Boolean> call( TableColumn<Job,Boolean> param )
{
return new CheckBoxTableCell<Job,Boolean>()
{
{
setAlignment( Pos.CENTER );
}
#Override
public void updateItem( Boolean item, boolean empty )
{
if ( ! empty )
{
TableRow row = getTableRow();
if ( row != null )
{
int rowNo = row.getIndex();
TableViewSelectionModel sm = getTableView().getSelectionModel();
if ( item ) sm.select( rowNo );
else sm.clearSelection( rowNo );
}
}
super.updateItem( item, empty );
}
};
}
} );
checkCol.setEditable( true );
checkCol.setMaxWidth( 50 );
checkCol.setMinWidth( 50 );
You are using checkBoxTableColumn.setCellValueFactory incorrectly.
Your TableView has data items of type T, and the setCellValueFactory method on a column is there to tell the column what value it must extract out of an object of type T to display.
You however are returning an observable value containing a GUI component (CheckBox), whereas you should be returning an observable Boolean value extracted from cellData.
See here for an Oracle tutorial on TableView: http://docs.oracle.com/javafx/2/ui_controls/table-view.htm#CJAGAAEE
Adding a checkbox column to a table where changes to the table checkbox are propogated back to the model object is quite simple:
TableColumn<Job,Boolean> checkCol = new TableColumn<>("Check");
checkCol.setCellValueFactory( new PropertyValueFactory<Job,Boolean>( "checkBoxValue" ) );
checkCol.setCellFactory( CheckBoxTableCell.forTableColumn( checkCol ) );
Note that "checkBoxValue" is the partial name of a property method in Job called checkBoxValueProperty() that returns a BooleanProperty. (It doesn't have to be called checkBoxValue, you can give it a different name, but it must end with Property.)