1 /* 2 * Copyright (c) 2011, 2012, 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 java.awt.*; 29 import java.awt.event.*; 30 import java.beans.PropertyChangeEvent; 31 32 import javax.swing.*; 33 import javax.swing.border.Border; 34 import javax.swing.event.*; 35 import javax.swing.plaf.*; 36 import javax.swing.plaf.basic.*; 37 import javax.swing.text.View; 38 39 import sun.swing.SwingUtilities2; 40 41 import apple.laf.JRSUIConstants.Size; 42 43 import com.apple.laf.AquaButtonExtendedTypes.TypeSpecifier; 44 import com.apple.laf.AquaUtilControlSize.Sizeable; 45 import com.apple.laf.AquaUtils.*; 46 47 public class AquaButtonUI extends BasicButtonUI implements Sizeable { 48 private static final String BUTTON_TYPE = "JButton.buttonType"; 49 private static final String SEGMENTED_BUTTON_POSITION = "JButton.segmentPosition"; 50 51 protected static final RecyclableSingleton<AquaButtonUI> buttonUI = new RecyclableSingletonFromDefaultConstructor<AquaButtonUI>(AquaButtonUI.class); 52 public static ComponentUI createUI(final JComponent c) { 53 return buttonUI.get(); 54 } 55 56 // Has the shared instance defaults been initialized? 57 private boolean defaults_initialized = false; 58 private Color defaultDisabledTextColor = null; 59 60 protected void installDefaults(final AbstractButton b) { 61 // load shared instance defaults 62 final String pp = getPropertyPrefix(); 63 64 if (!defaults_initialized) { 65 defaultDisabledTextColor = UIManager.getColor(pp + "disabledText"); 66 defaults_initialized = true; 67 } 68 69 setButtonMarginIfNeeded(b, UIManager.getInsets(pp + "margin")); 70 71 LookAndFeel.installColorsAndFont(b, pp + "background", pp + "foreground", pp + "font"); 72 LookAndFeel.installProperty(b, "opaque", UIManager.getBoolean(pp + "opaque")); 73 74 final Object borderProp = b.getClientProperty(BUTTON_TYPE); 75 boolean hasBorder = false; 76 77 if (borderProp != null) { 78 hasBorder = setButtonType(b, borderProp); 79 } 80 if (!hasBorder) setThemeBorder(b); 81 82 final Object segmentProp = b.getClientProperty(SEGMENTED_BUTTON_POSITION); 83 if (segmentProp != null) { 84 final Border border = b.getBorder(); 85 if (!(border instanceof AquaBorder)) return; 86 87 b.setBorder(AquaButtonExtendedTypes.getBorderForPosition(b, b.getClientProperty(BUTTON_TYPE), segmentProp)); 88 } 89 } 90 91 public void applySizeFor(final JComponent c, final Size size) { 92 // this space intentionally left blank 93 // (subclasses need to do work here) 94 } 95 96 protected void setThemeBorder(final AbstractButton b) { 97 // Set the correct border 98 final ButtonUI genericUI = b.getUI(); 99 if (!(genericUI instanceof AquaButtonUI)) return; 100 final AquaButtonUI ui = (AquaButtonUI)genericUI; 101 102 Border border = b.getBorder(); 103 if (!ui.isBorderFromProperty(b) && (border == null || border instanceof UIResource || border instanceof AquaButtonBorder)) { 104 // See BasicGraphicsUtils.getPreferredButtonSize - it returns null for preferred size, 105 // causing it to use the subcomponent's size, which doesn't allow space for Aqua pushbuttons 106 boolean iconFont = true; 107 if (isOnToolbar(b)) { 108 if (b instanceof JToggleButton) { 109 border = AquaButtonBorder.getToolBarButtonBorder(); 110 } else { 111 border = AquaButtonBorder.getBevelButtonBorder(); 112 } 113 } else if (b.getIcon() != null || b.getComponentCount() > 0) { 114 // radar 3308129 && (b.getText() == null || b.getText().equals(""))) 115 // we used to only do this for buttons that had images and no text 116 // now we do it for all buttons that have any images - they cannot 117 // be a default button. 118 border = AquaButtonBorder.getToggleButtonBorder(); 119 } else { 120 border = UIManager.getBorder(getPropertyPrefix() + "border"); 121 iconFont = false; 122 } 123 124 b.setBorder(border); 125 126 final Font currentFont = b.getFont(); 127 if (iconFont && (currentFont == null || currentFont instanceof UIResource)) { 128 b.setFont(UIManager.getFont("IconButton.font")); 129 } 130 } 131 } 132 133 protected static boolean isOnToolbar(final AbstractButton b) { 134 Component parent = b.getParent(); 135 while (parent != null) { 136 if (parent instanceof JToolBar) return true; 137 parent = parent.getParent(); 138 } 139 return false; 140 } 141 142 // A state that affects border has changed. Make sure we have the right one 143 protected static void updateBorder(final AbstractButton b) { 144 // See if the button has overridden the automatic button type 145 final Object prop = b.getClientProperty(BUTTON_TYPE); 146 if (prop != null) return; 147 148 final ButtonUI ui = b.getUI(); 149 if (!(ui instanceof AquaButtonUI)) return; 150 if (b.getBorder() != null) ((AquaButtonUI)ui).setThemeBorder(b); 151 } 152 153 protected void setButtonMarginIfNeeded(final AbstractButton b, final Insets insets) { 154 final Insets margin = b.getMargin(); 155 if (margin == null || (margin instanceof UIResource)) { 156 b.setMargin(insets); 157 } 158 } 159 160 public boolean isBorderFromProperty(final AbstractButton button) { 161 return button.getClientProperty(BUTTON_TYPE) != null; 162 } 163 164 protected boolean setButtonType(final AbstractButton b, final Object prop) { 165 if (!(prop instanceof String)) { 166 b.putClientProperty(BUTTON_TYPE, null); // so we know to use the automatic button type 167 return false; 168 } 169 170 final String buttonType = (String)prop; 171 boolean iconFont = true; 172 173 final TypeSpecifier specifier = AquaButtonExtendedTypes.getSpecifierByName(buttonType); 174 if (specifier != null) { 175 b.setBorder(specifier.getBorder()); 176 iconFont = specifier.setIconFont; 177 } 178 179 final Font currentFont = b.getFont(); 180 if (currentFont == null || currentFont instanceof UIResource) { 181 b.setFont(UIManager.getFont(iconFont ? "IconButton.font" : "Button.font")); 182 } 183 184 return true; 185 } 186 187 protected void installListeners(final AbstractButton b) { 188 final AquaButtonListener listener = createButtonListener(b); 189 if (listener != null) { 190 // put the listener in the button's client properties so that 191 // we can get at it later 192 b.putClientProperty(this, listener); 193 194 b.addMouseListener(listener); 195 b.addMouseMotionListener(listener); 196 b.addFocusListener(listener); 197 b.addPropertyChangeListener(listener); 198 b.addChangeListener(listener); 199 b.addAncestorListener(listener); 200 } 201 installHierListener(b); 202 AquaUtilControlSize.addSizePropertyListener(b); 203 } 204 205 protected void installKeyboardActions(final AbstractButton b) { 206 final BasicButtonListener listener = (BasicButtonListener)b.getClientProperty(this); 207 if (listener != null) listener.installKeyboardActions(b); 208 } 209 210 // Uninstall PLAF 211 public void uninstallUI(final JComponent c) { 212 uninstallKeyboardActions((AbstractButton)c); 213 uninstallListeners((AbstractButton)c); 214 uninstallDefaults((AbstractButton)c); 215 //BasicHTML.updateRenderer(c, ""); 216 } 217 218 protected void uninstallKeyboardActions(final AbstractButton b) { 219 final BasicButtonListener listener = (BasicButtonListener)b.getClientProperty(this); 220 if (listener != null) listener.uninstallKeyboardActions(b); 221 } 222 223 protected void uninstallListeners(final AbstractButton b) { 224 final AquaButtonListener listener = (AquaButtonListener)b.getClientProperty(this); 225 b.putClientProperty(this, null); 226 if (listener != null) { 227 b.removeMouseListener(listener); 228 b.removeMouseListener(listener); 229 b.removeMouseMotionListener(listener); 230 b.removeFocusListener(listener); 231 b.removeChangeListener(listener); 232 b.removePropertyChangeListener(listener); 233 b.removeAncestorListener(listener); 234 } 235 uninstallHierListener(b); 236 AquaUtilControlSize.addSizePropertyListener(b); 237 } 238 239 protected void uninstallDefaults(final AbstractButton b) { 240 LookAndFeel.uninstallBorder(b); 241 defaults_initialized = false; 242 } 243 244 // Create Listeners 245 protected AquaButtonListener createButtonListener(final AbstractButton b) { 246 return new AquaButtonListener(b); 247 } 248 249 // Paint Methods 250 public void paint(final Graphics g, final JComponent c) { 251 final AbstractButton b = (AbstractButton)c; 252 final ButtonModel model = b.getModel(); 253 254 final Insets i = c.getInsets(); 255 256 Rectangle viewRect = new Rectangle(b.getWidth(), b.getHeight()); 257 Rectangle iconRect = new Rectangle(); 258 Rectangle textRect = new Rectangle(); 259 260 // we are overdrawing here with translucent colors so we get 261 // a darkening effect. How can we avoid it. Try clear rect? 262 if (b.isOpaque()) { 263 g.setColor(c.getBackground()); 264 g.fillRect(viewRect.x, viewRect.y, viewRect.width, viewRect.height); 265 } 266 267 AquaButtonBorder aquaBorder = null; 268 if (((AbstractButton)c).isBorderPainted()) { 269 final Border border = c.getBorder(); 270 271 if (border instanceof AquaButtonBorder) { 272 // only do this if borders are on! 273 // this also takes care of focus painting. 274 aquaBorder = (AquaButtonBorder)border; 275 aquaBorder.paintButton(c, g, viewRect.x, viewRect.y, viewRect.width, viewRect.height); 276 } 277 } else { 278 if (b.isOpaque()) { 279 viewRect.x = i.left - 2; 280 viewRect.y = i.top - 2; 281 viewRect.width = b.getWidth() - (i.right + viewRect.x) + 4; 282 viewRect.height = b.getHeight() - (i.bottom + viewRect.y) + 4; 283 if (b.isContentAreaFilled() || model.isSelected()) { 284 if (model.isSelected()) // Toggle buttons 285 g.setColor(c.getBackground().darker()); 286 else g.setColor(c.getBackground()); 287 g.fillRect(viewRect.x, viewRect.y, viewRect.width, viewRect.height); 288 } 289 } 290 291 // needs focus to be painted 292 // for now we don't know exactly what to do...we'll see! 293 if (b.isFocusPainted() && b.hasFocus()) { 294 // paint UI specific focus 295 paintFocus(g, b, viewRect, textRect, iconRect); 296 } 297 } 298 299 // performs icon and text rect calculations 300 final String text = layoutAndGetText(g, b, aquaBorder, i, viewRect, iconRect, textRect); 301 302 // Paint the Icon 303 if (b.getIcon() != null) { 304 paintIcon(g, b, iconRect); 305 } 306 307 if (textRect.width == 0) { 308 textRect.width = 50; 309 } 310 311 if (text != null && !text.equals("")) { 312 final View v = (View)c.getClientProperty(BasicHTML.propertyKey); 313 if (v != null) { 314 v.paint(g, textRect); 315 } else { 316 paintText(g, b, textRect, text); 317 } 318 } 319 } 320 321 protected String layoutAndGetText(final Graphics g, final AbstractButton b, final AquaButtonBorder aquaBorder, final Insets i, Rectangle viewRect, Rectangle iconRect, Rectangle textRect) { 322 // re-initialize the view rect to the selected insets 323 viewRect.x = i.left; 324 viewRect.y = i.top; 325 viewRect.width = b.getWidth() - (i.right + viewRect.x); 326 viewRect.height = b.getHeight() - (i.bottom + viewRect.y); 327 328 // reset the text and icon rects 329 textRect.x = textRect.y = textRect.width = textRect.height = 0; 330 iconRect.x = iconRect.y = iconRect.width = iconRect.height = 0; 331 332 // setup the font 333 g.setFont(b.getFont()); 334 final FontMetrics fm = g.getFontMetrics(); 335 336 // layout the text and icon 337 final String originalText = b.getText(); 338 final String text = SwingUtilities.layoutCompoundLabel(b, fm, originalText, b.getIcon(), b.getVerticalAlignment(), b.getHorizontalAlignment(), b.getVerticalTextPosition(), b.getHorizontalTextPosition(), viewRect, iconRect, textRect, originalText == null ? 0 : b.getIconTextGap()); 339 if (text == originalText || aquaBorder == null) return text; // everything fits 340 341 // if the text didn't fit - check if the aqua border has alternate Insets that are more adhering 342 final Insets alternateContentInsets = aquaBorder.getContentInsets(b, b.getWidth(), b.getHeight()); 343 if (alternateContentInsets != null) { 344 // recursively call and don't pass AquaBorder 345 return layoutAndGetText(g, b, null, alternateContentInsets, viewRect, iconRect, textRect); 346 } 347 348 // there is no Aqua border, go with what we've got 349 return text; 350 } 351 352 protected void paintIcon(final Graphics g, final AbstractButton b, final Rectangle localIconRect) { 353 final ButtonModel model = b.getModel(); 354 Icon icon = b.getIcon(); 355 Icon tmpIcon = null; 356 357 if (icon == null) return; 358 359 if (!model.isEnabled()) { 360 if (model.isSelected()) { 361 tmpIcon = b.getDisabledSelectedIcon(); 362 } else { 363 tmpIcon = b.getDisabledIcon(); 364 } 365 } else if (model.isPressed() && model.isArmed()) { 366 tmpIcon = b.getPressedIcon(); 367 if (tmpIcon == null) { 368 if (icon instanceof ImageIcon) { 369 tmpIcon = new ImageIcon(AquaUtils.generateSelectedDarkImage(((ImageIcon)icon).getImage())); 370 } 371 } 372 } else if (b.isRolloverEnabled() && model.isRollover()) { 373 if (model.isSelected()) { 374 tmpIcon = b.getRolloverSelectedIcon(); 375 } else { 376 tmpIcon = b.getRolloverIcon(); 377 } 378 } else if (model.isSelected()) { 379 tmpIcon = b.getSelectedIcon(); 380 } 381 382 if (model.isEnabled() && b.isFocusOwner() && b.getBorder() instanceof AquaButtonBorder.Toolbar) { 383 if (tmpIcon == null) tmpIcon = icon; 384 if (tmpIcon instanceof ImageIcon) { 385 tmpIcon = AquaFocus.createFocusedIcon(tmpIcon, b, 3); 386 tmpIcon.paintIcon(b, g, localIconRect.x - 3, localIconRect.y - 3); 387 return; 388 } 389 } 390 391 if (tmpIcon != null) { 392 icon = tmpIcon; 393 } 394 395 icon.paintIcon(b, g, localIconRect.x, localIconRect.y); 396 } 397 398 /** 399 * As of Java 2 platform v 1.4 this method should not be used or overriden. 400 * Use the paintText method which takes the AbstractButton argument. 401 */ 402 protected void paintText(final Graphics g, final JComponent c, final Rectangle localTextRect, final String text) { 403 final Graphics2D g2d = g instanceof Graphics2D ? (Graphics2D)g : null; 404 405 final AbstractButton b = (AbstractButton)c; 406 final ButtonModel model = b.getModel(); 407 final FontMetrics fm = g.getFontMetrics(); 408 final int mnemonicIndex = AquaMnemonicHandler.isMnemonicHidden() ? -1 : b.getDisplayedMnemonicIndex(); 409 410 /* Draw the Text */ 411 if (model.isEnabled()) { 412 /*** paint the text normally */ 413 g.setColor(b.getForeground()); 414 } else { 415 /*** paint the text disabled ***/ 416 g.setColor(defaultDisabledTextColor); 417 } 418 getTextUIDrawing().drawStringUnderlineCharAt(c, g, text, mnemonicIndex, 419 localTextRect.x, 420 localTextRect.y + fm.getAscent()); 421 } 422 423 protected void paintText(final Graphics g, final AbstractButton b, final Rectangle localTextRect, final String text) { 424 paintText(g, (JComponent)b, localTextRect, text); 425 } 426 427 protected void paintButtonPressed(final Graphics g, final AbstractButton b) { 428 paint(g, b); 429 } 430 431 // Layout Methods 432 public Dimension getMinimumSize(final JComponent c) { 433 final Dimension d = getPreferredSize(c); 434 final View v = (View)c.getClientProperty(BasicHTML.propertyKey); 435 if (v != null) { 436 d.width -= v.getPreferredSpan(View.X_AXIS) - v.getMinimumSpan(View.X_AXIS); 437 } 438 return d; 439 } 440 441 public Dimension getPreferredSize(final JComponent c) { 442 final AbstractButton b = (AbstractButton)c; 443 444 // fix for Radar #3134273 445 final Dimension d = BasicGraphicsUtils.getPreferredButtonSize(b, b.getIconTextGap()); 446 if (d == null) return null; 447 448 final Border border = b.getBorder(); 449 if (border instanceof AquaButtonBorder) { 450 ((AquaButtonBorder)border).alterPreferredSize(d); 451 } 452 453 return d; 454 } 455 456 public Dimension getMaximumSize(final JComponent c) { 457 final Dimension d = getPreferredSize(c); 458 459 final View v = (View)c.getClientProperty(BasicHTML.propertyKey); 460 if (v != null) { 461 d.width += v.getMaximumSpan(View.X_AXIS) - v.getPreferredSpan(View.X_AXIS); 462 } 463 464 return d; 465 } 466 467 static final RecyclableSingleton<AquaHierarchyButtonListener> fHierListener = new RecyclableSingletonFromDefaultConstructor<AquaHierarchyButtonListener>(AquaHierarchyButtonListener.class); 468 static AquaHierarchyButtonListener getAquaHierarchyButtonListener() { 469 return fHierListener.get(); 470 } 471 472 // We need to know when ordinary JButtons are put on JToolbars, but not JComboBoxButtons 473 // JToggleButtons always have the same border 474 475 private boolean shouldInstallHierListener(final AbstractButton b) { 476 return (b instanceof JButton || b instanceof JToggleButton && !(b instanceof AquaComboBoxButton) && !(b instanceof JCheckBox) && !(b instanceof JRadioButton)); 477 } 478 479 protected void installHierListener(final AbstractButton b) { 480 if (shouldInstallHierListener(b)) { 481 // super put the listener in the button's client properties 482 b.addHierarchyListener(getAquaHierarchyButtonListener()); 483 } 484 } 485 486 protected void uninstallHierListener(final AbstractButton b) { 487 if (shouldInstallHierListener(b)) { 488 b.removeHierarchyListener(getAquaHierarchyButtonListener()); 489 } 490 } 491 492 static class AquaHierarchyButtonListener implements HierarchyListener { 493 // Everytime a hierarchy is change we need to check if the button if moved on or from 494 // a toolbar. If that is the case, we need to re-set the border of the button. 495 public void hierarchyChanged(final HierarchyEvent e) { 496 if ((e.getChangeFlags() & HierarchyEvent.PARENT_CHANGED) == 0) return; 497 498 final Object o = e.getSource(); 499 if (!(o instanceof AbstractButton)) return; 500 501 final AbstractButton b = (AbstractButton)o; 502 final ButtonUI ui = b.getUI(); 503 if (!(ui instanceof AquaButtonUI)) return; 504 505 if (!(b.getBorder() instanceof UIResource)) return; // if the border is not one of ours, or null 506 ((AquaButtonUI)ui).setThemeBorder(b); 507 } 508 } 509 510 class AquaButtonListener extends BasicButtonListener implements AncestorListener { 511 protected final AbstractButton b; 512 513 public AquaButtonListener(final AbstractButton b) { 514 super(b); 515 this.b = b; 516 } 517 518 public void focusGained(final FocusEvent e) { 519 ((Component)e.getSource()).repaint(); 520 } 521 522 public void focusLost(final FocusEvent e) { 523 // 10-06-03 VL: [Radar 3187049] 524 // If focusLost arrives while the button has been left-clicked this would disarm the button, 525 // causing actionPerformed not to fire on mouse release! 526 //b.getModel().setArmed(false); 527 ((Component)e.getSource()).repaint(); 528 } 529 530 public void propertyChange(final PropertyChangeEvent e) { 531 super.propertyChange(e); 532 533 final String propertyName = e.getPropertyName(); 534 535 // Repaint the button, since its border needs to handle the new state. 536 if (AquaFocusHandler.FRAME_ACTIVE_PROPERTY.equals(propertyName)) { 537 b.repaint(); 538 return; 539 } 540 541 if ("icon".equals(propertyName) || "text".equals(propertyName)) { 542 setThemeBorder(b); 543 return; 544 } 545 546 if (BUTTON_TYPE.equals(propertyName)) { 547 // Forced border types 548 final String value = (String)e.getNewValue(); 549 550 final Border border = AquaButtonExtendedTypes.getBorderForPosition(b, value, b.getClientProperty(SEGMENTED_BUTTON_POSITION)); 551 if (border != null) { 552 b.setBorder(border); 553 } 554 555 return; 556 } 557 558 if (SEGMENTED_BUTTON_POSITION.equals(propertyName)) { 559 final Border border = b.getBorder(); 560 if (!(border instanceof AquaBorder)) return; 561 562 b.setBorder(AquaButtonExtendedTypes.getBorderForPosition(b, b.getClientProperty(BUTTON_TYPE), e.getNewValue())); 563 } 564 565 if ("componentOrientation".equals(propertyName)) { 566 final Border border = b.getBorder(); 567 if (!(border instanceof AquaBorder)) return; 568 569 Object buttonType = b.getClientProperty(BUTTON_TYPE); 570 Object buttonPosition = b.getClientProperty(SEGMENTED_BUTTON_POSITION); 571 if (buttonType != null && buttonPosition != null) { 572 b.setBorder(AquaButtonExtendedTypes.getBorderForPosition(b, buttonType, buttonPosition)); 573 } 574 } 575 } 576 577 public void ancestorMoved(final AncestorEvent e) {} 578 579 public void ancestorAdded(final AncestorEvent e) { 580 updateDefaultButton(); 581 } 582 583 public void ancestorRemoved(final AncestorEvent e) { 584 updateDefaultButton(); 585 } 586 587 protected void updateDefaultButton() { 588 if (!(b instanceof JButton)) return; 589 if (!((JButton)b).isDefaultButton()) return; 590 591 final JRootPane rootPane = b.getRootPane(); 592 if (rootPane == null) return; 593 594 final RootPaneUI ui = rootPane.getUI(); 595 if (!(ui instanceof AquaRootPaneUI)) return; 596 ((AquaRootPaneUI)ui).updateDefaultButton(rootPane); 597 } 598 } 599 }