Source for javax.swing.plaf.basic.BasicGraphicsUtils

   1: /* BasicGraphicsUtils.java
   2:    Copyright (C) 2003, 2004, 2005 Free Software Foundation, Inc.
   3: 
   4: This file is part of GNU Classpath.
   5: 
   6: GNU Classpath is free software; you can redistribute it and/or modify
   7: it under the terms of the GNU General Public License as published by
   8: the Free Software Foundation; either version 2, or (at your option)
   9: any later version.
  10: 
  11: GNU Classpath is distributed in the hope that it will be useful, but
  12: WITHOUT ANY WARRANTY; without even the implied warranty of
  13: MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
  14: General Public License for more details.
  15: 
  16: You should have received a copy of the GNU General Public License
  17: along with GNU Classpath; see the file COPYING.  If not, write to the
  18: Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
  19: 02110-1301 USA.
  20: 
  21: Linking this library statically or dynamically with other modules is
  22: making a combined work based on this library.  Thus, the terms and
  23: conditions of the GNU General Public License cover the whole
  24: combination.
  25: 
  26: As a special exception, the copyright holders of this library give you
  27: permission to link this library with independent modules to produce an
  28: executable, regardless of the license terms of these independent
  29: modules, and to copy and distribute the resulting executable under
  30: terms of your choice, provided that you also meet, for each linked
  31: independent module, the terms and conditions of the license of that
  32: module.  An independent module is a module which is not derived from
  33: or based on this library.  If you modify this library, you may extend
  34: this exception to your version of the library, but you are not
  35: obligated to do so.  If you do not wish to do so, delete this
  36: exception statement from your version. */
  37: 
  38: package javax.swing.plaf.basic;
  39: 
  40: import gnu.classpath.SystemProperties;
  41: 
  42: import java.awt.Color;
  43: import java.awt.Dimension;
  44: import java.awt.Font;
  45: import java.awt.FontMetrics;
  46: import java.awt.Graphics;
  47: import java.awt.Graphics2D;
  48: import java.awt.Insets;
  49: import java.awt.Rectangle;
  50: import java.awt.font.FontRenderContext;
  51: import java.awt.font.LineMetrics;
  52: import java.awt.font.TextLayout;
  53: import java.awt.geom.Rectangle2D;
  54: 
  55: import javax.swing.AbstractButton;
  56: import javax.swing.Icon;
  57: import javax.swing.JComponent;
  58: import javax.swing.SwingUtilities;
  59: 
  60: 
  61: /**
  62:  * A utility class providing commonly used drawing and measurement
  63:  * routines.
  64:  *
  65:  * @author Sascha Brawer (brawer@dandelis.ch)
  66:  */
  67: public class BasicGraphicsUtils
  68: {
  69:   /**
  70:    * Used as a key for a client property to store cached TextLayouts in. This
  71:    * is used for speed-up drawing of text in
  72:    * {@link #drawString(Graphics, String, int, int, int)}.
  73:    */
  74:   static final String CACHED_TEXT_LAYOUT =
  75:     "BasicGraphicsUtils.cachedTextLayout";
  76: 
  77:   /**
  78:    * Constructor. It is utterly unclear why this class should
  79:    * be constructable, but this is what the API specification
  80:    * says.
  81:    */
  82:   public BasicGraphicsUtils()
  83:   {
  84:     // Nothing to do here.
  85:   }
  86: 
  87: 
  88:   /**
  89:    * Draws a rectangle that appears etched into the surface, given
  90:    * four colors that are used for drawing.
  91:    *
  92:    * <p><img src="doc-files/BasicGraphicsUtils-1.png" width="360"
  93:    * height="200" alt="[An illustration that shows which pixels
  94:    * get painted in what color]" />
  95:    *
  96:    * @param g the graphics into which the rectangle is drawn.
  97:    * @param x the x coordinate of the rectangle.
  98:    * @param y the y coordinate of the rectangle.
  99:    * @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 + 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: }