8208640: [a11y] [macos] Unable to navigate between Radiobuttons in Radio group using keyboard.
authorkaddepalli
Tue, 14 Aug 2018 12:50:39 -0700
changeset 51454 06417e487a28
parent 51453 59614cd6d8dc
child 51455 893c44b263fb
8208640: [a11y] [macos] Unable to navigate between Radiobuttons in Radio group using keyboard. Reviewed-by: prr, serb, psadhukhan, ssadetsky
src/java.desktop/macosx/classes/com/apple/laf/AquaButtonRadioUI.java
test/jdk/javax/swing/JRadioButton/8033699/bug8033699.java
--- 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");
             }
         });
     }