001    /* BasicGraphicsUtils.java
002       Copyright (C) 2003, 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    package javax.swing.plaf.basic;
039    
040    import gnu.classpath.SystemProperties;
041    
042    import java.awt.Color;
043    import java.awt.Dimension;
044    import java.awt.Font;
045    import java.awt.FontMetrics;
046    import java.awt.Graphics;
047    import java.awt.Graphics2D;
048    import java.awt.Insets;
049    import java.awt.Rectangle;
050    import java.awt.font.FontRenderContext;
051    import java.awt.font.LineMetrics;
052    import java.awt.font.TextLayout;
053    import java.awt.geom.Rectangle2D;
054    
055    import javax.swing.AbstractButton;
056    import javax.swing.Icon;
057    import javax.swing.JComponent;
058    import javax.swing.SwingUtilities;
059    
060    
061    /**
062     * A utility class providing commonly used drawing and measurement
063     * routines.
064     *
065     * @author Sascha Brawer (brawer@dandelis.ch)
066     */
067    public class BasicGraphicsUtils
068    {
069      /**
070       * Used as a key for a client property to store cached TextLayouts in. This
071       * is used for speed-up drawing of text in
072       * {@link #drawString(Graphics, String, int, int, int)}.
073       */
074      static final String CACHED_TEXT_LAYOUT =
075        "BasicGraphicsUtils.cachedTextLayout";
076    
077      /**
078       * Constructor. It is utterly unclear why this class should
079       * be constructable, but this is what the API specification
080       * says.
081       */
082      public BasicGraphicsUtils()
083      {
084        // Nothing to do here.
085      }
086    
087    
088      /**
089       * Draws a rectangle that appears etched into the surface, given
090       * four colors that are used for drawing.
091       *
092       * <p><img src="doc-files/BasicGraphicsUtils-1.png" width="360"
093       * height="200" alt="[An illustration that shows which pixels
094       * get painted in what color]" />
095       *
096       * @param g the graphics into which the rectangle is drawn.
097       * @param x the x coordinate of the rectangle.
098       * @param y the y coordinate of the rectangle.
099       * @param width the width of the rectangle in pixels.
100       * @param height the height of the rectangle in pixels.
101       *
102       * @param shadow the color that will be used for painting
103       *        the outer side of the top and left edges.
104       *
105       * @param darkShadow the color that will be used for painting
106       *        the inner side of the top and left edges.
107       *
108       * @param highlight the color that will be used for painting
109       *        the inner side of the bottom and right edges.
110       *
111       * @param lightHighlight the color that will be used for painting
112       *        the outer side of the bottom and right edges.
113       *
114       * @see #getEtchedInsets()
115       * @see javax.swing.border.EtchedBorder
116       */
117      public static void drawEtchedRect(Graphics g,
118                                        int x, int y, int width, int height,
119                                        Color shadow, Color darkShadow,
120                                        Color highlight, Color lightHighlight)
121      {
122        Color oldColor;
123        int x2, y2;
124    
125        oldColor = g.getColor();
126        x2 = x + width - 1;
127        y2 = y + height - 1;
128    
129        try
130        {
131          /* To understand this code, it might be helpful to look at the
132           * image "BasicGraphicsUtils-1.png" that is included with the
133           * JavaDoc. The file is located in the "doc-files" subdirectory.
134           *
135           * (x2, y2) is the coordinate of the most right and bottom pixel
136           * to be painted.
137           */
138          g.setColor(shadow);
139          g.drawLine(x, y, x2 - 1, y);                     // top, outer
140          g.drawLine(x, y + 1, x, y2 - 1);                 // left, outer
141    
142          g.setColor(darkShadow);
143          g.drawLine(x + 1, y + 1, x2 - 2, y + 1);         // top, inner
144          g.drawLine(x + 1, y + 2, x + 1, y2 - 2);         // left, inner
145          
146          g.setColor(highlight);
147          g.drawLine(x + 1, y2 - 1, x2 - 1, y2 - 1);       // bottom, inner
148          g.drawLine(x2 - 1, y + 1, x2 - 1, y2 - 2);       // right, inner
149    
150          g.setColor(lightHighlight);
151          g.drawLine(x, y2, x2, y2);                       // bottom, outer
152          g.drawLine(x2, y, x2, y2 - 1);                   // right, outer
153        }
154        finally
155        {
156          g.setColor(oldColor);
157        }
158      }
159      
160      
161      /**
162       * Determines the width of the border that gets painted by
163       * {@link #drawEtchedRect}.
164       *
165       * @return an <code>Insets</code> object whose <code>top</code>,
166       *         <code>left</code>, <code>bottom</code> and
167       *         <code>right</code> field contain the border width at the
168       *         respective edge in pixels.
169       */
170      public static Insets getEtchedInsets()
171      {
172        return new Insets(2, 2, 2, 2);
173      }
174    
175    
176      /**
177       * Draws a rectangle that appears etched into the surface, given
178       * two colors that are used for drawing.
179       *
180       * <p><img src="doc-files/BasicGraphicsUtils-2.png" width="360"
181       * height="200" alt="[An illustration that shows which pixels
182       * get painted in what color]" />
183       *
184       * @param g the graphics into which the rectangle is drawn.
185       * @param x the x coordinate of the rectangle.
186       * @param y the y coordinate of the rectangle.
187       * @param width the width of the rectangle in pixels.
188       * @param height the height of the rectangle in pixels.
189       *
190       * @param shadow the color that will be used for painting the outer
191       *        side of the top and left edges, and for the inner side of
192       *        the bottom and right ones.
193       *
194       * @param highlight the color that will be used for painting the
195       *        inner side of the top and left edges, and for the outer
196       *        side of the bottom and right ones.
197       *
198       * @see #getGrooveInsets()
199       * @see javax.swing.border.EtchedBorder
200       */
201      public static void drawGroove(Graphics g,
202                                    int x, int y, int width, int height,
203                                    Color shadow, Color highlight)
204      {
205        /* To understand this, it might be helpful to look at the image
206         * "BasicGraphicsUtils-2.png" that is included with the JavaDoc,
207         * and to compare it with "BasicGraphicsUtils-1.png" which shows
208         * the pixels painted by drawEtchedRect.  These image files are
209         * located in the "doc-files" subdirectory.
210         */
211        drawEtchedRect(g, x, y, width, height,
212                       /* outer topLeft */     shadow,
213                       /* inner topLeft */     highlight,
214                       /* inner bottomRight */ shadow,
215                       /* outer bottomRight */ highlight);
216      }
217    
218    
219      /**
220       * Determines the width of the border that gets painted by
221       * {@link #drawGroove}.
222       *
223       * @return an <code>Insets</code> object whose <code>top</code>,
224       *         <code>left</code>, <code>bottom</code> and
225       *         <code>right</code> field contain the border width at the
226       *         respective edge in pixels.
227       */
228      public static Insets getGrooveInsets()
229      {
230        return new Insets(2, 2, 2, 2);
231      }
232      
233    
234      /**
235       * Draws a border that is suitable for buttons of the Basic look and
236       * feel.
237       *
238       * <p><img src="doc-files/BasicGraphicsUtils-3.png" width="500"
239       * height="300" alt="[An illustration that shows which pixels
240       * get painted in what color]" />
241       *
242       * @param g the graphics into which the rectangle is drawn.
243       * @param x the x coordinate of the rectangle.
244       * @param y the y coordinate of the rectangle.
245       * @param width the width of the rectangle in pixels.
246       * @param height the height of the rectangle in pixels.
247       *
248       * @param isPressed <code>true</code> to draw the button border
249       *        with a pressed-in appearance; <code>false</code> for
250       *        normal (unpressed) appearance.
251       *
252       * @param isDefault <code>true</code> to draw the border with
253       *        the appearance it has when hitting the enter key in a
254       *        dialog will simulate a click to this button;
255       *        <code>false</code> for normal appearance.
256       *
257       * @param shadow the shadow color.
258       * @param darkShadow a darker variant of the shadow color.
259       * @param highlight the highlight color.
260       * @param lightHighlight a brighter variant of the highlight  color.
261       */
262      public static void drawBezel(Graphics g,
263                                   int x, int y, int width, int height,
264                                   boolean isPressed, boolean isDefault,
265                                   Color shadow, Color darkShadow,
266                                   Color highlight, Color lightHighlight)
267      {
268        Color oldColor = g.getColor();
269    
270        /* To understand this, it might be helpful to look at the image
271         * "BasicGraphicsUtils-3.png" that is included with the JavaDoc,
272         * and to compare it with "BasicGraphicsUtils-1.png" which shows
273         * the pixels painted by drawEtchedRect.  These image files are
274         * located in the "doc-files" subdirectory.
275         */
276        try
277        {
278          if ((isPressed == false) && (isDefault == false))
279          {
280            drawEtchedRect(g, x, y, width, height,
281                           lightHighlight, highlight,
282                           shadow, darkShadow);
283          }
284    
285          if ((isPressed == true) && (isDefault == false))
286          {
287            g.setColor(shadow);
288            g.drawRect(x + 1, y + 1, width - 2, height - 2);
289          }
290    
291          if ((isPressed == false) && (isDefault == true))
292          {
293            g.setColor(darkShadow);
294            g.drawRect(x, y, width - 1, height - 1);
295            drawEtchedRect(g, x + 1, y + 1, width - 2, height - 2,
296                           lightHighlight, highlight,
297                           shadow, darkShadow);
298          }
299    
300          if ((isPressed == true) && (isDefault == true))
301          {
302            g.setColor(darkShadow);
303            g.drawRect(x, y, width - 1, height - 1);
304            g.setColor(shadow);
305            g.drawRect(x + 1, y + 1, width - 3, height - 3);
306          }
307        }
308        finally
309        {
310          g.setColor(oldColor);
311        }
312      }
313      
314      
315      /**
316       * Draws a rectangle that appears lowered into the surface, given
317       * four colors that are used for drawing.
318       *
319       * <p><img src="doc-files/BasicGraphicsUtils-4.png" width="360"
320       * height="200" alt="[An illustration that shows which pixels
321       * get painted in what color]" />
322       *
323       * <p><strong>Compatibility with the Sun reference
324       * implementation:</strong> The Sun reference implementation seems
325       * to ignore the <code>x</code> and <code>y</code> arguments, at
326       * least in JDK 1.3.1 and 1.4.1_01.  The method always draws the
327       * rectangular area at location (0, 0). A bug report has been filed
328       * with Sun; its &#x201c;bug ID&#x201d; is 4880003.  The GNU Classpath
329       * implementation behaves correctly, thus not replicating this bug.
330       *
331       * @param g the graphics into which the rectangle is drawn.
332       * @param x the x coordinate of the rectangle.
333       * @param y the y coordinate of the rectangle.
334       * @param width the width of the rectangle in pixels.
335       * @param height the height of the rectangle in pixels.
336       *
337       * @param shadow the color that will be used for painting
338       *        the inner side of the top and left edges.
339       *
340       * @param darkShadow the color that will be used for painting
341       *        the outer side of the top and left edges.
342       *
343       * @param highlight the color that will be used for painting
344       *        the inner side of the bottom and right edges.
345       *
346       * @param lightHighlight the color that will be used for painting
347       *        the outer side of the bottom and right edges.
348       */
349      public static void drawLoweredBezel(Graphics g,
350                                          int x, int y, int width, int height,
351                                          Color shadow, Color darkShadow,
352                                          Color highlight, Color lightHighlight)
353      {
354        /* Like drawEtchedRect, but swapping darkShadow and shadow.
355         *
356         * To understand this, it might be helpful to look at the image
357         * "BasicGraphicsUtils-4.png" that is included with the JavaDoc,
358         * and to compare it with "BasicGraphicsUtils-1.png" which shows
359         * the pixels painted by drawEtchedRect.  These image files are
360         * located in the "doc-files" subdirectory.
361         */
362        drawEtchedRect(g, x, y, width, height,
363                       darkShadow, shadow,
364                       highlight, lightHighlight);
365      }
366      
367      
368      /**
369       * Draws a String at the given location, underlining the first
370       * occurence of a specified character. The algorithm for determining
371       * the underlined position is not sensitive to case. If the
372       * character is not part of <code>text</code>, the text will be
373       * drawn without underlining. Drawing is performed in the current
374       * color and font of <code>g</code>.
375       *
376       * <p><img src="doc-files/BasicGraphicsUtils-5.png" width="500"
377       * height="100" alt="[An illustration showing how to use the
378       * method]" />
379       *
380       * @param g the graphics into which the String is drawn.
381       *
382       * @param text the String to draw.
383       *
384       * @param underlinedChar the character whose first occurence in
385       *        <code>text</code> will be underlined. It is not clear
386       *        why the API specification declares this argument to be
387       *        of type <code>int</code> instead of <code>char</code>.
388       *        While this would allow to pass Unicode characters outside
389       *        Basic Multilingual Plane 0 (U+0000 .. U+FFFE), at least
390       *        the GNU Classpath implementation does not underline
391       *        anything if <code>underlinedChar</code> is outside
392       *        the range of <code>char</code>.
393       *        
394       * @param x the x coordinate of the text, as it would be passed to
395       *        {@link java.awt.Graphics#drawString(java.lang.String,
396       *        int, int)}.
397       *
398       * @param y the y coordinate of the text, as it would be passed to
399       *        {@link java.awt.Graphics#drawString(java.lang.String,
400       *        int, int)}.
401       */
402      public static void drawString(Graphics g, String text,
403                                    int underlinedChar, int x, int y)
404      {
405        int index = -1;
406    
407        /* It is intentional that lower case is used. In some languages,
408         * the set of lowercase characters is larger than the set of
409         * uppercase ones. Therefore, it is good practice to use lowercase
410         * for such comparisons (which really means that the author of this
411         * code can vaguely remember having read some Unicode techreport
412         * with this recommendation, but is too lazy to look for the URL).
413         */
414        if ((underlinedChar >= 0) || (underlinedChar <= 0xffff))
415          index = text.toLowerCase().indexOf(
416            Character.toLowerCase((char) underlinedChar));
417    
418        drawStringUnderlineCharAt(g, text, index, x, y);
419      }
420    
421    
422      /**
423       * Draws a String at the given location, underlining the character
424       * at the specified index. Drawing is performed in the current color
425       * and font of <code>g</code>.
426       *
427       * <p><img src="doc-files/BasicGraphicsUtils-5.png" width="500"
428       * height="100" alt="[An illustration showing how to use the
429       * method]" />
430       *
431       * @param g the graphics into which the String is drawn.
432       *
433       * @param text the String to draw.
434       *
435       * @param underlinedIndex the index of the underlined character in
436       *        <code>text</code>.  If <code>underlinedIndex</code> falls
437       *        outside the range <code>[0, text.length() - 1]</code>, the
438       *        text will be drawn without underlining anything.
439       *        
440       * @param x the x coordinate of the text, as it would be passed to
441       *        {@link java.awt.Graphics#drawString(java.lang.String,
442       *        int, int)}.
443       *
444       * @param y the y coordinate of the text, as it would be passed to
445       *        {@link java.awt.Graphics#drawString(java.lang.String,
446       *        int, int)}.
447       *
448       * @since 1.4
449       */
450      public static void drawStringUnderlineCharAt(Graphics g, String text,
451                                                   int underlinedIndex,
452                                                   int x, int y)
453      {
454        Graphics2D g2;
455        Rectangle2D.Double underline;
456        FontRenderContext frc;
457        FontMetrics fmet;
458        LineMetrics lineMetrics;
459        Font font;
460        TextLayout layout;
461        double underlineX1, underlineX2;
462        boolean drawUnderline;
463        int textLength;
464    
465        textLength = text.length();
466        if (textLength == 0)
467          return;
468    
469        drawUnderline = (underlinedIndex >= 0) && (underlinedIndex < textLength);
470    
471        // FIXME: unfortunately pango and cairo can't agree on metrics
472        // so for the time being we continue to *not* use TextLayouts.
473        if (true || !(g instanceof Graphics2D))
474        {
475          /* Fall-back. This is likely to produce garbage for any text
476           * containing right-to-left (Hebrew or Arabic) characters, even
477           * if the underlined character is left-to-right.
478           */
479          g.drawString(text, x, y);
480          if (drawUnderline)
481          {
482            fmet = g.getFontMetrics();
483            g.fillRect(
484              /* x */ x + fmet.stringWidth(text.substring(0, underlinedIndex)),
485              /* y */ y + fmet.getDescent() - 1,
486              /* width */ fmet.charWidth(text.charAt(underlinedIndex)),
487              /* height */ 1);
488          }
489    
490          return;
491        }
492    
493        g2 = (Graphics2D) g;
494        font = g2.getFont();
495        frc = g2.getFontRenderContext();
496        lineMetrics = font.getLineMetrics(text, frc);
497        layout = new TextLayout(text, font, frc);
498    
499        /* Draw the text. */
500        layout.draw(g2, x, y);
501        if (!drawUnderline)
502          return;
503    
504        underlineX1 = x + layout.getLogicalHighlightShape(
505         underlinedIndex, underlinedIndex).getBounds2D().getX();
506        underlineX2 = x + layout.getLogicalHighlightShape(
507         underlinedIndex + 1, underlinedIndex + 1).getBounds2D().getX();
508    
509        underline = new Rectangle2D.Double();
510        if (underlineX1 < underlineX2)
511        {
512          underline.x = underlineX1;
513          underline.width = underlineX2 - underlineX1;
514        }
515        else
516        {
517          underline.x = underlineX2;
518          underline.width = underlineX1 - underlineX2;
519        }
520    
521        
522        underline.height = lineMetrics.getUnderlineThickness();
523        underline.y = lineMetrics.getUnderlineOffset();
524        if (underline.y == 0)
525        {
526          /* Some fonts do not specify an underline offset, although they
527           * actually should do so. In that case, the result of calling
528           * lineMetrics.getUnderlineOffset() will be zero. Since it would
529           * look very ugly if the underline was be positioned immediately
530           * below the baseline, we check for this and move the underline
531           * below the descent, as shown in the following ASCII picture:
532           *
533           *   #####       ##### #
534           *  #     #     #     #
535           *  #     #     #     #
536           *  #     #     #     #
537           *   #####       ######        ---- baseline (0)
538           *                    #
539           *                    #
540           * ------------------###----------- lineMetrics.getDescent()
541           */
542          underline.y = lineMetrics.getDescent();
543        }
544    
545        underline.y += y;
546        g2.fill(underline);
547      }
548    
549      /**
550       * Draws a string on the specified component.
551       *
552       * @param c the component
553       * @param g the Graphics context
554       * @param text the string
555       * @param underlinedChar the character to be underlined
556       * @param x the X location
557       * @param y the Y location
558       */
559      static void drawString(JComponent c, Graphics g, String text,
560                                    int underlinedChar, int x, int y)
561      {
562        int index = -1;
563    
564        /* It is intentional that lower case is used. In some languages,
565         * the set of lowercase characters is larger than the set of
566         * uppercase ones. Therefore, it is good practice to use lowercase
567         * for such comparisons (which really means that the author of this
568         * code can vaguely remember having read some Unicode techreport
569         * with this recommendation, but is too lazy to look for the URL).
570         */
571        if ((underlinedChar >= 0) || (underlinedChar <= 0xffff))
572          index = text.toLowerCase().indexOf(
573            Character.toLowerCase((char) underlinedChar));
574    
575        drawStringUnderlineCharAt(c, g, text, index, x, y);
576      }
577    
578    
579      /**
580       * Draws a String at the given location, underlining the character
581       * at the specified index. Drawing is performed in the current color
582       * and font of <code>g</code>.
583       *
584       * <p><img src="doc-files/BasicGraphicsUtils-5.png" width="500"
585       * height="100" alt="[An illustration showing how to use the
586       * method]" />
587       *
588       * This is an accelerated version of the method with the same name. It
589       * uses a pre-laid out TextLayout stored in a client property.
590       *
591       * @param c the component that is drawn
592       * @param g the graphics into which the String is drawn.
593       *
594       * @param text the String to draw.
595       *
596       * @param underlinedIndex the index of the underlined character in
597       *        <code>text</code>.  If <code>underlinedIndex</code> falls
598       *        outside the range <code>[0, text.length() - 1]</code>, the
599       *        text will be drawn without underlining anything.
600       *        
601       * @param x the x coordinate of the text, as it would be passed to
602       *        {@link java.awt.Graphics#drawString(java.lang.String,
603       *        int, int)}.
604       *
605       * @param y the y coordinate of the text, as it would be passed to
606       *        {@link java.awt.Graphics#drawString(java.lang.String,
607       *        int, int)}.
608       */
609      static void drawStringUnderlineCharAt(JComponent c, Graphics g, String text,
610                                            int underlinedIndex,
611                                            int x, int y)
612      {
613        Graphics2D g2;
614        Rectangle2D.Double underline;
615        FontRenderContext frc;
616        FontMetrics fmet;
617        LineMetrics lineMetrics;
618        Font font;
619        TextLayout layout;
620        double underlineX1, underlineX2;
621        boolean drawUnderline;
622        int textLength;
623    
624        textLength = text.length();
625        if (textLength == 0)
626          return;
627    
628        drawUnderline = (underlinedIndex >= 0) && (underlinedIndex < textLength);
629    
630        // FIXME: unfortunately pango and cairo can't agree on metrics
631        // so for the time being we continue to *not* use TextLayouts.
632        if (!(g instanceof Graphics2D)
633           || SystemProperties.getProperty("gnu.javax.swing.noGraphics2D") != null)
634        {
635          /* Fall-back. This is likely to produce garbage for any text
636           * containing right-to-left (Hebrew or Arabic) characters, even
637           * if the underlined character is left-to-right.
638           */
639          g.drawString(text, x, y);
640          if (drawUnderline)
641          {
642            fmet = g.getFontMetrics();
643            g.fillRect(
644              /* x */ x + fmet.stringWidth(text.substring(0, underlinedIndex)),
645              /* y */ y + fmet.getDescent() - 1,
646              /* width */ fmet.charWidth(text.charAt(underlinedIndex)),
647              /* height */ 1);
648          }
649    
650          return;
651        }
652    
653        g2 = (Graphics2D) g;
654        font = g2.getFont();
655        frc = g2.getFontRenderContext();
656        lineMetrics = font.getLineMetrics(text, frc);
657        layout = (TextLayout) c.getClientProperty(CACHED_TEXT_LAYOUT);
658        if (layout == null)
659          {
660            layout = new TextLayout(text, font, frc);
661            System.err.println("Unable to use cached TextLayout for: " + text);
662          }
663    
664        /* Draw the text. */
665        layout.draw(g2, x, y);
666        if (!drawUnderline)
667          return;
668    
669        underlineX1 = x + layout.getLogicalHighlightShape(
670         underlinedIndex, underlinedIndex).getBounds2D().getX();
671        underlineX2 = x + layout.getLogicalHighlightShape(
672         underlinedIndex + 1, underlinedIndex + 1).getBounds2D().getX();
673    
674        underline = new Rectangle2D.Double();
675        if (underlineX1 < underlineX2)
676        {
677          underline.x = underlineX1;
678          underline.width = underlineX2 - underlineX1;
679        }
680        else
681        {
682          underline.x = underlineX2;
683          underline.width = underlineX1 - underlineX2;
684        }
685    
686        
687        underline.height = lineMetrics.getUnderlineThickness();
688        underline.y = lineMetrics.getUnderlineOffset();
689        if (underline.y == 0)
690        {
691          /* Some fonts do not specify an underline offset, although they
692           * actually should do so. In that case, the result of calling
693           * lineMetrics.getUnderlineOffset() will be zero. Since it would
694           * look very ugly if the underline was be positioned immediately
695           * below the baseline, we check for this and move the underline
696           * below the descent, as shown in the following ASCII picture:
697           *
698           *   #####       ##### #
699           *  #     #     #     #
700           *  #     #     #     #
701           *  #     #     #     #
702           *   #####       ######        ---- baseline (0)
703           *                    #
704           *                    #
705           * ------------------###----------- lineMetrics.getDescent()
706           */
707          underline.y = lineMetrics.getDescent();
708        }
709    
710        underline.y += y;
711        g2.fill(underline);
712      }
713    
714      /**
715       * Draws a rectangle, simulating a dotted stroke by painting only
716       * every second pixel along the one-pixel thick edge. The color of
717       * those pixels is the current color of the Graphics <code>g</code>.
718       * Any other pixels are left unchanged.
719       *
720       * <p><img src="doc-files/BasicGraphicsUtils-7.png" width="360"
721       * height="200" alt="[An illustration that shows which pixels
722       * get painted]" />
723       *
724       * @param g the graphics into which the rectangle is drawn.
725       * @param x the x coordinate of the rectangle.
726       * @param y the y coordinate of the rectangle.
727       * @param width the width of the rectangle in pixels.
728       * @param height the height of the rectangle in pixels.
729       */
730      public static void drawDashedRect(Graphics g,
731                                        int x, int y, int width, int height)
732      {
733        int right = x + width - 1;
734        int bottom = y + height - 1;
735    
736        /* Draw the top and bottom edge of the dotted rectangle. */
737        for (int i = x; i <= right; i += 2)
738        {
739          g.drawLine(i, y, i, y);
740          g.drawLine(i, bottom, i, bottom);
741        }
742    
743        /* Draw the left and right edge of the dotted rectangle. */
744        for (int i = y; i <= bottom; i += 2)
745        {
746          g.drawLine(x, i, x, i);
747          g.drawLine(right, i, right, i);
748        }
749      }
750    
751      /**
752       * Determines the preferred width and height of an AbstractButton,
753       * given the gap between the button&#x2019;s text and icon.
754       *
755       * @param b the button whose preferred size is determined.
756       *
757       * @param textIconGap the gap between the button&#x2019;s text and
758       *        icon.
759       *
760       * @return a <code>Dimension</code> object whose <code>width</code>
761       *         and <code>height</code> fields indicate the preferred
762       *         extent in pixels.
763       *
764       * @see javax.swing.SwingUtilities#layoutCompoundLabel(JComponent, 
765       *      FontMetrics, String, Icon, int, int, int, int, Rectangle, Rectangle, 
766       *      Rectangle, int)
767       */
768      public static Dimension getPreferredButtonSize(AbstractButton b,
769                                                     int textIconGap)
770      {
771        // These cached rectangles are use here and in BasicButtonUI.paint(),
772        // so these two methods must never be executed concurrently. Maybe
773        // we must use other Rectangle instances here. OTOH, Swing is
774        // designed to be not thread safe, and every layout and paint operation
775        // should be performed from the EventDispatchThread, so it _should_ be
776        // OK to do this optimization.
777        Rectangle viewRect = BasicButtonUI.viewR;
778        viewRect.x = 0;
779        viewRect.y = 0;
780        viewRect.width = Short.MAX_VALUE;
781        viewRect.height = Short.MAX_VALUE;
782        Rectangle iconRect = BasicButtonUI.iconR;
783        iconRect.x = 0;
784        iconRect.y = 0;
785        iconRect.width = 0;
786        iconRect.height = 0;
787        Rectangle textRect = BasicButtonUI.textR;
788        textRect.x = 0;
789        textRect.y = 0;
790        textRect.width = 0;
791        textRect.height = 0;
792    
793       SwingUtilities.layoutCompoundLabel(
794          b, // for the component orientation
795          b.getFontMetrics(b.getFont()), // see comment above
796          b.getText(),
797          b.getIcon(),
798          b.getVerticalAlignment(), 
799          b.getHorizontalAlignment(),
800          b.getVerticalTextPosition(),
801          b.getHorizontalTextPosition(),
802          viewRect, iconRect, textRect,
803          textIconGap);
804    
805        /*  +------------------------+       +------------------------+
806         *  |                        |       |                        |
807         *  | ICON                   |       | CONTENTCONTENTCONTENT  |
808         *  |          TEXTTEXTTEXT  |  -->  | CONTENTCONTENTCONTENT  |
809         *  |          TEXTTEXTTEXT  |       | CONTENTCONTENTCONTENT  |
810         *  +------------------------+       +------------------------+
811         */
812    
813        Rectangle contentRect =
814          SwingUtilities.computeUnion(textRect.x, textRect.y, textRect.width,
815                                      textRect.height, iconRect);
816    
817        Insets insets = b.getInsets();
818        return new Dimension(insets.left + contentRect.width + insets.right,
819                             insets.top + contentRect.height + insets.bottom);
820      }
821    }