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

Automatically selecting a new JTree node in a model listener can cause unusual behavior.

    XMLWordPrintable

Details

    • b36
    • x86
    • other

    Description

      FULL PRODUCT VERSION :
      java version "1.8.0_144"
      Java(TM) SE Runtime Environment (build 1.8.0_144-b01)
      Java HotSpot(TM) 64-Bit Server VM (build 25.144-b01, mixed mode)

      ADDITIONAL OS VERSION INFORMATION :
      Windows 10.0.15063

      A DESCRIPTION OF THE PROBLEM :
      Automatically selecting a newly-inserted row (via a TreeModelListener) in an unexpanded parent node in a JTree causes an extra blank row to appear in the tree, when using DefaultTreeModel. Deleting the newly-inserted (non-blank) row causes all subsequent nodes in the tree to vanish (they're still there if you click around, however). No documentation in JTree, TreeModelEvent, TreeModelListener, or DefaultTreeModel appeared to have any restriction on what can be done in a TreeModelListener, and the listener seems like an obvious place to implement select-on-insertion, especially when there are many places in code that might add nodes.

      The problem appears to be:

      1) Listeners are called in reverse order of their addition by DefaultTreeModel.

      2) Selection occurs in the last-added listener, and "auto-expand on select" is on, which causes the creation of a node tracked in a list of visible rows in VariableHeightLayoutCache.

      3) The first-added listener from BasicTreeUI also creates a node tracked in the list of visible rows, resulting in two child nodes tracked as visible rows even though there's only one in the TreeModel - this causes various oddities like the vanishing nodes.

      Stacks for (2) and (3) are below.

      Insertion of visible node on auto-expand (2):

      Thread [AWT-EventQueue-0] (Suspended (breakpoint at line 1521 in VariableHeightLayoutCache$TreeStateNode))
      VariableHeightLayoutCache$TreeStateNode.expand(boolean) line: 1521
      VariableHeightLayoutCache$TreeStateNode.expand() line: 1288
      VariableHeightLayoutCache.ensurePathIsExpanded(TreePath, boolean) line: 984
      VariableHeightLayoutCache.setExpandedState(TreePath, boolean) line: 182
      MetalTreeUI(BasicTreeUI).updateExpandedDescendants(TreePath) line: 1696
      BasicTreeUI$Handler.treeExpanded(TreeExpansionEvent) line: 3805
      JTree.fireTreeExpanded(TreePath) line: 2764
      JTree.setExpandedState(TreePath, boolean) line: 3579
      JTree.expandPath(TreePath) line: 2212
      JTree.makeVisible(TreePath) line: 2070
      BasicTreeUI$Handler.valueChanged(TreeSelectionEvent) line: 3751
      DefaultTreeSelectionModel.fireValueChanged(TreeSelectionEvent) line: 635
      DefaultTreeSelectionModel.notifyPathChange(Vector<?>, TreePath) line: 1093
      DefaultTreeSelectionModel.setSelectionPaths(TreePath[]) line: 294
      DefaultTreeSelectionModel.setSelectionPath(TreePath) line: 188
      JTree.setSelectionPath(TreePath) line: 1634
      MinimalDemo$1.treeNodesInserted(TreeModelEvent) line: 32
      DefaultTreeModel.fireTreeNodesInserted(Object, Object[], int[], Object[]) line: 517
      DefaultTreeModel.nodesWereInserted(TreeNode, int[]) line: 314
      DefaultTreeModel.insertNodeInto(MutableTreeNode, MutableTreeNode, int) line: 241
      MinimalDemo.lambda$0(DefaultTreeModel, DefaultMutableTreeNode, DefaultMutableTreeNode, ActionEvent) line: 48
      1860513229.actionPerformed(ActionEvent) line: not available
      Timer.fireActionPerformed(ActionEvent) line: 313
      Timer$DoPostEvent.run() line: 245
      InvocationEvent.dispatch() line: 311
      EventQueue.dispatchEventImpl(AWTEvent, Object) line: 756
      EventQueue.access$500(EventQueue, AWTEvent, Object) line: 97
      EventQueue$3.run() line: 709
      EventQueue$3.run() line: 703
      AccessController.doPrivileged(PrivilegedAction<T>, AccessControlContext) line: not available [native method]
      ProtectionDomain$JavaSecurityAccessImpl.doIntersectionPrivilege(PrivilegedAction<T>, AccessControlContext, AccessControlContext) line: 80
      EventQueue.dispatchEvent(AWTEvent) line: 726
      EventDispatchThread.pumpOneEventForFilters(int) line: 201
      EventDispatchThread.pumpEventsForFilter(int, Conditional, EventFilter) line: 116
      EventDispatchThread.pumpEventsForHierarchy(int, Conditional, Component) line: 105
      EventDispatchThread.pumpEvents(int, Conditional) line: 101
      EventDispatchThread.pumpEvents(Conditional) line: 93
      EventDispatchThread.run() line: 82

      Insertion of node from BasicTreeUI (3):

      Thread [AWT-EventQueue-0] (Suspended (breakpoint at line 804 in VariableHeightLayoutCache))
      VariableHeightLayoutCache.createNodeAt(VariableHeightLayoutCache$TreeStateNode, int) line: 804
      VariableHeightLayoutCache.treeNodesInserted(TreeModelEvent) line: 491
      BasicTreeUI$Handler.treeNodesInserted(TreeModelEvent) line: 3878
      DefaultTreeModel.fireTreeNodesInserted(Object, Object[], int[], Object[]) line: 517
      DefaultTreeModel.nodesWereInserted(TreeNode, int[]) line: 314
      DefaultTreeModel.insertNodeInto(MutableTreeNode, MutableTreeNode, int) line: 241
      MinimalDemo.lambda$0(DefaultTreeModel, DefaultMutableTreeNode, DefaultMutableTreeNode, ActionEvent) line: 48
      1860513229.actionPerformed(ActionEvent) line: not available
      Timer.fireActionPerformed(ActionEvent) line: 313
      Timer$DoPostEvent.run() line: 245
      InvocationEvent.dispatch() line: 311
      EventQueue.dispatchEventImpl(AWTEvent, Object) line: 756
      EventQueue.access$500(EventQueue, AWTEvent, Object) line: 97
      EventQueue$3.run() line: 709
      EventQueue$3.run() line: 703
      AccessController.doPrivileged(PrivilegedAction<T>, AccessControlContext) line: not available [native method]
      ProtectionDomain$JavaSecurityAccessImpl.doIntersectionPrivilege(PrivilegedAction<T>, AccessControlContext, AccessControlContext) line: 80
      EventQueue.dispatchEvent(AWTEvent) line: 726
      EventDispatchThread.pumpOneEventForFilters(int) line: 201
      EventDispatchThread.pumpEventsForFilter(int, Conditional, EventFilter) line: 116
      EventDispatchThread.pumpEventsForHierarchy(int, Conditional, Component) line: 105
      EventDispatchThread.pumpEvents(int, Conditional) line: 101
      EventDispatchThread.pumpEvents(Conditional) line: 93
      EventDispatchThread.run() line: 82


      STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
      1) Create a JTree with a DefaultTreeModel containing a root node with three immediate children.

      2) Install a listener on the model that calls JTree.setSelectionPath() on a newly-inserted node in treeNodesInserted().

      3) Add a child node to the first child of the root.


      EXPECTED VERSUS ACTUAL BEHAVIOR :
      EXPECTED -
      The new child node should be visible and selected.
      ACTUAL -
      The new child node appears, unselected, with a blank row underneath it. Deleting the new child node causes all subsequent rows to vanish.

      REPRODUCIBILITY :
      This bug can be reproduced always.

      ---------- BEGIN SOURCE ----------
      import javax.swing.JFrame;
      import javax.swing.JTree;
      import javax.swing.Timer;
      import javax.swing.WindowConstants;
      import javax.swing.event.TreeModelEvent;
      import javax.swing.event.TreeModelListener;
      import javax.swing.tree.DefaultMutableTreeNode;
      import javax.swing.tree.DefaultTreeModel;
      import javax.swing.tree.TreePath;

      public class MinimalDemo {
          public static void main(final String[] args) {
              final DefaultMutableTreeNode root = new DefaultMutableTreeNode("Root");
              final DefaultTreeModel model = new DefaultTreeModel(root);
              final JTree tree = new JTree(model);

              final DefaultMutableTreeNode node1 = new DefaultMutableTreeNode("Node 1");
              root.add(node1);
              root.add(new DefaultMutableTreeNode("Node 2"));
              root.add(new DefaultMutableTreeNode("Node 3"));

              model.addTreeModelListener(new TreeModelListener() {
                  @Override
                  public void treeNodesChanged(final TreeModelEvent event) {
                      // Do nothing.
                  }

                  @Override
                  public void treeNodesInserted(final TreeModelEvent event) {
                      final TreePath pathToLastInsertedChild =
                          event.getTreePath().pathByAddingChild(event.getChildren()[event.getChildren().length - 1]);
                      tree.setSelectionPath(pathToLastInsertedChild);
                  }

                  @Override
                  public void treeNodesRemoved(final TreeModelEvent event) {
                      // Do nothing.
                  }

                  @Override
                  public void treeStructureChanged(final TreeModelEvent event) {
                      // Do nothing.
                  }
              });

              // Automated addition/removal of a child node.
              final DefaultMutableTreeNode childNode = new DefaultMutableTreeNode("child");
              final Timer insertTimer = new Timer(3000, event -> model.insertNodeInto(childNode, node1, 0));
              insertTimer.setRepeats(false);
              insertTimer.start();
              final Timer removeTimer = new Timer(6000, event -> model.removeNodeFromParent(childNode));
              removeTimer.setRepeats(false);
              removeTimer.start();

              final JFrame mainFrame = new JFrame();
              mainFrame.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
              mainFrame.setContentPane(tree);
              mainFrame.setSize(640, 480);
              mainFrame.setLocationRelativeTo(null);
              mainFrame.setVisible(true);
          }
      }

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

      CUSTOMER SUBMITTED WORKAROUND :
      Five different workarounds:

      1) Pre-expand the parent node before adding another child. This is not an option when the parent has no other children.

      2) Turn off "auto-expand on select" via JTree.setExpandsSelectedPaths().

      3) Override DefaultTreeModel.fireTreeNodesInserted() to notify listeners in order instead of reverse order.

      4) Use SwingUtilities.invokeLater() to do the selection in TreeModel.treeNodesInserted().

      5) Call JTree.setSelectionPath() immediately after DefaultTreeModel.insertNodeInto().


      Attachments

        1. Capture.png
          14 kB
        2. Capture.png
          Capture.png
          14 kB
        3. MinimalDemo.java
          2 kB
        4. MinimalDemo.java
          2 kB
        5. MinimalDemoFixed.java
          3 kB
        6. MinimalDemoFixed.java
          3 kB

        Issue Links

          Activity

            People

              kaddepalli Krishna Addepalli
              webbuggrp Webbug Group
              Votes:
              0 Vote for this issue
              Watchers:
              6 Start watching this issue

              Dates

                Created:
                Updated:
                Resolved: