Java Fx - How to populate a combo box with different data [duplicate] - javafx

I have a TableView with two columns named Product and Brand. A Product can be of different Brands. Eg. TV has different brands like Samsung, Sony etc. I am showing Brands of a Product in ComboBoxes.
This is how i am adding a ComboBoxTableCell for my Brand Column.
ObservableList<String> catList = FXCollections.observableArrayList();
categoryCol.setCellFactory(t -> {
ComboBoxTableCell comboCell = new ComboBoxTableCell(catList);
return comboCell;
});
contactTable.getColumns().add(categoryCol);
Now in these ComboBoxes i want to add values of Brands. Since There will be different Products so their Brands will be different also. Eg.
Product | Model
----------------------------------------------
TV | ComboBox[Samsung, Sony, Panasonic]
Monitor | ComboBox[Dell, Microsoft, Apple ]
Now since the ComboBoxes have same data model (ObservableList) how can i add different values to them by selecting the items in table. Is it possible to do ? Thanks in advance for any help.

First of all you need a custom row class in which you store the elements,
then you have to #Override the startEdit() from theComboBoxTreeTableCell for example this way :
#Override public void startEdit() {
MyCustomRow currentRow = getTableRow().getItem();
getItems().setAll(currentRow.getModels());
super.startEdit();
}
}
MyCustomRow:
package mypackage;
import javafx.beans.property.SimpleStringProperty;
import java.util.List;
import java.util.Map;
public class MyCustomRow {
private SimpleStringProperty product;
private SimpleStringProperty model;
private List<String> allModels;
public MyCustomRow(
String product,
String model,
List<String> models) {
this.product = new SimpleStringProperty(product);
this.model = new SimpleStringProperty(product);
this.allModels = models;
}
public String getProduct() {
return product.get();
}
public SimpleStringProperty productProperty() {
return product;
}
public String getModel() {
return model.get();
}
public SimpleStringProperty modelProperty() {
return model;
}
public List<String> getModels() {
return allModels;
}
}
Then in your contoller class you can say:
ObservableList<String> carList = FXCollections.observableArrayList();
categoryCol.setCellFactory(t -> new ComboBoxTableCell(carList){
#Override public void startEdit() {
MyCustomRow currentRow = getTableRow().getItem();
getItems().setAll(currentRow.getModels());
super.startEdit();
}
});
categoryCol.setCellValueFactory(v -> v.getValue().modelProperty());
contactTable.getColumns().add(categoryCol);
So each row you add the appropriate models. So in the ComboBox you will have only those items(models) which belong to the product

Related

JavaFX Application fails to compile on lambda expression

