Javafx combobox with checkbox - javafx

I want to know how to change the selectionmodel of javafxml combobox so that it can allow multiple seletion. I try to create a JavaFX ComboBox with CheckBoxes in the dropdown menu. My code:
public void alLStatus() throws SQLException {
allStatus.clear();
cb_statuItem.getItems().clear();
DbManager test = new DbManager();
String sql = "SELECT allStatus FROM Items ;";
Statement stmt = test.connect().createStatement();
ResultSet rs = stmt.executeQuery(sql);
while (rs.next()) {
Items a = new Items();
a.setStatusItems(rs.getString("statusItems"));
allStatus.add(a);
}
test.disconnect();
cb_statuItem.setItems(allStatus);
cb_statuItem.setConverter(new StringConverter<Items>() {
#Override
public String toString(Items object) {
// //System.out.print("converting object: ");
if (object == null) {
// //System.out.println("null");
return "[none]";
}
// //System.out.println(object.toString());
return object.getStatusItems();
}
#Override
public Items fromString(String string) {
throw new RuntimeException("not required for non editable ComboBox");
}
});
cb_statuItem.setCellFactory(new Callback<ListView<Items>, ListCell<Items>>() {
#Override
public ListCell<Items> call(ListView<Items> param) {
return new ListCell<Items>() {
private CheckBox cb = new CheckBox();
private BooleanProperty booleanProperty;
{
cb.setOnAction(e->getListView().getSelectionModel().select(getItem()));
}
#Override
protected void updateItem(Items item, boolean empty) {
super.updateItem(item, empty);
if (!empty) {
booleanProperty = item.selectedProperty();
cb.selectedProperty().bindBidirectional(booleanProperty);
setGraphic(cb);
setText(item.getStatusItems() + "");
} else {
setGraphic(null);
setText(null);
}
}
};
}
});
}
ComboBox show text like status of Items but don't allow to click on checkbox, it's automatically close.
I want to check more items from comboBox and get them in list. BooleanProperty is always return false.
Any contribution will be appreciated thanks.

Take a look at controlsfx, it's a library for JavaFX components, and has a component called
CheckComboBox, which seems to be exactly what you need.

