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 @@
-
---
+ 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
--- 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("
+ *
<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;