1 /* 2 * Copyright (c) 2011, 2018, Oracle and/or its affiliates. All rights reserved. 3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4 * 5 * This code is free software; you can redistribute it and/or modify it 6 * under the terms of the GNU General Public License version 2 only, as 7 * published by the Free Software Foundation. Oracle designates this 8 * particular file as subject to the "Classpath" exception as provided 9 * by Oracle in the LICENSE file that accompanied this code. 10 * 11 * This code is distributed in the hope that it will be useful, but WITHOUT 12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 14 * version 2 for more details (a copy is included in the LICENSE file that 15 * accompanied this code). 16 * 17 * You should have received a copy of the GNU General Public License version 18 * 2 along with this work; if not, write to the Free Software Foundation, 19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 20 * 21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 22 * or visit www.oracle.com if you need additional information or have any 23 * questions. 24 */ 25 26 package com.apple.laf; 27 28 import javax.swing.JComponent; 29 import javax.swing.ImageIcon; 30 import javax.swing.JRadioButton; 31 import javax.swing.Icon; 32 import javax.swing.AbstractButton; 33 import javax.swing.AbstractAction; 34 import javax.swing.KeyStroke; 35 import javax.swing.DefaultButtonModel; 36 import javax.swing.ButtonGroup; 37 import javax.swing.ButtonModel; 38 import javax.swing.plaf.ComponentUI; 39 40 import java.awt.Component; 41 import java.awt.AWTKeyStroke; 42 import java.awt.KeyboardFocusManager; 43 44 import java.awt.event.ActionEvent; 45 import java.awt.event.KeyListener; 46 import java.awt.event.KeyEvent; 47 48 import apple.laf.JRSUIConstants.Widget; 49 import com.apple.laf.AquaUtilControlSize.SizeVariant; 50 import com.apple.laf.AquaUtilControlSize.SizeDescriptor; 51 import com.apple.laf.AquaUtils.RecyclableSingleton; 52 import com.apple.laf.AquaUtils.RecyclableSingletonFromDefaultConstructor; 53 54 import java.util.HashSet; 55 import java.util.Set; 56 import java.util.Enumeration; 57 58 public class AquaButtonRadioUI extends AquaButtonLabeledUI { 59 private KeyListener keyListener = null; 60 61 @SuppressWarnings("serial") 62 private class SelectPreviousBtn extends AbstractAction { 63 public SelectPreviousBtn() { 64 super("Previous"); 65 } 66 67 @Override 68 public void actionPerformed(ActionEvent e) { 69 AquaButtonRadioUI.this.selectRadioButton(e, false); 70 } 71 } 72 73 @SuppressWarnings("serial") 74 private class SelectNextBtn extends AbstractAction { 75 public SelectNextBtn() { 76 super("Next"); 77 } 78 79 @Override 80 public void actionPerformed(ActionEvent e){ 81 AquaButtonRadioUI.this.selectRadioButton(e, true); 82 } 83 } 84 85 private static final RecyclableSingleton<AquaButtonRadioUI> instance = new RecyclableSingletonFromDefaultConstructor<AquaButtonRadioUI>(AquaButtonRadioUI.class); 86 private static final RecyclableSingleton<ImageIcon> sizingIcon = new RecyclableSingleton<ImageIcon>() { 87 protected ImageIcon getInstance() { 88 return new ImageIcon(AquaNativeResources.getRadioButtonSizerImage()); 89 } 90 }; 91 92 public static ComponentUI createUI(final JComponent b) { 93 return instance.get(); 94 } 95 96 public static Icon getSizingRadioButtonIcon(){ 97 return sizingIcon.get(); 98 } 99 100 protected String getPropertyPrefix() { 101 return "RadioButton" + "."; 102 } 103 104 protected AquaButtonBorder getPainter() { 105 return new RadioButtonBorder(); 106 } 107 108 public static class RadioButtonBorder extends LabeledButtonBorder { 109 public RadioButtonBorder() { 110 super(new SizeDescriptor(new SizeVariant().replaceMargins("RadioButton.margin"))); 111 painter.state.set(Widget.BUTTON_RADIO); 112 } 113 114 public RadioButtonBorder(final RadioButtonBorder other) { 115 super(other); 116 } 117 } 118 119 private KeyListener createKeyListener() { 120 if (keyListener == null) { 121 keyListener = new KeyHandler(); 122 } 123 124 return keyListener; 125 } 126 127 private boolean isValidRadioButtonObj(Object obj) { 128 return ((obj instanceof JRadioButton) && 129 ((JRadioButton)obj).isVisible() && 130 ((JRadioButton)obj).isEnabled()); 131 } 132 @Override 133 protected void installListeners(AbstractButton button) { 134 super.installListeners(button); 135 136 //Only for JRadioButton 137 if (!(button instanceof JRadioButton)) 138 return; 139 140 keyListener = createKeyListener(); 141 button.addKeyListener(keyListener); 142 143 button.setFocusTraversalKeysEnabled(false); 144 145 button.getActionMap().put("Previous", new SelectPreviousBtn()); 146 button.getActionMap().put("Next", new SelectNextBtn()); 147 148 button.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT). 149 put(KeyStroke.getKeyStroke("UP"), "Previous"); 150 button.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT). 151 put(KeyStroke.getKeyStroke("DOWN"), "Next"); 152 button.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT). 153 put(KeyStroke.getKeyStroke("LEFT"), "Previous"); 154 button.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT). 155 put(KeyStroke.getKeyStroke("RIGHT"), "Next"); 156 } 157 158 @Override 159 protected void uninstallListeners(AbstractButton button) { 160 super.uninstallListeners(button); 161 162 //Only for JRadioButton 163 if (!(button instanceof JRadioButton)) 164 return; 165 166 //Unmap actions from the arrow keys. 167 button.getActionMap().remove("Previous"); 168 button.getActionMap().remove("Next"); 169 170 button.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT). 171 remove(KeyStroke.getKeyStroke("UP")); 172 button.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT). 173 remove(KeyStroke.getKeyStroke("DOWN")); 174 button.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT). 175 remove(KeyStroke.getKeyStroke("LEFT")); 176 button.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT). 177 remove(KeyStroke.getKeyStroke("RIGHT")); 178 179 if (keyListener != null ) { 180 button.removeKeyListener(keyListener); 181 keyListener = null; 182 } 183 } 184 185 /** 186 * Select radio button based on "Previous" or "Next" operation 187 * 188 * @param event, the event object. 189 * @param next, indicate if it's next one 190 */ 191 private void selectRadioButton(ActionEvent event, boolean next) { 192 Object eventSrc = event.getSource(); 193 194 //Check whether the source is JRadioButton, if so, whether it is visible 195 if(!isValidRadioButtonObj(eventSrc)) 196 return; 197 198 ButtonGroupInfo btnGroupInfo = new ButtonGroupInfo((JRadioButton)eventSrc); 199 btnGroupInfo.selectNewButton(next); 200 } 201 202 /** 203 * ButtonGroupInfo, used to get related info in button group 204 * for given radio button. 205 */ 206 private class ButtonGroupInfo { 207 JRadioButton activeBtn = null; 208 209 JRadioButton firstBtn = null; 210 JRadioButton lastBtn = null; 211 212 JRadioButton previousBtn = null; 213 JRadioButton nextBtn = null; 214 215 HashSet<JRadioButton> btnsInGroup = null; 216 boolean srcFound = false; 217 218 public ButtonGroupInfo(JRadioButton btn) { 219 activeBtn = btn; 220 btnsInGroup = new HashSet<JRadioButton>(); 221 } 222 223 //Check if given object is in the button group 224 boolean containsInGroup(Object obj){ 225 return btnsInGroup.contains(obj); 226 } 227 228 //Check if the next object to gain focus belongs 229 //to the button group or not 230 Component getFocusTransferBaseComponent(boolean next) { 231 return firstBtn; 232 } 233 234 boolean getButtonGroupInfo() { 235 if (activeBtn == null) 236 return false; 237 238 btnsInGroup.clear(); 239 240 //Get the button model from ths source. 241 ButtonModel model = activeBtn.getModel(); 242 if (!(model instanceof DefaultButtonModel)) 243 return false; 244 245 // If the button model is DefaultButtonModel, and use it, otherwise return. 246 DefaultButtonModel bm = (DefaultButtonModel) model; 247 248 //get the ButtonGroup of the button from the button model 249 ButtonGroup group = bm.getGroup(); 250 if (group == null) 251 return false; 252 253 Enumeration<AbstractButton> e = group.getElements(); 254 if (e == null) 255 return false; 256 257 while(e.hasMoreElements()) { 258 AbstractButton curElement = e.nextElement(); 259 if(!isValidRadioButtonObj(curElement)) 260 continue; 261 262 btnsInGroup.add((JRadioButton) curElement); 263 264 // If firstBtn is not set yet, curElement is that first button 265 if (null == firstBtn) 266 firstBtn = (JRadioButton)curElement; 267 268 if (activeBtn == curElement) 269 srcFound = true; 270 else if (!srcFound) { 271 //The source has not been yet found and the current element 272 // is the last previousBtn 273 previousBtn = (JRadioButton) curElement; 274 } else if (nextBtn == null) { 275 //The source has been found and the current element 276 //is the next valid button of the list 277 nextBtn = (JRadioButton) curElement; 278 } 279 280 //Set new last "valid" JRadioButton of the list 281 lastBtn = (JRadioButton)curElement; 282 } 283 284 return true; 285 } 286 287 /** 288 * Find the new radio button that focus needs to be 289 * moved to in the group, select the button 290 * 291 * @param next, indicate if it's arrow up/left or down/right 292 */ 293 void selectNewButton(boolean next) { 294 if (!getButtonGroupInfo()) 295 return; 296 297 if (srcFound) { 298 JRadioButton newSelectedBtn = null; 299 if (next) { 300 //Select Next button. Cycle to the first button if the source 301 //button is the last of the group. 302 newSelectedBtn = (null == nextBtn) ? firstBtn : nextBtn; 303 } else { 304 //Select previous button. Cycle to the last button if the source 305 //button is the first button of the group. 306 newSelectedBtn = (null == previousBtn) ? lastBtn: previousBtn; 307 } 308 if (newSelectedBtn != null && newSelectedBtn != activeBtn) { 309 newSelectedBtn.requestFocusInWindow(); 310 newSelectedBtn.setSelected(true); 311 } 312 } 313 } 314 315 /** 316 * Find the button group the passed in JRadioButton belongs to, and 317 * move focus to next component of the last button in the group 318 * or previous compoennt of first button 319 * 320 * @param next, indicate if jump to next component or previous 321 */ 322 void jumpToNextComponent(boolean next) { 323 if (!getButtonGroupInfo()) { 324 //In case the button does not belong to any group, it needs 325 //to be treated as a component 326 if(activeBtn != null) { 327 lastBtn = activeBtn; 328 firstBtn = activeBtn; 329 } else 330 return; 331 } 332 333 //If next component in the parent window is not in the button 334 //group, current active button will be base, otherwise, the base 335 // will be first or last button in the button group 336 Component focusBase = getFocusTransferBaseComponent(next); 337 if (focusBase != null){ 338 if (next) { 339 KeyboardFocusManager. 340 getCurrentKeyboardFocusManager().focusNextComponent(focusBase); 341 } else { 342 KeyboardFocusManager. 343 getCurrentKeyboardFocusManager().focusPreviousComponent(focusBase); 344 } 345 } 346 } 347 } 348 349 /** 350 * Radiobutton KeyListener 351 */ 352 private class KeyHandler implements KeyListener { 353 //This listener checks if the key event is a focus traversal key event 354 // on a radio button, consume the event if so and move the focus 355 // to next/previous component 356 @Override 357 public void keyPressed(KeyEvent e) { 358 AWTKeyStroke stroke = AWTKeyStroke.getAWTKeyStrokeForEvent(e); 359 if (stroke != null && e.getSource() instanceof JRadioButton) { 360 JRadioButton source = (JRadioButton) e.getSource(); 361 boolean next = isFocusTraversalKey(source, 362 KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS, stroke); 363 if (next || isFocusTraversalKey(source, 364 KeyboardFocusManager.BACKWARD_TRAVERSAL_KEYS, stroke)) { 365 e.consume(); 366 ButtonGroupInfo btnGroupInfo = new ButtonGroupInfo(source); 367 btnGroupInfo.jumpToNextComponent(next); 368 } 369 } 370 } 371 372 private boolean isFocusTraversalKey(JComponent c, int id, 373 AWTKeyStroke stroke) { 374 Set<AWTKeyStroke> keys = c.getFocusTraversalKeys(id); 375 return keys != null && keys.contains(stroke); 376 } 377 378 @Override public void keyReleased(KeyEvent e) {} 379 380 @Override public void keyTyped(KeyEvent e) {} 381 } 382 }