Reputation: 784
I've got a simple application that displays an item that has been selected from a combobox in a table. However, when the item is selected in the combobox, the remaining items are filtered to include items who's names are included in the selected item. For example, in the following MCVE if you were to select "Apple" from the combobox, the controlling list would be filtered to contain "Apple" and "Pineapple".
Occasionally the combobox will reset to no longer display the selected item after the filter is applied. The problem occurs when you select an item that doesn't have any other items in the resulting filtered list. For example if you select "Banana" or "Pineapple" from the combobox, instead of displaying the selected item the combobox will display the prompt text.
Please see the following MCVE
Main.java
package sample;
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;
public class Main extends Application {
@Override
public void start(Stage primaryStage) throws Exception{
Parent root = FXMLLoader.load(getClass().getResource("sample.fxml"));
primaryStage.setTitle("ComboBox Issues");
primaryStage.setScene(new Scene(root, 300, 275));
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
Controller.java
package sample;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.beans.property.ReadOnlyStringWrapper;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.collections.transformation.FilteredList;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.scene.control.ComboBox;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.util.StringConverter;
import java.util.function.Predicate;
public class Controller {
@FXML
private TableView<Fruit> fruityTable;
@FXML
private ComboBox<Fruit> fruitSelector;
private ObservableList<Fruit> selectedFruits;
private ObservableList<Predicate<Fruit>> filterCriteria;
private Predicate<Fruit> fruitFilter;
@FXML
private TableColumn<Fruit, String> fruitNameColumn;
@FXML
private TableColumn<Fruit, String> fruitColorColumn;
@FXML
void addSelectedFruit(ActionEvent event) {
if (fruitSelector.getValue() != null) {
Fruit selectedFruit = getSelectedFruitFromComboBox();
final String name = selectedFruit.getName().toLowerCase();
fruitFilter = selectableFruits -> selectableFruits.getName().toLowerCase().contains(name);
Platform.runLater(() -> filterCriteria.add(fruitFilter));
this.selectedFruits.add(selectedFruit);
event.consume();
}
}
private Fruit getSelectedFruitFromComboBox() {
return fruitSelector.getValue();
}
@FXML
void initialize() {
Fruit apple = new Fruit("Apple", "Red");
Fruit pineapple = new Fruit("Pineapple", "Brown");
Fruit banana = new Fruit("Banana", "Yellow");
ObservableList<Fruit> fruitSelectorItems = FXCollections.observableArrayList();
fruitSelectorItems.addAll(apple, pineapple, banana);
initializeFruitSelector(fruitSelectorItems);
initializeFruitTable();
}
private void initializeFruitSelector(ObservableList<Fruit> fruitSelectorItems) {
FilteredList<Fruit> filteredFruit = new FilteredList<>(fruitSelectorItems, x -> true);
fruitSelector.setItems(filteredFruit);
filterCriteria = FXCollections.observableArrayList();
filteredFruit.predicateProperty().bind(Bindings.createObjectBinding(() ->
filterCriteria.stream().reduce(x-> true, Predicate::and), filterCriteria));
fruitSelector.setConverter(createFruitChooserConverter());
}
private StringConverter<Fruit> createFruitChooserConverter() {
return new StringConverter<Fruit>() {
@Override
public String toString(Fruit item) {
if (item == null ) {
return null;
} else {
return item.getName();
}
}
@Override
public Fruit fromString(String string) {
return null;
}
};
}
private void initializeFruitTable() {
selectedFruits = FXCollections.observableArrayList();
fruitNameColumn.setCellValueFactory(cellData -> formatFruitNameColumnText(cellData.getValue()));
fruitColorColumn.setCellValueFactory(cellData -> formatFruitColorColumnText(cellData.getValue()));
fruityTable.setItems(selectedFruits);
}
private ObservableValue<String> formatFruitColorColumnText(Fruit fruit) {
ReadOnlyStringWrapper color;
if (fruit == null) {
color = null;
} else {
color = new ReadOnlyStringWrapper(fruit.getColor());
}
return color;
}
private ObservableValue<String> formatFruitNameColumnText(Fruit fruit) {
ReadOnlyStringWrapper name;
if (fruit == null) {
name = null;
} else {
name = new ReadOnlyStringWrapper(fruit.getName());
}
return name;
}
}
Fruit.java
package sample;
public class Fruit {
private String name;
private String color;
Fruit(String name, String color){
this.name = name;
this.color = color;
}
public String getColor() {
return color;
}
public String getName() {
return name;
}
}
sample.fxml
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.ComboBox?>
<?import javafx.scene.control.TableColumn?>
<?import javafx.scene.control.TableView?>
<?import javafx.scene.layout.ColumnConstraints?>
<?import javafx.scene.layout.GridPane?>
<?import javafx.scene.layout.RowConstraints?>
<GridPane alignment="center" hgap="10" vgap="10" xmlns="http://javafx.com/javafx/8.0.111" xmlns:fx="http://javafx.com/fxml/1" fx:controller="sample.Controller">
<children>
<TableView fx:id="fruityTable" prefHeight="200.0" prefWidth="201.0" GridPane.columnIndex="1">
<columns>
<TableColumn fx:id="fruitNameColumn" prefWidth="75.0" text="Name" />
<TableColumn fx:id="fruitColorColumn" prefWidth="75.0" text="Color" />
</columns>
</TableView>
<ComboBox fx:id="fruitSelector" onAction="#addSelectedFruit" prefWidth="150.0" promptText="Choose a fruit" />
</children>
<columnConstraints>
<ColumnConstraints minWidth="10.0" prefWidth="100.0" />
<ColumnConstraints />
</columnConstraints>
<rowConstraints>
<RowConstraints />
</rowConstraints>
</GridPane>
This seems to be a bug with the JavaFX combobox, but I haven't seen anyone with a similar issue (perhaps because it's an uncommon requirement to filter the same combobox after a selection?) Or am I doing something incorrectly?
Edit
As James_D notes in the comments, this issue is not present in newer version of Java (Java 8u131 at least). I am forced to use Java 8u25 for now however. The main reason I am concerned with this issue is because it allows the user to select the same item twice. So a solution that prevents the user from duplicating items in the table would be acceptable to me.
Upvotes: 0
Views: 919
Reputation: 784
@James_D helped me figure out that I was experiencing a bug that has been fixed in newer versions of Java. But since I am currently unable to update my applications version of Java, I still needed to figure out a way to prevent the user from being able to add duplicate items to the list. Once I phrased the problem in this way, it was easy to determine a decent work around.
I modified the filter to exclude the selected item in the filtered list.
fruitFilter = selectableFruits -> selectableFruits.getName().toLowerCase().contains(name) && !selectableFruits.getName().toLowerCase().equals(name);
The combobox still doesn't display the selected item (it can't now, because it's no longer in the list) but the user wont be able to select a duplicate item any more.
Upvotes: 0