8208640: [a11y] [macos] Unable to navigate between Radiobuttons in Radio group using keyboard.
Reviewed-by: prr, serb, psadhukhan, ssadetsky
--- a/src/java.desktop/macosx/classes/com/apple/laf/AquaButtonRadioUI.java Tue Aug 14 09:38:29 2018 -0700
+++ b/src/java.desktop/macosx/classes/com/apple/laf/AquaButtonRadioUI.java Tue Aug 14 12:50:39 2018 -0700
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2011, 2012, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2011, 2018, Oracle and/or its affiliates. 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
@@ -25,15 +25,63 @@
package com.apple.laf;
-import javax.swing.*;
+import javax.swing.JComponent;
+import javax.swing.ImageIcon;
+import javax.swing.JRadioButton;
+import javax.swing.Icon;
+import javax.swing.AbstractButton;
+import javax.swing.AbstractAction;
+import javax.swing.KeyStroke;
+import javax.swing.DefaultButtonModel;
+import javax.swing.ButtonGroup;
+import javax.swing.ButtonModel;
import javax.swing.plaf.ComponentUI;
-import apple.laf.JRSUIConstants.*;
+import java.awt.Component;
+import java.awt.AWTKeyStroke;
+import java.awt.KeyboardFocusManager;
+
+import java.awt.event.ActionEvent;
+import java.awt.event.KeyListener;
+import java.awt.event.KeyEvent;
-import com.apple.laf.AquaUtilControlSize.*;
-import com.apple.laf.AquaUtils.*;
+import apple.laf.JRSUIConstants.Widget;
+import com.apple.laf.AquaUtilControlSize.SizeVariant;
+import com.apple.laf.AquaUtilControlSize.SizeDescriptor;
+import com.apple.laf.AquaUtils.RecyclableSingleton;
+import com.apple.laf.AquaUtils.RecyclableSingletonFromDefaultConstructor;
+
+import java.util.HashSet;
+import java.util.Set;
+import java.util.Enumeration;
public class AquaButtonRadioUI extends AquaButtonLabeledUI {
+ private KeyListener keyListener = null;
+
+ @SuppressWarnings("serial")
+ private class SelectPreviousBtn extends AbstractAction {
+ public SelectPreviousBtn() {
+ super("Previous");
+ }
+
+ @Override
+ public void actionPerformed(ActionEvent e) {
+ AquaButtonRadioUI.this.selectRadioButton(e, false);
+ }
+ }
+
+ @SuppressWarnings("serial")
+ private class SelectNextBtn extends AbstractAction {
+ public SelectNextBtn() {
+ super("Next");
+ }
+
+ @Override
+ public void actionPerformed(ActionEvent e) {
+ AquaButtonRadioUI.this.selectRadioButton(e, true);
+ }
+ }
+
private static final RecyclableSingleton<AquaButtonRadioUI> instance = new RecyclableSingletonFromDefaultConstructor<AquaButtonRadioUI>(AquaButtonRadioUI.class);
private static final RecyclableSingleton<ImageIcon> sizingIcon = new RecyclableSingleton<ImageIcon>() {
protected ImageIcon getInstance() {
@@ -45,7 +93,7 @@
return instance.get();
}
- public static Icon getSizingRadioButtonIcon(){
+ public static Icon getSizingRadioButtonIcon() {
return sizingIcon.get();
}
@@ -67,4 +115,269 @@
super(other);
}
}
+
+ private KeyListener createKeyListener() {
+ if (keyListener == null) {
+ keyListener = new KeyHandler();
+ }
+
+ return keyListener;
+ }
+
+ private boolean isValidRadioButtonObj(Object obj) {
+ return ((obj instanceof JRadioButton) &&
+ ((JRadioButton)obj).isVisible() &&
+ ((JRadioButton)obj).isEnabled());
+ }
+
+ @Override
+ protected void installListeners(AbstractButton button) {
+ super.installListeners(button);
+
+ //Only for JRadioButton
+ if (!(button instanceof JRadioButton))
+ return;
+
+ keyListener = createKeyListener();
+ button.addKeyListener(keyListener);
+
+ button.setFocusTraversalKeysEnabled(false);
+
+ button.getActionMap().put("Previous", new SelectPreviousBtn());
+ button.getActionMap().put("Next", new SelectNextBtn());
+
+ button.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).
+ put(KeyStroke.getKeyStroke("UP"), "Previous");
+ button.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).
+ put(KeyStroke.getKeyStroke("DOWN"), "Next");
+ button.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).
+ put(KeyStroke.getKeyStroke("LEFT"), "Previous");
+ button.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).
+ put(KeyStroke.getKeyStroke("RIGHT"), "Next");
+ }
+
+ @Override
+ protected void uninstallListeners(AbstractButton button) {
+ super.uninstallListeners(button);
+
+ //Only for JRadioButton
+ if (!(button instanceof JRadioButton))
+ return;
+
+ //Unmap actions from the arrow keys.
+ button.getActionMap().remove("Previous");
+ button.getActionMap().remove("Next");
+
+ button.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).
+ remove(KeyStroke.getKeyStroke("UP"));
+ button.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).
+ remove(KeyStroke.getKeyStroke("DOWN"));
+ button.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).
+ remove(KeyStroke.getKeyStroke("LEFT"));
+ button.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).
+ remove(KeyStroke.getKeyStroke("RIGHT"));
+
+ if (keyListener != null ) {
+ button.removeKeyListener(keyListener);
+ keyListener = null;
+ }
+ }
+
+ /**
+ * Select radio button based on "Previous" or "Next" operation
+ *
+ * @param event, the event object.
+ * @param next, indicate if it's next one
+ */
+ private void selectRadioButton(ActionEvent event, boolean next) {
+ Object eventSrc = event.getSource();
+
+ //Check whether the source is JRadioButton, if so, whether it is visible
+ if (!isValidRadioButtonObj(eventSrc))
+ return;
+
+ ButtonGroupInfo btnGroupInfo = new ButtonGroupInfo((JRadioButton)eventSrc);
+ btnGroupInfo.selectNewButton(next);
+ }
+
+ /**
+ * ButtonGroupInfo, used to get related info in button group
+ * for given radio button.
+ */
+ private class ButtonGroupInfo {
+ JRadioButton activeBtn = null;
+
+ JRadioButton firstBtn = null;
+ JRadioButton lastBtn = null;
+
+ JRadioButton previousBtn = null;
+ JRadioButton nextBtn = null;
+
+ HashSet<JRadioButton> btnsInGroup = null;
+ boolean srcFound = false;
+
+ public ButtonGroupInfo(JRadioButton btn) {
+ activeBtn = btn;
+ btnsInGroup = new HashSet<JRadioButton>();
+ }
+
+ //Check if given object is in the button group
+ boolean containsInGroup(Object obj) {
+ return btnsInGroup.contains(obj);
+ }
+
+ //Check if the next object to gain focus belongs
+ //to the button group or not
+ Component getFocusTransferBaseComponent(boolean next) {
+ return firstBtn;
+ }
+
+ boolean getButtonGroupInfo() {
+ if (activeBtn == null)
+ return false;
+
+ btnsInGroup.clear();
+
+ //Get the button model from ths source.
+ ButtonModel model = activeBtn.getModel();
+ if (!(model instanceof DefaultButtonModel))
+ return false;
+
+ // If the button model is DefaultButtonModel, and use it, otherwise return.
+ DefaultButtonModel bm = (DefaultButtonModel) model;
+
+ //get the ButtonGroup of the button from the button model
+ ButtonGroup group = bm.getGroup();
+ if (group == null)
+ return false;
+
+ Enumeration<AbstractButton> e = group.getElements();
+ if (e == null)
+ return false;
+
+ while (e.hasMoreElements()) {
+ AbstractButton curElement = e.nextElement();
+ if (!isValidRadioButtonObj(curElement))
+ continue;
+
+ btnsInGroup.add((JRadioButton) curElement);
+
+ // If firstBtn is not set yet, curElement is that first button
+ if (null == firstBtn)
+ firstBtn = (JRadioButton)curElement;
+
+ if (activeBtn == curElement)
+ srcFound = true;
+ else if (!srcFound) {
+ //The source has not been yet found and the current element
+ // is the last previousBtn
+ previousBtn = (JRadioButton) curElement;
+ } else if (nextBtn == null) {
+ //The source has been found and the current element
+ //is the next valid button of the list
+ nextBtn = (JRadioButton) curElement;
+ }
+
+ //Set new last "valid" JRadioButton of the list
+ lastBtn = (JRadioButton)curElement;
+ }
+
+ return true;
+ }
+
+ /**
+ * Find the new radio button that focus needs to be
+ * moved to in the group, select the button
+ *
+ * @param next, indicate if it's arrow up/left or down/right
+ */
+ void selectNewButton(boolean next) {
+ if (!getButtonGroupInfo())
+ return;
+
+ if (srcFound) {
+ JRadioButton newSelectedBtn = null;
+ if (next) {
+ //Select Next button. Cycle to the first button if the source
+ //button is the last of the group.
+ newSelectedBtn = (null == nextBtn) ? firstBtn : nextBtn;
+ } else {
+ //Select previous button. Cycle to the last button if the source
+ //button is the first button of the group.
+ newSelectedBtn = (null == previousBtn) ? lastBtn: previousBtn;
+ }
+ if (newSelectedBtn != null && newSelectedBtn != activeBtn) {
+ newSelectedBtn.requestFocusInWindow();
+ newSelectedBtn.setSelected(true);
+ }
+ }
+ }
+
+ /**
+ * Find the button group the passed in JRadioButton belongs to, and
+ * move focus to next component of the last button in the group
+ * or previous compoennt of first button
+ *
+ * @param next, indicate if jump to next component or previous
+ */
+ void jumpToNextComponent(boolean next) {
+ if (!getButtonGroupInfo()) {
+ //In case the button does not belong to any group, it needs
+ //to be treated as a component
+ if (activeBtn != null) {
+ lastBtn = activeBtn;
+ firstBtn = activeBtn;
+ } else
+ return;
+ }
+
+ //If next component in the parent window is not in the button
+ //group, current active button will be base, otherwise, the base
+ // will be first or last button in the button group
+ Component focusBase = getFocusTransferBaseComponent(next);
+ if (focusBase != null) {
+ if (next) {
+ KeyboardFocusManager.
+ getCurrentKeyboardFocusManager().focusNextComponent(focusBase);
+ } else {
+ KeyboardFocusManager.
+ getCurrentKeyboardFocusManager().focusPreviousComponent(focusBase);
+ }
+ }
+ }
+ }
+
+ /**
+ * Radiobutton KeyListener
+ */
+ private class KeyHandler implements KeyListener {
+ //This listener checks if the key event is a focus traversal key event
+ // on a radio button, consume the event if so and move the focus
+ // to next/previous component
+ @Override
+ public void keyPressed(KeyEvent e) {
+ AWTKeyStroke stroke = AWTKeyStroke.getAWTKeyStrokeForEvent(e);
+ if (stroke != null && e.getSource() instanceof JRadioButton) {
+ JRadioButton source = (JRadioButton) e.getSource();
+ boolean next = isFocusTraversalKey(source,
+ KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS, stroke);
+ if (next || isFocusTraversalKey(source,
+ KeyboardFocusManager.BACKWARD_TRAVERSAL_KEYS, stroke)) {
+ e.consume();
+ ButtonGroupInfo btnGroupInfo = new ButtonGroupInfo(source);
+ btnGroupInfo.jumpToNextComponent(next);
+ }
+ }
+ }
+
+ private boolean isFocusTraversalKey(JComponent c, int id,
+ AWTKeyStroke stroke) {
+ Set<AWTKeyStroke> keys = c.getFocusTraversalKeys(id);
+ return keys != null && keys.contains(stroke);
+ }
+
+ @Override public void keyReleased(KeyEvent e) {}
+
+ @Override public void keyTyped(KeyEvent e) {}
+ }
}
--- a/test/jdk/javax/swing/JRadioButton/8033699/bug8033699.java Tue Aug 14 09:38:29 2018 -0700
+++ b/test/jdk/javax/swing/JRadioButton/8033699/bug8033699.java Tue Aug 14 12:50:39 2018 -0700
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2014, 2016, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2014, 2018, Oracle and/or its affiliates. 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
@@ -26,7 +26,7 @@
* @key headful
* @library ../../regtesthelpers
* @build Util
- * @bug 8033699 8154043 8167160
+ * @bug 8033699 8154043 8167160 8208640
* @summary Incorrect radio button behavior when pressing tab key
* @run main bug8033699
*/
@@ -59,12 +59,9 @@
private static JRadioButton radioBtnSingle;
public static void main(String args[]) throws Throwable {
- SwingUtilities.invokeAndWait(new Runnable() {
- @Override
- public void run() {
+ SwingUtilities.invokeAndWait(() -> {
changeLAF();
createAndShowGUI();
- }
});
robot = new Robot();
@@ -96,19 +93,14 @@
// down key circle back to first button in grouped radio button
runTest8();
- SwingUtilities.invokeAndWait(new Runnable() {
- @Override
- public void run() {
- mainFrame.dispose();
- }
- });
+ SwingUtilities.invokeAndWait(() -> mainFrame.dispose());
}
private static void changeLAF() {
String currentLAF = UIManager.getLookAndFeel().toString();
System.out.println(currentLAF);
currentLAF = currentLAF.toLowerCase();
- if (currentLAF.contains("aqua") || currentLAF.contains("nimbus")) {
+ if (currentLAF.contains("nimbus")) {
try {
UIManager.setLookAndFeel("javax.swing.plaf.metal.MetalLookAndFeel");
} catch (Exception ex) {
@@ -167,13 +159,10 @@
hitKey(robot, KeyEvent.VK_TAB);
hitKey(robot, KeyEvent.VK_TAB);
- SwingUtilities.invokeAndWait(new Runnable() {
- @Override
- public void run() {
- if (KeyboardFocusManager.getCurrentKeyboardFocusManager().getFocusOwner() != radioBtnSingle) {
- System.out.println("Radio Button Group Go To Next Component through Tab Key failed");
- throw new RuntimeException("Focus is not on Radio Button Single as Expected");
- }
+ SwingUtilities.invokeAndWait(() -> {
+ if (KeyboardFocusManager.getCurrentKeyboardFocusManager().getFocusOwner() != radioBtnSingle) {
+ System.out.println("Radio Button Group Go To Next Component through Tab Key failed");
+ throw new RuntimeException("Focus is not on Radio Button Single as Expected");
}
});
}
@@ -181,13 +170,10 @@
// Non-Grouped Radio button as a single component when traversing through tab key
private static void runTest2() throws Exception {
hitKey(robot, KeyEvent.VK_TAB);
- SwingUtilities.invokeAndWait(new Runnable() {
- @Override
- public void run() {
- if (KeyboardFocusManager.getCurrentKeyboardFocusManager().getFocusOwner() != btnEnd) {
- System.out.println("Non Grouped Radio Button Go To Next Component through Tab Key failed");
- throw new RuntimeException("Focus is not on Button End as Expected");
- }
+ SwingUtilities.invokeAndWait(() -> {
+ if (KeyboardFocusManager.getCurrentKeyboardFocusManager().getFocusOwner() != btnEnd) {
+ System.out.println("Non Grouped Radio Button Go To Next Component through Tab Key failed");
+ throw new RuntimeException("Focus is not on Button End as Expected");
}
});
}
@@ -197,13 +183,10 @@
hitKey(robot, KeyEvent.VK_SHIFT, KeyEvent.VK_TAB);
hitKey(robot, KeyEvent.VK_SHIFT, KeyEvent.VK_TAB);
hitKey(robot, KeyEvent.VK_SHIFT, KeyEvent.VK_TAB);
- SwingUtilities.invokeAndWait(new Runnable() {
- @Override
- public void run() {
- if (KeyboardFocusManager.getCurrentKeyboardFocusManager().getFocusOwner() != radioBtn1) {
- System.out.println("Radio button Group/Non Grouped Radio Button SHIFT-Tab Key Test failed");
- throw new RuntimeException("Focus is not on Radio Button A as Expected");
- }
+ SwingUtilities.invokeAndWait(() -> {
+ if (KeyboardFocusManager.getCurrentKeyboardFocusManager().getFocusOwner() != radioBtn1) {
+ System.out.println("Radio button Group/Non Grouped Radio Button SHIFT-Tab Key Test failed");
+ throw new RuntimeException("Focus is not on Radio Button A as Expected");
}
});
}
@@ -212,13 +195,10 @@
private static void runTest4() throws Exception {
hitKey(robot, KeyEvent.VK_DOWN);
hitKey(robot, KeyEvent.VK_RIGHT);
- SwingUtilities.invokeAndWait(new Runnable() {
- @Override
- public void run() {
- if (KeyboardFocusManager.getCurrentKeyboardFocusManager().getFocusOwner() != radioBtn3) {
- System.out.println("Radio button Group UP/LEFT Arrow Key Move Focus Failed");
- throw new RuntimeException("Focus is not on Radio Button C as Expected");
- }
+ SwingUtilities.invokeAndWait(() -> {
+ if (KeyboardFocusManager.getCurrentKeyboardFocusManager().getFocusOwner() != radioBtn3) {
+ System.out.println("Radio button Group UP/LEFT Arrow Key Move Focus Failed");
+ throw new RuntimeException("Focus is not on Radio Button C as Expected");
}
});
}
@@ -226,13 +206,10 @@
private static void runTest5() throws Exception {
hitKey(robot, KeyEvent.VK_UP);
hitKey(robot, KeyEvent.VK_LEFT);
- SwingUtilities.invokeAndWait(new Runnable() {
- @Override
- public void run() {
- if (KeyboardFocusManager.getCurrentKeyboardFocusManager().getFocusOwner() != radioBtn1) {
- System.out.println("Radio button Group Left/Up Arrow Key Move Focus Failed");
- throw new RuntimeException("Focus is not on Radio Button A as Expected");
- }
+ SwingUtilities.invokeAndWait(() -> {
+ if (KeyboardFocusManager.getCurrentKeyboardFocusManager().getFocusOwner() != radioBtn1) {
+ System.out.println("Radio button Group Left/Up Arrow Key Move Focus Failed");
+ throw new RuntimeException("Focus is not on Radio Button A as Expected");
}
});
}
@@ -240,39 +217,30 @@
private static void runTest6() throws Exception {
hitKey(robot, KeyEvent.VK_UP);
hitKey(robot, KeyEvent.VK_UP);
- SwingUtilities.invokeAndWait(new Runnable() {
- @Override
- public void run() {
- if (KeyboardFocusManager.getCurrentKeyboardFocusManager().getFocusOwner() != radioBtn2) {
- System.out.println("Radio button Group Circle Back To First Button Test");
- throw new RuntimeException("Focus is not on Radio Button B as Expected");
- }
+ SwingUtilities.invokeAndWait(() -> {
+ if (KeyboardFocusManager.getCurrentKeyboardFocusManager().getFocusOwner() != radioBtn2) {
+ System.out.println("Radio button Group Circle Back To First Button Test");
+ throw new RuntimeException("Focus is not on Radio Button B as Expected");
}
});
}
private static void runTest7() throws Exception {
hitKey(robot, KeyEvent.VK_TAB);
- SwingUtilities.invokeAndWait(new Runnable() {
- @Override
- public void run() {
- if (KeyboardFocusManager.getCurrentKeyboardFocusManager().getFocusOwner() != btnMiddle) {
- System.out.println("Separate Component added in button group layout");
- throw new RuntimeException("Focus is not on Middle Button as Expected");
- }
+ SwingUtilities.invokeAndWait(() -> {
+ if (KeyboardFocusManager.getCurrentKeyboardFocusManager().getFocusOwner() != btnMiddle) {
+ System.out.println("Separate Component added in button group layout");
+ throw new RuntimeException("Focus is not on Middle Button as Expected");
}
});
}
private static void runTest8() throws Exception {
hitKey(robot, KeyEvent.VK_TAB);
- SwingUtilities.invokeAndWait(new Runnable() {
- @Override
- public void run() {
- if (KeyboardFocusManager.getCurrentKeyboardFocusManager().getFocusOwner() != radioBtnSingle) {
- System.out.println("Separate Component added in button group layout");
- throw new RuntimeException("Focus is not on Radio Button Single as Expected");
- }
+ SwingUtilities.invokeAndWait(() -> {
+ if (KeyboardFocusManager.getCurrentKeyboardFocusManager().getFocusOwner() != radioBtnSingle) {
+ System.out.println("Separate Component added in button group layout");
+ throw new RuntimeException("Focus is not on Radio Button Single as Expected");
}
});
}