? openide/html.diff
? openide/src/org/openide/util/U_head.txt
? openide/src/org/openide/util/U_nav.txt
Index: openide/openide-spec-vers.properties
===================================================================
RCS file: /cvs/openide/openide-spec-vers.properties,v
retrieving revision 1.118
diff -r1.118 openide-spec-vers.properties
7c7
< org.openide.specification.version=4.10
---
> org.openide.specification.version=4.11
Index: openide/api/doc/changes/apichanges.xml
===================================================================
RCS file: /cvs/openide/api/doc/changes/apichanges.xml,v
retrieving revision 1.165
diff -r1.165 apichanges.xml
116c116,142
<
---
> getFormattedDisplayName
. An interface,
> HTMLStatus
has been created which extends
> FileSystem.Status
, has been created, which allows
> filesystems to supply HTML formatted status information, by
> implementing it on their FileSystem.Status
implementation.
> If one is present, DataNode will use it to supply HTML formatted
> text to Explorer.
> Utilities.renderString()
. If the underlying
> * FileSystem.Status
is an instance of HTMLStatus,
> * this method will return non-null if status information is added.
> *
> * @return a string containing compliant HTML markup or null
> * @see org.openide.util.Utilities.renderHTML
> * @see org.openide.nodes.Node.getFormattedDisplayName
> * @since 1.73 */
> public String getFormattedDisplayName() {
> try {
> FileSystem.Status stat =
> obj.getPrimaryFile().getFileSystem().getStatus();
> if (stat instanceof FileSystem.HTMLStatus) {
> FileSystem.HTMLStatus hstat = (FileSystem.HTMLStatus) stat;
> String result = hstat.annotateNameHTML (
> super.getDisplayName(), obj.files());
>
> //Make sure the super string was really modified
> if (!super.getDisplayName().equals(result)) {
> return result;
> }
> }
> } catch (FileStateInvalidException e) {
> //do nothing and fall through
> }
> return super.getFormattedDisplayName();
> }
Index: openide/src/org/openide/explorer/view/NodeRenderer.java
===================================================================
RCS file: /cvs/openide/src/org/openide/explorer/view/NodeRenderer.java,v
retrieving revision 1.23
diff -r1.23 NodeRenderer.java
218c218,391
<
---
>
> static class BaseRenderer extends javax.swing.JComponent {
> private javax.swing.Icon icon=null;
> private String txt="";//NOI18N
> private int iconTextGap=0;
> protected boolean hasFocus=false;
> protected boolean selected = false;
> private boolean ndCalcPrefSize=true;
> private Color selectionForeground=null;
> private Color selectionBackground=null;
> private Color selectionBorder=null;
>
> private boolean isHTML=true;
>
> public BaseRenderer() {
> updateUI();
> }
>
> public void setHTML(boolean html) {
> isHTML = html;
> }
>
> public javax.swing.Icon getIcon() {
> return icon;
> }
> java.awt.Dimension prefSize = null;
>
> public java.awt.Dimension getPreferredSize () {
> if (ndCalcPrefSize) {
> calcPrefSize();
> }
> return prefSize;
> }
>
> public void updateUI() {
> super.updateUI();
> selectionForeground =
> UIManager.getColor ("Tree.selectionForeground"); //NOI18N
> selectionBackground =
> UIManager.getColor ("Tree.selectionBackground"); //NOI18N
> selectionBorder =
> UIManager.getColor ("Tree.selectionBorderColor"); //NOI18N
> if (selectionForeground == null) {
> selectionForeground = Color.BLACK;
> }
> if (selectionBackground == null) {
> selectionBackground = new Color (153,153,204);
> }
> if (selectionBorder == null) {
> selectionBorder = new Color (102, 102, 153);
> }
> }
>
> private void calcPrefSize() {
> if (prefSize == null) {
> prefSize = new java.awt.Dimension();
> }
> java.awt.Font f = getFont();
> java.awt.Graphics g = getGraphics();
> if ((f == null) || (g == null)) {
> //We're just initializing the component, supply some dummy
> //values and quit
> prefSize.width = 30;
> prefSize.height = 16;
> return;
> }
> java.awt.FontMetrics fm = g.getFontMetrics(f);
>
> if (icon == null) {
> prefSize.height = fm.getHeight();
> } else {
> prefSize.height = Math.max (fm.getHeight(), icon.getIconHeight());
> }
> int w;
> if ((txt == null) || (txt.length()==0)) {
> prefSize.width = icon != null ? icon.getIconWidth() : 0;
> } else {
> if (isHTML) {
> prefSize.width = Math.round(Math.round(
> Utilities.renderString(txt, g, 0, 0, Integer.MAX_VALUE,
> Integer.MAX_VALUE, f, Color.BLACK, Utilities.STYLE_CLIP,
> false))) + 1;
> } else {
> prefSize.width = Math.round(Math.round(
> Utilities.renderPlainString(txt, g, 0, 0, Integer.MAX_VALUE,
> Integer.MAX_VALUE, f, Color.BLACK, Utilities.STYLE_CLIP,
> false))) + 1;
> }
> }
> if (icon != null) {
> prefSize.width += icon.getIconWidth() + iconTextGap;
> }
> }
>
> public void setIconTextGap (int val) {
> iconTextGap = val;
> }
>
> public int getIconTextGap () {
> return iconTextGap;
> }
>
> public void setIcon(javax.swing.Icon i) {
> if (i != icon) {
> icon = i;
> ndCalcPrefSize=true;
> }
> }
>
> public String getText() {
> return txt;
> }
>
> public void setText(String s) {
> if (s != txt) {
> txt = s;
> ndCalcPrefSize = true;
> }
> }
>
> public void paint (java.awt.Graphics g) {
> java.awt.Point p = getLocation();
>
> int w = icon == null ? 0 : icon.getIconWidth();
> int h = icon == null ? 0 : icon.getIconHeight();
>
> int width = getWidth();
> int height = getHeight();
>
> g.setColor (selected ? selectionBackground : getBackground()); //XXX
> int rectStart = w + iconTextGap-1;
> if (selected) {
> g.fillRect (rectStart, 0, getPreferredSize().width - (rectStart+1), height);
> }
> if (hasFocus) {
> g.setColor (selectionBorder); //XXX
> g.drawRect(rectStart, 0, getPreferredSize().width-(rectStart+1), height-1);
> }
>
> if (icon != null) {
> int iconY = 0;
> if (height > h) {
> iconY = (height - h) / 2;
> }
> icon.paintIcon(this, g, 0, iconY);
> }
> java.awt.FontMetrics fm = g.getFontMetrics(getFont());
> int baseline = fm.getHeight() - fm.getDescent();
>
> int stringX = icon == null ? 0 : icon.getIconWidth()
> + iconTextGap;
> if (g.hitClip (stringX, 0, width, height)) {
> if (isHTML) {
> Utilities.renderHTML (txt, g,
> stringX,
> baseline,
> Integer.MAX_VALUE,
> Integer.MAX_VALUE, getFont(),
> selected ? selectionForeground : getForeground(),
> Utilities.STYLE_CLIP,
> true);
> } else {
> Utilities.renderString (txt, g,
> stringX,
> baseline,
> Integer.MAX_VALUE,
> Integer.MAX_VALUE, getFont(),
> selected ? selectionForeground : getForeground(),
> Utilities.STYLE_CLIP,
> true);
> }
> }
> }
> }
221c394
< final static class Tree extends DefaultTreeCellRenderer {
---
> static class Tree extends BaseRenderer implements TreeCellRenderer {
255c428,435
< setText(vis.getDisplayName ());
---
> String s = vis.getFormattedDisplayName();
> if (s == null) {
> s = vis.getDisplayName();
> setHTML(false);
> } else {
> setHTML(true);
> }
> setText(s);
262c442
< this.hasFocus = hasFocus;
---
> this.hasFocus = hasFocus;
265,269c445
< if(sel) {
< setForeground(getTextSelectionColor());
< } else {
< setForeground(getTextNonSelectionColor());
< }
---
> setForeground (tree.getForeground());
287c463
< static final class List extends JLabel implements ListCellRenderer {
---
> static final class List extends BaseRenderer implements ListCellRenderer {
313c489,496
< setText(vis.getDisplayName ());
---
> String s = vis.getFormattedDisplayName();
> if (s == null) {
> s = vis.getDisplayName();
> setHTML(false);
> } else {
> setHTML(true);
> }
> setText(s);
349c532
< final static class Pane extends JLabel implements ListCellRenderer {
---
> final static class Pane extends BaseRenderer implements ListCellRenderer {
359,361d541
< setVerticalTextPosition(JLabel.BOTTOM);
< setHorizontalAlignment(JLabel.CENTER);
< setHorizontalTextPosition(JLabel.CENTER);
380c560,568
< setText(vis.getDisplayName ());
---
>
> String s = vis.getFormattedDisplayName();
> if (s == null) {
> s = vis.getDisplayName();
> setHTML(false);
> } else {
> setHTML(true);
> }
> setText(s);
Index: openide/src/org/openide/explorer/view/TreeTable.java
===================================================================
RCS file: /cvs/openide/src/org/openide/explorer/view/TreeTable.java,v
retrieving revision 1.28
diff -r1.28 TreeTable.java
66c66
< NodeRenderer rend = NodeRenderer.sharedInstance ();
---
> NodeRenderer.Tree rend = new TTRenderer();//NodeRenderer.sharedInstance ();
109a110,122
> /** Renderer subclass which hacks the clip rectangle. This should be
> * set from the renderer's getPreferredSize method (this works correctly
> * for a standard JTree but doesn't work for the embedded tree) */
> private class TTRenderer extends NodeRenderer.Tree {
> public void paint (Graphics g) {
> //hack the clipping rectangle
> Rectangle r = g.getClipBounds();
> r.width = TreeTable.this.getWidth();
> g.setClip(r.x, r.y, r.width, r.height);
> super.paint (g);
> }
> }
>
384c397
<
---
>
826,827c839,840
< this.clearSelection ();
< if(min != -1 && max != -1) {
---
> this.clearSelection ();
> if(min != -1 && max != -1) {
833c846
< if(selPath != null) {
---
> if(selPath != null) {
Index: openide/src/org/openide/explorer/view/TreeViewCellEditor.java
===================================================================
RCS file: /cvs/openide/src/org/openide/explorer/view/TreeViewCellEditor.java,v
retrieving revision 1.36
diff -r1.36 TreeViewCellEditor.java
44c44
<
---
> protected NodeRenderer.Tree bren;
49,50c49,51
< public TreeViewCellEditor(JTree tree, DefaultTreeCellRenderer renderer) {
< super(tree, renderer);
---
> public TreeViewCellEditor(JTree tree, NodeRenderer.Tree bren) { //XXX , TreeCellRenderer renderer) {
> super(tree,new DefaultTreeCellRenderer());
> this.bren = bren;
201,209c202,210
< if(renderer != null) {
< renderer.getTreeCellRendererComponent(tree, value, sel, expanded,
< leaf, row, true);
< editingIcon = renderer.getIcon ();
< offset = renderer.getIconTextGap () + editingIcon.getIconWidth ();
< } else {
< editingIcon = null;
< offset = 0;
< }
---
> if(bren != null) {
> bren.getTreeCellRendererComponent(tree, value, sel, expanded,
> leaf, row, true);
> editingIcon = bren.getIcon();
> offset = bren.getIconTextGap () + editingIcon.getIconWidth ();
> } else {
> editingIcon = null;
> offset = 0;
> }
266a268
>
268c270
< static class Ed extends DefaultCellEditor {
---
> class Ed extends DefaultCellEditor {
282a285
>
288a292
>
Index: openide/src/org/openide/explorer/view/VisualizerNode.java
===================================================================
RCS file: /cvs/openide/src/org/openide/explorer/view/VisualizerNode.java,v
retrieving revision 1.36
diff -r1.36 VisualizerNode.java
96a97,98
> /** cached formated display name */
> private String formattedDisplayName;
145c147
< displayName = node == null ? null : node.getDisplayName ();
---
> displayName = node == null ? null : node.getDisplayName();
149a152,158
> public String getFormattedDisplayName () {
> if (formattedDisplayName == UNKNOWN) {
> displayName = node == null ? null : node.getFormattedDisplayName();
> }
> return formattedDisplayName;
> }
>
333a343
> formattedDisplayName = node.getFormattedDisplayName ();
Index: openide/src/org/openide/filesystems/FileSystem.java
===================================================================
RCS file: /cvs/openide/src/org/openide/filesystems/FileSystem.java,v
retrieving revision 1.74
diff -r1.74 FileSystem.java
688a689,703
> /** An extension to the Status interface to allow filesystems to provide
> * HTML markup in a status string for display components.
> * @since 1.74
> */
> public static interface HTMLStatus extends Status {
> /** Provide status annotation including HTML markup, using
> * the limited subset of HTML markup supported by
> * Utilities.renderString()
. The returned markup should
> * contain opening and closing <HTML> tags
> * @since 1.74
> * @see org.openide.util.Utilities.renderHTML
> */
> public String annotateNameHTML (String name, java.util.Set files);
> }
>
Index: openide/src/org/openide/nodes/FilterNode.java
===================================================================
RCS file: /cvs/openide/src/org/openide/nodes/FilterNode.java,v
retrieving revision 1.79
diff -r1.79 FilterNode.java
399a400,415
>
> /** Get the formatted display name for the node. FilterNode
> * subclasses which do not delegate the display name must
> * override this method to return a formatted display name.
> *
> * @see org.openide.nodes.Node.getFormattedDisplayName
> * @return the formatted display name of the original node if
> * delegating the display name to the original node, or null
> */
> public String getFormattedDisplayName() {
> if (delegating (DELEGATE_GET_DISPLAY_NAME)) {
> return original.getFormattedDisplayName();
> } else {
> return null;
> }
> }
Index: openide/src/org/openide/nodes/Node.java
===================================================================
RCS file: /cvs/openide/src/org/openide/nodes/Node.java,v
retrieving revision 1.73
diff -r1.73 Node.java
96a97,102
> /** Property for a node's formatted display name. Clients interested in
> * this property should also assume it has changed if they receive an event
> * of PROP_DISPLAY_NAME
. */
> public static final String PROP_FORMATTED_DISPLAY_NAME =
> "formattedDisplayName"; //NOI18N
>
308a315,340
> }
>
> /** Get a display name containing inline markup, using the limited
> * subset of HTML supported by Utilities.renderString()
.
> * Explorer views will render nodes which return non-null from
> * this method using its return value rather than the result of
> * getDisplayName()
. Other uses of a Node's display
> * name (such as logging code) will use getDisplayName()
.
> *
> * Nodes that do not support HTML-ized display names should return > * null. Note that, unlike with Swing components, the String returned > * by this method need not contain opening HTML tags.
> * The default implementation returns null. > *
> * Nodes may fire formatting-only changes by firing
> * PROP_FORMATTED_DISPLAY_NAME
.
> *
> * Implementations whose display name may contain > or < characters
> * should take care to escape these characters or return null from this
> * method.
> * @return a string containing HTML compliant with the limited subset
> * of HTML supported by the lightweight renderer.
> * @see org.openide.util.Utilities.renderHTML
> * @since 1.73 */
> public String getFormattedDisplayName() {
> return null;
Index: openide/src/org/openide/util/Utilities.java
===================================================================
RCS file: /cvs/openide/src/org/openide/util/Utilities.java,v
retrieving revision 1.133
diff -r1.133 Utilities.java
20a21,22
> import java.awt.font.LineMetrics;
> import java.awt.geom.Rectangle2D;
34a37
> import java.util.Stack;
38a42
> import javax.swing.UIManager;
2510a2515,3382
> }
>
>
> /** Constant used by renderString
, renderPlainString
> * and renderHTML
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
> * and renderHTML
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
> * and renderHTML
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;
> /**Render a string to a graphics canvas, 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 defaultColor, int style, boolean paint) {
> g.setColor (defaultColor);
> 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 {
> g.drawString("...", x,y);
> }
> }
> } 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(" return renderHTML (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 (s, g, x, y, w, h, f, defaultColor, style,
> paint);
> }
>
> /** Stack object used during HTML rendering to hold previous colors in
> * the case of nested color entries. */
> private static Stack colorStack = null; //XXX check synchronization overhead, maybe find an unsynchronized stack impl?
>
> /** Implementation of HTML rendering */
> private static double _renderHTML (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 pos = 0; //skip the opening tag
> 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
>
> /* 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.)
> 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)
> */
>
> if (colorStack == null) {
> //create the stack used for storing colors in nested color tags
> colorStack = new Stack();
> } else {
> //clear it in case some bad html left junk behind
> 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;
> 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 (Character.isWhitespace (chars[pos])) {
> pos++;
> }
> }
>
> //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 width of the string, for estimating
> //when we'll be out of space and should start the ... in truncate
> //mode
> double chWidth = r.getWidth() / (nextTag - pos);
> //can return this sometimes, so handle it
> if (chWidth == Double.POSITIVE_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 = new Double((w - widthPainted)
> / chWidth).intValue();
> 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);
> 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;
> }
> }
>
> private static final boolean strictHTML = Boolean.getBoolean (
> "netbeans.lwhtml.strict"); //NOI18N
> private static Set badStrings=null;
> /** 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 " //NOI18N
> + new String(chh) + "\n Full HTML string:" + new String(chars);
> if (!strictHTML) {
> if (badStrings == null) {
> badStrings = new HashSet();
> }
> if (!badStrings.contains(msg)) {
> ErrorManager.getDefault().log(ErrorManager.WARNING, msg);
> System.err.println(msg); //Also print to stdout - ErrorManager warning will be cut off after first /n
> badStrings.add(msg);
> }
> } else {
> throw new IllegalArgumentException (out);
> }
> }
>
> /** 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);
> }
> }
> }
> return -1;