Here's the example sample code for the problem which can give some new idea. Just an example how Checkbox can be added to Combobox.
import org.controlsfx.control.CheckComboBox;
import javafx.collections.ObservableList;
public class ComboUI extends BaseUI implements Initializable {
#FXML
private CheckComboBox selector;
#FXML
private ComboBox<String> subAcc;
private List<String> filelist;
//These two functions: print_valuefromcheckcombobox & showDropdownValue added to
//print and add elements in check-combobox list.
public void print_valuefromcheckcombobox() {
ObservableList list = selector.getCheckModel().getCheckedItems();
this.filelist= list;
this.filelist.forEach(name -> {
System.out.println(name);
})
}
public void showDropdownValue () {
//Show Dropdown value: Adding elements in Comboobox
List<String> fileNameList = new ArrayList<>();
fileNameList.add("First");
fileNameList.add("Second");
selector.getItems().setAll(fileNameList);
}
In fxml file, Add where you want to put your combobox. (Just an example..)
<?import org.controlsfx.control.CheckComboBox?>
<ComboBox fx:id="subAcc" layoutX="136.0" layoutY="48.0" onAction="#showDropdownValue" prefWidth="150.0" promptText="Select Sub-Account" />
<CheckComboBox fx:id="selector" layoutX="157.0" layoutY="25.0" prefHeight="25.0" prefWidth="220.0" />
For Maven Project, Add following dependency.
<dependency>
<groupId>org.controlsfx</groupId>
<artifactId>controlsfx</artifactId>
<version>8.40.14</version>
</dependency>
<dependency>
<groupId>org.controlsfx</groupId>
<artifactId>controlsfx-samples</artifactId>
<version>8.40.14</version>
</dependency>
<dependency>
<groupId>org.controlsfx</groupId>
<artifactId>fxsampler</artifactId>
<version>1.0.10</version>
</dependency>

Related

Bind CheckBoxTableCell to BooleanBinding

I want to bind a CheckBox in a TableViewCell to a BooleanBinding. The following sample consists of a TableView with a column name and isEffectiveRequired. The checkbox in the column is bound to the Expression:
isRequired.or(name.isEqualTo("X"))
So an item is "effectivly required" when the item in the row is required OR the name is an X, then the expression should be true.
Unfortunately the CheckBox does not reflect the change. For debugging I added a textfield, showing the nameProperty, requiredProperty and the computed effectiveRequiredProperty.
Interestingly when returning just the isRequiredProperty instead of the binding the checkbox works.
public ObservableBooleanValue effectiveRequiredProperty() {
// Bindings with this work:
// return isRequired;
// with this not
return isRequired.or(name.isEqualTo(SPECIAL_STRING));
}
So what is the difference between a Property and a ObservableValue in regard to a CheckBox?
public class TableCellCBBinding extends Application {
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage primaryStage) throws Exception {
init(primaryStage);
primaryStage.show();
}
private void init(Stage primaryStage) {
primaryStage.setScene(new Scene(buildContent()));
}
private Parent buildContent() {
TableView<ViewModel> tableView = new TableView<>();
tableView.setItems(sampleEntries());
tableView.setEditable(true);
tableView.getColumns().add(buildRequiredColumn());
tableView.getColumns().add(buildNameColumn());
// Add a Textfield to show the values for the first item
// As soon as the name is set to "X", the effectiveRequiredProperty should evaluate to true and the CheckBox should reflect this but it does not
TextField text = new TextField();
ViewModel firstItem = tableView.getItems().get(0);
text.textProperty()
.bind(Bindings.format("%s | %s | %s", firstItem.nameProperty(), firstItem.isRequiredProperty(), firstItem.effectiveRequiredProperty()));
return new HBox(text, tableView);
}
private TableColumn<ViewModel, String> buildNameColumn() {
TableColumn<ViewModel, String> nameColumn = new TableColumn<>("Name");
nameColumn.setCellValueFactory(new PropertyValueFactory<>("name"));
nameColumn.setCellFactory(TextFieldTableCell.forTableColumn());
nameColumn.setEditable(true);
return nameColumn;
}
private TableColumn<ViewModel, Boolean> buildRequiredColumn() {
TableColumn<ViewModel, Boolean> requiredColumn = new TableColumn<>("isEffectiveRequired");
requiredColumn.setMinWidth(50);
// This is should bind my BindingExpression from to ViewModel to the CheckBox
requiredColumn.setCellValueFactory( p -> p.getValue().effectiveRequiredProperty());
requiredColumn.setCellFactory( CheckBoxTableCell.forTableColumn(requiredColumn));
return requiredColumn;
}
private ObservableList<ViewModel> sampleEntries() {
return FXCollections.observableArrayList(
new ViewModel(false, "A"),
new ViewModel(true, "B"),
new ViewModel(false, "C"),
new ViewModel(true, "D"),
new ViewModel(false, "E"));
}
public static class ViewModel {
public static final String SPECIAL_STRING = "X";
private final StringProperty name;
private final BooleanProperty isRequired;
public ViewModel(boolean isRequired, String name) {
this.name = new SimpleStringProperty(this, "name", name);
this.isRequired = new SimpleBooleanProperty(this, "isRequired", isRequired);
this.name.addListener((observable, oldValue, newValue) -> System.out.println(newValue));
}
public StringProperty nameProperty() {return name;}
public final String getName(){return name.get();}
public final void setName(String value){
name.set(value);}
public boolean isRequired() {
return isRequired.get();
}
public BooleanProperty isRequiredProperty() {
return isRequired;
}
public void setRequired(final boolean required) {
this.isRequired.set(required);
}
public ObservableBooleanValue effectiveRequiredProperty() {
// Bindings with this work:
// return isRequired;
// with this not
return isRequired.or(name.isEqualTo(SPECIAL_STRING));
}
}
}
When typing an X into the name the checkbox in the row should be checked.
When typing an X into the name the checkbox in the row is not checked. It's never checked like it is not bound at all.
CheckBoxXXCells don't live up to their doc when it comes to binding their selected state, f.i. (citing here just for signature, even if not set explicitely):
public final Callback <Integer,​ObservableValue<Boolean>> getSelectedStateCallback()
Returns the Callback that is bound to by the CheckBox shown on screen.
clearly talks about an ObservableValue, so we would expect that it at least shows the selection state.
Actually, the implementation does exactly nothing if it's not a property, the relevant part from its updateItem:
StringConverter<T> c = getConverter();
if (showLabel) {
setText(c.toString(item));
}
setGraphic(checkBox);
if (booleanProperty instanceof BooleanProperty) {
checkBox.selectedProperty().unbindBidirectional((BooleanProperty)booleanProperty);
}
ObservableValue<?> obsValue = getSelectedProperty();
if (obsValue instanceof BooleanProperty) {
booleanProperty = (ObservableValue<Boolean>) obsValue;
checkBox.selectedProperty().bindBidirectional((BooleanProperty)booleanProperty);
}
checkBox.disableProperty().bind(Bindings.not(
getTableView().editableProperty().and(
getTableColumn().editableProperty()).and(
editableProperty())
));
To work around, use a custom cell that updates the selected state in its updateItem. With the added quirk that we need to disable the check's firing to really keep the visuals in sync with backing state:
requiredColumn.setCellFactory(cc -> {
TableCell<ViewModel, Boolean> cell = new TableCell<>() {
CheckBox check = new CheckBox() {
#Override
public void fire() {
// do nothing - visualizing read-only property
// could do better, like actually changing the table's
// selection
}
};
{
getStyleClass().add("check-box-table-cell");
check.setOnAction(e -> {
e.consume();
});
}
#Override
protected void updateItem(Boolean item, boolean empty) {
super.updateItem(item, empty);
if (empty || item == null) {
setText(null);
setGraphic(null);
} else {
check.setSelected(item);
setGraphic(check);
}
}
};
return cell;
});

