JavaFX Boolean Binding Based on Selected Items in a TableView - java

I am attempting to enable a JavaFX Button depending on the aggregate of a property value in the selected rows in a TableView. The following is an example application that demonstrates the problem:
package test;
import java.util.Random;
import javafx.application.Application;
import javafx.beans.binding.Bindings;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.collections.ObservableList;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.MultipleSelectionModel;
import javafx.scene.control.SelectionMode;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
public class Main extends Application {
public static void main(final String[] args) throws Exception {
launch(args);
}
private static class Row {
private final BooleanProperty myProp;
public Row(final boolean value) {
myProp = new SimpleBooleanProperty(value);
}
public BooleanProperty propProperty() { return myProp; }
}
#Override
public void start(final Stage window) throws Exception {
// Create a VBox to hold the table and button
final VBox root = new VBox();
root.setMinSize(200, 200);
// Create the table, and enable multi-select
final TableView<Row> table = new TableView<>();
final MultipleSelectionModel<Row> selectionModel = table.getSelectionModel();
selectionModel.setSelectionMode(SelectionMode.MULTIPLE);
root.getChildren().add(table);
// Create a column based on the value of Row.propProperty()
final TableColumn<Row, Boolean> column = new TableColumn<>("Value");
column.setCellValueFactory(p -> p.getValue().propProperty());
table.getColumns().add(column);
// Add a button below the table
final Button button = new Button("Button");
root.getChildren().add(button);
// Populate the table with true/false values
final ObservableList<Row> rows = table.getItems();
rows.addAll(new Row(false), new Row(false), new Row(false));
// Start a thread to randomly modify the row values
final Random rng = new Random();
final Thread thread = new Thread(() -> {
// Flip the value in a randomly selected row every 10 seconds
try {
do {
final int i = rng.nextInt(rows.size());
System.out.println("Flipping row " + i);
Thread.sleep(10000);
final BooleanProperty prop = rows.get(i).propProperty();
prop.set(!prop.get());
} while (true);
} catch (final InterruptedException e) {
System.out.println("Exiting Thread");
}
}, "Row Flipper Thread");
thread.setDaemon(true);
thread.start();
// Bind the button's disable property such that the button
// is only enabled if one of the selected rows is true
final ObservableList<Row> selectedRows = selectionModel.getSelectedItems();
button.disableProperty().bind(Bindings.createBooleanBinding(() -> {
for (int i = 0; i < selectedRows.size(); ++i) {
if (selectedRows.get(i).propProperty().get()) {
return false;
}
}
return true;
}, selectedRows));
// Show the JavaFX window
final Scene scene = new Scene(root);
window.setScene(scene);
window.show();
}
}
To test, start the above application, and select the row indicated by the text "Flipping row N", where N is in [0, 2]. When the value of the selected row changes to true...
Observed Behavior button remains disabled.
Desired Behavior button becomes enabled.
Does anyone know how to create a BooleanBinding that exhibits the desired behavior?