I am trying to build a JavaFX Application to display a TreeTableView. Still setting up this whole thing. I got it to work with only one column without the Product class but i am struggling to make it work with the Product class and two columns. The following piece of code fails to compile:
col1.setCellValueFactory(
(TreeTableColumn.CellDataFeatures<Product, String> param) -> param.getValue().getValue().getNameProperty());
and spits out this error:
Error:(38, 121) java: incompatible types: bad return type in lambda expression
java.lang.String cannot be converted to javafx.beans.value.ObservableValue<java.lang.String>
This is the entire code:
public class Controller implements Initializable {
#FXML
private TreeTableView<Product> tableView;
#FXML
private TreeTableColumn<Product, String> col1;
#FXML
private TreeTableColumn<Product, String> col2;
TreeItem<Product> product1 = new TreeItem<>(new Product("Bread", "300g"));
TreeItem<Product> product2 = new TreeItem<>(new Product("Eggs", "5"));
TreeItem<Product> product3 = new TreeItem<>(new Product("Brad Pitt", "One and Only one"));
TreeItem<Product> product4 = new TreeItem<>(new Product("Moisturizer", "20"));
TreeItem<Product> product5 = new TreeItem<>(new Product("Horse Lubricant", "4"));
TreeItem<Product> root = new TreeItem<>(new Product("Name", "Quantity"));
#Override
public void initialize(URL url, ResourceBundle resourceBundle) {
root.getChildren().setAll(product1, product2, product3, product4, product5);
col1.setCellValueFactory(
(TreeTableColumn.CellDataFeatures<Product, String> param) -> param.getValue().getValue().getNameProperty());
col2.setCellValueFactory(
(TreeTableColumn.CellDataFeatures<Product, String> param) -> param.getValue().getValue().getQuantityProperty());
tableView.setRoot(root);
tableView.setShowRoot(false);
}
public class Product{
SimpleStringProperty nameProperty;
SimpleStringProperty quantityProperty;
public Product(String name, String quantity){
this.nameProperty = new SimpleStringProperty(name);
this.quantityProperty = new SimpleStringProperty(quantity);
}
public String getNameProperty() {
return nameProperty.get();
}
public SimpleStringProperty namePropertyProperty() {
return nameProperty;
}
public void setNameProperty(String nameProperty) {
this.nameProperty.set(nameProperty);
}
public String getQuantityProperty() {
return quantityProperty.get();
}
public SimpleStringProperty quantityPropertyProperty() {
return quantityProperty;
}
public void setQuantityProperty(String quantityProperty) {
this.quantityProperty.set(quantityProperty);
}
}
}
First, your Product class is not conventional. Typically the field name matches the property name (e.g. name, not nameProperty). Then you name your getter, setter, and property getter after the name of the property. For instance:
import javafx.beans.property.StringProperty;
import javafx.beans.property.SimpleStringProperty;
public class Product {
private final StringProperty name = new SimpleStringProperty(this, "name");
public final void setName(String name) { this.name.set(name); }
public final String getName() { return name.get(); }
public final StringProperty nameProperty() { return name; }
private final StringProperty quantity = new SimpleStringProperty(this, "quantity");
public final void setQuantity(String quantity) { this.quantity.set(quantity); }
public final String getQuantity() { return quantity.get(); }
public final StringProperty quantityProperty() { return quantity; }
public Product() {} // typically Java(FX)Beans provide no-arg constructors as well
public Product(String name, String quantity) {
setName(name);
setQuantity(quantity);
}
}
Note: Your class is a non-static nested (i.e. inner) class. This means each Product instance requires an instance of the enclosing class. If you want to keep Product a nested class, consider making it static. My example above assumes Product is in its own source file.
With that class, you would define your cell value factories like so:
TreeTableColumn<Product, String> nameCol = ...;
nameCol.setCellValueFactory(data -> data.getValue().getValue().nameProperty());
TreeTableColumn<Product, String> quantityCol = ...;
quantityCol.setCellValueFactory(data -> data.getValue().getValue().quantityProperty());
Notice the factories return the appropriate property of the Product instance. This solves your compilation error since StringProperty is an instance of ObservableValue<String>. It also means your table has direct access to the backing model's property, which helps with keeping the table up-to-date and also with implementing inline editing.
In case it helps, here's setting the cell value factory of nameCol using an anonymous class which explicitly shows all the types used:
nameCol.setCellValueFactory(new Callback<>() { // may have to explicitly define type arguments, depending on version of Java
#Override
public ObservableValue<String> call(TreeTableColumn.CellDataFeatures<Product, String> data) {
TreeItem<Product> treeItem = data.getValue();
Product product = treeItem.getValue();
return product.nameProperty();
}
});

JavaFX TreeTableView - Prevent editing of unavailable cells

