/*
* Sun Public License Notice
*
* The contents of this file are subject to the Sun Public License
* Version 1.0 (the "License"). You may not use this file except in
* compliance with the License. A copy of the License is available at
* http://www.sun.com/
*
* The Original Code is NetBeans. The Initial Developer of the Original
* Code is Sun Microsystems, Inc. Portions Copyright 1997-2003 Sun
* Microsystems, Inc. All Rights Reserved.
*/
/*
* HtmlRenderer.java
*
* Created on January 2, 2004, 12:49 AM
*/
package org.openide.awt;
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Insets;
import java.awt.KeyboardFocusManager;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.Shape;
import java.awt.font.LineMetrics;
import java.awt.geom.Area;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.lang.ref.Reference;
import java.lang.ref.SoftReference;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.Stack;
import java.util.StringTokenizer;
import javax.swing.BorderFactory;
import javax.swing.Icon;
import javax.swing.JComponent;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JTable;
import javax.swing.JTree;
import javax.swing.ListCellRenderer;
import javax.swing.SwingUtilities;
import javax.swing.UIManager;
import javax.swing.border.Border;
import javax.swing.table.TableCellRenderer;
import javax.swing.tree.TreeCellRenderer;
import org.openide.ErrorManager;
/** A generic cell renderer class which implements
* a lightweight html renderer supporting a minimal subset of HTML used for
* markup purposes only - basic font styles, colors, etc. Also supports
* named logical colors specified by a preceding ! character for specifying
* up colors that should be looked up in the current look and feel's UIDefaults
* (e.g. <font color=&!textText&>
).
*
* If you only need to paint some HTML quickly, use the static methods for
* painting - renderString
, renderPlainString
or
* renderHtml
. These methods differ as follows:
*
renderPlainString
* or renderHtml
as appropriate. Note this method does not tolerate
* whitespace in opening html tags - it expects exactly 6 characters to make up
* the opening tag if present.renderHtml
, but will also honor
* STYLE_TRUNCATE
, so strings can be rendered with trailing
* elipsis if there is not enough spacerenderPlainString
is faster for that. It is useful
* if you want to render a string you know to be compliant
* HTML markup, but which does not have opening and closing HTML tags (though
* they are harmless if present). * The following tags are supported, in upper or lower (but not mixed) case: * *
<B> | *Boldface text | *
<S> | *Strikethrough text | *
<U> | *Underline text | *
<I> | *Italic text | *
<EM> | *Emphasized text (same as italic) | *
<STRONG> | *Strong text (same as bold) | *
<font> | *Font color - font attributes other than color are not supported. Colors
* may be specified as hexidecimal strings, such as #FF0000 or as logical colors
* defined in the current look and feel by specifying a ! character as the first
* character of the color name. Logical colors are colors available from the
* current look and feel's UIManager. For example, <font
* color="!Tree.background"> will set the font color to the
* result of UIManager.getColor("Tree.background") .
* Font size tags are not supported.
* |
*
quot, lt, amp, lsquo, rsquo, ldquo, rdquo, ndash, mdash, ne,
* le, ge, copy, reg, trade.
. It also supports numeric entities
* (e.g. &8822;
).
* Why not use the JDK's HTML support? The JDK's HTML support works * well for stable components, but suffers from performance problems in the * case of cell renderers - each call to set the text (which happens once per * cell, per paint) causes a document tree to be created in memory. For small, * markup only strings, this is overkill. For rendering short strings * (for example, in a tree or table cell renderer) * with limited HTML, this method is approximately 10x faster than standard * Swing HTML rendering. * *
Specifying logical colors
* Hardcoded text colors are undesirable, as they can be incompatible (even
* invisible) on some look and feels or themes, depending on the background
* color.
* The lightweight HTML renderer supports a non-standard syntax for specifying
* font colors via a key for a color in the UI defaults for the current look
* and feel. This is accomplished by prefixing the key name with a !
* character. For example: <font color='!controlShadow'>
.
*
*
Modes of operation
* This method supports two modes of operation:
*
STYLE_CLIP
- as much text as will fit in the pixel width passed
* to the method should be painted, and the text should be cut off at the maximum
* width or clip rectangle maximum X boundary for the graphics object, whichever is
* smaller.STYLE_TRUNCATE
- paint as much text as will fit in the pixel
* width passed to the method, but paint the last three characters as .'s, in the
* same manner as a JLabel truncates its text when the available space is too
* small.
* The paint methods can also be used in non-painting mode to establish the space
* necessary to paint a string. This is accomplished by passing the value of the
* paint
argument as false. The return value will be the required
* width in pixels
* to display the text. Note that in order to retrieve an
* accurate value, the argument for available width should be passed
* as Integer.MAX_VALUE
or an appropriate maximum size - otherwise
* the return value will either be the passed maximum width or the required
* width, whichever is smaller. Also, the clip shape for the passed graphics
* object should be null or a value larger than the maximum possible render size,
* or text size measurement will stop at the clip bounds. HtmlRenderer.getGraphics()
* will always return non-null and non-clipped, and is suitable to pass in such a
* situation.
*
* This class exists primarily for internal use within NetBeans; it is
* non-final in order to support a few use cases in the window system and
* property sheet. It is not a general purpose component. If you need a
* cell renderer for a tree, table, etc. in which you want to display HTML
* without performance problems, use the static method sharedInstance()
,
* or call the static painting methods in your own component. Do not add instances
* of this class to an onscreen container, or access instances from other than the
* AWT event thread.
renderString
, renderPlainString
* renderHTML
, and HtmlRenderer.setRenderStyle
* if painting should simply be cut off at the boundary of the cooordinates passed. */
public static final int STYLE_CLIP=0;
/** Constant used by renderString
, renderPlainString
* renderHTML
and HtmlRenderer.setRenderStyle
if
* painting should produce an ellipsis (...)
* if the text would overlap the boundary of the coordinates passed */
public static final int STYLE_TRUNCATE=1;
//make public if at some point we want to support word-wrap (nd to implement
//for renderPlainString as well)
/** Constant used by renderString
, renderPlainString
* renderHTML
and HtmlRenderer.setRenderStyle
* if painting should word wrap the text. In
* this case, the return value of any of the above methods will be the
* height, rather than width painted. */
private static final int STYLE_WORDWRAP=2;
/** System property to cause exceptions to be thrown when unparsable
* html is encountered */
private static final boolean strictHTML = Boolean.getBoolean(
"netbeans.lwhtml.strict"); //NOI18N
/** System property to automatically turn on antialiasing for html strings */
private static final boolean antialias = Boolean.getBoolean(
"netbeans.cellrenderer.antialiasing"); //NOI18N
/** System property to disable the lightweight html renderer for debugging
* purposes */
private static final boolean swingRendering = Boolean.getBoolean(
"netbeans.cellrenderer.useSwingRendering"); //NOI18N
/** Cache for strings which have produced errors, so we don't post an
* error message more than once */
private static Set badStrings=null;
/** Field to hold the text of the label, used by the overridden set/getText -
* we *really* don't want to invoke any of Swing's logic here. */
private String text = null;
/** Field indicating the content type - True = HTML, False = plaintext,
* null = look for opening html tags */
private Boolean isHtml = null;
/** Field which, if set, indicates that the focused-selected color should
* should be used */
private boolean paintAsFocused = false;
/** Field indicating the cell render should paint using the selection
* background color */
private boolean selected = false;
/** Field holding the selection background aquired from a JTable or
* JList being rendered */
private Color selectionBackground = Color.BLUE;
/** Field holding the selection foreground aquired from a JTable or
* JList being rendered */
private Color selectionForeground = Color.WHITE;
/** Field holding the current border */
private Border border = null;
/** Field indicating we're going to paint centered, IconView style */
private boolean centered = false;
/** The default rendering style */
private int renderStyle = STYLE_CLIP;
/** Static field holding the default unfocused selection background color.
* NetBeans uses a different selection color if the tree/table/list has focus or
* not. */
private static Color unfocusedSelBg = null;
/** Static field holding the default unfocused selection foreground color.
* NetBeans uses a different selection color if the tree/table/list has focus or
* not. */
private static Color unfocusedSelFg = null;
/** Static field holding the default focused selection background color.
* NetBeans uses a different selection color if the tree/table/list has focus or
* not. */
private static Color defSelFg = null;
/** Static field holding the default unfocused selection background color.
* NetBeans uses a different selection color if the tree/table/list has focus or
* not. */
private static Color defSelBg = null;
/** The default focus outline border */
private static Border focusBorder = BorderFactory.createLineBorder (
UIManager.getColor ("List.focusCellHighlight") != null ?
UIManager.getColor ("List.focusCellHighlight") : Color.blue); // NOI18N
//XXX should be different for any look and feels?
/** Static field holding the shared instance */
private static HtmlRenderer sharedInstance;
/**Get the shared instance of HtmlRenderer - for nearly all use cases,
* neither subclassing nor constructing instances should be necessary.
* This method *must* be called from the event dispatch thread.
* Note that it will set the selected, paintAsFocused, text, content
* type, render style and cellHasFocus properties of the shared instance
* to their default values.
* * When do you want to pass in Boolean.TRUE or Boolean.FALSE? * If you want to render HTML which does not have opening HTML tags * as HTML (you know for sure the string you are passing * is legal HTML markup and any > or < characters are properly * escaped), pass Boolean.TRUE. If you want to render HTML text as its * literal markup, pass in Boolean.FALSE, and it will be rendered as a * plain string. If you don't know if you have HTML or not * and any HTML will be legal HTML supported by this renderer and open * with an <html> tag, pass null. *
* Note that a number of methods are overridden purely for performance
* reasons - in particular, this component will do nothing on calls to
* validate()
, and will fire no property changes.
*
* Remember to set the font correctly each time the instance is fetched -
* other code may have changed it, and this can result in magically
* changing fonts.
*
* @param html whether or not the value that will be passed to the next
* call to any of the tree/table/list cell renderers is HTML or not.
* This allows the minor optimization that the text will not be tested
* for opening/closing html tags, and it is not necessary for them to
* be included in the String for it to be rendered as HTML.
*/
public static final HtmlRenderer sharedInstance(boolean html) {
assert SwingUtilities.isEventDispatchThread();
if (sharedInstance == null) {
sharedInstance = new HtmlRenderer();
} else {
sharedInstance.selected = false;
sharedInstance.paintAsFocused = false;
sharedInstance.isHtml = html ? Boolean.TRUE : Boolean.FALSE;
sharedInstance.renderStyle = STYLE_CLIP;
sharedInstance.border = null;
sharedInstance.prefSize = null;
sharedInstance.text = null;
sharedInstance.setCentered(false);
sharedInstance.setIcon(null);
sharedInstance.setOpaque(false);
}
return sharedInstance;
}
/** Convenience getter for the shared generic html renderer instance.
* The instance returned will be configured to render as HTML only if the
* string passed to setText() starts with HTML tags.
* @return a static instance of HtmlRenderer
*/
public static final HtmlRenderer sharedInstance() {
HtmlRenderer result = sharedInstance(false);
sharedInstance.isHtml = null;
return result;
}
/** Convenience method for configuring the renderer to display the
* icon on top and the text centered below it, as used by
* org.openide.explorer.IconView */
public final void setCentered(boolean val) {
if (val != centered) {
if (val) {
setVerticalTextPosition(JLabel.BOTTOM);
setHorizontalAlignment(JLabel.CENTER);
setHorizontalTextPosition(JLabel.CENTER);
} else {
setVerticalTextPosition(JLabel.CENTER);
setHorizontalAlignment(JLabel.LEADING);
setHorizontalTextPosition(JLabel.TRAILING);
}
centered = val;
}
}
/** Convenience method to set indentation, as used in several cases in
* Explorer. Not typically used except in special cases. Call this
* method *after* any call to setCellHasFocus in order to preserve
* the focus outline. */
public final void setIndent (int indent) {
if (indent == 0) {
setBorder (null);
} else {
Border outer= border == focusBorder ? focusBorder :
BorderFactory.createEmptyBorder (1, 1, 1, 1);
setBorder (BorderFactory.createCompoundBorder (
BorderFactory.createEmptyBorder (0, indent, 0, 0),
outer
));
}
}
/** Set the rendering style with which the renderer should be paint.
* This can be STYLE_CLIP (simply cut off text that doesn't fit) or
* STYLE_TRUNCATE (which will stop before the string is fully painted
* if it won't fit, and append elipsis ("...")). */
public final void setRenderStyle(int val) {
renderStyle = val;
}
/** Get the current rendering style - STYLE_CLIP or STYLE_TRUNCATE.
* STYLE_TRUNCATE is the default for new instances and the instance
* returned by sharedInstance()
. The default after
* construction or calling sharedInstance()
is STYLE_CLIP */
public final int getRenderStyle() {
return renderStyle;
}
public final void setBorder(Border b) {
if (swingRendering) {
super.setBorder(b);
return;
}
this.border = b;
}
public final Border getBorder() {
if (swingRendering) {
return super.getBorder();
}
return this.border;
}
/** Overridden to do some trivial extra border handling - if isPaintAsFocused(),
* a border is used to produce the focus rectangle. The insets returned,
* however, will be 0 for top and bottom, so the border can be changed for
* different look and feels, but does not cause the text position to
* change when a cell is focused (which makes the text appear to jump
* when clicked). */
public Insets getInsets() { //non final for winsys override
if (swingRendering) {
return super.getInsets();
}
if (border != null) {
if (border == focusBorder) {
//We intentionally zero-out the top and bottom insets, so that
//setting a focus border does not cause the Y position of the
//text to change (making a cell "jump" when selected).
return emptyinsets;
} else {
return border.getBorderInsets(this);
}
} else {
return emptyinsets;
}
}
private static final Insets emptyinsets = new Insets (0,0,0,0);
public final void setText(String s) {
if (swingRendering) {
super.setText(s);
prefSize = null;
return;
}
/*if ((s != null && !s.equals(text)) || s ==null) {
isHtml = null;
}
*/
text = s;
}
public final String getText() {
if (swingRendering) {
return super.getText();
} else {
return text;
}
}
public final void setIcon(Icon ic) {
assert SwingUtilities.isEventDispatchThread();
if (ic != getIcon()) {
prefSize = null;
super.setIcon (ic);
}
}
/** Set the text, specifying whether it should be treated as HTML or
* not. If Boolean.TRUE
is passed, painting will use the
* HTML renderer automatically - use this if you know
* you are passing a string containing HTML markup, or if you are passing
* as String which contains HTML markup without opening/closing HTML
* tags. Pass Boolean.FALSE
if you either do not have HTML
* markup, or you want to render an HTML string literally as its markup.
* If you do not know whether you have HTML or not, passing null will
* cause the renderer to look for opening/closing HTML tags and either
* use plain or HTML rendering, depending on the result.
*
Note the renderer uses a very simple, fast test to decide if a
* string contains HTML markup - it must begin with
* <html> or <HTML>. No whitespace trimming or other checks
* are performed.
*
* @param s the text the renderer should display
* @param html - a Boolean value indicating if the text is known to
* contain HTML markup or not, or null if this is not known but the
* text will contain an opening html tag if it is to be rendered as
* html.
*/
public final void setText(String s, Boolean html) {
if (swingRendering) {
super.setText(s instanceof String ? (String) s : s != null ?
s.toString() : null);
prefSize = null;
return;
}
isHtml = html;
text = s instanceof String ? (String) s : s != null ?
s.toString() : null;
prefSize = null;
}
/** Overridden to do nothing. */
public final void validate() {
if (swingRendering) {
super.validate();
}
}
/** Returns true if the instance should paint as if it were selected (using
* the selection foreground and background colors instead of the standard
* foreground and background colors). Any of the cell renderer fetching
* methods will set this correctly based on the state of the cell being
* painted. */
public boolean isSelected() { //non final due to winsys override
return selected;
}
/** Set the selected state of the cell renderer, which determines whether
* the selected or unselected colors will be used for painting. This will
* be called by any of the cell renderer fetching methods if a renderer is
* requested for a selected cell */
public final void setSelected(boolean val) {
selected = val;
}
/** This property differs slightly from typical cell renderer usage -
* NetBeans uses a different background color to distinguish selected
* items in the focused tree/list/table and selected items if the
* tree/list/table is not focused. This state is determined in the
* cell renderer fetching methods, in the overridable method
* checkFocused()
. The default value is false. */
public final boolean isPaintAsFocused() {
return paintAsFocused;
}
/** Sets whether the seletion colors used as the background and foreground
* should be those for components which have focus or do not - NetBeans
* uses different colors for these. This will be called by any of the
* cell renderer fetcher methods with the result from
* checkFocused()
. */
public final void setPaintAsFocused(boolean b) {
paintAsFocused = b;
}
/** Returns whether the renderer will paint as though the current cell
* is focused, if isSelected()
also returns true. Typically this is
* whatever value was last passed for the hasFocus
argument
* to whichever cell renderer fetcher method was last called. If true,
* the renderer will paint a focus outline rectangle around itself.
* The default value is false. */
public final boolean isCellHasFocus() {
return border == focusBorder;
}
/** Set whether the renderer will paint as though the current cell is
* focused, if it is selected. */
public final void setCellHasFocus(boolean val) {
border = val ? focusBorder : null;
}
/** Fetch the current background color. Depending on the state of the
* renderer, this may return the last-set background color, the selection
* background color or the unfocused selection background color. */
public final Color getBackground() {
return super.getBackground();
}
/** Fetch the current foreground color. Depending on the state of the
* renderer, this may return the last-set foreground color, the selection
* foreground color or the unfocused selection foreground color. */
public Color getForeground() {
return super.getForeground();
}
/** Get the selection background which should be used for painting if
* isSelected() returns true. */
public final Color getSelectionBackground() {
return selectionBackground;
}
/** Set the selection background which should be used for painting if
* isSelected() returns true. */
public final void setSelectionBackground(Color c) {
selectionBackground = c;
}
/** Get the selection foreground which should be used for painting if
* isSelected() returns true. */
public final Color getSelectionForeground() {
return selectionForeground;
}
/** Set the selection foreground which should be used for painting if
* isSelected() returns true. */
public final void setSelectionForeground(Color c) {
selectionForeground = c;
}
/** HtmlRenderer intentionally overrides firePropertyChange to do nothing.
* This avoids performance problems when used as a renderer - in particular,
* it avoids the creation of document trees for small strings. */
protected final void firePropertyChange(String name, Object old, Object nue) {
if (swingRendering) {
super.firePropertyChange(name, old, nue);
}
}
// Cell renderer implementations
/** Basic implementation of a list cell renderer */
public final java.awt.Component getListCellRendererComponent(JList
list, Object value, int index, boolean isSelected, boolean hasFocus) {
assert SwingUtilities.isEventDispatchThread();
if (value == null) {
value = ""; //NOI18N
}
configureFrom(value, list, isSelected, hasFocus, list.getSelectionBackground(),
list.getSelectionForeground(), list.getBackground(),
list.getForeground());
return this;
}
/** Basic implementation of a table cell renderer. Will method will always
* return the same instance of HtmlRenderer it is called on after configuring
* itself appropriately for the passed arguments. */
public final java.awt.Component getTableCellRendererComponent(JTable table,
Object value, boolean isSelected, boolean hasFocus, int row, int column) {
assert SwingUtilities.isEventDispatchThread();
if (value == null) {
value = ""; //NOI18N
}
configureFrom(value, table, isSelected, hasFocus, table.getSelectionBackground(),
table.getSelectionForeground(), table.getBackground(),
table.getForeground());
return this;
}
/** Basic implementation of a tree cell renderer */
public final java.awt.Component getTreeCellRendererComponent(JTree tree, Object
value, boolean isSelected, boolean expanded, boolean leaf,
int row, boolean hasFocus) {
assert SwingUtilities.isEventDispatchThread();
if (value == null) {
value = ""; //NOI18N
}
configureFrom(value, tree, isSelected, hasFocus,
getFocusedSelectionBackground(), getFocusedSelectionForeground(),
tree.getBackground(), tree.getForeground());
return this;
}
/** Generic code to set properties appropriately from any of the renderer
* fetching methods */
private void configureFrom(Object value, JComponent target,
boolean selected, boolean cellHasFocus, Color selBg, Color selFg,
Color bg, Color fg) {
if (this == sharedInstance && value instanceof String && !swingRendering) {
//Content type has already been configured by sharedInstance(type),
//and if sharedInstance() with no args was called, it will be
//lazily resolved when painting
text = (String) value;
} else {
setText(value instanceof String ? (String) value : value.toString(),
null);
}
setSelected(selected);
if (selected) {
setPaintAsFocused(checkFocused(target));
} else {
setPaintAsFocused(false);
}
setBackground(bg);
setForeground(fg);
setSelectionBackground(selBg);
setSelectionForeground(selFg);
setCellHasFocus(cellHasFocus);
setEnabled (target.isEnabled());
}
private boolean checkFocused(JComponent c) {
Component focused =
KeyboardFocusManager.getCurrentKeyboardFocusManager().getPermanentFocusOwner();
boolean result = c == focused;
if (!result) {
result = c.isAncestorOf(focused);
}
return result;
}
/** Utility method to check if the text is HTML */
private boolean isHtml() {
if (text == null) {
return false;
}
//Lazy detection if setText(String s) not setText (Object, int) was
//called
if (isHtml == null) {
if (text.startsWith("= txtH) {
//Center the text if we have space
txtY = txtH + ins.top + ((availH / 2) - (txtH / 2)) - 4;
} else {
//Okay, it's not going to fit, punt.
txtY = 0;//txtH + ins.top - 1;
}
int txtX;
Icon icon = getIcon();
//Check the icon non-null and height (see TabData.NO_ICON for why)
if (icon != null && icon.getIconWidth() > 0 && icon.getIconHeight() > 0) {
int iconY;
if (availH > icon.getIconHeight()) {
//add 2 to make sure icon top pixels are not cut off by outline
iconY = ins.top + ((availH / 2) - (icon.getIconHeight() / 2));// + 2;
} else if (availH == icon.getIconHeight()){
//They're an exact match, make it 0
iconY = 0;
} else {
//Won't fit; make the top visible and cut the rest off (option:
//center it and clip it on top and bottom - probably even harder
//to recognize that way, though)
iconY = ins.top;
}
//add in the insets
int iconX = ins.left;
try {
//Diagnostic - the CPP module currently is constructing
//some ImageIcon from a null image in Options. So, catch it and at
//least give a meaningful message that indicates what node
//is the culprit
icon.paintIcon(this, g, iconX, iconY);
} catch (NullPointerException npe) {
ErrorManager.getDefault().annotate(npe, ErrorManager.EXCEPTION,
"Probably an ImageIcon with a null source image: " +
text, null, null, null); //NOI18N
ErrorManager.getDefault().notify(npe);
}
txtX = iconX + icon.getIconWidth() + getIconTextGap();
} else {
//If there's no icon, paint the text where the icon would start
txtX = ins.left;
}
String text = getText();
if (text == null) {
//No text, we're done
return;
}
//Get the available horizontal pixels for text
int txtW = icon != null ? getWidth() - (ins.left + ins.right +
icon.getIconWidth() + getIconTextGap()) : getWidth() -
(ins.left + ins.right);
Color foreground;
//Set up the correct foreground color based on the state
if (!isEnabled()) {
//XXX the HIE folks will probably want the ability to
//specify individual enabled/disabled foregrounds and
//backgrounds for different selection states.
foreground = UIManager.getColor("textInactiveText"); //NOI18N
} else if (isSelected() && isPaintAsFocused()) {
foreground = getSelectionForeground();
} else if (isSelected()) {
foreground = getUnfocusedSelectionForeground();
} else {
foreground = getForeground();
}
double wid;
if (isHtml()) {
wid = renderHTML(text, g, txtX, txtY, txtW, txtH, getFont(),
foreground, getRenderStyle(), true);
} else {
wid = renderString(text, g, txtX, txtY, txtW, txtH, getFont(),
foreground, getRenderStyle(), true);
}
}
/**Render a string to a graphics instance, using the same API as renderHTML().
* Can render a string using JLabel-style ellipsis (...) in the case that
* it will not fit in the passed rectangle, if the style parameter is
* STYLE_CLIP. Returns the width in pixels successfully painted.
* This method is not thread-safe and should not be called off
* the AWT thread.
*
* @see org.openide.util.Utilities.renderHTML */
public static double renderPlainString(String s, Graphics g, int x, int y, int w, int h, Font f, Color defaultColor, int style, boolean paint) {
assert SwingUtilities.isEventDispatchThread(); //XXX once build supports this, uncomment
//per Jarda's request, keep the word wrapping code but don't expose it.
if (style < 0 || style > 1) {
throw new IllegalArgumentException(
"Unknown rendering mode: " + style); //NOI18N
}
return _renderPlainString(s, g, x, y, w, h, f, defaultColor, style,
paint);
}
private static double _renderPlainString(String s, Graphics g, int x, int y, int w, int h, Font f, Color foreground, int style, boolean paint) {
g.setColor(foreground);
g.setFont(f);
FontMetrics fm = g.getFontMetrics(f);
Rectangle2D r = fm.getStringBounds(s, g);
if ((r.getWidth() <= w) || (style == STYLE_CLIP)) {
if (paint) {
g.drawString(s, x, y);
}
} else {
char[] chars = new char[s.length()];
s.getChars(0, s.length()-1, chars, 0);
if (chars.length == 0) {
return 0;
}
double chWidth = r.getWidth() / chars.length;
int estCharsOver = new Double((r.getWidth() - w) / chWidth).intValue();
if (style == STYLE_TRUNCATE) {
int length = chars.length - estCharsOver;
if (length <=0) {
return 0;
}
if (paint) {
if (length > 3) {
Arrays.fill(chars, length-3, length, '.');
g.drawChars(chars, 0, length, x, y);
} else {
Shape shape = g.getClip();
if (s != null) {
Area area = new Area(shape);
area.intersect (new Area(new Rectangle(x,y,w,h)));
g.setClip(area);
} else {
g.setClip(new Rectangle(x,y,w,h));
}
g.drawString("...", x,y);
g.setClip(shape);
}
}
} else {
//XXX implement plaintext word wrap if we want to support it at some point
}
}
return r.getWidth();
}
/** Render a string to a graphics context, using HTML markup if the string
* begins with html tags. Delegates to renderPlainString()
* or renderHTML()
as appropriate. See the documentation for
* renderHTML()
for details of the subset of HTML that is
* supported.
*
This method is not thread-safe and should not be called off
* the AWT thread.
* @param s The string to render
* @param g A graphics object into which the string should be drawn, or which should be
* used for calculating the appropriate size
* @param x The x coordinate to paint at.
* @param y The y position at which to paint. Note that this method does not calculate font
* height/descent - this value should be the baseline for the line of text, not
* the upper corner of the rectangle to paint in.
* @param w The maximum width within which to paint.
* @param h The maximum height within which to paint.
* @param f The base font to be used for painting or calculating string width/height.
* @param defaultColor The base color to use if no font color is specified as html tags
* @param style The wrapping style to use, either STYLE_CLIP
,
* or STYLE_TRUNCATE
* @param paint True if actual painting should occur. If false, this method will not actually
* paint anything, only return a value representing the width/height needed to
* paint the passed string.
* @return The width in pixels required
* to paint the complete string, or the passed parameter w
if it is
* smaller than the required width.
*/
public static double renderString(String s, Graphics g, int x, int y, int w, int h, Font f, Color defaultColor, int style, boolean paint) {
if (s.startsWith(" 1) {
throw new IllegalArgumentException(
"Unknown rendering mode: " + style); //NOI18N
}
return _renderHTML(6, s, g, x, y, w, h, f, defaultColor, style, paint);
} else {
return renderPlainString(s, g, x, y, w, h, f, defaultColor, style, paint);
}
}
/** Render a string as HTML using a fast, lightweight renderer supporting a limited
* subset of HTML. The following tags are supported, in upper or lower case:
*
*
<B> | *Boldface text | *
<S> | *Strikethrough text | *
<U> | *Underline text | *
<I> | *Italic text | *
<EM> | *Emphasized text (same as italic) | *
<STRONG> | *Strong text (same as bold) | *
<font> | *Font color - font attributes other than color are not supported. Colors
* may be specified as hexidecimal strings, such as #FF0000 or as logical colors
* defined in the current look and feel by specifying a ! character as the first
* character of the color name. Logical colors are colors available from the
* current look and feel's UIManager. For example, <font
* color="!Tree.background"> will set the font color to the
* result of UIManager.getColor("Tree.background") .
* Font size tags are not supported.
* |
*
quot, lt, amp, lsquo, rsquo, ldquo, rdquo, ndash, mdash, ne,
* le, ge, copy, reg, trade.
. It also supports numeric entities
* (e.g. &8822;
).
* When to use this method instead of the JDK's HTML support: when * rendering short strings (for example, in a tree or table cell renderer) * with limited HTML, this method is approximately 10x faster than JDK HTML * rendering (it does not build and parse a document tree). * *
Specifying logical colors
* Hardcoded text colors are undesirable, as they can be incompatible (even
* invisible) on some look and feels or themes.
* The lightweight HTML renderer supports a non-standard syntax for specifying
* font colors via a key for a color in the UI defaults for the current look
* and feel. This is accomplished by prefixing the key name with a !
* character. For example: <font color='!controlShadow'>
.
*
*
Modes of operation
* This method supports two modes of operation:
*
STYLE_CLIP
- as much text as will fit in the pixel width passed
* to the method should be painted, and the text should be cut off at the maximum
* width or clip rectangle maximum X boundary for the graphics object, whichever is
* smaller.STYLE_TRUNCATE
- paint as much text as will fit in the pixel
* width passed to the method, but paint the last three characters as .'s, in the
* same manner as a JLabel truncates its text when the available space is too
* small.
* This method can also be used in non-painting mode to establish the space
* necessary to paint a string. This is accomplished by passing the value of the
* paint
argument as false. The return value will be the required
* width in pixels
* to display the text. Note that in order to retrieve an
* accurate value, the argument for available width should be passed
* as Integer.MAX_VALUE
or an appropriate maximum size - otherwise
* the return value will either be the passed maximum width or the required
* width, whichever is smaller. Also, the clip shape for the passed graphics
* object should be null or a value larger than the maximum possible render size.
*
* This method will log a warning if it encounters HTML markup it cannot
* render. To aid diagnostics, if NetBeans is run with the argument
* -J-Dnetbeans.lwhtml.strict=true
an exception will be thrown
* when an attempt is made to render unsupported HTML.
* This method is not thread-safe and should not be called off * the AWT thread! *
* @param s The string to render
* @param g A graphics object into which the string should be drawn, or which should be
* used for calculating the appropriate size
* @param x The x coordinate to paint at.
* @param y The y position at which to paint. Note that this method does not calculate font
* height/descent - this value should be the baseline for the line of text, not
* the upper corner of the rectangle to paint in.
* @param w The maximum width within which to paint.
* @param h The maximum height within which to paint.
* @param f The base font to be used for painting or calculating string width/height.
* @param defaultColor The base color to use if no font color is specified as html tags
* @param style The wrapping style to use, either STYLE_CLIP
,
* or STYLE_TRUNCATE
* @param paint True if actual painting should occur. If false, this method will not actually
* paint anything, only return a value representing the width/height needed to
* paint the passed string.
* @return The width in pixels required
* to paint the complete string, or the passed parameter w
if it is
* smaller than the required width.
*/
public static double renderHTML(String s, Graphics g, int x, int y,
int w, int h, Font f,
Color defaultColor, int style,
boolean paint) {
assert SwingUtilities.isEventDispatchThread(); //XXX once build supports this, uncomment
//per Jarda's request, keep the word wrapping code but don't expose it.
if (style < 0 || style > 1) {
throw new IllegalArgumentException(
"Unknown rendering mode: " + style); //NOI18N
}
return _renderHTML(0, s, g, x, y, w, h, f, defaultColor, style,
paint);
}
/** Implementation of HTML rendering */
private static double _renderHTML(int pos, String s, Graphics g, int x,
int y, int w, int h, Font f, Color defaultColor, int style,
boolean paint) {
g.setColor(defaultColor);
g.setFont(f);
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;
//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()) {
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 \""
+ s + "\". Please report this at http://www.netbeans.org"); //NOI18N
throw aib;
}
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
}
done = true;
} else if (inTag) {
//If we're in a tag, don't paint, process it
pos++;
int tagEnd = pos;
while (!done && (chars[tagEnd] != '>')) {
done = tagEnd == chars.length -1;
tagEnd++;
}
if (inClosingTag) {
//Handle closing tags by resetting the Graphics object (font, etc.)
pos++;
switch (chars[pos]) {
case 'P' :
case 'p' :
case 'H' :
case 'h' : break; //ignore html opening/closing tags
case 'B' :
case 'b' :
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
}
if (italic) {
g.setFont(f.deriveFont(Font.ITALIC));
} else {
g.setFont(f.deriveFont(Font.PLAIN));
}
bold = false;
break;
case 'E' :
case 'e' : //em tag
case 'I' :
case 'i' :
if (bold) {
g.setFont(f.deriveFont(Font.BOLD));
} else {
g.setFont(f.deriveFont(Font.PLAIN));
}
if (!italic) {
throwBadHTML("Closing italics tag w/o" //NOI18N
+ "opening italics tag", pos, chars); //NOI18N
}
italic = false;
break;
case 'S' :
case 's' :
switch (chars[pos+1]) {
case 'T' :
case 't' : if (italic) {
g.setFont(f.deriveFont(
Font.ITALIC));
} else {
g.setFont(f.deriveFont(
Font.PLAIN));
}
bold = false;
break;
case '>' :
strikethrough = false;
break;
}
break;
case 'U' :
case 'u' : underline = false;
break;
case 'F' :
case 'f' :
if (colorStack.isEmpty()) {
g.setColor(defaultColor);
} else {
g.setColor((Color) 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' :
case 'b' :
switch (chars[pos+1]) {
case 'R' :
case 'r' :
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(f.deriveFont(Font.BOLD | Font.ITALIC));
} else {
g.setFont(f.deriveFont(Font.BOLD));
}
break;
}
break;
case 'e' : //em tag
case 'E' :
case 'I' :
case 'i' :
italic = true;
if (bold) {
g.setFont(f.deriveFont(Font.ITALIC | Font.BOLD));
} else {
g.setFont(f.deriveFont(Font.ITALIC));
}
break;
case 'S' :
case 's' :
switch (chars[pos+1]) {
case '>' :
strikethrough = true;
break;
case 'T' :
case 't' :
bold = true;
if (italic) {
g.setFont(f.deriveFont(Font.BOLD | Font.ITALIC));
} else {
g.setFont(f.deriveFont(Font.BOLD));
}
break;
}
break;
case 'U' :
case 'u' :
underline = true;
break;
case 'f' :
case 'F' :
Color c = findColor(chars, pos, tagEnd);
colorStack.push(g.getColor());
g.setColor(c);
break;
case 'P' :
case 'p' :
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;
}
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() && 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] == '&')) {
boolean inEntity=pos != chars.length-1;
if (inEntity) {
int newPos = substEntity(chars, pos+1);
inEntity = newPos != -1;
if (inEntity) {
pos = newPos;
isAmp = chars[pos] == '&';
//flag it so the next iteration won't think the <
//starts a tag
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)) {
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(g.getFont());
//Get the bounds of the substring we'll paint
Rectangle2D r = fm.getStringBounds(chars, pos, nextTag + 1, g);
//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 (style == STYLE_TRUNCATE) {
//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 - 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 * 2)))) ||
(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((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);
// System.err.println("Truncated set to true at " + pos + " (" + chars[pos] + ")");
truncated = true;
} else {
goToNextRow = true;
int lastChar = new Double(nextTag -
estCharsOver).intValue();
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);
}
goToNextRow = false;
widthPainted = 0;
if (chars[pos - 1 + length] == '<') {
length --;
}
} else if (brutalWrap) {
//wrap without checking word boundaries
length = (new Double(
(w - widthPainted) / chWidth)
).intValue();
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 = new Double(x +
r.getWidth()).intValue();
if (paint) {
if (strikethrough) {
int stPos = Math.round(
lm.getStrikethroughOffset()) +
g.getFont().getBaselineFor(chars[pos])
+ 1;
// int stThick = Math.round (lm.getStrikethroughThickness()); //XXX
g.drawLine(x, y + stPos, lineWidth, y + stPos);
}
if (underline) {
int stPos = Math.round(
lm.getUnderlineOffset()) +
g.getFont().getBaselineFor(chars[pos])
+ 1;
// int stThick = new Float (lm.getUnderlineThickness()).intValue(); //XXX
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;
}
}
/** Parse a font color tag and return an appopriate java.awt.Color instance */
private static Color findColor(final char[] ch, final int pos,
final int tagEnd) {
int colorPos = pos;
boolean useUIManager = false;
for (int i=pos; i < tagEnd; i ++) {
if (ch[i] == 'c') {
colorPos = i + 6;
if (ch[colorPos] == '\'' || ch[colorPos] == '"') {
colorPos++;
}
//skip the leading # character
if (ch[colorPos] == '#') {
colorPos++;
} else if (ch[colorPos] == '!') {
useUIManager = true;
colorPos++;
}
break;
}
}
if (colorPos == pos) {
String out = "Could not find color identifier in font declaration";
throwBadHTML(out, pos, ch);
}
//Okay, we're now on the first character of the hex color definition
String s;
if (useUIManager) {
int end = ch.length-1;
for (int i=colorPos; i < ch.length; i++) {
if (ch[i] == '"' || ch[i] == '\'') { //NOI18N
end = i;
break;
}
}
s = new String(ch, colorPos, end-colorPos);
} else {
s = new String(ch, colorPos, 6);
}
Color result=null;
if (useUIManager) {
result = UIManager.getColor(s);
//Not all look and feels will provide standard colors; handle it gracefully
if (result == null) {
throwBadHTML(
"Could not resolve logical font declared in HTML: " + s,
pos, ch);
result = UIManager.getColor("textText"); //NOI18N
//Avoid NPE in headless situation?
if (result == null) {
result = Color.BLACK;
}
}
} else {
try {
int rgb = Integer.parseInt(s, 16);
result = new Color(rgb);
} catch (NumberFormatException nfe) {
throwBadHTML(
"Illegal hexadecimal color text: " + s + //NOI18N
" in HTML string", colorPos, ch);
}
}
if (result == null) {
throwBadHTML("Unresolvable html color: " + s //NOI18N
+ " in HTML string \n ", pos, ch);
}
return result;
}
/** Definitions for a limited subset of sgml character entities */
private static final Object[] entities = new Object[] {
new char[] {'g','t'}, new char[] {'l','t'},
new char[] {'q','u','o','t'}, new char[] {'a','m','p'},
new char[] {'l','s','q','u','o'},
new char[] {'r','s','q','u','o'},
new char[] {'l','d','q','u','o'},
new char[] {'r','d','q','u','o'},
new char[] {'n','d','a','s','h'},
new char[] {'m','d','a','s','h'},
new char[] {'n','e'},
new char[] {'l','e'},
new char[] {'g','e'},
new char[] {'c','o','p','y'},
new char[] {'r','e','g'},
new char[] {'t','r','a','d','e'}
//The rest of the SGML entities are left as an excercise for the reader
}; //NOI18N
/** Mappings for the array of sgml character entities to characters */
private static final char[] entitySubstitutions = new char[] {
'>','<','"','&',8216, 8217, 8220, 8221, 8211, 8212, 8800, 8804, 8805,
169, 174, 8482
};
/** Find an entity at the passed character position in the passed array.
* If an entity is found, the trailing ; character will be substituted
* with the resulting character, and the position of that character
* in the array will be returned as the new position to render from,
* causing the renderer to skip the intervening characters */
private static final int substEntity(char[] ch, int pos) {
//There are no 1 character entities, abort
if (pos >= ch.length-2) {
return -1;
}
//if it's numeric, parse out the number
if (ch[pos] == '#') {
return substNumericEntity(ch, pos+1);
}
//Okay, we've potentially got a named character entity. Try to find it.
boolean match;
for (int i=0; i < entities.length; i++) {
char[] c = (char[]) entities[i];
match = true;
if (c.length < ch.length-pos) {
for (int j=0; j < c.length; j++) {
match &= c[j] == ch[j+pos];
}
} else {
match = false;
}
if (match) {
//if it's a match, we still need the trailing ;
if (ch[pos+c.length] == ';') {
//substitute the character referenced by the entity
ch[pos+c.length] = entitySubstitutions[i];
return pos+c.length;
}
}
}
return -1;
}
/** Finds a character defined as a numeric entity (e.g. „)
* and replaces the trailing ; with the referenced character, returning
* the position of it so the renderer can continue from there.
*/
private static final int substNumericEntity(char[] ch, int pos) {
for (int i=pos; i < ch.length; i++) {
if (ch[i] == ';') {
try {
ch[i] = (char) Integer.parseInt(
new String(ch, pos, i - pos));
return i;
} catch (NumberFormatException nfe) {
throwBadHTML("Unparsable numeric entity: " +
new String(ch, pos, i - pos), pos, ch); //NOI18N
}
}
}
return -1;
}
/** Throw an exception for unsupported or bad html, indicating where the problem is
* in the message */
private static void throwBadHTML(String msg, int pos, char[] chars) {
char[] chh = new char[pos];
Arrays.fill(chh, ' '); //NOI18N
chh[pos-1] = '^'; //NOI18N
String out = msg + "\n " + new String(chars) + "\n "
+ new String(chh) + "\n Full HTML string:" + new String(chars); //NOI18N
if (!strictHTML) {
if (ErrorManager.getDefault().isLoggable(ErrorManager.WARNING)) {
if (badStrings == null) {
badStrings = new HashSet();
}
if (!badStrings.contains(msg)) {
//ErrorManager bug, issue 38372 - log messages containing
//newlines are truncated - so for now we iterate the
//string we've just constructed
StringTokenizer tk = new StringTokenizer(out, "\n", false);
while (tk.hasMoreTokens()) {
ErrorManager.getDefault().log(ErrorManager.WARNING,
tk.nextToken());
}
badStrings.add(msg.intern());
}
}
} else {
throw new IllegalArgumentException(out);
}
}
private static Map hintsMap = null;
static final Map getHints() {
if (hintsMap == null) {
hintsMap = new HashMap();
hintsMap.put(RenderingHints.KEY_FRACTIONALMETRICS,
RenderingHints.VALUE_FRACTIONALMETRICS_ON);
hintsMap.put(RenderingHints.KEY_TEXT_ANTIALIASING,
RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
hintsMap.put(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
}
return hintsMap;
}
/*
//main method for testing
public static void main(String[] args) {
javax.swing.JFrame jf = new javax.swing.JFrame();
jf.getContentPane().setLayout (new java.awt.FlowLayout());
final HtmlRenderer ren = new HtmlRenderer();
final JLabel jl = new JLabel();
ren.setBackground (Color.YELLOW);
ren.setOpaque(true);
String s = "This is a test of HTML rendering which sucks, even in red";
// String s = "This is a test of HTML rendering which sucks, even in red while dying in a green room full of fish";
javax.swing.JSplitPane jsp = new javax.swing.JSplitPane();
javax.swing.JPanel jp = new javax.swing.JPanel();
// jp.setLayout(new java.awt.BorderLayout());
// jp.add(ren, java.awt.BorderLayout.CENTER);
// jp.add(jl, java.awt.BorderLayout.SOUTH);
jp.setLayout(new java.awt.FlowLayout());
jp.add(ren);
jp.add(jl);
jsp.setLeftComponent (jp);
final javax.swing.JTextField jtf = new javax.swing.JTextField(s);
jsp.setRightComponent(jtf);
jtf.addActionListener (new java.awt.event.ActionListener() {
public void actionPerformed(java.awt.event.ActionEvent ae) {
ren.setText(jtf.getText());
jl.setText(jtf.getText());
}
});
// jf.getContentPane().add (ren);
// jf.getContentPane().add (jl);
jf.getContentPane().add(jsp);
jl.setFont (jl.getFont().deriveFont(Font.PLAIN));
ren.setFont (jl.getFont().deriveFont(Font.PLAIN));
// ren.setCellRenderer(false);
// ren.setRenderStyle(STYLE_CLIP);
jl.setText (s);
ren.setText (s);
jf.setSize (400,400);
jf.setLocation (20,20);
System.err.println("jl preferred size " + jl.getPreferredSize());
jf.show();
}
*/
//***************Some static utility methods for standard colors***************/
/** Get the system-wide unfocused selection background color */
private static Color getUnfocusedSelectionBackground() {
if (unfocusedSelBg == null) {
//allow theme/ui custom definition
unfocusedSelBg =
UIManager.getColor("nb.explorer.unfocusedSelBg"); //NOI18N
if (unfocusedSelBg == null) {
//try to get standard shadow color
unfocusedSelBg = UIManager.getColor("controlShadow"); //NOI18N
if (unfocusedSelBg == null) {
//Okay, the look and feel doesn't suport it, punt
unfocusedSelBg = Color.lightGray;
}
//Lighten it a bit because disabled text will use controlShadow/
//gray
unfocusedSelBg = unfocusedSelBg.brighter();
}
}
return unfocusedSelBg;
}
/** Get the system-wide unfocused selection foreground color */
private static Color getUnfocusedSelectionForeground() {
if (unfocusedSelFg == null) {
//allow theme/ui custom definition
unfocusedSelFg =
UIManager.getColor("nb.explorer.unfocusedSelFg"); //NOI18N
if (unfocusedSelFg == null) {
//try to get standard shadow color
unfocusedSelFg = UIManager.getColor("textText"); //NOI18N
if (unfocusedSelFg == null) {
//Okay, the look and feel doesn't suport it, punt
unfocusedSelFg = Color.BLACK;
}
}
}
return unfocusedSelFg;
}
/** Returns the default focused selection background, to be used for
* components like JTree, which have no getSelectionBackrgound/Foreground
* method. Looks in the usual places with a fallback to a hardcoded
* color. */
private static Color getFocusedSelectionBackground() {
if (defSelBg == null) {
defSelBg = UIManager.getColor ("nb.explorer.focusedSelBg"); //NOI18N
if (defSelBg == null) {
defSelBg = UIManager.getColor ("Tree.selectionBackground"); //NOI18N
}
if (defSelBg == null) {
defSelBg = UIManager.getColor("info"); //NOI18N
}
if (defSelBg == null) {
defSelBg = Color.BLUE;
}
}
return defSelBg;
}
/** Returns the default focused selection foreground, to be used for
* components like JTree, which have no getSelectionBackrgound/Foreground
* method. Looks in the usual places with a fallback to a hardcoded
* color. */
private static Color getFocusedSelectionForeground() {
if (defSelFg == null) {
defSelFg = UIManager.getColor ("nb.explorer.focusedSelFg"); //NOI18N
if (defSelFg == null) {
defSelFg = UIManager.getColor ("Tree.selectionForeground"); //NOI18N
}
if (defSelFg == null) {
defSelFg = UIManager.getColor("infoText"); //NOI18N
}
if (defSelFg == null) {
defSelFg = Color.WHITE;
}
}
return defSelFg;
}
}