Your binding needs to be invalidated if any of the propPropertys of the selected rows change. Currently the binding is only observing the selected items list, which will fire events when the list contents change (i.e. items become selected or unselected) but not when properties belonging to items in that list change value.
To do this, create a list with an extractor:
final ObservableList<Row> selectedRows =
FXCollections.observableArrayList(r -> new Observable[]{r.propProperty()});
This list will fire events when items are added or removed, or when the propProperty() of any item in the list changes. (If you need to observe multiple values, you can do so by including them in the array of Observables.)
Of course, you still need this list to contain the selected items in the table. You can ensure this by binding the content of the list to the selectedItems of the selection model:
Bindings.bindContent(selectedRows, selectionModel.getSelectedItems());
Here is a version of your MCVE using this:
import java.util.Random;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.beans.Observable;
import javafx.beans.binding.Bindings;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.MultipleSelectionModel;
import javafx.scene.control.SelectionMode;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
public class Main extends Application {
public static void main(final String[] args) throws Exception {
launch(args);
}
private static class Row {
private final BooleanProperty myProp;
public Row(final boolean value) {
myProp = new SimpleBooleanProperty(value);
}
public BooleanProperty propProperty() { return myProp; }
}
#Override
public void start(final Stage window) throws Exception {
// Create a VBox to hold the table and button
final VBox root = new VBox();
root.setMinSize(200, 200);
// Create the table, and enable multi-select
final TableView<Row> table = new TableView<>();
final MultipleSelectionModel<Row> selectionModel = table.getSelectionModel();
selectionModel.setSelectionMode(SelectionMode.MULTIPLE);
root.getChildren().add(table);
// Create a column based on the value of Row.propProperty()
final TableColumn<Row, Boolean> column = new TableColumn<>("Value");
column.setCellValueFactory(p -> p.getValue().propProperty());
table.getColumns().add(column);
// Add a button below the table
final Button button = new Button("Button");
root.getChildren().add(button);
// Populate the table with true/false values
final ObservableList<Row> rows = table.getItems();
rows.addAll(new Row(false), new Row(false), new Row(false));
// Start a thread to randomly modify the row values
final Random rng = new Random();
final Thread thread = new Thread(() -> {
// Flip the value in a randomly selected row every 10 seconds
try {
do {
final int i = rng.nextInt(rows.size());
System.out.println("Flipping row " + i);
Thread.sleep(10000);
final BooleanProperty prop = rows.get(i).propProperty();
Platform.runLater(() -> prop.set(!prop.get()));
} while (true);
} catch (final InterruptedException e) {
System.out.println("Exiting Thread");
}
}, "Row Flipper Thread");
thread.setDaemon(true);
thread.start();
// Bind the button's disable property such that the button
// is only enabled if one of the selected rows is true
final ObservableList<Row> selectedRows =
FXCollections.observableArrayList(r -> new Observable[]{r.propProperty()});
Bindings.bindContent(selectedRows, selectionModel.getSelectedItems());
button.disableProperty().bind(Bindings.createBooleanBinding(() -> {
for (int i = 0; i < selectedRows.size(); ++i) {
if (selectedRows.get(i).propProperty().get()) {
return false;
}
}
return true;
}, selectedRows));
// Show the JavaFX window
final Scene scene = new Scene(root);
window.setScene(scene);
window.show();
}
}

Related

JavaFX TableView with CheckBoxes: retrieve the rows whose checkboxes are checked

