Hi Andy, Thank you; yes, that report was also myself via bugreport.java.com, I think -- I didn't actually realise it ended up anywhere.
The mail I dropped here though is somewhat easier to digest / has less typos (for anyone interested)! Kind Regards, Cormac On Tue, 6 Feb 2024 at 16:11, Andy Goryachev <andy.goryac...@oracle.com> wrote: > Thank you for reporting the issue! > > > > Is this the same scenario as described in > https://bugs.openjdk.org/browse/JDK-8321323 ? > > > > -andy > > > > > > *From: *openjfx-dev <openjfx-dev-r...@openjdk.org> on behalf of Cormac > Redmond <credm...@certak.com> > *Date: *Monday, February 5, 2024 at 12:31 > *To: *openjfx-dev@openjdk.org <openjfx-dev@openjdk.org> > *Subject: *TreeTableView / FilteredList momentary incorrect selection bug > > Hi folks, > > > > I have noticed an issue when combining TreeTableView and FilteredLists, > where a wrong node is "selected" (I believe during some shift selection > functionality in TreeTableView). Currently using JavaFX 21-ea+5 on Windows, > but occurs in later builds too. > > > > First noticed in a much more complex scenario with many components, I > narrowed it down quite a bit, and created the simplest example I could, to > demonstrate what I think is a bug. > > > > Let's say you have a tree (TableTreeView) displayed like this (as per code > below): > > > > root (invisible) > > | ggg1 > > | ggg1.1 > | xxx1.2 > | ggg1.3 > | bbb2 > | bbb2.1 > | bbb2.2 > | bbb2.3 > | aaa3 > | aaa3.1 > | aaa3.2 > | aaa3.3 > > > > If you select leaf node "aaa3.2", for example, and then filter using a > string "ggg", the node "bbb2", is being selected unexpectedly/incorrectly > in the process, where it shouldn't. This is the bug. > > > > Here's a simple way to reproduce the issue. Run the code, and look at the > tree first. Observe that a leaf node "aaa3.2" is selected for you (the code > selects this as a shortcut for you). > > > > Hit the button to filter with string "ggg", and notice the logging showing > that "bbb2" -- the leaf node's parent's sibling, is incorrectly momentarily > selected, before "null" is settled as the final selected value (null being > correct). Why is this happening? > > Sample output of running the below code: > > > Value of aaa3.2 from tree (for verification): aaa3.2 <---- printed to > show the node about to be selected is the correct node > Selecting item: aaa3.2 <---- printed to show the code is about to > select it > Selected item (as per listener): aaa3.2 <---- printed by the > listener, showing it was selected > About to filter on "ggg" <---- printed to show you hit the > button, now the list is filtering which will change the tree > Selected item (as per listener): bbb2 <---- printed by the > listener, showing bbb2 is selected , why is this happening along the way? > This seems like a bug. Maybe it's part of some "let's select the closest > sibling" logic, but...why? And if so, it's not a consistent pattern/logic > that I can understand. > Selected item (as per listener): null <---- printed by the > listener, showing null is "selected", which is fine / expected, as the > *real* selected item has been filtered out > > > > Runnable code: > > > > import javafx.application.Application; > import javafx.beans.binding.Bindings; > import javafx.beans.property.ObjectProperty; > import javafx.beans.property.SimpleObjectProperty; > import javafx.beans.value.ObservableValue; > import javafx.collections.FXCollections; > import javafx.collections.transformation.FilteredList; > import javafx.scene.Scene; > import javafx.scene.control.*; > import javafx.scene.layout.VBox; > import javafx.stage.Stage; > > import java.util.ArrayList; > import java.util.List; > import java.util.function.Predicate; > > public class TreeTableSelectBug extends Application { > private final TreeTableView<String> tree = new TreeTableView<>(); > private final ObjectProperty<Predicate<String>> filterPredicate = new > SimpleObjectProperty<>(); > > @Override > public void start(Stage primaryStage) throws Exception { > final VBox outer = new VBox(); > > tree.setShowRoot(false); > tree.getSelectionModel().setSelectionMode(SelectionMode.SINGLE); > tree.setRoot(createTree()); > addColumn(); > > // Print selection changes: there should only be two (initial > selection, then final selection to "null" when nodes are filtered), but > there is an extra one ("bbb2") in the middle. > > tree.getSelectionModel().selectedItemProperty().addListener((observable, > oldValue, newValue) > -> System.out.println("Selected item (as per listener): " > + (tree.getSelectionModel().getSelectedItem() == null ? "null" : > tree.getSelectionModel().getSelectedItem().getValue()))); > > final Button filterButton = new Button("Filter on \"ggg\""); > > outer.getChildren().addAll(filterButton, tree); > final Scene scene = new Scene(outer, 640, 480); > primaryStage.setScene(scene); > primaryStage.show(); > > // Select a lead node: aaa3 -> aaa3.2 (as an example) > final TreeItem<String> aaa32 = > tree.getRoot().getChildren().get(2).getChildren().get(1); > System.out.println("Value of aaa3.2 from tree (for verification): > " + aaa32.getValue()); > > // Expand it -- without expanding it, the bug won't occur > aaa32.getParent().setExpanded(true); > > System.out.println("Selecting item: " + aaa32.getValue()); > // Select an item, note it is printed. Same as a user clicking the > row. > tree.getSelectionModel().select(aaa32); > > filterButton.setOnAction(event -> { > System.out.println("About to filter on \"ggg\""); > > // Filter based on "ggg" (the top parent node) > filterPredicate.set(string -> > string.toLowerCase().trim().contains("ggg")); > > // BUG: The output is the below. Note that "bbb2" gets > selected along the way, for some reason. This is the bug. > // > // Output: > // Value of aaa3.2 from tree (for verification): aaa3.2 > // Selecting item: aaa3.2 > // Selected item (as per listener): aaa3.2 > // About to filter on "ggg": aaa3.2 > // Selected item (as per listener): bbb2 > // Selected item (as per listener): null > }); > } > > private SimpleTreeItem<String> createTree() { > > // So, we have a tree like this: > // ggg1 > // | ggg1.1 > // | xxx1.2 > // | ggg1.3 > // bbb2 > // | bbb2.1 > // | bbb2.2 > // | bbb2.3 > // aaa3 > // | children > // | aaa3.1 > // | aaa3.2 > // | aaa3.3 > > final List<SimpleTreeItem<String>> gggChildren = new ArrayList<>(); > gggChildren.add(new SimpleTreeItem<>("ggg1.1", null, > filterPredicate)); > gggChildren.add(new SimpleTreeItem<>("xxx1.2", null, > filterPredicate)); > gggChildren.add(new SimpleTreeItem<>("ggg1.3", null, > filterPredicate)); > final SimpleTreeItem<String> gggTree = new > SimpleTreeItem<>("ggg1", gggChildren, filterPredicate); > > final List<SimpleTreeItem<String>> bbbChildren = new ArrayList<>(); > bbbChildren.add(new SimpleTreeItem<>("bbb2.1", null, > filterPredicate)); > bbbChildren.add(new SimpleTreeItem<>("bbb2.2", null, > filterPredicate)); > bbbChildren.add(new SimpleTreeItem<>("bbb2.3", null, > filterPredicate)); > final SimpleTreeItem<String> bbbTree = new > SimpleTreeItem<>("bbb2", bbbChildren, filterPredicate); > > final List<SimpleTreeItem<String>> aaaChildren = new ArrayList<>(); > aaaChildren.add(new SimpleTreeItem<>("aaa3.1", null, > filterPredicate)); > aaaChildren.add(new SimpleTreeItem<>("aaa3.2", null, > filterPredicate)); > aaaChildren.add(new SimpleTreeItem<>("aaa3.3", null, > filterPredicate)); > final SimpleTreeItem<String> aaaTree = new > SimpleTreeItem<>("aaa3", aaaChildren, filterPredicate); > > final List<SimpleTreeItem<String>> rootChildren = new > ArrayList<>(); > rootChildren.add(gggTree); > rootChildren.add(bbbTree); > rootChildren.add(aaaTree); > > return new SimpleTreeItem<>("root", > rootChildren, > filterPredicate); > } > > static class SimpleTreeItem<T> extends TreeItem<T> { > > private final ObjectProperty<Predicate<T>> filter = new > SimpleObjectProperty<>(); > private FilteredList<SimpleTreeItem<T>> children; > > public SimpleTreeItem(final T value, List<SimpleTreeItem<T>> > children, ObservableValue<Predicate<T>> filter) { > super(value, null); > > if (filter != null) { > this.filter.bind(filter); > } > > if (children != null) { > addChildren(children); > } > } > > private void addChildren(List<SimpleTreeItem<T>> childrenParam) { > children = new > FilteredList<>(FXCollections.observableArrayList(childrenParam)); > > children.predicateProperty().bind(Bindings.createObjectBinding(() -> > SimpleTreeItem.this::showNode, filter)); > > Bindings.bindContent(getChildren(), children); > } > > private boolean showNode(SimpleTreeItem<T> node) { > if (filter.get() == null) { > return true; > } > > if (filter.get().test(node.getValue())) { > // Node is directly matched -> so show it > return true; > } > > if (node.children != null) { > // Are there children (or children of children...) that > are matched? If yes we also need to show this node > return > node.children.getSource().stream().anyMatch(this::showNode); > > } > return false; > } > } > > protected void addColumn() { > TreeTableColumn<String, String> column = new > TreeTableColumn<>("Some column"); > column.setPrefWidth(150); > > column.setCellFactory(param -> new TreeTableCell<>() { > @Override > protected void updateItem(String item, boolean empty) { > super.updateItem(item, empty); > if (empty || item == null) { > setText(null); > } else { > setText(item); > } > } > }); > > column.setCellValueFactory( > param -> param.getValue().valueProperty() > ); > tree.getColumns().add(column); > } > > public static void main(String[] args) { > launch(args); > } > } > > > > > > > > Kind Regards, > > Cormac > > > > >