I have a particular TreeTableView that displays a hierarchical tree of mixed types. These types do not necessarily have overlapping columns and as such the columns for some rows will be empty. As an example, consider the following classes:
public class Person {
private final StringProperty nameProperty;
private final StringProperty surnameProperty;
public Person() {
this.nameProperty = new SimpleStringProperty();
this.surnameProperty = new SimpleStringProperty();
}
public StringProperty nameProperty() {
return this.nameProperty;
}
public void setName(String value) {
this.nameProperty.set(value);
}
public String getName() {
return this.nameProperty.get();
}
public StringProperty surnameProperty() {
return this.surnameProperty;
}
public void setSurname(String value) {
this.surnameProperty.set(value);
}
public String getSurname() {
return this.surnameProperty.get();
}
}
public class Dog {
private final StringProperty nameProperty;
private final IntegerProperty ageProperty;
private final StringProperty breedProperty;
public Dog() {
this.nameProperty = new SimpleStringProperty();
this.ageProperty = new SimpleIntegerProperty();
this.breedProperty = new SimpleStringProperty();
}
public StringProperty nameProperty() {
return this.nameProperty;
}
public void setName(String value) {
this.nameProperty.set(value);
}
public String getName() {
return this.nameProperty.get();
}
public IntegerProperty ageProperty() {
return this.ageProperty;
}
public void setAge(int value) {
this.ageProperty.setValue(value);
}
public int getAge() {
return this.ageProperty.get();
}
public StringProperty breedProperty() {
return this.breedProperty;
}
public void setBreed(String breed) {
this.breedProperty.set(breed);
}
public String getBreed() {
return this.breedProperty.get();
}
}
If I construct the TreeTableView as follows:
TreeTableView<Object> treeTableView = new TreeTableView<>();
treeTableView.setEditable(true);
List<TreeTableColumn<Object, ?>> columns = treeTableView.getColumns();
TreeTableColumn<Object, String> nameColumn = new TreeTableColumn<>("Name");
nameColumn.setCellValueFactory(new TreeItemPropertyValueFactory<>("name"));
nameColumn.setCellFactory(TextFieldTreeTableCell.forTreeTableColumn());
columns.add(nameColumn);
TreeTableColumn<Object, String> surnameColumn = new TreeTableColumn<>("Surname");
surnameColumn.setCellFactory(TextFieldTreeTableCell.forTreeTableColumn());
surnameColumn.setCellValueFactory(new TreeItemPropertyValueFactory<>("surname"));
columns.add(surnameColumn);
TreeTableColumn<Object, Integer> ageColumn = new TreeTableColumn<>("Age");
ageColumn.setCellFactory(TextFieldTreeTableCell.forTreeTableColumn(new IntegerStringConverter()));
ageColumn.setCellValueFactory(new TreeItemPropertyValueFactory<>("age"));
columns.add(ageColumn);
TreeTableColumn<Object, String> breedColumn = new TreeTableColumn<>("Breed");
breedColumn.setCellFactory(TextFieldTreeTableCell.forTreeTableColumn());
breedColumn.setCellValueFactory(new TreeItemPropertyValueFactory<>("breed"));
columns.add(breedColumn);
TreeItem<Object> rootItem = new TreeItem<>();
treeTableView.setRoot(rootItem);
treeTableView.setShowRoot(false);
List<TreeItem<Object>> rootChildren = rootItem.getChildren();
Person john = new Person();
john.setName("John");
john.setSurname("Denver");
TreeItem<Object> johnTreeItem = new TreeItem<>(john);
rootChildren.add(johnTreeItem);
List<TreeItem<Object>> johnChildren = johnTreeItem.getChildren();
Dog charlie = new Dog();
charlie.setName("Charlie");
charlie.setAge(4);
charlie.setBreed("Labrador");
TreeItem<Object> charlieTreeItem = new TreeItem<>(charlie);
johnChildren.add(charlieTreeItem);
Dog daisy = new Dog();
daisy.setName("Daisy");
daisy.setAge(7);
daisy.setBreed("Bulldog");
TreeItem<Object> daisyTreeItem = new TreeItem<>(daisy);
johnChildren.add(daisyTreeItem);
I will get a TreeTableView that looks like:
The Age and Breed columns are empty for the TreeItems that contains Person objects. However, nothing stops me from editing Age or Breed cell for the top-most Person row. Setting a value in one of those cells doesn't change the Person object, but the value still hangs around there like it is committed.
Is there any way to prevent this from happening? I know that I could check for nulls in a custom TreeTableCell subclass and prevent the editing from kicking off in the startEdit() method. However, there are circumstances where a null-value is valid and preventing editing by checking nulls is not a feasible solution for all situations. Also, creating a custom TreeTableCell subclass for every datatype and corresponding columns is painful. It would have been nice if TreeItemPropertyValueFactory could provide for a way to abort the edit when no value is present for a particular cell.
Ok, I scraped together something by looking at the TreeItemPropertyValueFactory class itself for inspiration. This gives me the desired functionality, although I'm not sure if it is 100% correct or what the implications are of using it.
It basically comes down to installing a new cell-factory that checks if the cell-value-factory is of type TreeItemPropertyValueFactory. If it is the case, a new cell-factory is installed that delegates to the original but adds listeners for the table-row and tree-item properties. When the TreeItem changes, we get the row-data and see if we can access the desired property (via a PropertyReference that is cached for performance). If we can't (and we get the two exceptions) we assume that the property cannot be accessed and we set the cell's editable-property to false.
public <S, T> void disableUnavailableCells(TreeTableColumn<S, T> treeTableColumn) {
Callback<TreeTableColumn<S, T>, TreeTableCell<S, T>> cellFactory = treeTableColumn.getCellFactory();
Callback<CellDataFeatures<S, T>, ObservableValue<T>> cellValueFactory = treeTableColumn.getCellValueFactory();
if (cellValueFactory instanceof TreeItemPropertyValueFactory) {
TreeItemPropertyValueFactory<S, T> valueFactory = (TreeItemPropertyValueFactory<S, T>)cellValueFactory;
String property = valueFactory.getProperty();
Map<Class<?>, PropertyReference<T>> propertyRefCache = new HashMap<>();
treeTableColumn.setCellFactory(column -> {
TreeTableCell<S, T> cell = cellFactory.call(column);
cell.tableRowProperty().addListener((o1, oldRow, newRow) -> {
if (newRow != null) {
newRow.treeItemProperty().addListener((o2, oldTreeItem, newTreeItem) -> {
if (newTreeItem != null) {
S rowData = newTreeItem.getValue();
if (rowData != null) {
Class<?> rowType = rowData.getClass();
PropertyReference<T> reference = propertyRefCache.get(rowType);
if (reference == null) {
reference = new PropertyReference<>(rowType, property);
propertyRefCache.put(rowType, reference);
}
try {
reference.getProperty(rowData);
} catch (IllegalStateException e1) {
try {
reference.get(rowData);
} catch (IllegalStateException e2) {
cell.setEditable(false);
}
}
}
}
});
}
});
return cell;
});
}
}
For the example listed in the question, you can call it after you created all your columns as:
...
columns.forEach(this::disableUnavailableCells);
TreeItem<Object> rootItem = new TreeItem<>();
treeTableView.setRoot(rootItem);
treeTableView.setShowRoot(false);
...
You'll see that cells for the Age and Breed columns are now uneditable for Person entries whereas cells for the Surname column is now uneditable for Dog entries, which is what we want. Cells for the common Name column is editable for all entries as this is a common property among Person and Dog objects.

Adding values to selected javafx ComboBoxTableCell dynamically

I have a TableView with two columns named Product and Brand. A Product can be of different Brands. Eg. TV has different brands like Samsung, Sony etc. I am showing Brands of a Product in ComboBoxes.
This is how i am adding a ComboBoxTableCell for my Brand Column.
ObservableList<String> catList = FXCollections.observableArrayList();
categoryCol.setCellFactory(t -> {
ComboBoxTableCell comboCell = new ComboBoxTableCell(catList);
return comboCell;
});
contactTable.getColumns().add(categoryCol);
Now in these ComboBoxes i want to add values of Brands. Since There will be different Products so their Brands will be different also. Eg.
Product | Model
----------------------------------------------
TV | ComboBox[Samsung, Sony, Panasonic]
Monitor | ComboBox[Dell, Microsoft, Apple ]
Now since the ComboBoxes have same data model (ObservableList) how can i add different values to them by selecting the items in table. Is it possible to do ? Thanks in advance for any help.
First of all you need a custom row class in which you store the elements,
then you have to #Override the startEdit() from theComboBoxTreeTableCell for example this way :
#Override public void startEdit() {
MyCustomRow currentRow = getTableRow().getItem();
getItems().setAll(currentRow.getModels());
super.startEdit();
}
}
MyCustomRow:
package mypackage;
import javafx.beans.property.SimpleStringProperty;
import java.util.List;
import java.util.Map;
public class MyCustomRow {
private SimpleStringProperty product;
private SimpleStringProperty model;
private List<String> allModels;
public MyCustomRow(
String product,
String model,
List<String> models) {
this.product = new SimpleStringProperty(product);
this.model = new SimpleStringProperty(product);
this.allModels = models;
}
public String getProduct() {
return product.get();
}
public SimpleStringProperty productProperty() {
return product;
}
public String getModel() {
return model.get();
}
public SimpleStringProperty modelProperty() {
return model;
}
public List<String> getModels() {
return allModels;
}
}
Then in your contoller class you can say:
ObservableList<String> carList = FXCollections.observableArrayList();
categoryCol.setCellFactory(t -> new ComboBoxTableCell(carList){
#Override public void startEdit() {
MyCustomRow currentRow = getTableRow().getItem();
getItems().setAll(currentRow.getModels());
super.startEdit();
}
});
categoryCol.setCellValueFactory(v -> v.getValue().modelProperty());
contactTable.getColumns().add(categoryCol);
So each row you add the appropriate models. So in the ComboBox you will have only those items(models) which belong to the product

