in ide/editor.completion/src/org/netbeans/modules/editor/completion/PatchedHtmlRenderer.java [335:972]
static double _renderHTML(
String s, int pos, Graphics g, int x, int y, int w, int h, Font f, Color defaultColor, int style, boolean paint,
Color background, boolean disableColorChange
) {
// System.err.println ("rhs: " + y + " " + s);
if (f == null) {
f = UIManager.getFont("controlFont"); //NOI18N
if (f == null) {
int fs = 11;
Object cfs = UIManager.get("customFontSize"); //NOI18N
if (cfs instanceof Integer) {
fs = ((Integer) cfs).intValue();
}
f = new Font("Dialog", Font.PLAIN, fs); //NOI18N
}
}
//Thread safety - avoid allocating memory for the common case
Stack<Color> colorStack = SwingUtilities.isEventDispatchThread() ? PatchedHtmlRenderer.colorStack : new Stack<Color>();
g.setColor(defaultColor);
g.setFont(f);
GraphicsUtils.configureDefaultRenderingHints(g);
char[] chars = s.toCharArray();
int origX = x;
boolean done = false; //flag if rendering completed, either by finishing the string or running out of space
boolean inTag = false; //flag if the current position is inside a tag, and the tag should be processed rather than rendering
boolean inClosingTag = false; //flag if the current position is inside a closing tag
boolean strikethrough = false; //flag if a strikethrough line should be painted
boolean underline = false; //flag if an underline should be painted
boolean bold = false; //flag if text is currently bold
boolean italic = false; //flag if text is currently italic
boolean truncated = false; //flag if the last possible character has been painted, and the next loop should paint "..." and return
double widthPainted = 0; //the total width painted, for calculating needed space
double heightPainted = 0; //the total height painted, for calculating needed space
boolean lastWasWhitespace = false; //flag to skip additional whitespace if one whitespace char already painted
double lastHeight = 0; //the last line height, for calculating total required height
double dotWidth = 0;
boolean dotsPainted = false;
//Calculate the width of a . character if we may need to truncate
if (style == STYLE_TRUNCATE) {
dotWidth = g.getFontMetrics().charWidth('.'); //NOI18N
}
/* How this all works, for anyone maintaining this code (hopefully it will
never need it):
1. The string is converted to a char array
2. Loop over the characters. Variable pos is the current point.
2a. See if we're in a tag by or'ing inTag with currChar == '<'
If WE ARE IN A TAG:
2a1: is it an opening tag?
If YES:
- Identify the tag, Configure the Graphics object with
the appropriate font, color, etc. Set pos = the first
character after the tag
If NO (it's a closing tag)
- Identify the tag. Reconfigure the Graphics object
with the state it should be in outside the tag
(reset the font if italic, pop a color off the stack, etc.)
2b. If WE ARE NOT IN A TAG
- Locate the next < or & character or the end of the string
- Paint the characters using the Graphics object
- Check underline and strikethrough tags, and paint line if
needed
See if we're out of space, and do the right thing for the style
(paint ..., give up or skip to the next line)
*/
//Clear any junk left behind from a previous rendering loop
colorStack.clear();
//Enter the painting loop
while (!done) {
if (pos == s.length()) {
if( truncated && paint && !dotsPainted ) {
g.setColor(defaultColor);
g.setFont(f);
g.drawString("…", x, y); //NOI18N
}
return widthPainted;
}
//see if we're in a tag
try {
inTag |= (chars[pos] == '<');
} catch (ArrayIndexOutOfBoundsException e) {
//Should there be any problem, give a meaningful enough
//message to reproduce the problem
ArrayIndexOutOfBoundsException aib = new ArrayIndexOutOfBoundsException(
"HTML rendering failed at position " + pos + " in String \"" //NOI18N
+s + "\". Please report this at http://www.netbeans.org"
); //NOI18N
if (STRICT_HTML) {
throw aib;
} else {
Logger.getLogger(PatchedHtmlRenderer.class.getName()).log(Level.WARNING, null, aib);
return renderPlainString(s, g, x, y, w, h, f, defaultColor, style, paint);
}
}
inClosingTag = inTag && ((pos + 1) < chars.length) && (chars[pos + 1] == '/'); //NOI18N
if (truncated) {
//Then we've almost run out of space, time to print ... and quit
g.setColor(defaultColor);
g.setFont(f);
if (paint) {
g.drawString("…", x, y); //NOI18N
dotsPainted = true; //make sure we paint the dots only once
}
done = true;
} else if (inTag) {
//If we're in a tag, don't paint, process it
pos++;
int tagEnd = pos;
//#54237 - if done and end of string -> wrong html
done = tagEnd >= (chars.length - 1);
while (!done && (chars[tagEnd] != '>')) {
done = tagEnd == (chars.length - 1);
tagEnd++;
}
if (done) {
throwBadHTML("Matching '>' not found", pos, chars); //NOI18N
break;
}
if (inClosingTag) {
//Handle closing tags by resetting the Graphics object (font, etc.)
pos++;
switch (chars[pos]) {
case 'P': //NOI18N
case 'p': //NOI18N
case 'H': //NOI18N
case 'h':
break; //ignore html opening/closing tags
case 'B': //NOI18N
case 'b': //NOI18N
if ((chars[pos + 1] == 'r') || (chars[pos + 1] == 'R')) {
break;
}
if (!bold) {
throwBadHTML("Closing bold tag w/o " + //NOI18N
"opening bold tag", pos, chars // NOI18N
); //NOI18N
}
if (italic) {
g.setFont(deriveFont(f, Font.ITALIC));
} else {
g.setFont(deriveFont(f, Font.PLAIN));
}
bold = false;
break;
case 'E': //NOI18N
case 'e': //em tag
case 'I': //NOI18N
case 'i': //NOI18N
if (bold) {
g.setFont(deriveFont(f, Font.BOLD));
} else {
g.setFont(deriveFont(f, Font.PLAIN));
}
if (!italic) {
throwBadHTML("Closing italics tag w/o" //NOI18N
+"opening italics tag", pos, chars // NOI18N
); //NOI18N
}
italic = false;
break;
case 'S': //NOI18N
case 's': //NOI18N
switch (chars[pos + 1]) {
case 'T': //NOI18N
case 't':
if (italic) { //NOI18N
g.setFont(deriveFont(f, Font.ITALIC));
} else {
g.setFont(deriveFont(f, Font.PLAIN));
}
bold = false;
break;
case '>': //NOI18N
strikethrough = false;
break;
}
break;
case 'U': //NOI18N
case 'u':
underline = false; //NOI18N
break;
case 'F': //NOI18N
case 'f': //NOI18N
if (colorStack.isEmpty()) {
g.setColor(defaultColor);
} else {
g.setColor(colorStack.pop());
}
break;
default:
throwBadHTML("Malformed or unsupported HTML", //NOI18N
pos, chars
);
}
} else {
//Okay, we're in an opening tag. See which one and configure the Graphics object
switch (chars[pos]) {
case 'B': //NOI18N
case 'b': //NOI18N
switch (chars[pos + 1]) {
case 'R': //NOI18N
case 'r': //NOI18N
if (style == STYLE_WORDWRAP) {
x = origX;
int lineHeight = g.getFontMetrics().getHeight();
y += lineHeight;
heightPainted += lineHeight;
widthPainted = 0;
}
break;
case '>':
bold = true;
if (italic) {
g.setFont(deriveFont(f, Font.BOLD | Font.ITALIC));
} else {
g.setFont(deriveFont(f, Font.BOLD));
}
break;
}
break;
case 'e': //NOI18N //em tag
case 'E': //NOI18N
case 'I': //NOI18N
case 'i': //NOI18N
italic = true;
if (bold) {
g.setFont(deriveFont(f, Font.ITALIC | Font.BOLD));
} else {
g.setFont(deriveFont(f, Font.ITALIC));
}
break;
case 'S': //NOI18N
case 's': //NOI18N
switch (chars[pos + 1]) {
case '>':
strikethrough = true;
break;
case 'T':
case 't':
bold = true;
if (italic) {
g.setFont(deriveFont(f, Font.BOLD | Font.ITALIC));
} else {
g.setFont(deriveFont(f, Font.BOLD));
}
break;
}
break;
case 'U': //NOI18N
case 'u': //NOI18N
underline = true;
break;
case 'f': //NOI18N
case 'F': //NOI18N
Color c = findColor(chars, pos, tagEnd);
colorStack.push(g.getColor());
if (!disableColorChange) {
g.setColor(c);
}
break;
case 'P': //NOI18N
case 'p': //NOI18N
if (style == STYLE_WORDWRAP) {
x = origX;
int lineHeight = g.getFontMetrics().getHeight();
y += (lineHeight + (lineHeight / 2));
heightPainted = y + lineHeight;
widthPainted = 0;
}
break;
case 'H':
case 'h': //Just an opening HTML tag
if (pos == 1) {
break;
} else { // fallthrough warning
throwBadHTML("Malformed or unsupported HTML", pos, chars); //NOI18N
break;
}
default:
throwBadHTML("Malformed or unsupported HTML", pos, chars); //NOI18N
}
}
pos = tagEnd + (done ? 0 : 1);
inTag = false;
} else {
//Okay, we're not in a tag, we need to paint
if (lastWasWhitespace) {
//Skip multiple whitespace characters
while ((pos < (s.length() - 1)) && Character.isWhitespace(chars[pos])) {
pos++;
}
//Check strings terminating with multiple whitespace -
//otherwise could get an AIOOBE here
if (pos == (chars.length - 1)) {
return (style != STYLE_WORDWRAP) ? widthPainted : heightPainted;
}
}
//Flag to indicate if an ampersand entity was processed,
//so the resulting & doesn't get treated as the beginning of
//another entity (and loop endlessly)
boolean isAmp = false;
//Flag to indicate the next found < character really should
//be painted (it came from an entity), it is not the beginning
//of a tag
boolean nextLtIsEntity = false;
int nextTag = chars.length - 1;
if ((chars[pos] == '&')) { //NOI18N
boolean inEntity = pos != (chars.length - 1);
if (inEntity) {
int newPos = substEntity(chars, pos + 1);
inEntity = newPos != -1;
if (inEntity) {
pos = newPos;
isAmp = chars[pos] == '&'; //NOI18N
nextLtIsEntity = chars[pos] == '<';
} else {
nextLtIsEntity = false;
isAmp = true;
}
}
} else {
nextLtIsEntity = false;
}
for (int i = pos; i < chars.length; i++) {
if ((chars[i] == '<' && !nextLtIsEntity) || (chars[i] == '&' && !isAmp && i != chars.length - 1)) {
nextTag = i - 1;
break;
}
//Reset these flags so we don't skip all & or < chars for the rest of the string
isAmp = false;
nextLtIsEntity = false;
}
FontMetrics fm = g.getFontMetrics();
//Get the bounds of the substring we'll paint
Rectangle2D r = fm.getStringBounds(chars, pos, nextTag + 1, g);
if (Utilities.isMac()) {
// #54257 - on macosx + chinese/japanese fonts, the getStringBounds() method returns bad value
r.setRect(r.getX(), r.getY(), (double)fm.stringWidth(new String(chars, pos, nextTag - pos + 1)), r.getHeight());
}
//Store the height, so we can add it if we're in word wrap mode,
//to return the height painted
lastHeight = r.getHeight();
//Work out the length of this tag
int length = (nextTag + 1) - pos;
//Flag to be set to true if we run out of space
boolean goToNextRow = false;
//Flag that the current line is longer than the available width,
//and should be wrapped without finding a word boundary
boolean brutalWrap = false;
//Work out the per-character avg width of the string, for estimating
//when we'll be out of space and should start the ... in truncate
//mode
double chWidth;
if (truncated) {
//if we're truncating, use the width of one dot from an
//ellipsis to get an accurate result for truncation
chWidth = dotWidth;
} else {
//calculate an average character width
chWidth = r.getWidth() / (nextTag+1 - pos);
//can return this sometimes, so handle it
if ((chWidth == Double.POSITIVE_INFINITY) || (chWidth == Double.NEGATIVE_INFINITY)) {
chWidth = fm.getMaxAdvance();
}
}
if (
((style != STYLE_CLIP) &&
((style == STYLE_TRUNCATE) && ((widthPainted + r.getWidth()) > (w /*- (chWidth * 3)*/)))) ||
/** mkleint - commented out the "- (chWidth *3) because it makes no sense to strip the text and add dots when it fits exactly
* into the rendering rectangle.. with this condition we stripped even strings that came close to the limit..
**/
((style == STYLE_WORDWRAP) && ((widthPainted + r.getWidth()) > w))
) {
if (chWidth > 3) {
double pixelsOff = (widthPainted + (r.getWidth() + 5)) - w;
double estCharsOver = pixelsOff / chWidth;
if (style == STYLE_TRUNCATE) {
int charsToPaint = Math.round(Math.round(Math.ceil((w - widthPainted) / chWidth)));
/* System.err.println("estCharsOver = " + estCharsOver);
System.err.println("Chars to paint " + charsToPaint + " chwidth = " + chWidth + " widthPainted " + widthPainted);
System.err.println("Width painted + width of tag: " + (widthPainted + r.getWidth()) + " available: " + w);
*/
int startPeriodsPos = (pos + charsToPaint) - 3;
if (startPeriodsPos >= chars.length) {
startPeriodsPos = chars.length - 4;
}
length = (startPeriodsPos - pos);
if (length < 0) {
length = 0;
}
r = fm.getStringBounds(chars, pos, pos + length, g);
if (Utilities.isMac()) {
// #54257 - on macosx + chinese/japanese fonts, the getStringBounds() method returns bad value
r.setRect(r.getX(), r.getY(), (double)fm.stringWidth(new String(chars, pos, length)), r.getHeight());
}
// System.err.println("Truncated set to true at " + pos + " (" + chars[pos] + ")");
truncated = true;
} else {
//Word wrap mode
goToNextRow = true;
int lastChar = (int)(nextTag - estCharsOver);
//Unlike Swing's word wrap, which does not wrap on tag boundaries correctly, if we're out of space,
//we're out of space
brutalWrap = x == 0;
for (int i = lastChar; i > pos; i--) {
lastChar--;
if (Character.isWhitespace(chars[i])) {
length = (lastChar - pos) + 1;
brutalWrap = false;
break;
}
}
if ((lastChar <= pos) && (length > estCharsOver) && !brutalWrap) {
x = origX;
y += r.getHeight();
heightPainted += r.getHeight();
boolean boundsChanged = false;
while (!done && Character.isWhitespace(chars[pos]) && (pos < nextTag)) {
pos++;
boundsChanged = true;
done = pos == (chars.length - 1);
}
if (pos == nextTag) {
lastWasWhitespace = true;
}
if (boundsChanged) {
//recalculate the width we will add
r = fm.getStringBounds(chars, pos, nextTag + 1, g);
if (Utilities.isMac()) {
// #54257 - on macosx + chinese/japanese fonts, the getStringBounds() method returns bad value
r.setRect(r.getX(), r.getY(), (double)fm.stringWidth(new String(chars, pos, nextTag - pos + 1)), r.getHeight());
}
}
goToNextRow = false;
widthPainted = 0;
if (chars[pos - 1 + length] == '<') {
length--;
}
} else if (brutalWrap) {
//wrap without checking word boundaries
length = (int)((w - widthPainted) / chWidth);
if ((pos + length) > nextTag) {
length = (nextTag - pos);
}
goToNextRow = true;
}
}
}
}
if (!done) {
if (paint) {
g.drawChars(chars, pos, length, x, y);
}
if (strikethrough || underline) {
LineMetrics lm = fm.getLineMetrics(chars, pos, length - 1, g);
int lineWidth = (int)(x + r.getWidth());
if (paint) {
if (strikethrough) {
int stPos = Math.round(lm.getStrikethroughOffset()) +
g.getFont().getBaselineFor(chars[pos]) + 1;
//PENDING - worth supporting with g.setStroke()? A one pixel line is most likely
//good enough
//int stThick = Math.round (lm.getStrikethroughThickness());
g.drawLine(x, y + stPos, lineWidth, y + stPos);
}
if (underline) {
int stPos = Math.round(lm.getUnderlineOffset()) +
g.getFont().getBaselineFor(chars[pos]) + 1;
//PENDING - worth supporting with g.setStroke()? A one pixel line is most likely
//good enough
//int stThick = Float.valueOf(lm.getUnderlineThickness()).intValue();
g.drawLine(x, y + stPos, lineWidth, y + stPos);
}
}
}
if (goToNextRow) {
//if we're in word wrap mode and need to go to the next
//line, reconfigure the x and y coordinates
x = origX;
y += r.getHeight();
heightPainted += r.getHeight();
widthPainted = 0;
pos += (length);
//skip any leading whitespace
while ((pos < chars.length) && (Character.isWhitespace(chars[pos])) && (chars[pos] != '<')) {
pos++;
}
lastWasWhitespace = true;
done |= (pos >= chars.length);
} else {
x += r.getWidth();
widthPainted += r.getWidth();
lastWasWhitespace = Character.isWhitespace(chars[nextTag]);
pos = nextTag + 1;
}
done |= (nextTag == chars.length);
}
}
}
if (style != STYLE_WORDWRAP) {
return widthPainted;
} else {
return heightPainted + lastHeight;
}
}