1 /*
   2  * Copyright (c) 1997, 2019, 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 sun.swing.SwingUtilities2;
  29 import sun.awt.AppContext;
  30 
  31 import java.awt.*;
  32 import java.awt.event.*;
  33 import java.io.Serializable;
  34 import javax.swing.*;
  35 import javax.swing.border.*;
  36 import java.awt.*;
  37 import java.awt.event.*;
  38 import javax.swing.plaf.ButtonUI;
  39 import javax.swing.plaf.UIResource;
  40 import javax.swing.plaf.ComponentUI;
  41 import javax.swing.text.View;
  42 
  43 /**
  44  * BasicButton implementation
  45  *
  46  * @author Jeff Dinkins
  47  */
  48 public class BasicButtonUI extends ButtonUI{
  49     // Visual constants
  50     // NOTE: This is not used or set any where. Were we allowed to remove
  51     // fields, this would be removed.
  52     /**
  53      * The default gap between a text and an icon.
  54      */
  55     protected int defaultTextIconGap;
  56 
  57     // Amount to offset text, the value of this comes from
  58     // defaultTextShiftOffset once setTextShiftOffset has been invoked.
  59     private int shiftOffset = 0;
  60     // Value that is set in shiftOffset once setTextShiftOffset has been
  61     // invoked. The value of this comes from the defaults table.
  62     /**
  63      * The default offset of a text.
  64      */
  65     protected int defaultTextShiftOffset;
  66 
  67     private static final String propertyPrefix = "Button" + ".";
  68 
  69     private static final Object BASIC_BUTTON_UI_KEY = new Object();
  70 
  71     // ********************************
  72     //          Create PLAF
  73     // ********************************
  74     /**
  75      * Returns an instance of {@code BasicButtonUI}.
  76      *
  77      * @param c a component
  78      * @return an instance of {@code BasicButtonUI}
  79      */
  80     public static ComponentUI createUI(JComponent c) {
  81         AppContext appContext = AppContext.getAppContext();
  82         BasicButtonUI buttonUI =
  83                 (BasicButtonUI) appContext.get(BASIC_BUTTON_UI_KEY);
  84         if (buttonUI == null) {
  85             buttonUI = new BasicButtonUI();
  86             appContext.put(BASIC_BUTTON_UI_KEY, buttonUI);
  87         }
  88         return buttonUI;
  89     }
  90 
  91     /**
  92      * Returns the property prefix.
  93      *
  94      * @return the property prefix
  95      */
  96     protected String getPropertyPrefix() {
  97         return propertyPrefix;
  98     }
  99 
 100 
 101     // ********************************
 102     //          Install PLAF
 103     // ********************************
 104     public void installUI(JComponent c) {
 105         installDefaults((AbstractButton) c);
 106         installListeners((AbstractButton) c);
 107         installKeyboardActions((AbstractButton) c);
 108         BasicHTML.updateRenderer(c, ((AbstractButton) c).getText());
 109     }
 110 
 111     /**
 112      * Installs default properties.
 113      *
 114      * @param b an abstract button
 115      */
 116     protected void installDefaults(AbstractButton b) {
 117         // load shared instance defaults
 118         String pp = getPropertyPrefix();
 119 
 120         defaultTextShiftOffset = UIManager.getInt(pp + "textShiftOffset");
 121 
 122         // set the following defaults on the button
 123         if (b.isContentAreaFilled()) {
 124             LookAndFeel.installProperty(b, "opaque", Boolean.TRUE);
 125         } else {
 126             LookAndFeel.installProperty(b, "opaque", Boolean.FALSE);
 127         }
 128 
 129         if(b.getMargin() == null || (b.getMargin() instanceof UIResource)) {
 130             b.setMargin(UIManager.getInsets(pp + "margin"));
 131         }
 132 
 133         LookAndFeel.installColorsAndFont(b, pp + "background",
 134                                          pp + "foreground", pp + "font");
 135         LookAndFeel.installBorder(b, pp + "border");
 136 
 137         Object rollover = UIManager.get(pp + "rollover");
 138         if (rollover != null) {
 139             LookAndFeel.installProperty(b, "rolloverEnabled", rollover);
 140         }
 141 
 142         LookAndFeel.installProperty(b, "iconTextGap", Integer.valueOf(4));
 143     }
 144 
 145     /**
 146      * Registers listeners.
 147      *
 148      * @param b an abstract button
 149      */
 150     protected void installListeners(AbstractButton b) {
 151         BasicButtonListener listener = createButtonListener(b);
 152         if(listener != null) {
 153             b.addMouseListener(listener);
 154             b.addMouseMotionListener(listener);
 155             b.addFocusListener(listener);
 156             b.addPropertyChangeListener(listener);
 157             b.addChangeListener(listener);
 158         }
 159     }
 160 
 161     /**
 162      * Registers keyboard actions.
 163      *
 164      * @param b an abstract button
 165      */
 166     protected void installKeyboardActions(AbstractButton b){
 167         BasicButtonListener listener = getButtonListener(b);
 168 
 169         if(listener != null) {
 170             listener.installKeyboardActions(b);
 171         }
 172     }
 173 
 174 
 175     // ********************************
 176     //         Uninstall PLAF
 177     // ********************************
 178     public void uninstallUI(JComponent c) {
 179         uninstallKeyboardActions((AbstractButton) c);
 180         uninstallListeners((AbstractButton) c);
 181         uninstallDefaults((AbstractButton) c);
 182         BasicHTML.updateRenderer(c, "");
 183     }
 184 
 185     /**
 186      * Unregisters keyboard actions.
 187      *
 188      * @param b an abstract button
 189      */
 190     protected void uninstallKeyboardActions(AbstractButton b) {
 191         BasicButtonListener listener = getButtonListener(b);
 192         if(listener != null) {
 193             listener.uninstallKeyboardActions(b);
 194         }
 195     }
 196 
 197     /**
 198      * Unregisters listeners.
 199      *
 200      * @param b an abstract button
 201      */
 202     protected void uninstallListeners(AbstractButton b) {
 203         BasicButtonListener listener = getButtonListener(b);
 204         if(listener != null) {
 205             b.removeMouseListener(listener);
 206             b.removeMouseMotionListener(listener);
 207             b.removeFocusListener(listener);
 208             b.removeChangeListener(listener);
 209             b.removePropertyChangeListener(listener);
 210         }
 211     }
 212 
 213     /**
 214      * Uninstalls default properties.
 215      *
 216      * @param b an abstract button
 217      */
 218     protected void uninstallDefaults(AbstractButton b) {
 219         LookAndFeel.uninstallBorder(b);
 220     }
 221 
 222     // ********************************
 223     //        Create Listeners
 224     // ********************************
 225     /**
 226      * Returns a new instance of {@code BasicButtonListener}.
 227      *
 228      * @param b an abstract button
 229      * @return a new instance of {@code BasicButtonListener}
 230      */
 231     protected BasicButtonListener createButtonListener(AbstractButton b) {
 232         return new BasicButtonListener(b);
 233     }
 234 
 235     /**
 236      * Returns the default gap between a text and an icon.
 237      *
 238      * @param b an abstract button
 239      * @return the default gap between text and an icon
 240      */
 241     public int getDefaultTextIconGap(AbstractButton b) {
 242         return defaultTextIconGap;
 243     }
 244 
 245     /* These rectangles/insets are allocated once for all
 246      * ButtonUI.paint() calls.  Re-using rectangles rather than
 247      * allocating them in each paint call substantially reduced the time
 248      * it took paint to run.  Obviously, this method can't be re-entered.
 249      */
 250     private static Rectangle viewRect = new Rectangle();
 251     private static Rectangle textRect = new Rectangle();
 252     private static Rectangle iconRect = new Rectangle();
 253 
 254     // ********************************
 255     //          Paint Methods
 256     // ********************************
 257 
 258     public void paint(Graphics g, JComponent c)
 259     {
 260         AbstractButton b = (AbstractButton) c;
 261         ButtonModel model = b.getModel();
 262 
 263         String text = layout(b, SwingUtilities2.getFontMetrics(b, g),
 264                b.getWidth(), b.getHeight());
 265 
 266         clearTextShiftOffset();
 267 
 268         // perform UI specific press action, e.g. Windows L&F shifts text
 269         if (model.isArmed() && model.isPressed()) {
 270             paintButtonPressed(g,b);
 271         }
 272 
 273         // Paint the Icon
 274         if(b.getIcon() != null) {
 275             paintIcon(g,c,iconRect);
 276         }
 277 
 278         if (text != null && !text.isEmpty()){
 279             View v = (View) c.getClientProperty(BasicHTML.propertyKey);
 280             if (v != null) {
 281                 v.paint(g, textRect);
 282             } else {
 283                 paintText(g, b, textRect, text);
 284             }
 285         }
 286 
 287         if (b.isFocusPainted() && b.hasFocus()) {
 288             // paint UI specific focus
 289             paintFocus(g,b,viewRect,textRect,iconRect);
 290         }
 291     }
 292 
 293     /**
 294      * Paints an icon of the current button.
 295      *
 296      * @param g an instance of {@code Graphics}
 297      * @param c a component
 298      * @param iconRect a bounding rectangle to render the icon
 299      */
 300     protected void paintIcon(Graphics g, JComponent c, Rectangle iconRect){
 301             AbstractButton b = (AbstractButton) c;
 302             ButtonModel model = b.getModel();
 303             Icon icon = b.getIcon();
 304             Icon tmpIcon = null;
 305 
 306             if(icon == null) {
 307                return;
 308             }
 309 
 310             Icon selectedIcon = null;
 311 
 312             /* the fallback icon should be based on the selected state */
 313             if (model.isSelected()) {
 314                 selectedIcon = b.getSelectedIcon();
 315                 if (selectedIcon != null) {
 316                     icon = selectedIcon;
 317                 }
 318             }
 319 
 320             if(!model.isEnabled()) {
 321                 if(model.isSelected()) {
 322                    tmpIcon = b.getDisabledSelectedIcon();
 323                    if (tmpIcon == null) {
 324                        tmpIcon = selectedIcon;
 325                    }
 326                 }
 327 
 328                 if (tmpIcon == null) {
 329                     tmpIcon = b.getDisabledIcon();
 330                 }
 331             } else if(model.isPressed() && model.isArmed()) {
 332                 tmpIcon = b.getPressedIcon();
 333                 if(tmpIcon != null) {
 334                     // revert back to 0 offset
 335                     clearTextShiftOffset();
 336                 }
 337             } else if(b.isRolloverEnabled() && model.isRollover()) {
 338                 if(model.isSelected()) {
 339                    tmpIcon = b.getRolloverSelectedIcon();
 340                    if (tmpIcon == null) {
 341                        tmpIcon = selectedIcon;
 342                    }
 343                 }
 344 
 345                 if (tmpIcon == null) {
 346                     tmpIcon = b.getRolloverIcon();
 347                 }
 348             }
 349 
 350             if(tmpIcon != null) {
 351                 icon = tmpIcon;
 352             }
 353 
 354             if(model.isPressed() && model.isArmed()) {
 355                 icon.paintIcon(c, g, iconRect.x + getTextShiftOffset(),
 356                         iconRect.y + getTextShiftOffset());
 357             } else {
 358                 icon.paintIcon(c, g, iconRect.x, iconRect.y);
 359             }
 360 
 361     }
 362 
 363     /**
 364      * Method which renders the text of the current button.
 365      *
 366      * As of Java 2 platform v 1.4 this method should not be used or overriden.
 367      * Use the paintText method which takes the AbstractButton argument.
 368      *
 369      * @param g an instance of {@code Graphics}
 370      * @param c a component
 371      * @param textRect a bounding rectangle to render the text
 372      * @param text a string to render
 373      */
 374     protected void paintText(Graphics g, JComponent c, Rectangle textRect, String text) {
 375         AbstractButton b = (AbstractButton) c;
 376         ButtonModel model = b.getModel();
 377         FontMetrics fm = SwingUtilities2.getFontMetrics(c, g);
 378         int mnemonicIndex = b.getDisplayedMnemonicIndex();
 379 
 380         /* Draw the Text */
 381         if(model.isEnabled()) {
 382             /*** paint the text normally */
 383             g.setColor(b.getForeground());
 384             SwingUtilities2.drawStringUnderlineCharAt(c, g,text, mnemonicIndex,
 385                                           textRect.x + getTextShiftOffset(),
 386                                           textRect.y + fm.getAscent() + getTextShiftOffset());
 387         }
 388         else {
 389             /*** paint the text disabled ***/
 390             g.setColor(b.getBackground().brighter());
 391             SwingUtilities2.drawStringUnderlineCharAt(c, g,text, mnemonicIndex,
 392                                           textRect.x, textRect.y + fm.getAscent());
 393             g.setColor(b.getBackground().darker());
 394             SwingUtilities2.drawStringUnderlineCharAt(c, g,text, mnemonicIndex,
 395                                           textRect.x - 1, textRect.y + fm.getAscent() - 1);
 396         }
 397     }
 398 
 399     /**
 400      * Method which renders the text of the current button.
 401      *
 402      * @param g Graphics context
 403      * @param b Current button to render
 404      * @param textRect Bounding rectangle to render the text
 405      * @param text String to render
 406      * @since 1.4
 407      */
 408     protected void paintText(Graphics g, AbstractButton b, Rectangle textRect, String text) {
 409         paintText(g, (JComponent)b, textRect, text);
 410     }
 411 
 412     // Method signature defined here overriden in subclasses.
 413     // Perhaps this class should be abstract?
 414     /**
 415      * Paints a focused button.
 416      *
 417      * @param g an instance of {@code Graphics}
 418      * @param b an abstract button
 419      * @param viewRect a bounding rectangle to render the button
 420      * @param textRect a bounding rectangle to render the text
 421      * @param iconRect a bounding rectangle to render the icon
 422      */
 423     protected void paintFocus(Graphics g, AbstractButton b,
 424                               Rectangle viewRect, Rectangle textRect, Rectangle iconRect){
 425     }
 426 
 427 
 428     /**
 429      * Paints a pressed button.
 430      *
 431      * @param g an instance of {@code Graphics}
 432      * @param b an abstract button
 433      */
 434     protected void paintButtonPressed(Graphics g, AbstractButton b){
 435     }
 436 
 437     /**
 438      * Clears the offset of the text.
 439      */
 440     protected void clearTextShiftOffset(){
 441         this.shiftOffset = 0;
 442     }
 443 
 444     /**
 445      * Sets the offset of the text.
 446      */
 447     protected void setTextShiftOffset(){
 448         this.shiftOffset = defaultTextShiftOffset;
 449     }
 450 
 451     /**
 452      * Returns the offset of the text.
 453      *
 454      * @return the offset of the text
 455      */
 456     protected int getTextShiftOffset() {
 457         return shiftOffset;
 458     }
 459 
 460     // ********************************
 461     //          Layout Methods
 462     // ********************************
 463     public Dimension getMinimumSize(JComponent c) {
 464         Dimension d = getPreferredSize(c);
 465         View v = (View) c.getClientProperty(BasicHTML.propertyKey);
 466         if (v != null) {
 467             d.width -= v.getPreferredSpan(View.X_AXIS) - v.getMinimumSpan(View.X_AXIS);
 468         }
 469         return d;
 470     }
 471 
 472     public Dimension getPreferredSize(JComponent c) {
 473         AbstractButton b = (AbstractButton)c;
 474         return BasicGraphicsUtils.getPreferredButtonSize(b, b.getIconTextGap());
 475     }
 476 
 477     public Dimension getMaximumSize(JComponent c) {
 478         Dimension d = getPreferredSize(c);
 479         View v = (View) c.getClientProperty(BasicHTML.propertyKey);
 480         if (v != null) {
 481             d.width += v.getMaximumSpan(View.X_AXIS) - v.getPreferredSpan(View.X_AXIS);
 482         }
 483         return d;
 484     }
 485 
 486     /**
 487      * Returns the baseline.
 488      *
 489      * @throws NullPointerException {@inheritDoc}
 490      * @throws IllegalArgumentException {@inheritDoc}
 491      * @see javax.swing.JComponent#getBaseline(int, int)
 492      * @since 1.6
 493      */
 494     public int getBaseline(JComponent c, int width, int height) {
 495         super.getBaseline(c, width, height);
 496         AbstractButton b = (AbstractButton)c;
 497         String text = b.getText();
 498         if (text == null || text.isEmpty()) {
 499             return -1;
 500         }
 501         FontMetrics fm = b.getFontMetrics(b.getFont());
 502         layout(b, fm, width, height);
 503         return BasicHTML.getBaseline(b, textRect.y, fm.getAscent(),
 504                                      textRect.width, textRect.height);
 505     }
 506 
 507     /**
 508      * Returns an enum indicating how the baseline of the component
 509      * changes as the size changes.
 510      *
 511      * @throws NullPointerException {@inheritDoc}
 512      * @see javax.swing.JComponent#getBaseline(int, int)
 513      * @since 1.6
 514      */
 515     public Component.BaselineResizeBehavior getBaselineResizeBehavior(
 516             JComponent c) {
 517         super.getBaselineResizeBehavior(c);
 518         if (c.getClientProperty(BasicHTML.propertyKey) != null) {
 519             return Component.BaselineResizeBehavior.OTHER;
 520         }
 521         switch(((AbstractButton)c).getVerticalAlignment()) {
 522         case AbstractButton.TOP:
 523             return Component.BaselineResizeBehavior.CONSTANT_ASCENT;
 524         case AbstractButton.BOTTOM:
 525             return Component.BaselineResizeBehavior.CONSTANT_DESCENT;
 526         case AbstractButton.CENTER:
 527             return Component.BaselineResizeBehavior.CENTER_OFFSET;
 528         }
 529         return Component.BaselineResizeBehavior.OTHER;
 530     }
 531 
 532     private String layout(AbstractButton b, FontMetrics fm,
 533                           int width, int height) {
 534         Insets i = b.getInsets();
 535         viewRect.x = i.left;
 536         viewRect.y = i.top;
 537         viewRect.width = width - (i.right + viewRect.x);
 538         viewRect.height = height - (i.bottom + viewRect.y);
 539 
 540         textRect.x = textRect.y = textRect.width = textRect.height = 0;
 541         iconRect.x = iconRect.y = iconRect.width = iconRect.height = 0;
 542 
 543         // layout the text and icon
 544         return SwingUtilities.layoutCompoundLabel(
 545             b, fm, b.getText(), b.getIcon(),
 546             b.getVerticalAlignment(), b.getHorizontalAlignment(),
 547             b.getVerticalTextPosition(), b.getHorizontalTextPosition(),
 548             viewRect, iconRect, textRect,
 549             b.getText() == null ? 0 : b.getIconTextGap());
 550     }
 551 
 552     /**
 553      * Returns the ButtonListener for the passed in Button, or null if one
 554      * could not be found.
 555      */
 556     private BasicButtonListener getButtonListener(AbstractButton b) {
 557         MouseMotionListener[] listeners = b.getMouseMotionListeners();
 558 
 559         if (listeners != null) {
 560             for (MouseMotionListener listener : listeners) {
 561                 if (listener instanceof BasicButtonListener) {
 562                     return (BasicButtonListener) listener;
 563                 }
 564             }
 565         }
 566         return null;
 567     }
 568 
 569 }