PieChart (JavaFx) didn't display my Label on Event - JavaFx - java

I try to create a label on click for my PieChart, but unfortunately my label is never visible.
I found a similar topic on StackOverFlow : Label not showing on mouse event JavaFx
But my application is not as simple. I can't add my Label to the list of children because of my architecture.
(You can found a diagram here : http://i.stack.imgur.com/ZFJaR.png )
Here my code :
PieChartNode.java
package nodeStatsVision.chartFactory;
import java.util.ArrayList;
import javafx.application.Platform;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.event.EventHandler;
import javafx.scene.Node;
import javafx.scene.chart.PieChart;
import javafx.scene.control.Label;
import javafx.scene.input.MouseEvent;
import javafx.scene.paint.Color;
import nodeStatsVision.beans.ListRepere;
import nodeStatsVision.beans.OptionsChart;
import nodeStatsVision.beans.ValueStat;
/**
*
* #author Zombkey.
*/
public class PieChartNode implements ChartNode {
private ListRepere categories;
private ArrayList<ValueStat> values;
private ObservableList<PieChart.Data> pieChartData;
private Node node;
public PieChartNode(ListRepere categories, ArrayList<ValueStat> values){
this.categories = categories;
this.values = values;
pieChartData = FXCollections.observableArrayList();
node = new PieChart(pieChartData);
Platform.runLater(new Runnable() {
#Override
public void run() {
formatData();
}
});
}
private void formatData() {
final Label caption = new Label("");
caption.setTextFill(Color.DARKORANGE);
caption.setStyle("-fx-font: 24 arial;");
for(ValueStat v : values){
PieChart.Data dataTemp = new PieChart.Data(v.getCategorie().getStringName(),v.getDoubleValue());
pieChartData.add(dataTemp);
dataTemp.getNode().addEventHandler(MouseEvent.MOUSE_CLICKED,
new EventHandler<MouseEvent>() {
#Override public void handle(MouseEvent e) {
System.out.println("event : "+v.getCategorie().getStringName()+" : "+v.getDoubleValue());
caption.setTranslateX(e.getSceneX());
caption.setTranslateY(e.getSceneY());
caption.setText(String.valueOf(dataTemp.getPieValue()));
caption.setVisible(true);
System.out.println("label "+caption);
}
});
}
}
#Override
public Node getNodeGraph() {
return node;
}
#Override
public void setOptions(OptionsChart optionsChart) {
//To implemente
}
}
Have you a idea about, how add my Label to the scene ?
Thanks !
(Other question, Why the Node of PieChart.Data is on ReadOnly ?)
Zombkey.
PS : Sorry about my english, I'm a French student, I'm still learning :)
Ps 2 : First time on StackOverflow, if I did mistake, tell me it !

Ok ! I found a solution for my case !
Semantically my Label is only for my PieChart. That's why I don't want had it to my SceneGraph.
My ChartFactory return a Node, then display it. So my node have to contain the PieChart AND the Label.
I create a Group with a StackPane. In the StackPane I add my PieChart and my Label. Then my factory return the Group as a Node.
Drop the code !
package nodeStatsVision.chartFactory;
import java.util.ArrayList;
import javafx.application.Platform;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.event.EventHandler;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.Parent;
import javafx.scene.chart.PieChart;
import javafx.scene.control.Label;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import nodeStatsVision.beans.ListRepere;
import nodeStatsVision.beans.OptionsChart;
import nodeStatsVision.beans.ValueStat;
/**
*
* #author Zombkey.
*/
public class PieChartNode implements ChartNode {
private ListRepere categories;
private ArrayList<ValueStat> values;
private ObservableList<PieChart.Data> pieChartData;
private Group group;
private Node node;
private final Label caption;
public PieChartNode(ListRepere categories, ArrayList<ValueStat> values){
this.categories = categories;
this.values = values;
group = new Group();
StackPane pane = new StackPane();
group.getChildren().add(pane);
pieChartData = FXCollections.observableArrayList();
node = new PieChart(pieChartData);
pane.getChildren().add(node);
caption = new Label("");
caption.setVisible(false);
caption.getStyleClass().addAll("chart-line-symbol", "chart-series-line");
caption.setStyle("-fx-font-size: 12; -fx-font-weight: bold;");
caption.setMinSize(Label.USE_PREF_SIZE, Label.USE_PREF_SIZE);
pane.getChildren().add(caption);
Platform.runLater(new Runnable() {
#Override
public void run() {
formatData();
}
});
}
private void formatData() {
for(ValueStat v : values){
PieChart.Data dataTemp = new PieChart.Data(v.getCategorie().getStringName(),v.getDoubleValue());
pieChartData.add(dataTemp);
dataTemp.getNode().addEventHandler(MouseEvent.MOUSE_ENTERED,
new EventHandler<MouseEvent>() {
#Override public void handle(MouseEvent e) {
caption.setTranslateX(e.getX());
caption.setTranslateY(e.getY());
caption.setText(String.valueOf(dataTemp.getPieValue()));
caption.setVisible(true);
}
});
dataTemp.getNode().addEventHandler(MouseEvent.MOUSE_EXITED,
new EventHandler<MouseEvent>() {
#Override public void handle(MouseEvent e) {
caption.setVisible(false);
}
});
}
}
#Override
public Node getNodeGraph() {
return (Node)group;
}
#Override
public void setOptions(OptionsChart optionsChart) {
//To implemente
}
}
Thanks #eckig for your answers !

