<div dir="ltr"><div class="gmail_default" style="font-size:small;color:rgb(0,0,0)"><div class="gmail_default" style="font-family:verdana,sans-serif">Hi folks,</div><div class="gmail_default" style="font-family:verdana,sans-serif"><br></div><div class="gmail_default" style="font-family:verdana,sans-serif">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.</div><div class="gmail_default" style="font-family:verdana,sans-serif"><br></div><div class="gmail_default" style="font-family:verdana,sans-serif">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.</div><div class="gmail_default" style="font-family:verdana,sans-serif"><br></div><div class="gmail_default" style="font-family:verdana,sans-serif">Let's say you have a tree (TableTreeView) displayed like this (as per code below):</div><div class="gmail_default" style=""><font face="monospace"><br></font></div><div class="gmail_default" style=""><font face="monospace">root (invisible)</font></div><div class="gmail_default" style=""><font face="monospace">   | ggg1</font></div><div class="gmail_default" style=""><font face="monospace">       | ggg1.1<br>       | xxx1.2<br>       | ggg1.3<br>   | bbb2<br>       | bbb2.1<br>       | bbb2.2<br>       | bbb2.3<br>   | aaa3<br>       | aaa3.1<br>       | aaa3.2<br>       | aaa3.3</font><br></div><div class="gmail_default" style="font-family:verdana,sans-serif"><br></div><div class="gmail_default" style="font-family:verdana,sans-serif">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.<br></div><div class="gmail_default" style="font-family:verdana,sans-serif"><br></div><div class="gmail_default" style="font-family:verdana,sans-serif">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). </div><div class="gmail_default" style="font-family:verdana,sans-serif"><br></div><div class="gmail_default" style="font-family:verdana,sans-serif">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?<br><br>Sample output of running the below code:</div><div class="gmail_default" style=""><br><font face="monospace">Value of aaa3.2 from tree (for verification): aaa3.2      <---- printed to show the node about to be selected is the correct node<br>Selecting item: aaa3.2        <---- printed to show the code is about to select it<br>Selected item (as per listener): aaa3.2         <---- printed by the listener, showing it was selected<br>About to filter on "ggg"            <---- printed to show you hit the button, now the list is filtering which will change the tree<br>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.<br>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</font></div><div class="gmail_default" style="font-family:verdana,sans-serif"><br></div><div class="gmail_default" style="font-family:verdana,sans-serif">Runnable code:</div><div class="gmail_default" style="font-family:verdana,sans-serif"><br></div><div class="gmail_default" style=""><font face="monospace">import javafx.application.Application;<br>import javafx.beans.binding.Bindings;<br>import javafx.beans.property.ObjectProperty;<br>import javafx.beans.property.SimpleObjectProperty;<br>import javafx.beans.value.ObservableValue;<br>import javafx.collections.FXCollections;<br>import javafx.collections.transformation.FilteredList;<br>import javafx.scene.Scene;<br>import javafx.scene.control.*;<br>import javafx.scene.layout.VBox;<br>import javafx.stage.Stage;<br><br>import java.util.ArrayList;<br>import java.util.List;<br>import java.util.function.Predicate;<br><br>public class TreeTableSelectBug extends Application {<br>    private final TreeTableView<String> tree = new TreeTableView<>();<br>    private final ObjectProperty<Predicate<String>> filterPredicate = new SimpleObjectProperty<>();<br><br>    @Override<br>    public void start(Stage primaryStage) throws Exception {<br>        final VBox outer = new VBox();<br><br>        tree.setShowRoot(false);<br>        tree.getSelectionModel().setSelectionMode(SelectionMode.SINGLE);<br>        tree.setRoot(createTree());<br>        addColumn();<br><br>        // 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.<br>        tree.getSelectionModel().selectedItemProperty().addListener((observable, oldValue, newValue)<br>                -> System.out.println("Selected item (as per listener): " + (tree.getSelectionModel().getSelectedItem() == null ? "null" : tree.getSelectionModel().getSelectedItem().getValue())));<br><br>        final Button filterButton = new Button("Filter on \"ggg\"");<br><br>        outer.getChildren().addAll(filterButton, tree);<br>        final Scene scene = new Scene(outer, 640, 480);<br>        primaryStage.setScene(scene);<br>        primaryStage.show();<br><br>        // Select a lead node: aaa3 -> aaa3.2 (as an example)<br>        final TreeItem<String> aaa32 = tree.getRoot().getChildren().get(2).getChildren().get(1);<br>        System.out.println("Value of aaa3.2 from tree (for verification): " + aaa32.getValue());<br><br>        // Expand it -- without expanding it, the bug won't occur<br>        aaa32.getParent().setExpanded(true);<br><br>        System.out.println("Selecting item: " + aaa32.getValue());<br>        // Select an item, note it is printed. Same as a user clicking the row.<br>        tree.getSelectionModel().select(aaa32);<br><br>        filterButton.setOnAction(event -> {<br>            System.out.println("About to filter on \"ggg\"");<br><br>            // Filter based on "ggg" (the top parent node)<br>            filterPredicate.set(string -> string.toLowerCase().trim().contains("ggg"));<br><br>            // BUG: The output is the below. Note that "bbb2" gets selected along the way, for some reason. This is the bug.<br>            //<br>            // Output:<br>            // Value of aaa3.2 from tree (for verification): aaa3.2<br>            // Selecting item: aaa3.2<br>            // Selected item (as per listener): aaa3.2<br>            // About to filter on "ggg": aaa3.2<br>            // Selected item (as per listener): bbb2<br>            // Selected item (as per listener): null<br>        });<br>    }<br><br>    private SimpleTreeItem<String> createTree() {<br><br>        // So, we have a tree like this:<br>        // ggg1<br>        //  | ggg1.1<br>        //  | xxx1.2<br>        //  | ggg1.3<br>        // bbb2<br>        //  | bbb2.1<br>        //  | bbb2.2<br>        //  | bbb2.3<br>        // aaa3<br>        //  | children<br>        //  | aaa3.1<br>        //  | aaa3.2<br>        //  | aaa3.3<br><br>        final List<SimpleTreeItem<String>> gggChildren = new ArrayList<>();<br>        gggChildren.add(new SimpleTreeItem<>("ggg1.1", null, filterPredicate));<br>        gggChildren.add(new SimpleTreeItem<>("xxx1.2", null, filterPredicate));<br>        gggChildren.add(new SimpleTreeItem<>("ggg1.3", null, filterPredicate));<br>        final SimpleTreeItem<String> gggTree = new SimpleTreeItem<>("ggg1", gggChildren, filterPredicate);<br><br>        final List<SimpleTreeItem<String>> bbbChildren = new ArrayList<>();<br>        bbbChildren.add(new SimpleTreeItem<>("bbb2.1", null, filterPredicate));<br>        bbbChildren.add(new SimpleTreeItem<>("bbb2.2", null, filterPredicate));<br>        bbbChildren.add(new SimpleTreeItem<>("bbb2.3", null, filterPredicate));<br>        final SimpleTreeItem<String> bbbTree = new SimpleTreeItem<>("bbb2", bbbChildren, filterPredicate);<br><br>        final List<SimpleTreeItem<String>> aaaChildren = new ArrayList<>();<br>        aaaChildren.add(new SimpleTreeItem<>("aaa3.1", null, filterPredicate));<br>        aaaChildren.add(new SimpleTreeItem<>("aaa3.2", null, filterPredicate));<br>        aaaChildren.add(new SimpleTreeItem<>("aaa3.3", null, filterPredicate));<br>        final SimpleTreeItem<String> aaaTree = new SimpleTreeItem<>("aaa3", aaaChildren, filterPredicate);<br><br>        final List<SimpleTreeItem<String>> rootChildren = new ArrayList<>();<br>        rootChildren.add(gggTree);<br>        rootChildren.add(bbbTree);<br>        rootChildren.add(aaaTree);<br><br>        return new SimpleTreeItem<>("root",<br>                rootChildren,<br>                filterPredicate);<br>    }<br><br>    static class SimpleTreeItem<T> extends TreeItem<T> {<br><br>        private final ObjectProperty<Predicate<T>> filter = new SimpleObjectProperty<>();<br>        private FilteredList<SimpleTreeItem<T>> children;<br><br>        public SimpleTreeItem(final T value, List<SimpleTreeItem<T>> children, ObservableValue<Predicate<T>> filter) {<br>            super(value, null);<br><br>            if (filter != null) {<br>                this.filter.bind(filter);<br>            }<br><br>            if (children != null) {<br>                addChildren(children);<br>            }<br>        }<br><br>        private void addChildren(List<SimpleTreeItem<T>> childrenParam) {<br>            children = new FilteredList<>(FXCollections.observableArrayList(childrenParam));<br>            children.predicateProperty().bind(Bindings.createObjectBinding(() -> SimpleTreeItem.this::showNode, filter));<br><br>            Bindings.bindContent(getChildren(), children);<br>        }<br><br>        private boolean showNode(SimpleTreeItem<T> node) {<br>            if (filter.get() == null) {<br>                return true;<br>            }<br><br>            if (filter.get().test(node.getValue())) {<br>                // Node is directly matched -> so show it<br>                return true;<br>            }<br><br>            if (node.children != null) {<br>                // Are there children (or children of children...) that are matched? If yes we also need to show this node<br>                return node.children.getSource().stream().anyMatch(this::showNode);<br><br>            }<br>            return false;<br>        }<br>    }<br><br>    protected void addColumn() {<br>        TreeTableColumn<String, String> column = new TreeTableColumn<>("Some column");<br>        column.setPrefWidth(150);<br><br>        column.setCellFactory(param -> new TreeTableCell<>() {<br>            @Override<br>            protected void updateItem(String item, boolean empty) {<br>                super.updateItem(item, empty);<br>                if (empty || item == null) {<br>                    setText(null);<br>                } else {<br>                    setText(item);<br>                }<br>            }<br>        });<br><br>        column.setCellValueFactory(<br>                param -> param.getValue().valueProperty()<br>        );<br>        tree.getColumns().add(column);<br>    }<br><br>    public static void main(String[] args) {<br>        launch(args);<br>    }<br>}<br></font><br></div><div class="gmail_default" style="font-family:verdana,sans-serif"><br></div><div class="gmail_default" style="font-family:verdana,sans-serif"><br></div><div class="gmail_default" style="font-family:verdana,sans-serif"><br></div><div class="gmail_default" style="font-family:verdana,sans-serif">Kind Regards,</div><div class="gmail_default" style="font-family:verdana,sans-serif">Cormac</div></div><div><div dir="ltr" class="gmail_signature" data-smartmail="gmail_signature"><div dir="ltr"><div><br></div><div><br></div></div></div></div></div>