I am using cell factory for listview with checkboxes like:
listView.setCellFactory(CheckBoxListCell.forListView(new Callback < Bean, ObservableValue < Boolean >> () {
#Override
public ObservableValue < Boolean > call(Bean item) {
BooleanProperty observable = new SimpleBooleanProperty();
observable.addListener((obs, wasSelected, isNowSelected) -> {
if (isNowSelected) {
if (!beanChoices.contains(item.toString())) {
beanChoices.add(item.toString());
observable.setValue(true);
//listView.scrollTo(listView.getItems().size() - 1);
}
} else if (wasSelected) {
if (beanChoices.contains(item.toString())) {
beanChoices.remove(item.toString());
observable.setValue(false);
}
}
});
/* [Code] which compares values with bean item string value and select observable to true for that for edit mode
but here the observer not called for beanItem that are under scrollpane of listview. But on scroll it gets called. */
return observable;
}
}));
It works fine but not for all cases.
Case: When I have say more than 10 entries, the scrollpane comes. Say I have beanChoices to be checked that are at 8 or 9 index(you have to scroll to view them). The listener is not called for the items not visible(that are under scrollpane). On Debug, I found that listener is called when I scroll down.
Problem: when I get checked values from beanChoices for above case, it return empty.
Detail: I have beanChoices which I need to make checked for listview items (edit mode). When I update without changing anything. (Assume that the value which is under the scrollpane of listview will be selected and added to beanChoices)
The Callback is used to retrieve the property for the checked state when the item is associated with a cell. The item may be removed from a cell and put in a new one at any time. This is how ListView (and similar controls like TableView) works. CheckBoxListCell simply gets the checked state property every time a new item is associated with the cell.
The return value is also used to set the initial state of the CheckBox. Since you do not properly initialize the property with the correct value the initial state is not preserved.
Also note that it makes little sense to update the value of the property to the new value in the change listener. It happens anyway.
Since BooleanProperty is a wrapper for primitive boolean the possible values are true and false; the ChangeListener only gets called when !Objects.equals(oldValue, newValue) you can be sure that isNowSelected = !wasSelected.
Of course you also need to return the value:
#Override
public ObservableValue < Boolean > call(Bean item) {
final String value = item.toString();
BooleanProperty observable = new SimpleBooleanProperty(beanChoices.contains(value));
observable.addListener((obs, wasSelected, isNowSelected) -> {
if (isNowSelected) {
beanChoices.add(value);
} else {
beanChoices.remove(value);
}
});
return observable;
}
I also recommend using a Collection of Beans instead of relying on the string representation of the objects. toString many not produce unique results and Beans.equals would be the better choice to compare the objects.
Related
I'm writing a small program for a project for Uni and it's basically a library program to manage books anr read/write to JSON file instead of using a database cause it'd be simpler since it's my first proper Java application.
I'm utilizing a TextField to filter a ListView with all the books' titles, and it works, it shows the correct book in the list and updates the corresponding informations on screen when that book is selected, the issue is that even if the program works as intended, it throws an error everytime I update the search field I get an ArrayIndexOutOfBoundsException. The full stack is as follows:
Exception in thread "JavaFX Application Thread" java.lang.ArrayIndexOutOfBoundsException: Index -1 out of bounds for length 7
at javafx.base#19-ea/javafx.collections.transformation.FilteredList.get(FilteredList.java:172)
at com.libraryproject.javalibrary/com.libraryproject.javalibrary.MainViewController.populateDetails(MainViewController.java:200)
at com.libraryproject.javalibrary/com.libraryproject.javalibrary.MainViewController.lambda$initialize$3(MainViewController.java:127)
at javafx.graphics#19-ea/com.sun.javafx.application.PlatformImpl.lambda$runLater$10(PlatformImpl.java:457)
at java.base/java.security.AccessController.doPrivileged(AccessController.java:399)
at javafx.graphics#19-ea/com.sun.javafx.application.PlatformImpl.lambda$runLater$11(PlatformImpl.java:456)
at javafx.graphics#19-ea/com.sun.glass.ui.InvokeLaterDispatcher$Future.run$$$capture(InvokeLaterDispatcher.java:96)
at javafx.graphics#19-ea/com.sun.glass.ui.InvokeLaterDispatcher$Future.run(InvokeLaterDispatcher.java)
at javafx.graphics#19-ea/com.sun.glass.ui.win.WinApplication._runLoop(Native Method)
at javafx.graphics#19-ea/com.sun.glass.ui.win.WinApplication.lambda$runLoop$3(WinApplication.java:184)
After some googling, some people suggested that when updating the GUI from user input, one should do it in the Application Thread, which to be honest I'm not absolutely sure what that means, but anyway I followed the advice and wrapped the functions that would then update the UI variables in a Platform.runLater(() -> {} , but the issue still remains, and it's the stack above, at this point I have absolutely no idea what the problem could be, so, following the stack posted, let's see the code of the parts that are shown:
I'm using a FilteredList to, well, filter the listrView using the search, here's the code managing that and most of the initialize method:
private FilteredList<Book> filteredBooks;
...
...
// inside the initialize method
#Override
public void initialize(URL arg0, ResourceBundle arg1) {
// Populate the variable we use throughout the program with the data from the JSON file
filteredBooks = new FilteredList<Book>(handleJSON.getBooks());
// Then update the list view for the first time
populateView(filteredBooks);
...
...
// section of code responsible to check for search changes, when found, fires populateView once more, this time with the variable updated.
searchField.textProperty().addListener((obs, oldText, newText) -> {
filteredBooks.setPredicate(book -> {
if(newText == null || newText.isEmpty() || newText.isBlank()) {
return true;
}
String lowerCaseCompare = newText.toLowerCase();
if(book.getTitle().toLowerCase().contains(lowerCaseCompare)) {
return true;
}
return false;
});
Platform.runLater(() -> populateView(filteredBooks));
}); // Listener
...
...
...
// This one handles the selection of an item in the list, when selected, the fields on the other side of the windows will get populated with the respective data from the book based on the id from the list, since they essentialy share the same FilteredList
listView.getSelectionModel().selectedItemProperty().addListener((obs, oldSel, newSel) -> {
Platform.runLater(() -> {
populateDetails(listView.getSelectionModel().selectedIndexProperty().getValue(), filteredBooks);
editButton.setDisable(false);
});
As you can see I wrapped all of the function that will update the ListView and fields in the window with Platform.runLater, but it doesn't seem to help.
Now for the populateView function that fires the first time the program is opened and everytime there's a change in the searchfiield:
public void populateView(FilteredList<Book> booksList) {
// clears the listview to avoid old elements stacking in the list.
listView.getSelectionModel().clearSelection();
listView.getItems().clear();
ObservableList<String> rawTitles = FXCollections.observableArrayList();
for(Book book: booksList) {
rawTitles.add(book.getTitle());
}
listView.setItems(rawTitles);
} // populateView()
And last but not least the populateDetails function that fills the fields about a book based on the list selection:
public void populateDetails(Integer selectedBookID, FilteredList<Book> books) {
Book currentBook = books.get(selectedBookID);
titleValue.setText(currentBook.getTitle());
authorValue.setText(currentBook.getAuthor());
languageValue.setText(currentBook.getLanguage());
genreValue.setText(currentBook.getGenre());
pagesValue.setText(currentBook.getPages().toString());
yearValue.setText(currentBook.getRelease().toString());
if (currentBook.getAvailable()) {
availableRadio.setSelected(true);
} else {
unavailableRadio.setSelected(true);
}
} // populateDetails
Thats basically I tried to use the runLater in different places just to be sure, I still get the same stack, any idea what could cause this?
The stack trace tells you exactly what the problem is. The ArrayIndexOutOfBoundsException occurs when you call get(..) on a FilteredList with the value -1, which you do on line 200 of MainViewController.java, in the populateDetails(...) method. Looking at your code, this line must be the line
Book currentBook = books.get(selectedBookID);
so selectedBookID must be the culprit, having the value -1.
selectedBookID is a parameter passed to the method, and you call the method from line 127 of MainController.java, in a lambda expression in the initialize() method. (Again, this information is in the stack trace.) The value you pass is
listView.getSelectionModel().selectedIndexProperty().getValue()
The documentation tells you explicitly when this happens:
The selected index is either -1, to represent that there is no selection, or an integer value that is within the range of the underlying data model size.
So your populate details needs to handle the case where nothing is selected (probably by clearing the text fields). I think it's cleaner to listen to the selectedItemProperty() instead of the selectedIndexProperty(), as it directly gives you the selected Book (or null if nothing is selected), and you don't have to retrieve the Book from the list:
public void populateDetails(Book currentBook) {
if (currentBook == null) {
titleValue.setText("");
authorValue.setText("");
languageValue.setText("");
genreValue.setText("");
pagesValue.setText("");
yearValue.setText("");
availableRadio.setSelected(false);
unavailableRadio.setSelected(false);
} else {
titleValue.setText(currentBook.getTitle());
authorValue.setText(currentBook.getAuthor());
languageValue.setText(currentBook.getLanguage());
genreValue.setText(currentBook.getGenre());
pagesValue.setText(currentBook.getPages().toString());
yearValue.setText(currentBook.getRelease().toString());
if (currentBook.getAvailable()) {
availableRadio.setSelected(true);
} else {
unavailableRadio.setSelected(true);
}
}
}
Your code is overkill; there is basically no need for the populateView() method. The filtered list will update its contents when you change the predicate, and notify observers that its content has changed. So you should just set the list view's items list to the filtered list directly. Then your listener for the search field only has to update the predicate, and the list view will automatically update.
Delete the populateView() method and update the initialize() method as:
public void initialize(URL arg0, ResourceBundle arg1) {
// Populate the variable we use throughout the program with the data from the JSON file
filteredBooks = new FilteredList<Book>(handleJSON.getBooks());
listView.setItems(filteredBooks);
// ...
// ...
// section of code responsible to check for search changes, when found, fires populateView once more, this time with the variable updated.
searchField.textProperty().addListener((obs, oldText, newText) -> {
filteredBooks.setPredicate(book -> {
if(newText == null || newText.isEmpty() || newText.isBlank()) {
return true;
}
String lowerCaseCompare = newText.toLowerCase();
return book.getTitle().toLowerCase().contains(lowerCaseCompare)
});
}); // Listener
// ...
// This one handles the selection of an item in the list, when selected, the fields on the other side of the windows will get populated with the respective data from the book based on the id from the list, since they essentialy share the same FilteredList
listView.getSelectionModel().selectedItemProperty().addListener(
(obs, oldSel, newSel) -> populateDetails(newSel)
);
}
I'm trying to make it so a selection is required in order to proceed and I am wondering if it is possible to achieve this using the ControlsFX Validator and ValidationSupport.
public static void test(ValidationSupport support, ListView listView) {
Validator<ObservableListWrapper> validator = (control, value) -> {
boolean condition = listView.getSelectionModel().getSelectedItem() == null;
return ValidationResult.fromMessageIf(control, "Please select an item", Severity.ERROR, condition);
};
support.registerValidator(listView, true, validator);
}
This does not work. Selecting an item on the list does not seem to effect this in any way. I am thinking this is because I'm using the listView parameter instead of the value one, but I do not know how to get the value one to correspond with the list view's selection model.
ValidationSupport uses ValueExtractor to extract an observable value which when changes a revalidation will be made. ValueExtractor uses ListView#itemsProperty as default for all ListViews, which means that ValidationSupport will revalidate only when itemsProperty is changed.
In order to change this for all instances of ListView, you can set an observable value extractor using the static method addObservableValueExtractor like this:
ValueExtractor.addObservableValueExtractor(c -> c instanceof ListView,
c -> ((ListView) c).getSelectionModel().selectedItemProperty());
If you want to change it for a specific instance of ListView you can try doing this:
ValueExtractor.addObservableValueExtractor(c -> c == listView,
c -> ((ListView) c).getSelectionModel().selectedItemProperty());
An example using ListView<File>:
ListView<File> listView = new ListView<>();
ValidationSupport support = new ValidationSupport();
ValueExtractor.addObservableValueExtractor(c -> c == listView,
c -> ((ListView<File>) c).getSelectionModel().selectedItemProperty());
Validator<File> validator = new Validator<>() {
#Override
public ValidationResult apply(Control control, File file) {
boolean condition = file == null;
return ValidationResult.fromMessageIf(control, "Please select a file", Severity.ERROR, condition);
}
};
support.registerValidator(listView, true, validator);
I'm writing a seating chart program using JavaFX. I have a table that keeps a list of students together that holds their name, grade, and whether they are present or absent (using a checkbox). I have a delete button that allows me to delete the students from the list. This works fine, however, whenever I delete the student object, the checkbox does not go along with it. I'm not sure what I would need to add to get that to work. Here is a snippet of the delete code. There are also two images below that show my problem. This is my first post so please let me know if I missed something. Please help! Thanks!
ObservableList<Student> items, sel;
items = currentTable.getItems();
sel = currentTable.getSelectionModel().getSelectedItems();
Student s = new Student("", "", 0, "");
for (Student p : sel) {
items.remove(p);
s = p;
}
Before Delete
After Delete
This has nothing to do with the delete or remove method. It has to do with what you did in TableColumn.setCellFactory().
To get the checkbox you shown, you should have used (in general) one of the two methods:
Overriding updateItem() in TableCell while setting Cell Factory
There is this empty parameter in updateItem() which indicates whether the row is empty or not. You need to use that to determine when not to show the checkbox.
column.setCellFactory(col -> {
return new TableCell<Foo, Boolean>() {
final CheckBox checkBox = new CheckBox();
#Override
public void updateItem(final Boolean selected, final boolean empty) {
super.updateItem(selected, empty);
if (!this.isEmpty()) {
setGraphic(checkBox);
setText("");
}
else {
setGraphic(null); // Remove checkbox if row is empty
setText("");
}
}
}
}
Using CheckBoxTableCell
JavaFX API has this convenient class CheckBoxTableCell that would do all these for you. Most people find this class hard to use because there are 2 things that you need to ensure to use it correctly:
The TableView that the column belongs to must be editable.
The TableColumn itself must be editable.
Example:
tableView.setEditable(true);
tableColumnSelected.setCellFactory(CheckBoxTableCell.forTableColumn(tableColumnSelected));
tableColumnSelected.setEditable(true);
As for whether which entry you want to be removed with the delete button, you just need to remove the correct items from the TableView.
In my JavaFx project I have a scene with a few views. For the footer of the window I have a class extending from TableView:
public class FooterView extends TableView<Area> implements ViewTemplate {...}
This displays a Table with some data from a .csv-file.
When it comes to assigning the value of the specific presentationmodel property to the cells I do it like that:
TableColumn<Area,Double> powerColumn = new TableColumn<>("Power Overview");
powerColumn.setCellValueFactory(new PropertyValueFactory<>("powerPerArea"));
this.setItems(filePM.getAreas()); //filePm is my filehandler
this.getColumns().addAll(powerColumn, other_columns);
getAreas() looks like this:
public List<Area> readCantonsFromFile() {
try (Stream<String> stream = getStreamOfLines(AREA_FILE_NAME)) {
return stream.skip(1)
.map(l -> new Area(l.split(DELIMITER, 12)))
.collect(Collectors.toList());
}
}
In the constructor of Area i set the properties. One of the properties is the mentioned powerPerArea
private final DoubleProperty powerPerArea = new SimpleDoubleProperty();
...
public void setPowerPerCanton(double powerPerCanton) {
this.powerPerCanton.set(powerPerCanton);
}
My question: Is there a way to change the value in the FooterView before the value is displayed? I tried something like this:
powerColumn.setCellValueFactory(Math.round(new PropertyValueFactory<>("powerPerArea")));
But it seems that I mixed up DoubleProperty, Double and ObservableDouble. Can I even modify the value in here?
The problem: I can not round the value in the setter because I add a double value in a loop through this function:
public void addPowerPerArea(double power){
double sum = getPowerPerCanton() + power;
setPowerPerCanton(sum);
}
And rounding the value in here would give me a wrong result. (rounding not precise enough). I need to do it in the end when all sums are added
You should use the cellValueFactory to determine which data are displayed in the cells: in this case the data returned by your PropertyValueFactory is the actual double value returned from powerPerAreaProperty().get(), which is exactly what you want.
If you want to control how the data are displayed, you should use a cellFactory. So to display the data in a particular format, including limiting the number of decimal places, you can do:
powerColumn.setCellFactory(tc -> new TableCell<Area, Double>() {
#Override
protected void updateItem(Double power, boolean empty) {
super.updateItem(power, empty);
if (empty) {
setText(null);
} else {
setText(String.format("%.0f", power.doubleValue()));
}
}
});
The point here is that you should not modify the data based on how you want to display it; the purpose of having both cellValueFactory and cellFactory is to separate the display of the data from the actual data itself.
An alternative to returning custom cells from a cellFactory would be to use a custom cellValueFactory to return the property formatted as string:
TableColumn<Area, String> powerColumn = new TableColumn<>("Power Overview");
powerColumn.setCellValueFactory(cd -> cd.getValue().powerPerAreaProperty().asString(""%.0f""));
I see two ways to do this. You could:
use the setCellFactory method and in the updateItem method you format it. Should look something like this, haven't tested
powerColumn.setCellFactory(column -> {
return new TableCell<Area, Double>() {
#Override
protected void updateItem(Double item, boolean empty) {
super.updateItem(Math.round(item), empty);
}
};
});
OR: You could make another property of your Area class that is bound to the existing powerOfArea property but returns the value rounded. I am sure that is somehow possible, you could just override some functions of a readOnlyDoubleProperty but there should be a better way. Maybe via DoubleBindings.
Is there a way to check if a ComboBox has any items in it or whether it is empty? I have an array of ComboBoxes and I need to go through each of them, if there are no items in the ComboBox, then I must hide it. This following code doesn't seem to work:
for (ComboBox cmb : comboBoxes) {
if (cmb.getItems().isEmpty()) {
cmb.hide();
}
}
The code for checking, if the ComboBox has no items is correct, you code for hiding the ComboBoxes is incorrect however.
ComboBox.hide only closes the popup showing the items, if it's open. It does not hide the ComboBox. To hide the ComboBox, you need to set the visibility:
for (ComboBox cmb : comboBoxes) {
if (cmb.getItems().isEmpty()) {
cmb.setVisible(false);
}
}
Alternatively to call a method to hide the ComboBoxes, you can bind the visibleProperty of the ComboBoxes to their own itemsProperty with a custom binding:
List<ComboBox<String>> comboBoxes = new ArrayList<>();
for(int i = 0; i< 10; i++) {
ComboBox<String> combo = new ComboBox<>();
combo.visibleProperty().bind(Bindings.createBooleanBinding(() -> !combo.getItems().isEmpty(),
combo.itemsProperty().get()));
comboBoxes.add(combo);
}
The advantage is, that you don't have to call any methods to hide your ComboBoxes, because the binding is evaluated automatically, therefore no one can see your empty combos.
The .getItems() method returns an ObservableList<T> so you can just check its .size(). This will tell you if it's empty.
for (ComboBox cmb : comboBoxes) {
if (cmb.getItems().size() <= 0) { // or cmb.getItems().isEmpty()
cmb.setVisible(false); }
}
If the ComboBox is populated by a List of its own, you could also just check if the list is empty with the same .size() call.