001/* BasicButtonUI.java --
002   Copyright (C) 2002, 2004, 2005  Free Software Foundation, Inc.
003
004This file is part of GNU Classpath.
005
006GNU Classpath is free software; you can redistribute it and/or modify
007it under the terms of the GNU General Public License as published by
008the Free Software Foundation; either version 2, or (at your option)
009any later version.
010
011GNU Classpath is distributed in the hope that it will be useful, but
012WITHOUT ANY WARRANTY; without even the implied warranty of
013MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
014General Public License for more details.
015
016You should have received a copy of the GNU General Public License
017along with GNU Classpath; see the file COPYING.  If not, write to the
018Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
01902110-1301 USA.
020
021Linking this library statically or dynamically with other modules is
022making a combined work based on this library.  Thus, the terms and
023conditions of the GNU General Public License cover the whole
024combination.
025
026As a special exception, the copyright holders of this library give you
027permission to link this library with independent modules to produce an
028executable, regardless of the license terms of these independent
029modules, and to copy and distribute the resulting executable under
030terms of your choice, provided that you also meet, for each linked
031independent module, the terms and conditions of the license of that
032module.  An independent module is a module which is not derived from
033or based on this library.  If you modify this library, you may extend
034this exception to your version of the library, but you are not
035obligated to do so.  If you do not wish to do so, delete this
036exception statement from your version. */
037
038
039package javax.swing.plaf.basic;
040
041import java.awt.Dimension;
042import java.awt.Font;
043import java.awt.FontMetrics;
044import java.awt.Graphics;
045import java.awt.Insets;
046import java.awt.Rectangle;
047import java.beans.PropertyChangeEvent;
048import java.beans.PropertyChangeListener;
049
050import javax.swing.AbstractButton;
051import javax.swing.ButtonModel;
052import javax.swing.Icon;
053import javax.swing.JButton;
054import javax.swing.JComponent;
055import javax.swing.LookAndFeel;
056import javax.swing.SwingUtilities;
057import javax.swing.UIManager;
058import javax.swing.plaf.ButtonUI;
059import javax.swing.plaf.ComponentUI;
060import javax.swing.plaf.UIResource;
061import javax.swing.text.View;
062
063/**
064 * A UI delegate for the {@link JButton} component.
065 */
066public 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}