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