? 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 < --- > > > Lightweight HTML rendering methods > > > > > > A lightweight HTML renderer which can render a limited subset of > HTML has been added to the APIs, and will be used in Explorer. > Nodes wishing to provide text rendered in HTML may do so by > returning subset-compliant, HTML formatted text from the new > method 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. > > > > > > > > Index: openide/loaders/src/org/openide/loaders/DataNode.java =================================================================== RCS file: /cvs/openide/loaders/src/org/openide/loaders/DataNode.java,v retrieving revision 1.4 diff -r1.4 DataNode.java 154a155,183 > > /** Get a display name formatted using the limited HTML subset supported > * by 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. > *
> * The lightweight html renderer supports the following named sgml character > * entities: 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: > *

    > *
  1. 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.
  2. > *
  3. 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.
  4. > *
> *

> * 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. &#8222;) > * 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;