Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
import java.awt.IllegalComponentStateException;
import java.awt.KeyboardFocusManager;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.Window;
import java.awt.event.KeyEvent;
import java.beans.PropertyChangeEvent;
Expand Down Expand Up @@ -69,6 +70,7 @@
import javax.swing.JScrollPane;
import javax.swing.JTree;
import javax.swing.KeyStroke;
import javax.swing.tree.TreePath;

import sun.awt.AWTAccessor;
import sun.lwawt.LWWindowPeer;
Expand Down Expand Up @@ -765,6 +767,46 @@ private static Object[] getChildrenAndRolesRecursive(final Accessible a, final C
return invokeAndWait(new Callable<Object[]>() {
public Object[] call() throws Exception {
ArrayList<Object> allChildren = new ArrayList<Object>();
Accessible at;
if (a instanceof CAccessible) {
at = CAccessible.getSwingAccessible(a);
} else {
at = a;
}

if (at instanceof JTree tree && tree.getAccessibleContext() instanceof AccessibleComponent aComp) {
TreePath[] paths = null;
if (whichChildren == JAVA_AX_ALL_CHILDREN || whichChildren == JAVA_AX_VISIBLE_CHILDREN) {
int count = tree.getRowCount();
paths = new TreePath[count];
for (int i = 0; i < count; i++) {
paths[i] = tree.getPathForRow(i);
}
} else if (whichChildren == JAVA_AX_SELECTED_CHILDREN) {
paths = tree.getSelectionPaths();
}
if (paths != null) {
for (TreePath path : paths) {
Rectangle bounds = tree.getPathBounds(path);
if (bounds == null) continue;

Accessible node = aComp.getAccessibleAt(new Point(bounds.x + bounds.width / 2, bounds.y + bounds.height / 2));
if (node == null) continue;

AccessibleContext ac = node.getAccessibleContext();
if (ac == null) continue;

if (whichChildren == JAVA_AX_VISIBLE_CHILDREN && !ac.getAccessibleStateSet().contains(AccessibleState.VISIBLE))
continue;

allChildren.add(node);
allChildren.add(ac.getAccessibleRole());
allChildren.add(String.valueOf(tree.isRootVisible() ? path.getPathCount() - 1 : path.getPathCount() - 2));
}
return allChildren.toArray();
}
}

ArrayList<Object> currentLevelChildren = new ArrayList<Object>();
ArrayList<Accessible> parentStack = new ArrayList<Accessible>();
HashMap<Accessible, List<Object>> childrenOfParent = new HashMap<>();
Expand Down Expand Up @@ -805,9 +847,9 @@ public Object[] call() throws Exception {
continue;
}

if ((cac.getAccessibleStateSet().contains(AccessibleState.SELECTED) && (whichChildren == JAVA_AX_SELECTED_CHILDREN)) ||
(cac.getAccessibleStateSet().contains(AccessibleState.VISIBLE) && (whichChildren == JAVA_AX_VISIBLE_CHILDREN)) ||
(whichChildren == JAVA_AX_ALL_CHILDREN)) {
if ((whichChildren == JAVA_AX_SELECTED_CHILDREN && cac.getAccessibleStateSet().contains(AccessibleState.SELECTED)) ||
(whichChildren == JAVA_AX_VISIBLE_CHILDREN && cac.getAccessibleStateSet().contains(AccessibleState.VISIBLE)) ||
whichChildren == JAVA_AX_ALL_CHILDREN) {
allChildren.add(ca);
allChildren.add(role);
allChildren.add(String.valueOf(currentLevel));
Expand Down
123 changes: 123 additions & 0 deletions test/jdk/java/awt/a11y/AccessibleJTreeVoiceOverFreezeTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
/*
* Copyright (c) 2026, JetBrains s.r.o.. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/

import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTree;
import javax.swing.SwingUtilities;
import javax.swing.tree.DefaultMutableTreeNode;
import java.awt.Dimension;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

/*
* @test
* @summary JBR-10147 Test for freezes when VoiceOver interacts with large trees
* @author dmitry.drobotov@jetbrains.com
* @requires (os.family == "mac")
* @run main/manual AccessibleJTreeVoiceOverFreezeTest
*/

public class AccessibleJTreeVoiceOverFreezeTest extends AccessibleComponentTest {
private static final int NODE_COUNT = 100_000;
private static final int LEVELS = 10;
private static final int FREEZE_TIMEOUT_SECONDS = 10;

@Override
public CountDownLatch createCountDownLatch() {
return new CountDownLatch(1);
}

private void createTree() {
INSTRUCTIONS = """
INSTRUCTIONS:
Check that there is no freeze while navigating a large JTree with VoiceOver enabled.

Turn VoiceOver on (Cmd + F5).
Tab to the tree.
Move the selection using Up/Down arrows multiple times.

The test will automatically fail if the EDT is unresponsive for 10 seconds.
A short freeze is acceptable on the first focus of the tree.
If the UI doesn't freeze, press PASS.
If you notice a freeze that was not detected automatically, press FAIL.""";

DefaultMutableTreeNode root = new DefaultMutableTreeNode("Root");
DefaultMutableTreeNode current = root;
for (int level = 1; level <= LEVELS; level++) {
for (int i = 1; i <= NODE_COUNT / LEVELS; i++) {
current.add(new DefaultMutableTreeNode("Level" + level + " Node" + i));
}
current = (DefaultMutableTreeNode) current.getFirstChild();
}

JTree tree = new JTree(root);
tree.setRootVisible(true);
for (int i = 0; i < tree.getRowCount(); i++) {
tree.expandRow(i);
}

JScrollPane scrollPane = new JScrollPane(tree);
scrollPane.setPreferredSize(new Dimension(400, 600));

JPanel panel = new JPanel();
panel.add(scrollPane);

exceptionString = "AccessibleJTreeVoiceOverFreezeTest test failed!";
super.createUI(panel, "AccessibleJTreeVoiceOverFreezeTest");
}

public static void main(String[] args) throws Exception {
AccessibleJTreeVoiceOverFreezeTest test = new AccessibleJTreeVoiceOverFreezeTest();

countDownLatch = test.createCountDownLatch();
SwingUtilities.invokeLater(test::createTree);

Thread freezeMonitor = new Thread(() -> {
while (countDownLatch.getCount() > 0) {
CountDownLatch ping = new CountDownLatch(1);
SwingUtilities.invokeLater(ping::countDown);
try {
if (!ping.await(FREEZE_TIMEOUT_SECONDS, TimeUnit.SECONDS) && countDownLatch.getCount() > 0) {
testResult = false;
countDownLatch.countDown();
return;
}
//noinspection BusyWait
Thread.sleep(500);
} catch (InterruptedException e) {
return;
}
}
}, "freeze-monitor");
freezeMonitor.setDaemon(true);
freezeMonitor.start();

//noinspection ResultOfMethodCallIgnored
countDownLatch.await(15, TimeUnit.MINUTES);

if (!testResult) {
throw new RuntimeException(exceptionString);
}
}
}