# HG changeset patch # User van # Date 1413237580 25200 # Node ID 84c3e31f3c4a49cbdd0b3ae5eeafb4136aeabc29 # Parent 5e38c7e9b978437f6cdcfdb5692a7c83a3d618d1 8033699: Incorrect radio button behavior Reviewed-by: azvegint, alexsch diff -r 5e38c7e9b978 -r 84c3e31f3c4a src/share/classes/javax/swing/plaf/basic/BasicRadioButtonUI.java --- a/src/share/classes/javax/swing/plaf/basic/BasicRadioButtonUI.java Fri Oct 10 20:14:42 2014 +0400 +++ b/src/share/classes/javax/swing/plaf/basic/BasicRadioButtonUI.java Mon Oct 13 14:59:40 2014 -0700 @@ -1,5 +1,5 @@ /* - * Copyright (c) 1997, 2008, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1997, 2014, 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 @@ -33,7 +33,8 @@ import javax.swing.text.View; import sun.swing.SwingUtilities2; import sun.awt.AppContext; - +import java.util.Enumeration; +import java.util.HashSet; /** * RadioButtonUI implementation for BasicRadioButtonUI @@ -44,15 +45,27 @@ { private static final Object BASIC_RADIO_BUTTON_UI_KEY = new Object(); + /** + * The icon. + */ protected Icon icon; private boolean defaults_initialized = false; private final static String propertyPrefix = "RadioButton" + "."; + private KeyListener keyListener = null; + // ******************************** // Create PLAF // ******************************** + + /** + * Returns an instance of {@code BasicRadioButtonUI}. + * + * @param b a component + * @return an instance of {@code BasicRadioButtonUI} + */ public static ComponentUI createUI(JComponent b) { AppContext appContext = AppContext.getAppContext(); BasicRadioButtonUI radioButtonUI = @@ -64,6 +77,7 @@ return radioButtonUI; } + @Override protected String getPropertyPrefix() { return propertyPrefix; } @@ -71,7 +85,8 @@ // ******************************** // Install PLAF // ******************************** - protected void installDefaults(AbstractButton b){ + @Override + protected void installDefaults(AbstractButton b) { super.installDefaults(b); if(!defaults_initialized) { icon = UIManager.getIcon(getPropertyPrefix() + "icon"); @@ -82,15 +97,80 @@ // ******************************** // Uninstall PLAF // ******************************** - protected void uninstallDefaults(AbstractButton b){ + @Override + protected void uninstallDefaults(AbstractButton b) { super.uninstallDefaults(b); defaults_initialized = false; } + /** + * Returns the default icon. + * + * @return the default icon + */ public Icon getDefaultIcon() { return icon; } + // ******************************** + // Install Listeners + // ******************************** + @Override + protected void installListeners(AbstractButton button) { + super.installListeners(button); + + // Only for JRadioButton + if (!(button instanceof JRadioButton)) + return; + + keyListener = createKeyListener(); + button.addKeyListener(keyListener); + + // Need to get traversal key event + button.setFocusTraversalKeysEnabled(false); + + // Map actions to the arrow keys + 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"); + } + + // ******************************** + // UnInstall Listeners + // ******************************** + @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; + } + } /* These Dimensions/Rectangles are allocated once for all * RadioButtonUI.paint() calls. Re-using rectangles @@ -106,6 +186,7 @@ /** * paint the radio button */ + @Override public synchronized void paint(Graphics g, JComponent c) { AbstractButton b = (AbstractButton) c; ButtonModel model = b.getModel(); @@ -195,7 +276,14 @@ } } - protected void paintFocus(Graphics g, Rectangle textRect, Dimension size){ + /** + * Paints focused radio button. + * + * @param g an instance of {@code Graphics} + * @param textRect bounds + * @param size the size of radio button + */ + protected void paintFocus(Graphics g, Rectangle textRect, Dimension size) { } @@ -213,6 +301,7 @@ /** * The preferred size of the radio button */ + @Override public Dimension getPreferredSize(JComponent c) { if(c.getComponentCount() > 0) { return null; @@ -258,4 +347,262 @@ height += prefInsets.top + prefInsets.bottom; return new Dimension(width, height); } + + /////////////////////////// Private functions //////////////////////// + /** + * Creates the key listener to handle tab navigation in JRadioButton Group. + */ + 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()); + } + + /** + * 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) { + // Get the source of the event. + Object eventSrc = event.getSource(); + + // Check whether the source is JRadioButton, it so, whether it is visible + if (!isValidRadioButtonObj(eventSrc)) + return; + + ButtonGroupInfo btnGroupInfo = new ButtonGroupInfo((JRadioButton)eventSrc); + btnGroupInfo.selectNewButton(next); + } + + /////////////////////////// Inner Classes //////////////////////// + @SuppressWarnings("serial") + private class SelectPreviousBtn extends AbstractAction { + public SelectPreviousBtn() { + super("Previous"); + } + + public void actionPerformed(ActionEvent e) { + BasicRadioButtonUI.this.selectRadioButton(e, false); + } + } + + @SuppressWarnings("serial") + private class SelectNextBtn extends AbstractAction{ + public SelectNextBtn() { + super("Next"); + } + + public void actionPerformed(ActionEvent e) { + BasicRadioButtonUI.this.selectRadioButton(e, true); + } + } + + /** + * 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 btnsInGroup = null; + + boolean srcFound = false; + public ButtonGroupInfo(JRadioButton btn) { + activeBtn = btn; + btnsInGroup = new HashSet(); + } + + // 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){ + Component focusBaseComp = activeBtn; + Window container = SwingUtilities.getWindowAncestor(activeBtn); + if (container != null) { + FocusTraversalPolicy policy = container.getFocusTraversalPolicy(); + Component comp = next ? policy.getComponentAfter(container, activeBtn) + : policy.getComponentBefore(container, activeBtn); + + // If next component in the button group, use last/first button as base focus + // otherwise, use the activeBtn as the base focus + if (containsInGroup(comp)) { + focusBaseComp = next ? lastBtn : firstBtn; + } + } + + return focusBaseComp; + } + + boolean getButtonGroupInfo() { + if (activeBtn == null) + return false; + + btnsInGroup.clear(); + + // Get the button model from the 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; + + // Get all the buttons in the group + Enumeration 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 component 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; + } + + // Update the component we will use as base to transfer + // focus from + JComponent compTransferFocusFrom = activeBtn; + + // 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 KeyEvent.VK_TAB + // or shift + KeyEvent.VK_TAB event on a radio button, consume the event + // if so and move the focus to next/previous component + public void keyPressed(KeyEvent e) { + if (e.getKeyCode() == KeyEvent.VK_TAB) { + // Get the source of the event. + Object eventSrc = e.getSource(); + + // Check whether the source is a visible and enabled JRadioButton + if (isValidRadioButtonObj(eventSrc)) { + e.consume(); + ButtonGroupInfo btnGroupInfo = new ButtonGroupInfo((JRadioButton)eventSrc); + btnGroupInfo.jumpToNextComponent(!e.isShiftDown()); + } + } + } + + public void keyReleased(KeyEvent e) { + } + + public void keyTyped(KeyEvent e) { + } + } } diff -r 5e38c7e9b978 -r 84c3e31f3c4a test/javax/swing/JRadioButton/8033699/bug8033699.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test/javax/swing/JRadioButton/8033699/bug8033699.java Mon Oct 13 14:59:40 2014 -0700 @@ -0,0 +1,255 @@ +/* + * Copyright (c) 2014, 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 + * 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. + */ + + /* + * @test + * @library ../../regtesthelpers + * @build Util + * @bug 8033699 + * @summary Incorrect radio button behavior when pressing tab key + * @author Vivi An + * @run main bug8033699 + */ + +import javax.swing.*; +import javax.swing.event.*; +import java.awt.event.*; +import java.awt.*; +import sun.awt.SunToolkit; + +public class bug8033699 { + private static Robot robot; + private static SunToolkit toolkit; + + private static JButton btnStart; + private static ButtonGroup btnGrp; + private static JButton btnEnd; + private static JButton btnMiddle; + private static JRadioButton radioBtn1; + private static JRadioButton radioBtn2; + private static JRadioButton radioBtn3; + private static JRadioButton radioBtnSingle; + + public static void main(String args[]) throws Throwable { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + createAndShowGUI(); + } + }); + + robot = new Robot(); + Thread.sleep(100); + + robot.setAutoDelay(100); + toolkit = (SunToolkit) Toolkit.getDefaultToolkit(); + + // tab key test grouped radio button + runTest1(); + + // tab key test non-grouped radio button + runTest2(); + + // shift tab key test grouped and non grouped radio button + runTest3(); + + // left/up key test in grouped radio button + runTest4(); + + // down/right key test in grouped radio button + runTest5(); + + // tab from radio button in group to next component in the middle of button group layout + runTest6(); + + // tab to radio button in group from component in the middle of button group layout + runTest7(); + + // down key circle back to first button in grouped radio button + runTest8(); + } + + private static void createAndShowGUI() { + JFrame mainFrame = new JFrame("Bug 8033699 - 8 Tests for Grouped/Non Group Radio Buttons"); + + btnStart = new JButton("Start"); + btnEnd = new JButton("End"); + btnMiddle = new JButton("Middle"); + + JPanel box = new JPanel(); + box.setLayout(new BoxLayout(box, BoxLayout.Y_AXIS)); + box.setBorder(BorderFactory.createTitledBorder("Grouped Radio Buttons")); + radioBtn1 = new JRadioButton("A"); + radioBtn2 = new JRadioButton("B"); + radioBtn3 = new JRadioButton("C"); + + ButtonGroup btnGrp = new ButtonGroup(); + btnGrp.add(radioBtn1); + btnGrp.add(radioBtn2); + btnGrp.add(radioBtn3); + radioBtn1.setSelected(true); + + box.add(radioBtn1); + box.add(radioBtn2); + box.add(btnMiddle); + box.add(radioBtn3); + + radioBtnSingle = new JRadioButton("Not Grouped"); + radioBtnSingle.setSelected(true); + + mainFrame.getContentPane().add(btnStart); + mainFrame.getContentPane().add(box); + mainFrame.getContentPane().add(radioBtnSingle); + mainFrame.getContentPane().add(btnEnd); + + mainFrame.getRootPane().setDefaultButton(btnStart); + btnStart.requestFocus(); + + mainFrame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + mainFrame.setLayout(new BoxLayout(mainFrame.getContentPane(), BoxLayout.Y_AXIS)); + + mainFrame.setSize(300, 300); + mainFrame.setLocation(200, 200); + mainFrame.setVisible(true); + mainFrame.toFront(); + } + + // Radio button Group as a single component when traversing through tab key + private static void runTest1() throws Exception{ + hitKey(robot, KeyEvent.VK_TAB); + hitKey(robot, KeyEvent.VK_TAB); + + SwingUtilities.invokeAndWait(new Runnable() { + 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"); + } + } + }); + } + + // 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() { + 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"); + } + } + }); + } + + // Non-Grouped Radio button and Group Radio button as a single component when traversing through shift-tab key + private static void runTest3() throws Exception{ + hitKey(robot, KeyEvent.VK_SHIFT, KeyEvent.VK_TAB); + hitKey(robot, KeyEvent.VK_SHIFT, KeyEvent.VK_TAB); + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + if (KeyboardFocusManager.getCurrentKeyboardFocusManager().getFocusOwner() != radioBtn3) { + System.out.println("Radio button Group/Non Grouped Radio Button SHIFT-Tab Key Test failed"); + throw new RuntimeException("Focus is not on Radio Button C as Expected"); + } + } + }); + } + + // Using arrow key to move focus in radio button group + private static void runTest4() throws Exception{ + hitKey(robot, KeyEvent.VK_UP); + hitKey(robot, KeyEvent.VK_LEFT); + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + if (KeyboardFocusManager.getCurrentKeyboardFocusManager().getFocusOwner() != radioBtn1) { + System.out.println("Radio button Group UP/LEFT Arrow Key Move Focus Failed"); + throw new RuntimeException("Focus is not on Radio Button A as Expected"); + } + } + }); + } + + private static void runTest5() throws Exception{ + hitKey(robot, KeyEvent.VK_DOWN); + hitKey(robot, KeyEvent.VK_RIGHT); + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + if (KeyboardFocusManager.getCurrentKeyboardFocusManager().getFocusOwner() != radioBtn3) { + System.out.println("Radio button Group Left/Up Arrow Key Move Focus Failed"); + throw new RuntimeException("Focus is not on Radio Button C as Expected"); + } + } + }); + } + + private static void runTest6() throws Exception{ + hitKey(robot, KeyEvent.VK_DOWN); + hitKey(robot, KeyEvent.VK_DOWN); + SwingUtilities.invokeAndWait(new Runnable() { + 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 A as Expected"); + } + } + }); + } + + private static void runTest7() throws Exception{ + hitKey(robot, KeyEvent.VK_TAB); + SwingUtilities.invokeAndWait(new Runnable() { + 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"); + } + } + }); + } + + private static void runTest8() throws Exception{ + hitKey(robot, KeyEvent.VK_TAB); + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + if (KeyboardFocusManager.getCurrentKeyboardFocusManager().getFocusOwner() != radioBtn3) { + System.out.println("Separate Component added in button group layout"); + throw new RuntimeException("Focus is not on Radio Button C as Expected"); + } + } + }); + } + + private static void hitKey(Robot robot, int keycode) { + robot.keyPress(keycode); + robot.keyRelease(keycode); + toolkit.realSync(); + } + + private static void hitKey(Robot robot, int mode, int keycode) { + robot.keyPress(mode); + robot.keyPress(keycode); + robot.keyRelease(mode); + robot.keyRelease(keycode); + toolkit.realSync(); + } +}