JavaFX FilteredList, filtering based on property of items in the list

I have a case where I need to filter a ObservableList<Item> based on some properties of the items (i.e. the condition is internal and not external). I found out that javafx has FilteredList so I tried it. I could set the predicate and filtering works, until the property value that determines the filtering changes. Setting the predicate is done now like following:
list.setPredicate(t -> !t.filteredProperty().get())
Since the predicate returns boolean and not BooleanProperty, the changes to that property are not reflected on the list.
Is there any easy solution to this? I could try to do some workarounds, e.g. create a separate list and sync that, or reset the predicate every time the property changes in one item hopefully retriggering the filtering, but I first wanted to ask in case someone knows a pretty solution as those workarounds certainly are not.
Create the underlying list with an extractor. This will enable the underlying list to fire update events when the filteredProperty() of any elements change. The FilteredList will observe these events and so will update accordingly:
ObservableList<Item> baseList = FXCollections.observableArrayList(item ->
new Observable[] {item.filteredProperty()});
FilteredList<Item> list = new FilteredList<>(baseList, t -> ! t.filteredProperty().get());
Quick demo:
import java.util.stream.IntStream;
import javafx.beans.Observable;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener.Change;
import javafx.collections.ObservableList;
import javafx.collections.transformation.FilteredList;
public class DynamicFilteredListTest {
public static void main(String[] args) {
ObservableList<Item> baseList = FXCollections.observableArrayList(item ->
new Observable[] {item.filteredProperty()});
FilteredList<Item> list = new FilteredList<>(baseList, t -> ! t.isFiltered());
list.addListener((Change<? extends Item> c) -> {
while (c.next()) {
if (c.wasAdded()) {
System.out.println(c.getAddedSubList()+ " added to filtered list");
}
if (c.wasRemoved()) {
System.out.println(c.getRemoved()+ " removed from filtered list");
}
}
});
System.out.println("Adding ten items to base list:\n");
IntStream.rangeClosed(1, 10).mapToObj(i -> new Item("Item "+i)).forEach(baseList::add);
System.out.println("\nFiltered list now:\n"+list);
System.out.println("\nSetting filtered flag for alternate items in base list:\n");
IntStream.range(0, 5).map(i -> 2*i).mapToObj(baseList::get).forEach(i -> i.setFiltered(true));
System.out.println("\nFiltered list now:\n"+list);
}
public static class Item {
private final StringProperty name = new SimpleStringProperty() ;
private final BooleanProperty filtered = new SimpleBooleanProperty() ;
public Item(String name) {
setName(name);
}
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 BooleanProperty filteredProperty() {
return this.filtered;
}
public final boolean isFiltered() {
return this.filteredProperty().get();
}
public final void setFiltered(final boolean filtered) {
this.filteredProperty().set(filtered);
}
#Override
public String toString() {
return getName();
}
}
}
If you are using database loading function plus need to filter multiple fields, this solution will help.
ObservableList<PurchaseOrder> poData = FXCollections.observableArrayList();
FilteredList<PurchaseOrder> filteredData;
private void load() {
PurchaseOrder po = new PurchaseOrder();
try {
poData = po.loadTable("purchase_orders", beanFields); // Database loading data
} catch (SQLException ex) {
Logger.getLogger(PurchaseOrdersController.class.getName()).log(Level.SEVERE, null, ex);
}
filteredData = new FilteredList<>(poData, t -> true); //Encapsulate data with filter
poTable.setItems(filteredData); //Load filtered data into table
//Set event trigger for all filter textboxes
txtFilter.textProperty().addListener(obs->{
filter(filteredData);
});
txtFilter2.textProperty().addListener(obs->{
filter(filteredData);
});
}
private void filter(FilteredList<PurchaseOrder> filteredData)
{
filteredData.setPredicate(PurchaseOrder -> {
// If all filters are empty then display all Purchase Orders
if ((txtFilter.getText() == null || txtFilter.getText().isEmpty())
&& (txtFilter2.getText() == null || txtFilter2.getText().isEmpty())) {
return true;
}
// Convert filters to lower case
String lowerCaseFilter = txtFilter.getText().toLowerCase();
String lowerCaseFilter2 = txtFilter2.getText().toLowerCase();
//If fails any given criteria, fail completely
if(txtFilter.getText().length()>0)
if (PurchaseOrder.get("vendor_name").toLowerCase().contains(lowerCaseFilter) == false)
return false;
if(txtFilter2.getText().length()>0)
if (PurchaseOrder.get("PONumber").toLowerCase().contains(lowerCaseFilter2) == false)
return false;
return true; // Matches given criteria
});
}