Interacting with custom CellFactory node adds row to TableView selection list?

I have a TableView with a CellFactory that places a ComboBox into one of the columns. The TableView has SelectionMode.MULTIPLE enabled but it is acting odd with the ComboBox cell.
When the users clicks on the ComboBox to select a value, that row is added to the list of selected rows. Instead, clicking on the ComboBox should either select that row and deselect all others (unless CTRL is being held), or it should not select the row at all, but only allow for interaction with the ComboBox.
I am not sure how to achieve this.
Here is a complete example to demonstrate the issue:
import javafx.application.Application;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import javafx.util.StringConverter;
enum Manufacturer {
HP, DELL, LENOVO, ASUS, ACER;
}
public class TableViewSelectionIssue extends Application {
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage primaryStage) {
// Simple Interface
VBox root = new VBox(10);
root.setAlignment(Pos.CENTER);
root.setPadding(new Insets(10));
// Simple TableView
TableView<ComputerPart> tableView = new TableView<>();
TableColumn<ComputerPart, Manufacturer> colManufacturer = new TableColumn<>("Manufacturer");
TableColumn<ComputerPart, String> colItem = new TableColumn<>("Item");
tableView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
colManufacturer.setCellValueFactory(t -> t.getValue().manufacturerProperty());
colItem.setCellValueFactory(t -> t.getValue().itemNameProperty());
tableView.getColumns().addAll(colManufacturer, colItem);
// CellFactory to display ComboBox in colManufacturer
colManufacturer.setCellFactory(param -> new ManufacturerTableCell(colManufacturer, FXCollections.observableArrayList(Manufacturer.values())));
// Add sample items
tableView.getItems().addAll(
new ComputerPart("Keyboard"),
new ComputerPart("Mouse"),
new ComputerPart("Monitor"),
new ComputerPart("Motherboard"),
new ComputerPart("Hard Drive")
);
root.getChildren().add(tableView);
// Show the stage
primaryStage.setScene(new Scene(root));
primaryStage.setTitle("Sample");
primaryStage.show();
}
}
class ComputerPart {
private final ObjectProperty<Manufacturer> manufacturer = new SimpleObjectProperty<>();
private final StringProperty itemName = new SimpleStringProperty();
public ComputerPart(String itemName) {
this.itemName.set(itemName);
}
public Manufacturer getManufacturer() {
return manufacturer.get();
}
public void setManufacturer(Manufacturer manufacturer) {
this.manufacturer.set(manufacturer);
}
public ObjectProperty<Manufacturer> manufacturerProperty() {
return manufacturer;
}
public String getItemName() {
return itemName.get();
}
public void setItemName(String itemName) {
this.itemName.set(itemName);
}
public StringProperty itemNameProperty() {
return itemName;
}
}
class ManufacturerTableCell extends TableCell<ComputerPart, Manufacturer> {
private final ComboBox<Manufacturer> cboStatus;
ManufacturerTableCell(TableColumn<ComputerPart, Manufacturer> column, ObservableList<Manufacturer> items) {
this.cboStatus = new ComboBox<>();
this.cboStatus.setItems(items);
this.cboStatus.setConverter(new StringConverter<Manufacturer>() {
#Override
public String toString(Manufacturer object) {
return object.name();
}
#Override
public Manufacturer fromString(String string) {
return null;
}
});
this.cboStatus.disableProperty().bind(column.editableProperty().not());
this.cboStatus.setOnShowing(event -> {
final TableView<ComputerPart> tableView = getTableView();
tableView.getSelectionModel().select(getTableRow().getIndex());
tableView.edit(tableView.getSelectionModel().getSelectedIndex(), column);
});
this.cboStatus.valueProperty().addListener((observable, oldValue, newValue) -> {
if (isEditing()) {
commitEdit(newValue);
column.getTableView().refresh();
}
});
setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
}
#Override
protected void updateItem(Manufacturer item, boolean empty) {
super.updateItem(item, empty);
setText(null);
if (empty) {
setGraphic(null);
} else {
this.cboStatus.setValue(item);
this.setGraphic(this.cboStatus);
}
}
}
The example begins with a predictable UI:
However, when interacting with the ComboBox in the Manufacturer column, the corresponding row is selected. This is expected for the first row, but it does not get deselected when interacting with another ComboBox.
How can I prevent subsequent interactions with a ComboBox from adding to the selected rows? It should behave like any other click on a TableRow, should it not?
I am using JDK 8u161.
Note: I understand there is a ComboBoxTableCell class available, but I've not been able to find any examples of how to use one properly; that is irrelevant to my question, though, unless the ComboBoxTableCell behaves differently.
Since you want an "always editing" cell, your implementation should behave more like CheckBoxTableCell than ComboBoxTableCell. The former bypasses the normal editing mechanism of the TableView. As a guess, I think it's your use of the normal editing mechanism that causes the selection issues—why exactly, I'm not sure.
Modifying your ManufactureTableCell to be more like CheckBoxTableCell, it'd look something like:
class ManufacturerTableCell extends TableCell<ComputerPart, Manufacturer> {
private final ComboBox<Manufacturer> cboStatus;
private final IntFunction<Property<Manufacturer>> extractor;
private Property<Manufacturer> property;
ManufacturerTableCell(IntFunction<Property<Manufacturer>> extractor, ObservableList<Manufacturer> items) {
this.extractor = extractor;
this.cboStatus = new ComboBox<>();
this.cboStatus.setItems(items);
// removed StringConverter for brevity (accidentally)
setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
cboStatus.addEventHandler(MouseEvent.MOUSE_PRESSED, event -> {
if (event.isShortcutDown()) {
getTableView().getSelectionModel().select(getIndex(), getTableColumn());
} else {
getTableView().getSelectionModel().clearAndSelect(getIndex(), getTableColumn());
}
event.consume();
});
}
#Override
protected void updateItem(Manufacturer item, boolean empty) {
super.updateItem(item, empty);
setText(null);
clearProperty();
if (empty) {
setGraphic(null);
} else {
property = extractor.apply(getIndex());
Bindings.bindBidirectional(cboStatus.valueProperty(), property);
setGraphic(cboStatus);
}
}
private void clearProperty() {
setGraphic(null);
if (property != null) {
Bindings.unbindBidirectional(cboStatus.valueProperty(), property);
}
}
}
And you'd install it like so:
// note you could probably share the same ObservableList between all cells
colManufacturer.setCellFactory(param ->
new ManufacturerTableCell(i -> tableView.getItems().get(i).manufacturerProperty(),
FXCollections.observableArrayList(Manufacturer.values())));
As already mentioned, the above implementation bypasses the normal editing mechanism; it ties the value of the ComboBox directly to the model item's property. The implementation also adds a MOUSE_PRESSED handler to the ComboBox that selects the row (or cell if using cell selection) as appropriate. Unfortunately, I'm not quite understanding how to implement selection when Shift is down so only "Press" and "Shortcut+Press" is handled.
The above works how I believe you want it to, but I could only test it out using JavaFX 12.

