Path: blob/master/src/java.desktop/macosx/classes/com/apple/laf/AquaButtonUI.java
66646 views
/*1* Copyright (c) 2011, 2021, Oracle and/or its affiliates. All rights reserved.2* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.3*4* This code is free software; you can redistribute it and/or modify it5* under the terms of the GNU General Public License version 2 only, as6* published by the Free Software Foundation. Oracle designates this7* particular file as subject to the "Classpath" exception as provided8* by Oracle in the LICENSE file that accompanied this code.9*10* This code is distributed in the hope that it will be useful, but WITHOUT11* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or12* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License13* version 2 for more details (a copy is included in the LICENSE file that14* accompanied this code).15*16* You should have received a copy of the GNU General Public License version17* 2 along with this work; if not, write to the Free Software Foundation,18* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.19*20* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA21* or visit www.oracle.com if you need additional information or have any22* questions.23*/2425package com.apple.laf;2627import java.awt.*;28import java.awt.event.*;29import java.beans.PropertyChangeEvent;3031import javax.swing.*;32import javax.swing.border.Border;33import javax.swing.event.*;34import javax.swing.plaf.*;35import javax.swing.plaf.basic.*;36import javax.swing.text.View;3738import sun.swing.SwingUtilities2;3940import apple.laf.JRSUIConstants.Size;4142import com.apple.laf.AquaButtonExtendedTypes.TypeSpecifier;43import com.apple.laf.AquaUtilControlSize.Sizeable;44import com.apple.laf.AquaUtils.*;4546public class AquaButtonUI extends BasicButtonUI implements Sizeable {47private static final String BUTTON_TYPE = "JButton.buttonType";48private static final String SEGMENTED_BUTTON_POSITION = "JButton.segmentPosition";4950private static final RecyclableSingleton<AquaButtonUI> buttonUI = new RecyclableSingletonFromDefaultConstructor<AquaButtonUI>(AquaButtonUI.class);51public static ComponentUI createUI(final JComponent c) {52return buttonUI.get();53}5455// Has the shared instance defaults been initialized?56private boolean defaults_initialized = false;57private Color defaultDisabledTextColor = null;5859protected void installDefaults(final AbstractButton b) {60// load shared instance defaults61final String pp = getPropertyPrefix();6263if (!defaults_initialized) {64defaultDisabledTextColor = UIManager.getColor(pp + "disabledText");65defaults_initialized = true;66}6768setButtonMarginIfNeeded(b, UIManager.getInsets(pp + "margin"));6970LookAndFeel.installColorsAndFont(b, pp + "background", pp + "foreground", pp + "font");71LookAndFeel.installProperty(b, "opaque", UIManager.getBoolean(pp + "opaque"));7273final Object borderProp = b.getClientProperty(BUTTON_TYPE);74boolean hasBorder = false;7576if (borderProp != null) {77hasBorder = setButtonType(b, borderProp);78}79if (!hasBorder) setThemeBorder(b);8081final Object segmentProp = b.getClientProperty(SEGMENTED_BUTTON_POSITION);82if (segmentProp != null) {83final Border border = b.getBorder();84if (!(border instanceof AquaBorder)) return;8586b.setBorder(AquaButtonExtendedTypes.getBorderForPosition(b, b.getClientProperty(BUTTON_TYPE), segmentProp));87}88}8990public void applySizeFor(final JComponent c, final Size size) {91// this space intentionally left blank92// (subclasses need to do work here)93}9495protected void setThemeBorder(final AbstractButton b) {96// Set the correct border97final ButtonUI genericUI = b.getUI();98if (!(genericUI instanceof AquaButtonUI)) return;99final AquaButtonUI ui = (AquaButtonUI)genericUI;100101Border border = b.getBorder();102if (!ui.isBorderFromProperty(b) && (border == null || border instanceof UIResource || border instanceof AquaButtonBorder)) {103// See BasicGraphicsUtils.getPreferredButtonSize - it returns null for preferred size,104// causing it to use the subcomponent's size, which doesn't allow space for Aqua pushbuttons105boolean iconFont = true;106if (isOnToolbar(b)) {107if (b instanceof JToggleButton) {108border = AquaButtonBorder.getToolBarButtonBorder();109} else {110border = AquaButtonBorder.getBevelButtonBorder();111}112} else if (b.getIcon() != null || b.getComponentCount() > 0) {113// radar 3308129 && (b.getText() == null || b.getText().equals("")))114// we used to only do this for buttons that had images and no text115// now we do it for all buttons that have any images - they cannot116// be a default button.117border = AquaButtonBorder.getToggleButtonBorder();118} else {119border = UIManager.getBorder(getPropertyPrefix() + "border");120iconFont = false;121}122123b.setBorder(border);124125final Font currentFont = b.getFont();126if (iconFont && (currentFont == null || currentFont instanceof UIResource)) {127b.setFont(UIManager.getFont("IconButton.font"));128}129}130}131132protected static boolean isOnToolbar(final AbstractButton b) {133Component parent = b.getParent();134while (parent != null) {135if (parent instanceof JToolBar) return true;136parent = parent.getParent();137}138return false;139}140141// A state that affects border has changed. Make sure we have the right one142protected static void updateBorder(final AbstractButton b) {143// See if the button has overridden the automatic button type144final Object prop = b.getClientProperty(BUTTON_TYPE);145if (prop != null) return;146147final ButtonUI ui = b.getUI();148if (!(ui instanceof AquaButtonUI)) return;149if (b.getBorder() != null) ((AquaButtonUI)ui).setThemeBorder(b);150}151152protected void setButtonMarginIfNeeded(final AbstractButton b, final Insets insets) {153final Insets margin = b.getMargin();154if (margin == null || (margin instanceof UIResource)) {155b.setMargin(insets);156}157}158159public boolean isBorderFromProperty(final AbstractButton button) {160return button.getClientProperty(BUTTON_TYPE) != null;161}162163protected boolean setButtonType(final AbstractButton b, final Object prop) {164if (!(prop instanceof String)) {165b.putClientProperty(BUTTON_TYPE, null); // so we know to use the automatic button type166return false;167}168169final String buttonType = (String)prop;170boolean iconFont = true;171172final TypeSpecifier specifier = AquaButtonExtendedTypes.getSpecifierByName(buttonType);173if (specifier != null) {174b.setBorder(specifier.getBorder());175iconFont = specifier.setIconFont;176}177178final Font currentFont = b.getFont();179if (currentFont == null || currentFont instanceof UIResource) {180b.setFont(UIManager.getFont(iconFont ? "IconButton.font" : "Button.font"));181}182183return true;184}185186protected void installListeners(final AbstractButton b) {187super.installListeners(b);188AquaButtonListener listener = getAquaButtonListener(b);189if (listener != null) {190// put the listener in the button's client properties so that191// we can get at it later192b.putClientProperty(this, listener);193194b.addAncestorListener(listener);195}196installHierListener(b);197AquaUtilControlSize.addSizePropertyListener(b);198}199200protected void installKeyboardActions(final AbstractButton b) {201final BasicButtonListener listener = (BasicButtonListener)b.getClientProperty(this);202if (listener != null) listener.installKeyboardActions(b);203}204205// Uninstall PLAF206public void uninstallUI(final JComponent c) {207uninstallKeyboardActions((AbstractButton)c);208uninstallListeners((AbstractButton)c);209uninstallDefaults((AbstractButton)c);210//BasicHTML.updateRenderer(c, "");211}212213protected void uninstallKeyboardActions(final AbstractButton b) {214final BasicButtonListener listener = (BasicButtonListener)b.getClientProperty(this);215if (listener != null) listener.uninstallKeyboardActions(b);216}217218protected void uninstallListeners(final AbstractButton b) {219super.uninstallListeners(b);220final AquaButtonListener listener = (AquaButtonListener)b.getClientProperty(this);221b.putClientProperty(this, null);222if (listener != null) {223b.removeAncestorListener(listener);224}225uninstallHierListener(b);226AquaUtilControlSize.removeSizePropertyListener(b);227}228229protected void uninstallDefaults(final AbstractButton b) {230LookAndFeel.uninstallBorder(b);231defaults_initialized = false;232}233234// Create Listeners235protected AquaButtonListener createButtonListener(final AbstractButton b) {236return new AquaButtonListener(b);237}238239/**240* Returns the AquaButtonListener for the passed in Button, or null if one241* could not be found.242*/243private AquaButtonListener getAquaButtonListener(AbstractButton b) {244MouseMotionListener[] listeners = b.getMouseMotionListeners();245246if (listeners != null) {247for (MouseMotionListener listener : listeners) {248if (listener instanceof AquaButtonListener) {249return (AquaButtonListener) listener;250}251}252}253return null;254}255256// Paint Methods257public void paint(final Graphics g, final JComponent c) {258final AbstractButton b = (AbstractButton)c;259final ButtonModel model = b.getModel();260261final Insets i = c.getInsets();262263Rectangle viewRect = new Rectangle(b.getWidth(), b.getHeight());264Rectangle iconRect = new Rectangle();265Rectangle textRect = new Rectangle();266267// we are overdrawing here with translucent colors so we get268// a darkening effect. How can we avoid it. Try clear rect?269if (b.isOpaque()) {270g.setColor(c.getBackground());271g.fillRect(viewRect.x, viewRect.y, viewRect.width, viewRect.height);272}273274AquaButtonBorder aquaBorder = null;275if (((AbstractButton)c).isBorderPainted()) {276final Border border = c.getBorder();277278if (border instanceof AquaButtonBorder) {279// only do this if borders are on!280// this also takes care of focus painting.281aquaBorder = (AquaButtonBorder)border;282aquaBorder.paintButton(c, g, viewRect.x, viewRect.y, viewRect.width, viewRect.height);283}284} else {285if (b.isOpaque()) {286viewRect.x = i.left - 2;287viewRect.y = i.top - 2;288viewRect.width = b.getWidth() - (i.right + viewRect.x) + 4;289viewRect.height = b.getHeight() - (i.bottom + viewRect.y) + 4;290if (b.isContentAreaFilled() || model.isSelected()) {291if (model.isSelected()) // Toggle buttons292g.setColor(c.getBackground().darker());293else g.setColor(c.getBackground());294g.fillRect(viewRect.x, viewRect.y, viewRect.width, viewRect.height);295}296}297298// needs focus to be painted299// for now we don't know exactly what to do...we'll see!300if (b.isFocusPainted() && b.hasFocus()) {301// paint UI specific focus302paintFocus(g, b, viewRect, textRect, iconRect);303}304}305306// performs icon and text rect calculations307final String text = layoutAndGetText(g, b, aquaBorder, i, viewRect, iconRect, textRect);308309// Paint the Icon310if (b.getIcon() != null) {311paintIcon(g, b, iconRect);312}313314if (textRect.width == 0) {315textRect.width = 50;316}317318if (text != null && !text.isEmpty()) {319final View v = (View)c.getClientProperty(BasicHTML.propertyKey);320if (v != null) {321v.paint(g, textRect);322} else {323paintText(g, b, textRect, text);324}325}326}327328protected void paintFocus(Graphics g, AbstractButton b,329Rectangle viewRect, Rectangle textRect, Rectangle iconRect) {330Graphics2D g2d = null;331Stroke oldStroke = null;332Object oldAntialiasingHint = null;333Color oldColor = g.getColor();334if (g instanceof Graphics2D) {335g2d = (Graphics2D)g;336oldStroke = g2d.getStroke();337oldAntialiasingHint = g2d.getRenderingHint(RenderingHints.KEY_ANTIALIASING);338g2d.setStroke(new BasicStroke(3));339g2d.setRenderingHint(340RenderingHints.KEY_ANTIALIASING,341RenderingHints.VALUE_ANTIALIAS_ON);342343}344Color ringColor = UIManager.getColor("Focus.color");345g.setColor(ringColor);346g.drawRoundRect(5, 3, b.getWidth() - 10, b.getHeight() - 7, 15, 15);347if (g2d != null) {348// Restore old state of Java2D renderer349g2d.setStroke(oldStroke);350g2d.setRenderingHint(351RenderingHints.KEY_ANTIALIASING,352oldAntialiasingHint);353}354g.setColor(oldColor);355}356357protected String layoutAndGetText(final Graphics g, final AbstractButton b, final AquaButtonBorder aquaBorder, final Insets i, Rectangle viewRect, Rectangle iconRect, Rectangle textRect) {358// re-initialize the view rect to the selected insets359viewRect.x = i.left;360viewRect.y = i.top;361viewRect.width = b.getWidth() - (i.right + viewRect.x);362viewRect.height = b.getHeight() - (i.bottom + viewRect.y);363364// reset the text and icon rects365textRect.x = textRect.y = textRect.width = textRect.height = 0;366iconRect.x = iconRect.y = iconRect.width = iconRect.height = 0;367368// setup the font369g.setFont(b.getFont());370final FontMetrics fm = g.getFontMetrics();371372// layout the text and icon373final String originalText = b.getText();374final 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());375if (text == originalText || aquaBorder == null) return text; // everything fits376377// if the text didn't fit - check if the aqua border has alternate Insets that are more adhering378final Insets alternateContentInsets = aquaBorder.getContentInsets(b, b.getWidth(), b.getHeight());379if (alternateContentInsets != null) {380// recursively call and don't pass AquaBorder381return layoutAndGetText(g, b, null, alternateContentInsets, viewRect, iconRect, textRect);382}383384// there is no Aqua border, go with what we've got385return text;386}387388protected void paintIcon(final Graphics g, final AbstractButton b, final Rectangle localIconRect) {389final ButtonModel model = b.getModel();390Icon icon = b.getIcon();391Icon tmpIcon = null;392393if (icon == null) return;394395if (!model.isEnabled()) {396if (model.isSelected()) {397tmpIcon = b.getDisabledSelectedIcon();398} else {399tmpIcon = b.getDisabledIcon();400}401} else if (model.isPressed() && model.isArmed()) {402tmpIcon = b.getPressedIcon();403if (tmpIcon == null) {404if (icon instanceof ImageIcon) {405tmpIcon = new ImageIcon(AquaUtils.generateSelectedDarkImage(((ImageIcon)icon).getImage()));406}407}408} else if (b.isRolloverEnabled() && model.isRollover()) {409if (model.isSelected()) {410tmpIcon = b.getRolloverSelectedIcon();411} else {412tmpIcon = b.getRolloverIcon();413}414} else if (model.isSelected()) {415tmpIcon = b.getSelectedIcon();416}417418if (model.isEnabled() && b.isFocusOwner() && b.getBorder() instanceof AquaButtonBorder.Toolbar) {419if (tmpIcon == null) tmpIcon = icon;420if (tmpIcon instanceof ImageIcon) {421tmpIcon = AquaFocus.createFocusedIcon(tmpIcon, b, 3);422tmpIcon.paintIcon(b, g, localIconRect.x - 3, localIconRect.y - 3);423return;424}425}426427if (tmpIcon != null) {428icon = tmpIcon;429}430431icon.paintIcon(b, g, localIconRect.x, localIconRect.y);432}433434/**435* As of Java 2 platform v 1.4 this method should not be used or overriden.436* Use the paintText method which takes the AbstractButton argument.437*/438protected void paintText(final Graphics g, final JComponent c, final Rectangle localTextRect, final String text) {439final Graphics2D g2d = g instanceof Graphics2D ? (Graphics2D)g : null;440441final AbstractButton b = (AbstractButton)c;442final ButtonModel model = b.getModel();443final FontMetrics fm = g.getFontMetrics();444final int mnemonicIndex = AquaMnemonicHandler.isMnemonicHidden() ? -1 : b.getDisplayedMnemonicIndex();445446/* Draw the Text */447if (model.isEnabled()) {448/*** paint the text normally */449g.setColor(b.getForeground());450} else {451/*** paint the text disabled ***/452g.setColor(defaultDisabledTextColor);453}454SwingUtilities2.drawStringUnderlineCharAt(c, g, text, mnemonicIndex, localTextRect.x, localTextRect.y + fm.getAscent());455}456457protected void paintText(final Graphics g, final AbstractButton b, final Rectangle localTextRect, final String text) {458paintText(g, (JComponent)b, localTextRect, text);459}460461protected void paintButtonPressed(final Graphics g, final AbstractButton b) {462paint(g, b);463}464465// Layout Methods466public Dimension getMinimumSize(final JComponent c) {467final Dimension d = getPreferredSize(c);468final View v = (View)c.getClientProperty(BasicHTML.propertyKey);469if (v != null) {470d.width -= v.getPreferredSpan(View.X_AXIS) - v.getMinimumSpan(View.X_AXIS);471}472return d;473}474475public Dimension getPreferredSize(final JComponent c) {476final AbstractButton b = (AbstractButton)c;477478// fix for Radar #3134273479final Dimension d = BasicGraphicsUtils.getPreferredButtonSize(b, b.getIconTextGap());480if (d == null) return null;481482final Border border = b.getBorder();483if (border instanceof AquaButtonBorder) {484((AquaButtonBorder)border).alterPreferredSize(d);485}486487return d;488}489490public Dimension getMaximumSize(final JComponent c) {491final Dimension d = getPreferredSize(c);492493final View v = (View)c.getClientProperty(BasicHTML.propertyKey);494if (v != null) {495d.width += v.getMaximumSpan(View.X_AXIS) - v.getPreferredSpan(View.X_AXIS);496}497498return d;499}500501private static final RecyclableSingleton<AquaHierarchyButtonListener> fHierListener = new RecyclableSingletonFromDefaultConstructor<AquaHierarchyButtonListener>(AquaHierarchyButtonListener.class);502static AquaHierarchyButtonListener getAquaHierarchyButtonListener() {503return fHierListener.get();504}505506// We need to know when ordinary JButtons are put on JToolbars, but not JComboBoxButtons507// JToggleButtons always have the same border508509private boolean shouldInstallHierListener(final AbstractButton b) {510return (b instanceof JButton || b instanceof JToggleButton && !(b instanceof AquaComboBoxButton) && !(b instanceof JCheckBox) && !(b instanceof JRadioButton));511}512513protected void installHierListener(final AbstractButton b) {514if (shouldInstallHierListener(b)) {515// super put the listener in the button's client properties516b.addHierarchyListener(getAquaHierarchyButtonListener());517}518}519520protected void uninstallHierListener(final AbstractButton b) {521if (shouldInstallHierListener(b)) {522b.removeHierarchyListener(getAquaHierarchyButtonListener());523}524}525526static class AquaHierarchyButtonListener implements HierarchyListener {527// Everytime a hierarchy is change we need to check if the button if moved on or from528// a toolbar. If that is the case, we need to re-set the border of the button.529public void hierarchyChanged(final HierarchyEvent e) {530if ((e.getChangeFlags() & HierarchyEvent.PARENT_CHANGED) == 0) return;531532final Object o = e.getSource();533if (!(o instanceof AbstractButton)) return;534535final AbstractButton b = (AbstractButton)o;536final ButtonUI ui = b.getUI();537if (!(ui instanceof AquaButtonUI)) return;538539if (!(b.getBorder() instanceof UIResource)) return; // if the border is not one of ours, or null540((AquaButtonUI)ui).setThemeBorder(b);541}542}543544class AquaButtonListener extends BasicButtonListener implements AncestorListener {545protected final AbstractButton b;546547public AquaButtonListener(final AbstractButton b) {548super(b);549this.b = b;550}551552public void focusGained(final FocusEvent e) {553((Component)e.getSource()).repaint();554}555556public void focusLost(final FocusEvent e) {557// 10-06-03 VL: [Radar 3187049]558// If focusLost arrives while the button has been left-clicked this would disarm the button,559// causing actionPerformed not to fire on mouse release!560//b.getModel().setArmed(false);561b.getModel().setPressed(false);562((Component)e.getSource()).repaint();563}564565public void propertyChange(final PropertyChangeEvent e) {566super.propertyChange(e);567568final String propertyName = e.getPropertyName();569570// Repaint the button, since its border needs to handle the new state.571if (AquaFocusHandler.FRAME_ACTIVE_PROPERTY.equals(propertyName)) {572b.repaint();573return;574}575576if ("icon".equals(propertyName) || "text".equals(propertyName)) {577setThemeBorder(b);578return;579}580581if (BUTTON_TYPE.equals(propertyName)) {582// Forced border types583final String value = (String)e.getNewValue();584585final Border border = AquaButtonExtendedTypes.getBorderForPosition(b, value, b.getClientProperty(SEGMENTED_BUTTON_POSITION));586if (border != null) {587b.setBorder(border);588}589590return;591}592593if (SEGMENTED_BUTTON_POSITION.equals(propertyName)) {594final Border border = b.getBorder();595if (!(border instanceof AquaBorder)) return;596597b.setBorder(AquaButtonExtendedTypes.getBorderForPosition(b, b.getClientProperty(BUTTON_TYPE), e.getNewValue()));598}599600if ("componentOrientation".equals(propertyName)) {601final Border border = b.getBorder();602if (!(border instanceof AquaBorder)) return;603604Object buttonType = b.getClientProperty(BUTTON_TYPE);605Object buttonPosition = b.getClientProperty(SEGMENTED_BUTTON_POSITION);606if (buttonType != null && buttonPosition != null) {607b.setBorder(AquaButtonExtendedTypes.getBorderForPosition(b, buttonType, buttonPosition));608}609}610}611612public void ancestorMoved(final AncestorEvent e) {}613614public void ancestorAdded(final AncestorEvent e) {615updateDefaultButton();616}617618public void ancestorRemoved(final AncestorEvent e) {619updateDefaultButton();620}621622protected void updateDefaultButton() {623if (!(b instanceof JButton)) return;624if (!((JButton)b).isDefaultButton()) return;625626final JRootPane rootPane = b.getRootPane();627if (rootPane == null) return;628629final RootPaneUI ui = rootPane.getUI();630if (!(ui instanceof AquaRootPaneUI)) return;631((AquaRootPaneUI)ui).updateDefaultButton(rootPane);632}633}634}635636637