1 /* BasicGraphicsUtils.java 2 Copyright (C) 2003 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., 59 Temple Place, Suite 330, Boston, MA 19 02111-1307 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 java.awt.Color; 41 import java.awt.Dimension; 42 import java.awt.Font; 43 import java.awt.FontMetrics; 44 import java.awt.Graphics; 45 import java.awt.Graphics2D; 46 import java.awt.Insets; 47 import java.awt.Rectangle; 48 49 import java.awt.font.FontRenderContext; 50 import java.awt.font.LineMetrics; 51 import java.awt.font.TextLayout; 52 53 import java.awt.geom.Rectangle2D; 54 55 import javax.swing.AbstractButton; 56 import javax.swing.SwingUtilities; 57 58 59 /** 60 * A utility class providing commonly used drawing and measurement 61 * routines. 62 * 63 * @author Sascha Brawer (brawer@dandelis.ch) 64 */ 65 public class BasicGraphicsUtils 66 { 67 /** 68 * Constructor. It is utterly unclear why this class should 69 * be constructable, but this is what the API specification 70 * says. 71 */ BasicGraphicsUtils()72 public BasicGraphicsUtils() 73 { 74 } 75 76 77 /** 78 * Draws a rectangle that appears etched into the surface, given 79 * four colors that are used for drawing. 80 * 81 * <p><img src="doc-files/BasicGraphicsUtils-1.png" width="360" 82 * height="200" alt="[An illustration that shows which pixels 83 * get painted in what color]" /> 84 * 85 * @param g the graphics into which the rectangle is drawn. 86 * @param x the x coordinate of the rectangle. 87 * @param y the y coordinate of the rectangle. 88 * @param width the width of the rectangle in pixels. 89 * @param height the height of the rectangle in pixels. 90 * 91 * @param shadow the color that will be used for painting 92 * the outer side of the top and left edges. 93 * 94 * @param darkShadow the color that will be used for painting 95 * the inner side of the top and left edges. 96 * 97 * @param highlight the color that will be used for painting 98 * the inner side of the bottom and right edges. 99 * 100 * @param lightHighlight the color that will be used for painting 101 * the outer side of the bottom and right edges. 102 * 103 * @see #getEtchedInsets() 104 * @see javax.swing.border.EtchedBorder 105 */ drawEtchedRect(Graphics g, int x, int y, int width, int height, Color shadow, Color darkShadow, Color highlight, Color lightHighlight)106 public static void drawEtchedRect(Graphics g, 107 int x, int y, int width, int height, 108 Color shadow, Color darkShadow, 109 Color highlight, Color lightHighlight) 110 { 111 Color oldColor; 112 int x2, y2; 113 114 oldColor = g.getColor(); 115 x2 = x + width - 1; 116 y2 = y + height - 1; 117 118 try 119 { 120 /* To understand this code, it might be helpful to look at the 121 * image "BasicGraphicsUtils-1.png" that is included with the 122 * JavaDoc. The file is located in the "doc-files" subdirectory. 123 * 124 * (x2, y2) is the coordinate of the most right and bottom pixel 125 * to be painted. 126 */ 127 g.setColor(shadow); 128 g.drawLine(x, y, x2 - 1, y); // top, outer 129 g.drawLine(x, y + 1, x, y2 - 1); // left, outer 130 131 g.setColor(darkShadow); 132 g.drawLine(x + 1, y + 1, x2 - 2, y + 1); // top, inner 133 g.drawLine(x + 1, y + 2, x + 1, y2 - 2); // left, inner 134 135 g.setColor(highlight); 136 g.drawLine(x + 1, y2 - 1, x2 - 1, y2 - 1); // bottom, inner 137 g.drawLine(x2 - 1, y + 1, x2 - 1, y2 - 2); // right, inner 138 139 g.setColor(lightHighlight); 140 g.drawLine(x, y2, x2, y2); // bottom, outer 141 g.drawLine(x2, y, x2, y2 - 1); // right, outer 142 } 143 finally 144 { 145 g.setColor(oldColor); 146 } 147 } 148 149 150 /** 151 * Determines the width of the border that gets painted by 152 * {@link #drawEtchedRect}. 153 * 154 * @return an <code>Insets</code> object whose <code>top</code>, 155 * <code>left</code>, <code>bottom</code> and 156 * <code>right</code> field contain the border width at the 157 * respective edge in pixels. 158 */ getEtchedInsets()159 public static Insets getEtchedInsets() 160 { 161 return new Insets(2, 2, 2, 2); 162 } 163 164 165 /** 166 * Draws a rectangle that appears etched into the surface, given 167 * two colors that are used for drawing. 168 * 169 * <p><img src="doc-files/BasicGraphicsUtils-2.png" width="360" 170 * height="200" alt="[An illustration that shows which pixels 171 * get painted in what color]" /> 172 * 173 * @param g the graphics into which the rectangle is drawn. 174 * @param x the x coordinate of the rectangle. 175 * @param y the y coordinate of the rectangle. 176 * @param width the width of the rectangle in pixels. 177 * @param height the height of the rectangle in pixels. 178 * 179 * @param shadow the color that will be used for painting the outer 180 * side of the top and left edges, and for the inner side of 181 * the bottom and right ones. 182 * 183 * @param highlight the color that will be used for painting the 184 * inner side of the top and left edges, and for the outer 185 * side of the bottom and right ones. 186 * 187 * @see #getGrooveInsets() 188 * @see javax.swing.border.EtchedBorder 189 */ drawGroove(Graphics g, int x, int y, int width, int height, Color shadow, Color highlight)190 public static void drawGroove(Graphics g, 191 int x, int y, int width, int height, 192 Color shadow, Color highlight) 193 { 194 /* To understand this, it might be helpful to look at the image 195 * "BasicGraphicsUtils-2.png" that is included with the JavaDoc, 196 * and to compare it with "BasicGraphicsUtils-1.png" which shows 197 * the pixels painted by drawEtchedRect. These image files are 198 * located in the "doc-files" subdirectory. 199 */ 200 drawEtchedRect(g, x, y, width, height, 201 /* outer topLeft */ shadow, 202 /* inner topLeft */ highlight, 203 /* inner bottomRight */ shadow, 204 /* outer bottomRight */ highlight); 205 } 206 207 208 /** 209 * Determines the width of the border that gets painted by 210 * {@link #drawGroove}. 211 * 212 * @return an <code>Insets</code> object whose <code>top</code>, 213 * <code>left</code>, <code>bottom</code> and 214 * <code>right</code> field contain the border width at the 215 * respective edge in pixels. 216 */ getGrooveInsets()217 public static Insets getGrooveInsets() 218 { 219 return new Insets(2, 2, 2, 2); 220 } 221 222 223 /** 224 * Draws a border that is suitable for buttons of the Basic look and 225 * feel. 226 * 227 * <p><img src="doc-files/BasicGraphicsUtils-3.png" width="500" 228 * height="300" alt="[An illustration that shows which pixels 229 * get painted in what color]" /> 230 * 231 * @param g the graphics into which the rectangle is drawn. 232 * @param x the x coordinate of the rectangle. 233 * @param y the y coordinate of the rectangle. 234 * @param width the width of the rectangle in pixels. 235 * @param height the height of the rectangle in pixels. 236 * 237 * @param isPressed <code>true</code> to draw the button border 238 * with a pressed-in appearance; <code>false</code> for 239 * normal (unpressed) appearance. 240 * 241 * @param isDefault <code>true</code> to draw the border with 242 * the appearance it has when hitting the enter key in a 243 * dialog will simulate a click to this button; 244 * <code>false</code> for normal appearance. 245 * 246 * @param shadow the shadow color. 247 * @param darkShadow a darker variant of the shadow color. 248 * @param highlight the highlight color. 249 * @param lightHighlight a brighter variant of the highlight color. 250 */ drawBezel(Graphics g, int x, int y, int width, int height, boolean isPressed, boolean isDefault, Color shadow, Color darkShadow, Color highlight, Color lightHighlight)251 public static void drawBezel(Graphics g, 252 int x, int y, int width, int height, 253 boolean isPressed, boolean isDefault, 254 Color shadow, Color darkShadow, 255 Color highlight, Color lightHighlight) 256 { 257 Color oldColor = g.getColor(); 258 259 /* To understand this, it might be helpful to look at the image 260 * "BasicGraphicsUtils-3.png" that is included with the JavaDoc, 261 * and to compare it with "BasicGraphicsUtils-1.png" which shows 262 * the pixels painted by drawEtchedRect. These image files are 263 * located in the "doc-files" subdirectory. 264 */ 265 try 266 { 267 if ((isPressed == false) && (isDefault == false)) 268 { 269 drawEtchedRect(g, x, y, width, height, 270 lightHighlight, highlight, 271 shadow, darkShadow); 272 } 273 274 if ((isPressed == true) && (isDefault == false)) 275 { 276 g.setColor(shadow); 277 g.drawRect(x + 1, y + 1, width - 2, height - 2); 278 } 279 280 if ((isPressed == false) && (isDefault == true)) 281 { 282 g.setColor(darkShadow); 283 g.drawRect(x, y, width - 1, height - 1); 284 drawEtchedRect(g, x + 1, y + 1, width - 2, height - 2, 285 lightHighlight, highlight, 286 shadow, darkShadow); 287 } 288 289 if ((isPressed == true) && (isDefault == true)) 290 { 291 g.setColor(darkShadow); 292 g.drawRect(x, y, width - 1, height - 1); 293 g.setColor(shadow); 294 g.drawRect(x + 1, y + 1, width - 3, height - 3); 295 } 296 } 297 finally 298 { 299 g.setColor(oldColor); 300 } 301 } 302 303 304 /** 305 * Draws a rectangle that appears lowered into the surface, given 306 * four colors that are used for drawing. 307 * 308 * <p><img src="doc-files/BasicGraphicsUtils-4.png" width="360" 309 * height="200" alt="[An illustration that shows which pixels 310 * get painted in what color]" /> 311 * 312 * <p><strong>Compatibility with the Sun reference 313 * implementation:</strong> The Sun reference implementation seems 314 * to ignore the <code>x</code> and <code>y</code> arguments, at 315 * least in JDK 1.3.1 and 1.4.1_01. The method always draws the 316 * rectangular area at location (0, 0). A bug report has been filed 317 * with Sun; its “bug ID” is 4880003. The GNU Classpath 318 * implementation behaves correctly, thus not replicating this bug. 319 * 320 * @param g the graphics into which the rectangle is drawn. 321 * @param x the x coordinate of the rectangle. 322 * @param y the y coordinate of the rectangle. 323 * @param width the width of the rectangle in pixels. 324 * @param height the height of the rectangle in pixels. 325 * 326 * @param shadow the color that will be used for painting 327 * the inner side of the top and left edges. 328 * 329 * @param darkShadow the color that will be used for painting 330 * the outer side of the top and left edges. 331 * 332 * @param highlight the color that will be used for painting 333 * the inner side of the bottom and right edges. 334 * 335 * @param lightHighlight the color that will be used for painting 336 * the outer side of the bottom and right edges. 337 */ drawLoweredBezel(Graphics g, int x, int y, int width, int height, Color shadow, Color darkShadow, Color highlight, Color lightHighlight)338 public static void drawLoweredBezel(Graphics g, 339 int x, int y, int width, int height, 340 Color shadow, Color darkShadow, 341 Color highlight, Color lightHighlight) 342 { 343 /* Like drawEtchedRect, but swapping darkShadow and shadow. 344 * 345 * To understand this, it might be helpful to look at the image 346 * "BasicGraphicsUtils-4.png" that is included with the JavaDoc, 347 * and to compare it with "BasicGraphicsUtils-1.png" which shows 348 * the pixels painted by drawEtchedRect. These image files are 349 * located in the "doc-files" subdirectory. 350 */ 351 drawEtchedRect(g, x, y, width, height, 352 darkShadow, shadow, 353 highlight, lightHighlight); 354 } 355 356 357 /** 358 * Draws a String at the given location, underlining the first 359 * occurence of a specified character. The algorithm for determining 360 * the underlined position is not sensitive to case. If the 361 * character is not part of <code>text</code>, the text will be 362 * drawn without underlining. Drawing is performed in the current 363 * color and font of <code>g</code>. 364 * 365 * <p><img src="doc-files/BasicGraphicsUtils-5.png" width="500" 366 * height="100" alt="[An illustration showing how to use the 367 * method]" /> 368 * 369 * @param g the graphics into which the String is drawn. 370 * 371 * @param text the String to draw. 372 * 373 * @param underlinedChar the character whose first occurence in 374 * <code>text</code> will be underlined. It is not clear 375 * why the API specification declares this argument to be 376 * of type <code>int</code> instead of <code>char</code>. 377 * While this would allow to pass Unicode characters outside 378 * Basic Multilingual Plane 0 (U+0000 .. U+FFFE), at least 379 * the GNU Classpath implementation does not underline 380 * anything if <code>underlinedChar</code> is outside 381 * the range of <code>char</code>. 382 * 383 * @param x the x coordinate of the text, as it would be passed to 384 * {@link java.awt.Graphics#drawString(java.lang.String, 385 * int, int)}. 386 * 387 * @param y the y coordinate of the text, as it would be passed to 388 * {@link java.awt.Graphics#drawString(java.lang.String, 389 * int, int)}. 390 */ drawString(Graphics g, String text, int underlinedChar, int x, int y)391 public static void drawString(Graphics g, String text, 392 int underlinedChar, int x, int y) 393 { 394 int index = -1; 395 396 /* It is intentional that lower case is used. In some languages, 397 * the set of lowercase characters is larger than the set of 398 * uppercase ones. Therefore, it is good practice to use lowercase 399 * for such comparisons (which really means that the author of this 400 * code can vaguely remember having read some Unicode techreport 401 * with this recommendation, but is too lazy to look for the URL). 402 */ 403 if ((underlinedChar >= 0) || (underlinedChar <= 0xffff)) 404 index = text.toLowerCase().indexOf( 405 Character.toLowerCase((char) underlinedChar)); 406 407 drawStringUnderlineCharAt(g, text, index, x, y); 408 } 409 410 411 /** 412 * Draws a String at the given location, underlining the character 413 * at the specified index. Drawing is performed in the current color 414 * and font of <code>g</code>. 415 * 416 * <p><img src="doc-files/BasicGraphicsUtils-5.png" width="500" 417 * height="100" alt="[An illustration showing how to use the 418 * method]" /> 419 * 420 * @param g the graphics into which the String is drawn. 421 * 422 * @param text the String to draw. 423 * 424 * @param underlinedIndex the index of the underlined character in 425 * <code>text</code>. If <code>underlinedIndex</code> falls 426 * outside the range <code>[0, text.length() - 1]</code>, the 427 * text will be drawn without underlining anything. 428 * 429 * @param x the x coordinate of the text, as it would be passed to 430 * {@link java.awt.Graphics#drawString(java.lang.String, 431 * int, int)}. 432 * 433 * @param y the y coordinate of the text, as it would be passed to 434 * {@link java.awt.Graphics#drawString(java.lang.String, 435 * int, int)}. 436 * 437 * @since 1.4 438 */ drawStringUnderlineCharAt(Graphics g, String text, int underlinedIndex, int x, int y)439 public static void drawStringUnderlineCharAt(Graphics g, String text, 440 int underlinedIndex, 441 int x, int y) 442 { 443 Graphics2D g2; 444 Rectangle2D.Double underline; 445 FontRenderContext frc; 446 FontMetrics fmet; 447 LineMetrics lineMetrics; 448 Font font; 449 TextLayout layout; 450 double underlineX1, underlineX2; 451 boolean drawUnderline; 452 int textLength; 453 454 textLength = text.length(); 455 if (textLength == 0) 456 return; 457 458 drawUnderline = (underlinedIndex >= 0) && (underlinedIndex < textLength); 459 460 if (!(g instanceof Graphics2D)) 461 { 462 /* Fall-back. This is likely to produce garbage for any text 463 * containing right-to-left (Hebrew or Arabic) characters, even 464 * if the underlined character is left-to-right. 465 */ 466 g.drawString(text, x, y); 467 if (drawUnderline) 468 { 469 fmet = g.getFontMetrics(); 470 g.fillRect( 471 /* x */ x + fmet.stringWidth(text.substring(0, underlinedIndex)), 472 /* y */ y + fmet.getDescent() - 1, 473 /* width */ fmet.charWidth(text.charAt(underlinedIndex)), 474 /* height */ 1); 475 } 476 477 return; 478 } 479 480 g2 = (Graphics2D) g; 481 font = g2.getFont(); 482 frc = g2.getFontRenderContext(); 483 lineMetrics = font.getLineMetrics(text, frc); 484 layout = new TextLayout(text, font, frc); 485 486 /* Draw the text. */ 487 layout.draw(g2, x, y); 488 if (!drawUnderline) 489 return; 490 491 underlineX1 = x + layout.getLogicalHighlightShape( 492 underlinedIndex, underlinedIndex).getBounds2D().getX(); 493 underlineX2 = x + layout.getLogicalHighlightShape( 494 underlinedIndex + 1, underlinedIndex + 1).getBounds2D().getX(); 495 496 underline = new Rectangle2D.Double(); 497 if (underlineX1 < underlineX2) 498 { 499 underline.x = underlineX1; 500 underline.width = underlineX2 - underlineX1; 501 } 502 else 503 { 504 underline.x = underlineX2; 505 underline.width = underlineX1 - underlineX2; 506 } 507 508 509 underline.height = lineMetrics.getUnderlineThickness(); 510 underline.y = lineMetrics.getUnderlineOffset(); 511 if (underline.y == 0) 512 { 513 /* Some fonts do not specify an underline offset, although they 514 * actually should do so. In that case, the result of calling 515 * lineMetrics.getUnderlineOffset() will be zero. Since it would 516 * look very ugly if the underline was be positioned immediately 517 * below the baseline, we check for this and move the underline 518 * below the descent, as shown in the following ASCII picture: 519 * 520 * ##### ##### # 521 * # # # # 522 * # # # # 523 * # # # # 524 * ##### ###### ---- baseline (0) 525 * # 526 * # 527 * ------------------###----------- lineMetrics.getDescent() 528 */ 529 underline.y = lineMetrics.getDescent(); 530 } 531 532 underline.y += y; 533 g2.fill(underline); 534 } 535 536 537 /** 538 * Draws a rectangle, simulating a dotted stroke by painting only 539 * every second pixel along the one-pixel thick edge. The color of 540 * those pixels is the current color of the Graphics <code>g</code>. 541 * Any other pixels are left unchanged. 542 * 543 * <p><img src="doc-files/BasicGraphicsUtils-7.png" width="360" 544 * height="200" alt="[An illustration that shows which pixels 545 * get painted]" /> 546 * 547 * @param g the graphics into which the rectangle is drawn. 548 * @param x the x coordinate of the rectangle. 549 * @param y the y coordinate of the rectangle. 550 * @param width the width of the rectangle in pixels. 551 * @param height the height of the rectangle in pixels. 552 */ drawDashedRect(Graphics g, int x, int y, int width, int height)553 public static void drawDashedRect(Graphics g, 554 int x, int y, int width, int height) 555 { 556 int right = x + width - 1; 557 int bottom = y + height - 1; 558 559 /* Draw the top and bottom edge of the dotted rectangle. */ 560 for (int i = x; i <= right; i += 2) 561 { 562 g.drawLine(i, y, i, y); 563 g.drawLine(i, bottom, i, bottom); 564 } 565 566 /* Draw the left and right edge of the dotted rectangle. */ 567 for (int i = y; i <= bottom; i += 2) 568 { 569 g.drawLine(x, i, x, i); 570 g.drawLine(right, i, right, i); 571 } 572 } 573 574 575 /** 576 * Determines the preferred width and height of an AbstractButton, 577 * given the gap between the button’s text and icon. 578 * 579 * @param b the button whose preferred size is determined. 580 * 581 * @param textIconGap the gap between the button’s text and 582 * icon. 583 * 584 * @return a <code>Dimension</code> object whose <code>width</code> 585 * and <code>height</code> fields indicate the preferred 586 * extent in pixels. 587 * 588 * @see javax.swing.SwingUtilities#layoutCompoundLabel 589 */ getPreferredButtonSize(AbstractButton b, int textIconGap)590 public static Dimension getPreferredButtonSize(AbstractButton b, 591 int textIconGap) 592 { 593 Rectangle contentRect; 594 Rectangle viewRect; 595 Rectangle iconRect = new Rectangle(); 596 Rectangle textRect = new Rectangle(); 597 Insets insets = b.getInsets(); 598 599 /* For determining the ideal size, do not assume a size restriction. */ 600 viewRect = new Rectangle(0, 0, 601 /* width */ Integer.MAX_VALUE, 602 /* height */ Integer.MAX_VALUE); 603 604 /* java.awt.Toolkit.getFontMetrics is deprecated. However, it 605 * seems not obvious how to get to the correct FontMetrics object 606 * otherwise. The real problem probably is that the method 607 * javax.swing.SwingUtilities.layoutCompundLabel should take a 608 * LineMetrics, not a FontMetrics argument. But fixing this that 609 * would change the public API. 610 */ 611 SwingUtilities.layoutCompoundLabel( 612 b, // for the component orientation 613 b.getToolkit().getFontMetrics(b.getFont()), // see comment above 614 b.getText(), 615 b.getIcon(), 616 b.getVerticalAlignment(), 617 b.getHorizontalAlignment(), 618 b.getVerticalTextPosition(), 619 b.getHorizontalTextPosition(), 620 viewRect, iconRect, textRect, 621 textIconGap); 622 623 624 /* +------------------------+ +------------------------+ 625 * | | | | 626 * | ICON | | CONTENTCONTENTCONTENT | 627 * | TEXTTEXTTEXT | --> | CONTENTCONTENTCONTENT | 628 * | TEXTTEXTTEXT | | CONTENTCONTENTCONTENT | 629 * +------------------------+ +------------------------+ 630 */ 631 contentRect = textRect.union(iconRect); 632 633 return new Dimension(insets.left + contentRect.width + insets.right, 634 insets.top + contentRect.height + insets.bottom); 635 } 636 } 637