Uploaded image for project: 'JDK'
  1. JDK
  2. JDK-8321323

TreeTableView selecting selects wrong item on tree filtering / changing

XMLWordPrintable

    • Icon: Bug Bug
    • Resolution: Unresolved
    • Icon: P4 P4
    • tbd
    • jfx11, 8, jfx17, jfx21, jfx22
    • javafx
    • generic
    • generic

      ADDITIONAL SYSTEM INFORMATION :
      Windows 11, 10, Linux.

      JDKs: openjdk-21.0.1 graalvm-ce-java19.0.1-22.3.0 openjdk-17

      A DESCRIPTION OF THE PROBLEM :
      TreeTableView, in the scenario in the sample code (and some similar), "selects" an unselected, irrelevant item, (and fires an event) upon filtering the tree if you have a child node selected which it, and its parent, is in the process of being "filtered" out. This appears to be a bug in the shift selection logic.

      STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
      Run the code. Note the tree, note "aaa3.2" is selected (to save you a step). Click the filtering button to apply a filter on "ggg", and note that "bbb2" is selected (and logged to console) along the way, before "null" is selected (as you'd expect).

      EXPECTED VERSUS ACTUAL BEHAVIOR :
      EXPECTED -
      That "bbb2" is not selected as no code, nor the user, selected it.
      ACTUAL -
      "bb2" is incorrectly selected and events triggered.

      ---------- BEGIN SOURCE ----------
      package com.certak.ui.mvvm.bk.treetable;

      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 on 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\": " + aaa32.getValue());

                  // 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:
                  // aaa32 value from tree: TreeItem [ value: aaa3.2 ]
                  // Selected item: aaa3.2
                  // Selected item: bbb2
                  // Selected item: null
              });
          }

          private SimpleTreeItem<String> createTree() {

              // So, we have a tree like this:
              // ggg1
              // | ggg.1.1
              // | xxx.1.2
              // | ggg.1.3
              // bbb2
              // | bbb.2.1
              // | bbb.2.2
              // | bbb.2.3
              // aaa3
              // | children
              // | aaa.3.1
              // | aaa.3.2
              // | aaa.3.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);
                          setGraphic(null);
                          setOnMouseClicked(null);
                      } else {
                          setText(item);
                      }
                  }
              });

              column.setCellValueFactory(
                      param -> param.getValue().valueProperty()
              );
              tree.getColumns().add(column);
          }

          public static void main(String[] args) {
              launch(args);
          }
      }


      ---------- END SOURCE ----------

      FREQUENCY : always


      edit 2023/12/05:
      there is more. Adding a listener to selected items outputs the following:
      ```
              tree.getSelectionModel().getSelectedItems().addListener((Observable x) -> {
                  System.out.println("-selected.items: " + tree.getSelectionModel().getSelectedItems());
              });
      ```
      About to filter on "ggg": aaa3.2
      Selected item (as per listener): bbb2
      -selected.items: [TreeItem [ value: bbb2 ]]
      Selected item (as per listener): null

      (notice that there is only one change with bbb2 and nothing that reflects clearing of the selection).

            angorya Andy Goryachev
            webbuggrp Webbug Group
            Votes:
            0 Vote for this issue
            Watchers:
            5 Start watching this issue

              Created:
              Updated: