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


Reply via email to