TreeTableView / FilteredList momentary incorrect selection bug
Cormac Redmond
credmond at certak.com
Tue Feb 6 16:20:58 UTC 2024
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.goryachev at 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-retn at openjdk.org> on behalf of Cormac
> Redmond <credmond at certak.com>
> *Date: *Monday, February 5, 2024 at 12:31
> *To: *openjfx-dev at openjdk.org <openjfx-dev at 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
>
>
>
>
>
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://mail.openjdk.org/pipermail/openjfx-dev/attachments/20240206/327a1a76/attachment-0001.htm>
More information about the openjfx-dev
mailing list