I have got a ObservableList and I linked it to multiple CheckMenuItem it's shown in a TableView and I can filter out one predicate.
I did that by using .filter(Predicate p) and updated my TableView to it's return value. When I wanted to unfilter, I simply set it back on my ObservableList.
But I can't wrap my head around on how to remove multiple filteres to my ObservableList. I can apply them if i just keep using .filter(Predicate p) with different predicates on the returned lists, but how to I remove a spcific filter?
Greets
The code
ObservableList<DataType> data = FXCollections.observableArrayList();
table.setItems(data.filter(predicate));
is equivalent to
ObservableList<DataType> data = FXCollections.observableArrayList();
table.setItems(new FilteredList<DataType>(data, predicate));
and the code
ObservableList<DataType> data = FXCollections.observableArrayList();
table.setItems(data.filter(predicate1).filter(predicate2));
is logically equivalent to
ObservableList<DataType> data = FXCollections.observableArrayList();
table.setItems(new FilteredList<DataType>(data, predicate1.and(predicate2)));
So you can achieve what you need by keeping a reference to the filtered list and updating its predicate:
ObservableList<DataType> data = FXCollections.observableArrayList();
FilteredList<DataType> filteredData = new FilteredList<>(data, x -> true);
table.setItems(filteredData);
Now you can use predicate1 only with:
filteredData.setPredicate(predicate1);
add predicate2:
filteredData.setPredicate(predicate1.and(predicate2));
remove predicate1:
filteredData.setPredicate(predicate2);
remove all filters:
filteredData.setPredicate(x -> true);
If you wanted a really esoteric solution (which is almost certainly overkill), you could keep an ObservableList<Predicate<DataType>>:
ObservableList<Predicate<DataType>> filters = FXCollections.observableArrayList();
and then bind the predicate of the filtered list to combining all filters with a logical and:
filteredList.predicateProperty().bind(Bindings.createObjectBinding(() ->
filters.stream().reduce(x -> true, Predicate::and),
filters));
Then you can just add and remove predicates to the filters list, e.g.
filters.add(predicate1);
filters.add(predicate2);
filters.remove(predicate1);
and the table data will automatically update.
Related
I'm not able to convert below snippet in Java 8 stream format.
List<String> titles = Arrays.asList("First Name", "Last Name");
for (FirstClass first : firstClassList) {
for (SecondClass second : first.getSecondClassList()) {
for (ThirdClass third : second.getThirdClassList()) {
if(!titles.contains(third.getField())) {
second.getThirdClassList().remove(third);
}
}
}
}
I'm comparing third level nested list object against the input list of fields. If fields are not matching then I'm removing them from original list.
How can I achieve this using Java 8 syntax.
Edit: I want List of FirstClass to be returned.
I don't think streams win you anything in this case. All you do is iterate over the nested lists and either the enhanced for loop or forEach is more straightforward.
The improvements can come from using removeIf to modify the list and, possibly, from moving the rejection logic out of the loop:
Predicate<ThirdClass> reject = third -> !titles.contains(third.getField());
firstClassList.forEeach(first ->
first.getSecondClassList().forEach(second ->
second.getThirdClassList().removeIf(reject)
)
);
Get a stream of SecondClass objects by flattening the firstClassList and for each SecondClass get the filtered list of ThirdClass objects and set it back in the SecondClass
List<String> titles = Arrays.asList("First Name", "Last Name");
firstClassList
.stream()
.flatMap(firstClass -> firstClass.getSecondClassList().stream())
.forEach(secondClass -> {
List<ThirdClass> filteredThirdClasses = secondClass.getThirdClassList()
.stream()
.filter(thirdClass -> titles.contains(thirdClass.getField()))
.collect(toList());
secondClass.setThirdClassList(filteredThirdClasses);
}
);
First you can use Stream.map() and Stream.flatMap() to get a Stream containing a List of ThirdClass. To remove the items matching the condition you could use Collection.removeIf(), which removes all items from a collection matching the given condition:
firstClassList.stream() // Stream<FirstClass>
.map(FirstClass::getSecondClassList) // Stream<List<SecondClass>>
.flatMap(Collection::stream) // Stream<SecondClass>
.map(SecondClass::getThirdClassList) // Stream<List<ThirdClass>>
.forEach(thirdList -> thirdList.removeIf(third -> !titles.contains(third.getField())));
This modifies the original List, just like you did in your example. You then can use firstClassList as result for further processing.
Beside that I would recommend using a Set<String> instead of a List<String> for your titles, because it has a time complexity of O(1) instead of O(n):
Set<String> titles = new HashSet<>(Arrays.asList("First Name", "Last Name"));
Assuming the three levels in your data-structure are collections then a solution could be:
flatten the structure to a stream of the leaf level
filter by the required titles
final List<ThirdClassList> results = firstClassList
.stream()
.flatMap(FirstClassList::getSecondClassList)
.flatMap(FirstClassList::getThirdClassList)
.filter(third -> !titles.contains(third))
.collect(toList());
This will give you the leaf level objects to be removed, though this is only half the solution of course, you still want to remove them.
If you are the author of the list classes then perhaps you could have a reference from each third level to its 'parent' second level so removing is then a relatively simple second step:
results.forEach(third -> third.parent().remove(this));
where third.parent() returns the second level object.
I have two controllers, one main screen that shows a list of parts and a button to open a second controller window to add a new part.
When I add a part, it adds to the arraylist just fine, but the tableview does not update. I have a search field at the top, and if I hit the button it shows me the new item, but nothing I have tried will get the table to refresh and display the new item when it is added.
Here is the MainController
#Override
public void initialize(URL url, ResourceBundle rb) {
if (firstLoad) {
inventory.addPart(new Part(1,"Widget",1.13,5,1,8));
inventory.addPart(new Part(2,"Sprocket",2.88,5,1,8));
inventory.addPart(new Part(3,"Gear",3.46,5,1,8));
firstLoad = false;
}
colPartID.setCellValueFactory(new PropertyValueFactory<>("partID"));
colPartName.setCellValueFactory(new PropertyValueFactory<>("name"));
colPartInvLevel.setCellValueFactory(new PropertyValueFactory<>("inStock"));
colPartPrice.setCellValueFactory(new PropertyValueFactory<>("price"));
tblParts.setItems(FXCollections.observableArrayList(inventory.getAllParts()));
}
inventory class has a static arraylist of parts
private static ArrayList<Part> allParts = new ArrayList<>();
and the addpartcontroller just adds to the arraylist, which works just fine
inv.addPart(new Part(1,"test",2,3.46));
stage.close();
After the stage is closed, the main screen doesn't seem to update at all, the parts table view still has the 3 parts in it
If I leave the search textfield blank and hit the search button, the 4th part shows up
FilteredList<Part> filteredData = new FilteredList<>(FXCollections.observableArrayList(inventory.getAllParts()), p -> true);
String newValue = txtPartSearch.getText();
filteredData.setPredicate(part -> {
if (newValue == null || newValue.isEmpty()) {
return true;
}
String lowerCaseFilter = newValue.toLowerCase();
if (part.getName().toLowerCase().contains(lowerCaseFilter)) {
return true;
}
return Integer.toString(part.getPartID()).equals(lowerCaseFilter);
});
SortedList<Part> sortedData = new SortedList<>(filteredData);
sortedData.comparatorProperty().bind(tblParts.comparatorProperty());
tblParts.setItems(sortedData);
I have tried tblParts.refresh(), I have also tried having the addpartcontroller call a method in maincontroller to set the table before closing, but the table never refreshes unless I call the search method.
EDIT:
Everything works fine if all done within the maincontroller. For instance if I remove the entire search code above and replace it with the following two lines (which execute on the main controller when pressed), then the table on the main controller updates to show the new item right away.
inventory.addPart(new Part(6,"Test",5.23,4,2,8));
tblParts.setItems(FXCollections.observableArrayList(inventory.getAllParts()));
There are a couple things wrong with your code. The most relevant being that you use FXCollections.observableArrayList(Collection). This Javadoc of this method states:
Creates a new observable array list and adds a content of collection col to it.
In other words, it copies the Collection into a new ObservableList that is backed by an ArrayList. Any updates to the original Collection will never even be added to the ObservableList. You should be using FXCollections.observableList(List) if you want the passed List to be the backing List.
The Javadoc for FXCollections.observableList(List) (emphasis mine):
Constructs an ObservableList that is backed by the specified list. Mutation operations on the ObservableList instance will be reported to observers that have registered on that instance.
Note that mutation operations made directly to the underlying list are not reported to observers of any ObservableList that wraps it.
This Javadoc hints at the second issue. Unless you are doing differently in code you haven't posted it appears you add the elements to the ArrayList field (named allParts). Because of this the ObservableList is never aware anything changes and thus no change events are fired. The firing of change events is coded in the ObservableList. If you want to be notified of changes you must only access the list via the ObservableList that wraps the ArrayList.
In this case, your code would still work (when you call tableView.refresh()) if it wasn't for the fact you also wrap your ObservableList in a FilteredList. A FilteredList creates a "view" of the backing ObservableList. If the backing ObservableList never fires any changes the FilteredList is never notified of any changes which means it never knows to update this "view". This means when you add elements to the ArrayList these new elements are "outside the view" (if you were to replace an element that is "inside the view" the change would be visible).
You can see this behavior with the following code:
import java.util.*;
import javafx.collections.*;
import javafx.collections.transformation.*;
public class Main {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("Hello");
list.add("World");
ObservableList<String> obsList = FXCollections.observableList(list);
FilteredList<String> filteredList = new FilteredList<>(obsList);
System.out.println("INITIAL STATE");
System.out.printf("\tList: %s%n", list);
System.out.printf("\tFilteredList: %s%n", filteredList);
list.add("Goodbye");
list.add("World");
System.out.println("AFTER ADDING ELEMENTS");
System.out.printf("\tList: %s%n", list);
System.out.printf("\tFilteredList: %s%n", filteredList);
list.set(0, "Foo");
list.set(1, "Bar");
System.out.println("AFTER REPLACING ELEMENTS");
System.out.printf("\tList: %s%n", list);
System.out.printf("\tFilteredList: %s%n", filteredList);
}
}
Which prints out:
INITIAL STATE
List: [Hello, World]
FilteredList: [Hello, World]
AFTER ADDING ELEMENTS
List: [Hello, World, Goodbye, World]
FilteredList: [Hello, World]
AFTER REPLACING ELEMENTS
List: [Foo, Bar, Goodbye, World]
FilteredList: [Foo, Bar]
Taking all this into account the easiest way to fix your code would be to make allParts an ObservableList. Otherwise you must take care to only use the ObservableList that you created around the ArrayList.
Edit
You also mention that, "If I leave the search textfield blank and hit the search button, the 4th part shows up". I want to address this. Here is your code with explanations why the new Part shows up in the TableView when you hit search:
/*
* I extracted the FXCollections.observableArrayList(Collection) out of the FilteredList
* constructor to more easily see what is going on.
*/
/*
* You create a **new** ObservableList using FXCollections.observableArrayList(Collection). This basically
* creates a *snapshot* of the List returnd by getAllParts() as it currently is. At this point the 4th
* Part is in that returned List. This means the newly created ObservableList will also contian the new
* Part (since observableArrayList(Collection) copies the data). However, the *old* ObservableList that
* was already set on the TableView *does not* contain that 4th Part.
*/
ObservableList<Part> parts = FXCollections.observableArrayList(inventory.getAllParts());
// You create a FilteredList that performs no filtering around "parts". Note
// here that a Predicate that always returns true is equivalent to passing null
// as the Predicate.
FilteredList<Part> filteredData = new FilteredList<>(parts, p -> true);
// Get the search criteria
String newValue = txtPartSearch.getText();
filteredData.setPredicate(part -> {
if (newValue == null || newValue.isEmpty()) {
return true; // don't filter if there is no search criteria
// since the newValue will be null or blank in this
// case no Parts are filtered
}
// filter based on lower-case names and search critiera
String lowerCaseFilter = newValue.toLowerCase();
if (part.getName().toLowerCase().contains(lowerCaseFilter)) {
return true;
}
// else filter by ID
return Integer.toString(part.getPartID()).equals(lowerCaseFilter);
});
// Wrap the FilteredList in a SortedList and bind the comparatorProperty to
// the comparatorProperty of the TableView (allows sorting by column).
SortedList<Part> sortedData = new SortedList<>(filteredData);
sortedData.comparatorProperty().bind(tblParts.comparatorProperty());
// Set the sortedData to the TableView
tblParts.setItems(sortedData);
So, the fundamental reason why when you search you see the new Part show up is because you are creating a new ObservableList every time you search. This new ObservableList has the most recent state of the getAllParts() List. Also, as I already mentioned in the comments, your edit is basically doing the same thing as your sorting code. Since you do:
inventory.addPart(new Part(6,"Test",5.23,4,2,8));
tblParts.setItems(FXCollections.observableArrayList(inventory.getAllParts()));
which adds the Part before creating the ObservableList. Again, FXCollections.observableArrayList(Collection) takes a snapshot of the Collection which, when the method is called here, contains that new Part. If you were to flip the code to:
tblParts.setItems(FXCollections.observableArrayList(inventory.getAllParts()));
inventory.addPart(new Part(6, "Test", 5.23, 4, 2, 8));
then the TableView's items property will not contain the new Part. However, the allParts ArrayList in inventory will.
I want to order a random number of Nodes according to their width. But I fail to calculate the sum of the width (using their properties), I have the following example code - I fail to get informed about the change of one of the properties:
#Override
public void start(Stage arg0) throws Exception {
List<SimpleIntegerProperty> l = IntStream.range(0, 10)
.mapToObj(SimpleIntegerProperty::new)
.collect(Collectors.toList());
ObservableList<IntegerProperty> widthAr = FXCollections.observableArrayList();
widthAr.addAll(l);
IntegerBinding nextheight = Bindings.createIntegerBinding(() -> widthAr.stream()
.mapToInt(IntegerProperty::get)
.sum(), widthAr);
nextheight.addListener((v, o, n) -> System.out.println("New value: " + v.getValue()));
//Now change randomly one of the IntegerProperties
ScheduledExecutorService tfsqueryScheduler = Executors.newScheduledThreadPool(1);
tfsqueryScheduler.scheduleAtFixedRate(() -> {
System.out.println("Changing");
int i = (int) Math.round(Math.random() * 9.4);
SimpleIntegerProperty v = l.get(i);
v.set(0);
}, 0, 3, TimeUnit.SECONDS);
System.out.println("Start...");
}
The nextheight.addListener is never called :( ... any ideas?
Thanks!
By default, ObservableLists only fire updates if the structure of the list changes (e.g. items are added to or removed from the list), not if the state of individual elements in the list changes. To create a list which fires notifications if properties belonging to any of its elements change, you need to create the list with an extractor.
In this case the property you are interested in is just the list element itself, so you need to replace
ObservableList<IntegerProperty> widthAr = FXCollections.observableArrayList();
with
ObservableList<IntegerProperty> widthAr =
FXCollections.observableArrayList(w -> new Observable[] {w});
Note also that, depending on your real use case, you may need to ensure your binding is not prematurely garbage collected, by making it a field instead of a local variable.
When creating the IntegerProperty here:
Bindings.createIntegerBinding(() -> widthAr.stream()
.mapToInt(IntegerProperty::get)
.sum(), widthAr);
you also need to add all the elements in widthAr as dependencies, because the list will not notify should it's element change, only if an element gets added or removed.
NOTE: This would not work if removing or adding elements from the List, but you don't do that.
Here's a example i used in a project. The model is about a Group with a list of recipes, and each Recipe has a list of Materials with name and amount. This only holds the structure and doesn't have any prices loaded.
The prices are pulled from a external list of reagents. So first i had to map all reagents to each material.
grupos.stream().map(Grupo::getRecetas).flatMap(List::stream).map(Receta::getObsMateriales).flatMap(List::stream)
.forEach(material -> material.setReagent(buscarReagent(material.getNombre())));
Next, after all prices are loaded to the material's reagentProperty. It's time create a binding that sums the cost from all material*amount. I also setted the recipe's reagentProperty while streaming but it has nothing to do with the sum. Since everything is done with bindings if the source reagent updates it's price this sum should update dynamically.
grupos.stream().map(Grupo::getRecetas).flatMap(List::stream).forEach(receta -> {
receta.setReagent(buscarReagent(receta.getNombre()));//doesn't affect the sum binding
receta.costoProduccionProperty()
.bind(Bindings.createLongBinding(
() -> receta.getObsMateriales().stream().mapToLong(m -> m.getReagent().getValue()*m.getCantidad()).sum(),
receta.getObsMateriales()));
});
I want to put the data from a TableView into a Collection of Maps. Each Map represents a row in the table and contains the columnName-entry-pairs of all visible columns.
Now the tricky part for me is to get the data from the visible columns only.
On the TableView, I could call getItems(), but how could I then check which column is visible?
With getColumns(), I can check which column is visible, but how could I then get the corresponding data?
The easiest thing would be a way to iterate through all visible columns - but I can't find a way to do this.
Can anyone give me a hint or a pointer in the right direction?
You can try this, though I did not test this:
final ObservableList<Map<String, Object>> collection = FXCollections.observableArrayList();
tableView.getItems().forEach(item -> {
final Map<String, Object> itemMap = new HashMap<>();
tableView.getVisibleLeafColumns().forEach(column -> {
itemMap.put(column.getText(), column.getCellObservableValue(item).getValue());
}
collection.add(itemMap);
});
I want a certain column of a TableView to be sorted by default. How would I do this? I tried doing column.setSortType(SortType.ASCENDING);, as well as putting it in a runLater call. I looked at the JavaDocs and stuff and all I can see that may be of interest is the peculiar setSortPolicy method.
To perform a "one-off" sort, call
tableView.getSortOrder().setAll(...);
passing in the TableColumn(s) by which you want the data sorted.
To make the sort persist even as the items in the list change, create a SortedList and pass it to the table's setItems(...) method. (To change the items, you will still manipulate the underlying list.)
As an example, using the usual contact table example you could do:
TableView<Person> table = new TableView<>();
TableColumn<Person, String> firstNameCol = new TableColumn<>("First Name");
firstNameCol.setCellValueFactory(cellData -> cellData.getValue().firstNameProperty());
TableColumn<Person, String> lastNameCol = new TableColumn<>("
Last Name");
firstNameCol.setCellValueFactory(cellData -> cellData.getValue().lastNameProperty());
ObservableList<Person> data = FXCollections.observableArrayList();
SortedList<Person> sortedData = new SortedList<>(data);
// this ensures the sortedData is sorted according to the sort columns in the table:
sortedData.comparatorProperty().bind(table.comparatorProperty());
table.setItems(sortedData);
// programmatically set a sort column:
table.getSortOrder().addAll(firstNameCol);
// note that you should always manipulate the underlying list, not the sortedList:
data.addAll(new Person(...), new Person(...));
Called at the end of the table initialisation...
colChecked.setSortType(TreeTableColumn.SortType.DESCENDING);
colDate.setSortType(TreeTableColumn.SortType.DESCENDING);
treeView.getSortOrder().setAll(colChecked, colDate);