You create and style your Label named caption but never add it to the SceneGraph.
Somewhere it has to be added to a Parent element, otherwise it will not get displayed.
Your PieChart gets added to a parent element, otherwise it will not be displayed. The same way goes for all other JavaFX Nodes.
As to your second question, read the JavaDocs:
Readonly access to the node that represents the pie slice. You can use this to add mouse event listeners etc.

You could use Tooltip to display a value:
for (final PieChart.Data temp : pieChart.getData()) {
Tooltip tooltip = new Tooltip(String.valueOf(temp.getPieValue()));
Tooltip.install(temp.getNode(), tooltip);
}

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.

JavaFX: Make Chips Editable in JFXChipView

I want to ask if it is possible to make a chip in JFXChipView editable once it has been set.
You can create your own JFXChip and implement a behavior to enable editing. First, you need to have an editable label. I looked up online and I found this post: JavaFX custom control - editable label. Then, you can extend JFXChip to use that EditableLabel:
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXChip;
import com.jfoenix.controls.JFXChipView;
import com.jfoenix.svg.SVGGlyph;
import javafx.beans.binding.Bindings;
import javafx.beans.property.Property;
import javafx.scene.layout.HBox;
public class EditableChip<T> extends JFXChip<Property<T>> {
protected final HBox root;
public EditableChip(JFXChipView<Property<T>> view, Property<T> item) {
super(view, item);
JFXButton closeButton = new JFXButton(null, new SVGGlyph());
closeButton.getStyleClass().add("close-button");
closeButton.setOnAction(event -> {
view.getChips().remove(item);
event.consume();
});
// Create the label with an initial value from the item
String initialValue = view.getConverter().toString(item);
EditableLabel label = new EditableLabel(initialValue);
label.setMaxWidth(100);
// Bind the item to the text in the label
item.bind(Bindings.createObjectBinding(() -> view.getConverter().fromString(label.getText()).getValue(), label.textProperty()));
root = new HBox(label, closeButton);
getChildren().setAll(root);
}
}
Note: I am using Property<T> instead of using the desired class T because JFXChipView stores the item the first time you add it. And in that case, you're going to get the values as you entered them the first time when calling JFXChipView#getChips().
Sample application:
import com.jfoenix.controls.JFXChipView;
import javafx.application.Application;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleStringProperty;
import javafx.scene.Scene;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import javafx.util.StringConverter;
public class EditableChipViewApp extends Application {
#Override
public void start(Stage primaryStage) {
JFXChipView<Property<String>> chipView = new JFXChipView<>();
chipView.setChipFactory(EditableChip::new);
chipView.setConverter(new StringConverter<Property<String>>() {
#Override
public String toString(Property<String> object) {
return object == null ? null : object.getValue();
}
#Override
public Property<String> fromString(String string) {
return new SimpleStringProperty(string);
}
});
VBox container = new VBox(chipView);
Scene scene = new Scene(container, 800, 600);
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
Result:
This is how you get the actual values of the chips:
List<String> chipsValues = chipView.getChips().stream().map(Property::getValue).collect(Collectors.toList());

Missing GUI elements when adding from different files

I am trying to add GUIs, created from individual files and add them into my main code.
While it seems to be working, kind of, however, it is missing some elements. For example, in my GridPane, there are a label and a text, both of which are missing. Likewise, for my treeview, there is a treeitem within, however, that is missing as well.
What I am trying to attempt is to reduce the amount of code in the main field and as well as to call relevant events between the Guis, eg. if I select something in the TreeView, that selected TreeItem information will be populated in the GridPane.
Client.java
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.layout.HBox;
import javafx.stage.Stage;
public class Client extends Application
{
private treeviewGui tvGui;
private gridpaneGui inputFieldsGui;
public void init()
{
tvGui = new treeviewGui();
inputFieldsGui = new gridpaneGui();
}
#Override
public void start(Stage topView)
{
topView.setTitle("Test Application");
HBox mainLayout = new HBox(10);
mainLayout.getChildren().addAll(tvGui, inputFieldsGui);
Scene scene = new Scene(mainLayout);
topView.centerOnScreen();
topView.setScene(scene);
topView.show();
}
public static void main(String[] argv)
{
launch(argv);
}
}
treeviewGui.java
import javafx.scene.control.*;
public class treeviewGui extends TreeView
{
private TreeView treeview;
public treeviewGui()
{
treeview = new TreeView();
preload();
}
private void preload()
{
TreeItem<String> newTI = new TreeItem<>("blah");
treeview.setRoot(newTI);
}
}
gridPane.java
import javafx.scene.control.*;
import javafx.scene.layout.*;
import javafx.geometry.Pos;
import javafx.scene.text.Text;
public class gridpaneGui extends GridPane
{
private GridPane gridPane;
public Text fnameTxt;
public gridpaneGui()
{
gridPane = new GridPane();
gridPane.setAlignment(Pos.CENTER);
gridPane.setHgap(5);
gridPane.setVgap(5);
// First Name
Label fnameLbl = new Label("First Name");
fnameTxt = new Text("-");
gridPane.addRow(0, fnameLbl, fnameTxt);
}
public void setFname(String nameStr)
{
fnameTxt.setText(nameStr);
}
}

TreeTableView selection contains null values after source change

A list of basic values is filtered by a (changing) predicate. The FilteredList is mapped to TreeItems and this resulting list is then used as the root TreeItems children.
When a selection was made on the TreeTableView and afterwards the predicate changes, accessing the selected items results in a NullPointerException.
It seems to me that items contained in the change are null. Is there a design flaw in this coarse concept?
This does not happen for the classes TreeView and ListView.
I tried to produce a MCVE using https://github.com/TomasMikula/EasyBind for the mapping:
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import org.fxmisc.easybind.EasyBind;
import javafx.application.Application;
import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
import javafx.beans.binding.Bindings;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.collections.transformation.FilteredList;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.control.SelectionMode;
import javafx.scene.control.Spinner;
import javafx.scene.control.TreeItem;
import javafx.scene.control.TreeTableColumn;
import javafx.scene.control.TreeTableView;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
public class App extends Application {
// fields protect bound lists from GC
private ObservableList<DataItem> itemizedDataPool;
private FilteredList<Data> filteredDataPool;
private ObservableList<Data> selectedData;
static class Data {
final int value;
public Data(int value) {
this.value = value;
}
}
static class DataItem extends TreeItem<Data> {
final Data data;
public DataItem(Data data) {
this.data = data;
}
}
#Override
public void start(Stage primaryStage) throws IOException {
List<Data> dataPool = new ArrayList<Data>();
for (int i = 1; i < 20; i++) {
dataPool.add(new Data(i));
}
filteredDataPool = new FilteredList<>(FXCollections.observableArrayList(dataPool));
TreeTableView<Data> listView = createTreeTableView();
Spinner<?> lowerBoundSelector = createLowerBoundFilter();
Label sumLabel = createSummarizingLabel(listView.getSelectionModel().getSelectedItems());
Parent root = new VBox(listView, lowerBoundSelector, sumLabel);
Scene scene = new Scene(root, 768, 480);
primaryStage.setScene(scene);
primaryStage.show();
}
private TreeTableView<Data> createTreeTableView() {
itemizedDataPool = EasyBind.map(filteredDataPool, DataItem::new);
TreeItem<Data> itemRoot = new TreeItem<>();
Bindings.bindContent(itemRoot.getChildren(), itemizedDataPool);
TreeTableView<Data> listView = new TreeTableView<>(itemRoot);
listView.setShowRoot(false);
itemRoot.setExpanded(true);
listView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
listView.getColumns().add(new TreeTableColumn<>("Data"));
return listView;
}
private Label createSummarizingLabel(ObservableList<TreeItem<Data>> selectedItems) {
Label sumLabel = new Label();
selectedData = EasyBind.map(selectedItems, (TreeItem<Data> t) -> ((DataItem) t).data);
selectedData.addListener(new InvalidationListener() {
#Override
public void invalidated(Observable observable) {
int sum = 0;
for (Data d : selectedData) {
sum += d.value;
}
sumLabel.setText("Sum: " + sum);
}
});
return sumLabel;
}
private Spinner<Integer> createLowerBoundFilter() {
Spinner<Integer> lowerBoundSelector = new Spinner<>(0, 20, 0, 1);
lowerBoundSelector.valueProperty().addListener(new InvalidationListener() {
#Override
public void invalidated(Observable observable) {
filteredDataPool.setPredicate(t -> t.value > lowerBoundSelector.getValue());
}
});
return lowerBoundSelector;
}
public static void main(String[] args) {
launch(args);
}
}
Problem
TreeTableView uses TreeTableViewArrayListSelectionModel, which extends MultipleSelectionModelBase, which uses ReadOnlyUnbackedObservableList, which uses (and contains) SelectionListIterator, which has a broken implementation for its method nextIndex.
Thanks to fabian for pointing that out.
He also filed a bug report (http://bugs.java.com/bugdatabase/view_bug.do?bug_id=8145887).
Workaround
Using a buffer in between could provide an effective workaround for the problem above. I tried several approaches. setAll on selection invalidation and Bindings.bindContent do not work. In both cases I received null values in the list. The straightforward "solution" is to simply filter the nulls out. This leads to the inefficient but apparently effective code below.
// [...]
TreeTableView<Data> listView = createTreeTableView();
selectionBuffer = FXCollections.observableArrayList();
listView.getSelectionModel().getSelectedItems().addListener(new InvalidationListener() {
#Override
public void invalidated(Observable observable) {
selectionBuffer.clear();
for (TreeItem<Data> t : listView.getSelectionModel().getSelectedItems()) {
if (t != null) {
selectionBuffer.add(t);
}
}
}
});
// [...]
Using selectionBuffer instead of listView.getSelectionModel().getSelectedItems() should now compensate the implementation problem in nextIndex.

FilteredList breaks after entering a space

I have a ListView with a TextField above it. If a user enters in a search query into the textfield, the listview will update and filter itself to show relevant results.
The ListView shows items from a FilteredList, which is filled with Employee objects. Each Employee has a first and last name.
package application.ctrl;
import java.io.IOException;
import java.net.URL;
import java.util.ResourceBundle;
import javafx.collections.transformation.FilteredList;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.fxml.Initializable;
import javafx.geometry.Pos;
import javafx.geometry.Side;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.CustomMenuItem;
import javafx.scene.control.ListView;
import javafx.scene.control.TextField;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import application.Main;
import application.objects.Employee;
import application.objects.EmployeeDatabase;
public class EmployeePickerWidget extends VBox implements Initializable {
#FXML
private TextField textField;
#FXML
private Button addNewEmployee;
#FXML
private ListView<Employee> employeeList;
private FilteredList<Employee> filteredList;
private ContextMenu cm;
private CustomMenuItem item;
private ClickedEmployeeInterface parent;
public EmployeePickerWidget(ClickedEmployeeInterface parent) {
FXMLLoader loader = new FXMLLoader(this.getClass().getResource(
Main.EMPLOYEE_PICKER));
loader.setRoot(this);
loader.setController(this);
try {
loader.load();
} catch (IOException e) {
e.printStackTrace();
}
this.parent = parent;
}
#Override
public void initialize(URL location, ResourceBundle resources) {
setupEmployeeListView();
setupTextField();
}
private void setupEmployeeListView() {
filteredList = new FilteredList<Employee>(EmployeeDatabase.getInstance()
.getObservableList());
employeeList = new ListView<Employee>();
employeeList.setItems(filteredList);
employeeList.setOnMouseClicked(arg0 -> {
if (employeeList.getSelectionModel().getSelectedItem() != null) {
cm.hide();
parent.handleClickedEmployee();
}
});
}
private void setupTextField() {
textField.textProperty().addListener(
(observable, oldValue, newValue) -> {
filteredList.setPredicate(employee -> {
return filterHelper(employee, newValue);
});
});
textField.setText(" ");
textField.setText("");
textField.setOnMouseClicked(event -> cm
.show(textField, Side.BOTTOM, 0, 0));
cm = new ContextMenu();
item = new CustomMenuItem();
VBox container = new VBox();
container.setAlignment(Pos.CENTER_RIGHT);
container.getChildren().add(employeeList);
Button defineEmployeeBtn = new Button("Define New Employee");
defineEmployeeBtn.setOnAction(event -> {
FXMLLoader loader = new FXMLLoader(getClass().getResource(
Main.DEFINE_NEW_EMPLOYEE));
Parent root = null;
try {
root = loader.load();
} catch (IOException e) {
e.printStackTrace();
}
Scene newScene = new Scene(root);
Stage newStage = new Stage();
newStage.setScene(newScene);
newStage.show();
});
container.getChildren().add(defineEmployeeBtn);
item.setContent(container);
cm.getItems().add(item);
}
private boolean filterHelper(Employee employee, String query) {
String first = employee.getFirst().toLowerCase(), last = employee
.getLast().toLowerCase();
String[] querySplit = query.replace(",", "\\s").split("\\s+");
int length = querySplit.length;
for (int i = 0; i < length; i++)
querySplit[i] = querySplit[i].toLowerCase();
if (length == 1) {
if (first.contains(querySplit[0]) || last.contains(querySplit[0]))
return true;
else
return false;
} else if (length == 2) {
if (first.contains(querySplit[0]) || last.contains(querySplit[0]))
if (first.contains(querySplit[1]) || last.contains(querySplit[1]))
return true;
return false;
} else if (length == 3) {
return false;
}
return false;
}
public Employee getEmployee() {
return employeeList.getSelectionModel().getSelectedItem();
}
#FXML
public void addNewEmployee() {
}
}
interface ClickedEmployeeInterface {
void handleClickedEmployee();
}
If there were 3 employees named "Donald Trump", "Donald Smith", and "Donald Jackson" in the database, then the following needs to happen:
Typing up to the word "Donald" will show all 3 results.
Typing a space after Donald (resulting in "Donald ") will still show 3 results.
Typing a T after the previous query (resulting in "Donald T") should only show 1 result.
The problem is, after I enter in a space, the ListView breaks, and all of my Employees disappear from the ListView. When I click outside of the textfield and click back in again, it triggers this:
textField.setOnMouseClicked(event -> cm
.show(textField, Side.BOTTOM, 0, 0));
And my ListView suddenly works again, showing that one Employee.
How do I make the ListView filter properly without having to click out and back in?
I do not have the FXML file, so I wasn't able to replicate your problem. There are multiple problems with your code and this is the not the optimum solution, still, I have edited your answer to give you hints and help you understand the areas where you might have committed logical errors
import javafx.application.Application;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.event.Event;
import javafx.event.EventHandler;
import javafx.geometry.Side;
import javafx.scene.Scene;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.Label;
import javafx.scene.control.MenuItem;
import javafx.scene.control.TextField;
import javafx.scene.layout.GridPane;
import javafx.stage.Stage;
public class DemoList extends Application {
#Override
public void start(Stage stage) throws Exception {
GridPane gridPane = new GridPane();
Label label = new Label("Name");
final TextField textField = new TextField();
textField.setFocusTraversable(false);
textField.setPromptText("Please Type Here");
final ContextMenu cm = new ContextMenu();
final ObservableList<String> employeeList = FXCollections
.observableArrayList();
employeeList.addAll("Donald Duck", "Donald Mouse", "Donald Goofy");
textField.textProperty().addListener(new ChangeListener<String>() {
#Override
public void changed(ObservableValue<? extends String> arg0,
String arg1, String arg2) {
// To clear the Context Menu so that same items are not added
// multiple times
cm.getItems().clear();
for (String employee : employeeList) {
if (filterHelper(employee, arg2)) {
cm.getItems().add(new MenuItem(employee));
}
}
}
});
textField.setOnMouseClicked(new EventHandler<Event>() {
#Override
public void handle(Event arg0) {
// To clear the Context Menu so that same items are not added
// multiple times
cm.getItems().clear();
//Adding the data for initial click
for (String employee : employeeList) {
if (filterHelper(employee, textField.getText())) {
cm.getItems().add(new MenuItem(employee));
}
}
cm.show(textField, Side.BOTTOM, 0, 0);
}
});
gridPane.add(label, 0, 0);
gridPane.add(textField, 0, 1);
Scene scene = new Scene(gridPane, 300, 300);
stage.setScene(scene);
stage.show();
}
private boolean filterHelper(String employee, String query) {
//Splitting Employee name to fetch first and last name
String first = employee.split(" ")[0].toLowerCase(), last = employee
.split(" ")[1].toLowerCase();
String[] querySplit = query.replace(",", "\\s").split("\\s+");
int length = querySplit.length;
for (int i = 0; i < length; i++)
querySplit[i] = querySplit[i].toLowerCase();
/**
* Avoid adding unnecessary return statement
* I have removed all the 'return false' statements
* The last return will take care of all the 'return false'
*/
//only single word
if (length == 1) {
if (first.startsWith(querySplit[0])
|| last.startsWith(querySplit[0]))
return true;
}
//two words, considering first word is first name
//and second word is last name
else if (length == 2) {
if (first.startsWith(querySplit[0])
&& last.startsWith(querySplit[1]))
return true;
}
return false;
}
public static void main(String[] args) {
launch(args);
}
}

Categories