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 &#x201c;bug ID&#x201d; 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&#x2019;s text and icon.
578    *
579    * @param b the button whose preferred size is determined.
580    *
581    * @param textIconGap the gap between the button&#x2019;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