JavaFx How to make checkbox visible in tableColumnCell using cellFactory

I want to add checkBox to tableCell but i dont know what to do to make checkBox visible when i use .setCellFactory
I want to achive this with cellFactory function ---> checkBoxVisible
when i want to make use of cellFactory checkBox are not visible
----> wrong effect
window1.setCellFactory(new BooleanColorCellFactory());
window2.setCellFactory(new BooleanColorCellFactory());
This is BooleanColorCellFactory Class
#Override
public TableCell<Server, CheckBox> call(TableColumn<Server, CheckBox> param) {
return new TableCell<Server, CheckBox>(){
#Override
protected void updateItem(CheckBox item, boolean empty) {
super.updateItem(item, empty);
// if(!empty) {
// setVisible(true);
// setEditable(true);
// getChildren().add(item);
// setText(item.toString());
// if(item.isSelected())
// setStyle(" -fx-background-color: Green");
// else
// setStyle(" -fx-background-color: RED");
}
// }
};
}
}
I have tried some things but nothing was working.
What i need to add in BooleanColorCellFactory to make this work ?
UPDATE:
So i was playing around and i manage to get step closer to solution by adding this into BooleanColorCellFactory Class
if(!getChildren().contains(item))
getChildren().add(item);
but it is buggy and dont looks well and its added after i start scrolling(which is weird behavior for me)--> click
You shouldn't put a Node inside the item class unless you really need to. Furthermore never access the children of a Control directly unless you're writing a Skin for this Control.
You should instead add a BooleanProperty to the Server class:
private final BooleanProperty window1 = new SimpleBooleanProperty();
public boolean isWindow1() {
return window1.get();
}
public void setWindow1(boolean value) {
window1.set(value);
}
public BooleanProperty window1Property() {
return window1;
}
TableColumn<Server, Boolean> window1;
Callback<TableColumn<Server, Boolean>, TableCell<Server, Boolean>> factory = new BooleanColorCellFactory();
// cellValueFactory returns property
window1.setCellValueFactory(new PropertyValueFactory<>("window1"));
window1.setCellFactory(factory);
...
window2.setCellFactory(factory);
#Override
public TableCell<Server, Boolean> call(TableColumn<Server, Boolean> param) {
return new TableCell<Server, Boolean>(){
private final CheckBox checkBox = new CheckBox();
{
checkBox.selectedProperty().addListener((o, oldValue, newValue) -> {
// save new value in row item
WritableValue<Boolean> property = (WritableValue<Boolean>) getTableColumn().getCellObservableValue​(getIndex());
property.setValue(newValue);
});
}
#Override
protected void updateItem(Boolean item, boolean empty) {
super.updateItem(item, empty);
if (empty || item == null) {
setGraphic(null);
setStyle(null);
} else {
setGraphic(checkBox);
checkBox.setSelected(item);
setStyle(item ? "-fx-background-color: Green" : "-fx-background-color: RED");
}
}
};
}
Procede accordingly for window2

