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 }