001 /* BasicButtonUI.java -- 002 Copyright (C) 2002, 2004, 2005 Free Software Foundation, Inc. 003 004 This file is part of GNU Classpath. 005 006 GNU Classpath is free software; you can redistribute it and/or modify 007 it under the terms of the GNU General Public License as published by 008 the Free Software Foundation; either version 2, or (at your option) 009 any later version. 010 011 GNU Classpath is distributed in the hope that it will be useful, but 012 WITHOUT ANY WARRANTY; without even the implied warranty of 013 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 014 General Public License for more details. 015 016 You should have received a copy of the GNU General Public License 017 along with GNU Classpath; see the file COPYING. If not, write to the 018 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 019 02110-1301 USA. 020 021 Linking this library statically or dynamically with other modules is 022 making a combined work based on this library. Thus, the terms and 023 conditions of the GNU General Public License cover the whole 024 combination. 025 026 As a special exception, the copyright holders of this library give you 027 permission to link this library with independent modules to produce an 028 executable, regardless of the license terms of these independent 029 modules, and to copy and distribute the resulting executable under 030 terms of your choice, provided that you also meet, for each linked 031 independent module, the terms and conditions of the license of that 032 module. An independent module is a module which is not derived from 033 or based on this library. If you modify this library, you may extend 034 this exception to your version of the library, but you are not 035 obligated to do so. If you do not wish to do so, delete this 036 exception statement from your version. */ 037 038 039 package javax.swing.plaf.basic; 040 041 import java.awt.Dimension; 042 import java.awt.Font; 043 import java.awt.FontMetrics; 044 import java.awt.Graphics; 045 import java.awt.Insets; 046 import java.awt.Rectangle; 047 import java.beans.PropertyChangeEvent; 048 import java.beans.PropertyChangeListener; 049 050 import javax.swing.AbstractButton; 051 import javax.swing.ButtonModel; 052 import javax.swing.Icon; 053 import javax.swing.JButton; 054 import javax.swing.JComponent; 055 import javax.swing.LookAndFeel; 056 import javax.swing.SwingUtilities; 057 import javax.swing.UIManager; 058 import javax.swing.plaf.ButtonUI; 059 import javax.swing.plaf.ComponentUI; 060 import javax.swing.plaf.UIResource; 061 import javax.swing.text.View; 062 063 /** 064 * A UI delegate for the {@link JButton} component. 065 */ 066 public class BasicButtonUI extends ButtonUI 067 { 068 /** 069 * Cached rectangle for layouting the label. Used in paint() and 070 * BasicGraphicsUtils.getPreferredButtonSize(). 071 */ 072 static Rectangle viewR = new Rectangle(); 073 074 /** 075 * Cached rectangle for layouting the label. Used in paint() and 076 * BasicGraphicsUtils.getPreferredButtonSize(). 077 */ 078 static Rectangle iconR = new Rectangle(); 079 080 /** 081 * Cached rectangle for layouting the label. Used in paint() and 082 * BasicGraphicsUtils.getPreferredButtonSize(). 083 */ 084 static Rectangle textR = new Rectangle(); 085 086 /** 087 * Cached Insets instance, used in paint(). 088 */ 089 static Insets cachedInsets; 090 091 /** 092 * The shared button UI. 093 */ 094 private static BasicButtonUI sharedUI; 095 096 /** 097 * The shared BasicButtonListener. 098 */ 099 private static BasicButtonListener sharedListener; 100 101 /** 102 * A constant used to pad out elements in the button's layout and 103 * preferred size calculations. 104 */ 105 protected int defaultTextIconGap = 4; 106 107 /** 108 * A constant added to the defaultTextIconGap to adjust the text 109 * within this particular button. 110 */ 111 protected int defaultTextShiftOffset; 112 113 private int textShiftOffset; 114 115 /** 116 * Factory method to create an instance of BasicButtonUI for a given 117 * {@link JComponent}, which should be an {@link AbstractButton}. 118 * 119 * @param c The component. 120 * 121 * @return A new UI capable of drawing the component 122 */ 123 public static ComponentUI createUI(final JComponent c) 124 { 125 if (sharedUI == null) 126 sharedUI = new BasicButtonUI(); 127 return sharedUI; 128 } 129 130 /** 131 * Returns the default gap between the button's text and icon (in pixels). 132 * 133 * @param b the button (ignored). 134 * 135 * @return The gap. 136 */ 137 public int getDefaultTextIconGap(AbstractButton b) 138 { 139 return defaultTextIconGap; 140 } 141 142 /** 143 * Sets the text shift offset to zero. 144 * 145 * @see #setTextShiftOffset() 146 */ 147 protected void clearTextShiftOffset() 148 { 149 textShiftOffset = 0; 150 } 151 152 /** 153 * Returns the text shift offset. 154 * 155 * @return The text shift offset. 156 * 157 * @see #clearTextShiftOffset() 158 * @see #setTextShiftOffset() 159 */ 160 protected int getTextShiftOffset() 161 { 162 return textShiftOffset; 163 } 164 165 /** 166 * Sets the text shift offset to the value in {@link #defaultTextShiftOffset}. 167 * 168 * @see #clearTextShiftOffset() 169 */ 170 protected void setTextShiftOffset() 171 { 172 textShiftOffset = defaultTextShiftOffset; 173 } 174 175 /** 176 * Returns the prefix for the UI defaults property for this UI class. 177 * This is 'Button' for this class. 178 * 179 * @return the prefix for the UI defaults property 180 */ 181 protected String getPropertyPrefix() 182 { 183 return "Button."; 184 } 185 186 /** 187 * Installs the default settings. 188 * 189 * @param b the button (<code>null</code> not permitted). 190 */ 191 protected void installDefaults(AbstractButton b) 192 { 193 String prefix = getPropertyPrefix(); 194 // Install colors and font. 195 LookAndFeel.installColorsAndFont(b, prefix + "background", 196 prefix + "foreground", prefix + "font"); 197 // Install border. 198 LookAndFeel.installBorder(b, prefix + "border"); 199 200 // Install margin property. 201 if (b.getMargin() == null || b.getMargin() instanceof UIResource) 202 b.setMargin(UIManager.getInsets(prefix + "margin")); 203 204 // Install rollover property. 205 Object rollover = UIManager.get(prefix + "rollover"); 206 if (rollover != null) 207 LookAndFeel.installProperty(b, "rolloverEnabled", rollover); 208 209 // Fetch default textShiftOffset. 210 defaultTextShiftOffset = UIManager.getInt(prefix + "textShiftOffset"); 211 212 // Make button opaque if needed. 213 if (b.isContentAreaFilled()) 214 LookAndFeel.installProperty(b, "opaque", Boolean.TRUE); 215 else 216 LookAndFeel.installProperty(b, "opaque", Boolean.FALSE); 217 } 218 219 /** 220 * Removes the defaults added by {@link #installDefaults(AbstractButton)}. 221 * 222 * @param b the button (<code>null</code> not permitted). 223 */ 224 protected void uninstallDefaults(AbstractButton b) 225 { 226 // The other properties aren't uninstallable. 227 LookAndFeel.uninstallBorder(b); 228 } 229 230 /** 231 * Creates and returns a new instance of {@link BasicButtonListener}. This 232 * method provides a hook to make it easy for subclasses to install a 233 * different listener. 234 * 235 * @param b the button. 236 * 237 * @return A new listener. 238 */ 239 protected BasicButtonListener createButtonListener(AbstractButton b) 240 { 241 // Note: The RI always returns a new instance here. However, 242 // the BasicButtonListener class is perfectly suitable to be shared 243 // between multiple buttons, so we return a shared instance here 244 // for efficiency. 245 if (sharedListener == null) 246 sharedListener = new BasicButtonListener(b); 247 return sharedListener; 248 } 249 250 /** 251 * Installs listeners for the button. 252 * 253 * @param b the button (<code>null</code> not permitted). 254 */ 255 protected void installListeners(AbstractButton b) 256 { 257 BasicButtonListener listener = createButtonListener(b); 258 if (listener != null) 259 { 260 b.addChangeListener(listener); 261 b.addPropertyChangeListener(listener); 262 b.addFocusListener(listener); 263 b.addMouseListener(listener); 264 b.addMouseMotionListener(listener); 265 } 266 // Fire synthetic property change event to let the listener update 267 // the TextLayout cache. 268 listener.propertyChange(new PropertyChangeEvent(b, "font", null, 269 b.getFont())); 270 } 271 272 /** 273 * Uninstalls listeners for the button. 274 * 275 * @param b the button (<code>null</code> not permitted). 276 */ 277 protected void uninstallListeners(AbstractButton b) 278 { 279 BasicButtonListener listener = getButtonListener(b); 280 if (listener != null) 281 { 282 b.removeChangeListener(listener); 283 b.removePropertyChangeListener(listener); 284 b.removeFocusListener(listener); 285 b.removeMouseListener(listener); 286 b.removeMouseMotionListener(listener); 287 } 288 } 289 290 protected void installKeyboardActions(AbstractButton b) 291 { 292 BasicButtonListener listener = getButtonListener(b); 293 if (listener != null) 294 listener.installKeyboardActions(b); 295 } 296 297 protected void uninstallKeyboardActions(AbstractButton b) 298 { 299 BasicButtonListener listener = getButtonListener(b); 300 if (listener != null) 301 listener.uninstallKeyboardActions(b); 302 } 303 304 /** 305 * Install the BasicButtonUI as the UI for a particular component. 306 * This means registering all the UI's listeners with the component, 307 * and setting any properties of the button which are particular to 308 * this look and feel. 309 * 310 * @param c The component to install the UI into 311 */ 312 public void installUI(final JComponent c) 313 { 314 super.installUI(c); 315 if (c instanceof AbstractButton) 316 { 317 AbstractButton b = (AbstractButton) c; 318 installDefaults(b); 319 // It is important to install the listeners before installing 320 // the keyboard actions, because the keyboard actions 321 // are actually installed on the listener instance. 322 installListeners(b); 323 installKeyboardActions(b); 324 BasicHTML.updateRenderer(b, b.getText()); 325 } 326 } 327 328 /** 329 * Uninstalls the UI from the component. 330 * 331 * @param c the component from which to uninstall the UI 332 */ 333 public void uninstallUI(JComponent c) 334 { 335 if (c instanceof AbstractButton) 336 { 337 AbstractButton b = (AbstractButton) c; 338 uninstallKeyboardActions(b); 339 uninstallListeners(b); 340 uninstallDefaults(b); 341 BasicHTML.updateRenderer(b, ""); 342 b.putClientProperty(BasicGraphicsUtils.CACHED_TEXT_LAYOUT, null); 343 } 344 } 345 346 /** 347 * Calculates the minimum size for the specified component. 348 * 349 * @param c the component for which to compute the minimum size 350 * 351 * @return the minimum size for the specified component 352 */ 353 public Dimension getMinimumSize(JComponent c) 354 { 355 Dimension size = getPreferredSize(c); 356 // When the HTML view has a minimum width different from the preferred 357 // width, then substract this here accordingly. The height is not 358 // affected by that. 359 View html = (View) c.getClientProperty(BasicHTML.propertyKey); 360 if (html != null) 361 { 362 size.width -= html.getPreferredSpan(View.X_AXIS) 363 - html.getPreferredSpan(View.X_AXIS); 364 } 365 return size; 366 } 367 368 /** 369 * Calculates the maximum size for the specified component. 370 * 371 * @param c the component for which to compute the maximum size 372 * 373 * @return the maximum size for the specified component 374 */ 375 public Dimension getMaximumSize(JComponent c) 376 { 377 Dimension size = getPreferredSize(c); 378 // When the HTML view has a maximum width different from the preferred 379 // width, then add this here accordingly. The height is not 380 // affected by that. 381 View html = (View) c.getClientProperty(BasicHTML.propertyKey); 382 if (html != null) 383 { 384 size.width += html.getMaximumSpan(View.X_AXIS) 385 - html.getPreferredSpan(View.X_AXIS); 386 } 387 return size; 388 } 389 390 /** 391 * Calculate the preferred size of this component, by delegating to 392 * {@link BasicGraphicsUtils#getPreferredButtonSize}. 393 * 394 * @param c The component to measure 395 * 396 * @return The preferred dimensions of the component 397 */ 398 public Dimension getPreferredSize(JComponent c) 399 { 400 AbstractButton b = (AbstractButton) c; 401 Dimension d = BasicGraphicsUtils.getPreferredButtonSize(b, 402 b.getIconTextGap()); 403 return d; 404 } 405 406 static Icon currentIcon(AbstractButton b) 407 { 408 Icon i = b.getIcon(); 409 ButtonModel model = b.getModel(); 410 411 if (model.isPressed() && b.getPressedIcon() != null && b.isEnabled()) 412 i = b.getPressedIcon(); 413 414 else if (model.isRollover()) 415 { 416 if (b.isSelected() && b.getRolloverSelectedIcon() != null) 417 i = b.getRolloverSelectedIcon(); 418 else if (b.getRolloverIcon() != null) 419 i = b.getRolloverIcon(); 420 } 421 422 else if (b.isSelected() && b.isEnabled()) 423 { 424 if (b.isEnabled() && b.getSelectedIcon() != null) 425 i = b.getSelectedIcon(); 426 else if (b.getDisabledSelectedIcon() != null) 427 i = b.getDisabledSelectedIcon(); 428 } 429 430 else if (! b.isEnabled() && b.getDisabledIcon() != null) 431 i = b.getDisabledIcon(); 432 433 return i; 434 } 435 436 /** 437 * Paint the component, which is an {@link AbstractButton}, according to 438 * its current state. 439 * 440 * @param g The graphics context to paint with 441 * @param c The component to paint the state of 442 */ 443 public void paint(Graphics g, JComponent c) 444 { 445 AbstractButton b = (AbstractButton) c; 446 447 Insets i = c.getInsets(cachedInsets); 448 viewR.x = i.left; 449 viewR.y = i.top; 450 viewR.width = c.getWidth() - i.left - i.right; 451 viewR.height = c.getHeight() - i.top - i.bottom; 452 textR.x = 0; 453 textR.y = 0; 454 textR.width = 0; 455 textR.height = 0; 456 iconR.x = 0; 457 iconR.y = 0; 458 iconR.width = 0; 459 iconR.height = 0; 460 461 Font f = c.getFont(); 462 g.setFont(f); 463 Icon icon = b.getIcon(); 464 String text = b.getText(); 465 text = SwingUtilities.layoutCompoundLabel(c, g.getFontMetrics(f), 466 text, icon, 467 b.getVerticalAlignment(), 468 b.getHorizontalAlignment(), 469 b.getVerticalTextPosition(), 470 b.getHorizontalTextPosition(), 471 viewR, iconR, textR, 472 text == null ? 0 473 : b.getIconTextGap()); 474 475 ButtonModel model = b.getModel(); 476 if (model.isArmed() && model.isPressed()) 477 paintButtonPressed(g, b); 478 479 if (icon != null) 480 paintIcon(g, c, iconR); 481 if (text != null) 482 { 483 View html = (View) b.getClientProperty(BasicHTML.propertyKey); 484 if (html != null) 485 html.paint(g, textR); 486 else 487 paintText(g, b, textR, text); 488 } 489 if (b.isFocusOwner() && b.isFocusPainted()) 490 paintFocus(g, b, viewR, textR, iconR); 491 } 492 493 /** 494 * Paint any focus decoration this {@link JComponent} might have. The 495 * component, which in this case will be an {@link AbstractButton}, 496 * should only have focus decoration painted if it has the focus, and its 497 * "focusPainted" property is <code>true</code>. 498 * 499 * @param g Graphics context to paint with 500 * @param b Button to paint the focus of 501 * @param vr Visible rectangle, the area in which to paint 502 * @param tr Text rectangle, contained in visible rectangle 503 * @param ir Icon rectangle, contained in visible rectangle 504 * 505 * @see AbstractButton#isFocusPainted() 506 * @see JComponent#hasFocus() 507 */ 508 protected void paintFocus(Graphics g, AbstractButton b, Rectangle vr, 509 Rectangle tr, Rectangle ir) 510 { 511 // In the BasicLookAndFeel no focus border is drawn. This can be 512 // overridden in subclasses to implement such behaviour. 513 } 514 515 /** 516 * Paint the icon for this component. Depending on the state of the 517 * component and the availability of the button's various icon 518 * properties, this might mean painting one of several different icons. 519 * 520 * @param g Graphics context to paint with 521 * @param c Component to paint the icon of 522 * @param iconRect Rectangle in which the icon should be painted 523 */ 524 protected void paintIcon(Graphics g, JComponent c, Rectangle iconRect) 525 { 526 AbstractButton b = (AbstractButton) c; 527 Icon i = currentIcon(b); 528 529 if (i != null) 530 { 531 ButtonModel m = b.getModel(); 532 if (m.isPressed() && m.isArmed()) 533 { 534 int offs = getTextShiftOffset(); 535 i.paintIcon(c, g, iconRect.x + offs, iconRect.y + offs); 536 } 537 else 538 i.paintIcon(c, g, iconRect.x, iconRect.y); 539 } 540 } 541 542 /** 543 * Paints the background area of an {@link AbstractButton} in the pressed 544 * state. This means filling the supplied area with a darker than normal 545 * background. 546 * 547 * @param g The graphics context to paint with 548 * @param b The button to paint the state of 549 */ 550 protected void paintButtonPressed(Graphics g, AbstractButton b) 551 { 552 if (b.isContentAreaFilled() && b.isOpaque()) 553 { 554 Rectangle area = new Rectangle(); 555 SwingUtilities.calculateInnerArea(b, area); 556 g.setColor(UIManager.getColor(getPropertyPrefix() + "shadow")); 557 g.fillRect(area.x, area.y, area.width, area.height); 558 } 559 } 560 561 /** 562 * Paints the "text" property of an {@link AbstractButton}. 563 * 564 * @param g The graphics context to paint with 565 * @param c The component to paint the state of 566 * @param textRect The area in which to paint the text 567 * @param text The text to paint 568 */ 569 protected void paintText(Graphics g, JComponent c, Rectangle textRect, 570 String text) 571 { 572 AbstractButton b = (AbstractButton) c; 573 Font f = b.getFont(); 574 g.setFont(f); 575 FontMetrics fm = g.getFontMetrics(f); 576 577 if (b.isEnabled()) 578 { 579 g.setColor(b.getForeground()); 580 // FIXME: Underline mnemonic. 581 BasicGraphicsUtils.drawString(b, g, text, -1, textRect.x, 582 textRect.y + fm.getAscent()); 583 } 584 else 585 { 586 String prefix = getPropertyPrefix(); 587 g.setColor(UIManager.getColor(prefix + "disabledText")); 588 // FIXME: Underline mnemonic. 589 BasicGraphicsUtils.drawString(b, g, text, -1, textRect.x, 590 textRect.y + fm.getAscent()); 591 } 592 } 593 594 /** 595 * Paints the "text" property of an {@link AbstractButton}. 596 * 597 * @param g The graphics context to paint with 598 * @param b The button to paint the state of 599 * @param textRect The area in which to paint the text 600 * @param text The text to paint 601 * 602 * @since 1.4 603 */ 604 protected void paintText(Graphics g, AbstractButton b, Rectangle textRect, 605 String text) 606 { 607 paintText(g, (JComponent) b, textRect, text); 608 } 609 610 /** 611 * A helper method that finds the BasicButtonListener for the specified 612 * button. This is there because this UI class is stateless and 613 * shared for all buttons, and thus can't store the listener 614 * as instance field. (We store our shared instance in sharedListener, 615 * however, subclasses may override createButtonListener() and we would 616 * be lost in this case). 617 * 618 * @param b the button 619 * 620 * @return the UI event listener 621 */ 622 private BasicButtonListener getButtonListener(AbstractButton b) 623 { 624 // The listener gets installed as PropertyChangeListener, 625 // so look for it in the list of property change listeners. 626 PropertyChangeListener[] listeners = b.getPropertyChangeListeners(); 627 BasicButtonListener l = null; 628 for (int i = 0; listeners != null && l == null && i < listeners.length; 629 i++) 630 { 631 if (listeners[i] instanceof BasicButtonListener) 632 l = (BasicButtonListener) listeners[i]; 633 } 634 return l; 635 } 636 }