Index: openide/openide-spec-vers.properties =================================================================== RCS file: /cvs/openide/openide-spec-vers.properties,v --- openide/openide-spec-vers.properties 1.118 +++ openide/openide-spec-vers.properties @@ -7,1 +7,1 @@ -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 --- openide/api/doc/changes/apichanges.xml 1.165 +++ openide/api/doc/changes/apichanges.xml @@ -116,1 +116,27 @@ - --- + + + 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 --- openide/loaders/src/org/openide/loaders/DataNode.java 1.4 +++ openide/loaders/src/org/openide/loaders/DataNode.java @@ -155,0 +155,29 @@ + + /** 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 --- openide/src/org/openide/explorer/view/NodeRenderer.java 1.23 +++ openide/src/org/openide/explorer/view/NodeRenderer.java @@ -218,1 +218,174 @@ - --- + + 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); + } + } + } + } @@ -221,1 +394,1 @@ - final static class Tree extends DefaultTreeCellRenderer { --- + static class Tree extends BaseRenderer implements TreeCellRenderer { @@ -255,1 +428,8 @@ - setText(vis.getDisplayName ()); --- + String s = vis.getFormattedDisplayName(); + if (s == null) { + s = vis.getDisplayName(); + setHTML(false); + } else { + setHTML(true); + } + setText(s); @@ -262,1 +442,1 @@ - this.hasFocus = hasFocus; --- + this.hasFocus = hasFocus; @@ -265,5 +445,1 @@ - if(sel) { - setForeground(getTextSelectionColor()); - } else { - setForeground(getTextNonSelectionColor()); - } --- + setForeground (tree.getForeground()); @@ -287,1 +463,1 @@ - static final class List extends JLabel implements ListCellRenderer { --- + static final class List extends BaseRenderer implements ListCellRenderer { @@ -313,1 +489,8 @@ - setText(vis.getDisplayName ()); --- + String s = vis.getFormattedDisplayName(); + if (s == null) { + s = vis.getDisplayName(); + setHTML(false); + } else { + setHTML(true); + } + setText(s); @@ -349,1 +532,1 @@ - final static class Pane extends JLabel implements ListCellRenderer { --- + final static class Pane extends BaseRenderer implements ListCellRenderer { @@ -359,3 +542,0 @@ - setVerticalTextPosition(JLabel.BOTTOM); - setHorizontalAlignment(JLabel.CENTER); - setHorizontalTextPosition(JLabel.CENTER); @@ -380,1 +560,9 @@ - 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 --- openide/src/org/openide/explorer/view/TreeTable.java 1.28 +++ openide/src/org/openide/explorer/view/TreeTable.java @@ -66,1 +66,1 @@ - NodeRenderer rend = NodeRenderer.sharedInstance (); --- + NodeRenderer.Tree rend = new TTRenderer();//NodeRenderer.sharedInstance (); @@ -110,0 +110,13 @@ + /** 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); + } + } + @@ -384,1 +397,1 @@ - --- + @@ -826,2 +839,2 @@ - this.clearSelection (); - if(min != -1 && max != -1) { --- + this.clearSelection (); + if(min != -1 && max != -1) { @@ -833,1 +846,1 @@ - 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 --- openide/src/org/openide/explorer/view/TreeViewCellEditor.java 1.36 +++ openide/src/org/openide/explorer/view/TreeViewCellEditor.java @@ -44,1 +44,1 @@ - --- + protected NodeRenderer.Tree bren; @@ -49,2 +49,3 @@ - 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,9 +202,9 @@ - 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; + } @@ -267,0 +268,1 @@ + @@ -268,1 +270,1 @@ - static class Ed extends DefaultCellEditor { --- + class Ed extends DefaultCellEditor { @@ -283,0 +285,1 @@ + @@ -289,0 +292,1 @@ + Index: openide/src/org/openide/explorer/view/VisualizerNode.java =================================================================== RCS file: /cvs/openide/src/org/openide/explorer/view/VisualizerNode.java,v --- openide/src/org/openide/explorer/view/VisualizerNode.java 1.36 +++ openide/src/org/openide/explorer/view/VisualizerNode.java @@ -97,0 +97,2 @@ + /** cached formated display name */ + private String formattedDisplayName; @@ -145,1 +147,1 @@ - displayName = node == null ? null : node.getDisplayName (); --- + displayName = node == null ? null : node.getDisplayName(); @@ -150,0 +152,7 @@ + public String getFormattedDisplayName () { + if (formattedDisplayName == UNKNOWN) { + displayName = node == null ? null : node.getFormattedDisplayName(); + } + return formattedDisplayName; + } + @@ -334,0 +343,1 @@ + formattedDisplayName = node.getFormattedDisplayName (); Index: openide/src/org/openide/filesystems/FileSystem.java =================================================================== RCS file: /cvs/openide/src/org/openide/filesystems/FileSystem.java,v --- openide/src/org/openide/filesystems/FileSystem.java 1.74 +++ openide/src/org/openide/filesystems/FileSystem.java @@ -689,0 +689,15 @@ + /** 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 --- openide/src/org/openide/nodes/FilterNode.java 1.79 +++ openide/src/org/openide/nodes/FilterNode.java @@ -400,0 +400,16 @@ + + /** 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 --- openide/src/org/openide/nodes/Node.java 1.73 +++ openide/src/org/openide/nodes/Node.java @@ -97,0 +97,6 @@ + /** 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 + @@ -309,0 +315,26 @@ + } + + /** 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 --- openide/src/org/openide/util/Utilities.java 1.133 +++ openide/src/org/openide/util/Utilities.java @@ -21,0 +21,2 @@ +import java.awt.font.LineMetrics; +import java.awt.geom.Rectangle2D; @@ -35,0 +37,1 @@ +import java.util.Stack; @@ -39,0 +42,1 @@ +import javax.swing.UIManager; @@ -2511,0 +2515,868 @@ + } + + + /** 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(" + * + * <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;