-
Bug
-
Resolution: Fixed
-
P3
-
8, 9
-
b36
-
x86
-
other
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().
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().
- relates to
-
JDK-8190281 Code cleanup in src\java.desktop\share\classes\javax\swing\tree\VariableHeightLayoutCache.java
- Resolved