KickJava   Java API By Example, From Geeks To Geeks.

Java > Open Source Codes > org > netbeans > modules > editor > completion > PatchedHtmlRenderer


1 /*
2  * The contents of this file are subject to the terms of the Common Development
3  * and Distribution License (the License). You may not use this file except in
4  * compliance with the License.
5  *
6  * You can obtain a copy of the License at http://www.netbeans.org/cddl.html
7  * or http://www.netbeans.org/cddl.txt.
8  *
9  * When distributing Covered Code, include this CDDL Header Notice in each file
10  * and include the License file at http://www.netbeans.org/cddl.txt.
11  * If applicable, add the following below the CDDL Header, with the fields
12  * enclosed by brackets [] replaced by your own identifying information:
13  * "Portions Copyrighted [year] [name of copyright owner]"
14  *
15  * The Original Software is NetBeans. The Initial Developer of the Original
16  * Software is Sun Microsystems, Inc. Portions Copyright 1997-2006 Sun
17  * Microsystems, Inc. All Rights Reserved.
18  */

19
20 package org.netbeans.modules.editor.completion;
21
22 import java.awt.Color JavaDoc;
23 import java.awt.Font JavaDoc;
24 import java.awt.FontMetrics JavaDoc;
25 import java.awt.Graphics JavaDoc;
26 import java.awt.Rectangle JavaDoc;
27 import java.awt.Shape JavaDoc;
28 import java.awt.font.LineMetrics JavaDoc;
29 import java.awt.geom.Area JavaDoc;
30 import java.awt.geom.Rectangle2D JavaDoc;
31 import java.util.Arrays JavaDoc;
32 import java.util.HashSet JavaDoc;
33 import java.util.Set JavaDoc;
34 import java.util.Stack JavaDoc;
35 import java.util.StringTokenizer JavaDoc;
36 import javax.swing.SwingUtilities JavaDoc;
37 import javax.swing.UIManager JavaDoc;
38 import org.openide.ErrorManager;
39 import org.openide.util.Utilities;
40
41 /**
42  * Temporary patched {@link org.openide.awt.HtmlRenderer} to allow disabling
43  * of the color change in case when the particular item is selected.
44  */