How to make TableCell editable , so it automatically updates the data class?

I am making a system for a school project , and one part of it is a TableView that is populated with rows using my own data class InventoryData that has properties correspondent to the table columns. I would like to make cells in some columns editable using a TextField, so that when an edit is committed, it will update the InventoryData object's relevant property.
I tried setting TextFieldTableCell.forTableColumn() as the cell factory of the columns. Although, now after committing the edit, the text in the cell will change, I don't think it is changing the property in the InventoryData object. The reason why I think that, is because when I try to edit that cell again ( after already being edited once), the TextField shows the former value ( before the first edit).
Did I do something wrong , or is that normal behavior and I have to implement the commits myself?
Here's the code for InventoryData :
package UILayer.TableData;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleStringProperty;
import ModelLayer.Product;
public class InventoryData {
// From Product
private Product productObj;
private SimpleIntegerProperty id;
private SimpleStringProperty name;
// Constructor - converts Product obj into InventoryData
public InventoryData(Product product)
{
this.productObj = product;
this.id = new SimpleIntegerProperty(product.getId());
this.name = new SimpleStringProperty(product.getName())
}
// GET & SET
public Product getProduct()
{
return productObj;
}
public int getId() {
return id.get();
}
public void setId(int id) {
this.id.set(id);
}
public String getName() {
return name.get();
}
public void setName(String name) {
this.name.set(name);
productObj.setName(name);
System.out.println(productObj.getName());
}
}
You need your InventoryData class to use the JavaFX Properties pattern. Specifically it needs property-type accessor methods in order to retrieve the property in the table cells. Without this, the cell value factory just calls the standard getName() or getId() method, and wraps the result in a ReadOnlyStringWrapper (or ReadOnlyIntegerWrapper): the table cell cannot change the values of those wrappers (since they are read only).
public class InventoryData {
// From Product
private Product productObj;
private IntegerProperty id;
private StringProperty name;
// Constructor - converts Product obj into InventoryData
public InventoryData(Product product)
{
this.productObj = product;
this.id = new SimpleIntegerProperty(product.getId());
this.name = new SimpleStringProperty(product.getName())
this.name.addListener((obs, oldName, newName) ->
productObj.setName(newName));
}
// GET & SET
public Product getProduct()
{
return productObj;
}
public IntegerProperty idProperty() {
return id ;
}
public final int getId() {
return idProperty().get();
}
public final void setId(int id) {
idProperty().set(id);
}
public StringProperty nameProperty() {
return name ;
}
public final String getName() {
return nameProperty().get();
}
public final void setName(String name) {
this.nameProperty().set(name);
// productObj.setName(name);
// System.out.println(productObj.getName());
}
}

Resources