BasicRadioButtonUI.java revision 12345:fbf897c33625
1123682Sache/* 2123682Sache * Copyright (c) 1997, 2015, Oracle and/or its affiliates. All rights reserved. 3123682Sache * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4123682Sache * 5123682Sache * This code is free software; you can redistribute it and/or modify it 6123682Sache * under the terms of the GNU General Public License version 2 only, as 7123682Sache * published by the Free Software Foundation. Oracle designates this 8123682Sache * particular file as subject to the "Classpath" exception as provided 9123682Sache * by Oracle in the LICENSE file that accompanied this code. 10123682Sache * 11123682Sache * This code is distributed in the hope that it will be useful, but WITHOUT 12123682Sache * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13123682Sache * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 14123682Sache * version 2 for more details (a copy is included in the LICENSE file that 15123682Sache * accompanied this code). 16123682Sache * 17123682Sache * You should have received a copy of the GNU General Public License version 18123682Sache * 2 along with this work; if not, write to the Free Software Foundation, 19123682Sache * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 20123682Sache * 21123682Sache * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 22123682Sache * or visit www.oracle.com if you need additional information or have any 23123682Sache * questions. 24123682Sache */ 25123682Sache 26123682Sachepackage javax.swing.plaf.basic; 27123682Sache 28123682Sacheimport java.awt.*; 29123682Sacheimport java.awt.event.*; 30123682Sacheimport javax.swing.*; 31123682Sacheimport javax.swing.border.*; 32123682Sacheimport javax.swing.plaf.*; 33123682Sacheimport javax.swing.text.View; 34123682Sacheimport sun.swing.SwingUtilities2; 35123682Sacheimport sun.awt.AppContext; 36123682Sacheimport java.util.Enumeration; 37123682Sacheimport java.util.HashSet; 38123682Sacheimport java.util.Set; 39123682Sache 40123682Sache/** 41123682Sache * RadioButtonUI implementation for BasicRadioButtonUI 42123682Sache * 43123682Sache * @author Jeff Dinkins 44123682Sache */ 45123682Sachepublic class BasicRadioButtonUI extends BasicToggleButtonUI 46123682Sache{ 47123682Sache private static final Object BASIC_RADIO_BUTTON_UI_KEY = new Object(); 48123682Sache 49123682Sache /** 50123682Sache * The icon. 51123682Sache */ 52123682Sache protected Icon icon; 53123682Sache 54123682Sache private boolean defaults_initialized = false; 55123682Sache 56123682Sache private final static String propertyPrefix = "RadioButton" + "."; 57123682Sache 58123682Sache private KeyListener keyListener = null; 59123682Sache 60123682Sache // ******************************** 61123682Sache // Create PLAF 62123682Sache // ******************************** 63123682Sache 64123682Sache /** 65123682Sache * Returns an instance of {@code BasicRadioButtonUI}. 66123682Sache * 67123682Sache * @param b a component 68123682Sache * @return an instance of {@code BasicRadioButtonUI} 69123682Sache */ 70123682Sache public static ComponentUI createUI(JComponent b) { 71123682Sache AppContext appContext = AppContext.getAppContext(); 72123682Sache BasicRadioButtonUI radioButtonUI = 73123682Sache (BasicRadioButtonUI) appContext.get(BASIC_RADIO_BUTTON_UI_KEY); 74123682Sache if (radioButtonUI == null) { 75123682Sache radioButtonUI = new BasicRadioButtonUI(); 76123682Sache appContext.put(BASIC_RADIO_BUTTON_UI_KEY, radioButtonUI); 77123682Sache } 78123682Sache return radioButtonUI; 79123682Sache } 80123682Sache 81123682Sache @Override 82123682Sache protected String getPropertyPrefix() { 83123682Sache return propertyPrefix; 84123682Sache } 85123682Sache 86123682Sache // ******************************** 87123682Sache // Install PLAF 88123682Sache // ******************************** 89123682Sache @Override 90123682Sache protected void installDefaults(AbstractButton b) { 91123682Sache super.installDefaults(b); 92123682Sache if(!defaults_initialized) { 93123682Sache icon = UIManager.getIcon(getPropertyPrefix() + "icon"); 94123682Sache defaults_initialized = true; 95123682Sache } 96123682Sache } 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 Component focusBaseComp = activeBtn; 442 Container container = focusBaseComp.getFocusCycleRootAncestor(); 443 if (container != null) { 444 FocusTraversalPolicy policy = container.getFocusTraversalPolicy(); 445 Component comp = next ? policy.getComponentAfter(container, activeBtn) 446 : policy.getComponentBefore(container, activeBtn); 447 448 // If next component in the button group, use last/first button as base focus 449 // otherwise, use the activeBtn as the base focus 450 if (containsInGroup(comp)) { 451 focusBaseComp = next ? lastBtn : firstBtn; 452 } 453 } 454 455 return focusBaseComp; 456 } 457 458 boolean getButtonGroupInfo() { 459 if (activeBtn == null) 460 return false; 461 462 btnsInGroup.clear(); 463 464 // Get the button model from the source. 465 ButtonModel model = activeBtn.getModel(); 466 if (!(model instanceof DefaultButtonModel)) 467 return false; 468 469 // If the button model is DefaultButtonModel, and use it, otherwise return. 470 DefaultButtonModel bm = (DefaultButtonModel) model; 471 472 // get the ButtonGroup of the button from the button model 473 ButtonGroup group = bm.getGroup(); 474 if (group == null) 475 return false; 476 477 // Get all the buttons in the group 478 Enumeration<AbstractButton> e = group.getElements(); 479 if (e == null) 480 return false; 481 482 while (e.hasMoreElements()) { 483 AbstractButton curElement = e.nextElement(); 484 if (!isValidRadioButtonObj(curElement)) 485 continue; 486 487 btnsInGroup.add((JRadioButton) curElement); 488 489 // If firstBtn is not set yet, curElement is that first button 490 if (null == firstBtn) 491 firstBtn = (JRadioButton) curElement; 492 493 if (activeBtn == curElement) 494 srcFound = true; 495 else if (!srcFound) { 496 // The source has not been yet found and the current element 497 // is the last previousBtn 498 previousBtn = (JRadioButton) curElement; 499 } else if (nextBtn == null) { 500 // The source has been found and the current element 501 // is the next valid button of the list 502 nextBtn = (JRadioButton) curElement; 503 } 504 505 // Set new last "valid" JRadioButton of the list 506 lastBtn = (JRadioButton) curElement; 507 } 508 509 return true; 510 } 511 512 /** 513 * Find the new radio button that focus needs to be 514 * moved to in the group, select the button 515 * 516 * @param next, indicate if it's arrow up/left or down/right 517 */ 518 void selectNewButton(boolean next) { 519 if (!getButtonGroupInfo()) 520 return; 521 522 if (srcFound) { 523 JRadioButton newSelectedBtn = null; 524 if (next) { 525 // Select Next button. Cycle to the first button if the source 526 // button is the last of the group. 527 newSelectedBtn = (null == nextBtn) ? firstBtn : nextBtn; 528 } else { 529 // Select previous button. Cycle to the last button if the source 530 // button is the first button of the group. 531 newSelectedBtn = (null == previousBtn) ? lastBtn : previousBtn; 532 } 533 if (newSelectedBtn != null && 534 (newSelectedBtn != activeBtn)) { 535 newSelectedBtn.requestFocusInWindow(); 536 newSelectedBtn.setSelected(true); 537 } 538 } 539 } 540 541 /** 542 * Find the button group the passed in JRadioButton belongs to, and 543 * move focus to next component of the last button in the group 544 * or previous component of first button 545 * 546 * @param next, indicate if jump to next component or previous 547 */ 548 void jumpToNextComponent(boolean next) { 549 if (!getButtonGroupInfo()){ 550 // In case the button does not belong to any group, it needs 551 // to be treated as a component 552 if (activeBtn != null){ 553 lastBtn = activeBtn; 554 firstBtn = activeBtn; 555 } 556 else 557 return; 558 } 559 560 // Update the component we will use as base to transfer 561 // focus from 562 JComponent compTransferFocusFrom = activeBtn; 563 564 // If next component in the parent window is not in 565 // the button group, current active button will be 566 // base, otherwise, the base will be first or last 567 // button in the button group 568 Component focusBase = getFocusTransferBaseComponent(next); 569 if (focusBase != null){ 570 if (next) { 571 KeyboardFocusManager. 572 getCurrentKeyboardFocusManager().focusNextComponent(focusBase); 573 } else { 574 KeyboardFocusManager. 575 getCurrentKeyboardFocusManager().focusPreviousComponent(focusBase); 576 } 577 } 578 } 579 } 580 581 /** 582 * Radiobutton KeyListener 583 */ 584 private class KeyHandler implements KeyListener { 585 586 // This listener checks if the key event is a focus traversal key event 587 // on a radio button, consume the event if so and move the focus 588 // to next/previous component 589 public void keyPressed(KeyEvent e) { 590 AWTKeyStroke stroke = AWTKeyStroke.getAWTKeyStrokeForEvent(e); 591 if (stroke != null && e.getSource() instanceof JRadioButton) { 592 JRadioButton source = (JRadioButton) e.getSource(); 593 boolean next = isFocusTraversalKey(source, 594 KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS, 595 stroke); 596 if (next || isFocusTraversalKey(source, 597 KeyboardFocusManager.BACKWARD_TRAVERSAL_KEYS, 598 stroke)) { 599 e.consume(); 600 ButtonGroupInfo btnGroupInfo = new ButtonGroupInfo(source); 601 btnGroupInfo.jumpToNextComponent(next); 602 } 603 } 604 } 605 606 private boolean isFocusTraversalKey(JComponent c, int id, 607 AWTKeyStroke stroke) { 608 Set<AWTKeyStroke> keys = c.getFocusTraversalKeys(id); 609 return keys != null && keys.contains(stroke); 610 } 611 612 public void keyReleased(KeyEvent e) { 613 } 614 615 public void keyTyped(KeyEvent e) { 616 } 617 } 618} 619