JavaFX Disable TableColumn based on checkbox state

Am looking to disable a TableColumn<CustomObject, String> tableColumn based on a field value in the CustomObject only when the TableColumn<CustomObject, Boolean> tableColumnTwo checkbox is checked. I can disable the textbox inside public void updateItem(String s, boolean empty) however not sure how to check the state of checkbox inside updateItem
Below is the relevant code snippet, would highly appreciate if anyone can shed light on this
#FXML
private TableColumn<CustomObject, Boolean> tableColumnTwo;
#FXML
private TableColumn<CustomObject, String> tableColumn;
tableColumn.setCellFactory(
new Callback<TableColumn<CustomObject, String>, TableCell<CustomObject, String>>() {
#Override
public TableCell<CustomObject, String> call(TableColumn<CustomObject, String> paramTableColumn) {
return new TextFieldTableCell<CustomObject, String>(new DefaultStringConverter()) {
#Override
public void updateItem(String s, boolean empty) {
super.updateItem(s, empty);
TableRow<CustomObject> currentRow = getTableRow();
if(currentRow.getItem() != null && !empty) {
if (currentRow.getItem().getPetrified() == false) { // Need to check if checkbox is checked or not
setDisable(true);
setEditable(false);
this.setStyle("-fx-background-color: red");
} else {
setDisable(false);
setEditable(true);
setStyle("");
}
}
}
};
}
});
You can add a listener on the checkbox, which when checked will cause the table refresh.
data = FXCollections.observableArrayList(new Callback<CustomObject, Observable[]>() {
#Override
public Observable[] call(CustomObject param) {
return new Observable[]{param.petrifiedProperty()};
}
});
data.addListener(new ListChangeListener<CustomObject>() {
#Override
public void onChanged(ListChangeListener.Change<? extends CustomObject> c) {
while (c.next()) {
if (c.wasUpdated()) {
tableView.setItems(null);
tableView.layout();
tableView.setItems(FXCollections.observableList(data));
}
}
}
});
Your cellFactory would remain the same and would get called when a checkbox is checked/unchecked.
Usually, we expect cells being updated whenever they are notified about a change in the underlying data. To make certain that a notification is fired by the data on changing a property of an item, we need a list with an extractor on the properties that we are interested in, something like:
ObservableList<CustomObject> data = FXCollections.observableArrayList(
c -> new Observable[] {c.petrifiedProperty()}
);
With that in place the list fires a list change of type update whenever the pretified property changes.
Unfortunately, that's not enough due to a bug in fx: cells are not updated when receiving a listChange of type update from the underlying items. A dirty way around (read: don't use once the bug is fixed, it's using emergency api!) is to install a listener on the items and call table.refresh() when receiving an update.
An example:
import java.util.logging.Logger;
//import de.swingempire.fx.util.FXUtils;
import javafx.application.Application;
import javafx.beans.Observable;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.scene.Scene;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableRow;
import javafx.scene.control.TableView;
import javafx.scene.control.cell.CheckBoxTableCell;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.scene.control.cell.TextFieldTableCell;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
import javafx.util.converter.DefaultStringConverter;
/**
* CheckBoxTableCell: update editable state of one column based of
* the boolean in another column
* https://stackoverflow.com/q/46290417/203657
*
* Bug in skins: cell not updated on listChange.wasUpdated
*
* reported as
* https://bugs.openjdk.java.net/browse/JDK-8187665
*/
#SuppressWarnings({ "rawtypes", "unchecked" })
public class TableViewUpdateBug extends Application {
/**
* TableCell that updates state based on another value in the row.
*/
public static class DisableTextFieldTableCel extends TextFieldTableCell {
public DisableTextFieldTableCel() {
super(new DefaultStringConverter());
}
/**
* Just to see whether or not this is called on update notification
* from the items (it's not)
*/
#Override
public void updateIndex(int index) {
super.updateIndex(index);
// LOG.info("called? " + index);
}
/**
* Implemented to change background based on
* visible property of row item.
*/
#Override
public void updateItem(Object item, boolean empty) {
super.updateItem(item, empty);
TableRow<TableColumn> currentRow = getTableRow();
boolean editable = false;
if (!empty && currentRow != null) {
TableColumn column = currentRow.getItem();
if (column != null) {
editable = column.isVisible();
}
}
if (!empty) {
setDisable(!editable);
setEditable(editable);
if (editable) {
this.setStyle("-fx-background-color: red");
} else {
this.setStyle("-fx-background-color: green");
}
} else {
setStyle("-fx-background-color: null");
}
}
}
#Override
public void start(Stage primaryStage) {
// data: list of tableColumns with extractor on visible property
ObservableList<TableColumn> data = FXCollections.observableArrayList(
c -> new Observable[] {c.visibleProperty()});
data.addAll(new TableColumn("first"), new TableColumn("second"));
TableView<TableColumn> table = new TableView<>(data);
table.setEditable(true);
// hack-around: call refresh
data.addListener((ListChangeListener) c -> {
boolean wasUpdated = false;
boolean otherChange = false;
while(c.next()) {
if (c.wasUpdated()) {
wasUpdated = true;
} else {
otherChange = true;
}
}
if (wasUpdated && !otherChange) {
table.refresh();
}
//FXUtils.prettyPrint(c);
});
TableColumn<TableColumn, String> text = new TableColumn<>("Text");
text.setCellFactory(c -> new DisableTextFieldTableCel());
text.setCellValueFactory(new PropertyValueFactory<>("text"));
TableColumn<TableColumn, Boolean> visible = new TableColumn<>("Visible");
visible.setCellValueFactory(new PropertyValueFactory<>("visible"));
visible.setCellFactory(CheckBoxTableCell.forTableColumn(visible));
table.getColumns().addAll(text, visible);
BorderPane root = new BorderPane(table);
Scene scene = new Scene(root, 300, 150);
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
#SuppressWarnings("unused")
private static final Logger LOG = Logger
.getLogger(TableViewUpdateBug.class.getName());
}

FXML Dynamically initialize ObservableList for ComboBox and TableView

I am trying to make a custom builder proposed in Dan Nicks's comment to this question.
The idea is to set combo's data before constructing it.
combo.fxml:
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.ComboBox?>
<ComboBox fx:id="combo1" items="${itemLoader.items}" prefWidth="150.0"
xmlns:fx="http://javafx.com/fxml/1">
</ComboBox>
The class that provides the data:
public class ComboLoader {
public ObservableList<Item> items;
public ComboLoader() {
items = FXCollections.observableArrayList(createItems());
}
private List<Item> createItems() {
return IntStream.rangeClosed(0, 5)
.mapToObj(i -> "Item "+i)
.map(Item::new)
.collect(Collectors.toList());
}
public ObservableList<Item> getItems(){
return items;
}
public static class Item {
private final StringProperty name = new SimpleStringProperty();
public Item(String name) {
this.name.set(name);
}
public final StringProperty nameProperty() {
return name;
}
}
}
And the test:
public class ComboTest extends Application {
#Override
public void start(Stage primaryStage) throws IOException {
primaryStage.setTitle("Populate combo from custom builder");
Group group = new Group();
GridPane grid = new GridPane();
grid.setPadding(new Insets(25, 25, 25, 25));
group.getChildren().add(grid);
FXMLLoader loader = new FXMLLoader();
ComboBox combo = loader.load(getClass().getResource("combo.fxml"));
loader.getNamespace().put("itemLoader", new ComboLoader());
grid.add(combo, 0, 0);
Scene scene = new Scene(group, 450, 175);
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
No errors produced, but combo is not populated.
What is missing ?
BTW: a similar solution for TableView works fine:
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.TableView?>
<?import javafx.scene.control.TableColumn?>
<?import javafx.scene.control.cell.PropertyValueFactory?>
<TableView items="${itemLoader.items}" xmlns:fx="http://javafx.com/fxml/1">
<columns>
<TableColumn text="Item">
<cellValueFactory><PropertyValueFactory property="name" /></cellValueFactory>
</TableColumn>
</columns>
</TableView>
Starting from a nit-pick, I did some experiments on how to actually implement what I tried to outline in my comments to c0der's answer.
The basic idea is to follow the same approach for the listCell as for the data, that is configure both content and appearance via namespace (my learn item of the day). The ingredients:
a generic custom listCell configurable with a function to convert an item to text
a generic "cellFactory factory" class for providing a cellFactory creating that cell
The cell/factory:
public class ListCellFactory<T> {
private Function<T, String> textProvider;
public ListCellFactory(Function<T, String> provider) {
this.textProvider = provider;
}
public Callback<ListView<T>, ListCell<T>> getCellFactory() {
return cc -> new CListCell<>(textProvider);
}
public ListCell<T> getButtonCell() {
return getCellFactory().call(null);
}
public static class CListCell<T> extends ListCell<T> {
private Function<T, String> converter;
public CListCell(Function<T, String> converter) {
this.converter = Objects.requireNonNull(converter, "converter must not be null");
}
#Override
protected void updateItem(T item, boolean empty) {
super.updateItem(item, empty);
if (empty) {
setText(null);
} else {
setText(converter.apply(item));
}
}
}
}
The fxml to create and configure the combo:
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.ComboBox?>
<ComboBox fx:id="combo1" items="${itemLoader.items}"
cellFactory="${cellFactoryProvider.cellFactory}"
buttonCell = "${cellFactoryProvider.buttonCell}"
prefWidth="150.0"
xmlns:fx="http://javafx.com/fxml/1">
</ComboBox>
An example to use it:
public class LocaleLoaderApp extends Application {
private ComboBox<Locale> loadCombo(Object itemLoader, Function<Locale, String> extractor) throws IOException {
FXMLLoader loader = new FXMLLoader(getClass().getResource("comboloader.fxml"));
loader.getNamespace().put("itemLoader", itemLoader);
loader.getNamespace().put("cellFactoryProvider", new ListCellFactory<Locale>(extractor));
ComboBox<Locale> combo = loader.load();
return combo;
}
#Override
public void start(Stage primaryStage) throws IOException {
primaryStage.setTitle("Populate combo from custom builder");
Group group = new Group();
GridPane grid = new GridPane();
grid.setPadding(new Insets(25, 25, 25, 25));
group.getChildren().add(grid);
LocaleProvider provider = new LocaleProvider();
grid.add(loadCombo(provider, Locale::getDisplayName), 0, 0);
grid.add(loadCombo(provider, Locale::getLanguage), 1, 0);
Scene scene = new Scene(group, 450, 175);
primaryStage.setScene(scene);
primaryStage.show();
}
public static class LocaleProvider {
ObservableList<Locale> locales = FXCollections.observableArrayList(Locale.getAvailableLocales());
public ObservableList<Locale> getItems() {
return locales;
}
}
public static void main(String[] args) {
launch(args);
}
}
Edited following comments by kleopatra:
Loading combo.fxml given in the question with Strings can be done using the following loader:
//load observable list with strings
public class ComboStringLoader {
private final ObservableList<String> items;
public ComboStringLoader() {
items = FXCollections.observableArrayList(createStrings());
}
private List<String> createStrings() {
return IntStream.rangeClosed(0, 5)
.mapToObj(i -> "String "+i)
.map(String::new)
.collect(Collectors.toList());
}
//name of this method corresponds to itemLoader.items in xml.
//if xml name was itemLoader.a this method should have been getA().
public ObservableList<String> getItems(){
return items;
}
}
Loading combo with Item instances in a similar fashion simply means that Item#toString for the text in the combo:
//load observable list with Item#toString
public class ComboObjectLoader1 {
public ObservableList<Item> items;
public ComboObjectLoader1() {
items = FXCollections.observableArrayList(createItems());
}
private List<Item> createItems() {
return IntStream.rangeClosed(0, 5)
.mapToObj(i -> "Item "+i)
.map(Item::new)
.collect(Collectors.toList());
}
public ObservableList<Item> getItems(){
return items;
}
}
Where Item is defined as:
class Item {
private final StringProperty name = new SimpleStringProperty();
public Item(String name) {
this.name.set(name);
}
public final StringProperty nameProperty() {
return name;
}
#Override
public String toString() {
return name.getValue();
}
}
A better approach is to load combo with custom ListCell<item>:
//load observable list with custom ListCell
public class ComboObjectLoader2 {
private final ObservableList<ItemListCell> items;
public ComboObjectLoader2() {
items =FXCollections.observableArrayList (createCells());
}
private List<ItemListCell> createCells() {
return IntStream.rangeClosed(0, 5)
.mapToObj(i -> "Item "+i)
.map(Item::new)
.map(ItemListCell::new)
.collect(Collectors.toList());
}
public ObservableList<ItemListCell> getItems(){
return items;
}
}
class ItemListCell extends ListCell<Item> {
private final Label text;
public ItemListCell(Item item) {
text = new Label(item.nameProperty().get());
setGraphic(new Pane(text));
}
#Override
public void updateItem(Item item, boolean empty) {
super.updateItem(item, empty);
if (empty) {
setText(null);
setGraphic(null);
} else {
text.setText(item.nameProperty().get());
}
}
}
The last but not least alternative is to set a custom ListCell<Item> as a cell factory for the combo.
This can be done by adding a controller to the fxml file:
combo2.fxml:
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.ComboBox?>
<ComboBox fx:id="combo1" items="${itemLoader.items}" prefWidth="150.0" xmlns:fx="http://javafx.com/fxml/1"
xmlns="http://javafx.com/javafx/10.0.1" fx:controller="test.ComboObjectLoaderAndController">
</ComboBox>
Where ComboObjectLoaderAndController is both a loader and a controller:
//loads observable list with Items and serves as controller to set cell factory
public class ComboObjectLoaderAndController {
public ObservableList<Item> items;
#FXML ComboBox<Item> combo1;
public ComboObjectLoaderAndController() {
items = FXCollections.observableArrayList(createItems());
}
#FXML
public void initialize() {
combo1.setCellFactory(l->new ItemListCell());
}
private List<Item> createItems() {
return IntStream.rangeClosed(0, 5)
.mapToObj(i -> "Item "+i)
.map(Item::new)
.collect(Collectors.toList());
}
public ObservableList<Item> getItems(){
return items;
}
class ItemListCell extends ListCell<Item>{
#Override
public void updateItem(Item item, boolean empty) {
super.updateItem(item, empty);
if (empty) {
setText(null);
setGraphic(null);
} else {
setText(item.nameProperty().get());
}
}
}
}
Edit:
following kleopatra's answer I added a generic custom ListCell
public class ObjectListCell<T> extends ListCell<T> {
Function<T,String> textSupplier;
public ObjectListCell(Function<T,String> textSupplier) {
this.textSupplier = textSupplier;
}
public Callback<ListView<T>, ListCell<T>> getFactory() {
return cc -> new ObjectListCell<>(textSupplier);
}
public ListCell<T> getButtonCell() {
return getFactory().call(null);
}
#Override
public void updateItem(T t, boolean empty) {
super.updateItem(t, empty);
if (t== null || empty) {
setText(null);
setGraphic(null);
} else {
setText(textSupplier.apply(t));
}
}
}
The factory is set in the fxml file:
combo3.fxml:
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.ComboBox?>
<ComboBox fx:id="combo1" items="${itemLoader.items}" cellFactory="${cellFactoryProvider.factory}"
buttonCell = "${cellFactoryProvider.buttonCell}"
prefWidth="150.0" xmlns:fx="http://javafx.com/fxml/1" xmlns="http://javafx.com/javafx/10.0.1">
</ComboBox>
A test class:
public class ComboTest extends Application {
#Override
public void start(Stage primaryStage) throws IOException {
primaryStage.setTitle("Populate combo from custom builder");
//Combo of Strings
FXMLLoader loader = new FXMLLoader(getClass().getResource("combo.fxml"));
loader.getNamespace().put("itemLoader", new ComboStringLoader());
ComboBox<String>stringCombo = loader.load();
//Combo of Item
loader = new FXMLLoader(getClass().getResource("combo.fxml"));
loader.getNamespace().put("itemLoader", new ComboObjectLoader1());
ComboBox<Item>objectsCombo1 = loader.load();
//Combo of custom ListCell
loader = new FXMLLoader(getClass().getResource("combo.fxml"));
loader.getNamespace().put("itemLoader", new ComboObjectLoader2());
ComboBox<ItemListCell>objectsCombo2 = loader.load();
//Combo of Item with custom ListCell factory
loader = new FXMLLoader(getClass().getResource("combo2.fxml"));
loader.getNamespace().put("itemLoader", new ComboObjectLoaderAndController());
ComboBox<Item>objectsCombo3 = loader.load();
//Combo of Item with custom ListCell factory. Factory is set in FXML
loader = new FXMLLoader(getClass().getResource("combo3.fxml"));
loader.getNamespace().put("itemLoader", new ComboObjectLoader1());
loader.getNamespace().put("cellFactoryProvider", new ObjectListCell<Item>(t -> t.nameProperty().get()));
ComboBox<Item>objectsCombo4 = loader.load();
HBox pane = new HBox(25, stringCombo, objectsCombo1,objectsCombo2, objectsCombo3, objectsCombo4);
pane.setPadding(new Insets(25, 25, 25, 25));
Scene scene = new Scene(pane, 550, 175);
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}

Resources