I've been searching for a while, but all I found seems very old and can't get it to work and I'm very confused.
I have a tableview with a checkbox in a column header (select all) and another checkbox for each row (select row). What I am trying to achieve is to get all the rows whose checkboxes are checked to perform an action.
Here's what it looks like:
And here's the code in my controller:
package com.comparador.controller;
import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.ResourceBundle;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import com.comparador.ComparadorPreciosApplication;
import com.comparador.entity.Commerce;
import com.comparador.entity.Items;
import com.comparador.entity.ShoppingListPrices;
import com.comparador.repository.CommerceRepository;
import com.comparador.repository.ProductRepository;
import com.comparador.service.ShoppingService;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.fxml.Initializable;
import javafx.scene.Scene;
import javafx.scene.SceneAntialiasing;
import javafx.scene.control.Button;
import javafx.scene.control.CheckBox;
import javafx.scene.control.Label;
import javafx.scene.control.SelectionMode;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableColumn.CellEditEvent;
import javafx.scene.control.TableView;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.scene.control.cell.TextFieldTableCell;
import javafx.stage.Stage;
import javafx.util.converter.IntegerStringConverter;
#Component
public class ShoppingController implements Initializable {
// #Autowired
// #Qualifier("lblTitulo")
private String titulo = "Productos";
#Autowired
private ProductRepository productRepository;
#Autowired
private CommerceRepository commerceRepository;
#Autowired
private ShoppingService shoppingService;
#FXML
private Label lblTitulo;
#FXML
private Button btBack;
#FXML
private TableView<Items> tvProducts;
#FXML
private TableColumn<Items, CheckBox> colSelected; //THE CHECKBOX COLUMN
#FXML
private TableColumn<Items, String> colName;
#FXML
private TableColumn<Items, Integer> colAmount;
#FXML
private TableView<ShoppingListPrices> tvTotalPrices;
#FXML
private TableColumn<ShoppingListPrices, String> colCommerce;
#FXML
private TableColumn<ShoppingListPrices, Double> colTotal;
private CheckBox selectAll;
List<ShoppingListPrices> shoppingList = new ArrayList<>();
#Override
public void initialize(URL location, ResourceBundle resources) {
colName.setCellValueFactory(new PropertyValueFactory<>("name"));
colAmount.setCellValueFactory(new PropertyValueFactory<>("amount"));
colAmount.setCellFactory(TextFieldTableCell.forTableColumn(new IntegerStringConverter()));
// colSelected.setCellFactory(CheckBoxTableCell.forTableColumn(colSelected));
// colSelected.setCellValueFactory(cellData -> new ReadOnlyBooleanWrapper(cellData.getValue().getChecked()));
colSelected.setCellValueFactory(new PropertyValueFactory<>("selected"));
colCommerce.setCellValueFactory(new PropertyValueFactory<>("commerceName"));
colTotal.setCellValueFactory(new PropertyValueFactory<>("total"));
lblTitulo.setText(titulo);
tvProducts.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
reloadTableViewProducts();
selectAll = new CheckBox();
selectAll.setOnAction(event -> {
event.consume();
tvProducts.getItems().forEach(item -> {
item.getSelected().setSelected(selectAll.isSelected());
});
});
setShoppingList();
colSelected.setGraphic(selectAll);
}
#FXML
public void editAmount(CellEditEvent<Items, Integer> event) {
Items item = event.getRowValue();
if(event.getTableColumn().getText().equals("Cantidad")) {
item.setAmount(event.getNewValue());
}
setShoppingList();
}
/*
* CLICKING ON A CHECKBOX SHOULD CALL THIS METHOD AND ADD THE ROW TO "selectedItems"
*/
#FXML
public void setShoppingList() {
List<Items> selectedItems = new ArrayList<>();
//Before trying this I was selecting each row by Ctrl + Clicking on it
// List<Items> selectedItems = tvProducts.getSelectionModel().getSelectedItems();
//This didn't seem to work
// List<ShoppingListItems> selectedItems = tvProducts.getItems().filtered(x->x.getSelected() == true);
List<Commerce> commerces = commerceRepository.findByNameContaining("");
ShoppingListPrices pricesMixingCommerces = shoppingService.getCheapestShoppingList(commerces, selectedItems);
List<ShoppingListPrices> pricesByCommerce = shoppingService.getShoppingListsPerCommerce(commerces, selectedItems);
shoppingList = new ArrayList<>();
shoppingList.add(pricesMixingCommerces);
shoppingList.addAll(pricesByCommerce);
ObservableList<ShoppingListPrices> resultOL = FXCollections.observableArrayList();
resultOL.addAll(shoppingList);
tvTotalPrices.setItems(resultOL);
}
#FXML
public void openShoppingList() throws IOException {
FXMLLoader loader = new FXMLLoader(getClass().getResource("/shoppingList.fxml"));
ShoppingListController shoppingListController = new ShoppingListController();
loader.setControllerFactory(ComparadorPreciosApplication.applicationContext::getBean);
loader.setController(shoppingListController);
shoppingListController.setup(tvTotalPrices.getSelectionModel().getSelectedItem());
try {
Scene scene = new Scene(loader.load(), 800, 400, true, SceneAntialiasing.BALANCED);
Stage stage = new Stage();//(Stage) btBack.getScene().getWindow();
stage.setUserData(tvTotalPrices.getSelectionModel().getSelectedItem());
stage.setScene(scene);
stage.show();
} catch (IOException e) {
e.printStackTrace();
}
}
#FXML
public void goBack() {
FXMLLoader loader = new FXMLLoader(ComparadorPreciosApplication.class.getResource("/index.fxml"));
loader.setControllerFactory(ComparadorPreciosApplication.applicationContext::getBean);
try {
Scene scene = new Scene(loader.load(), 800, 800, false, SceneAntialiasing.BALANCED);
Stage stage = (Stage) btBack.getScene().getWindow();
stage.setScene(scene);
stage.show();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
private void reloadTableViewProducts() {
List<String> productNames = productRepository.findOnProductPerName("");
List<Items> items = new ArrayList<>();
for(String name : productNames) {
//items.add(new Items(new SimpleBooleanProperty(false), name, 1));
Items item = new Items((CheckBox) new CheckBox(), name, 1);
item.getSelected().setSelected(false);
items.add(item);
}
ObservableList<Items> itemsOL = FXCollections.observableArrayList();
itemsOL.addAll(items);
tvProducts.setItems(itemsOL);
}
}
Your Items class should not reference any UI objects, including CheckBox. The model should ideally not even know the view exists. If you plan on having Items track if it's selected itself, then it should expose a BooleanProperty representing this state. With a properly configured table and column, the check box associated with an item and the item's selected property will remain synchronized. And since the items of the table keep track of their own selected state, getting all the selected items is relatively straightforward. Simply iterate/stream the items and grab all the selected ones.
Here's an example using CheckBoxTableCell:
import javafx.application.Application;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.cell.CheckBoxTableCell;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
public class Main extends Application {
#Override
public void start(Stage primaryStage) {
var table = new TableView<Item>();
table.setEditable(true);
table.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY);
for (int i = 0; i < 50; i++) {
table.getItems().add(new Item("Item #" + (i + 1)));
}
var selectedCol = new TableColumn<Item, Boolean>("Selected");
// configure cell factory to use a cell implementation that displays a CheckBox
selectedCol.setCellFactory(CheckBoxTableCell.forTableColumn(selectedCol));
// link CheckBox and model selected property
selectedCol.setCellValueFactory(data -> data.getValue().selectedProperty());
table.getColumns().add(selectedCol);
var nameCol = new TableColumn<Item, String>("Name");
nameCol.setCellValueFactory(data -> data.getValue().nameProperty());
table.getColumns().add(nameCol);
var button = new Button("Print checked items");
button.setOnAction(e -> {
// filter for selected items and collect into a list
var checkedItems = table.getItems().stream().filter(Item::isSelected).toList();
// log selected items
System.out.printf("There are %,d checked items:%n", checkedItems.size());
for (var item : checkedItems) {
System.out.println(" " + item);
}
});
var root = new BorderPane();
root.setTop(button);
root.setCenter(table);
root.setPadding(new Insets(10));
BorderPane.setMargin(button, new Insets(0, 0, 10, 0));
BorderPane.setAlignment(button, Pos.CENTER_RIGHT);
primaryStage.setScene(new Scene(root, 600, 400));
primaryStage.show();
}
public static class Item {
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 BooleanProperty selected = new SimpleBooleanProperty(this, "selected");
public final void setSelected(boolean selected) { this.selected.set(selected); }
public final boolean isSelected() { return selected.get(); }
public final BooleanProperty selectedProperty() { return selected; }
public Item() {}
public Item(String name) {
setName(name);
}
#Override
public String toString() {
return String.format("Item(name=%s, selected=%s)", getName(), isSelected());
}
}
}
Note that TableView has a selection model. That is not the same thing. It's used for the selection of rows or cells of the table (and thus works best on a per-table basis). You, however, want to be able to "check" items, and that requires keeping track of that state differently--an item's row could be selected while the item is not checked, and vice versa.
And note I recommend that any model class used with TableView expose JavaFX properties (like the Item class in the example above). It makes it much easier to work with TableView. But that could interfere with other parts of your code (e.g., Spring). In that case, you could do one of three things:
Create a simple adapter class that holds a reference to the "real" object and provides a BooleanProperty. This adapter class would only be used for the TableView.
Create a more complex adapter class that mirrors the "real" class in content, but exposes the properties as JavaFX properties (e.g., BooleanProperty, StringProperty, etc.). Map between them as you cross layer boundaries in your application.
In the controller, or wherever you have the TableView, keep the selected state external to the model class. For instance, you could use a Map<Item, BooleanProperty>.
I probably would only use this approach as a last resort, if ever.

Application will not display when JavaFX code is ran

I'm working on a very small, brief application to calculate charges for an upcoming conference. The app displayed fine when I ran the code until I added my event handler. Everything seems to be in check so I am unsure of what is happening. Any insight would be much appreciated.
//JavaFX imports
import javafx.application.Application;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.RadioButton;
import javafx.scene.control.ToggleGroup;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
public class ConferenceRegistration extends Application {
// Create radio buttons for conference event options
RadioButton generalAdmissionButton, studentAdmissionButton, keynoteDinnerButton, eCommerceButton, webFutureButton,
advancedJavaButton, securityButton;
Label labelAdmission, labelOptionalEvents, totalChargesLabel;
Button totalCharges;
public static void main(String[] args) {
// launch the application
launch(args);
}
#Override
public void start(Stage stage) throws Exception {
// Label for admission type selection
labelAdmission = new Label("Please select your Admission type: ");
// mandatory selection for conference
generalAdmissionButton = new RadioButton("General Admission: $895");
studentAdmissionButton = new RadioButton("Student Admission: $495");
// Create toggle group for either admission group
ToggleGroup optionalEvents = new ToggleGroup();
generalAdmissionButton.setToggleGroup(optionalEvents);
studentAdmissionButton.setToggleGroup(optionalEvents);
// Label for optional conference events
labelOptionalEvents = new Label("Please Select All Optional Events You Will Be Attending: ");
// set values for optional conference events
keynoteDinnerButton = new RadioButton("Keynote Speech Dinner: $30");
eCommerceButton = new RadioButton("Introduction to E-commerce: $295");
webFutureButton = new RadioButton("The Future of the Web: $295");
advancedJavaButton = new RadioButton("Advanced Java Programming: $395");
securityButton = new RadioButton("Network Security: $395");
// Button for calculating total Conference charges
totalCharges = new Button("Calculate Total");
totalCharges.setOnAction(new TotalChargesCalculator());
// create Vbox container and add all labels, buttons
VBox vbox = new VBox(10, labelAdmission, generalAdmissionButton, studentAdmissionButton, labelOptionalEvents,
keynoteDinnerButton, eCommerceButton, webFutureButton, advancedJavaButton, securityButton, totalCharges,
totalChargesLabel);
// format vbox
vbox.setAlignment(Pos.CENTER);
vbox.setPadding(new Insets(20));
// create and set scene
Scene scene = new Scene(vbox);
stage.setTitle("Conference Registration");
stage.setScene(scene);
// show stage
stage.show();
}
class TotalChargesCalculator implements EventHandler<ActionEvent> {
#Override
public void handle(ActionEvent arg0) {
int result = 0;
try {
// check which radio buttons are selected
if (generalAdmissionButton.isSelected()) {
result = result + 895;
}
if (studentAdmissionButton.isSelected()) {
result = result + 495;
}
if (keynoteDinnerButton.isSelected()) {
result = result + 295;
}
if (eCommerceButton.isSelected()) {
result = result + 295;
}
if (webFutureButton.isSelected()) {
result = result + 295;
}
if (advancedJavaButton.isSelected()) {
result = result + 395;
}
if (securityButton.isSelected()) {
result = result + 395;
}
totalChargesLabel.setText(String.valueOf(result));
} catch (Exception e) {
if (generalAdmissionButton.isSelected() == false || studentAdmissionButton.isSelected() == false) {
totalChargesLabel.setText("Please Select Admission Type.");
}
}
}
}
}
Thanks for your time. I look forward to learning what I am overlooking.
You are not initializing totalChargesLabel.
Initialize it to an empty Label before adding it to the VBox:
totalChargesLabel = new Label();

How to increase the number of possible selections within a ToggleGroup (e.g. RadioButtons)?

I am somewhat new to programming and new to OOP (2nd Java project over all right now) and would love any hints or help.
I am currently working on a character creation program for my very own pen&paper game. I am using JavaFX (without FXML and thus without SceneBuilder) for the GUI part. I am working with Eclipse Neon on JDK1.8.0_131.
Here is my issue:
tl;dr: How to increase the number of possible selections within a ToggleGroup?
I am about to create a list of options for the user to choose from. The list consist of about 30 different advantages he or she can choose to improve their character. The allowed maximum of chosen options depends on the character and varies around 5. I already implemented an array of pairs (I know about HashMaps), where each entry is a pair consisting of the advantage's name and an integer representing its costs (they vary in their values, so in their costs).
The list itself should now be implemented via
ScrollPane scrollAdv = new ScrollPane();
VBox vBoxAdv = new VBox();
scrollAdv.setContent(vBoxAdv);
Pair<String, Integer>[] listAdv = info.getAdvantages();
for (int i = 0; i < listAdv.length; i++) {
String name = listAdv[i].getKey(); // delivers the 1st entry of a pair
int costs = listAdv[i].getValue(); // delivers the 2nd entry of a pair
ToggleButton toggleButton = new ToggleButton();
toggleButton.setUserData(name);
toggleButton.setText(name + " (" + costs + ")");
vBoxAdv.getChildren().add(toggleButton);
}
Note that I don't care too much about ToggleButtons and they could easily be replaced with RadioButtons. Both work the same way, if I understood the documentation correctly. They both use ToggleGroup to make sure, only one option is selected.
So while you probably guessed so by now, what I want to do is give the user the possibility to chose more than one option at once. I do not want to make it so the user has to chose one option after the other, while resetting the list in between.
Thanks for reading and thanks in advance for any help or hints.
edit: I could always just add a counter which refreshes whenever one option is selected or deselected and blocks any selection if it's < 1, but I thought that there should be a better solution, e.g. increase the built-in limit of 1 which ToggleGroup seems to be using.
One way would be to disable the remaining toggles when the limit is reached. Here's a ToggleSet class that does that:
import java.util.ArrayList;
import java.util.List;
import javafx.beans.Observable;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.IntegerBinding;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.collections.transformation.FilteredList;
import javafx.scene.Node ;
import javafx.scene.control.Toggle;
public class ToggleSet<T extends Node & Toggle> {
private final ObservableList<T> toggles = FXCollections.observableArrayList(t -> new Observable[] {t.selectedProperty()});
private final FilteredList<T> selectedToggles = toggles.filtered(t -> ((Toggle)t).isSelected());
private final IntegerProperty maximumSelectable = new SimpleIntegerProperty(0);
private final IntegerBinding numSelected = Bindings.size(selectedToggles);
public ToggleSet(int maximumSelectable) {
this.maximumSelectable.addListener((obs, oldMax, newMax) -> {
if (newMax.intValue() < numSelected.get()) {
List<Toggle> togglesToClear = new ArrayList<>(selectedToggles.subList(0, numSelected.get() - newMax.intValue()));
togglesToClear.forEach(t -> t.setSelected(false));
}
});
setMaximumSelectable(maximumSelectable);
}
public ToggleSet() {
this(0);
}
public ObservableList<T> getSelectedToggles() {
return FXCollections.unmodifiableObservableList(selectedToggles) ;
}
public IntegerProperty maximumSelectableProperty() {
return maximumSelectable ;
}
public final int getMaximumSelectable() {
return maximumSelectableProperty().get();
}
public final void setMaximumSelectable(int maximumSelectable) {
maximumSelectableProperty().set(maximumSelectable);
}
public void addToggle(T toggle) {
if (numSelected.get() >= getMaximumSelectable()) {
toggle.setSelected(false);
}
toggles.add(toggle);
toggle.disableProperty().bind(toggle.selectedProperty().not().and(numSelected.greaterThanOrEqualTo(maximumSelectable)));
}
public void removeToggle(T toggle) {
toggles.remove(toggle);
toggle.disableProperty().unbind();
}
}
Here's an example testing it:
import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.CheckBox;
import javafx.scene.control.Label;
import javafx.scene.control.RadioButton;
import javafx.scene.control.Spinner;
import javafx.scene.control.ToggleButton;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox;
import javafx.stage.Stage;
public class ToggleSetTest extends Application {
#Override
public void start(Stage primaryStage) {
ToggleSet<ToggleButton> toggleSet = new ToggleSet<>(5);
GridPane grid = new GridPane() ;
Spinner<Integer> maxSelectedSpinner = new Spinner<>(0, 20, 5);
maxSelectedSpinner.getValueFactory().valueProperty().bindBidirectional(toggleSet.maximumSelectableProperty().asObject());
grid.add(new HBox(2, new Label("Maximum selected"), maxSelectedSpinner), 0, 0, 2, 1);
grid.addRow(1, new Label("Selection"), new Label("Include in set"));
for (int i = 1; i <= 20 ; i++) {
RadioButton button = new RadioButton("Button "+i);
CheckBox checkBox = new CheckBox();
checkBox.selectedProperty().addListener((obs, wasChecked, isNowChecked) -> {
if (isNowChecked) {
toggleSet.addToggle(button);
} else {
toggleSet.removeToggle(button);
}
});
checkBox.setSelected(true);
grid.addRow(i + 1, button, checkBox);
}
grid.setPadding(new Insets(10));
grid.setHgap(5);
grid.setVgap(2);
Scene scene = new Scene(grid);
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
If you want the same behavior as the ToggleGroup, where the previous selection becomes unselected, it's a little trickier, but the following should work:
import java.util.ArrayList;
import java.util.List;
import javafx.beans.Observable;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.value.ChangeListener;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.Node ;
import javafx.scene.control.Toggle;
public class ToggleSet<T extends Node & Toggle> {
private final ObservableList<T> toggles = FXCollections.observableArrayList(t -> new Observable[] {t.selectedProperty()});
private final ObservableList<T> selectedToggles = FXCollections.observableArrayList();
private final IntegerProperty maximumSelectable = new SimpleIntegerProperty(0);
private final ChangeListener<Boolean> toggleListener = (obs, wasSelected, isNowSelected) -> {
#SuppressWarnings("unchecked")
T toggle = (T) ((Property<?>)obs).getBean();
if (isNowSelected) {
selectedToggles.add(toggle);
ensureWithinMax();
} else {
selectedToggles.remove(toggle);
}
};
public ToggleSet(int maximumSelectable) {
this.maximumSelectable.addListener((obs, oldMax, newMax) -> ensureWithinMax());
setMaximumSelectable(maximumSelectable);
}
private void ensureWithinMax() {
if (this.maximumSelectable.get() < selectedToggles.size()) {
List<Toggle> togglesToClear = new ArrayList<>(selectedToggles.subList(0, selectedToggles.size() - this.maximumSelectable.get()));
togglesToClear.forEach(t -> t.setSelected(false));
}
}
public ToggleSet() {
this(0);
}
public ObservableList<T> getSelectedToggles() {
return FXCollections.unmodifiableObservableList(selectedToggles) ;
}
public IntegerProperty maximumSelectableProperty() {
return maximumSelectable ;
}
public final int getMaximumSelectable() {
return maximumSelectableProperty().get();
}
public final void setMaximumSelectable(int maximumSelectable) {
maximumSelectableProperty().set(maximumSelectable);
}
public void addToggle(T toggle) {
if (toggle.isSelected()) {
selectedToggles.add(toggle);
ensureWithinMax();
}
toggle.selectedProperty().addListener(toggleListener);
toggles.add(toggle);
}
public void removeToggle(T toggle) {
toggle.selectedProperty().removeListener(toggleListener);
toggles.remove(toggle);
}
}
(Use the same test code.)

JavaFX TableView selected cell flickers when adding data

I have a JavaFX TableView with single cell selection enabled. When a user selects a cell the selection highlight will flicker when new data is added to the table
A small example that demonstrates the problem:
import java.util.Random;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.beans.property.ReadOnlyStringWrapper;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.Scene;
import javafx.scene.control.SelectionMode;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
import javafx.util.Callback;
public class SelectionBug extends Application
{
public static void main(
String[] args)
{
Application.launch(args);
}
#Override
public void start(
Stage primaryStage) throws Exception
{
final ObservableList<DummyData> list = FXCollections.observableArrayList();
final TableView<DummyData> tableView = new TableView<>(list);
tableView.getColumns().add(createColumn(item -> item.getColumn1()));
tableView.getColumns().add(createColumn(item -> item.getColumn2()));
tableView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
tableView.getSelectionModel().setCellSelectionEnabled(true);
final Thread thread = new Thread(() ->
{
while (true)
{
Platform.runLater(() -> list.add(new DummyData()));
try
{
Thread.sleep(1000);
}
catch (InterruptedException e)
{
//do nothing
}
}
});
thread.setDaemon(true);
thread.start();
final BorderPane root = new BorderPane();
root.setCenter(tableView);
primaryStage.setScene(new Scene(root, 500, 500));
primaryStage.show();
}
private TableColumn<DummyData, String> createColumn(
final Callback<DummyData, String> dataGetter)
{
final TableColumn<DummyData, String> column = new TableColumn<>();
column.setCellValueFactory(cellData -> new ReadOnlyStringWrapper(dataGetter.call(cellData.getValue())));
return column;
}
private static class DummyData
{
private final String mColumn1;
private final String mColumn2;
public DummyData()
{
final Random ramdom = new Random();
mColumn1 = Integer.toString(ramdom.nextInt(1000));
mColumn2 = Integer.toString(ramdom.nextInt(1000));
}
public String getColumn1()
{
return mColumn1;
}
public String getColumn2()
{
return mColumn2;
}
}
}
If you run that and select a cell, you'll see the flickering.
My digging so far suggests it's to do with cell recycling in the table view: I changed the Cell Factory to assign and log out a unique ID and the cell's item for each cell object and found that the ID <-> item relationship is not constant; each cell object gets moved around the tableview showing different data with every update to the data model. This means that the selected property is modified on every update, causing the pseudoClassState to change. I suspect it's a subtle timing issue with when the cell is taken out of the tableview and when the cell's selected property is changed
Has anyone else seen this problem, and does anyone have any kind of workaround?
Probably a bit late for you, but I had a similar problem and managed to solve it by wrapping the TableView in an extra AnchorPane.

SelectedItems empty if multiple rows selected using different columns

I have a TableView in SelectionMode.MULTIPLE. Using a ListChangeListener I'm able to catch the selection of multiple rows (by pressing Shift).
However my solution only works if the items are being selected in the same column OR in the area without columns. Gif for illustration with 4 examples:
OK: Selecting 3 items using Shift in State column
OK: Selecting 4 items using Shift in Idx column
OK: Selecting 4 items using Shift starting from State column to area without columns
Error: Trying to select 4 items using Shift starting from State column to Data Item column
The problem seems to be that the SelectedItems-list is apparently empty in the last example. I'd really appreciate your help regarding this issue.
Here is my approach:
ObservableList<DataRowModel> dataRows = FXCollections.observableArrayList();
dataRows.addAll(dataSetModel.getRows());
tableDataRow.setItems(dataRows);
tableDataRowStateColumn.setCellValueFactory(f -> f.getValue().getState());
tableDataRow.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
tableDataRow.getSelectionModel().getSelectedItems()
.addListener((ListChangeListener.Change<? extends DataRowModel> c) -> {
while (c.next()) {
c.getRemoved().stream().forEach(remitem -> remitem.setSelected(false));
c.getAddedSubList().stream().forEach(additem -> additem.setSelected(true));
System.out.println(c.getList()); //Empty [] when selected using different columns
}
});
Just for a better understanding of my code: setSelected(...) sets a BooleanProperty on my DataRowModel which is bound to the State-Column.
Without context the reason for using this selected-property seems to be quite silly. However, there are various other fragments of code with ChangeListeners bound to the selected-property.
SSCCE ready to run:
import javafx.application.Application;
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;
import javafx.collections.ObservableList;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.control.SelectionMode;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.stage.Stage;
public class TableViewSample extends Application {
private TableView<DataRowModel> tableDataRow = new TableView<DataRowModel>();
private TableColumn<DataRowModel, String> tableDataRowNameColumn = new TableColumn<>("Data Item");
private TableColumn<DataRowModel, String> tableDataRowStateColumn = new TableColumn<>("State");
private final ObservableList<DataRowModel> dataRows =
FXCollections.observableArrayList(
new DataRowModel("Concinna", false),
new DataRowModel("Concinna", false),
new DataRowModel("Concinna", false),
new DataRowModel("Concinna", false),
new DataRowModel("Concinna", false)
);
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage stage) {
Scene scene = new Scene(new Group());
stage.setTitle("Table View Sample");
stage.setWidth(500);
stage.setHeight(500);
tableDataRow.setItems(dataRows);
tableDataRowNameColumn.setCellValueFactory(f -> f.getValue().getName());
tableDataRowStateColumn.setCellValueFactory(f -> f.getValue().getState());
tableDataRow.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
tableDataRow.getSelectionModel().getSelectedItems()
.addListener((ListChangeListener.Change<? extends DataRowModel> c) -> {
while (c.next()) {
c.getRemoved().stream().forEach(remitem -> remitem.setSelected(false));
c.getAddedSubList().stream().forEach(additem -> additem.setSelected(true));
}
});
tableDataRow.getColumns().addAll(tableDataRowNameColumn, tableDataRowStateColumn);
((Group) scene.getRoot()).getChildren().addAll(tableDataRow);
stage.setScene(scene);
stage.show();
}
public static class DataRowModel {
private StringProperty name = new SimpleStringProperty(this, "name", "");
private BooleanProperty selected = new SimpleBooleanProperty(this, "selected", true);
private StringProperty state = new SimpleStringProperty(this, "state", "");
public DataRowModel(String name, boolean selected) {
this.name.setValue(name);
this.selected.setValue(selected);
this.selected.addListener((observable, oldVal, newVal) -> {
getState(); // Refresh State value
});
}
public StringProperty getName() {
return name;
}
public BooleanProperty isSelected() {
return selected;
}
public void setSelected(boolean selected) {
if (this.selected.getValue() != selected)
this.selected.setValue(selected);
}
public StringProperty getState() {
String stateStr = "";
if (selected.getValue())
stateStr += "Selected";
state.setValue(stateStr);
return state;
}
}
}
I was able to generate this by editing the Oracle's Person tableview example.
This is a bug, filed as https://bugs.openjdk.java.net/browse/JDK-8096787, and fixed in version 8u60 which is expected to be released in August 2015.

Categories