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 }