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    }