1 /*
   2  * Copyright (c) 1997, 2014, 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 javax.swing.plaf.basic;
  27 
  28 import java.awt.*;
  29 import java.awt.event.*;
  30 import javax.swing.*;
  31 import javax.swing.border.*;
  32 import javax.swing.plaf.*;
  33 import javax.swing.text.View;
  34 import sun.swing.SwingUtilities2;
  35 import sun.awt.AppContext;
  36 import java.util.Enumeration;
  37 import java.util.HashSet;
  38 
  39 /**
  40  * RadioButtonUI implementation for BasicRadioButtonUI
  41  *
  42  * @author Jeff Dinkins
  43  */
  44 public class BasicRadioButtonUI extends BasicToggleButtonUI
  45 {
  46     private static final Object BASIC_RADIO_BUTTON_UI_KEY = new Object();
  47 
  48     /**
  49      * The icon.
  50      */
  51     protected Icon icon;
  52 
  53     private boolean defaults_initialized = false;
  54 
  55     private final static String propertyPrefix = "RadioButton" + ".";
  56 
  57     private KeyListener keyListener = null;
  58 
  59     // ********************************
  60     //        Create PLAF
  61     // ********************************
  62 
  63     /**
  64      * Returns an instance of {@code BasicRadioButtonUI}.
  65      *
  66      * @param b a component
  67      * @return an instance of {@code BasicRadioButtonUI}
  68      */
  69     public static ComponentUI createUI(JComponent b) {
  70         AppContext appContext = AppContext.getAppContext();
  71         BasicRadioButtonUI radioButtonUI =
  72                 (BasicRadioButtonUI) appContext.get(BASIC_RADIO_BUTTON_UI_KEY);
  73         if (radioButtonUI == null) {
  74             radioButtonUI = new BasicRadioButtonUI();
  75             appContext.put(BASIC_RADIO_BUTTON_UI_KEY, radioButtonUI);
  76         }
  77         return radioButtonUI;
  78     }
  79 
  80     @Override
  81     protected String getPropertyPrefix() {
  82         return propertyPrefix;
  83     }
  84 
  85     // ********************************
  86     //        Install PLAF
  87     // ********************************
  88     @Override
  89     protected void installDefaults(AbstractButton b) {
  90         super.installDefaults(b);
  91         if(!defaults_initialized) {
  92             icon = UIManager.getIcon(getPropertyPrefix() + "icon");
  93             defaults_initialized = true;
  94         }
  95     }
  96 
  97     // ********************************
  98     //        Uninstall PLAF
  99     // ********************************
 100     @Override
 101     protected void uninstallDefaults(AbstractButton b) {
 102         super.uninstallDefaults(b);
 103         defaults_initialized = false;
 104     }
 105 
 106     /**
 107      * Returns the default icon.
 108      *
 109      * @return the default icon
 110      */
 111     public Icon getDefaultIcon() {
 112         return icon;
 113     }
 114 
 115     // ********************************
 116     //        Install Listeners
 117     // ********************************
 118     @Override
 119     protected void installListeners(AbstractButton button) {
 120         super.installListeners(button);
 121 
 122         // Only for JRadioButton
 123         if (!(button instanceof JRadioButton))
 124             return;
 125 
 126         keyListener = createKeyListener();
 127         button.addKeyListener(keyListener);
 128 
 129         // Need to get traversal key event
 130         button.setFocusTraversalKeysEnabled(false);
 131 
 132         // Map actions to the arrow keys
 133         button.getActionMap().put("Previous", new SelectPreviousBtn());
 134         button.getActionMap().put("Next", new SelectNextBtn());
 135 
 136         button.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).
 137             put(KeyStroke.getKeyStroke("UP"), "Previous");
 138         button.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).
 139             put(KeyStroke.getKeyStroke("DOWN"), "Next");
 140         button.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).
 141             put(KeyStroke.getKeyStroke("LEFT"), "Previous");
 142         button.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).
 143             put(KeyStroke.getKeyStroke("RIGHT"), "Next");
 144     }
 145 
 146     // ********************************
 147     //        UnInstall Listeners
 148     // ********************************
 149     @Override
 150     protected void uninstallListeners(AbstractButton button) {
 151         super.uninstallListeners(button);
 152 
 153         // Only for JRadioButton
 154         if (!(button instanceof JRadioButton))
 155             return;
 156 
 157         // Unmap actions from the arrow keys
 158         button.getActionMap().remove("Previous");
 159         button.getActionMap().remove("Next");
 160         button.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT)
 161                     .remove(KeyStroke.getKeyStroke("UP"));
 162         button.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT)
 163                     .remove(KeyStroke.getKeyStroke("DOWN"));
 164         button.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT)
 165                     .remove(KeyStroke.getKeyStroke("LEFT"));
 166         button.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT)
 167                     .remove(KeyStroke.getKeyStroke("RIGHT"));
 168 
 169         if (keyListener != null) {
 170             button.removeKeyListener(keyListener);
 171             keyListener = null;
 172         }
 173     }
 174 
 175     /* These Dimensions/Rectangles are allocated once for all
 176      * RadioButtonUI.paint() calls.  Re-using rectangles
 177      * rather than allocating them in each paint call substantially
 178      * reduced the time it took paint to run.  Obviously, this
 179      * method can't be re-entered.
 180      */
 181     private static Dimension size = new Dimension();
 182     private static Rectangle viewRect = new Rectangle();
 183     private static Rectangle iconRect = new Rectangle();
 184     private static Rectangle textRect = new Rectangle();
 185 
 186     /**
 187      * paint the radio button
 188      */
 189     @Override
 190     public synchronized void paint(Graphics g, JComponent c) {
 191         AbstractButton b = (AbstractButton) c;
 192         ButtonModel model = b.getModel();
 193 
 194         Font f = c.getFont();
 195         g.setFont(f);
 196         FontMetrics fm = SwingUtilities2.getFontMetrics(c, g, f);
 197 
 198         Insets i = c.getInsets();
 199         size = b.getSize(size);
 200         viewRect.x = i.left;
 201         viewRect.y = i.top;
 202         viewRect.width = size.width - (i.right + viewRect.x);
 203         viewRect.height = size.height - (i.bottom + viewRect.y);
 204         iconRect.x = iconRect.y = iconRect.width = iconRect.height = 0;
 205         textRect.x = textRect.y = textRect.width = textRect.height = 0;
 206 
 207         Icon altIcon = b.getIcon();
 208         Icon selectedIcon = null;
 209         Icon disabledIcon = null;
 210 
 211         String text = SwingUtilities.layoutCompoundLabel(
 212             c, fm, b.getText(), altIcon != null ? altIcon : getDefaultIcon(),
 213             b.getVerticalAlignment(), b.getHorizontalAlignment(),
 214             b.getVerticalTextPosition(), b.getHorizontalTextPosition(),
 215             viewRect, iconRect, textRect,
 216             b.getText() == null ? 0 : b.getIconTextGap());
 217 
 218         // fill background
 219         if(c.isOpaque()) {
 220             g.setColor(b.getBackground());
 221             g.fillRect(0,0, size.width, size.height);
 222         }
 223 
 224 
 225         // Paint the radio button
 226         if(altIcon != null) {
 227 
 228             if(!model.isEnabled()) {
 229                 if(model.isSelected()) {
 230                    altIcon = b.getDisabledSelectedIcon();
 231                 } else {
 232                    altIcon = b.getDisabledIcon();
 233                 }
 234             } else if(model.isPressed() && model.isArmed()) {
 235                 altIcon = b.getPressedIcon();
 236                 if(altIcon == null) {
 237                     // Use selected icon
 238                     altIcon = b.getSelectedIcon();
 239                 }
 240             } else if(model.isSelected()) {
 241                 if(b.isRolloverEnabled() && model.isRollover()) {
 242                         altIcon = b.getRolloverSelectedIcon();
 243                         if (altIcon == null) {
 244                                 altIcon = b.getSelectedIcon();
 245                         }
 246                 } else {
 247                         altIcon = b.getSelectedIcon();
 248                 }
 249             } else if(b.isRolloverEnabled() && model.isRollover()) {
 250                 altIcon = b.getRolloverIcon();
 251             }
 252 
 253             if(altIcon == null) {
 254                 altIcon = b.getIcon();
 255             }
 256 
 257             altIcon.paintIcon(c, g, iconRect.x, iconRect.y);
 258 
 259         } else {
 260             getDefaultIcon().paintIcon(c, g, iconRect.x, iconRect.y);
 261         }
 262 
 263 
 264         // Draw the Text
 265         if(text != null) {
 266             View v = (View) c.getClientProperty(BasicHTML.propertyKey);
 267             if (v != null) {
 268                 v.paint(g, textRect);
 269             } else {
 270                 paintText(g, b, textRect, text);
 271             }
 272             if(b.hasFocus() && b.isFocusPainted() &&
 273                textRect.width > 0 && textRect.height > 0 ) {
 274                 paintFocus(g, textRect, size);
 275             }
 276         }
 277     }
 278 
 279     /**
 280      * Paints focused radio button.
 281      *
 282      * @param g an instance of {@code Graphics}
 283      * @param textRect bounds
 284      * @param size the size of radio button
 285      */
 286     protected void paintFocus(Graphics g, Rectangle textRect, Dimension size) {
 287     }
 288 
 289 
 290     /* These Insets/Rectangles are allocated once for all
 291      * RadioButtonUI.getPreferredSize() calls.  Re-using rectangles
 292      * rather than allocating them in each call substantially
 293      * reduced the time it took getPreferredSize() to run.  Obviously,
 294      * this method can't be re-entered.
 295      */
 296     private static Rectangle prefViewRect = new Rectangle();
 297     private static Rectangle prefIconRect = new Rectangle();
 298     private static Rectangle prefTextRect = new Rectangle();
 299     private static Insets prefInsets = new Insets(0, 0, 0, 0);
 300 
 301     /**
 302      * The preferred size of the radio button
 303      */
 304     @Override
 305     public Dimension getPreferredSize(JComponent c) {
 306         if(c.getComponentCount() > 0) {
 307             return null;
 308         }
 309 
 310         AbstractButton b = (AbstractButton) c;
 311 
 312         String text = b.getText();
 313 
 314         Icon buttonIcon = b.getIcon();
 315         if(buttonIcon == null) {
 316             buttonIcon = getDefaultIcon();
 317         }
 318 
 319         Font font = b.getFont();
 320         FontMetrics fm = b.getFontMetrics(font);
 321 
 322         prefViewRect.x = prefViewRect.y = 0;
 323         prefViewRect.width = Short.MAX_VALUE;
 324         prefViewRect.height = Short.MAX_VALUE;
 325         prefIconRect.x = prefIconRect.y = prefIconRect.width = prefIconRect.height = 0;
 326         prefTextRect.x = prefTextRect.y = prefTextRect.width = prefTextRect.height = 0;
 327 
 328         SwingUtilities.layoutCompoundLabel(
 329             c, fm, text, buttonIcon,
 330             b.getVerticalAlignment(), b.getHorizontalAlignment(),
 331             b.getVerticalTextPosition(), b.getHorizontalTextPosition(),
 332             prefViewRect, prefIconRect, prefTextRect,
 333             text == null ? 0 : b.getIconTextGap());
 334 
 335         // find the union of the icon and text rects (from Rectangle.java)
 336         int x1 = Math.min(prefIconRect.x, prefTextRect.x);
 337         int x2 = Math.max(prefIconRect.x + prefIconRect.width,
 338                           prefTextRect.x + prefTextRect.width);
 339         int y1 = Math.min(prefIconRect.y, prefTextRect.y);
 340         int y2 = Math.max(prefIconRect.y + prefIconRect.height,
 341                           prefTextRect.y + prefTextRect.height);
 342         int width = x2 - x1;
 343         int height = y2 - y1;
 344 
 345         prefInsets = b.getInsets(prefInsets);
 346         width += prefInsets.left + prefInsets.right;
 347         height += prefInsets.top + prefInsets.bottom;
 348         return new Dimension(width, height);
 349     }
 350 
 351     /////////////////////////// Private functions ////////////////////////
 352     /**
 353      * Creates the key listener to handle tab navigation in JRadioButton Group.
 354      */
 355     private KeyListener createKeyListener() {
 356          if (keyListener == null) {
 357             keyListener = new KeyHandler();
 358         }
 359         return keyListener;
 360     }
 361 
 362 
 363     private boolean isValidRadioButtonObj(Object obj) {
 364         return ((obj instanceof JRadioButton) &&
 365                     ((JRadioButton) obj).isVisible() &&
 366                     ((JRadioButton) obj).isEnabled());
 367     }
 368 
 369     /**
 370      * Select radio button based on "Previous" or "Next" operation
 371      *
 372      * @param event, the event object.
 373      * @param next, indicate if it's next one
 374      */
 375     private void selectRadioButton(ActionEvent event, boolean next) {
 376         // Get the source of the event.
 377         Object eventSrc = event.getSource();
 378 
 379         // Check whether the source is JRadioButton, it so, whether it is visible
 380         if (!isValidRadioButtonObj(eventSrc))
 381             return;
 382 
 383         ButtonGroupInfo btnGroupInfo = new ButtonGroupInfo((JRadioButton)eventSrc);
 384         btnGroupInfo.selectNewButton(next);
 385     }
 386 
 387     /////////////////////////// Inner Classes ////////////////////////
 388     @SuppressWarnings("serial")
 389     private class SelectPreviousBtn extends AbstractAction {
 390         public SelectPreviousBtn() {
 391             super("Previous");
 392         }
 393 
 394         public void actionPerformed(ActionEvent e) {
 395            BasicRadioButtonUI.this.selectRadioButton(e, false);
 396         }
 397     }
 398 
 399     @SuppressWarnings("serial")
 400     private class SelectNextBtn extends AbstractAction{
 401         public SelectNextBtn() {
 402             super("Next");
 403         }
 404 
 405         public void actionPerformed(ActionEvent e) {
 406             BasicRadioButtonUI.this.selectRadioButton(e, true);
 407         }
 408     }
 409 
 410     /**
 411      * ButtonGroupInfo, used to get related info in button group
 412      * for given radio button
 413      */
 414     private class ButtonGroupInfo {
 415 
 416         JRadioButton activeBtn = null;
 417 
 418         JRadioButton firstBtn = null;
 419         JRadioButton lastBtn = null;
 420 
 421         JRadioButton previousBtn = null;
 422         JRadioButton nextBtn = null;
 423 
 424         HashSet<JRadioButton> btnsInGroup = null;
 425 
 426         boolean srcFound = false;
 427         public ButtonGroupInfo(JRadioButton btn) {
 428             activeBtn = btn;
 429             btnsInGroup = new HashSet<JRadioButton>();
 430         }
 431 
 432         // Check if given object is in the button group
 433         boolean containsInGroup(Object obj){
 434            return btnsInGroup.contains(obj);
 435         }
 436 
 437         // Check if the next object to gain focus belongs
 438         // to the button group or not
 439         Component getFocusTransferBaseComponent(boolean next){
 440             Component focusBaseComp = activeBtn;
 441             Window container = SwingUtilities.getWindowAncestor(activeBtn);
 442             if (container != null) {
 443                 FocusTraversalPolicy policy = container.getFocusTraversalPolicy();
 444                 Component comp = next ? policy.getComponentAfter(container, activeBtn)
 445                                       : policy.getComponentBefore(container, activeBtn);
 446 
 447                 // If next component in the button group, use last/first button as base focus
 448                 // otherwise, use the activeBtn as the base focus
 449                 if (containsInGroup(comp)) {
 450                     focusBaseComp = next ? lastBtn : firstBtn;
 451                 }
 452             }
 453 
 454             return focusBaseComp;
 455         }
 456 
 457         boolean getButtonGroupInfo() {
 458             if (activeBtn == null)
 459                 return false;
 460 
 461             btnsInGroup.clear();
 462 
 463             // Get the button model from the source.
 464             ButtonModel model = activeBtn.getModel();
 465             if (!(model instanceof DefaultButtonModel))
 466                 return false;
 467 
 468             // If the button model is DefaultButtonModel, and use it, otherwise return.
 469             DefaultButtonModel bm = (DefaultButtonModel) model;
 470 
 471             // get the ButtonGroup of the button from the button model
 472             ButtonGroup group = bm.getGroup();
 473             if (group == null)
 474                 return false;
 475 
 476             // Get all the buttons in the group
 477             Enumeration<AbstractButton> e = group.getElements();
 478             if (e == null)
 479                 return false;
 480 
 481             while (e.hasMoreElements()) {
 482                 AbstractButton curElement = e.nextElement();
 483                 if (!isValidRadioButtonObj(curElement))
 484                     continue;
 485 
 486                 btnsInGroup.add((JRadioButton) curElement);
 487 
 488                 // If firstBtn is not set yet, curElement is that first button
 489                 if (null == firstBtn)
 490                     firstBtn = (JRadioButton) curElement;
 491 
 492                 if (activeBtn == curElement)
 493                     srcFound = true;
 494                 else if (!srcFound) {
 495                     // The source has not been yet found and the current element
 496                     // is the last previousBtn
 497                     previousBtn = (JRadioButton) curElement;
 498                 } else if (nextBtn == null) {
 499                     // The source has been found and the current element
 500                     // is the next valid button of the list
 501                     nextBtn = (JRadioButton) curElement;
 502                 }
 503 
 504                 // Set new last "valid" JRadioButton of the list
 505                 lastBtn = (JRadioButton) curElement;
 506             }
 507 
 508             return true;
 509         }
 510 
 511         /**
 512           * Find the new radio button that focus needs to be
 513           * moved to in the group, select the button
 514           *
 515           * @param next, indicate if it's arrow up/left or down/right
 516           */
 517         void selectNewButton(boolean next) {
 518             if (!getButtonGroupInfo())
 519                 return;
 520 
 521             if (srcFound) {
 522                 JRadioButton newSelectedBtn = null;
 523                 if (next) {
 524                     // Select Next button. Cycle to the first button if the source
 525                     // button is the last of the group.
 526                     newSelectedBtn = (null == nextBtn) ? firstBtn : nextBtn;
 527                 } else {
 528                     // Select previous button. Cycle to the last button if the source
 529                     // button is the first button of the group.
 530                     newSelectedBtn = (null == previousBtn) ? lastBtn : previousBtn;
 531                 }
 532                 if (newSelectedBtn != null &&
 533                     (newSelectedBtn != activeBtn)) {
 534                     newSelectedBtn.requestFocusInWindow();
 535                     newSelectedBtn.setSelected(true);
 536                 }
 537             }
 538         }
 539 
 540         /**
 541           * Find the button group the passed in JRadioButton belongs to, and
 542           * move focus to next component of the last button in the group
 543           * or previous component of first button
 544           *
 545           * @param next, indicate if jump to next component or previous
 546           */
 547         void jumpToNextComponent(boolean next) {
 548             if (!getButtonGroupInfo()){
 549                 // In case the button does not belong to any group, it needs
 550                 // to be treated as a component
 551                 if (activeBtn != null){
 552                     lastBtn = activeBtn;
 553                     firstBtn = activeBtn;
 554                 }
 555                 else
 556                     return;
 557             }
 558 
 559             // Update the component we will use as base to transfer
 560             // focus from
 561             JComponent compTransferFocusFrom = activeBtn;
 562 
 563             // If next component in the parent window is not in
 564             // the button group, current active button will be
 565             // base, otherwise, the base will be first or last
 566             // button in the button group
 567             Component focusBase = getFocusTransferBaseComponent(next);
 568             if (focusBase != null){
 569                 if (next) {
 570                     KeyboardFocusManager.
 571                         getCurrentKeyboardFocusManager().focusNextComponent(focusBase);
 572                 } else {
 573                     KeyboardFocusManager.
 574                         getCurrentKeyboardFocusManager().focusPreviousComponent(focusBase);
 575                 }
 576             }
 577         }
 578     }
 579 
 580     /**
 581      * Radiobutton KeyListener
 582      */
 583     private class KeyHandler implements KeyListener {
 584 
 585         // This listener checks if the key event is a KeyEvent.VK_TAB
 586         // or shift + KeyEvent.VK_TAB event on a radio button, consume the event
 587         // if so and move the focus to next/previous component
 588         public void keyPressed(KeyEvent e) {
 589             if (e.getKeyCode() == KeyEvent.VK_TAB) {
 590                  // Get the source of the event.
 591                 Object eventSrc = e.getSource();
 592 
 593                 // Check whether the source is a visible and enabled JRadioButton
 594                 if (isValidRadioButtonObj(eventSrc)) {
 595                     e.consume();
 596                     ButtonGroupInfo btnGroupInfo = new ButtonGroupInfo((JRadioButton)eventSrc);
 597                     btnGroupInfo.jumpToNextComponent(!e.isShiftDown());
 598                 }
 599             }
 600         }
 601 
 602         public void keyReleased(KeyEvent e) {
 603         }
 604 
 605         public void keyTyped(KeyEvent e) {
 606         }
 607     }
 608 }