45 public final class PatchedHtmlRenderer {
46
47     /** Stack object used during HTML rendering to hold previous colors in
48      * the case of nested color entries. */

49     private static Stack JavaDoc<Color JavaDoc> colorStack = new Stack JavaDoc<Color JavaDoc>();
50
51     /**
52      * Constant used by {@link #renderString renderString}, {@link #renderPlainString renderPlainString},
53      * {@link #renderHTML renderHTML}, and {@link Renderer#setRenderStyle}
54      * if painting should simply be cut off at the boundary of the cooordinates passed.
55      */

56     public static final int STYLE_CLIP = 0;
57
58     /**
59      * Constant used by {@link #renderString renderString}, {@link #renderPlainString renderPlainString},
60      * {@link #renderHTML renderHTML}, and {@link Renderer#setRenderStyle} if
61      * painting should produce an ellipsis (...)
62      * if the text would overlap the boundary of the coordinates passed.
63      */

64     public static final int STYLE_TRUNCATE = 1;
65
66     /**
67      * Constant used by {@link #renderString renderString}, {@link #renderPlainString renderPlainString},
68      * {@link #renderHTML renderHTML}, and {@link Renderer#setRenderStyle}
69      * if painting should word wrap the text. In
70      * this case, the return value of any of the above methods will be the
71      * height, rather than width painted.
72      */

73     private static final int STYLE_WORDWRAP = 2;
74
75     /** System property to cause exceptions to be thrown when unparsable
76      * html is encountered */

77     private static final boolean STRICT_HTML = Boolean.getBoolean("netbeans.lwhtml.strict"); //NOI18N
78

79     /** Cache for strings which have produced errors, so we don't post an
80      * error message more than once */

81     private static Set JavaDoc<String JavaDoc> badStrings = null;
82
83     /** Definitions for a limited subset of SGML character entities */
84     private static final Object JavaDoc[] entities = new Object JavaDoc[] {
85             new char[] { 'g', 't' }, new char[] { 'l', 't' }, //NOI18N
86
new char[] { 'q', 'u', 'o', 't' }, new char[] { 'a', 'm', 'p' }, //NOI18N
87
new char[] { 'l', 's', 'q', 'u', 'o' }, //NOI18N
88
new char[] { 'r', 's', 'q', 'u', 'o' }, //NOI18N
89
new char[] { 'l', 'd', 'q', 'u', 'o' }, //NOI18N
90
new char[] { 'r', 'd', 'q', 'u', 'o' }, //NOI18N
91
new char[] { 'n', 'd', 'a', 's', 'h' }, //NOI18N
92
new char[] { 'm', 'd', 'a', 's', 'h' }, //NOI18N
93
new char[] { 'n', 'e' }, //NOI18N
94
new char[] { 'l', 'e' }, //NOI18N
95
new char[] { 'g', 'e' }, //NOI18N
96
new char[] { 'c', 'o', 'p', 'y' }, //NOI18N
97
new char[] { 'r', 'e', 'g' }, //NOI18N
98
new char[] { 't', 'r', 'a', 'd', 'e' }, //NOI18N
99
new char[] { 'n', 'b', 's', 'p' //NOI18N
100
}
101         }; //NOI18N
102

103     /** Mappings for the array of SGML character entities to characters */
104     private static final char[] entitySubstitutions = new char[] {
105             '>', '<', '"', '&', 8216, 8217, 8220, 8221, 8211, 8212, 8800, 8804, 8805, //NOI18N
106
169, 174, 8482, ' '
107         };
108     private PatchedHtmlRenderer() {
109         //do nothing
110
}
111
112     /**
113      * Render a string to a graphics instance, using the same API as {@link #renderHTML renderHTML}.
114      * Can render a string using JLabel-style ellipsis (...) in the case that
115      * it will not fit in the passed rectangle, if the style parameter is
116      * {@link #STYLE_CLIP}. Returns the width in pixels successfully painted.
117      * <strong>This method is not thread-safe and should not be called off
118      * the AWT thread.</strong>
119      *
120      * @see #renderHTML
121      */

122     public static double renderPlainString(
123         String JavaDoc s, Graphics JavaDoc g, int x, int y, int w, int h, Font JavaDoc f, Color JavaDoc defaultColor, int style, boolean paint
124     ) {
125         //per Jarda's request, keep the word wrapping code but don't expose it.
126
if ((style < 0) || (style > 1)) {
127             throw new IllegalArgumentException JavaDoc("Unknown rendering mode: " + style); //NOI18N
128
}
129
130         return _renderPlainString(s, g, x, y, w, h, f, defaultColor, style, paint);
131     }
132
133     private static double _renderPlainString(
134         String JavaDoc s, Graphics JavaDoc g, int x, int y, int w, int h, Font JavaDoc f, Color JavaDoc foreground, int style, boolean paint
135     ) {
136         if (f == null) {
137             f = UIManager.getFont("controlFont"); //NOI18N
138

139             if (f == null) {
140                 int fs = 11;
141                 Object JavaDoc cfs = UIManager.get("customFontSize"); //NOI18N
142

143                 if (cfs instanceof Integer JavaDoc) {
144                     fs = ((Integer JavaDoc) cfs).intValue();
145                 }
146
147                 f = new Font JavaDoc("Dialog", Font.PLAIN, fs); //NOI18N
148
}
149         }
150
151         FontMetrics JavaDoc fm = g.getFontMetrics(f);
152         Rectangle2D JavaDoc r = fm.getStringBounds(s, g);
153
154         if (paint) {
155             g.setColor(foreground);
156             g.setFont(f);
157
158             if ((r.getWidth() <= w) || (style == STYLE_CLIP)) {
159                 g.drawString(s, x, y);
160             } else {
161                 char[] chars = s.toCharArray();
162
163                 if (chars.length == 0) {
164                     return 0;
165                 }
166
167                 double chWidth = r.getWidth() / chars.length;
168                 int estCharsOver = new Double JavaDoc((r.getWidth() - w) / chWidth).intValue();
169
170                 if (style == STYLE_TRUNCATE) {
171                     int length = chars.length - estCharsOver;
172
173                     if (length <= 0) {
174                         return 0;
175                     }
176
177                     if (paint) {
178                         if (length > 3) {
179                             Arrays.fill(chars, length - 3, length, '.'); //NOI18N
180
g.drawChars(chars, 0, length, x, y);
181                         } else {
182                             Shape JavaDoc shape = g.getClip();
183
184                             if (s != null) {
185                                 Area JavaDoc area = new Area JavaDoc(shape);
186                                 area.intersect(new Area JavaDoc(new Rectangle JavaDoc(x, y, w, h)));
187                                 g.setClip(area);
188                             } else {
189                                 g.setClip(new Rectangle JavaDoc(x, y, w, h));
190                             }
191
192                             g.drawString("...", x, y); // NOI18N
193
g.setClip(shape);
194                         }
195                     }
196                 } else {
197                     //TODO implement plaintext word wrap if we want to support it at some point
198
}
199             }
200         }
201
202         return r.getWidth();
203     }
204
205     /**
206      * Render a string to a graphics context, using HTML markup if the string
207      * begins with <code>&lt;html&gt;</code>. Delegates to {@link #renderPlainString renderPlainString}
208      * or {@link #renderHTML renderHTML} as appropriate. See the class documentation for
209      * for details of the subset of HTML that is
210      * supported.
211      * @param s The string to render
212      * @param g A graphics object into which the string should be drawn, or which should be
213      * used for calculating the appropriate size
214      * @param x The x coordinate to paint at.
215      * @param y The y position at which to paint. Note that this method does not calculate font
216      * height/descent - this value should be the baseline for the line of text, not
217      * the upper corner of the rectangle to paint in.
218      * @param w The maximum width within which to paint.
219      * @param h The maximum height within which to paint.
220      * @param f The base font to be used for painting or calculating string width/height.
221      * @param defaultColor The base color to use if no font color is specified as html tags
222      * @param style The wrapping style to use, either {@link #STYLE_CLIP},
223      * or {@link #STYLE_TRUNCATE}
224      * @param paint True if actual painting should occur. If false, this method will not actually
225      * paint anything, only return a value representing the width/height needed to
226      * paint the passed string.
227      * @return The width in pixels required
228      * to paint the complete string, or the passed parameter <code>w</code> if it is
229      * smaller than the required width.
230      */

231     public static double renderString(
232         String JavaDoc s, Graphics JavaDoc g, int x, int y, int w, int h, Font JavaDoc f, Color JavaDoc defaultColor, int style, boolean paint
233     ) {
234         switch (style) {
235         case STYLE_CLIP:
236         case STYLE_TRUNCATE:
237             break;
238
239         default:
240             throw new IllegalArgumentException JavaDoc("Unknown rendering mode: " + style); //NOI18N
241
}
242
243         // System.err.println ("rps: " + y + " " + s);
244
if (s.startsWith("<html") || s.startsWith("<HTML")) { //NOI18N
245

246             return _renderHTML(s, 6, g, x, y, w, h, f, defaultColor, style, paint, null, false);
247         } else {
248             return renderPlainString(s, g, x, y, w, h, f, defaultColor, style, paint);
249         }
250     }
251
252     /**
253      * Render a string as HTML using a fast, lightweight renderer supporting a limited
254      * subset of HTML. See class Javadoc for details.
255      *
256      * <P>
257      * This method can also be used in non-painting mode to establish the space
258      * necessary to paint a string. This is accomplished by passing the value of the
259      * <code>paint</code> argument as false. The return value will be the required
260      * width in pixels
261      * to display the text. Note that in order to retrieve an
262      * accurate value, the argument for available width should be passed
263      * as {@link Integer#MAX_VALUE} or an appropriate maximum size - otherwise
264      * the return value will either be the passed maximum width or the required
265      * width, whichever is smaller. Also, the clip shape for the passed graphics
266      * object should be null or a value larger than the maximum possible render size.
267      * <P>
268      * This method will log a warning if it encounters HTML markup it cannot
269      * render. To aid diagnostics, if NetBeans is run with the argument
270      * <code>-J-Dnetbeans.lwhtml.strict=true</code> an exception will be thrown
271      * when an attempt is made to render unsupported HTML.
272      * @param s The string to render
273      * @param g A graphics object into which the string should be drawn, or which should be
274      * used for calculating the appropriate size
275      * @param x The x coordinate to paint at.
276      * @param y The y position at which to paint. Note that this method does not calculate font
277      * height/descent - this value should be the baseline for the line of text, not
278      * the upper corner of the rectangle to paint in.
279      * @param w The maximum width within which to paint.
280      * @param h The maximum height within which to paint.
281      * @param f The base font to be used for painting or calculating string width/height.
282      * @param defaultColor The base color to use if no font color is specified as html tags
283      * @param style The wrapping style to use, either {@link #STYLE_CLIP},
284      * or {@link #STYLE_TRUNCATE}
285      * @param paint True if actual painting should occur. If false, this method will not actually
286      * paint anything, only return a value representing the width/height needed to
287      * paint the passed string.
288      * @return The width in pixels required
289      * to paint the complete string, or the passed parameter <code>w</code> if it is
290      * smaller than the required width.
291      */

292     public static double renderHTML(
293         String JavaDoc s, Graphics JavaDoc g, int x, int y, int w, int h, Font JavaDoc f, Color JavaDoc defaultColor, int style, boolean paint, boolean disableColorChange
294     ) {
295         //per Jarda's request, keep the word wrapping code but don't expose it.
296
if ((style < 0) || (style > 1)) {
297             throw new IllegalArgumentException JavaDoc("Unknown rendering mode: " + style); //NOI18N
298
}
299
300         return _renderHTML(s, 0, g, x, y, w, h, f, defaultColor, style, paint, null, disableColorChange);
301     }
302
303     /** Implementation of HTML rendering */
304     static double _renderHTML(
305         String JavaDoc s, int pos, Graphics JavaDoc g, int x, int y, int w, int h, Font JavaDoc f, Color JavaDoc defaultColor, int style, boolean paint,
306         Color JavaDoc background, boolean disableColorChange
307     ) {
308         // System.err.println ("rhs: " + y + " " + s);
309
if (f == null) {
310             f = UIManager.getFont("controlFont"); //NOI18N
311

312             if (f == null) {
313                 int fs = 11;
314                 Object JavaDoc cfs = UIManager.get("customFontSize"); //NOI18N
315

316                 if (cfs instanceof Integer JavaDoc) {
317                     fs = ((Integer JavaDoc) cfs).intValue();
318                 }
319
320                 f = new Font JavaDoc("Dialog", Font.PLAIN, fs); //NOI18N
321
}
322         }
323
324         //Thread safety - avoid allocating memory for the common case
325
Stack JavaDoc<Color JavaDoc> colorStack = SwingUtilities.isEventDispatchThread() ? PatchedHtmlRenderer.colorStack : new Stack JavaDoc<Color JavaDoc>();
326
327         g.setColor(defaultColor);
328         g.setFont(f);
329
330         char[] chars = s.toCharArray();
331         int origX = x;
332         boolean done = false; //flag if rendering completed, either by finishing the string or running out of space
333
boolean inTag = false; //flag if the current position is inside a tag, and the tag should be processed rather than rendering
334
boolean inClosingTag = false; //flag if the current position is inside a closing tag
335
boolean strikethrough = false; //flag if a strikethrough line should be painted
336
boolean underline = false; //flag if an underline should be painted
337
boolean bold = false; //flag if text is currently bold
338
boolean italic = false; //flag if text is currently italic
339
boolean truncated = false; //flag if the last possible character has been painted, and the next loop should paint "..." and return
340
double widthPainted = 0; //the total width painted, for calculating needed space
341
double heightPainted = 0; //the total height painted, for calculating needed space
342
boolean lastWasWhitespace = false; //flag to skip additional whitespace if one whitespace char already painted
343
double lastHeight = 0; //the last line height, for calculating total required height
344

345         double dotsWidth = 0;
346
347         //Calculate the width of a . character if we may need to truncate
348
if (style == STYLE_TRUNCATE) {
349             dotsWidth = g.getFontMetrics().stringWidth("..."); //NOI18N
350
}
351
352         /* How this all works, for anyone maintaining this code (hopefully it will
353           never need it):
354           1. The string is converted to a char array
355           2. Loop over the characters. Variable pos is the current point.
356             2a. See if we're in a tag by or'ing inTag with currChar == '<'
357               If WE ARE IN A TAG:
358                2a1: is it an opening tag?
359                  If YES:
360                    - Identify the tag, Configure the Graphics object with
361                      the appropriate font, color, etc. Set pos = the first
362                      character after the tag
363                  If NO (it's a closing tag)
364                    - Identify the tag. Reconfigure the Graphics object
365                      with the state it should be in outside the tag
366                      (reset the font if italic, pop a color off the stack, etc.)
367             2b. If WE ARE NOT IN A TAG
368                - Locate the next < or & character or the end of the string
369                - Paint the characters using the Graphics object
370                - Check underline and strikethrough tags, and paint line if
371                  needed
372             See if we're out of space, and do the right thing for the style
373             (paint ..., give up or skip to the next line)
374          */

375         //Clear any junk left behind from a previous rendering loop
376
colorStack.clear();
377
378         //Enter the painting loop
379
while (!done) {
380             if (pos == s.length()) {
381                 return widthPainted;
382             }
383
384             //see if we're in a tag
385
try {
386                 inTag |= (chars[pos] == '<');
387             } catch (ArrayIndexOutOfBoundsException JavaDoc e) {
388                 //Should there be any problem, give a meaningful enough
389
//message to reproduce the problem
390
ArrayIndexOutOfBoundsException JavaDoc aib = new ArrayIndexOutOfBoundsException JavaDoc(
391                         "HTML rendering failed at position " + pos + " in String \"" //NOI18N
392
+s + "\". Please report this at http://www.netbeans.org"
393                     ); //NOI18N
394

395                 if (STRICT_HTML) {
396                     throw aib;
397                 } else {
398                     ErrorManager.getDefault().notify(ErrorManager.WARNING, aib);
399
400                     return renderPlainString(s, g, x, y, w, h, f, defaultColor, style, paint);
401                 }
402             }
403
404             inClosingTag = inTag && ((pos + 1) < chars.length) && (chars[pos + 1] == '/'); //NOI18N
405

406             if (truncated) {
407                 //Then we've almost run out of space, time to print ... and quit
408
g.setColor(defaultColor);
409                 g.setFont(f);
410
411                 if (paint) {
412                     g.drawString("...", x, y); //NOI18N
413
}
414
415                 done = true;
416             } else if (inTag) {
417                 //If we're in a tag, don't paint, process it
418
pos++;
419
420                 int tagEnd = pos;
421
422                 //#54237 - if done and end of string -> wrong html
423
done = tagEnd >= (chars.length - 1);
424
425                 while (!done && (chars[tagEnd] != '>')) {
426                     done = tagEnd == (chars.length - 1);
427                     tagEnd++;
428                 }
429
430                 if (done) {
431                     throw new IllegalArgumentException JavaDoc("HTML rendering failed on string \"" + s + "\""); // NOI18N
432
}
433
434                 if (inClosingTag) {
435                     //Handle closing tags by resetting the Graphics object (font, etc.)
436
pos++;
437
438                     switch (chars[pos]) {
439                     case 'P': //NOI18N
440
case 'p': //NOI18N
441
case 'H': //NOI18N
442
case 'h':
443                         break; //ignore html opening/closing tags
444

445                     case 'B': //NOI18N
446
case 'b': //NOI18N
447

448                         if ((chars[pos + 1] == 'r') || (chars[pos + 1] == 'R')) {
449                             break;
450                         }
451
452                         if (!bold) {
453                             throwBadHTML("Closing bold tag w/o " + //NOI18N
454
"opening bold tag", pos, chars // NOI18N
455
); //NOI18N
456
}
457
458                         if (italic) {
459                             g.setFont(deriveFont(f, Font.ITALIC));
460                         } else {
461                             g.setFont(deriveFont(f, Font.PLAIN));
462                         }
463
464                         bold = false;
465
466                         break;
467
468                     case 'E': //NOI18N
469
case 'e': //em tag
470
case 'I': //NOI18N
471
case 'i': //NOI18N
472

473                         if (bold) {
474                             g.setFont(deriveFont(f, Font.BOLD));
475                         } else {
476                             g.setFont(deriveFont(f, Font.PLAIN));
477                         }
478
479                         if (!italic) {
480                             throwBadHTML("Closing italics tag w/o" //NOI18N
481
+"opening italics tag", pos, chars // NOI18N
482
); //NOI18N
483
}
484
485                         italic = false;
486
487                         break;
488
489                     case 'S': //NOI18N
490
case 's': //NOI18N
491

492                         switch (chars[pos + 1]) {
493                         case 'T': //NOI18N
494
case 't':
495
496                             if (italic) { //NOI18N
497
g.setFont(deriveFont(f, Font.ITALIC));
498                             } else {
499                                 g.setFont(deriveFont(f, Font.PLAIN));
500                             }
501
502                             bold = false;
503
504                             break;
505
506                         case '>': //NOI18N
507
strikethrough = false;
508
509                             break;
510                         }
511
512                         break;
513
514                     case 'U': //NOI18N
515
case 'u':
516                         underline = false; //NOI18N
517

518                         break;
519
520                     case 'F': //NOI18N
521
case 'f': //NOI18N
522

523                         if (colorStack.isEmpty()) {
524                             g.setColor(defaultColor);
525                         } else {
526                             g.setColor(colorStack.pop());
527                         }
528
529                         break;
530
531                     default:
532                         throwBadHTML("Malformed or unsupported HTML", //NOI18N
533
pos, chars
534                         );
535                     }
536                 } else {
537                     //Okay, we're in an opening tag. See which one and configure the Graphics object
538
switch (chars[pos]) {
539                     case 'B': //NOI18N
540
case 'b': //NOI18N
541

542                         switch (chars[pos + 1]) {
543                         case 'R': //NOI18N
544
case 'r': //NOI18N
545

546                             if (style == STYLE_WORDWRAP) {
547                                 x = origX;
548
549                                 int lineHeight = g.getFontMetrics().getHeight();
550                                 y += lineHeight;
551                                 heightPainted += lineHeight;
552                                 widthPainted = 0;
553                             }
554
555                             break;
556
557                         case '>':
558                             bold = true;
559
560                             if (italic) {
561                                 g.setFont(deriveFont(f, Font.BOLD | Font.ITALIC));
562                             } else {
563                                 g.setFont(deriveFont(f, Font.BOLD));
564                             }
565
566                             break;
567                         }
568
569                         break;
570
571                     case 'e': //NOI18N //em tag
572
case 'E': //NOI18N
573
case 'I': //NOI18N
574
case 'i': //NOI18N
575
italic = true;
576
577                         if (bold) {
578                             g.setFont(deriveFont(f, Font.ITALIC | Font.BOLD));
579                         } else {
580                             g.setFont(deriveFont(f, Font.ITALIC));
581                         }
582
583                         break;
584
585                     case 'S': //NOI18N
586
case 's': //NOI18N
587

588                         switch (chars[pos + 1]) {
589                         case '>':
590                             strikethrough = true;
591
592                             break;
593
594                         case 'T':
595                         case 't':
596                             bold = true;
597
598                             if (italic) {
599                                 g.setFont(deriveFont(f, Font.BOLD | Font.ITALIC));
600                             } else {
601                                 g.setFont(deriveFont(f, Font.BOLD));
602                             }
603
604                             break;
605                         }
606
607                         break;
608
609                     case 'U': //NOI18N
610
case 'u': //NOI18N
611
underline = true;
612
613                         break;
614
615                     case 'f': //NOI18N
616
case 'F': //NOI18N
617

618                         Color JavaDoc c = findColor(chars, pos, tagEnd);
619                         colorStack.push(g.getColor());
620
621                         if (background != null) {
622                             //c = org.openide.awt.HtmlLabelUI.ensureContrastingColor(c, background);
623
}
624
625                         if (!disableColorChange) {
626                             g.setColor(c);
627                         }
628
629                         break;
630
631                     case 'P': //NOI18N
632
case 'p': //NOI18N
633

634                         if (style == STYLE_WORDWRAP) {
635                             x = origX;
636
637                             int lineHeight = g.getFontMetrics().getHeight();
638                             y += (lineHeight + (lineHeight / 2));
639                             heightPainted = y + lineHeight;
640                             widthPainted = 0;
641                         }
642
643                         break;
644
645                     case 'H':
646                     case 'h': //Just an opening HTML tag
647

648                         if (pos == 1) {
649                             break;
650                         }
651
652                     default:
653                         throwBadHTML("Malformed or unsupported HTML", pos, chars); //NOI18N
654
}
655                 }
656
657                 pos = tagEnd + (done ? 0 : 1);
658                 inTag = false;
659             } else {
660                 //Okay, we're not in a tag, we need to paint
661
if (lastWasWhitespace) {
662                     //Skip multiple whitespace characters
663
while ((pos < (s.length() - 1)) && Character.isWhitespace(chars[pos])) {
664                         pos++;
665                     }
666
667                     //Check strings terminating with multiple whitespace -
668
//otherwise could get an AIOOBE here
669
if (pos == (chars.length - 1)) {
670                         return (style != STYLE_WORDWRAP) ? widthPainted : heightPainted;
671                     }
672                 }
673
674                 //Flag to indicate if an ampersand entity was processed,
675
//so the resulting & doesn't get treated as the beginning of
676
//another entity (and loop endlessly)
677
boolean isAmp = false;
678
679                 //Flag to indicate the next found < character really should
680
//be painted (it came from an entity), it is not the beginning
681
//of a tag
682
boolean nextLtIsEntity = false;
683                 int nextTag = chars.length - 1;
684
685                 if ((chars[pos] == '&')) { //NOI18N
686

687                     boolean inEntity = pos != (chars.length - 1);
688
689                     if (inEntity) {
690                         int newPos = substEntity(chars, pos + 1);
691                         inEntity = newPos != -1;
692
693                         if (inEntity) {
694                             pos = newPos;
695                             isAmp = chars[pos] == '&'; //NOI18N
696

697                             nextLtIsEntity = chars[pos] == '<';
698                         } else {
699                             nextLtIsEntity = false;
700                             isAmp = true;
701                         }
702                     }
703                 } else {
704                     nextLtIsEntity = false;
705                 }
706
707                 for (int i = pos; i < chars.length; i++) {
708                     if (((chars[i] == '<') && (!nextLtIsEntity)) || ((chars[i] == '&') && !isAmp)) { //NOI18N
709
nextTag = i - 1;
710
711                         break;
712                     }
713
714                     //Reset these flags so we don't skip all & or < chars for the rest of the string
715
isAmp = false;
716                     nextLtIsEntity = false;
717                 }
718
719                 FontMetrics JavaDoc fm = g.getFontMetrics();
720
721                 //Get the bounds of the substring we'll paint
722
Rectangle2D JavaDoc r = fm.getStringBounds(chars, pos, nextTag + 1, g);
723
724                 //Store the height, so we can add it if we're in word wrap mode,
725
//to return the height painted
726
lastHeight = r.getHeight();
727
728                 //Work out the length of this tag
729
int length = (nextTag + 1) - pos;
730
731                 //Flag to be set to true if we run out of space
732
boolean goToNextRow = false;
733
734                 //Flag that the current line is longer than the available width,
735
//and should be wrapped without finding a word boundary
736
boolean brutalWrap = false;
737
738                 //Work out the per-character avg width of the string, for estimating
739
//when we'll be out of space and should start the ... in truncate
740
//mode
741
double chWidth = r.getWidth() / length;;
742
743                 if (style == STYLE_TRUNCATE) {
744                     double newWidth = widthPainted + r.getWidth();
745                     if (newWidth > (w - dotsWidth)) {
746                         if (newWidth > w || _renderHTML(s, 0, g.create(), x, y, Integer.MAX_VALUE, h, f, defaultColor, STYLE_CLIP, false, background, disableColorChange) > w) {
747                             double pixelsOff = widthPainted + r.getWidth() - w - dotsWidth;
748                             
749                             double estCharsOver = pixelsOff / chWidth;
750                             
751                             length = new Double JavaDoc((w - dotsWidth - widthPainted) / chWidth).intValue();
752                             
753                             if (length < 0) {
754                                 length = 0;
755                             }
756                             
757                             r = fm.getStringBounds(chars, pos, pos + length, g);
758                             
759                             truncated = true;
760                         }
761                     }
762                 } else if (style == STYLE_WORDWRAP) {
763                     if ((widthPainted + r.getWidth()) > w && chWidth > 3) {
764                         double pixelsOff = (widthPainted + (r.getWidth() + 5)) - w;
765
766                         double estCharsOver = pixelsOff / chWidth;
767
768                         goToNextRow = true;
769                         
770                         int lastChar = new Double JavaDoc(nextTag - estCharsOver).intValue();
771                         
772                         //Unlike Swing's word wrap, which does not wrap on tag boundaries correctly, if we're out of space,
773
//we're out of space
774
brutalWrap = x == 0;
775                         
776                         for (int i = lastChar; i > pos; i--) {
777                             lastChar--;
778                             
779                             if (Character.isWhitespace(chars[i])) {
780                                 length = (lastChar - pos) + 1;
781                                 brutalWrap = false;
782                                 
783                                 break;
784                             }
785                         }
786                         
787                         if ((lastChar <= pos) && (length > estCharsOver) && !brutalWrap) {
788                             x = origX;
789                             y += r.getHeight();
790                             heightPainted += r.getHeight();
791                             
792                             boolean boundsChanged = false;
793                             
794                             while (!done && Character.isWhitespace(chars[pos]) && (pos < nextTag)) {
795                                 pos++;
796                                 boundsChanged = true;
797                                 done = pos == (chars.length - 1);
798                             }
799                             
800                             if (pos == nextTag) {
801                                 lastWasWhitespace = true;
802                             }
803                             
804                             if (boundsChanged) {
805                                 //recalculate the width we will add
806
r = fm.getStringBounds(chars, pos, nextTag + 1, g);
807                             }
808                             
809                             goToNextRow = false;
810                             widthPainted = 0;
811                             
812                             if (chars[pos - 1 + length] == '<') {
813                                 length--;
814                             }
815                         } else if (brutalWrap) {
816                             //wrap without checking word boundaries
817
length = (new Double JavaDoc((w - widthPainted) / chWidth)).intValue();
818                             
819                             if ((pos + length) > nextTag) {
820                                 length = (nextTag - pos);
821                             }
822                             
823                             goToNextRow = true;
824                         }
825                     }
826                 }
827
828                 if (!done) {
829                     if (paint) {
830                         g.drawChars(chars, pos, length, x, y);
831                     }
832
833                     if (strikethrough || underline) {
834                         LineMetrics JavaDoc lm = fm.getLineMetrics(chars, pos, length - 1, g);
835                         int lineWidth = new Double JavaDoc(x + r.getWidth()).intValue();
836
837                         if (paint) {
838                             if (strikethrough) {
839                                 int stPos = Math.round(lm.getStrikethroughOffset()) +
840                                     g.getFont().getBaselineFor(chars[pos]) + 1;
841
842                                 //PENDING - worth supporting with g.setStroke()? A one pixel line is most likely
843
//good enough
844
//int stThick = Math.round (lm.getStrikethroughThickness());
845
g.drawLine(x, y + stPos, lineWidth, y + stPos);
846                             }
847
848                             if (underline) {
849                                 int stPos = Math.round(lm.getUnderlineOffset()) +
850                                     g.getFont().getBaselineFor(chars[pos]) + 1;
851
852                                 //PENDING - worth supporting with g.setStroke()? A one pixel line is most likely
853
//good enough
854
//int stThick = new Float (lm.getUnderlineThickness()).intValue();
855
g.drawLine(x, y + stPos, lineWidth, y + stPos);
856                             }
857                         }
858                     }
859
860                     if (goToNextRow) {
861                         //if we're in word wrap mode and need to go to the next
862
//line, reconfigure the x and y coordinates
863
x = origX;
864                         y += r.getHeight();
865                         heightPainted += r.getHeight();
866                         widthPainted = 0;
867                         pos += (length);
868
869                         //skip any leading whitespace
870
while ((pos < chars.length) && (Character.isWhitespace(chars[pos])) && (chars[pos] != '<')) {
871                             pos++;
872                         }
873
874                         lastWasWhitespace = true;
875                         done |= (pos >= chars.length);
876                     } else {
877                         x += r.getWidth();
878                         widthPainted += r.getWidth();
879                         lastWasWhitespace = Character.isWhitespace(chars[nextTag]);
880                         pos = nextTag + 1;
881                     }
882
883                     done |= (nextTag == chars.length);
884                 }
885             }
886         }
887
888         if (style != STYLE_WORDWRAP) {
889             return widthPainted;
890         } else {
891             return heightPainted + lastHeight;
892         }
893     }
894
895     /** Parse a font color tag and return an appopriate java.awt.Color instance */
896     private static Color JavaDoc findColor(final char[] ch, final int pos, final int tagEnd) {
897         int colorPos = pos;
898         boolean useUIManager = false;
899
900         for (int i = pos; i < tagEnd; i++) {
901             if (ch[i] == 'c') {
902                 colorPos = i + 6;
903
904                 if ((ch[colorPos] == '\'') || (ch[colorPos] == '"')) {
905                     colorPos++;
906                 }
907
908                 //skip the leading # character
909
if (ch[colorPos] == '#') {
910                     colorPos++;
911                 } else if (ch[colorPos] == '!') {
912                     useUIManager = true;
913                     colorPos++;
914                 }
915
916                 break;
917             }
918         }
919
920         if (colorPos == pos) {
921             String JavaDoc out = "Could not find color identifier in font declaration"; //NOI18N
922
throwBadHTML(out, pos, ch);
923         }
924
925         //Okay, we're now on the first character of the hex color definition
926
String JavaDoc s;
927
928         if (useUIManager) {
929             int end = ch.length - 1;
930
931             for (int i = colorPos; i < ch.length; i++) {
932                 if ((ch[i] == '"') || (ch[i] == '\'')) { //NOI18N
933
end = i;
934
935                     break;
936                 }
937             }
938
939             s = new String JavaDoc(ch, colorPos, end - colorPos);
940         } else {
941             s = new String JavaDoc(ch, colorPos, 6);
942         }
943
944         Color JavaDoc result = null;
945
946         if (useUIManager) {
947             result = UIManager.getColor(s);
948
949             //Not all look and feels will provide standard colors; handle it gracefully
950
if (result == null) {
951                 throwBadHTML("Could not resolve logical font declared in HTML: " + s, //NOI18N
952
pos, ch
953                 );
954                 result = UIManager.getColor("textText"); //NOI18N
955

956                 //Avoid NPE in headless situation?
957
if (result == null) {
958                     result = Color.BLACK;
959                 }
960             }
961         } else {
962             try {
963                 int rgb = Integer.parseInt(s, 16);
964                 result = new Color JavaDoc(rgb);
965             } catch (NumberFormatException JavaDoc nfe) {
966                 throwBadHTML("Illegal hexadecimal color text: " + s + //NOI18N
967
" in HTML string", colorPos, ch // NOI18N
968
); //NOI18N
969
}
970         }
971
972         if (result == null) {
973             throwBadHTML("Unresolvable html color: " + s //NOI18N
974
+" in HTML string \n ", pos, ch // NOI18N
975
); //NOI18N
976
}
977
978         return result;
979     }
980
981     /**
982      * Workaround for Apple bug 3644261 - after using form editor, all boldface
983      * fonts start showing up with incorrect metrics, such that all boldface
984      * fonts in the entire IDE are displayed 12px below where they should be.
985      * Embarrassing and awful.
986      */

987     private static final Font JavaDoc deriveFont(Font JavaDoc f, int style) {
988         // return f.deriveFont(style);
989
// see #49973 for details.
990
Font JavaDoc result = Utilities.isMac() ? new Font JavaDoc(f.getName(), style, f.getSize()) : f.deriveFont(style);
991
992         return result;
993     }
994
995     /** Find an entity at the passed character position in the passed array.
996      * If an entity is found, the trailing ; character will be substituted
997      * with the resulting character, and the position of that character
998      * in the array will be returned as the new position to render from,
999      * causing the renderer to skip the intervening characters */

1000    private static final int substEntity(char[] ch, int pos) {
1001        //There are no 1 character entities, abort
1002
if (pos >= (ch.length - 2)) {
1003            return -1;
1004        }
1005
1006        //if it's numeric, parse out the number
1007
if (ch[pos] == '#') { //NOI18N
1008

1009            return substNumericEntity(ch, pos + 1);
1010        }
1011
1012        //Okay, we've potentially got a named character entity. Try to find it.
1013
boolean match;
1014
1015        for (int i = 0; i < entities.length; i++) {
1016            char[] c = (char[]) entities[i];
1017            match = true;
1018
1019            if (c.length < (ch.length - pos)) {
1020                for (int j = 0; j < c.length; j++) {
1021                    match &= (c[j] == ch[j + pos]);
1022                }
1023            } else {
1024                match = false;
1025            }
1026
1027            if (match) {
1028                //if it's a match, we still need the trailing ;
1029
if (ch[pos + c.length] == ';') { //NOI18N
1030

1031                    //substitute the character referenced by the entity
1032
ch[pos + c.length] = entitySubstitutions[i];
1033
1034                    return pos + c.length;
1035                }
1036            }
1037        }
1038
1039        return -1;
1040    }
1041
1042    /** Finds a character defined as a numeric entity (e.g. &amp;#8222;)
1043     * and replaces the trailing ; with the referenced character, returning
1044     * the position of it so the renderer can continue from there.
1045     */

1046    private static final int substNumericEntity(char[] ch, int pos) {
1047        for (int i = pos; i < ch.length; i++) {
1048            if (ch[i] == ';') {
1049                try {
1050                    ch[i] = (char) Integer.parseInt(new String JavaDoc(ch, pos, i - pos));
1051
1052                    return i;
1053                } catch (NumberFormatException JavaDoc nfe) {
1054                    throwBadHTML("Unparsable numeric entity: " + //NOI18N
1055
new String JavaDoc(ch, pos, i - pos), pos, ch
1056                    ); //NOI18N
1057
}
1058            }
1059        }
1060
1061        return -1;
1062    }
1063
1064    /** Throw an exception for unsupported or bad html, indicating where the problem is
1065     * in the message */

1066    private static void throwBadHTML(String JavaDoc msg, int pos, char[] chars) {
1067        char[] chh = new char[pos];
1068        Arrays.fill(chh, ' '); //NOI18N
1069
chh[pos - 1] = '^'; //NOI18N
1070

1071        String JavaDoc out = msg + "\n " + new String JavaDoc(chars) + "\n " + new String JavaDoc(chh) + "\n Full HTML string:" + // NOI18N
1072
new String JavaDoc(chars); //NOI18N
1073

1074        if (!STRICT_HTML) {
1075            if (ErrorManager.getDefault().isLoggable(ErrorManager.WARNING)) {
1076                if (badStrings == null) {
1077                    badStrings = new HashSet JavaDoc<String JavaDoc>();
1078                }
1079
1080                if (!badStrings.contains(msg)) {
1081                    //ErrorManager bug, issue 38372 - log messages containing
1082
//newlines are truncated - so for now we iterate the
1083
//string we've just constructed
1084
StringTokenizer JavaDoc tk = new StringTokenizer JavaDoc(out, "\n", false); // NOI18N
1085

1086                    while (tk.hasMoreTokens()) {
1087                        ErrorManager.getDefault().log(ErrorManager.WARNING, tk.nextToken());
1088                    }
1089
1090                    badStrings.add(msg.intern());
1091                }
1092            }
1093        } else {
1094            throw new IllegalArgumentException JavaDoc(out);
1095        }
1096    }
1097
1098}
1099
Popular Tags